25
13

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.

VTuber Tech #1Advent Calendar 2019

Day 10

Unity Humanoid制御に指ロールを追加したい

Last updated at Posted at 2019-12-09

概要

UnityのHumanoidはモデルの差異を吸収し、シンプルなコードで動作するようにしてくれるので大変有り難いのですが、指ロールが規定されておらず、個人的に手の表現力が少し足りないと感じています。ボーン数はCPUの処理負荷に影響しますので、特に非力なモバイル機では指ボーンが省略されているモデルを使用することも多く、現段階でゲームエンジンとしては処理負荷的に指ボーンのロール制御させることへの優先順位はあまり高くないかもしれません。

下の画像は指ロール無し/ありの状態で似たような指ポーズを調整したものです。指ロールがあると、脱力している感じや、力を入れてる感じなどを表現し易くなります。何かを持たせようとしたときも、物の形に指をフィットさせることができます。より持ってる感を表現することができるようになり、CGっぽい雰囲気も軽減するように個人的に思います。
191209_FingerRoll.png
※Unity2018.4.13f1にて動作確認

モデルのローカル軸に依存しないで指をロール回転させたい

実際に指をロール回転させるには、指ボーンのTransformを取得し、ボーンのローカル軸でロール方向に回転を加えるのが一般的な制御ですが、モデルの作成時に指ボーンをどの向きで仕込んだかによって回転させる軸が変化します。自前で作成したモデルであれば指ボーンのローカル軸を統一してロール回転処理を決め打ちで制御できますが、AssetStoreで購入したモデルなどが混在すると、どの軸を回せばいいのかが不定になってしまうことがあります。

また、VRMモデルを使用する場合は指ボーンローカル軸とは無関係にT-Poseで正規化された状態になり、指ボーンローカル軸と指の向きが合わない状態になってしまうため(特に親指)、指ボーンのローカル軸での回転で指ロール処理を行うことができません。
191207_VRMboneNormalize.png
そこで、解決方法として指ボーンとその一つ先のボーンのワールド座標からロール軸を毎フレーム算出し、その軸で回転を加えることでモデル制作時の環境やボーンが正規化されたVRMモデルでも指ロールを行えるようにします。
(画像の緑線が指の第3関節と第2関節ボーンを繋いだ回転軸になっています)
191207_RollAxis.png

親指は全ての関節をロールさせたい

親指だけはモデルの制作時の指の向きに大きく依存してしまい、なかなか統一的な処理が難しく、HumanoidのMuscle値だけではそもそもちゃんと握り状態にならないモデルもあります1。指ロール機能があれば、それもある程度吸収させることができます。人差し指から小指は根元をロール回転すれば、経験的にそれなりのシルエットになりますが、親指は全てのボーンをロール回転できるようにした方が指ポーズを調整し易いです。

ただし、前記のロール回転軸を計算する際に一つ先のボーンが必要になってくるので、「親指の先端ボーン」が入っていないモデルの場合は、親指の第一関節をロール回転させることができません。そういった場合でも、Unity上でHierarchyで追加したり、動的にスクリプトで追加したりすることでモデル改修を行わなくても対応が可能です。手元のアップを使用しないのであれば、先端ボーンのないモデルは親指の第一関節のロール回転は行わない判断もありかと思います。
191205_ThumbTipBone.png

親指のロールは、手首と人差し指の根元座標を繋いだベクトルでの回転軸を一つ追加しておくと2、更にシルエットの調整がやり易くなります(画像赤線、親指用に追加した回転軸)。
191207_ThumbRoll.png

指ロールを含んだ指ポーズをデータ化したい

指ポーズのデータといっても、Humanoid型Muscle値データには指のロール値が規定されていないのでHumanoid型のアニメーションデータ/Muscle値配列では、指ロールを含んだ形で保存することができません。そこでUnityEditor上で指ロールを追加したMuscle値(指ポーズ)を編集できるスクリプトを作成します。
191207_HandPoseAsset.png

下記のサンプルコードはScriptableObjectで実装していますので、シーン再生中の編集が可能です(シーン停止しても編集が残る)。

