GodotでAI生成レベルを使う:GLBから本物のシーンにするまで

生成したロケーションをGLBでGodotに読み込み、コリジョン・オクルーダー・ライティングと、作業を捨てずに済む再インポート手順まで。

Cuberta でエージェントが構築した町。通り、横断歩道、れんが造りの集合住宅とコテージ

.glb を Godot のプロジェクトフォルダに置けば、それはもうシーンです。プラグインも変換ツールも要らず、「1単位は何メートルか」と聞いてくるダイアログも出ません。glTF は Godot 自身のドキュメントが推奨と明記している形式で、インポーターはエンジンに同梱されています。生成されたロケーションから「実際に歩き回れるもの」までの最短経路であり、そして簡単なのはここまでです。ファイルが運べないものは結局あなたが組みます。コリジョン、オクルーダー、ライティングの判断、そして先週の作業を捨てずに済む再インポートの道筋です。

なぜ glTF が Godot への最短経路なのか

Godot の対応フォーマット一覧は優先順位についてはっきりしています。glTF 2.0 は推奨と書かれ、テキストの .gltf とバイナリの .glb の両方に対応します。FBX も使えます。4.3 以降は古い FBX2glTF ではなく ufbx インポーターを通ります。.blend も直接読めますが、この経路は裏で Blender 自身の glTF エクスポート機能を呼んでいます。

解釈し直すものが何もありません。Godot は右手系・Y軸が上・カメラの前方が -Z という座標系で、これは glTF 2.0 仕様が定める規約そのものです。どちらもメートル単位で動きます。ソースで 2.1m の扉はビューポートでも 2.1m の扉で、スケール係数に誰も触りません。形式そのものの比較はゲームエンジンにおける GLB と FBXの記事にあります。

以降のすべてを形づくる帰結が一つあります。Godot は元の3Dファイルに上書き保存できません。隣に .import を書き、変換後のリソースはプロジェクト内の隠しフォルダ .godot に置きます。.glb が唯一の真実であり、インポートされたシーンは毎回そこから作り直されます。サポートは書き出したプロジェクトでも第一級なので、同じファイルを GLTFDocument でランタイムに読み込むこともできます。

Cuberta はテクスチャ込みで GLB を書き出すので、エージェントが敷いた街区 — 道路、交差点、歩道、建物のブロック、ストリートファニチャー、ライティング — が一つのファイルとして届きます。res:// にコピーするだけです。

街区一つぶんで効いてくるインポート設定

FileSystem ドックで .glb を一度クリックすると、Import ドックに項目が並びます。デフォルトの多くはキャラクターには妥当ですが、町ひとつぶんの規模を想定したものはほとんどありません。

設定街区ならこうする理由
Meshes > Generate LODsオンのまま遠景の建物が手作業なしでポリゴンを落とし、4.6 で複数パーツのメッシュの形状保持が改善されました。
Meshes > Create Shadow Meshesオンシャドウパス用に頂点を溶接します。インポート時に一度払い、毎フレーム帯域を節約します。
Meshes > Light Baking本当に焼くときだけ Static Lightmapsインポート時に UV2 を展開します。大きなファイルで最も時間のかかる部分です。
Meshes > Lightmap Texel Size0.5 以上デフォルトの 0.2 は一部屋のスケールで、街区のものではありません。
Meshes > Ensure Tangentsノーマルマップがなければオフ出力が小さく、インポートが速くなります。
glTF > Embedded Texture HandlingExtract Textures実体のある画像ファイルになり、VRAM圧縮やミップマップを個別に設定できます。
Animation > Import静的なジオメトリならオフ読み込むものがありません。

ドック下の Advanced… ボタン、あるいはファイルのダブルクリックで Advanced Import Settings ダイアログが開きます。ノード単位の作業はここで行います。左に glTF 内の全ノードのツリー、中央にプレビュー、右にノードのオプション。書き出しに必ず紛れ込む補助ジオメトリ用の Skip Import もここです。

編集可能にする:継承シーンとインスタンス

インポートされたシーンは直接編集できません。これは意図的です。ツリー全体が再インポートのたびに作り直されるので、中に加えた変更は捨てられます。.glb に対して Scene > Open Scene… を選ぶと Godot は継承シーンの作成を提案します。右クリックの New Inherited Scene も同じ場所です。

そこで明記された制限はちょうど二つ。ベースシーンのノードは削除できないこと(追加はどこにでもできます)、そしてサブリソースをその場で編集できないことです。マテリアルは Actions… > Extract Materials が答えで、.tres を書き出しシーンの参照を張り替えます。もう一つの手はロケーションをレベルの .tscn にインスタンス化し、変更をそちらに持たせることです。

  • ロケーションに属するもの — コリジョンのプロキシ、オクルーダー、LightmapGI ノード、ナビゲーション領域。継承シーンへ、.glb 一つにつき一つ。
  • ゲームに属するもの — 初期位置、トリガー、スポーン、開く扉。それをインスタンス化するレベルシーンへ。
  • マテリアルに属するもの — 路面のカスタムシェーダー。抽出した .tres へ。設計上、再インポートを生き延びます。

