APIリファレンス

coreの型はMeltInterfaceTechnologies.ContactGlove.V2、Unityアダプターは.Unity名前空間にあります。受信したスナップショットは変更できません。データへのアクセスには、ホストとグローブを明示したキーを使います。

クライアントと接続設定

var client = new ContactGloveClient();
OperationResult result = client.Connect(new HostOptions
{
    HostId = "localhost",
    IpAddress = "127.0.0.1",
    ServerPort = 25791,
    ReceivePort = 0,
    SendPort = 0
});
HostOptionsのフィールド既定値制約・意味
HostIdlocalhost空文字列・空白のみを許可しない、アプリケーション内のセッションキー
DisplayNameDivingStationホストの表示用名称
IpAddress127.0.0.1IPAddress.TryParseで解析できる数値IP。DNS名は解決しない
ServerPort257911〜65535
ReceivePort257880〜65535。0はローカルポートの自動割り当て
SendPort00〜65535。0はローカルポートの自動割り当て
NotificationCapacity256ホストごとの通知キュー容量。8〜65536
StaleAfterSeconds0有限・非負の秒数。0はセッション単位の期限切れ診断を無効化
ProfileTimeoutSeconds5Profile問い合わせのローカル期限。有限・正の秒数

設定はセッション作成時にコピーされます。実際の使用ポートはGetDiagnostics(hostId)で確認してください。明示的に指定したポートは排他的にbindし、競合するとTransportUnavailableを返します。

クライアントメソッド用途
Connect(HostOptions)セッション開始、または同じ設定の既存セッションで登録再試行
Resume(string hostId)保持しているセッションで登録再試行
Disconnect(string hostId)1つのセッションを終了し、そのデータと購読を無効化
GetDiagnostics(string hostId)セッション診断を取得。存在しなければnull
GetDevices(string hostId)Deviceメタデータの一覧を取得。ホストがなければ空配列
PumpNotifications(int maximumPerHost = 256)所有スレッドで通知コールバックを実行
Dispose()全セッションを終了。繰り返し呼び出し可能

デバイスの識別とデータの有無

var key = new DeviceKey("localhost", "the-glove-id");

HostIdとGloveIdの組でデバイスを識別します。データが利用できない場合、取得APIはfalseとnullのスナップショットを返します。ゼロで埋めた仮のフレームは返しません。

取得APIスナップショット内容
TryGetDevice(key, out value)DeviceSnapshotデバイス情報
TryGetController(key, out value)ControllerSnapshotボタン・アナログ入力
TryGetErgonomics(key, out value)ErgonomicsSnapshot20個の角度と追跡状態
TryGetSkeleton(key, out value)SkeletonSnapshotRetarget済みスケルトンとProfile UUID
TryGetRawSkeleton(key, out value)RawSkeletonSnapshotRetarget前の手の骨格
TryGetRawSensorTransforms(key, out value)RawSensorSnapshot5個のセンサー中心pose

GetDataStatus(key, stream)はSuccess、UnknownHost、UnknownDevice、DataUnavailable、InvalidArgumentを区別します。対象はDeviceからRawSensorTransformsまでの6種類のキャッシュされたストリームです。ProfileResponseはポーリング対象ではありません。

メタデータと各ストリームは別々に到着します。メタデータを受信しただけでは現在の追跡成功は確認できません。UnknownとPairingのメタデータがキャッシュに残る場合もあります。Unconnectedを受信すると該当デバイスのストリームを削除し、Deviceの購読者へ通知します。ホスト・ストリームごとに保持するIDは最大16個です。容量超過による破棄は診断値に記録します。

DeviceSnapshot

Key、IsLeft、PairId、Name、Family、Color、Connection、BatteryPercentage、ExternalPowerPresent、SupportsHapticsなどを取得できます。

PairIdは相手側グローブのデバイスIDで、相手がない場合は空文字列です。Familyはデバイスの種類、Colorは表示色設定(DeviceColor型)です。

BatteryPercentageは0〜100、または無効・取得不能を表す-1です。ExternalPowerPresentは外部電源の検出であり、充電中の保証ではありません。このAPIのSupportsHapticsは本体振動の対応状況を表します。

ConnectionStateはUnknown=0(不明)、Connected=1(接続済み)、Unconnected=2(未接続)、Pairing=3(接続試行中)です。対象製品の種類はDeviceFamily.ContactGlove3=3で、現在のUnity対象製品はContactGlove3 Proです。

ConnectionStateはグローブの接続状態を表します。SDKとDivingStationの通信状態はSessionStateで確認します。

ControllerSnapshot

