Bakulog

獏の夢日記的な何か。

COM3D2のVRモードでPerception Neuronを使ってみた

COM3D2+VRなModの検証報告とか技術紹介的な記事です。

※本記事公開時点ではMod等は公開していません。リクエストを受けてか、あるいは本来やりたかったレベルに近づいたら公開するかもしれません。

1. 概要

大まかな内容はTwitterの動画で察してください。

https://twitter.com/baku_dreameater/status/1027579426073018368

参考: 思ったとおりに動かす前 https://twitter.com/baku_dreameater/status/1024303266576756742

1.1. やりたかったこと

  • COM3D2のVAS(バーチャルアバタースタジオ)モードに全身モーキャプを導入し、肘や指や脚をもっと可愛く動かしたい

1.2. やったこと

  • HTC Vive + Perception Neuronでリアルタイムにメイドさんを動かす
  • 関節の回転処理は全てFK(Forward Kinematic≒関節ごとの角度を単純にコピー)
  • ルートのボーン(Hips)は「HMDの位置姿勢 = キャラの目(頭)の位置姿勢」となるように計算して調整

ここで、とくに「HMDの位置姿勢 = キャラの目(頭)の位置姿勢」となるよう 調整計算をした理由は以下2つです。

  • HMD装着者からの見栄えがよい: HMDカメラと独立にキャラクターの頭部を動かしてしまうと、カメラのすぐ前にキャラの後頭部が出たり、逆にカメラが頭にめりこんだりするため、装着している人の視界が悪いです。
  • 精度がよい(はず): HMDのほうがNeuronより位置情報を信じられ、基準位置にふさわしそう

1.3. 考察

  • 〇いちおう思った通りの挙動は作れた
  • ×「頭を固定+全身FK」の組み合わせは精度が悪いと脚側に誤差が蓄積してキモい
  • ×自宅のPerception Neuronは磁化してダメになってる(かも)

2. 計算ポイント: HMDの位置に無理やり目(頭)を持ってくるには

ここからは、実装時に少しハマった回転計算の紹介です。

…の前に。前提として、Perception Neuronでリアルタイムに姿勢コピーする方法は昔の記事で紹介しています。こちらの記事の考え方は汎用性が高いので、対象がカスタムメイドであってもそのまま通用します。以下では、こちらの記事に既に載っている内容には触れません。

本題に戻ります。本記事でやった処理を何ステップかに区切ってみると、 Unityで以下5ステップのことを(ビルトインで勝手に実行されるIKの計算結果を上塗りしながら)やっています。

  1. FKで全身ボーンの角度を更新する。
  2. キャラクターを剛体とみなす。
  3. キャラクターのHipsの子孫オブジェクト(=今回は目)と、ターゲットになる外部オブジェクト(今回の場合はVRカメラ)を決める。
  4. 目の位置、回転 = カメラの位置、回転になるような条件式を求める。
  5. 4で求めた式が満たせるように、Hipsの位置と姿勢(positionrotation)を更新する。

ステップ1は昔の記事で紹介している範囲なので割愛します。 ステップ2ですが、これに直接対応する実装はありません。プログラム的な解釈としては「ここから先の処理では根本(Hips)のボーンだけpositionrotationを書き換えてよく、他のボーンでは書き換えはダメ」という感じになります。 ステップ3も実装はなく、単に準備として対象オブジェクトを定義しているだけです。

ステップ4と5が主計算になります。 この計算のやり方はいくつか考えられますが、ここでは直感的に解釈できるよう、位置、姿勢の式を別々に書きます。 まずはステップ4が示している「目の位置、回転 = ターゲット(VRカメラ)の位置、回転」の方程式。

  • 回転: Rot_hip * Rot_hip_to_eye = Rot_target
  • 位置: Pos_hip + Rot_hip * Pos_hip_to_eye = Pos_target

値の意味はこうです。

  • Rot_target, Pos_target: 目標オブジェクト、つまりVRカメラのワールド回転・位置
  • Rot_hip, Pos_hip: 最終的に求めたい、Hipsのワールド回転・位置
  • Rot_hip_to_eye, Pos_hip_to_Eye: FK計算が終了した時点で定まる、Hipsから見たEyeのローカル回転・位置

