💪

【Unity】muscleを使用した指制御入門

commits14 min read

この記事は、Unity #2 アドベントカレンダーの 16 日目、および愚者っとCorp.アドベントカレンダー2020の 17 日目の記事になります。

はじめに

どうもにー兄さん(@ninisan_drumath)と申します。
今回は自分が好きな Humanoid 制御に関する記事を書きます。

TL;DR

Unity で人型モデルの指制御をするには muscle がよさそう。

この記事を読んで得られる知見・対象読者

この記事を読んで得られる知見は以下の通りです。

  • 外部パッケージを使わない指制御の方法の一例
  • Unity で人型モデルをどう扱うか
  • muscle という概念
  • 指制御は沼なんじゃないかという疑念・焦り・諦め・お気持ち

対象読者は以下のとおりです。

  • Unity の基本知識がある
  • Unity でアバターを制御したい
  • Unity で指制御をしてみたい

この記事で扱わない事項

この記事では、以下のような内容は細かく扱いません。

  • VTuber システムをどう作るのか
  • センサリングを用いた開発の仕方
  • 全身の制御の仕方

検証環境

この記事は以下のような環境で検証されています。
ご参考になさってください。

環境 概要
OS Windows10 Home
Unity 2019.4.12

Humanoid制御とmuscle

まず、Unity でどのように人型のモデル(アバター)を制御するのかについて簡単に説明します。

ここでは VRoid Studio の公式 3D モデルである「千駄ヶ谷篠」を使わせていただきます。
手持ちで人型モデルをお持ちでない場合はリンクからダウンロードしていただき、UniVRMを使って Unity にインポートしてください。

UnityのHumanoidシステムについて

基本的な項目になりますので、
「もう知っているよ」という方は読み飛ばしていただいて構いません。

「世の中には 2 種類のリグしかない。人型か、それ以外か」
(出典:古事記)

冗談はさておき、Unity ではGenericHumanoidというリグがあります。
これらは字のごとく人間用のリグか一般的なリグかという意味で、Humanoid の場合は特別に、別の Humanoid のアニメーションを流用できたり、体の特定のボーンをスクリプトから制御しやすくなっていたりするのです。
そして Humanoid の定義を満たすためには人間を構成するのに必須なボーンがちゃんと設定してある必要があります。

先ほど Humanoid のモデルは特定のボーンを制御しやすいということを書きましたが、Humanoid のモデルは Animator コンポーネント経由でアバターのポーズ制御が可能になります。

例えば以下のコードを書くことで任意の Humanoid モデルの腕を曲げることができます。

HumanoidTest.cs
using System;
using UnityEngine;

namespace FingerOperations
{
    [RequireComponent(typeof(Animator))]
    public class HumanoidTest : MonoBehaviour
    {
        private Animator _animator;
        private void Start()
        {
            _animator = GetComponent<Animator>();
            
            _animator.GetBoneTransform(HumanBodyBones.LeftUpperArm)
                .Rotate(new Vector3(0,0,60f));
        }
    }
}

bent-arm
通常時

normal-arm
肩を曲げた状態

上のコードではボーンの Transform を直接操作して変えているため、もし肘や手首も曲げることになった時には肘と手首のボーンを同じように回転させてやる必要があります。このように、親のボーンから子のボーンにかけてポーズを決めていくような制御の方法を FK と言います。
逆に、末端のボーンの Transform を先に決め、そのボーンに合わせて親のボーンの Transform を決定するような制御の方法を IK と言い、多少書き方が変わりますが Unity では IK もサポートしています[1]

muscleシステムとは

前述のとおり Unity では、 Humanoid モデルの場合ボーンの FK/IK 制御が可能で、FK の場合は Transform の回転角を指定していました。
しかしこの制御の方法には「通常の人間ではありえないような回転が可能になってしまう」という問題があります。
このように意図しない回転が指定されることでボーンが変な曲がり方をしてしまう現象を骨折といいます。骨折がモーションキャプチャーをする際などで起こってしまうと見ているのがとても辛いです......特に自分のアバターで起きたりすると......。

そのような事態が起きないように、Unity の Humanoid ではボーンの回転角に制限をつけることができます。このシステムのことをmuscle システムと呼びます。
Humanoid を生成する際に「Avatar Config」というものを使うのですが、そこで各ボーンに回転角の制限をつけることが可能です。とくに自分で指定しなくても Unity 標準のボーンの制限がかかります。

