凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

Unity 向けの OSC 実装を作ってみた

はじめに

世の中に Unity 向けの OSC 実装は既にあるのですが、自分の理解を深めるため(+ お盆で暇だった期間 + αで)OSC の実装をしてみました。

OSC とは OpenSound Control のことで、型やデータ長などの情報を付加したデータの詰め方が定義されていて、主に UDP に乗っかって通信に使われることが多いです。

既に Unity 向けの実装は幾つかあります。

github.com

github.com

github.com

なので、私の方では UDP の実装も含む形でユーザからは最小限の設定で使えるよう意識して作ってみました。

追記(2021/11/29)

新しいバージョンについての記事を書きました。

tips.hecomi.com

ダウンロード

github.com

環境

使い方

インストール

以下の Release ページから最新版の .unitypackage をダウンロードしてプロジェクトへインポートして下さい。

サーバ

uOscServer コンポーネントを GameObject へアタッチするとサーバが起動します。その上で受信したデータを貰えるようイベントを登録してください。

using UnityEngine;
using uOSC;

public class ServerTest : MonoBehaviour
{
    void Start()
    {
        var server = GetComponent<uOscServer>();
        server.onDataReceived.AddListener(OnDataReceived);
    }

    void OnDataReceived(Message message)
    {
        // OSC のアドレス
        var msg = message.address + ": ";

        // object[] として OSC の引数がやってくる
        foreach (var value in message.values)
        {
            if      (value is int)    msg += (int)value;
            else if (value is float)  msg += (float)value;
            else if (value is string) msg += (string)value;
            else if (value is byte[]) msg += "byte[" + ((byte[])value).Length + "]";
        }

        Debug.Log(msg);
    }
}

OnDataReceived() はメインスレッドから呼ばれているので、Unity の API もこの中で利用できます。また、OSC バンドル(後述)は展開された上でこのイベントハンドラが呼ばれるので、自身で展開する必要はありません。

クライアント

サーバと同じように uOscClient コンポーネントをいずれかの GameObject にアタッチして下さい。uOscClient.Send() にアドレスと任意の引数(intfloatstringbyte[] のいずれか)を渡すことでサーバに送信されます。

using UnityEngine;
using uOSC;

public class ClientTest : MonoBehaviour
{
    void Update()
    {
        var client = GetComponent<uOscClient>();
        client.Send("/uOSC/test", 10, "hoge", 1.234f, new byte[] { 1, 2, 3 });
    }
}

起動・停止

ランタイムで起動・停止したい場合は uOscServer または uOscClientenable を切り替えて下さい。

テクスチャの送信例

テクスチャを送信したい場合は以下のようにします。余談ですが Unity 2017 からちょっと書き方が変わってるので注意です。

送信側

using UnityEngine;
using uOSC;

public class ClientBlobTest : MonoBehaviour
{
    [SerializeField]
    Texture2D texture;

    byte[] byteTexture_;

    void Start()
    {
#if UNITY_2017
        byteTexture_ = ImageConversion.EncodeToPNG(texture);
#else
        byteTexture_ = texture.EncodeToPNG();
#endif
    }

    void Update()
    {
        var client = GetComponent<uOscClient>();
        client.Send("/uOSC/blob", byteTexture_);
    }
}

受信側

using UnityEngine;
using uOSC;

namespace uOSC
{

public class ServerBlobTest : MonoBehaviour
{
    Texture2D texture_;

    void Start()
    {
        var server = GetComponent<uOscServer>();
        server.onDataReceived.AddListener(OnDataReceived);

        texture_ = new Texture2D(256, 256, TextureFormat.ARGB32, true);

        var renderer = GetComponent<Renderer>();
        renderer.material.mainTexture = texture_;
    }

