会社のサイトのトップでは、three.js で棒人間のキャラクターが動いています。コードで組んだ 3D のマスコットで、骨は無く、動きも手続きで作った簡単な物です。これをもっと生き生きと動かしたくて、画像から 3D を作る Pixal3D でマスコットを 3D にし、Mixamo で骨と既成の動きを付けられるかを技術検証しました。サイトには入れず、手元の確認ページだけで試しました。

その前に、画像から作った人物を整える検証をしていました。中の空洞を詰める直し(中空の殻を外から塗りつぶして詰める件)で表面が想定より凸凹し、色も参考画像どおりにはならず(4方向の絵を投影して色を貼る件)、見た目は粗いままでした。マスコットは形が単純なので、強くならして表面をなめらかにし、色の画像をやめて目を黒い粒にすれば、静止した見た目は整います(第2回で書きます)。ところが、動かすと崩れました。この連載は、その崩れを追った4回です。先に書いておくと、サイトに入れられる仕上がりにはならず、試して止めました。うまくいかなかった試しの話として読んでください。

第1回は、Pixal3D の 3D をそのまま Mixamo に上げて骨を入れ、three.js で動かすまでです。ここで最初の崩れ(腕を上げると脇が裂ける)に出会いました。

結論

  • Mixamo は GLB を受け付けません。OBJ にして、UV と色の画像を付けて zip で上げると、目の黒が骨入れ後の FBX に埋め込まれ、three.js でそのまま出ました。
  • 骨の名前が同じなので、動きだけの FBX を本体の AnimationMixer にそのまま当てられます。歩く・走るは、体の揺れや重心の移りがあり、手続きの動きより生き生きして見えました。
  • 腕を上げる動き(喜ぶ・手を振る)で、脇の胴の面が腕に引っぱられて裂けました。元のモデルの腕が体に近いと、自動の骨入れが胴の面まで腕の骨に割り当てます。
  • 腕を体から離した絵から作り直すと大きな裂けは消えましたが、脇の折れ目とお腹のしわが残りました。Mixamo の自動の骨入れだけでは足りず、重み(どの骨がどの面を動かすか)を手で直す必要があります。

試した環境

  • 絵: Codex で描いたマスコットの絵
  • 画像 → 3D: Pixal3D。AMD の GPU(ROCm)で動かすための移植版 trellis.cpp を使いました(移植版の作り方)。出力は GLB で、このマスコットは頂点 8.2 万・三角形 12.6 万でした。
  • 骨入れと動き: Mixamo の自動の骨入れ。Adobe のアカウントでログインして使います。
  • 再生: three.js の FBXLoader と AnimationMixer

流れは次の4段です。Mixamo では骨入れの後、本体と動きを別々の FBX で落とします。

絵から three.js で動かすまで

動きを別の骨格に写す方法もありますが(Mixamo の動きを 2.6 頭身の人物に写す件)、ここでは骨入れまで Mixamo に任せました。

GLB を OBJ と色の画像にして上げる

Mixamo は GLB を受け付けない

Mixamo に上げられる形は FBX・OBJ・ZIP だけで、GLB は受け付けませんでした。手元の 3D が GLB なのは、使った移植版の出力の形がそうだからで、Pixal3D そのものの制約ではありません。

そこで、依存の無い Node のスクリプトで、GLB を OBJ・MTL・色の画像の3つに書き出しました。メッシュも材質も1つずつで、頂点の番号が 32 ビットの GLB を前提にした簡単な物です。要所は次の4つです。

  1. GLB の頭から JSON の長さを読み、その後ろにある BIN の始まりを出す
  2. 頂点を 100 倍して cm にし、いちばん低い点を 0 にして足を床に置く(Y が上、正面が +Z)
  3. UV の V を 1 から引いて、OBJ の向きに合わせる
  4. 色の画像(baseColorTexture)は、バイトをそのまま書き出して MTL から指す
const len = b.readUInt32LE(12);
const j = JSON.parse(b.slice(20, 20 + len).toString());
const binStart = 20 + len + 8;
const S = 100;
// 頂点: cm にして、足を 0 に
lines.push(`v ${(b.readFloatLE(o) * S).toFixed(4)} ${((b.readFloatLE(o + 4) - minY) * S).toFixed(4)} ${(b.readFloatLE(o + 8) * S).toFixed(4)}`);
// UV: V を反転
lines.push(`vt ${b.readFloatLE(o).toFixed(6)} ${(1 - b.readFloatLE(o + 4)).toFixed(6)}`);
// 色の画像を取り出し、MTL から指す
const texIndex = j.materials[0].pbrMetallicRoughness.baseColorTexture.index;
const image = j.images[j.textures[texIndex].source];
fs.writeFileSync(path.join(outDir, `${name}.mtl`), `newmtl body\nKd 1 1 1\nmap_Kd ${name}.${ext}\n`);

OBJ・MTL・色の画像を zip にまとめると 2.5MB でした。

目の黒は、色の画像を付けないと消える

形だけの OBJ を上げると、骨入れ後の FBX では目が凹みだけになりました。目の黒は色の画像の側にあるので、形だけでは凹みしか残りません。

UV と色の画像を付けて zip で上げると、Mixamo は skin あり(肌つき)の FBX に色の画像を埋め込み、three.js で読むとそのまま目の黒が出ました。本体の FBX は、色なしで 5MB、色つきで 9MB です。