muscle システムでは、最小の回転角から最大の回転角までを[-1, 1]にマッピングします。
プログラマは回転角を-1 \leqq t \leqq 1floatで指定し、腕や脚がどれだけ曲げるかを制御できます。

言葉だけではうまく伝わらない気がするので、実際に muscle を使って腕を動かしてみましょう。
Left Forearm Stretchという muscle の値を 0,-1.0,1.0 と変えてみます。
すると結果は以下のようになりました。

muscle=0
muscle=0

muscle-1
muscle=-1.0

muscle=1
muscle=1.0

AnimationRgging(Unity 公式パッケージ)の BoneRenderer コンポーネントによって
腕や指のボーンを可視化して見やすくしています。

図を見るとわかる通り、muscle の値が-1 であれば腕を限界まで曲げ、1 のときに最大まで伸ばしています。

スクリプトによるmuscle制御

それでは muscle をスクリプトで制御する方法を見ていきましょう。

Humanoid モデルに対して以下のスクリプトを作成し、アタッチします。

SimpleMuscleControl.cs
using System;
using UnityEngine;

namespace FingerOperations
{
    [RequireComponent(typeof(Animator))]
    [ExecuteAlways]
    public class SimpleMuscleControl : MonoBehaviour
    {
        [SerializeField] private Animator _animator;

        // 左腕の曲げ具合
        [SerializeField, Range(-1.0f, 1.0f)] private float _leftArmStretched;

        private void Start()
        {
            if(_animator != null)
                _animator = GetComponent<Animator>();
        }

        private void Update()
        {
            if (_animator != null)
            {
                _animator = GetComponent<Animator>();
            }
            
            var handler = new HumanPoseHandler(_animator.avatar, _animator.transform);
            var humanpose = new HumanPose();
            handler.GetHumanPose(ref humanpose);

            // index=42はHumanTraitで、Left Forarm Stretchという名前で定義してあるmuscleです。
            humanpose.muscles[42] = Mathf.Clamp(_leftArmStretched, -1.0f, 1.0f);
            
            handler.SetHumanPose(ref humanpose);
        }
    }
}

ここでポイントとなるのが、以下になります。

var handler = new HumanPoseHandler(_animator.avatar, _animator.transform);
var humanpose = new HumanPose();
handler.GetHumanPose(ref humanpose);

// something...

handler.SetHumanPose(ref humanpose);

muscle を設定するには、対象の Humanoid モデルの Animator コンポーネントを取得し、それをもとにHumanPoseHandlerというオブジェクトを介してHumanPoseを取得・設定することで実現できます。

このスクリプトによってインスペクタから muscle 値を操作できます。SimpleMuscleControl クラスはExcuteAlways属性がついているので、Unity で再生ボタンを押さなくても実行できます。

simple muscle control

muscleによる指の制御

ここからやっと本題です。指を muscle で制御する方法を解説していきます。
基本的に、指の muscle 値も他と同じように[最小値, 最大値]を[-1.0, 1.0]にマッピングしたものを設定するのですが、ちょっと仕様が独特です。

まずはどの muscle が指制御に関わっているのかを調べてみましょう。HumanTrait [2]というクラスに muscle に関するいろいろな事柄が定義されているので参照してみます。

void ShowMusclesName()
{
    for (var i = 0; i < HumanTrait.MuscleCount; i++)
    {
        Debug.Log($"{i}: {HumanTrait.MuscleName[i]}");
    }
}

このような関数を実行するとすべての muscle が何を意味しているのかが一覧で取得できるので、得られた結果の中で指に関係するものだけを書き出してみました。

左手

muscle index muscle name 意味
55 Left Thumb 1 Stretched 左手の親指の第 3 関節の曲げ具合
56 Left Thumb Spread 左手の親指の開き具合
57 Left Thumb 2 Stretched 左手の親指の第 2 関節の曲げ具合
58 Left Thumb 3 Stretched 左手の親指の第 1 関節の曲げ具合
59 Left Index 1 Stretched 左手の人差し指の第 3 関節の曲げ具合
60 Left Index Spread 左手の人差し指の開き具合
61 Left Index 2 Stretched 左手の人差し指の第 2 関節の曲げ具合
62 Left Index 3 Stretched 左手の人差し指の第 1 関節の曲げ具合
63 Left Middle 1 Stretched 左手の中指の第 3 関節の曲げ具合
64 Left Middle Spread 左手の中指の開き具合
65 Left Middle 2 Stretched 左手の中指の第 2 関節の曲げ具合
66 Left Middle 3 Stretched 左手の中指の第 1 関節の曲げ具合
67 Left Ring 1 Stretched 左手の薬指の第 3 関節の曲げ具合
68 Left Ring Spread 左手の薬指の開き具合
69 Left Ring 2 Stretched 左手の薬指の第 2 関節の曲げ具合
70 Left Ring 3 Stretched 左手の薬指の第 1 関節の曲げ具合
71 Left Little 1 Stretched 左手の小指の第 3 関節の曲げ具合
72 Left Little Spread 左手の小指の開き具合
73 Left Little 2 Stretched 左手の小指の第 2 関節の曲げ具合
74 Left Little 3 Stretched 左手の小指の第 1 関節の曲げ具合

