mocopiのUnity SDK触ってみたメモ
公式の開発者向けサイト:
環境
- 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
セットアップ
- mocopiアプリをスマホ(今回はiPhone)でインストール
- mocopiアプリでモーショントラッキングのセットアップ
- mocopiアプリで「設定」から Unityを起動しているPC(今回はMacBook) のIPアドレスを指定する
- Portはデフォルトの
12351
のまま
- Portはデフォルトの
- mocopi Receiver Plugin for UnityをUnityのプロジェクトに入れる
- UnityでサンプルのSceneを開いて再生を開始する
- mocopiアプリで緑色の「送信」モードに変更し、送信を開始する
- 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
UdpTaskAsyncメソッドの先頭で
cancellationToken.Register(this.udpClient.Close);
でCancelされた時の処理を登録しておいてObjectDisposedExceptionがthrowされた時(Closeされた時)にループを抜けるとうまく連携できませんか?コメントありがとうございます!
の部分のご指摘ですよね?
詳しくは、UdpSocketのCloseの処理自体は既に実装されているのですが、awaitしている途中に呼ばれる可能性があり、かつ
ReceiveAsync
にCancellationの対応がないためObjectDisposedException
がthrowされる場合がある、という認識です。アドバイスを参考に、下記のようなcatchを挟んでループを抜けると良さそうかなと思いました。
はい、その通りです。
cancellationToken.Register(this.udpClient.Close);
はメソッド外からキャンセルしたら連動する想定でしたが既にCloseがあるのならcatch (ObjectDisposedException)
で十分だと思います。