VRChatのアバターOSCを使って、Expressions Menuからワールドギミックを制御する忘備録
VRChatのオープンベータにアバターOSCが実装されたので、アバターOSC経由で、Expressions Menuの値を取り出して、再度MIDIに変換して、VRChatクライアントに投げ返すゲートウェイを作ってみました。
まず、アバターから入出力できるデータ(パラメータ)ですが、基本的には、VRChatアバターの基本情報と、Expressions Menuで設定したパラメータにアクセスできます。基本の情報はOutputのみで、自分で定義したパラメータは読み書きできるようです。
この時点で、外部アプリから、小物の出し入れや、服の着換え、アイトラッキングアプリの自作などを想定していることは、読み解けますね。
本題ですが、OSCで自作パラメータの読み書きができるので、Expressions Menuで操作した自作パラメータをoutputして、MIDIに変換して、VRChatに返すコードを書いてみました。
MIDIをループバックするのにloopMIDIというソフトを利用しています。
まずは、アバターに仕込む、Expressions MenuとExpressionParameters ですが、こんな感じです。
Boolを2つ、Floatを2つ、外界に出してみようと思います。(仕組みが分かったら、自由に変更してください。)
以上です。
アバターギミックと違って、パラメータを出力するだけなので、AnimatorやAnimationは触りません。
Pythonのコードは下記の通りです。(mainすらない殴り書きですが…、後ほど整形しますm(__)m)
OSCを受け取ったら、MIDIを送る。それだけです。
VRChatは、MIDIを受け取ると、Udonでイベントが発生し、Note Number(音の高さ)と、Velocity(音の強さ)を取得することが出来ます。
Note Numberと、Velocityは、それぞれ、0-127の整数型で表されます。
個人的には、Note Numberをアドレス、Velocityを値として、扱うと便利かなと思っています。
# -*- coding: utf-8 -*-
from pythonosc import osc_server
from pythonosc.dispatcher import Dispatcher
import pygame.midi as m
IP = '127.0.0.1'
PORT = 9001
def color_handlerA(unused_addr, value):
""" 値を受信したときに行う処理 (ItemAはboolを想定) """
print(f'ItemA received : ({value})')
if value == True:
midiout.note_on(70,100)
else :
midiout.note_off(70)
def color_handlerB(unused_addr, value):
""" 値を受信したときに行う処理 (ItemBはboolを想定) """
print(f'ItemB received : ({value})')
if value == True:
midiout.note_on(71,100)
else :
midiout.note_off(71)
def color_handlerC(unused_addr, value):
""" 値を受信したときに行う処理 (ItemCはfloatを想定) """
print(f'ItemC received : ({value})')
midiout.note_on(60, int(value * 127) )
def color_handlerD(unused_addr, value):
""" 値を受信したときに行う処理 (ItemDはfloatを想定) """
print(f'ItemD received : ({value})')
midiout.note_on(61, int(value * 127) )
m.init()
i_num = m.get_count()
for i in range(i_num):
print(i)
print(m.get_device_info(i))
# 直前のMIDIポート一覧から仮想デバイスのポート(自分の環境では「IAC Driver My Port」)のIDを確認して、その数値にしてください
midiout = m.Output(3)
# URLにコールバック関数を割り当てる
dispatcher = Dispatcher()
dispatcher.map('/avatar/parameters/ItemA', color_handlerA)
dispatcher.map('/avatar/parameters/ItemB', color_handlerB)
dispatcher.map('/avatar/parameters/ItemC', color_handlerC)
dispatcher.map('/avatar/parameters/ItemD', color_handlerD)
# サーバを起動する
server = osc_server.ThreadingOSCUDPServer((IP, PORT), dispatcher)
print(f'Serving on {server.server_address}')
server.serve_forever()
midiout.close()
m.quit()
exit()
これで、VRChatのワールドにMIDIが送信できます。
あとは、VRChatワールド側のUdonですね。
UdonSharpのインストールは終わっているものとします。
空っぽのオブジェクトに、「VRC Midi Listener」と「Udon Behavior」を付けます。「VRC Midi Listener」は、NoteOnとNoteOffを有効化し、Behaviorには、自分自身のオブジェクトをセットします。
UdonSharpのコードは下記の通り。
とりあずローカルだけ対応しています。そこから先は適宜改造して下さい。
using UdonSharp;
using UnityEngine;
using VRC.SDKBase;
using VRC.Udon;
public class MIDI_Reciever1 : UdonSharpBehaviour
{
[UdonSynced(UdonSyncMode.None)]
private int[] midi_table = new int[128];
[SerializeField] private GameObject sw_Object0;
[SerializeField] private GameObject sw_Object1;
[SerializeField] private GameObject Rotate_ObjectX0;
[SerializeField] private GameObject Rotate_ObjectX1;
void Start()
{
}
public override void MidiNoteOn(int channel, int number, int velocity)
{
// 値の保存
midi_table[number] = velocity;
// Objectの回転
if (number == 60)
Rotate_ObjectX0.transform.rotation = Quaternion.Euler((180.0f * midi_table[60]) / 127.0f, 0.0f, 0.0f);
if (number == 61)
Rotate_ObjectX1.transform.rotation = Quaternion.Euler((180.0f * midi_table[61]) / 127.0f, 0.0f, 0.0f);
// Objectのオン
if (number == 70) sw_Object0.SetActive(true);
if (number == 71) sw_Object1.SetActive(true);
}
public override void MidiNoteOff(int channel, int number, int velocity)
{
// Objectのオフ
if (number == 70) sw_Object0.SetActive(false);
if (number == 71) sw_Object1.SetActive(false);
}
}
sw_Object0、sw_Object1、Rotate_ObjectX0、Rotate_ObjectX1に適当なワールドオブジェクトを設定していただくと、オン・オフとX軸周りに回転してるのが確認出来るかと思います。
あと、このままだと誰でもMIDIを送れてしまうので、実際にワールドに組み込む際は、リスナーのオブジェクトを親だけが有効化出来るようにする等の対策が必要になるかなと思います。
単純に変数を送れる仕組みとして考えると、かなりいろいろな応用が見込めますので、是非、試してみてください!
コメントを残す