右手

muscle index muscle name 意味
75 Right Thumb 1 Stretched 右手の親指の第 3 関節の曲げ具合
76 Right Thumb Spread 右手の親指の開き具合
77 Right Thumb 2 Stretched 右手の親指の第 2 関節の曲げ具合
78 Right Thumb 3 Stretched 右手の親指の第 1 関節の曲げ具合
79 Right Index 1 Stretched 右手の人差し指の第 3 関節の曲げ具合
80 Right Index Spread 右手の人差し指の開き具合
81 Right Index 2 Stretched 右手の人差し指の第 2 関節の曲げ具合
82 Right Index 3 Stretched 右手の人差し指の第 1 関節の曲げ具合
83 Right Middle 1 Stretched 右手の中指の第 3 関節の曲げ具合
84 Right Middle Spread 右手の中指の開き具合
85 Right Middle 2 Stretched 右手の中指の第 2 関節の曲げ具合
86 Right Middle 3 Stretched 右手の中指の第 1 関節の曲げ具合
87 Right Ring 1 Stretched 右手の薬指の第 3 関節の曲げ具合
88 Right Ring Spread 右手の薬指の開き具合
89 Right Ring 2 Stretched 右手の薬指の第 2 関節の曲げ具合
90 Right Ring 3 Stretched 右手の薬指の第 1 関節の曲げ具合
91 Right Little 1 Stretched 右手の小指の第 3 関節の曲げ具合
92 Right Little Spread 右手の小指の開き具合
93 Right Little 2 Stretched 右手の小指の第 2 関節の曲げ具合
94 Right Little 3 Stretched 左手の小指の第 1 関節の曲げ具合

思ったよりも多いですね......。よく見てみると、Stretched と Spread という種類があることに気が付きます。
人間の「手を開く」という動作は「指を伸ばす」ことと「指を広げること」の 2 つを実行しますが、まさにその操作が Unity でもできるようになっています。

実際に試してみましょう。
まず、以下のような片手の指の姿勢を記録する ScriptableObject を定義/生成し、インスペクタで値を色々変えてみます。

FingerConfig.cs
FingerConfig.cs
using UnityEngine;

namespace FingerOperations
{
    [CreateAssetMenu(fileName = "Finger Configuration", menuName = "Finger Configuration", order = 0)]
    public class FingerConfig : ScriptableObject
    {
        [Header("Thumb Config")]
        [SerializeField, Range(-1.0f, 1.0f)] public float thumb1Stretched;
        [SerializeField, Range(-1.0f, 1.0f)] public float thumb2Stretched;
        [SerializeField, Range(-1.0f, 1.0f)] public float thumb3Stretched;
        [SerializeField, Range(-1.0f, 1.0f)] public float thumbSpread;
        [Space]
        
        [Header("Index Config")]
        [SerializeField, Range(-1.0f, 1.0f)] public float index1Stretched;
        [SerializeField, Range(-1.0f, 1.0f)] public float index2Stretched;
        [SerializeField, Range(-1.0f, 1.0f)] public float index3Stretched;
        [SerializeField, Range(-1.0f, 1.0f)] public float indexSpread;
        [Space]
        
        [Header("Middle Config")]
        [SerializeField, Range(-1.0f, 1.0f)] public float middle1Stretched;
        [SerializeField, Range(-1.0f, 1.0f)] public float middle2Stretched;
        [SerializeField, Range(-1.0f, 1.0f)] public float middle3Stretched;
        [SerializeField, Range(-1.0f, 1.0f)] public float middleSpread;
        [Space]