    void OnDataReceived(Message message)
    {
        if (message.address == "/uOSC/blob")
        {
            var byteTexture = (byte[])message.values[0];
#if UNITY_2017
            ImageConversion.LoadImage(texture_, byteTexture, true);
#else
            texture_.LoadImage(byteTexture);
#endif
        }
    }
}

結果

f:id:hecomi:20170820190923p:plain

OSC バンドル

OSC バンドルは複数の OSC メッセージ(アドレスと引数の組)および OSC バンドルを入れ子でまとめることの出来る仕組みです。具体例を見てみると分かりやすいと思いますので、クライント側で OSC バンドルを作成して送信する例を以下に示します。

using UnityEngine;
using uOSC;

public class ClientBundleTest : MonoBehaviour
{
    void Update()
    {
        var client = GetComponent<uOscClient>();

        // Bundle (root)
        //   - Bundle 1 -> 現在時刻
        //     - Message 1-1
        //     - Message 1-2
        //     - Message 1-3
        //   - Bundle 2 -> 10 秒後
        //     - Message 2-1
        //     - Message 2-2
        //     - Message 2-3
        //   - Message 3 -> 即時

        var bundle1 = new Bundle(Timestamp.Now);
        bundle1.Add(new Message("/uOSC/root/bundle1/message1", 123, "hoge", new byte[] { 1, 2, 3, 4 }));
        bundle1.Add(new Message("/uOSC/root/bundle1/message2", 1.2345f));
        bundle1.Add(new Message("/uOSC/root/bundle1/message3", "abcdefghijklmn"));

        var date2 = System.DateTime.UtcNow.AddSeconds(10);
        var timestamp2 = Timestamp.CreateFromDateTime(date2);
        var bundle2 = new Bundle(timestamp2);
        bundle2.Add(new Message("/uOSC/root/bundle2/message1", 234, "fuga", new byte[] { 2, 3, 4 }));
        bundle2.Add(new Message("/uOSC/root/bundle2/message2", 2.3456f));
        bundle2.Add(new Message("/uOSC/root/bundle2/message3", "opqrstuvwxyz"));

        var root = new Bundle(Timestamp.Immediate);
        root.Add(bundle1);
        root.Add(bundle2);
        root.Add(new Message("/uOSC/root/message3"));

        client.Send(root);
    }
}

合計 7 つのメッセージがまとめられています。サーバ側で uOSC を使っている場合はこれらが展開され、7 回 onDataReceived イベントがコールされます。

f:id:hecomi:20170820191515p:plain

タイムスタンプ

上で見たように、OSC バンドルにはタイムスタンプが含まれています。これを利用するには以下のように Timestamp クラスの ToLocalTime() を呼出して DateTime 型に変換して下さい(そのままだと ulong 型です)。タイムスタンプは各メッセージのメンバに含まれています。

using UnityEngine;
using uOSC;

public class ServerTest : MonoBehaviour
{
    ...

    void OnDataReceived(Message message)
    {
        var dateTime = message.timestamp.ToLocalTime();
    }
}

このタイムスタンプを見て、自身のアプリケーションで即時実行するか遅延実行するかなどを使い分けて下さい(uOSC 側では遅延受け渡しは行いません)。

OSC について

データ構造をちょっとだけメモ書きしておきます。例えば OSC で "/hoge/fuga" アドレスに [ 100, 123.456f, "fuga", new byte[] { 1, 2, 3 } ] を送るとします。これを OSC の仕様に従って作ると以下のようなデータ列になります。

12 byte: "/hoge/fuga\0\0"
8 byte: ",ifsb\0\0\0"
4 byte: 100
4 byte: 123.456f
8 byte: "fuga\0\0\0"
4 byte: 3
4 byte: [ 1, 2, 3, 0 ]

合計 40 byte です。データは 4 byte になるように空き領域には \0 が詰められ、文字列は NULL 終端を含みます。byte 列の場合は直前に 4 byte の int でバッファのサイズが格納されています。どういったデータが格納されているかはアドレスに続いて、, から始まる形で各データ型が char の配列で格納されています。int なら ifloat なら f といった形です。データはビッグエンディアンなのでひっくり返す必要があります。

バンドルの場合は、最初のデータが文字列で #bundle から始まり、続いてタイムスタンプ、そして複数の要素ブロックになります。各要素のブロックはまずそのブロックのサイズが 4 byte の int で各のされていて、そのサイズだけブロックをパースします。ここで再度 #bundle から始まるようであれば、再帰的に Bundle を展開していく形になります。例えば、上記のメッセージを第 1 層と第 2 層に含むようなバンドルの例を見てみます。

8 byte: "#bundle\0"
8 byte: 0x00000001 // NTP タイムスタンプ
  // ---
  4 byte: 40 // サイズ
  12 byte: "/hoge/fuga\0\0"
  8 byte: ",ifsb\0\0\0"
  4 byte: 100
  4 byte: 123.456f
  8 byte: "fuga\0\0\0"
  4 byte: 3
  4 byte: [ 1, 2, 3, 0 ]
  // ---
  4 byte: 60 // サイズ
  8 byte: "#bundle\0"
  8 byte: 0x00000001
    // ---
    4 byte: 40 // サイズ
    12 byte: "/hoge/fuga\0\0"
    8 byte: ",ifsb\0\0\0"
    4 byte: 100
    4 byte: 123.456f
    8 byte: "fuga\0\0\0"
    4 byte: 3
    4 byte: [ 1, 2, 3, 0 ]

なお、タイムスタンプは上位 32 ビットに 1900 年 1 月 1 日からの秒数、下位 32 ビットに少数点以下の時間が含まれています。

このルールに従ってデータ列を生成・展開して上げれば良いわけですね。実装は以下の辺りをご参照下さい:

おわりに

まだ、基本型しかサポートしていなかったり、アドレス毎のルーティングもないのですが、今後自分で使っていく上で欲しい機能が出てきたらポチポチ追加していきたいと思います。何かご要望やバグなどありましたら @hecomi までお気軽にご連絡下さい。