e.blog

主にUnity周りのことについてまとめていきます

SkinnedMeshとBoneWeightについてメモ

概要

とある実装をしていて、アニメーション周りをわりと触ったのでそのメモです。
具体的には、メッシュカットを利用してメッシュを分断したあと、分断したメッシュもボーンアニメーションさせたくて実装したメモです。

実際に切断してアニメーションさせた動画↓

見てもらうと分かりますが、カットされて分断されても同じようにアニメーションしているのが分かるかと思います。

ちなみに、ボーンアニメーション(スキニング)については、前にWebGLで実装した際に解説した記事があるので、興味がある人は読んでみてください。

qiita.com

また、いつもお世話になっているマルペケさんのところの記事を参考にしたので、こちらのほうがもっと詳細です。

その61 完全ホワイトボックスなスキンメッシュアニメーションの解説

原理

スキニング自体の実装や概念については、上記の記事を参考にしてみてください。
ここでは、あくまでメッシュカットをしたときに実装した内容を元に、Unity上でどうしたらいいか、という視点から書いていきます。

とはいえ、簡単なボーンアニメーションの仕組みについて触れておきます。

ボーンアニメーションとは、ボーンと呼ばれる頂点群を定義し、そのボーンの動きに応じてポリゴンの各頂点が動かされる、という仕組みです。
では、どのボーンが動いたらどの頂点が動くのか。それを定義するのがBoneWeightです。

名前の通り、ボーンのウェイト、つまり「影響度」の値を保持します。
なので、メッシュは頂点数と同じだけのBoneWeight配列を保持しています。

BoneWeightは最大で4本までのボーンが設定できます。
ここで勘違いしないでもらいたいのは、モデル全体で4本だけ、ではなく、あくまで「1頂点に影響を与えることができるボーンは最大で4本」ということです。
これはマルペケさんの記事を見てもらうといいと思いますが、とにかくボーンの数を増やしてしまうとGPUで扱えるメモリの領域(レジスタ)がすぐに枯渇してしまうためです。

また、頂点に着目すれば、そもそも4本以上のボーンから影響を受けるケースというのは稀でしょう。
つまり、逆を言えば4本で十分とも言えます。

なのでBoneWeight構造体の定義を見てもらうと分かりますが、boneIndex0boneIndex3weight0weight3まで、配列ではなくそのままの名称で定義が書かれています。

ここで、boneIndexは、各ボーンに振られたインデックス番号、wegithは、そのボーンからどれくらいの影響を受けるかを示したものになっています。
なお、4本のボーンの影響度の合計は必ず1になるように正規化されます。

ボーンの簡単な説明は以上です。

今回はこのボーンの影響度を、メッシュカット後の各メッシュに適用し、それをアニメーションさせる、という実装を行いました。

ちなみに、メッシュカットについては以前、公開されていたコードを読んで解説した記事を書いたので、興味がある方はそちらも合わせてご覧ください。

qiita.com

登場人物

今回の実装にあたって、必要なクラスやプロパティなどがいくつか絡み合っているのでそれらを明確にします。

  • Mesh
  • SkinnedMeshRenderer
  • Animator
  • BoneWeight

Meshクラス

まずはMeshクラス。

Mesh mesh = new Mesh();
mesh.name = "Split Mesh";

// _cuttedMeshesは独自で定義したメッシュ情報を持つクラスの配列
mesh.vertices = _cuttedMeshes[i].Vertices;
mesh.triangles = _cuttedMeshes[i].Triangles;
mesh.normals = _cuttedMeshes[i].Normals;
mesh.uv = _cuttedMeshes[i].UVs;
mesh.subMeshCount = _cuttedMeshes[i].SubMeshCount;
for (int j = 0; j < _cuttedMeshes[i].SubMeshCount; j++)
{
    mesh.SetIndices(_cuttedMeshes[i].GetIndices(j), MeshTopology.Triangles, j);
}

mesh.bindposes = oriRenderer.sharedMesh.bindposes;
mesh.boneWeights = _cuttedMeshes[i].BoneWeights;

Meshはポリゴンを形成するために必要な情報をまとめて持っているクラスです。
verticestrianglesnormalsuvsubMeshCountsubMeshIndicesなどがそれに当たります。