        [Header("Ring Config")]
        [SerializeField, Range(-1.0f, 1.0f)] public float ring1Stretched;
        [SerializeField, Range(-1.0f, 1.0f)] public float ring2Stretched;
        [SerializeField, Range(-1.0f, 1.0f)] public float ring3Stretched;
        [SerializeField, Range(-1.0f, 1.0f)] public float ringSpread;
        [Space]

        [Header("Pinky Config")]
        [SerializeField, Range(-1.0f, 1.0f)] public float pinky1Stretched;
        [SerializeField, Range(-1.0f, 1.0f)] public float pinky2Stretched;
        [SerializeField, Range(-1.0f, 1.0f)] public float pinky3Stretched;
        [SerializeField, Range(-1.0f, 1.0f)] public float pinkySpread;
    }
}

finger config

この ScriptableObject を渡すと、左手の指の設定を反映させるようなスクリプトを作成し、Humanoid モデルにアタッチします。

FingerOperation.cs
FingerOperation.cs
using System;
using UnityEditor;
using UnityEngine;

namespace FingerOperations
{
    [ExecuteAlways]
    [RequireComponent(typeof(Animator))]
    public class FingerOperation : MonoBehaviour
    {
        [SerializeField] private Animator _animator;
        [SerializeField] private FingerConfig _fingerConfig;

        private void Start()
        {
            _animator = GetComponent<Animator>();
        }

        private void Update()
        {
            if (_animator != null)
            {
                _animator = GetComponent<Animator>();
            }

            var handler = new HumanPoseHandler(_animator.avatar, _animator.transform);
            var humanPose = new HumanPose();
            handler.GetHumanPose(ref humanPose);

            humanPose.muscles[55] = _fingerConfig.thumb1Stretched;
            humanPose.muscles[56] = _fingerConfig.thumbSpread;
            humanPose.muscles[57] = _fingerConfig.thumb2Stretched;
            humanPose.muscles[58] = _fingerConfig.thumb3Stretched;

            humanPose.muscles[59] = _fingerConfig.index1Stretched;
            humanPose.muscles[60] = _fingerConfig.indexSpread;
            humanPose.muscles[61] = _fingerConfig.index2Stretched;
            humanPose.muscles[62] = _fingerConfig.index3Stretched;

            humanPose.muscles[63] = _fingerConfig.middle1Stretched;
            humanPose.muscles[64] = _fingerConfig.middleSpread;
            humanPose.muscles[65] = _fingerConfig.middle2Stretched;
            humanPose.muscles[66] = _fingerConfig.middle3Stretched;

            humanPose.muscles[67] = _fingerConfig.ring1Stretched;
            humanPose.muscles[68] = _fingerConfig.ringSpread;
            humanPose.muscles[69] = _fingerConfig.ring2Stretched;
            humanPose.muscles[70] = _fingerConfig.ring3Stretched;

            humanPose.muscles[71] = _fingerConfig.pinky1Stretched;
            humanPose.muscles[72] = _fingerConfig.pinkySpread;
            humanPose.muscles[72] = _fingerConfig.pinky2Stretched;
            humanPose.muscles[74] = _fingerConfig.pinky3Stretched;

            handler.SetHumanPose(ref humanPose);
        }
    }
}

動かしてみると以下のようになります。
Stretched を動かした時には指が伸縮し、Spread を動かしたときには指が横方向に動いていることが確認できますでしょうか。

余談:角度による指制御

例えば「人差し指の第 1 関節の角度を 20 度にしたい」みたいな場合を考えましょう。
muscle の規定値をいじっていなければ HumanTrait.GetMuscleDefaultMax/Minメソッドを使って
指が最大(最小)何度まで曲げられるのかを取得できるので、
ここから角度を[min, max]->[-1, 1]にマッピングすることで解決できるでしょう。

人差し指の第 1 関節の例ですと以下のように結果が返ってきます。

Debug.Log($"max: {HumanTrait.GetMuscleDefaultMax(62)}");
Debug.Log($"min: {HumanTrait.GetMuscleDefaultMin(62)}");
max: 45
min: -45

おわりに

Unity で指制御に関する文献って実はあまりまとまっていなかったりするので、今回は備忘録的にまとめてみました。
いかがでしたか? (天下無双)

何かの参考になりましたら幸いです。
最後まで読んでいただきありがとうございました。

脚注
  1. https://docs.unity3d.com/ja/2019.4/Manual/InverseKinematics.html ↩︎

  2. https://docs.unity3d.com/ScriptReference/HumanTrait.html ↩︎

GitHubで編集を提案

Discussion

ログインするとコメントできます