みかづきメモ

主にプログラミング関連のメモ帳 ♪(✿╹ヮ╹)ノ 書いてあるコードは自己責任でご自由にどうぞ。記事本文の無断転載は禁止です。

SteamVR + Index Controller で Animation を使いながら Finger Tracking をしたい

先日の記事で指トラッキングをしましたが、 Animation Controller と使うと、
Animation で上書きされてしまってうまく動作しないらしいので、
改めて、 Animation Controller に対応した指トラッキングをやってみました。


前提環境は以下の環境です。

  • Unity Personal 2019.2.17f1
  • Windows 10
  • Valve Index

使用するアセットは以下の通りです。

この記事は、以下の 3D モデルで検証しています。

突然ですが、ここで 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 をチェックします。

f:id:MikazukiFuyuno:20191227182018p:plain
赤線の部分にチェック

これで、OnAnimatorIK が有効になったので、次は実際の処理を書いていきます。
ここで注意が必要なのが、モデルによって指のボーンの軸方向が異なるということです。
検証した上記4つのモデルでは、以下のようにベースの座標軸が異なっていました。
例えば、 RightIndexProximal のローカル座標軸を表示してみると、下のようになります。

キッシュ シャペル ルナーフ Vケットちゃん
f:id:MikazukiFuyuno:20191227182619p:plain:w200 f:id:MikazukiFuyuno:20191227182208p:plain:w200 f:id:MikazukiFuyuno:20191227182232p:plain:w200  f:id:MikazukiFuyuno:20191227182254p:plain:w200

キッシュおよびシャペルの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);

そうやって動かしたものが以下の動画です。

f:id:MikazukiFuyuno:20191227182330g:plain

ということで、 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

  1. こちらの記事が詳しいです。

  2. 「関節可動域」などのワードで調べると良いです。