7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

FinalIK.VRIK.GuessHandOrientations()でアバターの手首の回転を調整する

Last updated at Posted at 2020-05-27

#概要
ハンドコントローラの動きに合わせてIKでアバターを動かそうとするとき、ランタイムでアバターを切り替える場合は特に、各アバターによって手のボーンの初期姿勢が異なるために両手があらぬ方向を向くという問題がある気がします。

今回は、VRoomというHMDを被りながら作業通話やゲームをするツールを作っている過程で、上記の問題にどのように対処したのか、書きながら整理しようと思います。
かなり手探りで進めているので、いろいろ教えていただければ幸いです…

GitHub : Ytomi4/VRoom

####環境

  • Unity 2019.3.10
  • FinalIK ver1.9

####目指すところ
アプリ内でアバターを自由に切り替えることを前提に、プレイヤーの手にアバターの手が重なるよう、ボーンの初期姿勢が異なる様々なアバターに対応可能な調整方法を考える。

#解決したい問題の特定
便宜的に、アバターによって手首のボーンの初期姿勢が異なることから起こる問題と表現しましたが、具体的には下画像のような状況を指しています。
FinalIK_HandTransform.png

画像上段は、各アバターの左手にあたるボーン(VRIK.referencesのLeftHandに格納されるGameObject)の姿勢をGizmoで表示したものです。
左手ボーンのローカル空間において、赤が右方向(1,0,0)、緑が上方向(0,1,0)、青が前方向(0,0,1)であることを示しています。

上の例が示す通り、アバターの左手(掌)が左手ボーンのローカル空間でどの方向を向いているかは様々です(モデラーの方の出力による)。

Unitychan(画像左上)1では、左手が左手ボーンのローカル空間における左方向(-1,0,0)を向いており、Cygnet(画像中央上)2では左手ボーンのローカル空間における上方向(0,1,0)を向いています。Unitychan(画像左上)とVita(画像右上)3は、手がそれぞれのボーンのローカル空間で左方向(-1,0,0)を向いているという点では同じですが、x軸(赤)を中心に90°回転させたようになっており、たとえば手のひらに対する親指の方向の表現が異なります。

これにより、読み込んだアバターに単純にVRIKコンポーネントをアタッチするだけでは手首があらぬ方向を向きます(画像下段)。
Weightを1にして左手のボーンの回転RotationをTarget(画像ではVIVEコントローラーのモデルの先端)に完全に追従させると、「左手ボーンのローカル空間における右・上・前方向」と「コントローラーのローカル空間における右・上・前方向」とがぴったり重なるように左手ボーンが回転するため、アバターの手があらぬ方向を向いてしまう、と理解しました。

上記の理由からアバターによって種々の方向を向く手首を、なんとかコントローラーの前方向を向くようにしたいというのが今回の問題となります。

#解決の指針
たとえば、VRIKの公式チュートリアル動画(1:39~)では、コントローラーの位置と回転を反映させたシーン内のGameObjectの子オブジェクトをTargetに設定し、Transformを直接Inspectorからいじるという形で調整しています。

ただし、今回のケースでは、プレイ中のアバターの切り替えを前提としているのでInspectorから直接調整するという手法はとれず、アバター読み込み時にアバターの手のボーンの状態から適当な回転を計算し、親(コントローラー)に対してTargetを回転させる必要があります。

ここでかなり役に立ったのがGuessHandOrientations()でした(リファレンス)。これによって、回転を計算するのに必要な情報である、手首から手のひら方向手のひらから親指方向とみなせる手のボーンのローカル空間における軸を求められます。内容は決して複雑ではないのですが、言及している記事を自分が見つけられなかった、かつ公式のリファレンスがスカスカだったので、次の話に入る前に少しまとめておきます。

###VRIK.GuessHandOrientations()

VRIK.cs
public void GuessHandOrientations() {
			solver.GuessHandOrientations(references, false);
}

