Help us understand the problem. What is going on with this article?

ARKit 3のMotion CaptureでVRMを動かす【Unity】

はじめに

この記事はVTuber Tech #1 Advent Calendar 2019 1日目の記事です。

さて、iOSのARKit 3でMotion Captureの機能が追加されました。

UnityではARを扱うためのフリームワークであるAR FoundationでARKit 3の機能が使えるということで、早速試してみました。今回はVRM形式のウチの子『リリカちゃん』をARKit 3を使って動かして行こうと思います。

環境

ARKitにはいくつかの制約があり、Motion Captureを含む以下の機能の使用にはA12チップを搭載したデバイスである必要があります。つまりiPhone XS/XR以降の端末でしか使えません。

  • People Occlusion
  • Motion capture
  • フロントカメラ/バックカメラの同時使用
  • 複数フェイストラッキング

People Occlusion and the use of motion capture, simultaneous front and back camera, and multiple face tracking are supported on devices with A12/A12X Bionic chips, ANE, and TrueDepth Camera.

参考: ARKit 3 - Augmented Reality - Apple Developer

また、Motion Captureはバックカメラでしか動作しないようです。

When ARKit identifies a person in the back camera feed, it calls session(_:didAdd:), passing you an ARBodyAnchor you can use to track the body's movement.

参考: ARBodyTrackingConfiguration - ARKit | Apple Developer Documentation

また、AR Foundationについてもはまりどころがありました。

macOS CatalinaにおけるUnityの既知のバグで、iOS向けのビルドの描画がおかしくなる問題がありました。

Can't launch ARCollaborationData iOS · Issue #325 · Unity-Technologies/arfoundation-samples

こちらで示されているIssue Trackerによると2020.1以降で修正済みということで、今回はα版ですがUnity 2020.1.0a14.1541を使って実装してみました。
https://issuetracker.unity3d.com/issues/ios

アプローチ

Unity-Technologies/arfoundation-samplesをベースに実装しました。

上手くいかなかった方法

読み飛ばしてもらって問題ないです。m_HumanBodyManager.humanBodiesChangedから取得できる関節情報を直接Humanoid方に流し込む方法を試しました。コードはこんな感じ。

HumanoidTracker.cs
// VRM生成部分は省略
public class HumanoidTracker : MonoBehaviour
{
    [SerializeField] ARHumanBodyManager m_HumanBodyManager;

    private Animator _animator;

    void OnEnable()
    {
        m_HumanBodyManager.humanBodiesChanged += OnHumanBodiesChanged;
    }

    void OnDisable()
    {
        if (m_HumanBodyManager != null)
        {
            m_HumanBodyManager.humanBodiesChanged -= OnHumanBodiesChanged;
        }
    }

    private void Update()
    {
        var origin = FindObjectOfType<BoneController>()?.GetComponent<Animator>();
        if (_animator == null || origin == null)
        {
            return;
        }

        var originalHandler = new HumanPoseHandler(origin.avatar, origin.transform);
        var targetHandler = new HumanPoseHandler(_animator.avatar, _animator.transform);

        HumanPose humanPose = new HumanPose();
        originalHandler.GetHumanPose(ref humanPose);
        targetHandler.SetHumanPose(ref humanPose);

        _animator.rootPosition = origin.rootPosition;
        _animator.rootRotation = origin.rootRotation;
    }

    void OnHumanBodiesChanged(ARHumanBodiesChangedEventArgs eventArgs)
    {
        if (_animator == null)
        {
            return;
        }

        foreach (var humanBody in eventArgs.updated)
        {
            SetHumanBoneTransformToHumanoidPoses(humanBody);
        }
    }

    void SetHumanBoneTransformToHumanoidPoses(ARHumanBody body)
    {
        if (!body.joints.IsCreated)
        {
            return;
        }

        var bones = Enum.GetValues(typeof(HumanBodyBones)) as HumanBodyBones[];
        foreach (HumanBodyBones bone in bones)
        {
            if (bone < 0 || bone >= HumanBodyBones.LastBone)
            {
                continue;
            }

            var joint = HumanoidUtils.GetXRHumanBodyJoint(body, bone);
            Transform t = _animator.GetBoneTransform(bone);
            if (t != null)
            {
                t.localPosition = joint.localPose.position;
                t.localRotation = joint.localPose.rotation;
            }
        }
    }
}

