今日の記事は、Unity
のAdvent Calendarの9日目の記事です。
概要
VR開発をしていると、キャタクター(アバター)を表現する方法として頭と手だけの簡易的アバターか、通常のキャラクターモデルを用いたアバターの2種類から選ぶことになります。
そして後者、キャラクターモデルを利用したアバターを制作する際に利用できそうな方法を見つけたので、それのメモです。
具体的には、Avatar
のインスタンスをランタイム時に生成し、それをMecanimとしてキャラクターにアサインする方法となります。
ちなみに、今回の実装のヒントは、OptiTrackというモーションキャプチャーのシステム用に提供されているプラグインの中のソースコードを参考にしました。
プラグインはフリーで、以下からダウンロードすることができます。
実際に使ってみたのがこちら。Avatarなのでアニメーションを簡単にコピー、複製することができます。
Avatarを使ってVRIKのモーションをコピーして影分身してみたw pic.twitter.com/Z8sQW07ddw
— edom18@AR / MESON (@edo_m18) 2017年12月9日
この記事のサンプルはGitHubに上がっています。(ただ、元のサンプルはVRIKを使っているため、その部分はコメントアウトしてあります。もし実際に動くものを見たい場合はご自身でVRIKを導入してご確認ください)
必要クラス
今回利用するクラスは以下です。
それぞれが、アバターを構成するための情報を保持、伝達するためのものです。
- Avatar
- AvatarBuilder
- HumanBone
- SkeltonBone
- HumanDescription
- HumanPose
- HumanPoseHandler
- HumanTrait
大まかな手順
大まかな手順は、(Avatar
の設定を行ったことがある方であればイメージしやすいと思いますが)Configure Avatar
ボタンを押下して編集モードに入ったときにできることをプログラムから行う、というイメージです。
具体的には、HumanBone
とSkeletonBone
を用いてスケルトンとモデルの構造を定義し、それらを関連付け、最後に関節の曲がり具合などを設定した「設定オブジェクト(HumanDescription
)」とともに、Avatar
を生成する、という形です。
この設定を行う際、適切にセットアップが終わっていないと最後のAvatarBuilder
でビルドする段階でエラーが出てしまうので、セットアップは気をつける必要があります。
百聞は一見にしかず、ということで、まずは実際にコードを見てもらったほうがいいでしょう。
Avatarのセットアップコード
各種ボーンの設定と、アバターのビルドを行うメソッドの抜粋です。
/// <summary> /// アバターのセットアップ /// </summary> private void Setup() { // HumanBoneのためのリストを取得する string[] humanTraitBoneNames = HumanTrait.BoneName; List<HumanBone> humanBones = new List<HumanBone>(humanTraitBoneNames.Length); for (int i = 0; i < humanTraitBoneNames.Length; i++) { string humanBoneName = humanTraitBoneNames[i]; Transform bone; if (_transformDefinision.TryGetValue(humanBoneName, out bone)) { HumanBone humanBone = new HumanBone(); humanBone.humanName = humanBoneName; humanBone.boneName = bone.name; humanBone.limit.useDefaultValues = true; humanBones.Add(humanBone); } } List<SkeletonBone> skeletonBones = new List<SkeletonBone>(_skeletonBones.Count); for (int i = 0; i < _skeletonBones.Count; i++) { Transform bone = _skeletonBones[i]; SkeletonBone skelBone = new SkeletonBone(); skelBone.name = bone.name; skelBone.position = bone.localPosition; skelBone.rotation = bone.localRotation; skelBone.scale = Vector3.one; skeletonBones.Add(skelBone); } // HumanDescription(関節の曲がり方などを定義した構造体) HumanDescription humanDesc = new HumanDescription(); humanDesc.human = humanBones.ToArray(); humanDesc.skeleton = skeletonBones.ToArray(); humanDesc.upperArmTwist = 0.5f; humanDesc.lowerArmTwist = 0.5f; humanDesc.upperLegTwist = 0.5f; humanDesc.lowerLegTwist = 0.5f; humanDesc.armStretch = 0.05f; humanDesc.legStretch = 0.05f; humanDesc.feetSpacing = 0.0f; humanDesc.hasTranslationDoF = false; // アバターオブジェクトをビルド _srcAvatar = AvatarBuilder.BuildHumanAvatar(gameObject, humanDesc); if (!_srcAvatar.isValid || !_srcAvatar.isHuman) { Debug.LogError("setup error"); return; } _srchandler = new HumanPoseHandler(_srcAvatar, transform); _destHandler = new HumanPoseHandler(_destAvatar, _targetAnimator.transform); _initialized = true; }
ボーンのセットアップ
まず冒頭で行っているのが、ボーンのセットアップです。
ボーンには2種類あり、HumanBone
とSkeletonBone
の2種類です。
「人間の構造」を定義する「HumanBone」と実際の「SkeletonBone」
あくまで自分の理解で、という前置きが入りますが、HumanBone
は人間の構造を定義するためのボーンです。
そして実際のモデル(アバターに適用するオブジェクト)のボーン構造を示すのがSkeletonBone
です。
なぜこのふたつのボーン情報が必要なのかというと、モデルの中身を見たことがある人であればすぐにピンと来ると思いますが、モデルデータには人間にはないボーンが仕込まれている場合があります。
MMDなどは特にそれが顕著で、「よりよく見せるため」のボーンが仕込まれていたりします。
(例えばスカート用のボーンだったり、髪の毛用のボーンだったり)
そのため、人間の構造と同じ構造でボーンを定義することはほとんどなく、いくらかのボーンが人間の構造とは違った形になっているため、「実際のボーン構造のうち、どれが人間の構造としてのボーンか」を定義する必要がある、というわけです。(と理解しています)
そして、その関連付けを行っているのが、それぞれname
プロパティで指定される名称です。
どうやらUnity内では名称でそのマッチングを行っているようです。
なので、「人間としてのこのボーンは、対象モデルではこういう名称ですよ」という関連付けが必要、というわけですね。
コードとしては以下の部分ですね。
humanBone.boneName = bone.name;
// ... 中略 ...
skelBone.name = bone.name;
そして、「人間としてのどこのボーンか」という情報はhumanBone.humanName = humanBoneName;
で指定しています。
こうして、人間としてのボーンがどれか、というマッチングを行うことで、Mecanimではその情報を元にアニメーションしている、というわけのようです。
最初、SkeletonBone
が人間のボーン構造を示すものだと思って、それだけのTransformを指定して配列を生成していたんですが、「hoge Transformは fuga Transformの親じゃないとダメだよ」みたいなエラーが出て、少しハマりました。
今回の実装ではSkeletonBone
は以下のように、ルートから再帰的にTransform情報を拾って配列化して設定しています。
/// <summary> /// 再帰的にTransformを走査して、ボーン構造を生成する /// </summary> /// <param name="current">現在のTransform</param> private void RecursiveSkeleton(Transform current, ref List<Transform> skeletons) { skeletons.Add(current); for (int i = 0; i < current.childCount; i++) { Transform child = current.GetChild(i); RecursiveSkeleton(child, ref skeletons); } }
Unityが規定した名称を基に各ボーンの関連付けを行う
さて、もうひとつ重要なのがこの「Unityが規定した名称を基に関連付けを行う」という点です。
どういうことかというと、まずは以下のコードを見てください。
string[] humanTraitBoneNames = HumanTrait.BoneName; List<HumanBone> humanBones = new List<HumanBone>(humanTraitBoneNames.Length); for (int i = 0; i < humanTraitBoneNames.Length; i++) { string humanBoneName = humanTraitBoneNames[i]; Transform bone; if (_transformDefinision.TryGetValue(humanBoneName, out bone)) { HumanBone humanBone = new HumanBone(); humanBone.humanName = humanBoneName; humanBone.boneName = bone.name; humanBone.limit.useDefaultValues = true; humanBones.Add(humanBone); } }
HumanTrait.BoneName
というstring
型の配列から値を取り出し、それと、自分が定義した_transformDefinision
の中に値が含まれているかのチェックをしています。
このHumanTrait.BoneName
が、Unityが規定しているボーンの名称で、具体的にはNeck
などの人体の部位の名称が設定されています。
そしてここで定義されている名称とのマッピングを行っているのが_transformDefinision
なのです。
これ自体はシンプルに、インスペクタから手で設定してもらったTransformを設定しているだけです。
生成部分は以下のようになります。
/// <summary> /// アサインされたTransformからボーンのリストをセットアップする /// </summary> private void SetupBones() { _transformDefinision.Clear(); _transformDefinision.Add("Hips", _hips); _transformDefinision.Add("Spine", _spine); _transformDefinision.Add("Chest", _chest); _transformDefinision.Add("Neck", _neck); _transformDefinision.Add("Head", _head); _transformDefinision.Add("LeftShoulder", _leftShoulder); _transformDefinision.Add("LeftUpperArm", _leftUpperArm); _transformDefinision.Add("LeftLowerArm", _leftLowerArm); _transformDefinision.Add("LeftHand", _leftHand); _transformDefinision.Add("RightShoulder", _rightShoulder); _transformDefinision.Add("RightUpperArm", _rightUpperArm); _transformDefinision.Add("RightLowerArm", _rightLowerArm); _transformDefinision.Add("RightHand", _rightHand); _transformDefinision.Add("LeftUpperLeg", _leftUpperLeg); _transformDefinision.Add("LeftLowerLeg", _leftLowerLeg); _transformDefinision.Add("LeftFoot", _leftFoot); _transformDefinision.Add("RightUpperLeg", _rightUpperLeg); _transformDefinision.Add("RightLowerLeg", _rightLowerLeg); _transformDefinision.Add("RightFoot", _rightFoot); _transformDefinision.Add("LeftToes", _leftToes); _transformDefinision.Add("RightToes", _rightToes); }
あとは、この設定されたリストとマッチングして、該当のボーンの名称を、前述のように設定していく、という感じになります。
再掲すると以下の部分です。
humanBone.boneName = bone.name;
// ... 中略 ...
skelBone.name = bone.name;
人間の特性を定義する「HumanDescription」
ボーンのセットアップが終わったら、そのボーン構造を持つ人の特性がどんなものか、を定義する「HumanDescription」構造体を利用して、手の関節の回転などの状態を定義します。
HumanDescription humanDesc = new HumanDescription(); humanDesc.human = humanBones.ToArray(); humanDesc.skeleton = skeletonBones.ToArray(); humanDesc.upperArmTwist = 0.5f; humanDesc.lowerArmTwist = 0.5f; humanDesc.upperLegTwist = 0.5f; humanDesc.lowerLegTwist = 0.5f; humanDesc.armStretch = 0.05f; humanDesc.legStretch = 0.05f; humanDesc.feetSpacing = 0.0f; humanDesc.hasTranslationDoF = false;
アバターをビルド
以上で必要なデータが揃いました。
あとは、そのデータを利用して、アバターオブジェクトをビルドしてやれば完了です。
_srcAvatar = AvatarBuilder.BuildHumanAvatar(gameObject, humanDesc); if (!_srcAvatar.isValid || !_srcAvatar.isHuman) { Debug.LogError("setup error"); return; }
注意点として、ビルド後にエラーがないかのチェックが必要です。
上でも書きましたが、ボーンの構造などの状態がおかしいと、この時点でエラーが表示されます。
構造として適切でない場合はビルド時にエラーが出るのと同時に、isValid
とisHuman
のフラグがfalse
になるので、それをチェックして、失敗していた場合はやり直すなどの処置が必要になります。
(最初はここでエラーが出て、若干ハマった)
ただ、エラーを見てみるとしっかりと理由が書かれているので、それを元に修正していけば問題は解決できると思います。
無事ビルドが終わったら、HumanPoseHandler
を設定し、以後はUpdate
メソッド内でアバターの状態をコピーしてやれば完成です。
HumanPoseHandler
は、現在の状態を取得するためのハンドラ。
_srchandler = new HumanPoseHandler(_srcAvatar, transform); _destHandler = new HumanPoseHandler(_destAvatar, _targetAnimator.transform);
上記で取得したハンドラを用いてGet/Set
メソッドを用いて、Get
したポーズを、以下のようにして対象のアバターにコピーします。
private void Update() { if (!_initialized) { return; } if (_srchandler != null && _destHandler != null) { _srchandler.GetHumanPose(ref _humanPose); _destHandler.SetHumanPose(ref _humanPose); } }
GetHumanPose
メソッドでポーズ情報を取得し、SetHumanPose
でコピー先のHumanPoseHandler
にセットしてやれば動きが同期されるようになります。
ちなみに、感のいい方であればピンと来ているかもしれませんが、このコピー先を複数用意してやれば、いくらでも同じ動きをするモデルを用意することができます。
Mecanimによるアバター制御の恩恵が受けられる、というわけですね。
最後に
冒頭で載せた動画は、このアバターの仕組みを使って「アバターの現在のアニメーション状態をコピー」することで実現しています。
Avatarをランタイムで生成できることで、色々な値を調整することが可能になるのでVRには非常に適したものかなと思っています。