Observe Photon

なにかについて

VRChatでMidiを使ってVRVJをした話(忘備録)

需要がどこにあるかわからないけど書きたくなったのでこの前のド(VRVJ)について多少解説しようと思います(すでに1か月経っている)。 今回はShaderの話というよりも入力システム、コントローラー周りの話です。結局は入力されたTextureデータをどう処理するかの話だからとりあえずいいかなと思ってしてません。 入力の形態はVRChat内Shader製スライダーから今回のようなMidi入力に変わったけど結局出力側をどう作るかはあまり変わってないのもあります。

雰囲気としては

こんな感じです。

Midi設定

すべてのエフェクトの操作はMidi信号をSDK2のVRC_MidiNoteInを通して行っています。 すべてのMidiを一つのGameObjectでやるとインスペクターは

こうなります…。展開するとだいぶ重いです。 一つ一つ設定するのはとてもしんどいのでスクリプトですべて書いてます。

あまり見せるようにはできてないのですが

    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は

f:id:fotfla:20210119011334p:plain

MidiObject以下に生成されるようにしています。また別のスクリプトで 次のように16x16に収まるように配置してカメラで撮ってRenderTextureに描画してこれをCustomRenderTexutureに渡しています。

f:id:fotfla:20210119011558p:plain

演出処理の話

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を使った計算の話はこういった記事のほうがより詳細に記載されてるのでぜひとも御覧ください。

qiita.com

phi16.hatenablog.com

コントロール

今回のMidiのコントローラーはOrcaを使用しています。 もともとはAbletonでクリップを予め作っておいてLaunchPad(Midiコントローラー)で制御しようと思っていましたが、 複数種類のエフェクトと異なるリズムを用意することと、LaunchPadがVRChatに認識されて押したつもりのないノートまでが反応する(今はアップデートで認識させたいMidiバイスのみになったのでこの心配はなくなったと思う。)のであまり増やしたくなかったので自由にシーケンスを組めるOrcaを使いました。 こんな感じにOrcaからMidiを飛ばして各種エフェクトを制御してます。 youtu.be

これでおわりです。実際今はUdonがあるので組もうと思えばVRChat内で完結できるようになるし、UdonMidiが来ればそれでまた扱い方が変わるのでそんなに必要ないと思って軽めです。ありがとうございました。