前半はほぼサンプルコードそのままで、注目して欲しいのは後半のSetHumanBoneTransformToHumanoidPosesメソッドです。Unity標準Humanoidアバターのボーン構造であるHumanBodyBonesforeachで回して、ジョイントの値を取得しています。

void SetHumanBoneTransformToHumanoidPoses(ARHumanBody body)
{
    if (!body.joints.IsCreated)
    {
        return;
    }

    var bones = Enum.GetValues(typeof(HumanBodyBones)) as HumanBodyBones[];
    foreach (HumanBodyBones bone in bones)
    {
        if (bone < 0 || bone >= HumanBodyBones.LastBone)
        {
            continue;
        }

        var joint = HumanoidUtils.GetXRHumanBodyJoint(body, bone);
        Transform t = _animator.GetBoneTransform(bone);
        if (t != null)
        {
            t.localPosition = joint.localPose.position;
            t.localRotation = joint.localPose.rotation;
        }
    }
}

ARKit 3で取得できるジョイントは約90関節で、対するHumanoid型は訳54関節なので、対応付けをする必要があります。エディタ上で比較しながら対応させるメソッドを書きました。

HumanoidUtils.cs
using UnityEngine;
using UnityEngine.XR.ARFoundation;
using UnityEngine.XR.ARSubsystems;