街区が支払えるコリジョン

Godot がコリジョンを作る経路は二つです。Advanced Import Settings のノード単位の設定では、Generate > Physics が PhysicsBody3D の親を作り、コリジョンシェイプは MeshInstance3D の兄弟になります。Body Type で StaticBody3D / RigidBody3D / Area3D を選び、Shape Type で形状を選びます。ドキュメントの言い方は率直で、Trimesh は三角形単位の正確なコリジョンだが Static ボディでしか使えず、「静的なレベルジオメトリには Trimesh を使え」とあります。Decompose Convex には Precision があり、上げるほど詳細になりCPU負荷も上がります。

もう一つはソースファイルのノード名から読まれるサフィックスです。-col は三角形メッシュの静的コリジョンを子として付け、-colonly は見えるメッシュを消して StaticBody3D だけを残し、-convcol-convcolonly は凸包版です。区切りは -$_ のいずれでもよく、大文字小文字は区別されません。これは逆方向にも知っておく価値があります。生成されたオブジェクトが shop_col という名前だと驚くからです。nodes/use_node_type_suffixes を false にすれば、この仕組みは黙ります。

  • 地面、道路、歩道、縁石、階段 — Trimesh。プレイヤーがその上を歩くので、形状の忠実さがすべてです。
  • 建物の外殻 — Trimesh にしない。3万ポリゴンのファサードは3万ポリゴンのコリジョンになりますが、あなたはそれにぶつかるだけです。手作業のボックスを一つ二つで足ります。
  • ストリートファニチャー、柵、街灯 — 凸包で。プレイヤーが届かない場所なら何も付けない。
  • 植生 — たいていは何も付けない。葉のカード一枚ごとのコリジョンは、誰も同意していない請求書です。

Godot 自身の指針はもっと簡潔です。可能なら三角形メッシュや凸包ではなく、少数のプリミティブ形状を使うこと。4.6 からデフォルトの3D物理エンジンは Jolt になりましたが、trimesh の建物で埋めた町を良い考えにするほど速くはありません。

ライティング:焼くか、SDFGI に任せるか

項目LightmapGISDFGI
ベイク工程GPUで数分。ジオメトリを変えるたびにやり直しなし。カスケードがカメラ周辺に構築される
UV2の要否必要 — Light Baking を Static Lightmaps に不要
動的オブジェクトプローブ経由で間接光を受け取るGIを受け取るが、寄与はしない
反射なし。ReflectionProbe か Sky と併用独自の反射。不透明マテリアルのみ
実行時コストほぼゼロ。内蔵GPUでも通るGodot が備えるGI手法で最も高価
崩れる場面イテレーション速度カメラの高速移動。カスケードの切り替わりが見える

ドキュメントには、この議論を決着させたように見える一行があります。

手続き的に生成されたレベルには適さない。

この判断はランタイムで生成されるレベルについてのもので、その範囲では正しいものです。一度生成し、書き出して凍結したロケーションは、建てられた建物と同じただの静的ジオメトリで、ベイクは問題なく機能します。落とし穴は「凍結した」がどれだけ背負っているかです。港をまだ動かし大通りを広げている間は、再インポートのたびにベイクが無効になり、街区のベイクは分の単位です。

したがって切り分けは技術的というより時間的です。レイアウトが動いている間は SDFGI で進めます。Environment で有効にするだけでベイクは一切不要で、求められるのはメッシュのグローバルイルミネーションモードが Static であることだけ。それを制御するのがインポートドックの Light Baking です。レイアウトが落ち着いたら Static Lightmaps に切り替えて一度焼きます。UV2 の展開結果は再インポートをまたいでキャッシュされ、生成される .unwrap_cache ファイルはバージョン管理に含めるべきです。これがマシンやエンジンのバージョンをまたいで UV2 を一致させます。

オクルージョンカリング、町はほとんど壁でできているので

Godot の最適化ドキュメントは町を題材にしています。通りを歩くとき見えているのは数棟の建物と空と鳥が数羽ですが、素朴なレンダラーは一本後ろの通りも、そこにいる人々も、その先の建物も送り込みます。深度プリパスが省いてくれるのはシェーディングであって、送信ではありません。

プロジェクト設定で Rendering > Occlusion Culling > Use Occlusion Culling をオンにします。Advanced トグルが必要ですが再起動は不要です。OccluderInstance3D を追加し、選択した状態で3Dビューポートの Bake Occluders を押します。このベイクが効くかどうかは三点で決まります。

  • ベイクされるのは MeshInstance3D ノードだけです。MultiMeshInstance3D、パーティクル、CSG は無視されるので、MultiMesh へ移した植生はオクルーダーではなくなります。
  • 半透明マテリアルは除外されます。ガラスの塔が並ぶ広場は何も遮蔽しません。ドキュメントは、小さな部屋が多く不透明な壁が整った屋内で最も効くと述べています。
  • カルマスクと簡略化を使うこと。Bake > Cull Mask で動的オブジェクトを外し、Bake > Simplification は精度とCPU時間を交換します。見えているはずのものが消え始めたら値が高すぎます。

