先日の記事で指ト[ラッキングをしましたが、 Animation Controller と使うと、
Animation で上書きされてしまってうまく動作しないらしいので、
改めて、 Animation Controller に対応した指トラッキングをやってみました。
前提環境は以下の環境です。
- Unity Personal 2019.2.17f1
- Windows 10
- Valve Index
使用するアセットは以下の通りです。
この記事は、以下の 3D モデルで検証しています。
- オリジナル 3D モデル『キッシュ』
- 【CC0】オリジナルアバター「シャペル」【VRChat 想定】
- 【無料配布】オリジナル 3D モデル 『ルナーフ』
- V ケットちゃん
- 現在は配布されていません
突然ですが、ここで Unity の処理順を見ていきます。
Order of Execution for Event Functions
前回の記事で指を動かしていたのは Update
イベントの中でした。
しかし、 Animation を使用していた場合、ここで処理したものは上書きされます1。
かといって LateUpdate
でした場合、今度は IK の処理結果が上書きされます。
そこで、Animation の後で呼ばれ、 IK を考慮してくれる OnAnimatorIK
イベントで、
SetBoneLocalRotation
メソッドを使うことで制御してみました。
ただ、 OnAnimatorIK
の内部では HumanPose
の結果が反映されないようなので、
自分で良い感じに指を曲げる必要があります。
ちなみに、 LateUpdate
の中で HumanPose
を使うと無限に回転しました。
OnAnimatorIK
を有効にするには、まず Animation Controller を、
使用したい 3D モデルに設定する必要があります。
設定したら、 Animation Controller を開き、 Base Layer
にある歯車をクリックします。
クリックしたら、右側に出てきたポップアップの中の IK Pass
をチェックします。
赤線の部分にチェック
これで、OnAnimatorIK
が有効になったので、次は実際の処理を書いていきます。
ここで注意が必要なのが、モデルによって指のボーンの軸方向が異なるということです。
検証した上記 4 つのモデルでは、以下のようにベースの座標軸が異なっていました。
例えば、 RightIndexProximal のローカル座標軸を表示してみると、下のようになります。
キッシュ | シャペル | ルナーフ | V ケットちゃん |
---|---|---|---|
キッシュおよびシャペルの 2 つは Z 軸が下を向いているのに対し、
ルナーフは Z 軸が上を、V ケットちゃんは X 軸が下を向いています。
このことを考慮せずに全て同じように曲がる方向を処理してしまうと、
指があらぬ方向へ曲がってしまうことになります。
そのため、まずは Start
イベントの中で指をどの方向へ曲げるのが正しいのか、
つまりはグローバル座標で Y 軸マイナス側へ移動するのはどの方向かを調べます。
それは、以下のようなスクリプトで調べることが出来ます。
// 座標軸が下のようになっているものは無い前提 // * 指の種類・関節によって座標軸が異なる // * Y 軸が身体の外側を向いていない // * モデルが上下逆ではない (グローバル Y 軸プラスの方向が頭になっている) private void Start() { // サンプルとして左右それぞれ人差し指の第1関節の向きを取得し、比べる var leftIndexTransform = Animator.GetBoneTransform(HumanBodyBones.LeftIndexProximal); var (axis1, bool1) = DetectJointRotationAxis(leftIndexTransform); var rightIndexTransform = Animator.GetBoneTransform(HumanBodyBones.RightIndexProximal); var (axis2, bool2) = DetectJointRotationAxis(rightIndexTransform); } // (Axis, bool) = (縦方向の座標軸, 上向きなら true) private (Axis, bool) DetectJointRotationAxis(Transform transform) { if (Math.Abs(Vector3.Dot(transform.forward, Vector3.down)) > .99) { return (Axis.Z, Vector3.Dot(transform.forward, Vector3.down) < 0); } else if (Math.Abs(Vector3.Dot(transform.right, Vector3.down)) > .99) { return (Axis.X, Vector3.Dot(transform.right, Vector3.down) < 0); } // Y 軸が外側を向いていない throw new NotSupportedException(); }
上記のスクリプトで無事どの方向をベースに動かせば良いのかが分かったので、
次に、 3D モデルの初期状態のローカル Rotation を保存しておきます。
これは、保存しておいた初期状態の Rotation を元に回転する角度を決める為です。
各ボーンの Transform とローカル Rotation は以下のように取得できます。
var initialRotation = Animator.GetBoneTransform(HumanBodyBones.RightIndexProximal).localRotation;
これを各指および関節毎に、左右合計 30 個の値を保存しておきます。
ここまでが、 Start
イベントでの処理の内容です。
次に、 OnAnimatorIK
で実際に指を曲げていきます。
はじめに、各指がどの程度曲がるかどうかを以下のような感じで定義しました。
public struct Range<T> { public T Min { get; set; } public T Max { get; set; } public Range(T min, T max) { Min = min; Max = max; } } public struct Stretch { // どの程度の角度まで指が曲がるのか、マイナス (Min) は反る側、プラス (Max) は折りたたむ(?)側 public Range<float> RangeOfMotion { get; set; } // どの方向へ曲がるのか、 Z 軸基準で public Vector3 Direction { get; set; } } private readonly Dictionary<FingerCategory, Stretch[]> _fingerStretches = new Dictionary<FingerCategory, Stretch[]> { // 各指について、どの範囲稼動するのか、どの向きに曲がるのかを定義する { FingerCategory.Index, new [] { new Stretch { RangeOfMotion = new Range<float>(-12.5f, 90f), Directon = Vector3.right }, } } }
次に、上記で定義したものを元に、回転する角度と軸を計算します。
角度は前回も用いた以下のコードで計算が出来ます。
var stretch = _fingerStretches[FingerCategory.Index]; var curl = Skeleton.indexCurl; var angle = Mathf.Lerp(stretch.RangeOfMotion.Min, stretch.RangeOfMotion.Max, curl * weight);
回転軸については、以下のように求めます。
var angleAxis = CalcAngleAxis(angle, stretch.Direction); // angle 度、 Direction の方向に向く Quaternion が返る private Quaternion CalcAngleAxis(float angle, Vector3 direction) { // _axis は一番最初に求めておいた縦方向の座標軸 switch (_axis) { case Axis.X: // isReverse も一番最初に求めていた上向きかどうかの bool return Quaternion.AngleAxis(_isReverse ? -angle : angle, Quaternion.Euler(0, 90, 0) * vector); case Axis.Z: return Quaternion.AngleAxis(_isReverse ? -angle : angle, vector); default: throw new ArgumentOutOfRangeException(); } }
最後に、これを保存しておいた初期状態の Rotation にかけてあげ、
それで求まった値を SetBoneLocalRotation
に渡します。
var rotation = initialRotation * CalcAngleAxis(angle, stretch.Direction); Animator.SetBoneLocalRotation(HumanBodyBones.RightIndexProximal, rotation);
そうやって動かしたものが以下の動画です。
ということで、 Animation Controller を用いた状態で、
指トラッキングをする方法でした。
なお、記事の中のコードや Scene については、以下のリポジトリにて公開しています。
今回使用したコード・シーンについては、以下の場所にあります。
Assets/Scenes/004_FingerTrackingWithAnimation.unity
Assets/SteamVR_Sandbox/Avatar/AvatarFingerInput.cs
Assets/SteamVR_Sandbox/Enums/Axis.cs
Assets/SteamVR_Sandbox/Enums/FingerCategory.cs
Assets/SteamVR_Sandbox/Enums/FingerJoint.cs
Assets/SteamVR_Sandbox/Humanoid/Finger.cs
Assets/SteamVR_Sandbox/Humanoid/Hand.cs
Assets/SteamVR_Sandbox/Humanoid/Stretch.cs
Assets/SteamVR_Sandbox/Models/Range.cs
リポジトリはこちら:mika-sandbox/SteamVR-Sandbox
ということで、ではでは~ ◟(⁰ 𠆢 ⁰∗)⌟
おまけ:
各指がどの程度の曲がるのかは、私は医療情報2を元に調整し、下のように定義しました。
※厳密に現実に合わせる必要は無いので、見栄え良くなるように調整しています。
※親指の曲がる向きについては、まだ調整が必要そうです。
Finger Category | Min | Max | Direction |
---|---|---|---|
Thumb (Stretch1) | -15f | 60f | Vector3.right |
Thumb (Stretch2) | -10f | 80f | Vector3.right |
Thumb (Stretch3) | -10f | 40f | Vector3.right |
Others (Stretch1) | -12.5f | 90f | Vector3.right |
Others (Stretch2) | -7.5f | 100f | Vector3.right |
Others (Stretch3) | -7.5f | 80f | Vector3.right |