public static class HumanoidUtils
{
    // 3D joint skeleton
    enum JointIndices
    {
        Invalid = -1,
        Root = 0, // parent: <none> [-1]
        Hips = 1, // parent: Root [0]
        LeftUpLeg = 2, // parent: Hips [1]
        LeftLeg = 3, // parent: LeftUpLeg [2]
        LeftFoot = 4, // parent: LeftLeg [3]
        LeftToes = 5, // parent: LeftFoot [4]
        LeftToesEnd = 6, // parent: LeftToes [5]
        RightUpLeg = 7, // parent: Hips [1]
        RightLeg = 8, // parent: RightUpLeg [7]
        RightFoot = 9, // parent: RightLeg [8]
        RightToes = 10, // parent: RightFoot [9]
        RightToesEnd = 11, // parent: RightToes [10]
        Spine1 = 12, // parent: Hips [1]
        Spine2 = 13, // parent: Spine1 [12]
        Spine3 = 14, // parent: Spine2 [13]
        Spine4 = 15, // parent: Spine3 [14]
        Spine5 = 16, // parent: Spine4 [15]
        Spine6 = 17, // parent: Spine5 [16]
        Spine7 = 18, // parent: Spine6 [17]
        LeftShoulder1 = 19, // parent: Spine7 [18]
        LeftArm = 20, // parent: LeftShoulder1 [19]
        LeftForearm = 21, // parent: LeftArm [20]
        LeftHand = 22, // parent: LeftForearm [21]
        LeftHandIndexStart = 23, // parent: LeftHand [22]
        LeftHandIndex1 = 24, // parent: LeftHandIndexStart [23]
        LeftHandIndex2 = 25, // parent: LeftHandIndex1 [24]
        LeftHandIndex3 = 26, // parent: LeftHandIndex2 [25]
        LeftHandIndexEnd = 27, // parent: LeftHandIndex3 [26]
        LeftHandMidStart = 28, // parent: LeftHand [22]
        LeftHandMid1 = 29, // parent: LeftHandMidStart [28]
        LeftHandMid2 = 30, // parent: LeftHandMid1 [29]
        LeftHandMid3 = 31, // parent: LeftHandMid2 [30]
        LeftHandMidEnd = 32, // parent: LeftHandMid3 [31]
        LeftHandPinkyStart = 33, // parent: LeftHand [22]
        LeftHandPinky1 = 34, // parent: LeftHandPinkyStart [33]
        LeftHandPinky2 = 35, // parent: LeftHandPinky1 [34]
        LeftHandPinky3 = 36, // parent: LeftHandPinky2 [35]
        LeftHandPinkyEnd = 37, // parent: LeftHandPinky3 [36]
        LeftHandRingStart = 38, // parent: LeftHand [22]
        LeftHandRing1 = 39, // parent: LeftHandRingStart [38]
        LeftHandRing2 = 40, // parent: LeftHandRing1 [39]
        LeftHandRing3 = 41, // parent: LeftHandRing2 [40]
        LeftHandRingEnd = 42, // parent: LeftHandRing3 [41]
        LeftHandThumbStart = 43, // parent: LeftHand [22]
        LeftHandThumb1 = 44, // parent: LeftHandThumbStart [43]
        LeftHandThumb2 = 45, // parent: LeftHandThumb1 [44]
        LeftHandThumbEnd = 46, // parent: LeftHandThumb2 [45]
        Neck1 = 47, // parent: Spine7 [18]
        Neck2 = 48, // parent: Neck1 [47]
        Neck3 = 49, // parent: Neck2 [48]
        Neck4 = 50, // parent: Neck3 [49]
        Head = 51, // parent: Neck4 [50]
        Jaw = 52, // parent: Head [51]
        Chin = 53, // parent: Jaw [52]
        LeftEye = 54, // parent: Head [51]
        LeftEyeLowerLid = 55, // parent: LeftEye [54]
        LeftEyeUpperLid = 56, // parent: LeftEye [54]
        LeftEyeball = 57, // parent: LeftEye [54]
        Nose = 58, // parent: Head [51]
        RightEye = 59, // parent: Head [51]
        RightEyeLowerLid = 60, // parent: RightEye [59]
        RightEyeUpperLid = 61, // parent: RightEye [59]
        RightEyeball = 62, // parent: RightEye [59]
        RightShoulder1 = 63, // parent: Spine7 [18]
        RightArm = 64, // parent: RightShoulder1 [63]
        RightForearm = 65, // parent: RightArm [64]
        RightHand = 66, // parent: RightForearm [65]
        RightHandIndexStart = 67, // parent: RightHand [66]
        RightHandIndex1 = 68, // parent: RightHandIndexStart [67]
        RightHandIndex2 = 69, // parent: RightHandIndex1 [68]
        RightHandIndex3 = 70, // parent: RightHandIndex2 [69]
        RightHandIndexEnd = 71, // parent: RightHandIndex3 [70]
        RightHandMidStart = 72, // parent: RightHand [66]
        RightHandMid1 = 73, // parent: RightHandMidStart [72]
        RightHandMid2 = 74, // parent: RightHandMid1 [73]
        RightHandMid3 = 75, // parent: RightHandMid2 [74]
        RightHandMidEnd = 76, // parent: RightHandMid3 [75]
        RightHandPinkyStart = 77, // parent: RightHand [66]
        RightHandPinky1 = 78, // parent: RightHandPinkyStart [77]
        RightHandPinky2 = 79, // parent: RightHandPinky1 [78]
        RightHandPinky3 = 80, // parent: RightHandPinky2 [79]
        RightHandPinkyEnd = 81, // parent: RightHandPinky3 [80]
        RightHandRingStart = 82, // parent: RightHand [66]
        RightHandRing1 = 83, // parent: RightHandRingStart [82]
        RightHandRing2 = 84, // parent: RightHandRing1 [83]
        RightHandRing3 = 85, // parent: RightHandRing2 [84]
        RightHandRingEnd = 86, // parent: RightHandRing3 [85]
        RightHandThumbStart = 87, // parent: RightHand [66]
        RightHandThumb1 = 88, // parent: RightHandThumbStart [87]
        RightHandThumb2 = 89, // parent: RightHandThumb1 [88]
        RightHandThumbEnd = 90, // parent: RightHandThumb2 [89]
    }

