仮想と物理とエトセトラ

xRや物理とかごった煮の備忘録的技術ブログ

Photon(PUN2)でハンドトラッキングを共有する(簡易版)

今回も前回に続きPhotonに関する試作です。
ハンドトラッキングの情報をほかのユーザに知らせるために手のジョイント情報を共有します。
今回は手にメッシュは張らず、各jointを表示することで手を再現します。

今回は下記を一部参考にしました。

akihiro-document.azurewebsites.net

準備

まずはジョイント情報を共有するためのクラスを作成します。
流れは以下です。

  1. MRTKのAPIから各ジョイントのTransform(position, rotation)を取得する
  2. 自分のプレイヤーの手のアバターを各ジョイントのTransformに合わせる
  3. 各ジョイントの相対位置をほかのユーザに送信する
  4. 他のユーザは相対位置に沿って特定ユーザのハンドジョイントを再現する

この流れをスクリプトにすると以下の様になります。

  • HandTrackingSharing.cs
クリックで展開
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Microsoft.MixedReality.Toolkit.Input;
using Microsoft.MixedReality.Toolkit.Utilities;
using Photon.Pun;

/// <summary>
/// ジョイント情報格納用クラス
/// </summary>
[System.Serializable]
public class JointInfo
{
    public TrackedHandJoint targetJoint;
    public Transform targetTransform = null;

    public JointInfo(TrackedHandJoint targetJoint, Transform targetTransform)
    {
        this.targetJoint = targetJoint;
        this.targetTransform = targetTransform;
    }
}

/// <summary>
/// ジョイント情報共有用クラス
/// </summary>
public class HandTrackingSharing : MonoBehaviourPunCallbacks, IPunObservable
{
    /// <summary>
    /// 手の種類(左右)
    /// </summary>
    [SerializeField]
    Handedness whichHand = Handedness.Right;

    /// <summary>
    /// Transform未設定箇所にtmpJointPrefabを使用するか否か
    /// </summary>
    [SerializeField]
    bool useTmpJoint2UnconfigJoint = false;

    /// <summary>
    /// Transform未設定箇所に生成されるPrefab
    /// </summary>
    [SerializeField]
    GameObject tmpJointPrefab;

    /// <summary>
    /// 各ジョイント情報(ジョイント名, Transform)
    /// </summary>
    /// <typeparam name="JointInfo"></typeparam>
    /// <returns></returns>
    [SerializeField]
    private List<JointInfo> fingerJointInfos = new List<JointInfo>()
    {
        new JointInfo(TrackedHandJoint.Wrist, null),
        new JointInfo(TrackedHandJoint.Palm, null),
        new JointInfo(TrackedHandJoint.ThumbMetacarpalJoint, null),
        new JointInfo(TrackedHandJoint.ThumbProximalJoint, null),
        new JointInfo(TrackedHandJoint.ThumbDistalJoint, null),
        new JointInfo(TrackedHandJoint.ThumbTip, null),
        new JointInfo(TrackedHandJoint.IndexMetacarpal, null),
        new JointInfo(TrackedHandJoint.IndexKnuckle, null),
        new JointInfo(TrackedHandJoint.IndexMiddleJoint, null),
        new JointInfo(TrackedHandJoint.IndexDistalJoint, null),
        new JointInfo(TrackedHandJoint.IndexTip, null),
        new JointInfo(TrackedHandJoint.MiddleMetacarpal, null),
        new JointInfo(TrackedHandJoint.MiddleKnuckle, null),
        new JointInfo(TrackedHandJoint.MiddleMiddleJoint, null),
        new JointInfo(TrackedHandJoint.MiddleDistalJoint, null),
        new JointInfo(TrackedHandJoint.MiddleTip, null),
        new JointInfo(TrackedHandJoint.RingMetacarpal, null),
        new JointInfo(TrackedHandJoint.RingKnuckle, null),
        new JointInfo(TrackedHandJoint.RingMiddleJoint, null),
        new JointInfo(TrackedHandJoint.RingDistalJoint, null),
        new JointInfo(TrackedHandJoint.RingTip, null),
        new JointInfo(TrackedHandJoint.PinkyMetacarpal, null),
        new JointInfo(TrackedHandJoint.PinkyKnuckle, null),
        new JointInfo(TrackedHandJoint.PinkyMiddleJoint, null),
        new JointInfo(TrackedHandJoint.PinkyDistalJoint, null),
        new JointInfo(TrackedHandJoint.PinkyTip, null),
    };

    /// <summary>
    /// 有効なジョイント数
    /// </summary>
    private int currentActiveJointNum = 0;

