[Astro] #174 VMD to VRMA — MMD のモーションを、ブラウザで VRM のアニメーションに変換する

[Astro] #174 VMD to VRMA — MMD のモーションを、ブラウザで VRM のアニメーションに変換する

はじめに

前回は、Pandoc を WASM でブラウザに載せて、文書を変換するツールでした。

今回は、モーションの変換です。MMD のモーション(VMD)を、VRM で使えるアニメーション(VRMA)に変えます。変換は、ブラウザの中だけで終わります。ファイルは、外に出ません。

以前に作った VRM to MMD は、VRM を PMX にするツールでした。その逆の向き(モーション側)を、今回は作りました。作ったのは、VMD から VRMA への一方向だけです。VRMA から VMD へは、作っていません。

今回の要点は、次の 6 つです。

  1. VMD のボーンフレームだけを読んで、30fps で全フレームを焼き、VRMA(glTF)として書き出す
  2. 補間は、VMD のベジェ曲線をそのまま使う
  3. 腕は、A スタンスを基準にした補正が必要で、最初の式は間違っていた
  4. 足は、IK(足IK)を焼いて、FK の回転にする
  5. 変換した VRMA を、元の MMD モデルと VRM で、左右に並べて同時に再生できる
  6. 寄り道で、PMX の服が透ける問題の原因が見つかった(three.js の MMDToonMaterial)

完成画面

MMD と VRM を並べて、同じ時間で再生

左に MMD モデル、右に VRM を並べて、同じ走りモーションを再生している画面

左が、MMD のモデル(PMX と、元の VMD)です。右が、VRM(変換した VRMA)です。同じ時間で動くので、変換のずれが、そのまま見えます。

無視したボーンの一覧(ツリー)

右側に、変換で無視した VMD のボーンを、グループごとのツリーで表示した画面

VMD には、VRM に対応するボーンがないものが、たくさん入っています。それを、変換のたびに、グループに分けて出しています。

動画 GIF

右側に、変換で無視した VMD のボーンを、グループごとのツリーで表示した画面

動画 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 は、画面とプレビューだけです。

変換の流れ

  1. VMD のボーンフレームを読む
  2. ボーンの名前を、揃える(Shift_JIS から、全角と半角の揺れを直す)
  3. 30fps で、全フレームを、ベジェで補間して、焼く
  4. MMD の空間で、姿勢を組み立てる
  5. 腕と脚を、補正する
  6. Z を反転して、glTF の座標に直す
  7. 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 の警告と、指の軸の確認です。