まず一次的な処理としては、IKSolverVRクラスのインスタンスsolverからGuessHandOrientations (VRIK.References references, bool onlyIfZero)を呼び出し、次にsolver.rightHandとsolver.leftHandのwristToPalmAxispalmToThumbAxisに代入する値(Vector3)を計算しています。それぞれ、手首から手のひら方向手のひらから親指方向とみなせる手のボーンのローカル空間における軸を意味します。
FinalIK_HandTransform_cyg.png
上の画像の例では、wristToPalmAxisが(0,1,0)、palmToThumbAxisが(0,0,1)です。

もう少し詳しく見ると、wristToPalmAxisは手のボーンから肘にあたる場所のボーンへのベクトルtoForearmと、手のボーンのローカル空間におけるXYZ軸それぞれとの内積を取り、内積が最も小さくなる軸をVector3で返した後で、肘とは反対側を指すようひっくり返した(-1倍した)ものであると説明できます。Final IKのスクリプトから一部抜粋して引用します。

IKSolverVR.cs
leftArm.wristToPalmAxis = GuessWristToPalmAxis(references.leftHand, references.leftForearm);
IKSolverVR.cs
private Vector3 GuessWristToPalmAxis(Transform hand, Transform forearm) {
			Vector3 toForearm = forearm.position -hand.position;
			Vector3 axis = AxisTools.ToVector3(AxisTools.GetAxisToDirection(hand, toForearm));
			if (Vector3.Dot(toForearm, hand.rotation * axis) > 0f) axis = -axis;
			return axis;
		}

余談ですが、先の説明と引用したコードが示す通り、wristToPalmAxisは肘と手首の関係のみから求められ、手のひらと手首の位置関係は関係ないため、たとえば手首が小さく丸まっているようなアバターを想定した場合でも、wristToPalmAxisがコントローラーの前方向を向くように回転させれば、手首の丸まりを保持したままコントローラーに沿った手のかたちになるといえそうです。
モデラ―の方が出力してくれた肘から手にかけてのニュアンスを(極力)維持したまま、向きをコントローラーにあわせてそれらしく調整できるようになっているかというのはかなり重要な部分だと思います。

#具体的な解決方法
VRoomのCharacterControl.csでは、アバター読み込み時に以下のような処理を行うことにしました。

  • 読み込んだアバターにVRIKコンポーネントをアタッチする
  • AutoDetectReferences()でリファレンスを埋める
  • AdjustIKTargetsToAvatarsHands()でアバターに合わせてIKTargetの位置・回転を変える
    • GuessHandOrientations()で、solver.rightHandsolver.leftHandそれぞれのwristToPalmAxispalmToThumbAxisを求める
    • wristToPalmAxisがコントローラーの前方向(0,0,1)を向くような回転leftWristRotを求め、IKTargetを回転させる
      • このとき、あくまで回転させるのはコントローラーの子オブジェクトであるIKTargetのlocalRotationであるため、wristToPalmAxis(あるいは回転後のpalmToThumbAxis)と比較するコントローラーの前方向は常に(0.0,1)と表現できる 
    • コントローラーの前方向(0,0,1)を軸として、leftWristRotで回転したpalmToThumbAxisが、コントローラーの右方向(1,0,0)となす角だけ回る回転leftPalmRotAngleを求め、IKTargetを回転させる(左手の場合)
    • さらに回転したpalmToThumbAxisが左方向(-1,0,0)を向いている場合は、前方向(0,0,1)を軸に180°回転(左手の場合)
      • Vector3.Angleの返すleftPalmRotAngleはあくまで角度差であるため、90°反対に回転してpalmToThumbAxisと向くべき方向との間で180°の開きが生まれるパターンがある
    • 手の位置にオフセットを加える
      • オフセットを加えない場合、コントローラーの先端がアバターの手首の位置にあり、不自然に感じる
      • オフセットを加える方向はアバターによって異なるので、毎回TargetのlocalPositionはVector3.zeroに戻しておく必要がある
  • IKTargetの代入やstretchCurveの初期化など…
