把 AI 生成的 3D 场景、材质和模型批量导入 Unity、Unreal 或其他实时项目后,经常会遇到一种容易被平均帧率掩盖的问题:场景第二次进入很顺畅,第一次进入却会明显卡一下。
典型表现包括:
- 玩家第一次从测试区进入红色岩谷城市时,画面突然停顿;
- 同一局再次经过该区域,帧率恢复正常;
- 重启应用后重新进入,卡顿再次出现;
- 清理缓存或切换画质后,第一次看到某个特效又发生停顿;
- 平均 FPS 看起来正常,但某一帧或连续几帧的耗时异常升高。
这类问题不能简单归结为"模型面数太高"。很多资源不会在程序启动时全部准备完毕,而是在第一次真正使用时才完成 Shader 编译、纹理上传、网格创建、材质实例化或碰撞数据准备。
先给结论:不要先批量降低面数,也不要一开始就关闭全部后处理。先固定一条测试路线,对比冷启动首进、同局二次进入、重启后首进和清理缓存后首进。只有首次出现峰值时,排查重点应放在首次资源准备,而不是平均 FPS。
本文以玩家从空白测试区进入一座红色岩谷城市为例,依次拆分 Shader、纹理、网格、对象创建和加载时机。

图注:从参考图、线框到完整场景的流程适合说明资源准备链路,但首次加载是否卡顿,仍需通过帧时间、Shader 编译和纹理上传记录验证。静态图不能证明资源已异步加载,也不能证明目标设备性能达标。
为什么"第二次不卡"比平均FPS更值得关注?
平均 FPS 是一段时间内的平均结果,很容易掩盖短时峰值。
例如,一个场景在十秒内大部分时间都保持 60 FPS,但玩家进入岩谷的瞬间出现了 300 毫秒的单帧停顿。最终平均 FPS 可能仍然不低,玩家却会明显感觉画面"顿了一下"。
第一次和第二次进入的差异,可以帮助判断问题方向:
| 现象 | 优先怀疑方向 |
|---|---|
| 第一次进入卡,第二次顺畅 | Shader、纹理、网格或材质的首次准备 |
| 每次进入都卡 | 重复同步加载、重复创建或资源本身持续超载 |
| 只有清理缓存后卡 | 缓存、管线状态或预热结果被清除 |
| 只有切换画质后卡 | 新的 Shader 变体、纹理配置或后处理管线首次启用 |
测试时应固定构建版本、设备、分辨率、画质、摄像机路线、玩家速度、进入区域的时间点,以及是否清除缓存。
不要直接用编辑器结果推断打包版本。编辑器可能已经访问过资源,也可能附带额外的调试开销。两种情况都会影响首次加载表现。
第一步:用冷启动、热缓存和空场景建立基线
先不要修改资源。用同一个构建建立四组基础数据:
| 测试组 | 测试方式 | 主要用途 |
|---|---|---|
| 空场景 | 不进入岩谷 | 确认基础运行是否稳定 |
| 冷启动首进 | 重启应用后第一次进入 | 观察完整的首次准备峰值 |
| 同局二次进入 | 离开后再次进入 | 判断资源是否被复用 |
| 重启后首进 | 结束应用再进入 | 检查缓存是否跨会话有效 |
至少保存以下指标:
- 第一次异常发生时间;
- CPU 帧时间;
- GPU 帧时间;
- 最长单帧耗时;
- 区域进入耗时;
- 当帧新增对象或资源数量;
- 内存与显存变化;
- 是否发生 Shader 编译或纹理上传。
推荐使用统一格式记录:
text
build | device | run_type | route_time | first_spike
cpu_ms | gpu_ms | memory_mb | vram_mb
其中 run_type 可以填写 empty、cold_first、warm_second 或 restart_first。
如果空场景稳定、冷启动首进出现峰值、同局二次进入恢复正常,问题大概率集中在资源第一次使用时的准备过程。
第二步:先判断卡顿来自CPU、GPU还是存储等待
"场景很复杂"不是性能归因。只有先确定峰值发生在哪个环节,后续优化才不会变成盲目删资源。
1. CPU主线程峰值
CPU 主线程突然拉长,常见原因包括:
- 同步加载或反序列化场景数据;
- 同一帧创建大量 GameObject、Actor 或组件;
- 首次创建材质实例;
- 创建 Shader 或图形管线对象;
- 生成碰撞体或烹制碰撞数据;
- 集中解析资源依赖。
如果 CPU 帧时间明显升高,而 GPU 没有同步出现峰值,应优先检查同步读取、对象创建和运行时初始化。
2. GPU峰值
GPU 在第一次看到目标区域时出现峰值,可能来自:
- 首次使用的图形管线准备;
- 大量纹理集中上传;
- 高分辨率阴影、反射或体积资源首次生成;
- 后处理效果创建中间缓冲;
- 粒子、透明材质或体积雾同时进入画面。
要把"第一次看见对象"和"第一次创建或上传对象"放在同一条时间线上分析。
3. 存储与资源等待
如果 CPU 和 GPU 都没有持续计算,但线程正在等待读取,常见原因包括:
- 压缩资源解包;
- 小文件数量过多;
- 目标设备存储速度不足;
- 同一时间提交大量同步读取;
- 依赖资源没有提前整理。
不要只看总加载时长。两秒的异步加载若有明确进度,并且不阻塞操作,玩家未必觉得卡;反过来,100 毫秒的主线程停顿就可能明显破坏操作手感。
第三步:统计Shader变体和首次编译
Shader 可以理解为控制材质如何被 GPU 绘制的程序。同一个基础 Shader 会因为光照、阴影、透明、雾效、画质和平台设置产生多个变体。
AI 生成场景通常会快速引入多种材质,例如岩石、地表、玻璃、发光标识、建筑混合材质、体积雾和不同质量档位的替代材质。如果某些变体在第一次使用时才编译,就会出现"第一次看到这种材质时卡一下"。
记录首次编译信息
建议至少输出:
text
shader_name | keywords | material | trigger_object
compile_time | quality_level | platform
重点检查:
- 哪个 Shader 首次触发编译;
- 哪组关键字产生了新变体;
- 编译是否发生在玩家可操作阶段;
- 同一材质是否形成大量重复变体;
- 不同画质是否使用不同集合。
清理不会使用的组合
项目永远不会使用的光照、阴影或雾效组合,没有必要进入运行时变体集合。但"把所有变体全部收集并一次性预热"也不应成为默认方案。
过度预热可能导致:
- 启动时间明显变长;
- 内存占用增加;
- 低端设备启动压力上升;
- 提前准备大量玩家不会遇到的组合。
更合理的做法是按目标平台、画质档位和玩家路线预热真正需要的变体。
用对照组验证预热效果
准备两组测试:
- 不预热,记录第一次看到岩谷材质时的峰值;
- 只预热入口路线必需的 Shader,再记录首进峰值。
如果首进停顿明显减轻,同时启动时间和内存仍在可接受范围内,才能说明预热方案有效。
第四步:分离纹理上传、网格创建和材质实例化
Shader 并不是唯一原因。纹理、网格、碰撞和材质实例可能恰好在同一帧集中准备。
1. 用低分辨率纹理做对照
先用低分辨率占位纹理替换岩谷中的大纹理,保持模型、路线和材质逻辑不变。
如果卡顿明显减轻,继续检查:
- 纹理尺寸与压缩格式;
- mipmap 是否符合目标平台;
- 是否在同一帧批量上传;
- 多个材质是否重复加载同一张纹理;
- Base Color、Normal、Roughness 和 Mask 是否同时集中进入显存。
平均显存占用正常,不代表没有上传峰值。大量纹理集中提交,仍可能造成瞬时 CPU、GPU 或总线压力。
2. 单独测试网格与碰撞
保留低分辨率材质,逐步恢复场景网格,观察峰值是否随对象数量和网格复杂度变化。
重点检查:
- 是否同一帧创建大量对象;
- 是否在运行时生成碰撞体;
- 是否发生碰撞数据烹制;
- 是否重复创建相同网格;
- 合批或实例化是否触发额外处理。
如果只有恢复复杂网格后才出现峰值,问题更可能在网格、碰撞或对象初始化,而不是纹理。
3. 检查材质实例数量
某些导入或运行流程会为每个对象创建独立材质实例。岩谷中大量重复建筑、岩壁和装饰物,可能因此产生远高于预期的材质实例数。
建议记录:
text
texture_upload_mb | mesh_create_count | material_instance_count
collision_build_count | object_create_count
对象不多、材质实例却持续增长时,应检查能否共享材质,或使用实例参数表达颜色和数值差异。
第五步:按照玩家路线分阶段预加载
预加载不是把整张地图一次性塞进内存。更合理的方式是按照玩家什么时候会看见、什么时候必须碰撞来安排资源优先级。
入口必需资源
玩家离开测试区后立即需要的内容,包括:
- 入口附近地形和建筑;
- 当前视野内的主要材质;
- 玩家脚下和通道上的碰撞数据;
- 立即出现的灯光、环境和特效资源。
这部分应在开放入口前准备完成。
即将出现的资源
玩家继续前进几秒后可能进入视野的内容,例如岩谷内部建筑、远处山体、之后触发的粒子,以及即将进入镜头的阴影或反射资源。
这部分可以异步加载,但必须预留足够时间。
可以延后的资源
当前路线之外、不会进入镜头的背面细节、低优先级装饰和暂时不会触发的特效,可以推迟加载。
不要在进入场景的同一帧提交全部资源,而应按照优先级分批进入队列。
不要用固定等待时间冒充加载完成
"进入区域前等待两秒"不能证明资源已经准备好。设备、文件大小和 Shader 编译时间不同,固定等待会让高性能设备白等,也可能让低性能设备仍然来不及准备。
应使用真实状态决定是否开放后续区域,例如:
- 必需资源完成读取;
- 入口 Shader 完成预热;
- 关键纹理完成上传;
- 玩家区域碰撞已经可用。
如果加载失败,应显示明确状态,并保留一个可操作的安全区域,不能让玩家直接进入材质、网格或碰撞未准备完成的空间。
第六步:在最低目标设备复测启动、切换与回收
开发机顺畅,不代表目标设备也能稳定通过。AI 生成场景常包含高分辨率纹理、复杂材质和大量小对象,目标设备的存储速度、CPU 性能、显存和内存带宽都会影响首次准备。
至少比较:
- 开发机与最低目标设备;
- 高速存储与项目最低存储条件;
- 高、低画质;
- 不同目标分辨率;
- 冷启动首进与同局重复进入。
连续往返五次
让玩家沿固定路线进入岩谷、离开并重新进入,连续运行五轮,检查:
- 资源是否正确复用;
- 对象是否被重复创建;
- 卸载后能否正确重新加载;
- 内存或显存是否持续增长;
- 第二次以后是否仍存在相同峰值。
如果每次往返都会增加内存,说明资源可能没有释放,或旧引用仍然保持。
配置变化后重新冷启动
修改画质、阴影、雾效、分辨率、语言包、材质关键字或渲染后端后,应重新运行冷启动测试。
预热集合不能只覆盖开发机上的单一配置。否则高画质版本可能正常,低画质或另一个平台反而在首次进入时创建新的 Shader 和资源组合。
交付记录:不要只提供一个平均FPS
建议为每个构建和设备保存统一记录:
| build | device | run_type | first_spike | cpu_ms | gpu_ms | shader | upload_mb | object_count |
|---|---|---|---|---|---|---|---|---|
| 示例 | 目标设备A | cold_first | 12.4s | 42 | 18 | RockSurface | 86 | 240 |
| 示例 | 目标设备A | warm_second | 12.1s | 17 | 16 | --- | 4 | 12 |
上表只用于展示记录格式,不是本文案例的真实测试数据。正式交付时,数值必须来自 Profiler、运行日志或目标设备记录,不能用静态图、编辑器预览或主观感受替代。
如果需要先查看资产层级、材质分配和明显的资源异常,可以使用模型预览进行资产层预检。
它不能替代引擎中的 Shader 编译、纹理上传、网格和碰撞创建、异步加载、显存分配、画质切换或目标设备测试。
最后检查卡:首次进入卡顿时查这6步
1. 冷启动与热缓存基线
- 空场景是否稳定?
- 冷启动首进是否出现峰值?
- 同局二次进入是否恢复?
- 清缓存或重启后是否再次出现?
2. CPU、GPU与存储归因
- 峰值发生在主线程、渲染线程还是 GPU?
- 是否存在同步读取或线程等待?
- 异常帧中新增了哪些资源?
3. Shader变体
- 首次使用了哪些 Shader?
- 哪些关键字产生了新变体?
- 是否预热了不必要的组合?
- 各画质档位是否有不同集合?
4. 纹理上传
- 尺寸和压缩格式是否合理?
- 是否在同一帧批量上传?
- 是否重复加载相同纹理?
- mipmap 与目标平台是否匹配?
5. 网格、碰撞与材质实例
- 是否同一帧创建大量对象?
- 是否在运行时烹制碰撞?
- 是否重复创建网格?
- 材质实例是否被无意义复制?
6. 预加载与目标设备复测
- 入口必需资源是否在开放前准备完成?
- 是否按玩家路线分批加载?
- 是否使用真实完成状态,而不是固定等待时间?
- 最低目标设备和不同画质是否重新冷启动测试?
AI 生成的 3D 场景可以快速扩充内容,但"已经批量导入"不等于"运行时已经准备好"。第一次进入发生卡顿时,平均 FPS 只能反映画面的大致趋势,无法解释资源首次准备造成的单帧峰值。
更可靠的顺序是:先区分冷启动和热缓存,再确认 CPU、GPU 或存储来源;随后拆分 Shader、纹理、网格与对象创建,最后按玩家路线安排预加载,并在最低目标设备上复测。
你的首次卡顿更常发生在打开场景、第一次看到特效,还是切换画质之后?