HandPoseAsset.cs
using UnityEngine;

[CreateAssetMenu(fileName = "HandPose_", menuName = "Create HandPose Asset")]
public class HandPoseAsset : ScriptableObject
{
    [System.Serializable]
    public class FingerPoseThumb
    {
        [Range(-2f, 2f), Tooltip("指開き")] public float spread;
        [Range(-1f, 1f), Tooltip("指根本捻り")] public float roll;
        [Range(-2f, 2f), Tooltip("指1曲げ")] public float stretched1;
        [Range(-2f, 2f), Tooltip("指2曲げ")] public float stretched2;
        [Range(-3f, 3f), Tooltip("指3曲げ")] public float stretched3;
        [Range(-1f, 1f), Tooltip("指1捻り")] public float roll1;
        [Range(-1f, 1f), Tooltip("指2捻り")] public float roll2;
        [Range(-1f, 1f), Tooltip("指3捻り")] public float roll3;

        public void Lerp(FingerPoseThumb a, FingerPoseThumb b, float t)
        {
            spread = Mathf.Lerp(a.spread, b.spread, t);
            roll = Mathf.Lerp(a.roll, b.roll, t);
            stretched1 = Mathf.Lerp(a.stretched1, b.stretched1, t);
            stretched2 = Mathf.Lerp(a.stretched2, b.stretched2, t);
            stretched3 = Mathf.Lerp(a.stretched3, b.stretched3, t);
            roll1 = Mathf.Lerp(a.roll1, b.roll1, t);
            roll2 = Mathf.Lerp(a.roll2, b.roll2, t);
            roll3 = Mathf.Lerp(a.roll3, b.roll3, t);
        }
    }

    [System.Serializable]
    public class FingerPose
    {
        [Range(-2f, 2f), Tooltip("指開き")] public float spread;
        [Range(-1f, 1f), Tooltip("指捻り")] public float roll;
        [Range(-2f, 2f), Tooltip("指1曲げ")] public float stretched1;
        [Range(-2f, 2f), Tooltip("指2曲げ")] public float stretched2;
        [Range(-2f, 2f), Tooltip("指3曲げ")] public float stretched3;

        public void Lerp(FingerPose a, FingerPose b, float t)
        {
            spread = Mathf.Lerp(a.spread, b.spread, t);
            roll = Mathf.Lerp(a.roll, b.roll, t);
            stretched1 = Mathf.Lerp(a.stretched1, b.stretched1, t);
            stretched2 = Mathf.Lerp(a.stretched2, b.stretched2, t);
            stretched3 = Mathf.Lerp(a.stretched3, b.stretched3, t);
        }
    }

    [System.Serializable]
    public class HandPose
    {
        public FingerPoseThumb thumb;
        public FingerPose index;
        public FingerPose middle;
        public FingerPose ring;
        public FingerPose little;

        public void Lerp(HandPose a, HandPose b, float t)
        {
            thumb .Lerp(a.thumb,  b.thumb,  t);
            index .Lerp(a.index,  b.index,  t);
            middle.Lerp(a.middle, b.middle, t);
            ring  .Lerp(a.ring,   b.ring,   t);
            little.Lerp(a.little, b.little, t);
        }
    }

    public HandPose handPose;
}

指ポーズがUnityEditor上で作れるようになったら、それらをブレンドできる機能を追加し、アニメーション再生/IK処理の後に呼び出されるLateUpdate()関数内で指ポーズ処理を行うようにします。

指ロール処理サンプルコード

Humanoid型モデルのロール付きの指ポーズをさせるための処理のサンプルコードです。Animatorと同階層にコンポーネント追加してください。握り/デフォルト/開きの指ポーズをスライダーでブレンドすることができます。上記HandPoseAsset.csでデータ化された指ポーズファイルをインスペクタにセットする必要があります。

HandPoseController.cs
using UnityEngine;

public class HandPoseController : MonoBehaviour
{
    protected Animator animator;
    protected HumanPose humanPose;
    protected HumanPoseHandler poseHandler;

