VRChatでMidiを使ってVRVJをした話(忘備録)
需要がどこにあるかわからないけど書きたくなったのでこの前のド(VRVJ)について多少解説しようと思います(すでに1か月経っている)。 今回はShaderの話というよりも入力システム、コントローラー周りの話です。結局は入力されたTextureデータをどう処理するかの話だからとりあえずいいかなと思ってしてません。 入力の形態はVRChat内Shader製スライダーから今回のようなMidi入力に変わったけど結局出力側をどう作るかはあまり変わってないのもあります。
雰囲気としては
こんな感じです。#GHOSTCLUB
— 𝟶𝚋𝟺𝚔𝟹 (@3k4b0) 2021年1月15日
2021/01/15
OPEN 22:30(JST)
23:00 0b4k3 x fotfla ド pic.twitter.com/bNvc0QmWZe
Midi設定
すべてのエフェクトの操作はMidi信号をSDK2のVRC_MidiNoteInを通して行っています。 すべてのMidiを一つのGameObjectでやるとインスペクターは
こうなります…。展開するとだいぶ重いです。 一つ一つ設定するのはとてもしんどいのでスクリプトですべて書いてます。これを見てどう思うかで… pic.twitter.com/eybTnx9kHS
— fotfla (@fotfla) 2021年1月5日
あまり見せるようにはできてないのですが
void SetMidi() { var triggers = transform.GetComponent<VRC_Trigger>().Triggers; var notes = gameObject.GetComponents<VRC_MidiNoteIn>(); for (var i = 0; i < count; i++) { var noteNum = i + VRC_MidiNoteIn.Note.A0; if (Obj != null) { var instance = Instantiate(Obj); instance.name = System.Enum.GetName(typeof(VRC_MidiNoteIn.Note), noteNum); instance.transform.parent = transform; AddTirgger(triggers, instance, notes[i], noteNum); } } } void AddTirgger(List<VRC_Trigger.TriggerEvent> triggerEvent, GameObject obj, VRC_MidiNoteIn note, VRC_MidiNoteIn.Note noteNumber) { // NoteON var triggerOn = new VRC_Trigger.TriggerEvent(); triggerOn.Name = "NoteOn" + noteNumber; triggerOn.BroadcastType = VRC.SDKBase.VRC_EventHandler.VrcBroadcastType.AlwaysUnbuffered; triggerOn.TriggerType = VRC.SDKBase.VRC_Trigger.TriggerType.Custom; var vrcEventOn = new VRC.SDKBase.VRC_EventHandler.VrcEvent(); vrcEventOn.EventType = VRC_EventHandler.VrcEventType.SetGameObjectActive; vrcEventOn.ParameterObject = obj; vrcEventOn.ParameterBoolOp = VRC.SDKBase.VRC_EventHandler.VrcBooleanOp.True; triggerOn.Events.Add(vrcEventOn); triggerEvent.Add(triggerOn); // NoteOff var triggerOff = new VRC_Trigger.TriggerEvent(); triggerOff.Name = "NoteOff" + noteNumber; triggerOff.BroadcastType = VRC.SDKBase.VRC_EventHandler.VrcBroadcastType.AlwaysUnbuffered; triggerOff.TriggerType = VRC.SDKBase.VRC_Trigger.TriggerType.Custom; var vrcEventOff = new VRC.SDKBase.VRC_EventHandler.VrcEvent(); vrcEventOff.EventType = VRC_EventHandler.VrcEventType.SetGameObjectActive; vrcEventOff.ParameterObject = obj; vrcEventOff.ParameterBoolOp = VRC.SDKBase.VRC_EventHandler.VrcBooleanOp.False; triggerOff.Events.Add(vrcEventOff); triggerEvent.Add(triggerOff); AddMidiNoteIn(note, noteNumber, triggerOn, triggerOff); } void AddMidiNoteIn(VRC_MidiNoteIn note, VRC_MidiNoteIn.Note noteNumber, VRC_Trigger.TriggerEvent onTriggerEvent, VRC_Trigger.TriggerEvent offTriggerEvent) { note.note = noteNumber; note.OnNoteOn.TriggerObject = gameObject; note.OnNoteOn.CustomName = onTriggerEvent.Name; note.OnNoteOff.TriggerObject = gameObject; note.OnNoteOff.CustomName = offTriggerEvent.Name; }
とMidiの設定とMidiのオンオフで切り替えるGameObjectの設定を一括で生成しています。生成したGameObjectは
MidiObject以下に生成されるようにしています。また別のスクリプトで 次のように16x16に収まるように配置してカメラで撮ってRenderTextureに描画してこれをCustomRenderTexutureに渡しています。
演出処理の話
Midi情報の処理、Particleの情報の更新などはすべて一枚のCustomRenderTexutureで行っていて、 先程ののMidiのオンオフ情報もここで処理をして、減衰、乱数の生成、入力回数などを処理してます。
// Midi if(index.x >= 64 && index.y >= 64){ if(index.x < 80 && index.y < 80){ float input = tex2D(_Input, (index- 64) * _Input_TexelSize).r; float3 current = buffer.yzw; if(input > 0.5){ current.x = input; // 乱数生成 if(current.z > 0.5){ uint2 seed = index + floor(current.y * 65537); current.y = hash12(seed); current.z = 0; } } else { // Decay current.x -= _DecayTime; current.x = max(current.x,0); current.z = 1; } output = float4(input,current); } else if(index.x < 96 && index.y < 80){ float input = tex2D(_Input, (index- float2(80,64)) * _Input_TexelSize).r; float4 current = buffer; if(input > 0.5){ current.y = input; current.x = 0; // 入力回数16回を1ループとして計算 if(current.a > 0.5){ current.z += 1/16.0; current.z = frac(current.z); current.a = 0; } } else { // Decay current.y -= _DecayTime; current.y = max(current.y,0); current.a = 1; current.x += 0.01; if(current.x > 1){ current.x = 0; current.z = 0; } } output = current; } }
またParticle系はPosition、Velocity、Lifetimeなどを計算しています。 この計算されたCustomRenderTexutureを使って各演出用のShaderに渡してそれをもとに演出のオンオフ操作などをしています。
こういったCustomRenderTexutureを使った計算の話はこういった記事のほうがより詳細に記載されてるのでぜひとも御覧ください。
コントロール
今回のMidiのコントローラーはOrcaを使用しています。 もともとはAbletonでクリップを予め作っておいてLaunchPad(Midiコントローラー)で制御しようと思っていましたが、 複数種類のエフェクトと異なるリズムを用意することと、LaunchPadがVRChatに認識されて押したつもりのないノートまでが反応する(今はアップデートで認識させたいMidiデバイスのみになったのでこの心配はなくなったと思う。)のであまり増やしたくなかったので自由にシーケンスを組めるOrcaを使いました。 こんな感じにOrcaからMidiを飛ばして各種エフェクトを制御してます。 youtu.be
これでおわりです。実際今はUdonがあるので組もうと思えばVRChat内で完結できるようになるし、UdonにMidiが来ればそれでまた扱い方が変わるのでそんなに必要ないと思って軽めです。ありがとうございました。