デスクトップに VRM アバターを常駐させるアプリを作っていて、「利用者が自分の
VRM を持ち込める」ことを前提にした。そのときポーズの持ち回りで一日溶かしたので、
何が互換で何が互換でないかを実測つきで残しておく。
結論から言うと、モデルをまたいで使い回せるポーズ形式は実質 .vrma しかない。
何が起きたか
自作アバター用に用意した立ちポーズを、別の VRM(配布されているサンプル)に
当てたら、頭が真下を向いて顔が消えた。腕もあらぬ方向に曲がった。
ポーズのデータ自体は壊れていない。元のモデルでは正しく立つ。
原因:ボーンには「2 種類の座標系」がある
VRM を扱うライブラリ(three-vrm など)には、同じ「右上腕」に対して
2 つの見え方がある。
生のボーンノード
glTF のシーングラフにそのまま入っているノード。回転値は
そのモデルのバインドポーズ(初期姿勢)を基準にした絶対値になる。
モデリングした人が「腕を下ろした状態」で作れば、その下ろした状態が原点。
別の人が「T ポーズ」で作れば、T ポーズが原点。基準が統一されていない。
だから、あるモデルで「頭をまっすぐ前」にする回転値を、別のモデルに
そのまま入れると、そのモデルの基準からさらにその角度だけ回る。
結果、頭が真下を向く。
正規化ヒューマノイドボーン
VRM 仕様が定義している方。こちらは T ポーズを基準にした相対回転で、
どのモデルでも同じ意味になることが仕様で保証されている。
three-vrm なら vrm.humanoid.getNormalizedBoneNode('head') で取れる。
getRawBoneNode() の方が生ボーン。
ポーズを配る/使い回すなら、必ず正規化側で書く。
// これは他のモデルに持っていける
const bone = vrm.humanoid.getNormalizedBoneNode('rightUpperArm');
bone.rotation.z = 0.16;
// これはこのモデル専用
const raw = vrm.humanoid.getRawBoneNode('rightUpperArm');
raw.quaternion.set(...); // バインドポーズ依存
.vrma が答えだった
VRM Animation (.vrma) は VRM のために作られた
アニメーション形式で、ヒューマノイド基準で記録される。仕様として
モデル非依存が保証されている。変換も推測もいらない。
VRoid が無料の .vrma を配っているので、動作確認にはそれが使える。
three-vrm-animation で読める。
import { createVRMAnimationClip, VRMAnimationLoaderPlugin } from '@pixiv/three-vrm-animation';
loader.register((parser) => new VRMAnimationLoaderPlugin(parser));
const gltf = await loader.loadAsync(url);
const clip = createVRMAnimationClip(gltf.userData.vrmAnimations[0], vrm);
mixer.clipAction(clip).play();
createVRMAnimationClip は VRMLookAtQuaternionProxy が無いと警告を出す。
動くが、視線を扱うなら自分で作って渡した方がいい。
Unity の .anim は変換できない
「Unity Humanoid のアニメーションを変換すればいいのでは」と考えて、
かなり粘ったができなかった。理由を残しておく。
Unity Humanoid の .anim は「マッスル値」で記録される。
Left Upper Leg Front-Back のような名前で、-1〜1 の正規化値が入る。
名前は公開されているので、どの関節のどの軸かは分かる。しかし
マッスル値 0 が何の姿勢を指すのかが公開されていない。
実際に手元の「普通に立っているポーズ」を開くと、こうなっている。
Left Lower Leg Stretch: 0.999
膝を伸ばして立っているだけなのに、値は上限近く。
つまり 0 は T ポーズではない。基準姿勢が非公開なので、
-1〜1 を角度に戻す変換式が作れない。
ここは諦めて、.anim を読み込もうとしたら
「この形式は変換できない。.vrma を使ってほしい」と理由つきで
表示するようにした。黙って失敗するより、何をすればいいか分かる方がいい。
クォータニオンとオイラー角を混ぜない
もう一つ踏んだ罠。
初期姿勢をクォータニオンで作り、そこにオイラー角の成分を足していく、
という書き方をしていた。これは合成として成立しない。
同じポーズを両方の方法で作って角度差を測ったら 22.8度 ずれた。
小さい角度なら気づかないが、腕のように可動域が大きい部位で破綻する。
回転の合成はクォータニオンで統一すること。
ひねり(twist)は腕を曲げてから効く
これは知らないと詰まる。
upperArm の y 軸回転(腕の長軸まわりのひねり)は、
腕がまっすぐ下りているときはほとんど見た目が変わらない。
長軸まわりに回しても、腕の位置は動かないため。
ところが肘を曲げると、前腕が体の前を横切るように大きく動く。
「腕を組む」ポーズが作れなかったのは、これに気づいていなかったから。
肩を回そうとしていたが、正解は「ひねってから肘を折る」だった。
// 腕を組む
{ bone: 'rightUpperArm', axis: 'y', fn: (t) => hold(t) * 0.85 }, // ひねる
{ bone: 'rightUpperArm', axis: 'z', fn: (t) => hold(t) * 0.16 },
{ bone: 'rightLowerArm', axis: 'y', fn: (t) => hold(t) * 1.42 }, // 肘を折る
衣装によって「作れないポーズ」がある
最後に、技術というより設計の話。
腕を大きく動かすポーズは、袖の広い衣装だと角度が正しくても破綻する。
袖が平らな塊になる。これはスキニングの限界で、ポーズ側では直せない。
「どの VRM でも成立させる」という前提を置くなら、
体幹と頭で作るポーズに寄せるしかない。重心の移動、視線、上体の傾き。
腕を使うものは、肘を折るだけの小さいものに留める。
地味だが、これが一番効いた。
まとめ
| やりたいこと | 使うもの |
|---|---|
| モデルをまたぐアニメーション |
.vrma 一択 |
| コードで書く立ちポーズ | 正規化ヒューマノイドボーンへの相対回転 |
| そのモデル専用の作り込み | 生ボーンでもよい(配布はできない) |
Unity の .anim の変換 |
できない。基準姿勢が非公開 |
- 回転はクォータニオンで統一する
- ひねりは肘を曲げてから効く
- 衣装によって作れないポーズがある。腕より体幹
同じところで溶かす人が減れば。
こういう検証をしながら、VRM アバターをデスクトップに常駐させるアプリを作っています。
ここで書いた「正規化ボーンで書く」「.vrma を使う」はそのまま実装に入っています。