プロパティ値
A、B、System、TriggerButton、JoystickButton、TrackpadButtonButtonState.Unpressed=0、Touching=1、Pressed=2
Trigger、GripValue、GripForce0〜1
JoystickX、JoystickY、TrackpadX、TrackpadY-1〜1

各値にはDivingStationでのキャリブレーション・deadzone処理が適用されています。GripForceは正規化された入力で、N単位ではありません。通知は受け付けた入力レコードを渡すもので、すべてのボタン変化の通知を保証するものではありません。最初の状態を基準とし、中断や通知欠落後は保持している入力状態を解除してください。

TriggerButtonは3状態のボタン入力、Triggerはアナログのトリガー値です。GripValue(グリップ値)とGripForce(グリップ力量)は別のアナログ入力です。現在のContactGlove3のトラックパッド出力は離散値で、Xは0、Yは-1、0、1のいずれかです。

ErgonomicsSnapshot

Anglesには度単位で20個の値を格納します。TrackingはRetarget前の骨格を求めるIK(逆運動学)の計算結果です。

Index指順序
0〜3親指CMC、MCP、IP、tilt
4〜7人差し指MCP、PIP、DIP、tilt
8〜11中指MCP、PIP、DIP、tilt
12〜15薬指MCP、PIP、DIP、tilt
16〜19小指MCP、PIP、DIP、tilt

例えばAngles[6]は人差し指DIP関節の曲げ角度です。

CMC/MCP/IP/PIP/DIPの値は伸びた状態を基準とする曲げ角度、tiltはデフォルト状態を0°とする傾き角度です。

SkeletonSnapshotとRawSkeletonSnapshot

いずれもHandSnapshotを継承し、Positions(25個)、Rotations(19個)、Tracking、GeneratedAtを持ちます。SkeletonSnapshotにはさらにProfileUuidがあります。

位置のIndexは次の順序です。

Indexノードの順序
0wrist
1〜4thumb CMC、MCP、IP、tip
5〜9index CMC、MCP、PIP、DIP、tip
10〜14middle CMC、MCP、PIP、DIP、tip
15〜19ring CMC、MCP、PIP、DIP、tip
20〜24little CMC、MCP、PIP、DIP、tip

回転は、位置ノードIndexの1,2,3,5,6,7,8,10,11,12,13,15,16,17,18,20,21,22,23に対応します。固定の対応関係にはHandTopology.ParentsとHandTopology.RotationNodesを使用できます。

Raw Skeletonは追跡と関節計算を終えた、Retarget前の骨格です。RawとRetarget済みストリームの追跡状態は異なることがあります。

RawSensorSnapshot

Posesには親指、人差し指、中指、薬指、小指の順に5個のWirePoseを格納します。各値は手首を原点とする手モデル内の、推定センサー中心の位置・姿勢です。GeneratedAtで生成時刻、AgeSecondsで受信してからの経過時間を確認します。

位置は各センサーの中心、回転は手モデル内での各センサーの向きを表します。関節ごとの位置・回転はSkeletonSnapshotまたはRawSkeletonSnapshotで取得します。RawSensorSnapshotに追跡状態を表すTrackingプロパティはありません。

時刻、データの新しさ、追跡状態

ReceivedAtはローカルの単調増加時計による受信時刻(秒)、AgeSecondsは受信からの経過秒数です。骨格・角度・センサーのデータには、Unixミリ秒単位の生成時刻GeneratedAtもあります。DeviceSnapshotとControllerSnapshotには生成時刻はありません。

キャッシュは到着順に更新されます。各ストリームは独立して届くため、組み合わせて使う場合は時刻とデータの新しさを確認してください。

追跡状態使用できる用途
Tracked=0現在のIK結果。制御にはデータの新しさも確認
IkFailed=1表示・診断のみ
NotAttempted=2表示・診断のみ

NotAttemptedはIK処理を実施しなかったことを表します。前段階の失敗や入力欠損で試行できなかった場合だけでなく、更新間隔の制御などで姿勢計算が不要と判断された場合も含み、この状態自体はIK処理の失敗を意味しません。

IkFailedとNotAttemptedを制御、速度推定、補間の入力に使用してはいけません。セッションがReceivingでも追跡に失敗している場合があり、デバイスがConnectedでもストリームが未受信・期限切れの場合があります。セッション単位の期限切れ診断と、デバイスから届く接続状態は別です。

座標系とUnityでの表示

PositionsとRotationsには、DivingStationから受信した座標と回転が入ります。位置は手首を原点とする手モデル内の値で、単位はメートルです。左右の手で同じ軸の規則を使います。

