じぇしかめも

VRChatとインフラのお話がメインです。主に備忘録として、自分の理解を書いています。かならずしも事実とは限らないので、ご了承くださいな。

OSCメッセージでVRChatのアクションを実行する

はじめに

最近、VRChatに週1でペースちょこちょこログインして遊んでいる一方、現実とVRChatを結び付ける面白いものが作れないかと模索しています。 それなりに遊べるものをいくつか作ることができたのですが、いずれもOSCメッセージが情報伝搬手段のコア技術となっています。 ただ、個人的にはとっても有用な仕組みだと思っているものの、あまり情報が出回っていないせいか使用例はあまり見かけない印象です。 そこで、OSCメッセージによるトリガーとアクションを誰でも動かせるように、ハンズオン形式の説明をつらつら書いてみました。

今回はチュートリアルとして、ワールド上のCubeオブジェクトをアクティベート/ディアクティベートしてみます。 なおOSCメッセージの送信側はPythonスクリプト、 受信側はUnityで実装します。 誰でもと言いつつ申し訳ないのですが、最低限、下記のスキルがあることを前提とします。

  1. Unity(VRChat SDK)で自作ワールドをビルドして、ログインできる
  2. VRChatを起動するPCにPythonがインストールされており、PythonとpipコマンドをPowerShellから実行できる

2については、PowerShellを開いて下記のコマンドが実行できれば大丈夫です。

PS> python --version
PS> pip --version

OSCとは

そもそも"OSC"なんて知らない方が普通なので、ざっと調べてみます。

OpenSound Control(OSC)とは、電子楽器(特にシンセサイザー)やコンピュータなどの機器において音楽演奏データをネットワーク経由でリアルタイムに共有するための通信プロトコルである。
https://ja.wikipedia.org/wiki/OpenSound_Control

どうやら、OSCはMIDIと同じく音楽の演奏データ通信に使用することを目的として作られたプロトコルのようです。 ただメッセージの構造がシンプルで汎用性があるので、音楽用途以外でも問題なく使えます。 じゃあ具体的にどんな構造かというと、OSCメッセージは「アドレス」と「引数」から成り立っています(呼称にはバラつきがあるようです)。 アドレスは階層構造を持たせることができるので、受信側でパースしやすいように設計できます。 また引数については、一つのアドレスに対して複数定義してやることも可能なようです。 例えば、RGBで赤を表現するようなメッセージであれば"/color 255 0 0"や"/color/red 255"というように記述できます。
f:id:jscmla1118:20181110150441p:plain

VRChatにおけるOSCメッセージの取り扱い

VRChatには、様々なトリガーとアクションを組み合わせることで自作ワールドを構築できる仕組みが備わっています。 そのトリガーの1つとして、VRChat SDKには"VRC_OscButtonIn"が含まれており、特定のOSCメッセージを送信すれば発火させられます。 ただし実行できるのはスイッチのOn/Offのみで、"0"を受け取ればOff、"それ以外の数値"を受け取ればOnとして動作します。 とはいえ、VRChatの「外部」からトリガーを発火させられる貴重な仕組みなので、これが使用できると表現の幅が一気に広がります。 次節以降では、OSCメッセージを使用してオブジェクトのアクティベート/ディアクティベートを実装する方法を説明します。

OSCメッセージ送信側の実装

pythonoscのダウンロード

まず、これから作成するPythonスクリプトにてOSCメッセージを簡単に使用できるようにするため、pythonoscパッケージをダウンロードします。 PowerShellを起動し、pipコマンドでパッケージを取ってきます。 ハイフンが必要なので、忘れずに。

PS> pip install python-osc

スクリプトの実装

pythonoscをダウンロード出来たら、スクリプトを書いていきます。 メモ帳を開いて、お好きな場所に保存してください。 今回はファイル名を"osc_message_client.py"として保存し、使用します。

# -*- coding: utf-8 -*-
import sys
from pythonosc import udp_client
from pythonosc.osc_message_builder import OscMessageBuilder


def main():
    # 受信先のVRChat
    IP = '127.0.0.1'
    PORT = 9000

    # OSCメッセージ情報の読み込み
    address = sys.argv[1]
    arg = sys.argv[2]

    print('Address: '+address)
    print('Arg: '+arg)

    # OSCメッセージの構築
    client = udp_client.UDPClient(IP, PORT)
    msg = OscMessageBuilder(address=address)
    msg.add_arg(arg)
    m = msg.build()

    # OSCメッセージの送信
    client.send(m)


if __name__ == '__main__':
    main()

実行している処理の概要は、コメントに記載の通りです。 テスト用のスクリプトなので、エラーハンドリングなし、詳細な解説も割愛します。

スクリプトが実装できたら、PowerShellから試しに動かしてみます。 アドレスは"/cube/activate"、引数は"1"と"0"をそれぞれ指定してみます。

PS> cd [ファイルの保存先フォルダ]
PS> python .\osc_message_client.py /cube/activate 1
Address: /cube/activate
Arg: 1
PS> python .\osc_message_client.py /cube/activate 0
Address: /cube/activate
Arg: 0
PS>

