💃

mocopiのUnity SDK触ってみたメモ

2023/04/25に公開
3

公式の開発者向けサイト:

https://www.sony.net/Products/mocopi-dev/jp/

環境

  • MacBook Air M2, macOS 13.3.1 (ARM64)
  • iPhone 12 mini, iOS 16.3.1
  • MacとiPhoneは同じWifiに接続している状態
  • Unity 2021.3.0f1
    • 公式的には 2020.3.33f1 をサポートしているようだが、上記バージョンでも問題なく動作した
  • mocopi Receiver Plugin for Unity 1.0.2
  • BVH Sender 1.0.2

セットアップ

  1. mocopiアプリをスマホ(今回はiPhone)でインストール
  2. mocopiアプリでモーショントラッキングのセットアップ
  3. mocopiアプリで「設定」から Unityを起動しているPC(今回はMacBook) のIPアドレスを指定する
    • Portはデフォルトの 12351 のまま
  4. mocopi Receiver Plugin for UnityをUnityのプロジェクトに入れる
  5. UnityでサンプルのSceneを開いて再生を開始する
  6. mocopiアプリで緑色の「送信」モードに変更し、送信を開始する
  7. mocopiのトラッキングがUnityに反映されれば成功

つまづきポイント1: Firewallの設定

ありがちだが、PCのセキュリティソフトなどのFirewallで通信を弾いてしまうとモーションデータの送受信ができないので、うまくいかない場合は確認する。

FirewallをOffにしてもうまくいかない場合は、BVH Senderを使ってPC単体でデータを送った場合にUnityにモーションが反映されているか確認してみたり、WiresharkでUDP通信を確認してみたりするといいかも。

つまづきポイント2: UDPソケットの実装

(2023/06/02追記:SDK ver1.0.3, mocopi実機ではコードの変更をすることなくUDPの受信ができました)

MocopiUdpReceiver.cs の242行目 UdpTaskAsync のメソッドがUDPソケットの受信待ち受けの処理の実装になっているが、252行目の byte[] message = this.udpClient.Receive(ref remoteEP) がうまく動いていないのかBVH Senderのデータすら受け取れていない状態になっていた。

下記のように、非同期版の ReceiveAsync に差し替えると問題なく動作したが、Unityの再生終了時にCancellationが効かないためErrorのログが出るようになるので注意。
-> catchを追加してループを抜けるよう修正するのが良さそうです。

        private async void UdpTaskAsync(CancellationToken cancellationToken)
        {
            this.udpClient = new UdpClient(this.Port);

            while (!cancellationToken.IsCancellationRequested && this.udpClient != null)
            {
                int id = System.Threading.Thread.CurrentThread.ManagedThreadId;
                try
                {
                    // NOTE: 同期版Receiveでは動かなかったので、非同期版ReceiveAsyncを使用
                    // IPEndPoint remoteEP = null;
                    // byte[] message = this.udpClient.Receive(ref remoteEP);
                    var receiveResult = await this.udpClient.ReceiveAsync();
                    var message = receiveResult.Buffer;

                    lock (lockObject)
                    {
                        if (SonyMotionFormat.IsSmfBytes(message.Length, message))
                        {
                            // Processing of data acquired in "SonyMotionFormat"
                            this.HandleSonyMotionFormatData(message);
                        }
                    }
                }
                catch (SocketException e)
                {
                    Debug.Log($"[MocopiUdpReceiver] {e.Message} : {e.GetType()}");
                }
		// NOTE: await中にsocketがDisposeされる場合があるが、Cancelされている場合は正常終了なのでループを抜ける
                catch (ObjectDisposedException e)
                    when (cancellationToken.IsCancellationRequested)
                {
                    Debug.Log($"[MocopiUdpReceiver] Dispose udp socket while receiving. {e.Message} : {e.GetType()}");
                    break;
                }
                catch (System.Exception e)
                {
                    Debug.LogErrorFormat($"[MocopiUdpReceiver] Udp receive failed. {e.Message} : {e.GetType()}");
                    this.UdpStop();
                    this.OnUdpReceiveFailed?.Invoke(e);
                    break;
                }

                await Task.Delay(1);
            }
        }