SkeletonSnapshotはRetarget profileの手モデル、RawSkeletonSnapshotはRetarget前の手モデルを基準にします。どちらも手首が原点ですが、手モデルが異なるため座標をそのまま置き換えることはできません。センサーの位置・姿勢は、Retarget済みの骨格と同じ手モデル座標系です。

受信した座標の軸向き
+X指先から手首へ向かう方向。伸ばした中指はおおむね-Xへ伸びる
+Y手のひらから手の甲へ向かう方向
Z手幅方向

独自に表示する場合は、UnityPoseConverterで受信値をUnityの座標に変換してください。位置はXとYの値を保ち、Zの符号を反転します。長さの単位はメートルのままです。

Vector3 position = UnityPoseConverter.Position(wirePosition);
Quaternion rotation = UnityPoseConverter.Rotation(wireRotation);

位置の変換は(x, y, -z)です。受信したクォータニオンの成分順は(w,x,y,z)で、UnityのコンストラクターにはQuaternion(-x,-y,z,w)として渡します。wirePositionとwireRotationは、この例で変換する受信値の変数名です。

変換した位置は、手の表示ルートから見た位置です。25個の関節を同じ表示ルートの子として配置する場合は、各関節のlocalPositionに使えます。骨ごとに親子を作る場合は、親関節からの相対位置を求めて設定してください。ワールド座標が必要な場合は、表示ルートのTransformをdisplayRootとしてdisplayRoot.TransformPoint(position)を使います。表示ルートはアプリケーションで配置し、長さをメートルのまま使う場合は、表示ルートと親階層のScaleを(1,1,1)に保ってください。

19個の回転は、それぞれの親関節に対する回転です。UnityPoseConverter.ModelRotations(snapshot, destination)は親から子の順に回転を合成し、25個の関節の向きを手の表示ルート基準で求めます。destinationには25要素以上のQuaternion配列を渡します。ルートの回転はQuaternion.identity、指先の回転は親関節の向きとして計算します。

標準表示ではSkeletonRenderer.Apply(snapshot)に受信したスナップショットを渡してください。内部で位置を変換し、25個の球と24本の円柱を表示します。呼び出し前に位置を変換すると二重変換になるため、そのまま渡します。Apply(null)で非表示にします。Unityのメインスレッドで呼び出し、呼び出し側でTrackingとAgeSecondsを確認してください。

独自表示で変換した位置・回転にも、同じ変換を重ねないでください。XR HandsのposeはUnityの座標で取得できるため、そのposeにはUnityPoseConverterを使いません。

本体振動

OperationResult result = client.Vibrate(key, frequency, amplitude, duration);

引数は周波数(100〜500 Hz)、振幅(0〜1)、時間(0.001〜1秒)です。境界値を含みます。非有限値・範囲外の値はclampせず拒否します。

デバイスのメタデータが存在し、接続済みで、SupportsHapticsを報告している必要があります。メタデータとして受理したIDでも、振動のOSCアドレスに使えない場合があります。空ID、制御文字、スペース、/、#、OSCパターン文字*?[]{},を含むIDは、送信前にInvalidArgumentとして拒否します。

SuccessはローカルUDP送信の成功です。実際の動作を確認する応答はなく、再接続後にコマンドを再送しません。

Retarget profileの表示情報

IDisposable responseToken = client.SubscribeProfileResponse(hostId, response =>
    Debug.Log($"{response.RequestId}: {response.Status} {response.Name}"));
IDisposable timeoutToken = client.SubscribeProfileTimeout(hostId, timeout =>
    Debug.LogWarning($"Profile request {timeout.RequestId} timed out"));
OperationResult result = client.RequestRetargetProfileInfo(
    hostId, profileUuid, out int requestId);

ProfileResponseSnapshotにはHostId、RequestId、ProfileUuid、Status、Name、Description、ReceivedAtがあります。

ProfileStatus意味
Success=0Profileが存在し、名前と説明を取得できた
NotFound=1UUID形式は正しいが該当するProfileがない
InvalidRequest=2サーバーが不正な問い合わせを報告した

ローカルな引数エラーやタイムアウトは、サーバーから届く状態値とは別です。SDKが要求と応答を対応付け、対応しない応答の件数を診断値に記録します。未完了の要求はホストごとに最大128件で、超えるとTransportUnavailableを返します。既定のローカル期限は5秒です。Profile情報がタイムアウトしても、スケルトンの配信・デコードは停止しません。

通知が不要になったら2つのトークンを破棄してください。購読はホストの起動後に行います。

通知と所有権