    public HandPoseAsset open;
    public HandPoseAsset normal;
    public HandPoseAsset close;
    [Range(-1f, 1f)] public float handPoseValue;
    public HandPoseAsset.HandPose targetHandPose;
    public bool DisableFingerRoll;

    protected int[] fingerMuscleIndex = { 55, 59, 63, 67, 71 };    // 指のMuscle配列の先頭
    protected HumanBodyBones[] finglerBoneL =
    {
        HumanBodyBones.LeftThumbProximal,
        HumanBodyBones.LeftIndexProximal,
        HumanBodyBones.LeftMiddleProximal,
        HumanBodyBones.LeftRingProximal,
        HumanBodyBones.LeftLittleProximal,

        HumanBodyBones.LeftHand,
    };
    protected HumanBodyBones[] finglerBoneR =
    {
        HumanBodyBones.RightThumbProximal,
        HumanBodyBones.RightIndexProximal,
        HumanBodyBones.RightMiddleProximal,
        HumanBodyBones.RightRingProximal,
        HumanBodyBones.RightLittleProximal,

        HumanBodyBones.RightHand,
    };


    void Start()
    {
        animator = GetComponent<Animator>();
        poseHandler = new HumanPoseHandler(animator.avatar, animator.transform);
    }

    void Update()
    {
        // 3つの指ポーズを補間する
        if (handPoseValue <= 0f) 
        {
            // close <-> normal
            targetHandPose.Lerp(close.handPose, normal.handPose, handPoseValue + 1f);
        }
        else
        {
            // normal <-> open
            targetHandPose.Lerp(normal.handPose, open.handPose, handPoseValue);
        }
    }

    void LateUpdate()
    {
        // Humanoid.Muscle値を書き換えて指ポーズをセットしたい
        poseHandler.GetHumanPose(ref humanPose);
        overwriteHandPose_muscle(isLeft: true);
        overwriteHandPose_muscle(isLeft: false);
        poseHandler.SetHumanPose(ref humanPose);

        // Muscle処理の後に指をロールさせる
        overwriteHandPose_roll(isLeft: true);
        overwriteHandPose_roll(isLeft: false);
    }

    // 指ポーズをセット(muscle)
    protected void overwriteHandPose_muscle(bool isLeft)
    {
        setFingerMuscle(isLeft, fingerMuscleIndex[0], targetHandPose.thumb .spread, targetHandPose.thumb .stretched1, targetHandPose.thumb .stretched2, targetHandPose.thumb .stretched3);
        setFingerMuscle(isLeft, fingerMuscleIndex[1], targetHandPose.index .spread, targetHandPose.index .stretched1, targetHandPose.index .stretched2, targetHandPose.index .stretched3);
        setFingerMuscle(isLeft, fingerMuscleIndex[2], targetHandPose.middle.spread, targetHandPose.middle.stretched1, targetHandPose.middle.stretched2, targetHandPose.middle.stretched3);
        setFingerMuscle(isLeft, fingerMuscleIndex[3], targetHandPose.ring  .spread, targetHandPose.ring  .stretched1, targetHandPose.ring  .stretched2, targetHandPose.ring  .stretched3);
        setFingerMuscle(isLeft, fingerMuscleIndex[4], targetHandPose.little.spread, targetHandPose.little.stretched1, targetHandPose.little.stretched2, targetHandPose.little.stretched3);
    }

    // 指ポーズをセット(roll)
    protected void overwriteHandPose_roll(bool isLeft)
    {
        if (DisableFingerRoll == true) return;

        // 人差し指から小指のロール処理
        HumanBodyBones[] finglerBone = isLeft ? finglerBoneL : finglerBoneR;
        setFingerRoll(isLeft, finglerBone[1], finglerBone[1] + 1, targetHandPose.index .roll);
        setFingerRoll(isLeft, finglerBone[2], finglerBone[2] + 1, targetHandPose.middle.roll);
        setFingerRoll(isLeft, finglerBone[3], finglerBone[3] + 1, targetHandPose.ring  .roll);
        setFingerRoll(isLeft, finglerBone[4], finglerBone[4] + 1, targetHandPose.little.roll);

        // 親指ロール処理
        var thumb3 = animator.GetBoneTransform(finglerBone[0] + 2);
        var thumb3_tip = (thumb3 != null && thumb3.childCount > 0) ? thumb3.GetChild(0) : null;

        setFingerRoll(isLeft, finglerBone[5],     finglerBone[1],     targetHandPose.thumb.roll, overwriteRollBone: finglerBone[0]); // 手首 ⇒ 人差し指への軸で回転
        setFingerRoll(isLeft, finglerBone[0],     finglerBone[0] + 1, targetHandPose.thumb.roll1);
        setFingerRoll(isLeft, finglerBone[0] + 1, finglerBone[0] + 2, targetHandPose.thumb.roll2);
        setFingerRoll(isLeft, finglerBone[0] + 2, default,            targetHandPose.thumb.roll3, overwriteTarget: thumb3_tip);
    }