骨を入れて、three.js で再生する

Mixamo の印(あご・手首・肘・膝・股)は、左右対称の設定にして片側に置けば済みました。骨入れは1分ほどです。本体は「T ポーズ・skin あり」、動きは「skin なし・30fps」で落とし、歩く・走るは「In Place」にしました。動きの FBX は1本 0.2〜0.35MB です。

再生は、FBXLoader で本体を読み、動きの FBX の animations[0] を同じ AnimationMixer に当てるだけです。本体と動きで骨の名前が同じなので、動きだけの FBX がそのまま使えます。

const loader = new FBXLoader();
const body = await loader.loadAsync('mascot.fbx');   // T ポーズ・skin あり
const mixer = new THREE.AnimationMixer(body);
const walk = await loader.loadAsync('walking.fbx');  // skin なし
mixer.clipAction(walk.animations[0]).play();

一つ迷いやすいのは、骨を入れ直した本体には、前の本体用に落とした動きが使えないことです。骨の休みの向きが変わるためです。骨を入れ直したら、Mixamo で本体を切り替えて、動きを落とし直しました。

動かして見ると、歩く・走るは、体の揺れや重心の移りがあり、手続きの動きより生き生きして見えました。既成の動きを借りること自体はできる、というのが最初の手応えです。

Mixamo の歩く動きを当てたマスコットと、手続きで動かしていた人形を並べた画面

腕を上げたマスコットの脇の面が、扇のように引き裂かれている様子

腕を上げると、脇の胴の面が裂けた

腕が体に近い見本では、扇のように裂ける

困ったのは腕を上げる動き(喜ぶ・手を振る)です。最初のモデルは腕が体に近い見本の絵から作った物で、喜ぶ動きでは脇の胴の面が腕に引っぱられて、扇のように大きく裂けました。

腕が体に近い見本から作ったモデルで、喜ぶ動きの脇が扇のように裂けた画面

元のモデルの腕が体に近いと、自動の骨入れが胴の面まで腕の骨に割り当てます。腕を上げると、その胴の面まで腕と一緒に持ち上がります。Mixamo で動きを落とすときの Character Arm-Space(腕と体の間隔)を 82 にしてみましたが、待機の姿勢でも脇に穴が残りました。

腕を体から離した絵を頼む

それなら元の絵で腕を体から離せばよいと考え、Codex に今のマスコットの絵を添えて、同じ比率のまま描き直してもらいました。最初に「腕を真横に伸ばした T ポーズ」と頼むと、腕の上の縁が頭の下端に触れました。頭が大きく低いこの体型では、腕を真横にすると頭とぶつかります。腕を水平から約 20° 下げる指定にすると、頭から離れました。効いた指定は次の3つです。

- Both arms stretched straight out to the sides, fully extended, angled about 20 degrees downward from horizontal (a wide A-pose), palms facing down, mitten hands open and flat.
- The top of each arm starts clearly below the head: a visible band of background separates the top edge of each arm from the bottom of the head. The arms must not touch the head or the body anywhere; only the arm roots join the upper body.
- Legs straight, feet about shoulder width apart, with visible background between the legs.

ほかに、正面から胸の高さで、ほぼ平行投影で描くこと、無地の背景で影を付けないこと、文字を入れないことも添えました。腕と頭、腕と胴、脚どうしのあいだに背景が見えていることを、絵の段階で確かめておくのが要点です。

腕を水平から約 20° 下げ、頭と胴から離して描き直したマスコットの絵

大きな裂けは消え、折れとしわが残る

この絵から Pixal3D で作り直し、同じ手順で骨を入れました。喜ぶ動きの大きな裂けは消えましたが、胴の脇に折れ目が残り、待機の姿勢でもお腹に横じわが出ました。

腕を離した絵から作り直したモデルの喜ぶ動き。大きな裂けは消え、脇の折れ目が残る

絵の頼み方で減らせるのは、腕が胴にくっついていることによる大きな裂けまでで、その先は Mixamo の自動の重みのままでは消えませんでした。きれいにするには、Blender などで重みを手で直す必要があります。

まとめ

  • Mixamo は GLB を受け付けないので、OBJ・MTL・色の画像の zip にして上げる。色の画像を付けないと目の黒が消える。
  • 動きだけの FBX は、骨の名前が同じなので本体の AnimationMixer にそのまま当たる。骨を入れ直したら動きは落とし直す。
  • 腕が体に近いモデルでは、自動の骨入れが胴の面を腕の骨に割り当て、腕を上げると脇が裂ける。頭の大きい体型では、腕を水平から約 20° 下げた絵にすると頭とも胴とも離せる。
  • それでも脇の折れ目とお腹のしわは残り、重みを手で直す必要がある。

確かめていないこと

  • 道具を持つ・ロゴを押すなど、手の位置が決まった動きを、既成の動きと腕の逆算(IK)で作れるか。

おわりに

第1回で分かったのは、Pixal3D の 3D でも Mixamo の自動の骨入れと既成の動きは通り、歩く・走るなら手続きの動きより生き生きすること、ただし腕を上げる動きは自動の重みのままでは崩れることです。次回は、この重みを Blender で直していき、直すほど別の所が崩れた話を書きます。

※ Mixamo の受け付ける形や設定、three.js の振る舞いは執筆時点の情報で、いまは変わっている可能性があります。