SubscribeDevice、SubscribeController、SubscribeErgonomics、SubscribeSkeleton、SubscribeRawSkeleton、SubscribeRawSensorTransforms、SubscribeProfileResponse、SubscribeProfileTimeout、SubscribeNotificationLossはIDisposableの購読トークンを返します。

受信処理はデコード・キャッシュ更新の後、通知をキューに追加します。購読したコールバックは、所有スレッドがPumpNotificationsを呼んだときだけ実行されます。通常はUnityのUpdateで呼びます。所有スレッドは1つとし、再帰呼び出しや別スレッドからのpumpはInvalidOperationExceptionになります。ContactGloveBehaviourは自分のクライアントのpumpをすでに行います。

スナップショットは内部に専用の配列を持ち、読み取り専用のFixedValues<T>として公開します。後続の更新後も古いスナップショットを保持でき、CopyToでコピーできます。デコード時にオブジェクトと配列を確保します。

キューがあふれると、キューにある未処理の通知をすべて破棄して通知欠落を報告し、キャッシュされたController状態を消去します。破棄した通知にProfileの応答が含まれていた場合、その要求には応答もタイムアウトも通知されません。アプリケーションで保持している入力を解除し、新しいスナップショットで同期し直してください。新たな基準値の取得を押下変化として扱わないでください。コールバックの例外処理は分離し、例外の件数を記録します。コールバック内で購読やクライアントを破棄することもできます。

接続のライフサイクル

  1. Connectは受信用ソケットと、継続して使用する送信用ソケットを作成・bindし、登録を送信します。最初の成功はローカルの起動・送信を意味し、サーバーの応答確認ではありません。
  2. 同じ送信ソケットを、登録、単調増加時計による30秒ごとのkeepalive、Profile問い合わせ、振動、切断で使用します。時刻はUnityのtimeScaleに依存しません。
  3. 同じ設定のConnectまたはResumeは、ソケット、スナップショット、購読、未完了のProfile要求を保って登録を再試行します。登録の送信間隔は1秒以上に制限され、制限中はパケットを送らず成功を返す場合があります。
  4. 再試行には同じホストID、解析済みIP、サーバーポート、設定したローカルポートが必要です。最初の設定が0なら再試行でも0を指定します。異なる設定はInvalidArgumentとなり、既存セッションは保持します。その他の設定は作成時にコピーした値を使い続けます。
  5. 未受信・古いデータやProfileタイムアウトだけではセッションを自動破棄しません。登録の再試行は承認の証明でも、停止した受信処理の修復でもありません。
  6. Disconnect、Dispose、所有コンポーネントのDisableはセッションを終了し、購読・データ・未配信通知を無効化してソケットを解放します。新しいセッションでは購読し直してください。ポート0の場合、新しい送信元ポートになることがあります。

DivingStationの承認は、動作中の送信元endpointと受信ポートに対応付けられます。明示的な切断、または最後のConnectから90秒の経過でサーバーセッションが失効します。ポートを固定しても、後で作成する新しいセッションの承認が保存されるわけではありません。Disconnectが届かなければ、失効までサーバー側にセッションが残る場合があります。

ContactGloveBehaviourはDisable、Destroy、アプリケーション終了、Editorのアセンブリ再読み込み、Play終了時に停止します。domain/scene reloadを無効にしたPlay開始にもruntime hookで対応します。StartClientは冪等で、既存クライアントがある場合は新たなConnectを送信しません。既存セッションの登録再試行にはClient.Resumeを使用してください。

操作結果と診断

OperationResultにはCode、Detail、Succeededがあります。失敗を明示的に処理してください。

ResultCode意味
Successローカルの操作成功
InvalidArgument不正な値、または既存セッションと異なる設定
UnknownHost指定ホストの有効なセッションがない
UnknownDevice必要なデバイスメタデータがない
DataUnavailable要求したストリームが未受信
NotConnectedデバイスが接続されていない
Unsupportedデバイスがその機能に対応していない
TransportUnavailableローカル通信で操作できなかった、またはProfile要求の上限に達した
Disposedクライアントが破棄済み

SessionDiagnostics.StateはStopped、Starting、WaitingForData、Receiving、Stale、Faultedのいずれかです。WaitingForDataだけでは、承認待ち、サーバー不在、経路障害を区別できません。DisconnectしたホストのGetDiagnosticsは、Stoppedではなくnullを返します。

診断では実際の使用ポート、キュー長、受信datagram数、受理レコード数、パースエラー、キャッシュ上限による破棄、通知欠落、コールバック例外、想定外送信元、対応しないProfile応答、登録送信数、最終受信時刻、最終エラー、入力再同期の要否などを確認できます。トラブルシューティングとあわせて利用してください。