.blendはWebで開けない — Blender→Webは変換ではなく「GPUが食べられるものだけ残す翻訳」
3Dゲームを作ったのにモデルファイルが0個の理由まで
Blenderで作ったものをWebに出すのは、ファイル形式の変換ではない。GPUがそのまま食べられるものだけを残して、残りを捨てる翻訳だ。運ばれるのは形であって、絵ではない。読み終えれば、exportで何が消えるかを前もって知り、「Blenderではきれいだったのに、Webだとなんでこうなる」に驚かずに済む。
3Dゲームなのにモデルファイルが0個
うちで作った3Dかくれんぼゲームは、広場をNPC 24人が歩き回り、その中にプレイヤーが紛れ込む作りだ。three.jsで動いている。ところがリポジトリを探しても、.glb .gltf .fbx .blend がひとつもない。アバターはコードが箱で組み立てている。
const GEO = {
head: new THREE.BoxGeometry(0.42, 0.42, 0.4),
torso: new THREE.BoxGeometry(0.58, 0.68, 0.32),
limb: new THREE.BoxGeometry(0.17, 0.62, 0.2)
}
「Blenderで人をひとり作って入れればいいのでは?」この一言が飛ばしている段階が四つある。
1. .blendはやり取りするためのファイルではない
.blend は、Blenderがメモリに持っていた構造体をほぼそのままダンプしたファイルだ。内部構造がバージョンに縛られていて、まともに読めるのは事実上Blender自身だけ。ブラウザに .blend ローダーがないのは、誰も作らなかったからではなく、交換用に設計されたことがないからだ。Photoshopの .psd をそのままWebに載せないのと同じだ。
2. glTFはGPU寄りの保存形式
glTF 2.0(.gltf + .bin、ひとまとめにすると .glb)が標準になったのは圧縮率のおかげではない。保存の構造が最初からGPU寄りになっている。面は三角形に分割済みで、頂点データはバイナリバッファに入っているので、読んだものをほぼそのままGPUバッファに載せられる。マテリアルはPBR metallic-roughness(金属っぽさと粗さで質感を表す方式)の値いくつかで決まっている。
| 形式 | Webでは |
|---|---|
.blend |
開けない。Blender専用 |
| OBJ | 開けるがアニメーションがない |
| FBX | Autodeskの独自規格 |
| glTF / GLB | 事実上の標準。three.jsも <model-viewer> もそのまま読む |
3. exportは無損失ではない — ここが肝
| Blenderにあるもの | export後 |
|---|---|
| モディファイアスタック(Subdivision、Array、Boolean) | 頂点に焼き込まれるか、消える |
| ジオメトリノード・プロシージャルマテリアル | テクスチャにbakeしないと消える |
| Cyclesのノードグラフ | PBRの値いくつかに潰れる |
| 物理・パーティクル・シミュレーション | なし |
Blenderのビューポートの絵は、EEVEEやCycles(Blenderのレンダラー)が計算した結果だ。Webが受け取るのは頂点とテクスチャとPBRの定数だけで、絵はブラウザが一から描き直す。だから描画結果がBlenderと違って見えるのはバグではなく、もともとそうなるものだ。
4. ローダーからGPUまで
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
うちのリポジトリにはthree 0.183.1が入っていて、このパスがそのまま通る。GLTFLoaderがJSON(シーン構成)とバイナリバッファを読んで BufferGeometry・Material・Texture を作り、それがGPUに載った後は毎フレームの描画呼び出し(ドローコール)になる。
Webに出す方法は二つに分かれる。見せるだけなら <model-viewer> Webコンポーネントに .glb のURLを差せば終わり。うちの別のアプリもこの方式だ。シーンやゲームに入れて照明・カメラ・衝突を自分で握るなら、three.js + GLTFLoaderだ。
ではうちのゲームはなぜGLBを使わなかったか
面倒だったからではなく、ゲームのルールから出た結論だった。このゲームのルールは「プレイヤーはNPCにできない動作はできない」。だから歩く動作は時間ではなく累積移動距離に縛ってある。止まれば脚も止まる。その場で足踏みしていたら、それが一番の目印になってしまうからだ。
const s = Math.sin(walkPhase) * swing // walkPhaseは累積移動距離から出る
avatar.legL.rotation.x = s
avatar.legR.rotation.x = -s
Blenderで焼いたwalkアニメーションを AnimationMixer で回すと時間基準になる。距離基準に縛り直すには mixer.setTime(walkPhase) を自分で与える必要があり、その瞬間「Blenderでアニメーションを焼いてきた」利点はほとんど消える。
おまけに全員が同じに見えなければならない。隠れる人だけ違うところがひとつでもあれば、それがそのまま答えになる。モデルを使うと、シャツの色を変えるためだけにマテリアルを分ける作業がかえって増える。制約が厳しいほど、コードで作るジオメトリが有利になる。
性能はまだ宿題
今は一人あたり6パーツなので、メッシュは150個前後ある。ジオメトリを共有しているのでメモリは節約できているが、InstancedMesh を使っていないので、ドローコールはメッシュの数だけ出る。GLBのスキンメッシュに替えればメッシュ数は人数分に減るが、頂点シェーダーにスキニングのコストが乗る。どちらが安いかは測ってみないとわからず、モバイルでのFPS比較はまだやれていない。
3DモデルをWebサービスやゲームに載せることで悩んでいるなら、negabaroに聞いてみてほしい。上の判断も、このゲームを作りながら自分たちで下したものだ。
キーポイント
.blendは交換用の形式ではない。Webに出すならglTF / GLBにexport
モディファイア・ジオメトリノード・Cyclesノード・物理はexportで焼き込まれるか消える
見せるだけなら<model-viewer>、シーン・ゲームならthree.js + GLTFLoader
アニメーションを時間以外に縛るなら、コードで作るジオメトリのほうが合うことがある