前説
お久しぶりです。2021 年の春頃に『VRM はローカル軸の方向を破棄すべきでない』という記事を書かせて頂きました。その後、VRM 1.0 の仕様策定リポジトリにおいて
という決定がなされ、MIRO 氏の方からもアナウンスがありました。
ところで、議論の最中 MIRO 氏と直接対話させて頂く機会があったのですが、モデルごとに固有のローカル軸を保持することによって、『異なるモデル間でのアニメーションの共有・互換性について』非常に気にされておられるようでした(旧来の仕様ではローカル軸を破棄していた=ローカル軸が完全一致していた)。
結論から言うと、ローカル軸が異なるモデル間でもアニメーションの共有はある程度可能です。
この記事は『ローカル軸が統一されていない場合(VRM 0 の正規化を行わない場合)に起こりうる問題とその解決法』を僕が個人的に Godot を用いて検証したものをまとめたものとなります。
ポーズとレストについて再認識
実は冬休みに Godot 4 に向けてリターゲットの実装を行っていました。
ここで注意すべきなのが、Godot 3 から Godot 4 に変更するにあたって、ポーズの扱いが変わる事です。
Godot 3 ではポーズの中にレスト成分(ローカル軸の方向)が含まれないのに対し、Godot 4 ではポーズの中にレスト成分が含まれるようになります。
なんのこっちゃかと思われるかもしれませんが、簡単に図に表すとこんな感じです。
要するに Godot 3 ではポーズの値を全て 0 にするとレストになりましたが、Godot 4 ではポーズの値を全て 0 にすると全てのボーンが原点に集まります。
なぜそんな変更を加えるかと言うと、Godot 4 のアニメーション形式の方が glTF に格納されているデータそのままに近いからです。
この記事においては glTF のアニメーション形式をほぼそのまま採用している Godot 4 のアニメーションフォーマットを基準に説明を行います。ポーズの中にレスト成分が含まれているという点に注意して下さい。
リターゲットの分類
スケルトン構造の異なる 3D モデル間で破綻なくアニメーションを共有するには、アニメーションのリターゲットが必要です。前回の記事ではリターゲット要らないって言ってただろ! とツッコまれると困るので、ここで新たにリターゲットの方式について、大きく二つに分類します。
- 破壊的リターゲット
- 非破壊リターゲット
順に説明します。
破壊的リターゲット
『モデルのローカル軸を別のモデルのローカル軸に変更する』というリターゲットです。VRM 0 が行っていたローカル軸の破棄もこれに該当します。モデルのローカル軸を変更する為、元のローカル軸を参照するコンストレイントや IK 等が壊れる可能性があります。この理由から前回の記事では破壊的リターゲットを行なうことをやめるように主張しました。
非破壊リターゲット
僕が実装している Godot 4 のリターゲットシステムがこちらに該当します。モデルのローカル軸には手を加えず、アニメーション側に変換を施すので、これによってモデルに設定されているコンストレイントや IK 等が壊れることはありません。前回の記事の最後の方で『アニメーションに対して正規化を行なうべき』と書きましたが、その具体例がこの非破壊リターゲットとなります。
ちなみに、Unity Humanoid は一応 Muscle 制御の外では元のレストの値を保持しているので、非破壊リターゲットに分類されると考えてもよいでしょう。
非破壊リターゲットの実装
glTF が持つアニメーションデータをモデルに転送(非破壊リターゲット)する方法は、大きく3つに分けられると考えています。とりあえず暫定的に僕が Godot で使った名前を使って説明させて貰います。
- AbsoluteAnimation
- LocalAnimation(ローカルリターゲット)
- GlobalAnimation(グローバルリターゲット)
位置とスケールについては処理が追加されて説明が長くなるので、一旦、式などについては最も重要となる回転についてのみ説明を行います。前提としてクォータニオンについて、ざっくり理解できる程度の知識があればよいのですが、なくてもなんとなく分かって貰えるように尽力します。
左:Godette / CC by SirRichard94
中:コアトリーエ・クーランジュ (C) fuka mizno
右:アリシア・ソリッド / CC by DWANGO Co., Ltd.
上記のモデルを用いて、順に説明を行います。
前回の記事を読んでいただいた方には分かると思いますが、『関節ルールが同じ』というのは『ローカル軸の向きが完全に一致している』という意味ではないという点に注意して下さい。
AbsoluteAnimation
各ボーンのローカル座標における、クォータニオンの初期値 Quaternion(0, 0, 0; 1)
からの回転を転送します。レスト成分が含まれているため、ローカル軸を一致させていないと初期ポーズの時点でモデルが壊れます1。
実装は以下のようになります。
[抽出]
AnimQ = SrcPoseQ
[転送]
TgtPoseQ = AnimQ
実装は非常に単純です。
VRM 0 の正規化はこの実装においてはかなり有用と言えます。ただし有用なのは回転においてのみであり、位置やスケールにおいては解決されません。例を挙げると、脚長のモデルの位置アニメーションを短足のモデルに当てはめると、短足のモデルの脚が伸びてしまいます。
どこぞやのゲームのチートバグ動画みたいになってしまっていますが、実装が単純という事はすなわち処理的にも最も軽いので、アニメーションを作成したモデルとアニメーションを転送するモデルが全く同じであるという前提においてはこの実装が用いられることが一般的です。しかし異なる形状のモデル間でアニメーションを共有するのであれば、この実装では不十分であることは一目瞭然です。
LocalAnimation(ローカルリターゲット)
各ボーンのローカル座標における、レストからの相対的なローカル回転を転送します。
ちなみに、Godot 3 や Blender においてはこの LocalAnimation が暗黙的に採用されています。言い換えれば、それらのソフトにおいては単なるアニメーションの流し込みに過ぎないのですが、便宜上これもリターゲットと呼ぶことにします。
実装は以下のようになります。
[抽出]
AnimQ = SrcRestQ.inv * SrcPoseQ
[転送]
TgtPoseQ = TgtRestQ * AnimQ
まず、抽出においてはソースモデルのポーズにソースモデルのレストの逆クォータニオンをぶつけています。ポーズにはレスト成分が含まれているので、ソースモデルのポーズからレスト成分を抜いたもの、つまり相対的な回転のみが抽出される事になります。
ポーズがレスト成分とその差分であると捉えて抽出式を表すと、以下のようになります。
[抽出]
SrcPoseQ = SrcRestQ * SrcDiffQ
AnimQ = SrcRestQ.inv * SrcPoseQ
AnimQ = SrcRestQ.inv * SrcRestQ * SrcDiffQ
AnimQ = SrcDiffQ
転送に関しては、逆にターゲットモデルのレストにソースモデルのポーズの差分成分が合成されると考えて下さい。
AbsoluteAnimation と異なり、アニメーション(=ポーズ)にはレスト成分が含まれない為、初期ポーズは壊れません。また、脚が長くなったりすることもありません。
LocalAnimation は関節ルールが一致している場合に骨折が起こらないという点で有用です。関節ルールを一致させるとは、例えばヒザを曲げる軸が +X 軸なのか -X 軸なのかを、モデル間で統一することです。
僕が前回の記事で『関節ルールを決めろ』と主張していたのは LocalAnimation において骨折を防止できるという事と、先程も書いたようにいくつかの環境においてはこの LocalAnimation が一般的な実装であるという二点によるものです。
MMD 用モデルでは、この関節ルールについて慣例的なものがあるという話も聞きます2。また、ソフトウェアの自動設定に任せることもできるため、結果的に多くのモデルで関節ルールが一致するようです。とはいえそれらの値は MMD モデルの仕様として定まっているわけではありません。
VRM 1.0 ではローカル軸の方向を破棄しないことがひとまず決定しました。関節ルールに関してもあらかじめ明確な定義を設け、周知に努める必要があるでしょう。
GlobalAnimation(グローバルリターゲット)
では、モデルスペースにおけるグローバル回転だけを転送できれば、ローカル軸が揃っていなくてもある程度アニメーションを共有できるのではないでしょうか。やってみましょう。
方法はいくつか考えられますが、最も簡潔な実装として以下の式を用います。
[抽出]
AnimQ = SrcPGRestQ * SrcPoseQ * SrcRestQ.inv * SrcPGRestQ.inv
[転送]
TgtPoseQ = TgtPGRest.inv * AnimQ * TgtPGRest * TgtRest
PG というのは Parent Global の略で、要するに親のグローバルレストです。グローバルレストの算出方法は、親から子へ順に各レストの値を乗算すればよいです。このとき、レストはポーズとは異なり、モデルを読み込んだ後に変更されることは基本的にない為、グローバルレストの値を毎回乗算で算出する必要がないよう、どこかにキャッシュしておくことを推奨します。
一気に複雑になったように見えますが、本質的には VRM 0 の正規化と同様の事をアニメーションに対して行っているだけです。抽出時にはアニメーションの値の座標空間を Quaternion(0, 0, 0; 1)
にして格納し、転送時にはアニメーションの値の座標空間をターゲットモデルが持つボーンのローカル空間に戻しているのです。
つまり、このようにして抽出された GlobalAnimation と VRM 0 で正規化したモデルの LocalAnimation は等しいという事が言えます。
詳しく見ていきましょう。抽出においては、ポーズの差分成分にグローバルレストを適用しています。これにより回転の座標空間がグローバル(モデルスペース)の座標空間となります3。
[抽出]
AnimQ = SrcPGRestQ * SrcPoseQ * SrcRestQ.inv * SrcPGRestQ.inv
AnimQ = SrcPGRestQ * SrcRestQ * SrcDiffQ * SrcRestQ.inv * SrcPGRestQ.inv
AnimQ = SrcGRestQ * SrcDiffQ * SrcGRestQ.inv
転送においては、同じ要領で逆にアニメーションをターゲットモデルのボーンの親の座標空間に持っていき、ターゲットモデルのレストに対して左から乗算を行えば4、結果的にグローバル回転が転送されます。
親指などの、モデルによって向きが異なりやすいボーンでは骨折が起こる可能性はありますが、上でも触れたように VRM 0 と同じ事を内部的に行いアニメーションが転送されているので、VRM 0 で出来ていたことが出来なくなるということはありません。
ここで改めて VRM 0 の正規化とは何だったのかを考えてみましょう。
そもそもなぜ VRM 0 ではモデルの正規化が必要とされたのでしょう。MIRO 氏はアニメーションの共有を容易に行うためだと主張していましたが、アニメーションの共有は、先のような処理を加えることによってモデルの正規化なくとも実現できます。アニメーションを共有したいのであれば、まずアニメーションフォーマットを定義すべきであり、VRM 0 においてそれが行われなかったことが不幸であったと考えています。
アニメーションの転送をランタイムで行うための実装難易度は環境によって異なりますが、最悪インポーターがモデルの読み込み時に随時アニメーションを変換するような実装でもこれらの処理は可能です。
ただし、グローバルリターゲットだけあればよいというわけではありません。何度も言うように、グローバル変形には骨折の問題があります。
この例だと、特に親指に注目すると分かりやすいと思うのですが、ローカルリターゲットを用いたほうが、アニメーションが適切に転送されている事が確認できます。関節の向きがハッキリと分かる四肢や手指においては、骨格を尊重したアニメーションが転送されるという点で、グローバルリターゲットよりも『関節ルールを合わせた上でローカルリターゲットを用いる』という方式が推奨されるべきだと考えています。
VRM 1.0 ではモデルのローカル軸が保持されるようになるので、グローバル・ローカル設定を含むアニメーションフォーマット、並びに関節ルールが定義されることを期待します。
スケールのグローバルリターゲット
さて、回転に関してはクォータニオンの計算のみを考えれば良かったのですが、位置とスケールに関しては少し要領が異なってきます。
先にスケールのグローバルリターゲットから説明すると、まずスケールというのはクォータニオンの元となる基底変換行列の中に含まれます。この時クォータニオンを使わずに行列の乗算を行えばスケールは回転と同時に変換されますが、 グローバルリターゲットにおいてはせん断が発生する恐れがあります。
glTF においてはローカルのせん断5は許可されておらず、開発環境によってもローカルのせん断が許可されるかどうかはまちまちです。また、せん断を回避する方法はいくつかありますが6、それぞれ微妙に結果が異なるので、VRM のコンセプトである挙動の一貫性の為に、スケールをどう処理するか、またはスケールのアニメーションを許可するかしないか等を吟味する必要があります。
位置のグローバルリターゲット
位置に関しては、ポーズとレストの位置差分を親のグローバルレストで回転させる事で抽出・転送が可能です。この Vector3 の回転操作は Godot においては xform()
として実装されており、Unity においては Vector3 とクォータニオンの乗算として実装されています。
ここで問題となるのが位置の移動値が、モデルごとのボーンの長さの違いを考慮せずに転送されてしまうということです。要するに、脚長のモデルの歩行アニメーションがあるとして、それを短足のモデルに転送すると、短足のモデルが滑るように移動してしまいます。これを防ぐためには何らかの係数が必要なのですが、例えば Unity Humanoid においては恐らく Hips の地面からの高さのような値を用いていると考えられます。位置アニメーションに関しては、移動値をこのような係数で除算して正規化し格納するか、あるいは係数を別で持つかという事を吟味する必要があります。
ちなみに、移動値にだけこんな問題がある訳ではなく、実際は回転にも関節の可動域といったような係数を用いるべきなのですが、一般的な人型モデルではそれらの可動域がほぼ同一とみなされるために問題となっていないだけです。
IKについて考察
ここまでは FK アニメーションの話をしてきました。IK についてはきちんと検証までは出来ていないので、簡単に考察という形でローカル軸が揃っていない場合に IK にどのような影響が出るのかまとめていきたいと思います。
IK は大別して、『回転軸(ポールベクター)及びロール軸を完全に推定するもの』と『回転軸及びロール軸を事前に決め打ちしておくもの』とに分けられると思います。また、前者のオプションとして軸を手動で設定する、いわゆる複合型の物も考えられます。よくある、ヒジやヒザの関節を少しだけ曲げた状態でモデルを制作するという行為は、回転軸を推定するために必要です。
『回転軸及びロール軸の推定』のみを行っている場合は、モデル間のローカル軸が揃っていなくともさほどの影響はありません。一方、『回転軸及びロール軸の事前の決め打ち』を行っている場合には悪影響が出ると予想できます。
ここからが重要なのですが、それらの軸の決定方法に関わらず、モデルによってはボーンを曲げる方向が壊れるという IK は共通して、利用モデルそのもので直接 IK を実行しているように見受けられます。
リターゲットが可能であるというのは上記で触れたので、IK を実行する為の中間リグを用意して回転のみを利用モデルに転送すれば、トラッキングのようなケースにおいては7ボーンが壊れるという問題は防ぐことが可能だと考えております。中間リグには任意の IK アルゴリズムやそれに最適化されたリグを採用出来ます。
ちなみに、Blender における Rigify 等は、この中間リグ方式に近い形で実装されているように思います。Rigify はコンストレイントを用いていますが、非破壊リターゲットとコンストレイントは本質的にはほぼ同じ事を行っていると考えています。
モデル:逆脚のトゥーリエ (C) fuka mizno
極端な例を挙げると、中間リグとローカルリターゲットを合わせることで逆関節にも対応できますが……このようなケースではモデルにコンストレイントを組み込むほうが一般的かもしれません8。
中間リグ IK を採用することにより、回転の転送のみを行うようになった場合に問題があるとすれば、例えば中間リグが膝を上げて胸につけた時、短足のモデルでは膝が胸につかないというようなケースが考えられます。これはモデルによっては取ることが不可能なポーズというのが出てくることに起因するものなので、特に中間リグ IK だけがそのような問題を抱えているというわけではないのですが……。そのようなケースをどう解決していくかは今後の課題としたいと思います。
骨の数の違いへの対応
これに関しては特に VRM 0 や VRM 1.0 に限った話ではないのですが、アニメーションを生成したモデルよりもアニメーションを転送するモデルの方が骨の数が少ない時に、その差分をどう吸収するか考えておく必要があります。
例えば UpperChest にアニメーションを入れたとして、UpperChest を持たないモデルでは全くアニメーションしないと、それら2つのモデルを並べた時に明らかに不自然です。このケースにおいては、例えば Unity Humanoid においては Chest に変形を合成することで解決をしています。
VRM がルックスの一貫性を重視するのであれば、この骨の数の違いに対する処理を定義しておく必要があると考えております。
まとめ
既存のシステムが壊れるケースについて
結局、VRM 正規化の廃止によって既存のシステムが壊れるケースに関しては以下の通りです。
- AbsoluteAnimation または LocalAnimation を用いているケース
- IK に手動で軸の設定を行っているケース
AbsoluteAnimation はそもそもアニメーションの共有方法としては使われるべきではありません。LocalAnimation や IK においては関節ルールさえ決めておけば問題にならず、またグローバルリターゲットを用いた回避方法があるということを上で説明しました。
要点の箇条書き
- リターゲットには破壊的リターゲットと非破壊リターゲットの2種類ある
- VRM 0 はアニメーションフォーマットを持たないために、モデルに対して破壊的リターゲットを行っていたので問題を抱えていた
- VRM 1.0 ではモデルの関節ルール、並びにグローバル・ローカル設定を持つアニメーションフォーマットを定義すべき
- ローカル軸が揃っていない場合でもグローバルリターゲットによりアニメーションの共有がある程度可能である
- 関節ルールを定義しておけばローカルリターゲットを行い、骨折を防止したアニメーションの共有が可能である
- 位置のアニメーション関しては Hips の高さなどの係数を何らかの形で格納する必要がある
- スケールのアニメーションに関してはせん断についてどう処理を行うのかを吟味する必要がある
- IK については中間リグを用いた実装が推奨される
- 関節ルールを定義しておけばより多くの IK に対して柔軟に対応できる
- 非破壊リターゲットや中間リグ IK の実装難易度はコンストレイントとあまり変わらないか、もしくはそれ以下と推測される
- モデル間の骨の数の違いに対してどう対応するのかを吟味する必要がある
後書き
正直な所、モーショントラッキングシステムや Unity 外のパイプラインに携わるエンジニアの方々からはローカル軸の方向を保持して欲しいという希望の方が多かったように GitHub のリアクションや Twitter における反応から読み取っています。故に、それらのシステムに対して『ローカル軸を保持するようにする』という変更が問題となる事はほとんど無いと予想しております。
今後、ローカル軸を用いた素敵なシステムが開発されていくことを期待します。
また、前回の記事に引き続き、諸々協力していただいた V-Sekai コミュニティの方々にもこの場を借りて感謝を申し上げます。
-
初期ポーズからボーンを動かさずにキーフレームを打ったアニメーションですら正しく共有ができないという意味です。 ↩
-
MMD のローカル軸は編集のために存在しているものであり、ローカルリターゲット及びローカルアニメーションが実装されているというわけではありません。 ↩
-
Blender で変形を適用した後に Transform の値が全て初期値になるのと同じようなイメージです。 ↩
-
クォータニオンは乗算において交換法則を持ちません。左乗算と右乗算の違いについてざっくり説明すると、親子関係を持つ2つのオブジェクトがあるとして、乗算の左の要素は親を回転させて右の要素は子を回転させるイメージです。 ↩
-
ローカルのせん断とグローバルのせん断があります。Blender のようなローカルのせん断を許可しない環境においても、例えば親オブジェクトのスケールが
Vector3(0.5, 2, 2)
のような時に子オブジェクトを回転させると見た目上はせん断が発生します。各ローカルの基底変換行列自体は直交化されているが、親子関係によりせん断が生じる事をグローバルのせん断と呼んでいます。 ↩ -
行列からスケールを一時的に取り出し、正規直交化した上でクォータニオンで回転演算を行い、取り出していたスケールを戻す方法や、行列をそのまま乗算した後で正規直交化する方法などがあります。 ↩
-
物にぶつかった時に腕が曲がるというようなリアクションを行うケース、またはドアノブを握るというような明確なターゲットが存在するケースにおいてはモデル側のボーンの長さが重要になるので、中間リグ方式がどのケースにおいても有用であるとは一概に言えません。そのようなケースにおいては中間リグのボーンの長さをターゲットモデルに揃えたり、係数を用いる等すれば中間リグでもある程度解決できるかもしれませんが、関節ルールを決めた上で利用モデルそのもので IK を実行するのが良いのではないかと考えています。 ↩
-
逆関節の膝というケースでは基本的に2軸しかアニメーションしないので、ローカル軸の向きの変更のみで対応できますが、より多くのケースを考えると負スケールによる軸反転、もしくは Blender Constraint - Copy Rotation の Invert オプションのような軸反転が必要になることが考えられます。 ↩