    /// <summary>
    /// 手がトラッキングされているか
    /// </summary>
    private bool isHandTracked = true;

    // Start is called before the first frame update
    void Start()
    {
        SetTmpJointObj();
        currentActiveJointNum = GetActiveJoint();
    }

    // Update is called once per frame
    void Update()
    {
        if(photonView.IsMine)
        {
            isHandTracked = HandJointUtils.TryGetJointPose(fingerJointInfos[0].targetJoint, whichHand, out MixedRealityPose jointPose);
            GetJointInfo();
        }
        else
        {
            SetVisible();
        }
        
    }

    /// <summary>
    /// 各ジョイント情報取得関数
    /// </summary>
    private void GetJointInfo()
    {
        foreach(var fingerJointInfo in fingerJointInfos)
        {
            if (HandJointUtils.TryGetJointPose(fingerJointInfo.targetJoint, whichHand, out MixedRealityPose jointPose))
            {
                fingerJointInfo.targetTransform.position = jointPose.Position;
                fingerJointInfo.targetTransform.rotation = jointPose.Rotation;
            }
        }
    }

    /// <summary>
    /// 仮のジョイントを適用する関数
    /// </summary>
    private void SetTmpJointObj()
    {
        if (useTmpJoint2UnconfigJoint)
        {
            foreach (var fingerJointInfo in fingerJointInfos)
            {
                if (fingerJointInfo.targetTransform == null)
                {
                    if (tmpJointPrefab != null)
                    {
                        var jointObject = Instantiate(tmpJointPrefab, Vector3.zero, Quaternion.identity, this.transform);
                        fingerJointInfo.targetTransform = jointObject.transform;
                    }
                }
            }
        }

    }

    /// <summary>
    /// トラッキング状態によって表示/非表示を切り替え
    /// </summary>
    private void SetVisible()
    {
        foreach (var fingerJointInfo in fingerJointInfos)
        {
            if(fingerJointInfo.targetTransform != null)
            {
                fingerJointInfo.targetTransform.gameObject.SetActive(isHandTracked);
            }
        }
    }

    /// <summary>
    /// 有効なジョイント数取得
    /// </summary>
    /// <returns></returns>
    private int GetActiveJoint()
    {
        int jointNum = 0;
        foreach (var fingerJointInfo in fingerJointInfos)
        {
            if (fingerJointInfo.targetTransform != null) jointNum++;
        }
        return jointNum;
    }

    /// <summary>
    /// ジョイント情報共有関数
    /// </summary>
    /// <param name="stream"></param>
    /// <param name="info"></param>
    void IPunObservable.OnPhotonSerializeView(PhotonStream stream, PhotonMessageInfo info)
    {
        if (stream.IsWriting)
        {
            // 自オブジェクトの場合
            stream.SendNext(isHandTracked);
            stream.SendNext(currentActiveJointNum);
            foreach (var fingerJointInfo in fingerJointInfos)
            {
                if (fingerJointInfo.targetTransform != null)
                {
                    stream.SendNext(fingerJointInfo.targetTransform.localPosition);
                    stream.SendNext(fingerJointInfo.targetTransform.localRotation);
                }

            }

        }
        else
        {
            // 他ユーザのオブジェクトの場合
            isHandTracked = (bool)stream.ReceiveNext();
            int activeJointNum = (int)stream.ReceiveNext();
            if (activeJointNum == currentActiveJointNum)
            {
                // 有効なジョイント数が同じ場合は情報を受け取る
                foreach (var fingerJointInfo in fingerJointInfos)
                {
                    if (fingerJointInfo.targetTransform != null)
                    {
                        fingerJointInfo.targetTransform.localPosition = (Vector3)stream.ReceiveNext();
                        fingerJointInfo.targetTransform.localRotation = (Quaternion)stream.ReceiveNext();
                    }

                }
            }
        }

    }
}

次にAssets/Resources配下のPlayer用Prefabに左右の手用に空オブジェクトを追加します。

次に先ほど作成したスクリプトコンポーネントとして左右それぞれの空オブジェクトにアタッチします。
今回はUseTmpJoint2UnconfigJointにチェックを入れ、TmpJointPrefabを設定し、ジョイント位置に仮のオブジェクトを表示させます。

設定するPrefabのCubeは下記の設定で問題ありません。

これで簡易的ですが、ハンドトラッキングの情報を共有する準備ができました。

動作確認

単純な仕組みですが、無事ジョイント位置の共有を行うことができました。
移動に補間を入れるとなお見やすそうです。
次回以降で、手にメッシュを張ってより手らしく見えるようにします。