本トピックスではHumanoidの顔を動かしてはいません。 顔も動かしたい場合は 「 Kinect V2 で骨格と顔を認識してUnityの人型キャラクタをリアルタイムで動かす 」 を参照して下さい。
目次へ上記のzipファイルを展開して、フォルダ名を CheckNtKinectDll6/ と変更します。
上のメニュー -> File -> New Scene
上のメニュー -> File -> Save Scene as ... -> humanoid.scene
ここでは 「 MakeHuman: Unity5 で使用する人型モデルを作成する 」 で作成したデータ makehuman.zip を用います。
上記ファイルを展開するとexports/の下に AsianBoy.fbx と textures/ があるはずです。 これを Assets/Models/ の下にimportします。[注意]上の操作は
Assets -> Import New Asset... -> AsianBody.fbxから行なってもよいのですが、これだとAsianBoy.fbx に必要なtextureが 自動ではimportされず、モデルが真っ白になってしまいます。 この場合は Assets/Models/Materials/に生成された白いMaterialに対応する Textureを手動でimportしなくてはいけません。
必須なBoneが割り当てられていないと "Configure" にチェックがつかないのでわかりますが、 本プロジェクトでは Optional な Bone も利用しています。 Neck と UpperChest にも Bone が割り当てられていることを確認して下さい。
RigBone クラスは Humanoid と Boneが与えられると、 Humanoid に付加されている Animator コンポーネントから Bone の transform を取り出して管理するクラスです。
RigBone.cs |
/* * Copyright (c) 2017 Yoshihisa Nitta * Released under the MIT license * http://opensource.org/licenses/mit-license.php */ /* http://nw.tsuda.ac.jp/lec/unity5/ */ /* version 1.1: 2017/08/05 */ /* version 1.0: 2017/08/02 */ using System.Collections; using System.Collections.Generic; using UnityEngine; public class RigBone { public GameObject gameObject; public HumanBodyBones bone; public bool isValid; public Transform transform { get { return animator.GetBoneTransform(bone); } } Animator animator; Quaternion savedValue; public RigBone(GameObject g, HumanBodyBones b) { gameObject = g; bone = b; isValid = false; animator = gameObject.GetComponent<Animator>(); if (animator == null) { Debug.Log("no Animator Component"); return; } Avatar avatar = animator.avatar; if (avatar == null || !avatar.isHuman || !avatar.isValid) { Debug.Log("Avatar is not Humanoid or it is not valid"); return; } isValid = true; savedValue = animator.GetBoneTransform(bone).localRotation; } public void set(float a, float x, float y, float z) { set(Quaternion.AngleAxis(a, new Vector3(x,y,z))); } public void set(Quaternion q) { animator.GetBoneTransform(bone).localRotation = q; savedValue = q; } public void mul(float a, float x, float y, float z) { mul(Quaternion.AngleAxis(a, new Vector3(x,y,z))); } public void mul(Quaternion q) { Transform tr = animator.GetBoneTransform(bone); tr.localRotation = q * tr.localRotation; } public void offset(float a, float x, float y, float z) { offset(Quaternion.AngleAxis(a, new Vector3(x,y,z))); } public void offset(Quaternion q) { animator.GetBoneTransform(bone).localRotation = q * savedValue; } public void changeBone(HumanBodyBones b) { bone = b; savedValue = animator.GetBoneTransform(bone).localRotation; } } |
CharacterSkeleton クラスは、一人分の Humanoid のデータを管理し、 与えられた関節データを用いてポーズを変化させるクラスです。
set()メソッドでは、Kinect V2 で取得した関節データが渡されます。 最大で6人分の関節データがまとめて渡されてくるので、 現在着目している骨格のインデックスが第3引数 offset で 0 から 5 までの整数で指定されます。
人間の体の向きをHumanoid の向きに反映するように変更しています。 人間の骨盤の向き(y軸回りの回転)がHumanoid 全体の向きに、 人間の両肩の向きがHumanoid の上半身の向きになります。
CharacterSkeleton.cs |
/* * Copyright (c) 2017 Yoshihisa Nitta * Released under the MIT license * http://opensource.org/licenses/mit-license.php */ /* http://nw.tsuda.ac.jp/lec/kinect2/ */ /* version 1.3: 2017/08/11 */ /* version 1.2: 2017/08/10 */ /* version 1.1: 2017/08/07 */ /* version 1.0: 2017/08/06 */ using UnityEngine; using System.Collections; using System.Collections.Generic; class CharacterSkeleton { public const int // JointType JointType_SpineBase= 0, JointType_SpineMid= 1, JointType_Neck= 2, JointType_Head= 3, JointType_ShoulderLeft= 4, JointType_ElbowLeft= 5, JointType_WristLeft= 6, JointType_HandLeft= 7, JointType_ShoulderRight= 8, JointType_ElbowRight= 9, JointType_WristRight= 10, JointType_HandRight= 11, JointType_HipLeft= 12, JointType_KneeLeft= 13, JointType_AnkleLeft= 14, JointType_FootLeft= 15, JointType_HipRight= 16, JointType_KneeRight= 17, JointType_AnkleRight= 18, JointType_FootRight= 19, JointType_SpineShoulder= 20, JointType_HandTipLeft= 21, JointType_ThumbLeft= 22, JointType_HandTipRight= 23, JointType_ThumbRight= 24, // TrackingState TrackingState_NotTracked= 0, TrackingState_Inferred= 1, TrackingState_Tracked= 2, // Number bodyCount = 6, jointCount = 25; private static int[] jointSegment = new int[] { JointType_SpineBase, JointType_SpineMid, // Spine JointType_Neck, JointType_Head, // Neck // left JointType_ShoulderLeft, JointType_ElbowLeft, // LeftUpperArm JointType_ElbowLeft, JointType_WristLeft, // LeftLowerArm JointType_WristLeft, JointType_HandLeft, // LeftHand JointType_HipLeft, JointType_KneeLeft, // LeftUpperLeg JointType_KneeLeft, JointType_AnkleLeft, // LeftLowerLeg6 JointType_AnkleLeft, JointType_FootLeft, // LeftFoot // right JointType_ShoulderRight, JointType_ElbowRight, // RightUpperArm JointType_ElbowRight, JointType_WristRight, // RightLowerArm JointType_WristRight, JointType_HandRight, // RightHand JointType_HipRight, JointType_KneeRight, // RightUpperLeg JointType_KneeRight, JointType_AnkleRight, // RightLowerLeg JointType_AnkleRight, JointType_FootRight, // RightFoot }; public Vector3[] joint = new Vector3[jointCount]; public int[] jointState = new int[jointCount]; Dictionary<HumanBodyBones,Vector3> trackingSegment = null; Dictionary<HumanBodyBones, int> trackingState = null; private static HumanBodyBones[] humanBone = new HumanBodyBones[] { HumanBodyBones.Hips, HumanBodyBones.Spine, HumanBodyBones.UpperChest, HumanBodyBones.Neck, HumanBodyBones.Head, HumanBodyBones.LeftUpperArm, HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftHand, HumanBodyBones.LeftUpperLeg, HumanBodyBones.LeftLowerLeg, HumanBodyBones.LeftFoot, HumanBodyBones.RightUpperArm, HumanBodyBones.RightLowerArm, HumanBodyBones.RightHand, HumanBodyBones.RightUpperLeg, HumanBodyBones.RightLowerLeg, HumanBodyBones.RightFoot, }; private static HumanBodyBones[] targetBone = new HumanBodyBones[] { HumanBodyBones.Spine, HumanBodyBones.Neck, HumanBodyBones.LeftUpperArm, HumanBodyBones.LeftLowerArm, HumanBodyBones.LeftHand, HumanBodyBones.LeftUpperLeg, HumanBodyBones.LeftLowerLeg, HumanBodyBones.LeftFoot, HumanBodyBones.RightUpperArm, HumanBodyBones.RightLowerArm, HumanBodyBones.RightHand, HumanBodyBones.RightUpperLeg, HumanBodyBones.RightLowerLeg, HumanBodyBones.RightFoot, }; public GameObject humanoid; private Dictionary<HumanBodyBones, RigBone> rigBone = null; private bool isSavedPosition = false; private Vector3 savedPosition; private Quaternion savedHumanoidRotation; public CharacterSkeleton(GameObject h) { humanoid = h; rigBone = new Dictionary<HumanBodyBones, RigBone>(); foreach (HumanBodyBones bone in humanBone) { rigBone[bone] = new RigBone(humanoid,bone); } savedHumanoidRotation = humanoid.transform.rotation; trackingSegment = new Dictionary<HumanBodyBones,Vector3>(targetBone.Length); trackingState = new Dictionary<HumanBodyBones, int>(targetBone.Length); } private void swapJoint(int a, int b) { Vector3 tmp = joint[a]; joint[a] = joint[b]; joint[b] = tmp; int t = jointState[a]; jointState[a] = jointState[b]; jointState[b] = t; } public void set(float[] jt, int[] st, int offset, bool mirrored, bool move) { if (isSavedPosition == false && jointState[JointType_SpineBase] != TrackingState_NotTracked) { isSavedPosition = true; int j = offset * jointCount + JointType_SpineBase; savedPosition = new Vector3(jt[j*3],jt[j*3+1],jt[j*3+2]); } for (int i=0; i<jointCount; i++) { int j = offset * jointCount + i; if (mirrored) { joint[i] = new Vector3(-jt[j*3], jt[j*3+1], -jt[j*3+2]); } else { joint[i] = new Vector3(jt[j*3], jt[j*3+1], savedPosition.z*2-jt[j*3+2]); } jointState[i] = st[j]; } if (mirrored) { swapJoint(JointType_ShoulderLeft, JointType_ShoulderRight); swapJoint(JointType_ElbowLeft, JointType_ElbowRight); swapJoint(JointType_WristLeft, JointType_WristRight); swapJoint(JointType_HandLeft, JointType_HandRight); swapJoint(JointType_HipLeft, JointType_HipRight); swapJoint(JointType_KneeLeft, JointType_KneeRight); swapJoint(JointType_AnkleLeft, JointType_AnkleRight); swapJoint(JointType_FootLeft, JointType_FootRight); swapJoint(JointType_HandTipLeft, JointType_HandTipRight); swapJoint(JointType_ThumbLeft, JointType_ThumbRight); } for (int i=0; i<targetBone.Length; i++) { int s = jointSegment[2*i], e = jointSegment[2*i+1]; trackingSegment[targetBone[i]] = joint[e] - joint[s]; trackingState[targetBone[i]] = System.Math.Min(jointState[e],jointState[s]); } Vector3 waist = joint[JointType_HipRight] - joint[JointType_HipLeft]; waist = new Vector3(waist.x, 0, waist.z); Quaternion rot = Quaternion.FromToRotation(Vector3.right,waist); Quaternion rotInv = Quaternion.Inverse(rot); Vector3 shoulder = joint[JointType_ShoulderRight] - joint[JointType_ShoulderLeft]; shoulder = new Vector3(shoulder.x, 0, shoulder.z); Quaternion srot = Quaternion.FromToRotation(Vector3.right,shoulder); Quaternion srotInv = Quaternion.Inverse(srot); humanoid.transform.rotation = Quaternion.identity; foreach (HumanBodyBones bone in targetBone) { rigBone[bone].transform.rotation = rotInv * Quaternion.FromToRotation(Vector3.up,trackingSegment[bone]); } rigBone[HumanBodyBones.UpperChest].offset(srot); Quaternion bodyRot = rot; if (mirrored) { bodyRot = Quaternion.AngleAxis(180,Vector3.up) * bodyRot; } humanoid.transform.rotation = bodyRot; if (move == true) { Vector3 m = joint[JointType_SpineBase]; if (mirrored) m = new Vector3(-m.x, m.y, -m.z); humanoid.transform.position = m; } } } |
RigControl.cs |
/* * Copyright (c) 2017 Yoshihisa Nitta * Released under the MIT license * http://opensource.org/licenses/mit-license.php */ /* http://nw.tsuda.ac.jp/lec/kinect2/ */ /* version 1.0: 2017/08/06 */ using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Runtime.InteropServices; public class RigControl : MonoBehaviour { [DllImport ("NtKinectDll")] private static extern System.IntPtr getKinect(); [DllImport ("NtKinectDll")] private static extern int setSkeleton(System.IntPtr kinect, System.IntPtr data, System.IntPtr state, System.IntPtr id); int bodyCount = 6; int jointCount = 25; private System.IntPtr kinect; public GameObject humanoid; public bool mirror = true; public bool move = true; CharacterSkeleton skeleton; void Start () { kinect = getKinect(); skeleton = new CharacterSkeleton(humanoid); } void Update () { float[] data = new float[bodyCount * jointCount * 3]; int[] state = new int[bodyCount * jointCount]; int[] id = new int[bodyCount]; GCHandle gch = GCHandle.Alloc(data,GCHandleType.Pinned); GCHandle gch2 = GCHandle.Alloc(state,GCHandleType.Pinned); GCHandle gch3 = GCHandle.Alloc(id,GCHandleType.Pinned); int n = setSkeleton(kinect,gch.AddrOfPinnedObject(),gch2.AddrOfPinnedObject(),gch3.AddrOfPinnedObject()); gch.Free(); gch2.Free(); gch3.Free(); if (n > 0) { skeleton.set(data,state,0,mirror,move); } } } |
Transform Position (x,y,z) = (0, 1, -2)
RigController に付加されている "Rig Control (Script)" コンポーネントの mirrored がチェックされていると、 人間の鏡像の動きに追随します。 mirrored のチェックをはずすと Humanoid はカメラに背を向けて、人間の動きに そのまま追随するようになります。 (注)実行中のUnityのウィンドウの右に小さく表示されているKinect V2の認識中の画面は すでに鏡像になったものです。
Humanoid の transform.rotation の値をプログラムで変更するので、 AsianBoy の Transform の Rotation の初期値はなんであっても構いません。
実行中のキャプチャ画面はこちら CheckNtKinectDll6d.mp4。実行の途中で mirrored のチェックをはずしています。
また、moveのチェックをはずすと人間の場所の移動には追随しなくなります。
[注意] 骨格や顔の認識状態を表示するためにDLL内でOpenCVのウィンドウを生成しています。 後から生成されたこのウィンドウにフォーカスがあるときは (= Unityのウィンドウにフォーカスがない場合は) Unityの画面は変化しないので注意して下さい。 Unityのウィンドウの上部をクリックしてUnityのウィンドウにフォーカスが ある状態で動作を試して下さい。
[注意2] CharacterSkeleton.cs の中でオプショナルなBoneである HumanBodyBones.Neck および HumanBodyBones.UpperChest にアクセスしています。 もしもお使いのHumanoidデータでこのBoneが設定できていない場合は、エラーとなります。
[注意3] このプログラムでは顔の向きが変化しません。 顔認識をして得られた顔の向きを HumanBodyBones.Head の向きに反映させた方がよいと思われますが、 ここでは説明を簡単にするために省略しています。
6人までの Humanoid を同時に動かすプロジェクトを作成します。
File -> Save Scence as -> humanoid2.unity
ここでは makehuman で作成した次のデータ makehuman2.zipを使います。 AfricanBoy, AfricanGirl, AsianGirl, CaucasianBoy, CaucasianGirl および texture/ をそれぞれimport して、Animation Type を Humanoid に変更します。
Position や Rotation をそれぞれ次のように設定します。 Position の Z 座標を -10 として、カメラに写らなくしているだけです。
Model Name | Position | Rotation | ||||
---|---|---|---|---|---|---|
X | Y | Z | X | Y | Z | |
AsianBoy | 0 | 0 | -10 | 0 | 0 | 0 |
AsianGirl | 0 | 0 | -10 | 0 | 0 | 0 |
AfricanBoy | 0 | 0 | -10 | 0 | 0 | 0 |
AfricanGirl | 0 | 0 | -10 | 0 | 0 | 0 |
CaucasianBoy | 0 | 0 | -10 | 0 | 0 | 0 |
CaucasianGirl | 0 | 0 | -10 | 0 | 0 | 0 |
RigControl2.cs |
/* * Copyright (c) 2017 Yoshihisa Nitta * Released under the MIT license * http://opensource.org/licenses/mit-license.php */ /* http://nw.tsuda.ac.jp/lec/kinect2/ */ /* version 1.1: 2017/08/10 */ /* version 1.0: 2017/08/06 */ using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Runtime.InteropServices; public class RigControl2 : MonoBehaviour { [DllImport ("NtKinectDll")] private static extern System.IntPtr getKinect(); [DllImport ("NtKinectDll")] private static extern int setSkeleton(System.IntPtr kinect, System.IntPtr data, System.IntPtr state, System.IntPtr id); int bodyCount = 6; int jointCount = 25; private System.IntPtr kinect; public GameObject[] humanoid = new GameObject[] {null, null, null, null, null, null}; public bool[] mirror = new bool[] {true, true, true, true, true, true}; public bool[] move = new bool[] {true, true, true, true, true, true}; CharacterSkeleton[] skeleton = new CharacterSkeleton[] {null, null, null, null, null, null}; void Start () { kinect = getKinect(); for (int i=0; i<bodyCount; i++) { if (humanoid[i] != null) { skeleton[i] = new CharacterSkeleton(humanoid[i]); } } } void Update () { float[] data = new float[bodyCount * jointCount * 3]; int[] state = new int[bodyCount * jointCount]; int[] id = new int[bodyCount]; GCHandle gch = GCHandle.Alloc(data,GCHandleType.Pinned); GCHandle gch2 = GCHandle.Alloc(state,GCHandleType.Pinned); GCHandle gch3 = GCHandle.Alloc(id,GCHandleType.Pinned); int n = setSkeleton(kinect,gch.AddrOfPinnedObject(),gch2.AddrOfPinnedObject(),gch3.AddrOfPinnedObject()); gch.Free(); gch2.Free(); gch3.Free(); for (int i=0; i<bodyCount; i++) { if (i < n && skeleton[i] != null) { skeleton[i].set(data,state,i,mirror[i],move[i]); } else { humanoid[i].transform.position = new Vector3(0,0,-10); } } } } |