背景:3D 遮挡与 LOD为什么会变成真实问题
团队做一个小型 3D 城镇时,最初的思路很直接:把房屋、摊位、NPC、树和装饰全部放进主场景。编辑器里看起来很舒服,运行到低端显卡上却立刻露馅。玩家站在街角时,看不见的室内家具仍在渲染准备队列里,远处屋顶还是高模,广场另一头的 NPC 动画照常更新。美术觉得模型已经不算重,程序也觉得单个脚本不复杂,可组合起来就是超预算。后来我们把“看得见、看得清、用得上”拆成三层,才把场景稳定住。
3D 场景性能问题往往不是某一个模型太贵,而是可见性、细节层级和加载边界同时失控。遮挡剔除解决“被挡住还要不要画”,LOD 解决“远处要画多细”,可见距离解决“超出范围还要不要存在”,流式加载解决“什么时候把资源放进内存”。这几件事互相影响,如果只做 LOD,不做遮挡,街区背后的高楼仍然提交;如果只做遮挡,不做资源流式,内存依旧涨;如果只按距离卸载,转角处又会突然空白。
flowchart TD
A["相机位置与朝向"] --> B["区域网格/兴趣半径"]
B --> C["加载近区资源"]
B --> D["卸载远区资源"]
C --> E["遮挡剔除判断"]
E --> F{可见?}
F -- "否" --> G["保持逻辑或降级"]
F -- "是" --> H["按屏幕占比选择 LOD"]
H --> I["渲染提交"]
G --> B
I --> B
先把场景切成区域
开放场景不应该只有一个巨大根节点。我们按街区把场景切成区域,每个区域有静态网格、装饰、碰撞、NPC 刷新点和音频环境。区域不是为了让玩家感知地图边界,而是为了让客户端知道资源管理边界。玩家靠近时加载相邻区域,离开一段距离后卸载或降级。区域边界要结合转角、门洞、地形遮挡来切,不要机械按方格切。一个好的区域切分会让加载发生在玩家不敏感的位置,比如转过巷子前,下一段街景已经准备好。
在实际执行里,我会把这一点写进开发任务,而不是只留在口头约定里。负责功能的人需要说明输入数据从哪里来、失败时 UI 怎么退回、日志里能看到哪些字段、测试要如何复现。3D 遮挡与 LOD相关的问题通常不会在第一天爆发,它会在几轮需求叠加后变成难以定位的偶现缺陷。提前把规则落到代码和检查清单里,成本很低,收益却会在后续版本里持续出现。
LOD 不只是模型面数
很多人提到 LOD 只想到低模,但客户端里的 LOD 应该是一组表现降级策略。远处建筑可以换低模、合批材质、关闭窗内灯光;远处 NPC 可以停面部动画、降低骨骼更新频率、切成 billboard 或简化轮廓;远处树木可以减少风动画。我们给每类对象定义屏幕占比阈值,而不是固定距离。因为相机 FOV 和地形高度会改变“看起来有多大”。一个远但巨大的塔楼可能仍需要中等细节,一个近但被遮挡的小物件则可以完全跳过。
在实际执行里,我会把这一点写进开发任务,而不是只留在口头约定里。负责功能的人需要说明输入数据从哪里来、失败时 UI 怎么退回、日志里能看到哪些字段、测试要如何复现。3D 遮挡与 LOD相关的问题通常不会在第一天爆发,它会在几轮需求叠加后变成难以定位的偶现缺陷。提前把规则落到代码和检查清单里,成本很低,收益却会在后续版本里持续出现。
遮挡剔除要服务场景结构
遮挡剔除依赖场景结构。室内外混合场景里,墙体、山体、大型建筑是天然遮挡物;开阔广场则收益有限。我们不会为了追求剔除率把所有东西都塞进复杂遮挡系统,而是先看镜头路径。玩家经常经过的窄巷、室内门口、地下通道值得认真配置遮挡;高处俯瞰点则要承认可见面很大,靠 LOD 和可见距离兜底。剔除系统也要避免闪烁,遮挡状态切换要有缓冲,尤其是相机贴近墙角时。
在实际执行里,我会把这一点写进开发任务,而不是只留在口头约定里。负责功能的人需要说明输入数据从哪里来、失败时 UI 怎么退回、日志里能看到哪些字段、测试要如何复现。3D 遮挡与 LOD相关的问题通常不会在第一天爆发,它会在几轮需求叠加后变成难以定位的偶现缺陷。提前把规则落到代码和检查清单里,成本很低,收益却会在后续版本里持续出现。
流式加载要和玩法状态解耦
区域卸载不能把玩法状态直接丢掉。比如玩家在 A 区打开了宝箱,离开再回来不能恢复未打开状态;NPC 离开加载区后,它的任务状态仍然要保存在更高层的世界状态里。我们把区域场景当作表现和交互实例,把持久状态放在 WorldState 或存档系统。区域加载时读取状态生成表现,卸载时只提交必要变化。这样资源流式不会变成玩法逻辑的生命周期,也不会因为一次卸载导致任务回滚。
在实际执行里,我会把这一点写进开发任务,而不是只留在口头约定里。负责功能的人需要说明输入数据从哪里来、失败时 UI 怎么退回、日志里能看到哪些字段、测试要如何复现。3D 遮挡与 LOD相关的问题通常不会在第一天爆发,它会在几轮需求叠加后变成难以定位的偶现缺陷。提前把规则落到代码和检查清单里,成本很低,收益却会在后续版本里持续出现。
调试视图必须可视化预算
3D 优化很难靠肉眼判断。我们做了一个 debug overlay:显示当前加载区域、可见对象数、LOD 分布、遮挡剔除数、材质切换次数和三角面估算。美术在场景里走一圈,就能看到某个广场为什么超预算,是 LOD 阈值太激进,还是某些室内物件没有被区域管理。这个视图比口头沟通有效得多,因为它把性能问题变成可见的数据。
在实际执行里,我会把这一点写进开发任务,而不是只留在口头约定里。负责功能的人需要说明输入数据从哪里来、失败时 UI 怎么退回、日志里能看到哪些字段、测试要如何复现。3D 遮挡与 LOD相关的问题通常不会在第一天爆发,它会在几轮需求叠加后变成难以定位的偶现缺陷。提前把规则落到代码和检查清单里,成本很低,收益却会在后续版本里持续出现。
小团队也能做的实用顺序
如果时间有限,优先级可以这样排:先做区域加载,控制内存和节点数量;再做关键大物件的 LOD,降低远景成本;然后给窄巷、室内、山体背后配置遮挡;最后再细化 NPC、植被和特效的表现降级。不要一开始就追求完整开放世界框架。Godot 足够灵活,小团队只要把预算边界立起来,就能让 3D 场景从“能跑”走向“可维护地跑”。
在实际执行里,我会把这一点写进开发任务,而不是只留在口头约定里。负责功能的人需要说明输入数据从哪里来、失败时 UI 怎么退回、日志里能看到哪些字段、测试要如何复现。3D 遮挡与 LOD相关的问题通常不会在第一天爆发,它会在几轮需求叠加后变成难以定位的偶现缺陷。提前把规则落到代码和检查清单里,成本很低,收益却会在后续版本里持续出现。
先做内容预算表
3D 优化很容易陷入“感觉这个模型贵”的争论。我们会先给场景建立预算表:近景角色多少三角面、主建筑多少材质、远景装饰允许几档 LOD、同屏 NPC 上限多少、动态灯光最多几个、粒子发射器最多几个。预算不是为了限制创作,而是给内容生产一个可对齐的目标。没有预算时,每个资源都觉得自己只超了一点,合起来就超很多。
预算表要和目标设备绑定。桌面端、移动端、Web 导出可能完全不同。Godot 项目如果要多平台发布,LOD 阈值和材质质量最好由平台配置控制。高端平台可以保留更远可见距离,低端平台提前降级。不要把所有平台绑在同一套固定数值上,否则不是浪费性能,就是低端设备撑不住。
LOD 切换要避免视觉跳变
LOD 最大的问题不是低模难看,而是切换瞬间被玩家看见。建筑、石头这类静态物件可以用距离和屏幕占比切换,但要避免在玩家视线中心频繁跳。可以加滞回区间,也可以在转角、遮挡后切换。角色 LOD 更敏感,骨骼、材质和阴影变化都会被注意到。我们通常让角色在远处先降低动画频率和阴影,再切模型,最后才考虑 billboard。
材质也有 LOD。远景不需要复杂法线和多层效果,贴图尺寸可以更低,透明材质要慎用。很多场景的瓶颈不是三角面,而是材质切换和 overdraw。Godot 的渲染统计和外部 GPU 工具都要结合看,不能只盯模型面数。
遮挡配置要和镜头路径一起评审
遮挡剔除不是美术导入完场景后程序自动开个开关就结束。需要看玩家会从哪里看、相机会不会穿墙、有没有高处俯瞰。我们在关卡评审时会走一遍主要路线,把“天然遮挡点”标出来:门、墙角、山脊、隧道。只有这些地方适合投入遮挡配置。开阔区域就不要指望遮挡救场,要靠 LOD、实例化和远景合批。
遮挡还要注意动态物体。大型移动门、升降平台、可破坏墙体如果参与遮挡,状态变化后需要更新可见性逻辑。简单项目可以保守处理:动态遮挡物不作为主要遮挡,只作为视觉对象;真正决定剔除的是稳定地形和建筑。这样收益可能少一点,但错误风险低。
资源流式的单位别太碎
区域切分太粗会导致内存高,太碎会导致加载频繁。我们倾向以玩家 10 到 20 秒移动范围作为一个内容块,再根据转角和视线做微调。一个区域里包含相关模型、碰撞、导航、环境音和触发器。不要把每棵树都做成独立流式单元,管理开销会超过收益。对重复装饰,用 MultiMesh 或实例化策略降低成本。
加载过程要有优先级。玩家前方区域优先,身后区域延迟卸载,任务目标区域可以提前加载。资源加载不要集中在同一帧挂树,尤其是带碰撞和脚本的场景。可以先加载资源,再分帧实例化子节点。Godot 的主线程仍然会承担不少节点创建工作,分帧能显著降低卡顿。
失败路径要能降级
资源流式可能失败:文件缺失、版本不匹配、下载未完成、内存不足。客户端不能只留下空洞。对关键路径资源,进入区域前应阻止玩家并给出重试;对非关键装饰,可以用占位模型或直接跳过,同时记录日志。任务相关 NPC 加载失败时,要回退到安全点或打开错误提示。开放场景里,失败路径如果不设计,玩家可能走进一个没有碰撞或没有任务对象的区域。
我们还会保留一个“低配安全模式”。当设备内存压力高或连续加载失败时,降低远景距离、关闭部分装饰区域、减少 NPC 表现。这个模式不是给玩家手动选择的画质选项,而是客户端自救策略。它能让游戏继续可玩,而不是直接崩溃。
与导航和碰撞的关系
3D 区域流式加载时,视觉资源、碰撞和导航网格不一定同生命周期。远处建筑可以卸载高模,但地形碰撞可能仍要保留,避免玩家或 NPC 路径计算穿过空洞。NPC 远距离寻路也许只需要粗导航,不需要完整室内导航。我们会把区域资源拆成 visual、collision、navigation、logic 四类,按需求加载。玩家附近加载全部,远处任务区域可能只加载 logic 和粗导航,完全无关区域才全部卸载。
这个拆分对开放场景很重要。若视觉和碰撞绑在同一个场景里,卸载房屋模型时可能把阻挡也删掉,玩家从远处高速移动回来会穿模。反过来,只为保留碰撞而让整栋房子的材质和装饰常驻,也浪费。Godot 的场景可以组合,建议从资源组织阶段就把这些层次分开。
低端设备的验证方式
在开发机上跑得顺,不代表目标设备能承受。我们会准备一套低端配置档,强制使用更短可见距离、更低 LOD、更少动态灯光,并在开发机上模拟一部分压力。最终仍要上真机或目标显卡测试。测试路线包括快速转身、从室内冲到室外、传送到高处俯瞰、连续穿过区域边界。这些动作最容易暴露加载尖峰和 LOD 跳变。
记录数据时,除了 FPS,还要看显存、内存峰值、shader 编译卡顿和资源加载时间。很多 3D 项目第一次进入某个区域会卡,是 shader 或材质第一次使用引起的,不是模型加载本身。可以在加载界面预热关键材质,或者在区域进入前分批触发。优化要看完整链路,不要只看渲染提交数量。
结语
这类系统在 Godot 里往往不是“某个 API 会不会用”的问题,而是边界有没有提前说清楚。节点、资源、平台能力和业务状态都很灵活,灵活就意味着团队需要给它们加上可维护的秩序。我的经验是,先把生命周期、输入输出、失败路径和调试信息写明,再去追求抽象优雅。这样项目进入频繁迭代期时,新增需求不会把旧功能挤得变形,排查问题的人也能从日志、结构和约定里找到线索。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。