今回はメッシュを平面で切って分割するため、新しく計算した頂点群などを再設定しています。

そしてさらに、メッシュは(前述のように)ボーンの影響度についても知らなければなりません。(アニメーションさせたい場合)
そこで設定しているのがboneWeightbindposeです。

boneWeightは上で書いた通り、各頂点が影響を受けるボーンの情報です。
そしてbindposeは、「モデルのデフォルト位置としてのボーンの姿勢」です。

人型のモデルであれば、Tポーズ時の位置、と考えるといいと思います。
つまり、「動いていない状態のボーンの位置」ということですね。

なぜこれを定義しているかというと、ボーンというのは「動いた差分分だけ、頂点に影響を与える」ようになっているからです。
つまり、ボーンがまったく動かされないならTポーズのまま、ということですね。

そしてそこから、ボーンが曲がったり移動したりすることで、モデルは様々な形に変形されます。
さらにそれを連続して行うことでアニメーションが成立している、というわけです。

SkinnedMeshRenderer

アニメーションするために必要なSkinnedMeshRenderer
ボーンに対応した頂点を、ボーンの動きに追従して動かすことでアニメーションを行います。

そしてSkinnedMeshRendererはその名の通り、スキニングされるメッシュを表示するためのクラスになります。
そのため、いくつかのボーンなどの情報を持たせる必要があります。

// objは新規生成されたGameObject
SkinnedMeshRenderer renderer = obj.GetComponent<SkinnedMeshRenderer>();
renderer.sharedMesh = mesh;
renderer.materials = mats;
renderer.bones = newBones;
renderer.rootBone = newRootBone;

メッシュは頂点群や、それらのボーンの影響度を持つ「データ構造」と言っていいでしょう。
そしてSkinnedMeshRendererは、このデータ構造を用いて適切にボーンの影響を頂点に与えます。まさにレンダラですね。

ボーンアニメーションに必要な情報はここではふたつ。
ひとつはbones、そしてもうひとつがrootBoneです。

rootBoneは、どのボーンがルートになっているのかを示す、Trasnformへの参照です。
そしてbonesは、定義されているボーンへの参照の配列になっています。

ちなみに、なにかしらのモデルデータを開いてみると分かると思いますが、ボーンとして利用されるオブジェクトのツリー構造は、必ずしもボーンだけとは限りません。
例えばなにかをグルーピングするための目的で配置されているオブジェクトなどもあるでしょう。

つまり、ルートボーン以下にはボーンとして機能していないオブジェクトも当然ながら存在します。
そのため、ボーンとしてアサインされたもののみをbones配列が持っている、というわけですね。

Animator

メッシュやレンダラの設定を適切にしても、アニメーション自体を再生しないことには動きません。
ということで、Animatorクラスが必要になります。

今回の実装では、メッシュカット対象オブジェクトが持っていたアニメーションのデータをそのままコピーして使っています。

Animator anim = newObj.AddComponent<Animator>();
anim.runtimeAnimatorController = _target.transform.root.GetComponent<Animator>().runtimeAnimatorController;
anim.avatar = _target.transform.root.GetComponent<Animator>().avatar;

AnimatorAddComponentして、適切にアニメーションに必要なデータを設定(コピー)します。

AnimatorAnimatorControllerについては、Unityでアニメーションさせたことがある人には特に説明の必要はないでしょう。
今回はコピーしていますが、切断後のアニメーションを変える場合は単なるコピーではなく、新しいコントローラを設定してもいいと思います。

そして最後にAvatarをコピーして終了です。

まとめ

今回はあくまでボーンをどうアサインしていったらいいかのメモなので以上となります。
まとめると、

  • メッシュに頂点情報およびボーンウェイト情報とボーンのデフォルト位置を紐付ける
  • SkinnedMeshRendererに、ルートボーンと、アニメーションに使われるボーンの配列を登録する
  • Animatorを通じてアニメーションを行い、AnimatorControllerのステートマシンを用いて各アニメーションを制御する

という流れになります。
データ構造的にはとてもシンプルですね。

次回の記事では、メッシュカットにボーンアニメーションを設定したあたりを書こうと思います。