このチュートリアルでは、Unity ユーザーのスケルトンデータを使用して、アバターに生気を与える方法を紹介します。このためには、Nuitrack SDK とサポートしているセンサー、そしてオプションでモバイル端末が必要です。
ユーザーのスケルトンとモデルとなるスケルトンをマッピングすることで、アバターに生気を与えることができます。マッピングの方法は、ダイレクト マップ方式とインダイレクト マップ方式の二種類です。
インダイレクト マップ方式の場合、人間の姿をしたモデル、動物の姿をしたモデルの両方が利用できます。しかし、インダイレクト マップ方式は、十分な深さの没入感を提供していないので、バーチャルの体が別々に動いているような印象を受けるかもしれません。ユーザーの動きを遅れて再現するだけのアバターを実行しているような状態です。
ダイレクト マップ方式は、人間の姿をした/擬人化されたモデルに、より適しています。このマップ方式は、モデルの体系がユーザーの体系と全く同じであるため、動きが全く同じになるので、ゲームのより深い没入感が期待できます。しかし、ダイレクト マップ方式にも重要なデメリットがあります。それは、ユーザーとモデルの体型が大幅に異なる場合に生じるモデルの変形が生じるというものです。
ダイレクト マップ方式とインダイレクト マップ方式の違いを端的に公式化するとこうなります。
- インダイレクト マップ方式 = 関節の方向
- ダイレクト マップ方式 = 関節の方向 + 関節の位置 + 関節の拡大/縮小
まずは、より簡単な方法であるインダイレクト マップ方式を使ってアバターに生気を与え、その後、ゲームにより深い没入感を与えたい場合に使用できるダイレクト マップ方式に移行します。
このプロジェクトを作成するために必要なものは以下の通りです。
- Nuitrack Runtime と Nuitrack SDK
- サポートされているセンサー (サポートしているセンサーの一覧は、Nuitrack Webサイトを参照)
- Unity 2019.2.11f 以上
完成済みプロジェクトは Nuitrack SDK: [Unity 3D] > [NuitrackSDK.unitypackage] > [Tutorials] > [Avatar Animation]にあります。
- 注意
- error CS0234: The type or namespace name 'Policy' does not exist in the namespace 'System.Security' (are you missing an assembly reference?)
ビルド後に上記のエラーが出る場合は、エラーが出ているスクリプト内の System.Security.Policy の頭に // を付けてコメントアウトさせてください。
インダイレクト マップ方式
-
Nuitrack での最大関節数が 19 なので、より複雑なスケルトンを持つモデルでは調整が難しいので、ここでは、人間の姿をしたモデルを使用します。Unity Asset Store から人型のモデル (例えば Unity Chan Model) をダウンロードして、プログラムにインポートします。T のポーズ (両手を横に水平に伸ばした状態) にして、関節の位置が正しくマッチングされるようにします。
- 注意
- Plugins と Nuitrack の両フォルダーを Nuitrack SDK からインポートすることで、センサーと共に動作し、センサーが得たユーザーに関する情報を取得できます。NuitrackScripts プレハブをシーンにドラッグ アンド ドロップします。
-
空の C# スクリプト (RiggedAvatar.cs) を作成し、モデルにドラッグ アンド ドロップします。
まず、センサーの前のユーザーを認識している (表示されている) ことを確認します。
public class RiggedAvatar : MonoBehaviour
{
void Update()
{
if (CurrentUserTracker.CurrentSkeleton != null) ProcessSkeleton(CurrentUserTracker.CurrentSkeleton);
}
}
-
ユーザーが認識されていれば、モデルの位置の計算に移ります。インダイレクト マップ方式では、モデルの位置の計算には、胴体の関節位置が使用されます。y軸に沿って 180° 位置が回転するか、モデルが逆の方向に回転します。
if (CurrentUserTracker.CurrentSkeleton != null) ProcessSkeleton(CurrentUserTracker.CurrentSkeleton);
}
void ProcessSkeleton(nuitrack.Skeleton skeleton)
{
Vector3 torsoPos = Quaternion.Euler(0f, 180f, 0f) * (0.001f * skeleton.GetJoint(nuitrack.JointType.Torso).ToVector3());
transform.position = torsoPos;
}
}
-
プロジェクトのビルドを作成します。詳細は、「Nuitrack SDK を使って、初めての Unity プロジェクトを作成 (Android の)」チュートリアルの「ビルドのセットアップ」セクションをご覧ください。設定に間違いがなければ、モデルがユーザーの動きに合わせて動きます。この段階では、モデルの手足は回転しません。
-
空の C# スクリプト (ModelJoint.cs) を作成し、モデルの骨の処理に使用する ModelJoint クラスを定義します。[System.Serializable] として登録すると、[Inspector]タブに表示されます。
using UnityEngine;
[System.Serializable]
class ModelJoint
{
public Transform bone;
public nuitrack.JointType jointType;
[HideInInspector] public Quaternion baseRotOffset;
}
-
RiggedAvatarスクリプトに戻り、関節の一覧を作成します。[Inspector]タブで、必要な関節を確認します (Nuitrack とインダイレクト マップ方式を使用してモデルを動かす場合には、関節は11 箇所必要です)。
using UnityEngine;
using System.Collections.Generic;
public class RiggedAvatar :MonoBehaviour
{
[Header("Rigged model")]
[SerializeField]
ModelJoint[] modelJoints;
}
- 注意
- ほとんどの場合、モデルのスケルトンには、人間と同様、鎖骨が 2つあります。JointType (LeftCollar, RightCollar) にも、2種類の鎖骨があります。しかし、センサーから取得したスケルトンには、鎖骨が中央に1つだけとなります (次の画像を確認ください)。
-
利便性を考え、辞書を作成し、関節を入力していきます。
ModelJoint[] modelJoints;
Dictionary<nuitrack.JointType, ModelJoint> jointsRigged = new Dictionary<nuitrack.JointType, ModelJoint>();
void Update()
-
配列の関節のループ処理を行ない、モデルの骨の基本的な回転を記録します。モデルの関節と関節タイプ (jointType) が jointsRigged が辞書に追加されます。
Dictionary<nuitrack.JointType, ModelJoint> jointsRigged = new Dictionary<nuitrack.JointType, ModelJoint>();
void Start()
{
for (int i = 0; i < modelJoints.Length; i++)
{
modelJoints[i].baseRotOffset = modelJoints[i].bone.rotation;
jointsRigged.Add(modelJoints[i].jointType, modelJoints[i]);
}
}
void Update()
-
各関節に関する情報を、Nuitrack から取得します。モデルの骨の回転を計算します。ミラー化した関節の位置 (方向) を取り込み、モデルの骨のもとになる回転を追加します。
if (CurrentUserTracker.CurrentSkeleton != null) ProcessSkeleton(CurrentUserTracker.CurrentSkeleton);
}
foreach (var riggedJoint in jointsRigged)
{
nuitrack.Joint joint = skeleton.GetJoint(riggedJoint.Key);
ModelJoint modelJoint = riggedJoint.Value;
Quaternion jointOrient = Quaternion.Inverse(CalibrationInfo.SensorOrientation) * (joint.ToQuaternionMirrored()) * modelJoint.baseRotOffset;
modelJoint.bone.rotation = jointOrient;
}
}
-
モデルの全体的な動きと、関節の回転を確認します。
- 注意
- デフォルトの設定では、アバターがユーザーの動きと同じ動きをします。例えば、ユーザーが右手を挙げた場合に、アバターも右手を上げます。動きをミラー化 (鏡に映っているように) したい場合、MirrorFlipCamera スクリプトをダウンロードして、Unity カメラにドラッグ アンド ドロップします。さらに、[Inspector]タブの[Mirror Flip Camera (Script)]セクションにある[flipHorizontal]チェックボックスをオンにします。
ダイレクト マップ方式
-
ダイレクト マップ方式では、親と子関節の距離を考慮に入れるので、ModelJoint.cs スクリプトを変更する必要があります。変更するには、ModelJoint クラスに、タイプ、親関節の変形、親と子関節の距離を定義する行を追加します。
[HideInInspector] public Quaternion baseRotOffset;
public nuitrack.JointType parentJointType;
[HideInInspector] public Transform parentBone;
[HideInInspector] public float baseDistanceToParent;
}
-
RiggedAvatar.cs スクリプトにいくつかの新しい情報を追加していきます。[Inspector]タブで必要な関節を確認します。Nuitrack とダイレクト マップ方式を使用してモデルを動かす場合には、関節は17箇所必要です。お気づきの通り、ダイレクトマップ方式では、インダイレクト マップ方式より多くの関節を使用します。インダイレクト マップ方式では、いくつかの関節が他の関節に依存しており、関節はその階層に従って動きます。ダイレクト マップ方式では、すべての関節が独立しており、それぞれの関節の動きを指定する必要があります。注意: ダイレクト マップ方式の場合、必要な関節タイプだけではなく、すべての関節に関して親-子関係を指定する必要があります。
-
ダイレクト マップ方式では、胴体の位置だけでなく、他のすべての関節の位置を知る必要があるので、胴体の位置を定義している以下の行を削除する必要があります。
Vector3 torsoPos = Quaternion.Euler(0f, 180f, 0f) * (0.001f * skeleton.GetJoint(nuitrack.JointType.Torso).ToVector3());
transform.position = torsoPos;
その後、各関節の位置を定義する行を void ProcessSkeleton(nuitrack.Skeleton skeleton) 関数の foreach (var riggedJoint in jointsRigged) ループに追加する必要があります。
modelJoint.bone.rotation = jointOrient;
Vector3 newPos = Quaternion.Euler(0f, 180f, 0f) * (0.001f * joint.ToVector3());
modelJoint.bone.position = newPos;
}
-
この段階でプロジェクトをビルドすると、モデルの手足がアンバランスになるかもしれません。この状況を修正するために、子骨と親骨の基本的な距離の指定を AddBoneScale 関数を、void Start () に配置して行ないます。
void AddBoneScale(nuitrack.JointType targetJoint, nuitrack.JointType parentJoint)
{
Vector3 targetBonePos = jointsRigged[targetJoint].bone.position;
Vector3 parentBonePos = jointsRigged[parentJoint].bone.position;
jointsRigged[targetJoint].baseDistanceToParent = Vector3.Distance(parentBonePos, targetBonePos);
jointsRigged[targetJoint].parentBone = jointsRigged[parentJoint].bone;
jointsRigged[targetJoint].parentBone.parent = transform.root;
}
-
骨の拡大/縮小を実行します。デフォルトでは、骨の拡大/縮小率は (1,1,1) になっています。ユーザーの体系に応じて、動的に変更されます。void ProcessSkeleton(nuitrack.Skeleton skeleton) 機能を追加します。
if (modelJoint.parentBone != null)
{
Transform parentBone = modelJoint.parentBone;
float scaleDif = modelJoint.baseDistanceToParent / Vector3.Distance(newPos, parentBone.position);
parentBone.localScale = Vector3.one / scaleDif;
}
- 注意
- モデルの変形は、予想以上の結果になるかもしれません。これを防ぐには、モデルの体系とユーザーの体系が似ていることを確認してください。
- プロジェクトのビルドを作成します。モデルの全体のバランスが均等になり、動きも改善されるでしょう。
- 注意
- テスト実行時には、モバイル端末がなくても問題ありません。プロジェクトのテストは、コンピューターとセンサーだけでも実行できます。アプリは Unity 内で実行されます。