VRMモデルをランタイムで読み込んで使用する場合

自分でゴリゴリにSDK実装部分を書いても良いが、SDKをそのまま利用してパッと動かすだけなら、MocopiAvatar のコンポーネントをVRMの Animator のアタッチされている Transform (=Root)に AddComponent し、MocopiSimpleReceiver.AvatarSettings にPortと一緒に登録する。

下記、UniVRM ver0.108.0 を使用した雑なサンプルコード:

#nullable enable
using System;
using System.IO;
using System.Threading;
using Mocopi.Receiver;
using UnityEngine;
using UniVRM10;
using VRMShaders;

namespace Demo
{
    public sealed class MocopiVRMDemo : MonoBehaviour
    {
        [SerializeField] private MocopiSimpleReceiver? receiver = null;
        [SerializeField] private string vrmPath = string.Empty;
        [SerializeField] private int port = 12351;

        private async void Start()
        {
            if (receiver == null)
            {
                throw new NullReferenceException(nameof(receiver));
            }

            var binary = await File.ReadAllBytesAsync(vrmPath);

            var instance = await Vrm10.LoadBytesAsync(
                bytes: binary,
                canLoadVrm0X: true,
                controlRigGenerationOption: ControlRigGenerationOption.None,
                showMeshes: true,
                awaitCaller: new RuntimeOnlyAwaitCaller(),
                materialGenerator: null,
                vrmMetaInformationCallback: null,
                ct: CancellationToken.None
            );

            var animator = instance.transform.GetComponent<Animator>();
            var avatar = animator.gameObject.AddComponent<MocopiAvatar>();
            receiver.AvatarSettings.Add(
                new MocopiSimpleReceiver.MocopiSimpleReceiverAvatarSettings(
                    avatar,
                    port
                )
            );

            receiver.StartReceiving();
        }

        private void OnDestroy()
        {
            if (receiver != null)
            {
                receiver.StopReceiving();
            }
        }
    }
}

動的に読み込んだVRMアバターにmocopiのモーションが反映されれば成功。

気になったこと

Smoothingとは関係なく、一定間隔でUnity上のアバターがブルっと動く現象があった。

mocopiアプリ側では問題なく表示されていたので、UnityのSDKの実装の問題か、あるいはスマホ→PCの通信の問題?

Discussion

Mitsuhiro KogaMitsuhiro Koga

UdpTaskAsyncメソッドの先頭で cancellationToken.Register(this.udpClient.Close); でCancelされた時の処理を登録しておいてObjectDisposedExceptionがthrowされた時(Closeされた時)にループを抜けるとうまく連携できませんか?

mochinekomochineko

コメントありがとうございます!

Unityの再生終了時にCancellationが効かないためErrorのログが出るようになるので注意。

の部分のご指摘ですよね?

詳しくは、UdpSocketのCloseの処理自体は既に実装されているのですが、awaitしている途中に呼ばれる可能性があり、かつ ReceiveAsync にCancellationの対応がないため ObjectDisposedException がthrowされる場合がある、という認識です。

アドバイスを参考に、下記のようなcatchを挟んでループを抜けると良さそうかなと思いました。

        // NOTE: await中にsocketがDisposeされる場合があるが、Cancelされている場合は正常終了なのでループを抜ける
        catch (ObjectDisposedException e)
             when (cancellationToken.IsCancellationRequested)
        {
             Debug.Log($"[MocopiUdpReceiver] Dispose udp socket while receiving. {e.Message} : {e.GetType()}");
             break;
        }
Mitsuhiro KogaMitsuhiro Koga

はい、その通りです。cancellationToken.Register(this.udpClient.Close);はメソッド外からキャンセルしたら連動する想定でしたが既にCloseがあるのならcatch (ObjectDisposedException)で十分だと思います。