    public static XRHumanBodyJoint GetXRHumanBodyJoint(ARHumanBody body, HumanBodyBones bone)
    {
        switch (bone)
        {
            case HumanBodyBones.Hips:
                return body.joints[(int)JointIndices.Hips];
            case HumanBodyBones.LeftUpperLeg:
                return body.joints[(int)JointIndices.LeftUpLeg];
            case HumanBodyBones.RightUpperLeg:
                return body.joints[(int)JointIndices.RightUpLeg];
            case HumanBodyBones.LeftLowerLeg:
                return body.joints[(int)JointIndices.LeftLeg];
            case HumanBodyBones.RightLowerLeg:
                return body.joints[(int)JointIndices.RightLeg];
            case HumanBodyBones.LeftFoot:
                return body.joints[(int)JointIndices.LeftFoot];
            case HumanBodyBones.RightFoot:
                return body.joints[(int)JointIndices.RightFoot];
            case HumanBodyBones.Spine:
                return body.joints[(int)JointIndices.Spine1];
            case HumanBodyBones.Chest:
                return body.joints[(int)JointIndices.Spine6];
            case HumanBodyBones.UpperChest:
                return body.joints[(int)JointIndices.Spine7];
            case HumanBodyBones.Neck:
                return body.joints[(int)JointIndices.Neck1];
            case HumanBodyBones.Head:
                return body.joints[(int)JointIndices.Head];
            case HumanBodyBones.LeftShoulder:
                return body.joints[(int)JointIndices.LeftShoulder1];
            case HumanBodyBones.RightShoulder:
                return body.joints[(int)JointIndices.RightShoulder1];
            case HumanBodyBones.LeftUpperArm:
                return body.joints[(int)JointIndices.LeftArm];
            case HumanBodyBones.RightUpperArm:
                return body.joints[(int)JointIndices.RightArm];
            case HumanBodyBones.LeftLowerArm:
                return body.joints[(int)JointIndices.LeftForearm];
            case HumanBodyBones.RightLowerArm:
                return body.joints[(int)JointIndices.RightForearm];
            case HumanBodyBones.LeftHand:
                return body.joints[(int)JointIndices.LeftHand];
            case HumanBodyBones.RightHand:
                return body.joints[(int)JointIndices.RightHand];
            case HumanBodyBones.LeftToes:
                return body.joints[(int)JointIndices.LeftToes];
            case HumanBodyBones.RightToes:
                return body.joints[(int)JointIndices.RightToes];
            case HumanBodyBones.LeftEye:
                return body.joints[(int)JointIndices.LeftEye];
            case HumanBodyBones.RightEye:
                return body.joints[(int)JointIndices.RightEye];
            case HumanBodyBones.Jaw:
                return body.joints[(int)JointIndices.Jaw];
            case HumanBodyBones.LeftThumbProximal:
                return body.joints[(int)JointIndices.LeftHandThumbStart];
            case HumanBodyBones.LeftThumbIntermediate:
                return body.joints[(int)JointIndices.LeftHandThumb1];
            case HumanBodyBones.LeftThumbDistal:
                return body.joints[(int)JointIndices.LeftHandThumb2];
            case HumanBodyBones.LeftIndexProximal:
                return body.joints[(int)JointIndices.LeftHandIndex1];
            case HumanBodyBones.LeftIndexIntermediate:
                return body.joints[(int)JointIndices.LeftHandIndex2];
            case HumanBodyBones.LeftIndexDistal:
                return body.joints[(int)JointIndices.LeftHandIndex3];
            case HumanBodyBones.LeftMiddleProximal:
                return body.joints[(int)JointIndices.LeftHandMid1];
            case HumanBodyBones.LeftMiddleIntermediate:
                return body.joints[(int)JointIndices.LeftHandMid2];
            case HumanBodyBones.LeftMiddleDistal:
                return body.joints[(int)JointIndices.LeftHandMid3];
            case HumanBodyBones.LeftRingProximal:
                return body.joints[(int)JointIndices.LeftHandRing1];
            case HumanBodyBones.LeftRingIntermediate:
                return body.joints[(int)JointIndices.LeftHandRing2];
            case HumanBodyBones.LeftRingDistal:
                return body.joints[(int)JointIndices.LeftHandRing3];
            case HumanBodyBones.LeftLittleProximal:
                return body.joints[(int)JointIndices.LeftHandPinky1];
            case HumanBodyBones.LeftLittleIntermediate:
                return body.joints[(int)JointIndices.LeftHandPinky2];
            case HumanBodyBones.LeftLittleDistal:
                return body.joints[(int)JointIndices.LeftHandPinky3];
            case HumanBodyBones.RightThumbProximal:
                return body.joints[(int)JointIndices.RightHandThumbStart];
            case HumanBodyBones.RightThumbIntermediate:
                return body.joints[(int)JointIndices.RightHandThumb1];
            case HumanBodyBones.RightThumbDistal:
                return body.joints[(int)JointIndices.RightHandThumb2];
            case HumanBodyBones.RightIndexProximal:
                return body.joints[(int)JointIndices.RightHandIndex1];
            case HumanBodyBones.RightIndexIntermediate:
                return body.joints[(int)JointIndices.RightHandIndex2];
            case HumanBodyBones.RightIndexDistal:
                return body.joints[(int)JointIndices.RightHandIndex3];
            case HumanBodyBones.RightMiddleProximal:
                return body.joints[(int)JointIndices.RightHandMid1];
            case HumanBodyBones.RightMiddleIntermediate:
                return body.joints[(int)JointIndices.RightHandMid2];
            case HumanBodyBones.RightMiddleDistal:
                return body.joints[(int)JointIndices.RightHandMid3];
            case HumanBodyBones.RightRingProximal:
                return body.joints[(int)JointIndices.RightHandRing1];
            case HumanBodyBones.RightRingIntermediate:
                return body.joints[(int)JointIndices.RightHandRing2];
            case HumanBodyBones.RightRingDistal:
                return body.joints[(int)JointIndices.RightHandRing3];
            case HumanBodyBones.RightLittleProximal:
                return body.joints[(int)JointIndices.RightHandPinky1];
            case HumanBodyBones.RightLittleIntermediate:
                return body.joints[(int)JointIndices.RightHandPinky2];
            case HumanBodyBones.RightLittleDistal:
                return body.joints[(int)JointIndices.RightHandPinky3];
            default:
                return body.joints[(int)JointIndices.Invalid];
        }
    }
}

