用 AI 生成 Godot 关卡:从 GLB 到一个真正的场景

把生成好的场景以 GLB 导入 Godot,再为它加上碰撞、遮挡剔除、光照,以及一条不会丢掉工作成果的重新导入路径。

由智能体在 Cuberta 中搭建的小镇:街道、斑马线、红砖公寓楼与独栋住宅

把一个 .glb 放进 Godot 的项目目录,它已经是一个场景了。不需要插件,不需要转换工具,也不会弹出一个问你「一个单位等于多少」的对话框。glTF 是 Godot 官方文档标注为推荐的格式,导入器随引擎一起发布。这是从生成好的场景走到「可以在里面走动」的最短路径——而简单的部分也就到此为止。文件装不下的东西仍然要你自己搭:碰撞、遮挡体、一个关于光照的决定,以及一条不会毁掉上周成果的重新导入路径。

为什么 glTF 是进入 Godot 的最短路径

Godot 文档的格式支持页面对优先级说得很直白:glTF 2.0 被标为推荐,文本的 .gltf 和二进制的 .glb 都支持。FBX 能用——从 4.3 起走 ufbx 导入器,不再需要旧的 FBX2glTF;.blend 文件可以直接导入,但这条路在背后调用的是 Blender 自己的 glTF 导出功能。

没有任何东西需要被重新解释。Godot 使用右手坐标系、Y 轴向上、-Z 为相机前方,这正是 glTF 2.0 规范所确立的约定,而且两边都以米为单位。源文件里 2.1 米的门在视口里就是 2.1 米的门,没人需要去动缩放系数。更完整的格式之争在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 压缩和 mipmap。
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 提供精确的逐三角形碰撞,但只能配合静态刚体,而且「静态关卡几何体请用 Trimesh」。Decompose Convex 多一个 Precision,值越高碰撞越细、越吃 CPU。

另一条路是读源文件节点名里的后缀。-col 加一个三角网格静态碰撞子节点;-colonly 移除可见网格、只留 StaticBody3D;-convcol-convcolonly 是凸包版本。分隔符可以是 -$_,且不区分大小写——这一点反过来也要知道,因为一个恰好叫 shop_col 的生成物件会让你莫名其妙。把 nodes/use_node_type_suffixes 设为 false,整套机制就闭嘴了。

  • 地面、道路、人行道、路缘、台阶——Trimesh。玩家踩在这些上面,形状的忠实度就是全部意义。
  • 建筑外壳——不要 Trimesh。一个三万面的立面会变成三万面的碰撞体,而你只会撞上它。手工加一两个 box 就够。
  • 街道设施、围栏、路灯——凸包;玩家够不到的地方干脆什么都不加。
  • 植被——通常什么都不加。给每一张叶片贴片配碰撞体,是一笔没人同意支付的账单。

Godot 自己的说法更短:能用少数几个基本形状时,就别用三角网格或凸包。从 4.6 起默认的 3D 物理引擎是 Jolt——比旧的更快,但没快到能让一座 trimesh 建筑组成的城镇变成好主意。

光照:烘焙,还是交给 SDFGI

项目LightmapGISDFGI
烘焙步骤GPU 上几分钟,几何体一改就重来没有;级联围绕相机构建
是否需要 UV2需要——Light Baking 设为 Static Lightmaps不需要
动态物体通过光照探针接收间接光能接收 GI,但从不向它贡献
反射没有;需配 ReflectionProbe 或 Sky自带反射,仅限不透明材质
运行时开销接近为零;集成显卡也扛得住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。几百个节点本身不是问题,drawcall 才是。Forward+ 会在 MeshInstance3D 节点共享同一个网格并且同一个材质时自动实例化,无需设置——但仅限不透明或 alpha 测试材质,alpha 混合的永远不会,Mobile 和 Compatibility 渲染器则完全没有这项能力。

这是最该先查的坑。一个给每个物件都配独立材质的导出器,会悄无声息地关掉自动实例化,四十根一模一样的路灯就变成四十个 drawcall。先看材质数量,再看三角面数量;Extract Materials 之后把重复项指向同一个资源,对帧时间的贡献常常大于你能对网格做的一切——而网格那一侧是另一个话题

成千上万的东西——草、鹅卵石、围栏段、树——答案是 MultiMesh。Godot 文档把界线画在这里:需要持续处理的实例达到数千个时直接走服务器接口,到几十万乃至上百万时就只剩 MultiMesh。代价是单个实例没有视锥剔除:一个 MultiMesh 要么整个画,要么整个不画,所以按街区切分。

论距离,导入选项里的 Mesh LOD 是自动的,Visibility Ranges 是手动的补充。文档自己的例子就是一个城市街区:给一个低细节的 BatchOfHouses 网格设置 Visibility Range Begin,再把它设为四栋精细房屋的 Visibility Parent。做这些时盯着调试器 Monitors 标签页里的 Objects Drawn 和 Draw Calls,因为关于「哪一处改动起了作用」的直觉,大约有一半时间是错的。

重新导入而不损失一周

下面这个场景决定了整套流程是否真的可行:你在生成器里改了城镇,导出,覆盖 location.glb,Godot 重新导入。那两天在 Godot 这一侧做的工作会怎么样?

有些东西按设计就能保住。提取出来的材质在重新导入时不会被覆盖——但在源文件里改材质名会切断关联。UV2 展开缓存保得住,保存到文件的动画会保留你加进去的轨道。悄悄保不住的,是任何通过 NodePath 指向导入树内部的东西:如果导出把 Building_014 改了名,或把兄弟节点重排了顺序,继承场景里的覆盖设置就落在了空处。截至 4.7 还有一个未关闭的问题:GLB 重新导入后保存继承场景会重新生成节点的唯一 id,让 .tscn 的 diff 被噪声填满。

  • 把你的工作放成兄弟节点,而不是子节点。碰撞代理、遮挡体、LightmapGI 节点和出生点,都挂在你自己的那个节点下面,而不是挂在名字不由你决定的导入节点里。
  • 用 import script。Import 面板有一个 Import Script > Path 字段,指向一个带 _post_import(scene) 函数的 EditorScenePostImport。按名称模式分配碰撞、设置 GI 模式、替换材质——凡是能写成代码的,每次重新导入都会自己跑一遍。
  • 分块导出。一个街区一个 .glb,或者一种建筑类型一个再加一个布局文件。这样改动港口时只重新导入港口,收拾好的老城区分毫不动。在 Cuberta 里这不过是标出哪里让智能体建、哪里不建。

Godot 在这套流程里的优势真实,但很窄。它消除的是格式摩擦——没有转换步骤,没有单位谈判,没有坐标轴翻转——当你要把同一片场景重新导入二十次时,这比听上去值钱得多。它没有消除的是关卡设计。导入的树只是一堆带名字的几何体;碰撞、遮挡、光照,以及「这里是一个物件还是一千个」的判断,仍然是你的活。唯一值得自动化的,是让这些事在你每次按下 Reimport 时都以同样的方式发生。