アドレスと引数がスクリプトに渡り、特に問題なく動いているようです。 この時点で、OSCメッセージは送信されています。 しかし受信側をまだ構築していないので、見せかけ上は何も起こりません。 このメッセージは、また後ほど使用します。

OSCメッセージ受信側の実装

テスト用ワールドの構築

次は受信側の実装です。 こちらはVRChatの自作ワールドになるので、Unityを使用します。 まずは、プロジェクトを作成します。 お好みで、適当に名前を付けてください。 f:id:jscmla1118:20181110161219p:plain

オブジェクトの配置

プロジェクトを作成したら、シーンの保存とVRChat SDKをインポートします。 そしたら、ヒエラルキーにぱぱっと下記のものを追加しましょう。

  • 床用のPlane
  • トリガーハンドリング用の空オブジェクト
  • ターゲットになるCubeオブジェクト
  • リスポーン用のPrefab(VRCWorld)

階層構造は特に気にする必要はないですが、今回はCubeオブジェクトを空オブジェクトの子要素にします。 またマテリアルをCubeに適用していますが、特に意味はないので、お好みでどうぞ。 配置が終わったら、Cubeオブジェクトはディアクティブにしておきます。 f:id:jscmla1118:20181110164056p:plain

アクションの設定

OSCメッセージ受信時のアクションを設定していきます。 ちなみにトリガーとアクションは、ターゲットとなるCubeオブジェクトではなく、親要素の空オブジェクトに対して設定します。 というのも、Cubeオブジェクトにトリガーを設定してしまうとディアクティベートした時点でトリガーも無効になってしまい、 OSCメッセージを受信できなくなってしまうためです。

それでは、Hierarchyタブにて空オブジェクトを選択しましょう。 そしたら、InspectorタブのAdd Componentボタンをクリックして、VRC_Triggerを追加します。

f:id:jscmla1118:20181110165501p:plain

次に、アクティベートとディアクティベート用に、アクションを2つ追加してください。 その際に選択するトリガーは、"custom"でよいです。 下記のようにもろもろ設定して、図と同じようになれば大丈夫です。

アクティベーション設定
①Advance Modeにチェックを入れる
②"Always"を選択
③"OscButtonOn"を入力
④子要素のCubeオブジェクトを選択
⑤"True"を選択

ディアクティベート設定
⑥"OscButtonOff"を入力
⑦"False"を選択

f:id:jscmla1118:20181110170152p:plain

トリガーの設定

受信部の要である、トリガーの設定です。 アクションと同じく"Add Component"ボタンをクリックして、 "VRC_Osc Button In"を追加します。

f:id:jscmla1118:20181110171639p:plain

ここで設定する項目は、3つあります。

Address
これは、先ほど説明したOSCメッセージのアドレスです。先ほどのPythonスクリプトで渡した"/cube/activate"を入力します。

On Button On/Off
ここには、それぞれスイッチのOn/Off時のアクションを設定します。 左側の欄ではVRC_Triggerを追加したオブジェクト、右側の欄では実行するアクションを選択します。 今回のケースでは、オブジェクトはともに空オブジェクト、アクションは先に設定した"OscButtonOn"と"OscButtonOff"を選択します。

f:id:jscmla1118:20181110171623p:plain

これで、設定は完了です。

OSCメッセージの送受信テスト

さて、以上でUnity側の設定も完了したので、ワールドの動作確認を行いましょう。 通常のワールドテストを同じようにテストビルドを実行し、ログインしてください。 ちなみにウィンドウの操作を行うので、デスクトップモードが好ましいです。

ログイン時点では、Cubeオブジェクトはディアクティベートされているので見えません。 ここで、先ほど作ったスクリプトを再度叩いてみましょう。

PS> python .\osc_message_client.py /cube/activate 1
Address: /cube/activate
Arg: 1
PS> python .\osc_message_client.py /cube/activate 0
Address: /cube/activate
Arg: 0
PS>

"1"を送信するとCubeが出現し、"0"を送信するとCubeが消えるようになります。 これが確認できれば、無事にOSCメッセージがVRChatまで届いています。やったね。 f:id:jscmla1118:20181110174313g:plain

おわりに

ここまで、実際にサンプルを作成しながら基本的な動作と実装を紹介しました。 今回の例ではあまり魅力が伝わらなかったかもしれませんが、OSCメッセージを使いこなすことで、ArduinoRaspberry Piに繋いだセンサーと組み合わせたり、外部からの通信に基づいてメッセージを送信するなど、現実世界のイベントもトリガーとして使用できるようになります。一例ですが、下記のようにスマートフォンからトリガーを引くことも可能です。

一方で、VRChat側でもアドレスさえ設定してやれば多数のOSCメッセージを瞬時に受信できるので、複雑なアクションもすぐさま実行できます。扱い次第では、下記のように連続的で細かな操作もできるようになるので、ぜひいろいろ試してみてください。

参考

qiita.com pypi.org docs.vrchat.com