これを動かした結果がこれ。

原因はよく考えたら当たり前で、サンプルに含まれているモデルと、使用したVRMのボーン構造が異なるからです。Animatorで用いるリグ構造は親のボーンを基準として相対姿勢で表現するため、異なるボーン構造のモデルには適用できません。XRHumanBodyJointで取得できる値には、親ボーンからの相対姿勢を取得するLocalPoseの他に、ルートボーンからの相対姿勢を表すAnchorPoseがあるのすが、こちらも各関節の角度が一致しているモデルでないと使用できません。ただし、その差分を計測できれば可能性はありそうでした。

しかし、Issueによるとこの値はARKit 3の生の値ではないらしいです。そのため頑張って対応しても今後変更される可能性があり、あまり分が良い方法ではなさそうです。

参考: Example Rig for 3D Human Skeleton - Unity Forum

そこで、次の方法を試しました。

上手く行った方法

ほぼこのツイートの通りにしたら上手くいきました。

具体的には

  1. Package Managerで「FBX Exporter」をInstall
  2. Assets/Prefabs/Robot/ControlledRobot.prefabの上で右クリック
  3. Convert To FBX Linked Prefabを選択肢、Convertする
  4. 出力されたfbxを選択
  5. インスペクターでRigタブに切り替え、Animation TypeHumanoidに変更
  6. Configureを選択
  7. 大体いい感じに自動で設定してくれているが、数カ所おかしいので以下のように修正(Hierarchy Viewからドラッグアンドドロップで設定しないと親がおかしいと怒られた) スクリーンショット 2019-12-02 0.07.44.png スクリーンショット 2019-12-02 0.07.49.png スクリーンショット 2019-12-02 0.07.55.png
  8. Applyして終了
  9. プレハブしてBoneControllerをアタッチ
  10. Human Body TrackingSkelton Prefabに上記プレハブを設定
  11. 以下のスクリプトをアタッチして完成
HumanoidTracker.cs
using System.IO;
using UnityEngine;
using VRM;