    // Humanoid.Muscle値書き換え
    protected void setFingerMuscle(bool isLeft, int index, float spread, float stretched1, float stretched2, float stretched3)
    {
        if (isLeft == false) index += 20;    // Left ⇒ Right
        humanPose.muscles[index++] = stretched1;
        humanPose.muscles[index++] = spread;
        humanPose.muscles[index++] = stretched2;
        humanPose.muscles[index++] = stretched3;
    }

    // 指ロール処理
    protected void setFingerRoll(bool isLeft, HumanBodyBones bone, HumanBodyBones target, float roll, Transform overwriteTarget = null, HumanBodyBones overwriteRollBone = default)
    {
        if (target == default && overwriteTarget == null) return;
        var boneT = animator.GetBoneTransform((overwriteRollBone == default) ? bone : overwriteRollBone);
        var targetT = (overwriteTarget == null) ? animator.GetBoneTransform(target) : overwriteTarget;
        float sign = isLeft ? -1f : 1f;

        if (boneT != null && targetT != null) boneT.Rotate(boneT.position - targetT.position, roll * 90f * sign, Space.World);
    }
}

指ポーズ(HandPoseAsset)サンプルデータ

VRoidプロジェクト「千駄ヶ谷 篠」さんで作成した指ポーズ4つ「握り/デフォルト/開き/がおー」の指ポーズ(HandPoseAsset)サンプルデータです。参考になれば。
https://gist.github.com/mkt-/a71faba6e3a98d6c085fd2f224fa2d98

最後に

指ロールが使えるようになったら、次の段階としてアイテムを掴んだ時にそのアイテム固有の指ポーズを呼び出せるようにするなどの処理を追加していくとか、例えばVtuber的には歌配信を想定したマイク握りから試してみるのもいいかもしれませんね。

また、応用で基準となるモデルでデフォルト指ポーズに近くなるような指ポーズデータを混ぜ込むことで、モデル毎の初期ボーンの差異をある程度吸収するような処理も行うことができます(完全ではないにせよ作成した指ポーズファイルを初期ボーンの違うモデルに再利用できる)。

手の表現力が増えると動画コンテンツを作成する際、手元アップの映像の『間』が持つようになります。ドラマ撮影などではカットの選択肢が増え、映像表現としての幅が広がることが期待できるかと思います。

指ロールによる手の表現力アップは指にボーンさえ入っていればそれ以上のモデル改修が不要な処理ですので、Vtuber用映像制作の現場において費用対効果がかなり高い処理ではないかと個人的には考えています。

このページのサンプルコード及び指ポーズサンプルデータのライセンスはCC0とします。どうぞご自由に。それでは皆さま、よき指ロールライフを( ・ω・)ノシ


使用モデル

  1. HumanoidのConfigure画面である程度調整することができますが、モデル更新でFBX再インポートした時にリセットされたり、設定のLoad/Save機能はあるもののスクリプトから機能を呼び出せなかったりと、少々使い勝手が悪いので実行時に補正する方式をオススメします。

  2. 手首の代わりに親指根本ボーン位置を回転軸の一端にしてもよいのですが、親指根元ボーン位置よりも手首位置の方がモデル差異が少ないと判断し、ここでは手首位置から人差し指位置へのベクトルを回転軸とする方式を採用しています。

25
13
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
25
13

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?