方程式は直ちに解けます。以下のInv()はUnityのQuaternion.Inverse関数と同じ意味です。

  • 式1: `Rot_hip = Rot_target * Inv(Rot_hip_to_eye)
  • 式2: Pos_hip = Pos_target - Rot_target * Inv(Rot_hip_to_eye) * Pos_hip_to_eye

上式の右辺のうち、Rot_targetPos_targetCamera.main.transformから取得できます。残るPos_hip_to_eyeRot_hip_to_eyeを求めるため、別の式を立てます。

  • 回転: Rot_hip_before * Rot_hip_to_eye = Rot_eye
  • 位置: Pos_hip_before + Rot_hip_before * Pos_hip_top_eye = Pos_eye

こちらの式は目標オブジェクトではなく、目(頭)の位置と回転にかんする方程式になります。 増えた値の意味はこうです。

  • Rot_hip_before, Pos_hip_before: FK計算が終わった直後の時点での、Hipsのワールド回転・位置
  • Rot_eye, Pos_eye: FK計算が終わった直後の時点での、Eyeのワールド回転・位置

この式もすぐ解けます。

  • 式3: Rot_hip_to_eye = Inv(Rot_hip_before) * Rot_eye
  • 式4: Pos_hip_to_eye = Inv(Rot_hip_before) * (Pos_eye - Pos_hip_before)

これで式3,4の結果を式1,2に突っ込めば、更新すべきHipsの位置、回転が求まります。

あとは実装です。プログラムを抜粋すると、こんな感じになります。

//何かの初期化時にキャッシュしておく、頭から目への座標変換に必要な値
private Vector3 _pos_head_to_eye = Vector3.zero;
private Quaternion _rot_head_to_eye = Quaternion.identity;

//頭から目=カメラボーンへの変換を求める処理
public void InitializeHeadToCameraParameters()
{
    Transform camera = Camera.main.transform;
    Transform head = _headTransform; //_headTransformは何かの初期化で取得しておく

    _rot_head_to_eye = Quaternion.Inverse(head.rotation) * camera.rotation;
    _pos_head_to_eye = Quaternion.Inverse(head.rotation) * (camera.position - head.position);
}

//他の角度計算(FKでもIKでも)が一通り終わってから呼ぶことで、
//頭の位置をHMD位置に合わせるようにHipsを動かす
public void UpdateHips()
{
    //_hipsTransformや_headTransformは何かの初期化で取得しておく
    Transform hips = _hipsTransform; 
    Transform head = _headTransform;

    //めざす位置と姿勢: 今回の場合はメインカメラ
    Vector3 goalPosition = Camera.main.transform.position;
    Quaternion goalRotation = Camera.main.transform.rotation;

    //「Hipからみた目」への変換のために位置、回転を求める。
    //位置は、実はUnityの場合、数式なしで`Transform.InversTransformPoint`で一発計算可能。

    //式3: 記事内で`Rot_eye`とあった値は、
    //    `head.rotation * _rot_head_to_eye`という2段階の回転で求めている。それ以外は記事通り。
    Quaternion rot_hip_to_eye = Quaternion.Inverse(hips.rotation) * head.rotation * _rot_head_to_eye;
    //式4: 記事内で`Pos_eye`とあった値は、
    //    「頭の位置 + 頭→目のオフセット」という2項に分けて求めている。その後の計算は`InverseTransformPoint`に丸投げ。
    Vector3 pos_hip_to_eye = hips.InverseTransformPoint(head.position + head.rotation * _pos_head_to_eye);

    //式1,2で同じ回転成分が出てくるので計算しておく
    Quaternion rot_hip = goalRotation * Quaternion.Inverse(rot_hip_to_eye);
    //式1: そのまんま。
    hips.rotation = rot_hip;
    //式2: そのまんま。
    hips.position = goalPosition - rot_hip * pos_hip_to_eye;
}

3. まとめ

動画にもある通り、座標変換がうまく行っても結果は良いものではありませんでした。 このあたりはForward Kinematicで頑張り過ぎても限界ある感じがするので、 もっと良い方法が見つかったらまた紹介したいと思います。