CharacterControl.cs
private void AdjustIKTargetsToAvatarsHands()
{
        _vrik.GuessHandOrientations();

        //Targetのローカルポジションを(0,0,0)に戻しておく
        _leftHandIKTarget.transform.localPosition = Vector3.zero;
        _rightHandIKTarget.transform.localPosition = Vector3.zero;

        //左手
        //wristToPalmAxisがコントローラーの前方向(0,0,1)を向くよう回転
        Quaternion leftWristRot = Quaternion.FromToRotation(_vrik.solver.leftArm.wristToPalmAxis, Vector3.forward);
        _leftHandIKTarget.transform.localRotation = leftWristRot * Quaternion.identity;

        //palmToThumbAxisがコントローラーの右方向(1,0,0)となす角だけ前方向(0,0,1)を軸に回転
        float leftPalmRotAngle = Vector3.Angle(leftWristRot * _vrik.solver.leftArm.palmToThumbAxis, Vector3.right);
        Quaternion leftPalmRot = Quaternion.AngleAxis(leftPalmRotAngle, Vector3.forward);
        _leftHandIKTarget.transform.localRotation = leftPalmRot * _leftHandIKTarget.transform.localRotation;

        //palmToThumbAxisが左方向(-1,0,0)を向いている場合は、前方向(0,0,1)を軸に180°回転
        Vector3 leftThumbDirRotated = leftPalmRot * (leftWristRot * _vrik.solver.leftArm.palmToThumbAxis);
        if (Vector3.Dot(leftThumbDirRotated, Vector3.right) < 0)
        {
            _leftHandIKTarget.transform.localRotation = Quaternion.AngleAxis(180, Vector3.forward) * _leftHandIKTarget.transform.localRotation;
        }

        //手の位置にオフセットを加える
        _leftHandIKTarget.transform.localPosition -= _handOffset * Vector3.forward;


        //右手
        //wristToPalmAxisがコントローラーの前方向(0,0,1)を向くよう回転
        Quaternion rightWristRot = Quaternion.FromToRotation(_vrik.solver.rightArm.wristToPalmAxis, Vector3.forward);
        _rightHandIKTarget.transform.localRotation = rightWristRot * Quaternion.identity;

        //palmToThumbAxisがコントローラーの左方向(1,0,0)となす角だけ前方向(0,0,1)を軸に回転
        float rightPalmAngle = Vector3.Angle(rightWristRot * _vrik.solver.rightArm.palmToThumbAxis, Vector3.left);
        Quaternion rightPalmRot = Quaternion.AngleAxis(rightPalmAngle, Vector3.forward);
        _rightHandIKTarget.transform.localRotation = rightPalmRot * _rightHandIKTarget.transform.localRotation;

        //palmToThumbAxisが右方向(1,0,0)を向いている場合は、前方向(0,0,1)を軸に180°回転
        Vector3 rightThumbDirRoted = rightPalmRot * (rightWristRot * _vrik.solver.rightArm.palmToThumbAxis);
        if (Vector3.Dot(rightThumbDirRoted, Vector3.left) < 0)
        {
            _rightHandIKTarget.transform.localRotation = Quaternion.AngleAxis(180, Vector3.forward) * _rightHandIKTarget.transform.localRotation;
        }

        //手の位置にオフセットを加える
        _rightHandIKTarget.transform.localPosition -= _handOffset * Vector3.forward;
}

#実際の動き
FinalIK_HandTransform_performance.png

例に挙げた3体のアバターで確認したところ、プレイヤー側に操作を強いることなく、どのアバターの手もプレイヤーの手と違和感なく重なるようになっていました。

#課題

  • 確実にベストプラクティスがどこかにあるはずなので、調べる&読む
  • 例外をつくらないleftPalmRot
  • 他のボーンとの位置関係から、特定の部位の向いている方向をとるという考え方は応用が利きそう(たとえば指先前方向にRayを飛ばすなど)
  • そもそものアバター間の違いが、作られたモデリングソフトの違いによって発生していると思っているが、どうなのだろう
  • Quaternionの理解を深めたい
  1. https://unity-chan.com/contents/guideline/

  2. https://booth.pm/ja/items/1870320

  3. https://hub.vroid.com/characters/6193066630030526355/models/3525604181073039892

7
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?