public class HumanoidTracker : MonoBehaviour
{
    private Animator _animator;

    private void Start()
    {
        ImportVRMAsync();
    }

    private void Update()
    {
        var origin = FindObjectOfType<BoneController>()?.GetComponent<Animator>();
        if (_animator == null || origin == null)
        {
            return;
        }

        var originalHandler = new HumanPoseHandler(origin.avatar, origin.transform);
        var targetHandler = new HumanPoseHandler(_animator.avatar, _animator.transform);

        HumanPose humanPose = new HumanPose();
        originalHandler.GetHumanPose(ref humanPose);
        targetHandler.SetHumanPose(ref humanPose);

        _animator.rootPosition = origin.rootPosition;
        _animator.rootRotation = origin.rootRotation;
    }

    private void ImportVRMAsync()
    {
        //VRMファイルのパスを指定します
        var path = $"{Application.streamingAssetsPath}/lyrica_chloma.vrm";

        //ファイルをByte配列に読み込みます
        var bytes = File.ReadAllBytes(path);

        //VRMImporterContextがVRMを読み込む機能を提供します
        var context = new VRMImporterContext();

        // GLB形式でJSONを取得しParseします
        context.ParseGlb(bytes);

        // VRMのメタデータを取得
        var meta = context.ReadMeta(false); //引数をTrueに変えるとサムネイルも読み込みます

        //読み込めたかどうかログにモデル名を出力してみる
        Debug.LogFormat("meta: title:{0}", meta.Title);

        //非同期処理で読み込みます
        context.LoadAsync(_ => OnLoaded(context));
    }

    private void OnLoaded(VRMImporterContext context)
    {
        //読込が完了するとcontext.RootにモデルのGameObjectが入っています
        var root = context.Root;

        _animator = root.GetComponent<Animator>();
        root.transform.position = new Vector3(0, -1, 1);
        root.transform.rotation = Quaternion.Euler(0, 180f, 0);
        _animator.applyRootMotion = true;

        //メッシュを表示します
        context.ShowMeshes();
    }
}

参考: UniVRMを使ってVRMモデルをランタイムロードする方法

やっていることはさっきよりシンプルです。サンプルに含まれているモデルをHumanoid型として扱えるようになったので、Humanoid型としてVRMに値を流し込んでいます。その際にHumanPoseHandlerを経由する必要がある点に注意してください。

private void Update()
{
    var origin = FindObjectOfType<BoneController>()?.GetComponent<Animator>();
    if (_animator == null || origin == null)
    {
        return;
    }

    var originalHandler = new HumanPoseHandler(origin.avatar, origin.transform);
    var targetHandler = new HumanPoseHandler(_animator.avatar, _animator.transform);

    HumanPose humanPose = new HumanPose();
    originalHandler.GetHumanPose(ref humanPose);
    targetHandler.SetHumanPose(ref humanPose);

    _animator.rootPosition = origin.rootPosition;
    _animator.rootRotation = origin.rootRotation;
}

参考: ランタイムでAvatarを生成してアニメーションに利用する - e.blog

上記の実装をした結果がこんな感じです。

大分いい感じですね。ルート位置をロボットモデルと同じように変更する方法が分かってないので、わかる人がいたら教えて欲しいです。

最後に

いろいろ遠回りをしてしまいましたが、Humanoid型は異なる階層構造の人型モデルを同じように扱えることが分かりました。現状だと精度の面や使いやすさの面で難はありますが、デファクトの機能でモーキャプが簡単に扱えるのは面白いですね。

明日以降のVTuber Tech #1 Advent Calendar 2019も是非お楽しみに!

nkjzm
特に明示されていない場合、記事中のソースコードはパブリックドメインです。 月額制のメンターサービスで初心者向けの開発サポートをしているので、分からないことがあれば是非こちらで質問してください! → https://menta.work/plan/1115
https://nkjzm.github.io/
unity-game-dev-guild
趣味・仕事問わずUnityでゲームを作っている開発者のみで構成されるオンラインコミュニティです。Unityでゲームを開発・運用するにあたって必要なあらゆる知見を共有することを目的とします。
https://unity-game-dev-guild.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした