Azure Kinectでキャラクターを動かしてみた

# 概要

Azure Kinect DK と Body Tracking SDK を使ってVRMのキャラクターを動かしてみました。

# 成果物

Kinectからの生データをHumanoidのボーンに変換して動かしてます。（Final IK とかは無し）
![SnapCrab_KinectVTuber - SampleScene - PC Mac & Linux Standalone - Unity 201937f1 Personal DX11_2020-5-30_15-46-5_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109303/b84e381d-5411-5215-ced4-d00ba59473be.png)

# Body Tracking SDK って何？

Body Tracking SDK は Azure Kinect DKでボディトラッキングを行うためのパッケージです。
ちなみに、Azure Kinectには2つのSDKが存在して用途が違うようです。

- [Azure Kinect Sensor SDK](https://www.nuget.org/packages/Microsoft.Azure.Kinect.Sensor/)： カメラ(深度,RGB)・モーションセンサー・メタデータへのアクセス, 同期制御を行う
- [Azure Kinect Body Tracking SDK](https://www.nuget.org/packages/Microsoft.Azure.Kinect.BodyTracking/): ボディトラッキングを行う


# それでは作ってみましょ

### 1. SDKのインストールなど
https://docs.microsoft.com/ja-jp/azure/Kinect-dk/body-sdk-setup

### 2. Kinectから生データを取る
今回は、下のサンプルコードを参考にしてKinectのボーンデータをUnityの形式に変換させます。
https://github.com/microsoft/Azure-Kinect-Samples/tree/master/body-tracking-samples/sample_unity_bodytracking

SDKからQuaternionを取得するコードは複雑なので、
今回はサンプルコードの[Assets以下](https://github.com/microsoft/Azure-Kinect-Samples/tree/master/body-tracking-samples/sample_unity_bodytracking/Assets)を全てコピーしましょう。

`Assets/Scenes/Kinect4AzureSampleScene.unity`を開くと棒人間が現れるかと思います。
![SnapCrab_NoName_2020-5-30_16-49-17_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109303/d7fe3b2f-d977-3530-a69f-663d2404b0bf.png)



### 3. 生データからHumanoidのボーンに変換する

サンプルコードの`TrackerHandlerクラス`で人型のボーンが取れるので、
Humanoid のアニメーションデータに変換していきます。

スクリプト下方の `Dictionary<HumanBodyBones, JointId>` を返す定数値で、
Body Tracking SDKのボーン値 と Humanoidのボーン値 のマップを作っています。

さらに、手に関しては外れ値を引いて骨折してしまうため可動域を設けています。

```C#:KinectHumanoid.cs
using System.Collections.Generic;
using Microsoft.Azure.Kinect.BodyTracking;
using UnityEngine;

public class KinectHumanoid : MonoBehaviour
{
    [SerializeField] private TrackerHandler trackerHandler;
    [SerializeField] private Animator animator;
    [SerializeField] private GameObject pelvis;
    
    void Update()
    {
        MoveBody();
    }
    
    private void MoveBody()
    {
        transform.localPosition = pelvis.transform.localPosition;
        transform.localRotation = pelvis.transform.localRotation;

        upperBodyBoneMap.TryForeach(bone =>
        {
            var nextRot = trackerHandler.GetRelativeJointRotation(bone.Value);
            var prevRot = animator.GetBoneTransform(bone.Key).localRotation;
            animator.GetBoneTransform(bone.Key).localRotation = new Quaternion(nextRot.x, nextRot.y, nextRot.z, nextRot.w);
        });
        
        lowerBodyBoneMap.TryForeach(bone =>
        {
            var nextRot = trackerHandler.GetRelativeJointRotation(bone.Value);
            var prevRot = animator.GetBoneTransform(bone.Key).localRotation;
            animator.GetBoneTransform(bone.Key).localRotation = new Quaternion(nextRot.x, nextRot.y, nextRot.z, nextRot.w);
        });
        
        leftHandBoneMap.TryForeach(bone =>
        {
            var nextRotZ = trackerHandler.GetRelativeJointRotation(bone.Value).eulerAngles.z;
            var nextZ = nextRotZ <= 0 ? 0 : nextRotZ > 70 ? 70 : nextRotZ;
            var nextRotVec = new Vector3(0, 0, nextZ);
            var nextRot = Quaternion.Euler(nextRotVec);
            animator.GetBoneTransform(bone.Key).localRotation = new Quaternion(nextRot.x, nextRot.y, nextRot.z, nextRot.w);
        });
        
        rightHandBoneMap.TryForeach(bone =>
        {
            var nextRotZ = trackerHandler.GetRelativeJointRotation(bone.Value).eulerAngles.z;
            var nextZ = 0 >= nextRotZ ? 0 : nextRotZ >= 360 ? 360 : nextRotZ < 290 ? 290 : nextRotZ;
            var nextRotVec = new Vector3(0, 0, nextZ);
            var nextRot = Quaternion.Euler(nextRotVec);
            animator.GetBoneTransform(bone.Key).localRotation = new Quaternion(nextRot.x, nextRot.y, nextRot.z, nextRot.w);
        });
        
        leftThumbBoneMap.TryForeach(bone =>
        {
            var nextRotX = trackerHandler.GetRelativeJointRotation(bone.Value).eulerAngles.x;
            var nextX = nextRotX <= 0 ? 0 : nextRotX > 70 ? 70 : nextRotX;
            var nextRotVec = new Vector3(nextX, 0, 0);
            var nextRot = Quaternion.Euler(nextRotVec);
            animator.GetBoneTransform(bone.Key).localRotation = new Quaternion(nextRot.x, nextRot.y, nextRot.z, nextRot.w);
        });
        
        rightThumbBoneMap.TryForeach(bone =>
        {
            var nextRotX = trackerHandler.GetRelativeJointRotation(bone.Value).eulerAngles.x;
            var nextX = 0 >= nextRotX ? 0 : nextRotX >= 360 ? -360 : nextRotX < 290 ? -290 : -nextRotX;
            var nextRotVec = new Vector3(nextX, 0, 0);
            var nextRot = Quaternion.Euler(nextRotVec);
            animator.GetBoneTransform(bone.Key).localRotation = new Quaternion(nextRot.x, nextRot.y, nextRot.z, nextRot.w);
        });
    }
    
    private readonly Dictionary<HumanBodyBones, JointId> upperBodyBoneMap =
        new Dictionary<HumanBodyBones, JointId>
        {
            {HumanBodyBones.Head, JointId.Head},
            {HumanBodyBones.Neck, JointId.Neck},
            {HumanBodyBones.Chest, JointId.SpineChest},
            {HumanBodyBones.Spine, JointId.SpineNavel},
            {HumanBodyBones.LeftShoulder, JointId.ClavicleLeft},
            {HumanBodyBones.RightShoulder, JointId.ClavicleRight},
            {HumanBodyBones.LeftUpperArm, JointId.ShoulderLeft},
            {HumanBodyBones.RightUpperArm, JointId.ShoulderRight},
            {HumanBodyBones.LeftLowerArm, JointId.ElbowLeft},
            {HumanBodyBones.RightLowerArm, JointId.ElbowRight},
            {HumanBodyBones.LeftHand, JointId.WristLeft},
            {HumanBodyBones.RightHand, JointId.WristRight},
        };
    
    private readonly Dictionary<HumanBodyBones, JointId> lowerBodyBoneMap =
        new Dictionary<HumanBodyBones, JointId>
        {
            {HumanBodyBones.Hips, JointId.SpineNavel},
            {HumanBodyBones.LeftUpperLeg, JointId.HipLeft},
            {HumanBodyBones.RightUpperLeg, JointId.HipRight},
            {HumanBodyBones.LeftLowerLeg, JointId.KneeLeft},
            {HumanBodyBones.RightLowerLeg, JointId.KneeRight},
            {HumanBodyBones.LeftFoot, JointId.FootLeft},
            {HumanBodyBones.RightFoot, JointId.FootRight},
            {HumanBodyBones.LeftToes, JointId.FootLeft},
            {HumanBodyBones.RightToes, JointId.FootRight},
        };

    private readonly Dictionary<HumanBodyBones, JointId> leftHandBoneMap =
        new Dictionary<HumanBodyBones, JointId>
        {
            {HumanBodyBones.LeftIndexProximal, JointId.HandLeft},
            {HumanBodyBones.LeftIndexIntermediate, JointId.HandLeft},
            {HumanBodyBones.LeftIndexDistal, JointId.HandLeft},
            {HumanBodyBones.LeftMiddleProximal, JointId.HandLeft},
            {HumanBodyBones.LeftMiddleIntermediate, JointId.HandLeft},
            {HumanBodyBones.LeftMiddleDistal, JointId.HandLeft},
            {HumanBodyBones.LeftRingProximal, JointId.HandLeft},
            {HumanBodyBones.LeftRingIntermediate, JointId.HandLeft},
            {HumanBodyBones.LeftRingDistal, JointId.HandLeft},
            {HumanBodyBones.LeftLittleProximal, JointId.HandLeft},
            {HumanBodyBones.LeftLittleIntermediate, JointId.HandLeft},
            {HumanBodyBones.LeftLittleDistal, JointId.HandLeft},
        };

    private readonly Dictionary<HumanBodyBones, JointId> rightHandBoneMap =
        new Dictionary<HumanBodyBones, JointId>
        {
            {HumanBodyBones.RightIndexProximal, JointId.HandRight},
            {HumanBodyBones.RightIndexIntermediate, JointId.HandRight},
            {HumanBodyBones.RightIndexDistal, JointId.HandRight},
            {HumanBodyBones.RightMiddleProximal, JointId.HandRight},
            {HumanBodyBones.RightMiddleIntermediate, JointId.HandRight},
            {HumanBodyBones.RightMiddleDistal, JointId.HandRight},
            {HumanBodyBones.RightRingProximal, JointId.HandRight},
            {HumanBodyBones.RightRingIntermediate, JointId.HandRight},
            {HumanBodyBones.RightRingDistal, JointId.HandRight},
            {HumanBodyBones.RightLittleProximal, JointId.HandRight},
            {HumanBodyBones.RightLittleIntermediate, JointId.HandRight},
            {HumanBodyBones.RightLittleDistal, JointId.HandRight},
        };
    
    private readonly Dictionary<HumanBodyBones, JointId> leftThumbBoneMap =
        new Dictionary<HumanBodyBones, JointId>
        {
            {HumanBodyBones.LeftThumbProximal, JointId.ThumbLeft},
            {HumanBodyBones.LeftThumbIntermediate, JointId.ThumbLeft},
            {HumanBodyBones.LeftThumbDistal, JointId.ThumbLeft},
        };
    
    private readonly Dictionary<HumanBodyBones, JointId> rightThumbBoneMap =
        new Dictionary<HumanBodyBones, JointId>
        {
            {HumanBodyBones.RightThumbProximal, JointId.ThumbRight},
            {HumanBodyBones.RightThumbIntermediate, JointId.ThumbRight},
            {HumanBodyBones.RightThumbDistal, JointId.ThumbRight},
        };

    /* NOT ASSIGN */
    // {HumanBodyBones.UpperChest, JointId.SpineChest},
    // {HumanBodyBones.LeftEye, JointId.EyeLeft},
    // {HumanBodyBones.RightEye, JointId.EyeRight},
    // {HumanBodyBones.Jaw, JointId.Nose},
}
```
アサインできなかったボーンが少しありますが、なくても動きはしました。


Arrayの便利関数も用意。

```C#:ArrayExtensions.cs
public static class ArrayExtensions
{
    public static List<Exception> TryForeach<T>(this ICollection<T> array, Action<T> block)
    {
        var ex = new List<Exception>();
        foreach (var a in array)
        {
            try
            {
                block(a);
            }
            catch (Exception exception)
            {
                ex.Add(exception);
            }
        }
        return ex;
    }
}
```

出来たコンポーネントは、VRMのキャラクターにつけて各オブジェクトをアタッチしてください。
![SnapCrab_NoName_2020-5-30_17-7-44_No-00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109303/b0c64d59-d1d2-8f70-f852-b57a2f0f4ef6.png)


# まとめ

トラッキングが外れて関節が捻じ曲がるときはありますが、結構感動しますね！
Azure Kinect DKでVtuberしたい人の役に立ったら嬉しいです（^^）