[Astro] #174 VMD to VRMA — MMD のモーションを、ブラウザで VRM のアニメーションに変換する
はじめに
前回は、Pandoc を WASM でブラウザに載せて、文書を変換するツールでした。
[Astro] #173 Pandoc Converter — Pandoc を WASM でブラウザに載せ、文書を変換する
Markdown、Word、HTML、EPUB などの文書を、ブラウザの中で変換するツールを作りました。
lain-lab.com今回は、モーションの変換です。MMD のモーション(VMD)を、VRM で使えるアニメーション(VRMA)に変えます。変換は、ブラウザの中だけで終わります。ファイルは、外に出ません。
以前に作った VRM to MMD は、VRM を PMX にするツールでした。その逆の向き(モーション側)を、今回は作りました。作ったのは、VMD から VRMA への一方向だけです。VRMA から VMD へは、作っていません。
今回の要点は、次の 6 つです。
- VMD のボーンフレームだけを読んで、30fps で全フレームを焼き、VRMA(glTF)として書き出す
- 補間は、VMD のベジェ曲線をそのまま使う
- 腕は、A スタンスを基準にした補正が必要で、最初の式は間違っていた
- 足は、IK(足IK)を焼いて、FK の回転にする
- 変換した VRMA を、元の MMD モデルと VRM で、左右に並べて同時に再生できる
- 寄り道で、PMX の服が透ける問題の原因が見つかった(three.js の MMDToonMaterial)
完成画面
MMD と VRM を並べて、同じ時間で再生
左が、MMD のモデル(PMX と、元の VMD)です。右が、VRM(変換した VRMA)です。同じ時間で動くので、変換のずれが、そのまま見えます。
無視したボーンの一覧(ツリー)
VMD には、VRM に対応するボーンがないものが、たくさん入っています。それを、変換のたびに、グループに分けて出しています。
動画 GIF
動画 MP4
VMD と VRMA
VMD
VMD は、MMD のモーションのファイルです。ボーンごとに、「何フレーム目に、どの位置と回転か」と、補間の曲線を持っています。ボーンの名前は、Shift_JIS です。
今回読むのは、ボーンのフレームだけです。モーフ(表情)、カメラ、照明、IK の ON/OFF は、読みません。
VRMA
VRMA は、VRM Animation です。glTF のアニメーションに、VRMC_vrm_animation という拡張を足したものです。ボーンは、VRM のヒューマノイドの名前(hips、leftUpperArm など)で持ちます。
VRM の側で、three-vrm-animation を使うと、VRMA を、そのモデルに合わせて再生してくれます。VRM 0.x の場合は、x と z を反転して、hips の位置は、モデルの hips の高さに合わせて、拡大縮小されます。
自前で読み書きする
VMD の読み込みは、自前です。three.js の MMDLoader は、使っていません。モーションだけを取り出したいのと、補間を自分で扱いたいからです。
VRMA の書き出しも、自前です。three.js の GLTFExporter は、VRMC_vrm_animation を出せません。glTF の JSON とバイナリを、自分で組み立てています。
変換のコアは、vmd-core.js(約 24 KB、外部の依存なし)です。ページ側の vmd-to-vrma.js は、画面とプレビューだけです。
変換の流れ
- VMD のボーンフレームを読む
- ボーンの名前を、揃える(Shift_JIS から、全角と半角の揺れを直す)
- 30fps で、全フレームを、ベジェで補間して、焼く
- MMD の空間で、姿勢を組み立てる
- 腕と脚を、補正する
- Z を反転して、glTF の座標に直す
- VRMA(GLB)として、書き出す
名前
VMD のボーン名は、左足IK のように、全角の文字が混ざっています。NFKC で正規化すると、左足IK になります。左親指0 も、左親指0 になります。
こうして揃えたあと、VRM のヒューマノイドのボーンに、対応づけます。対応のないボーン(腕捩、足D、髪、スカートなど)は、「無視」に入れます。
ベジェ補間
VMD の補間は、キーごとに持っている、ベジェ曲線です。X、Y、Z、回転の 4 本があり、1 本が 4 つの数(x1、y1、x2、y2)です。次のキーが持っている曲線を、前のキーとの間で使います。
// 補間の曲線は、後ろのキーが持っている
const curve = next.interp; // X, Y, Z, 回転の 4 本 × (x1, y1, x2, y2)
線形、イーズイン、イーズアウトの中間が、期待どおりの値になることを、確かめています。
座標
MMD は、-Z が正面です。VRM 1.0 は、+Z が正面です。そのため、最後に Z を反転します。
- 位置:
z → -z - 回転(クォータニオン):
(x, y, z, w) → (-x, -y, z, w)
位置は、MMD の単位(1 が約 8 cm)に、0.08 を掛けて、メートルにします。
hips と spine
MMD のボーンの親子は、VRM とは違います。
- hips の回転は、
全ての親 · センター · グルーブ · 腰 · 下半身を、順に掛けたものです - spine の回転は、
下半身の逆 · 上半身です。MMD では、上半身が 腰 の子で、VRM では、spine が hips の子だからです - chest は 上半身2、upperChest は 上半身3 です
hips の位置は、全ての親、センター、グルーブ などの位置を、順に足して、0.08 を掛けて、z を反転して、rest の高さ(0.856 m)を足します。この 0.856 m は、MMD の標準モデルの下半身のピボット(10.7)に、0.08 を掛けたものです。three-vrm-animation が、VRM の hips の高さを、この 0.856 で割って、位置を拡大縮小します。
腕:A スタンスの補正
ここが、いちばん、つまずきました。
MMD の標準モデルは、腕を斜め下に下げた、A スタンスです(既定は 40°)。VRM は、腕を真横に伸ばした、T ポーズです。VMD の腕の回転は、A スタンスを基準にした差分です。VRM に移すときに、その分を、補正する必要があります。
最初の式は、間違っていた
最初は、こう考えました。「VMD の腕の回転は、ボーンのローカル軸の回転なので、軸の符号を、左右で反転すればよい」。複数の符号の組み合わせを用意して、選ぶ方式にしました。
走りのモーションで試すと、「右の腕が、横に上がる」ことになりました。別のツールでも同じ結果だったので、VMD の問題ではなく、変換の式の間違いでした。
実際の VMD を、数字で見る
実際の VMD(歩きのモーション)の腕の回転を見ると、次のとおりでした。
- 左腕:Z 軸まわりに -0.366
- 右腕:Z 軸まわりに +0.366
左右が対称で、「下げる」向きの回転です。ローカル軸の符号を反転すると、右腕が逆になってしまいます。
わかったことは、VMD の腕の回転は、ローカル軸ではなく、親のフレーム(世界の軸に揃ったもの)で見た、差分だということです。
正しい式
RA を、Z 軸まわりの回転(左が -θ、右が +θ、θ は A スタンスの角度)とします。MMD の空間で、次のようにします。
| ボーン | 式 |
|---|---|
| 肩 | q' = q |
| 上腕 | q' = q · RA |
| ひじ、手首、指 | q' = RA⁻¹ · q · RA |
そのあとに、Z を反転します。上腕にだけ、RA が残って、A スタンスから T ポーズへの差が、吸収されます。子のボーンでは、RA が打ち消し合います。
この式を、既定(A-frame)にしました。3 つほど、別の VMD で試して、すべて正常でした。以前の符号の方式は、選択肢として、残してあります。
脚:IK を FK に焼く
MMD のモーションは、足を、IK(足IK)で動かしています。VRM には、IK がありません。そのため、IK の結果を、足の回転(FK)に、焼きます。
足IKに、キーが 1 つでもあれば、IK を焼きます。そのときは、FK の 足 と ひざ のキーは、使いません。
解く
標準モデルの寸法を、固定で持っています(MMD の単位)。
- 下半身のピボット:(0, 10.7, 0)
- 足:(±0.85, 10.4, 0)
- 足首:高さ 1.4
- 太もも、すね:どちらも 4.5
- つま先 IK:足首から (0, -1.4, -1.4)
2 つのボーンの IK を、解析で解きます。ひざの向きは、下半身の正面の方向です。ひざは、X 軸だけで、後ろに曲がる向きだけにしています。
足首は、つま先 IK の位置に向けて、最小の回転で合わせます。足 IK の回転に、つま先が追従する形です。足首のモードは、3 つから選べます。
toeIK:つま先 IK に追従する、最小の回転(既定)orient:足 IK の回転を先に当てて、つま先の方向に合わせるfk:ひざまで IK、足首は FK だけ
寸法は、標準モデルのものです。VRM の脚の長さが違っても、出てくるのは、標準モデルでの IK の解の、角度です。hips の動きだけは、VRM 側が、自分の hips の高さの比で、拡大縮小します。
確かめたこと
IK を解いた結果の足首が、目標に届いているかを、順運動学で確かめました。誤差は、4e-7 でした。ひざが、X 軸だけで、後ろに曲がることも、確かめました。
VRMA の書き出し
出力は、GLB です。
- ノード:hips、spine、chest、(upperChest)、neck、head、肩、上腕、前腕、手、脚 3 本、(指)。rest は T ポーズで、単位はメートル、+Z が正面です
- 変化しないボーンは、2 つのキーに圧縮します(腕は、A スタンスの分があるので、いつも出します)
- クォータニオンは、符号が連続するように、そろえます
VRMC_vrm_animation(specVersion 1.0)に、ヒューマノイドの全ノードを書きます
glTF のバリデーターで、エラーは 0 でした(空のノードの、情報が出るだけです)。three-vrm-animation のローダーでも、読めます(ヒューマノイドの回転が 19 本、hips の位置、rest の hips は 0.856)。
プレビュー
VRM を落とすと、そのまま動く
画面に VRM を入れると、変換した VRMA を、そのまま再生します。ダウンロードするファイルと、プレビューが、同じものです。そのため、ここで合っていれば、ダウンロードしたものも、合っています。
最初は、毎回 VRM を落とすのが面倒だったので、起動時に、自動で読み込むようにしました。パスは、.astro の先頭の定数(DEFAULT_VRM)に、書いてあります。差し替えるのは、この 1 行だけです。
VMD を落とすと、すぐに変換が始まります。オプションを変えると、少し待って、また変換されます。
無視したボーンのツリー
VMD には、変換できないボーンが、たくさん入っています。最初は、ログのように、並べて出していました。意味がわからなかったので、グループのツリーにしました。
- 捩り(腕捩、手捩)
- 足D 系(足D、ひざD、足首D、足先EX)
- 目
- 顔のパーツ(まゆ、まぶた、唇、歯、舌 など)
- スカートの物理
- 髪、ツインテール、リボン
- 胸
- 指の根元ダミー
- その他
動きに影響しそうなグループ(捩り、足D 系、目)は、黄色にして、最初から開いています。それ以外は、閉じています。歩きのモーションでは、425 個のボーンが、無視されました。そのうち、動きに影響しそうなものは、17 個です。
MMD と VRM を並べる
変換が、うまくいったかを知るには、元の動きと、見比べるのが、いちばんです。そこで、MMD のモデルを、同じ画面に出しました。
- 左:MMD(PMX の ZIP と、元の VMD)
- 右:VRM(変換した VRMA)
同じ時間で動かすので、違いが、すぐにわかります。PMX の ZIP は、画面に落とせます。起動時に、自動で読み込むパスも、.astro の先頭に書いてあります(DEFAULT_PMX)。
作りかた
- MMD 側は、three.js の MMDLoader と、MMDAnimationHelper を使います。物理(髪、スカート)は、切っています
- 縮尺は、0.08 です(MMD の 1 が、約 8 cm)
- MMDLoader のモデルは、+Z が正面です。VRM 0.x は、-Z が正面です。カメラが -Z にあるときは、MMD を Y 軸まわりに 180° 回して、向きを揃えます
- 再生は、VRM が共有の時間で動き、MMD は、
helper.update(dt)を、普通に回します - 時間が巻き戻ったとき(ループ、Play)は、MMD の helper を作り直して、頭から始めます
ハマった:時間を直接指定すると、動かない
最初は、MMD 側も、時間を直接指定して(mixer.setTime(t))、helper.update(0) を呼ぶ形にしました。VRM 側と、同じ時間にしたかったからです。
ところが、PMX 側が、まったく動きませんでした。ボーンの位置を、時間を変えて読んでも、同じ値のままです。Sample の PMX では動いたのに、ページに組み込むと動かない、という状態でした。原因は、特定できていません。
直し方は、mmd_to_glb で動いていた形に、揃えることでした。helper から、メッシュを外して、作り直して、update(dt) を、そのまま回します。
function rebuildHelper() {
if (!mesh) return;
if (helper) { try { helper.remove(mesh); } catch (e) { /* 登録なし */ } }
helper = new MMDAnimationHelper({ afterglow: 0, physics: false });
helper.add(mesh, clip ? { animation: clip, physics: false } : { physics: false });
}
これで、動きました。時間を直接指定する方法は、MMDAnimationHelper とは、相性が悪いようです。
寄り道:PMX で、服が透ける
並べる比較を作る前に、PMX を用意する過程で、別の問題が出ました。VRM から変換した PMX を、mmd_to_glb で表示すると、上着の、切り抜きの部分から、スカートが消えて、下着が見える、というものです。横から見ると、透けません。
以前にも、直そうとして、直らなかった問題です。
疑ったものと、はずれたもの
最初は、PMX か、変換ツールが悪いと思いました。そのため、次のように、いろいろ試しました。
- テクスチャの alpha を、全部不透明にしたもの:透けは消えたが、切り抜きが、黒く飛び出した
- alpha が 200 以上を 255 にしたもの:透けたまま
- 完全に透明な三角形を、PMX から削る:直らなかった
- 別の Windows のアプリで変換:結果が、同じだった
PMX 専用のビューアで見ると、問題がありませんでした。つまり、PMX も、変換ツールも、正常でした。悪いのは、表示する側(mmd_to_glb)でした。
原因
three.js の MMDToonMaterial は、ShaderMaterial です。material.alphaTest を設定しても、シェーダーの uniforms.alphaTest には、反映されません。
そのため、アルファテストが、効いていませんでした。alpha が 0 のピクセルも、深度だけは、書き込みます。すると、あとから描く、スカートが、深度のテストで、隠れてしまいます。肌と下着は、先に描かれているので、見えてしまいます。
直し方
uniforms.alphaTest.value を、直接、設定します。
m.transparent = false;
m.depthWrite = true;
m.alphaTest = cutoff;
if (m.uniforms && m.uniforms.alphaTest) m.uniforms.alphaTest.value = cutoff;
m.needsUpdate = true;
もう 1 つ、注意がありました。MMDLoader は、テクスチャを、あとから読み込みます。transparent も、そのあとで、切り替わります。読み込みの直後に 1 回、設定するだけでは、足りません。manager.onLoad でも、もう一度、設定します。
顔、目、まゆ、口などの材質は、半透明の表現が必要なので、対象から外しています(名前で判定)。
Sample の PMX を、ヘッドレスの Chromium で描画して、スカートが欠けないことと、顔が普通であることを、確かめました。実際のブラウザでも、直ったことを、確認しました。
この修正は、並べる比較の MMD 側にも、入れてあります。
確認できたこと
- 合成した VMD(Node):何もキーがないとき、左右の腕が ±40° で下がる。足 IK が、目標に届く(誤差 4e-7)。ベジェの中間値。hips の位置。名前の揺れ
- VRMA:gltf-validator でエラー 0。three-vrm-animation のローダーで読める
- 実物の VMD(歩き、走り):腕の向きは、A-frame 方式で、正常。3 つほど試して、すべて正常
- 並べる比較:Sample の PMX で、ヘッドレスで動くこと。実機で、PMX と VRM が、同じ動きをすること
- 透ける問題:ヘッドレスで再現して、直ることを確認。実機でも、確認
未確認のこと・未対応のこと
- 指(特に親指)の軸:腕と同じ補正を使っている。標準モデルでも、親指は、軸が違う可能性がある
- 足 IK の寸法:標準モデルの値を、目分量で、固定している
- 表情(モーフ)、カメラ、照明:未対応
- IK の ON/OFF キー:常に ON として扱う
- 腕捩、手捩、足D 系:無視する(一覧に出る)
- 逆方向(VRMA から VMD):作っていない
- VMD は、3 つほどしか試していない
- Safari と Firefox:確かめていない
- コンソールの警告(動きには、影響しない):
unknown char code 131:MMDLoader が、VMD のモデル名を読むときの文字コードの警告VRMLookAtQuaternionProxy is not found:three-vrm-animation の警告。VRMLookAtQuaternionProxyを、先に作れば消える(まだ直していない)WebGL: INVALID_VALUE: uniform1fv: no array:MMD のマテリアルが、毎フレーム出している。描画は、正常。原因は、特定できていない
おわりに
VMD から VRMA への変換は、数式そのものより、「VMD の回転が、何を基準にしているか」を、正しく知ることが、難しいところでした。最初は、ローカル軸だと思い込んで、遠回りしました。実物の VMD の数字を、見てからは、早かったです。
変換が合っているかは、元の動きと並べると、すぐにわかります。並べる比較は、作ってよかったと思います。
寄り道の、透ける問題は、原因が、three.js の使い方の落とし穴でした。ツールが悪いと思い込んで、いろいろ試しましたが、本物の原因は、別の場所にありました。PMX 専用のビューアで、問題が出なかったことが、手がかりでした。
実装メモは、docs/vmd-to-vrma.md と、docs/vrm-to-mmd-alpha-seethrough.md に、残してあります。次にやるなら、VRMLookAtQuaternionProxy の警告と、指の軸の確認です。