オクルーダーはインポート時にも作れます。Generate > Occluder が Mesh + Occluder と Occluder Only を提供し、Simplification Distance で調整できます。名前サフィックスの -occ-occonly はソース側から同じことをします。実際に何が切られているか見るには Perspective > Display Advanced > Occlusion Culling Buffer を開きます。

ノード数、インスタンシング、そこから苦しくなる場所

生成された街区は、書き出したオブジェクトの数だけ MeshInstance3D になって届きます。数百ノードそれ自体は問題ではなく、問題はドローコールです。Forward+ は、MeshInstance3D ノードが同じメッシュかつ同じマテリアルを共有していれば設定なしで自動的にインスタンス化します。ただし不透明かアルファテストのマテリアルに限られ、アルファブレンドでは起きず、Mobile と Compatibility レンダラーでは一切行われません。

最初に確かめるべき落とし穴がこれです。オブジェクトごとに個別のマテリアルを吐くエクスポーターは自動インスタンシングを黙って無効にし、同一の街灯が40本あれば40ドローコールになります。ポリゴン数より先にマテリアル数を見てください。Extract Materials して重複を一つのリソースに向け直すほうが、メッシュに対してできるどんな作業よりフレームタイムに効くことがよくあります。メッシュ側はそれ自体が別の題材です。

数千単位で来るもの — 草、石畳、柵の一区画、木 — の答えは MultiMesh です。Godot のドキュメントは、常時処理される数千のインスタンスならサーバーを直接使い、数十万から数百万なら MultiMesh、と線を引いています。代償は個々のインスタンスがフラスタムカリングされないことで、MultiMesh は丸ごと描かれるか描かれないかのどちらかです。だから街区ごとに分けます。

距離については、インポートオプションの Mesh LOD が自動、Visibility Ranges が手動の補完です。ドキュメント自身の例がまさに市街ブロックで、低ディテールの BatchOfHouses メッシュに Visibility Range Begin を設定し、それを4棟の詳細な家の Visibility Parent にします。作業中はデバッガの Monitors タブで Objects Drawn と Draw Calls を見ていてください。どの変更が効いたかについての直感は、半分くらい外れます。

一週間を失わない再インポート

このワークフローが本物かどうかを決めるのは次の場面です。ジェネレーター側で町を変え、書き出し、location.glb を上書きし、Godot が再インポートする。Godot 側で二日かけた作業はどうなるのか。

一部は設計上そのまま残ります。抽出したマテリアルは再インポートで上書きされません。ただしソースでマテリアル名を変えるとリンクが切れます。UV2 の展開キャッシュは残り、ファイルに保存したアニメーションは追加したトラックを保持します。静かに残らないのは、インポートされたツリーへ NodePath で届いているものすべてです。書き出しが Building_014 の名前を変えたり兄弟の順序を入れ替えたりすれば、継承シーンのオーバーライドはどこにも着地しません。4.7 時点で未解決の問題もあり、GLB 再インポート後に継承シーンを保存するとノードの unique id が再生成され、.tscn の差分がノイズで埋まります。

  • 自分の作業は子ではなく兄弟に置く。コリジョンのプロキシ、オクルーダー、LightmapGI ノード、スポーン地点は自分のノードの下に置き、名前を自分で決められないインポートノードの子にはしません。
  • import script を使う。Import ドックの Import Script > Path に、_post_import(scene) を持つ EditorScenePostImport を指定します。名前パターンによるコリジョンの割り当て、GIモードの設定、マテリアルの差し替え — コードで書けるものは再インポートのたびに自分で走ります。
  • 分割して書き出す。街区ごとに .glb を一つ、あるいは建物タイプごとに一つとレイアウト用に一つ。そうすれば港の変更は港だけを再インポートし、仕上げ済みの旧市街には触れません。Cuberta では、エージェントに建てさせる場所と建てさせない場所を指定することがそれに当たります。

このワークフローにおける Godot の優位は本物ですが、狭いものです。取り除いてくれるのはフォーマットの摩擦 — 変換工程も、単位の交渉も、軸の反転もない — で、同じロケーションを20回再インポートする立場になると、これは聞こえるよりずっと価値があります。取り除いてくれないのはレベルデザインです。インポートされたツリーは名前の付いたジオメトリにすぎません。コリジョン、オクルージョン、ライティング、そして何を一つのオブジェクトとし何を千個とするかの判断は、依然としてあなたの仕事です。自動化する価値があるのは一点だけ、Reimport を押すたびにそれらが同じやり方で起きるようにすることです。