Chapter 15: Procedural Geometry
程序生成几何讨论的对象,是用参数、规则、随机种子和约束直接生成可渲染的几何数据。读完本章后,读者应能追踪一个程序化场景从输入参数到 mesh、instance buffer、LOD、碰撞代理和调试证据的完整路径,并能判断生成结果的可复现性、可控性和性能边界。
本章贯穿一个城市街区生成器。输入是一段道路中心线、街区尺寸、随机种子、建筑高度范围、立面模块集合和密度参数;输出是道路网格、地块边界、建筑体块、窗户实例、LOD 数据、碰撞代理和运行时缓存。这个例子足够小,可以在一个 chunk 内解释清楚;它也足够接近真实工程,因为城市生成会同时触及噪声、规则系统、实例化、流式加载和调试验证。
程序生成几何的核心判断是:生成器输出的仍然是图形管线认识的数据。无论生成逻辑来自噪声、Grammar、约束求解还是人工参数,最后都要落到顶点、索引、法线、切线、UV、材质 ID、instance transform、bounds 和碰撞表示上。渲染引擎只会按照这些资源和状态执行,不会因为数据来自程序而自动获得更高效率。
本章把程序生成拆成五层:参数与种子定义生成空间,噪声提供连续变化,规则系统组织结构,工程集成负责并行、缓存和流式加载,城市案例把这些对象放进一个可复查的 frame。后续 Chapter 16 会继续讨论 GPU 并行生成,本章聚焦 CPU / 引擎侧的几何生产、组织和验证。
15.1 程序生成几何的基本理念
程序生成几何(Procedural Geometry)是一条从描述到数据的生产路径。描述可以是少量参数、脚本规则、随机种子、编辑器控制点或约束集合;数据则是渲染管线可消费的资源,例如 mesh、instance 列表、材质绑定、bounds、LOD 和碰撞代理。它解决的问题是资产数量、变化规模和手工制作成本之间的矛盾。
在城市街区生成器中,设计者不直接摆放每一扇窗户和每一栋建筑。设计者提供道路曲线、地块宽度区间、建筑高度范围、立面模块库和种子。生成器根据这些输入切分地块,计算建筑 footprint,生成外墙面片,再把窗户、门、广告牌、空调外机等模块实例化到立面上。最终 draw call 看到的是 mesh buffer 和 instance buffer,编辑器看到的是可调参数与调试 overlay。
生成链可以按下面的边界理解。图中的每个节点都对应可检查的数据对象,而生成器质量取决于这些对象之间的转换是否稳定。
参数与 seed 决定生成空间。参数控制范围,例如街区尺寸、道路宽度、建筑高度和窗户间距;seed 控制随机选择,例如某个地块选用哪种屋顶、哪套窗户模块、哪种立面颜色。相同参数和相同 seed 在同一生成版本下应得到相同结果,这一点称为确定性(determinism)。确定性让生成结果可以被缓存、复现、回滚和联机同步。
规则与约束决定生成结果是否满足设计目标。规则描述“怎么长出来”,约束描述“生成后必须满足什么条件”。城市例子里,规则可以规定主路两侧生成商业楼,次级道路两侧生成住宅楼;约束可以规定建筑 footprint 不越过地块边界、门必须朝向道路、窗户间距保持在模块网格上、碰撞代理不穿过道路区域。
中间表示是程序生成几何的关键缓冲层。直接从输入参数生成顶点数组会让调试困难,因为任何视觉错误都只能回到大量顶点里排查。更稳定的做法是先生成道路 graph、地块 polygon、建筑 volume、立面 grid 这类中间数据,再从中间数据生成 mesh 和 instance。中间表示回答“生成器认为世界结构是什么”,mesh 只是这个结构的渲染表达。
程序生成几何的输出至少要覆盖渲染、物理和编辑三个系统。渲染系统需要顶点、索引、材质、instance transform 和 bounds;物理系统需要简化碰撞体、导航区域或遮挡代理;编辑系统需要 seed、参数、生成版本、调试颜色和可选的 baked asset。只输出可见 mesh 的生成器通常会在碰撞、LOD、streaming 或保存回放时暴露缺口。
判断一个程序生成模块是否可进入引擎,可以按这个顺序检查:输入是否可序列化,seed 是否可复现,中间表示是否可观察,输出资源是否完整,失败结果是否有诊断信息,缓存 key 是否覆盖生成版本。这个顺序先保障可控性,再讨论画面复杂度和性能收益。
15.2 常用噪声函数与分形算法
噪声函数是从坐标到数值的确定性映射。输入通常是二维或三维坐标,例如地形上的 (x, z) 或建筑立面上的 (u, v);输出通常是标量、向量或类别权重。它提供“看起来有变化、又能被 seed 复现”的连续随机性。城市生成器可以用噪声控制建筑高度起伏、立面脏污程度、广告牌密度和树木分布。
最基础的随机数每个格子之间没有连续关系。把它直接用于高度场,会得到跳变强烈的块状表面。图形生成更常使用 coherent noise,也就是相邻坐标输出相近、远距离坐标逐渐失去相关性的噪声。Perlin、Simplex 和 Worley 都属于常见过程纹理与几何生成工具,它们回答的问题分别偏向连续起伏、低方向伪影的连续变化、细胞状距离结构。
Perlin 噪声适合生成平滑起伏。它把空间划分为网格,在网格点放置伪随机梯度,再根据采样点到网格点的方向和插值函数得到连续值。用于城市时,它可以控制街区整体高度趋势:靠近中心区域的高度更高,边缘区域逐渐降低,同时在局部加入轻微扰动。
Simplex 噪声常用于更高维或需要减少方向性纹理的场景。它使用 simplex 单元组织空间,在二维中表现为三角形单元,在三维中表现为四面体单元。工程上可以把它视为一种更适合多维连续随机场的选择。对于城市案例,Simplex 可以服务于风化、污染、灯光亮度或植被稀疏度这类连续属性。
Worley 噪声也称 cellular noise。它把空间中的随机特征点作为中心,输出采样点到最近或第几个最近特征点的距离。这个距离结构天然形成细胞边界,因此适合生成石块、裂纹、细胞状分区、屋顶斑驳和城市地块扰动。把 Worley 用在地块边界上时,需要让它只影响视觉细节或次级分割,主道路拓扑仍应由规则和约束控制。
FBM(Fractal Brownian Motion)是一种把多层噪声叠加起来的分形方法。每一层称为 octave,高层频率更高、幅度更低。低频层控制大轮廓,高频层控制细节。城市生成器可以用低频 FBM 控制街区高度带,用中频层控制建筑之间的高度差,用高频层控制墙面颜色和污渍细节。
下面的伪代码展示 FBM 的最小结构。它返回一个稳定标量,调用者再把标量映射到高度、密度或材质权重上。
float fbm(vec2 p, int octave_count, float base_frequency, float base_amplitude, int seed) {
float value = 0.0;
float frequency = base_frequency;
float amplitude = base_amplitude;
float amplitude_sum = 0.0;
for (int octave = 0; octave < octave_count; ++octave) {
value += amplitude * coherent_noise(p * frequency, seed + octave * 131);
amplitude_sum += amplitude;
frequency *= 2.0;
amplitude *= 0.5;
}
return value / amplitude_sum;
}
这段代码的核心在于频率和幅度的分层关系。频率翻倍会让细节尺度减半,幅度衰减会让细节只补充局部变化。返回值经过归一化后更容易映射到工程参数,例如 height = mix(8m, 80m, value) 或 window_density = mix(0.4, 0.9, value)。
噪声进入几何系统时要同时检查采样空间、数值范围和使用位置。采样空间决定噪声是否跟世界坐标稳定绑定;数值范围决定后续映射是否越界;使用位置决定噪声影响的是结构、形状、材质还是实例密度。城市道路主拓扑通常使用规则生成,噪声只给局部弯曲、密度和外观提供变化。这样可以保留道路连通性,又能得到自然差异。
噪声错误通常表现为四类视觉症状。第一类是 seam,chunk 边界两侧采样坐标或 seed 不一致,导致高度、密度或材质在边界处断裂。第二类是 aliasing,高频噪声超过屏幕采样能力,远处表面闪烁。第三类是范围失控,噪声被直接映射到高度或偏移,生成穿插和浮空。第四类是结构失真,噪声修改了需要保持约束的对象,例如道路中心线、碰撞体或门的朝向。
调试噪声的稳定顺序是:先用灰度图显示原始噪声,再显示经过 remap 的参数值,再显示生成后的几何边界,最后检查渲染结果。只看最终画面会把采样错误、映射错误和 mesh 生成错误混在一起。把每层结果可视化,才能定位问题发生在噪声函数、参数映射还是几何构建阶段。
15.3 基于规则的生成系统(Grammar)
Grammar 是一组把符号或形状逐步改写成具体结构的规则。它在程序生成几何中承担结构组织角色:先给出抽象对象,再通过规则展开成更具体的对象。噪声擅长提供连续变化,Grammar 擅长表达层级和约束,例如道路如何分叉、建筑如何切分楼层、立面如何排布窗户。
L-system 常用于递归生长结构。它用字符串或符号序列表示当前状态,用规则把一个符号替换成多个符号。树枝、藤蔓、道路支路和管线网络都可以用这种思想表达。城市街区中,主干路可以作为初始符号,规则根据道路等级、长度和交叉口密度生成次级道路。每次扩展都要检测边界、最小夹角和可通行宽度。
Shape grammar 更适合建筑和空间分解。它把一个几何形状逐步切分成子形状,例如把建筑体块切成楼层,把楼层切成立面网格,把立面网格切成窗户、墙面、阳台和入口。它的优势在于每次规则应用都保持几何上下文。生成器可以知道当前面片的方向、尺寸、楼层编号和材质语义,再选择合适的模块。
Tile rule 和 constraint solving 用来处理局部拼接。Tile rule 规定相邻模块之间的连接条件,例如道路 tile 的出口方向、地砖边缘类型、墙面模块宽度。约束求解器根据这些条件选择一组互相兼容的 tile。城市生成器可以用 tile rule 生成街道设施,也可以用约束求解控制立面模块:转角位置使用转角模块,入口楼层必须有门,高层重复窗户模块。
下面是一段立面生成规则的简化配置。它把建筑正面按垂直方向拆成入口层、中间层和屋顶层,再按水平方向放置模块。真实工程会把尺寸、材质、anchor 和碰撞代理分开存储。
facade_rules:
ground_floor:
height: 4.5
modules: [door, shop_window, wall]
door_required: true
middle_floor:
repeat_until_roof: true
height: 3.2
modules: [window_pair, balcony, wall]
roof_band:
height: 1.2
modules: [cornice, wall]
这段规则的输入是一个建筑正面矩形,输出是若干带语义的 façade cell。每个 cell 再映射到 mesh 模块或 instance。规则中的 door_required 是约束,repeat_until_roof 是展开策略,modules 是候选集合。seed 只负责在候选集合中选择具体模块,不能覆盖门朝向道路、模块尺寸对齐和屋顶封边这类结构约束。
规则系统的主要风险是局部规则组合后产生全局失败。单个窗户模块看起来合法,但一整面墙可能因为累计误差出现最后一列宽度不足。单条道路分叉合法,但多次分叉可能形成无法通行的小三角地块。单个 tile 拼接合法,但整个区域可能陷入无解状态。工程上需要在规则应用之后加入验证阶段,把失败原因记录到中间表示上。
Seed control 是规则系统可复现的基础。每个层级应使用稳定的随机流,例如道路层、地块层、建筑层、立面层分别拥有独立 seed 派生方式。这样修改某栋楼的窗户随机选择,不会连带改变整片街区的道路结构。常用方式是用 chunk 坐标、全局 seed、生成版本和对象 ID 组合成 hash,再派生局部随机数。
Grammar 的工程价值在于它把“生成了什么”保存为可解释结构。出现窗户错位时,可以检查 façade cell;出现道路断开时,可以检查 road graph;出现建筑越界时,可以检查 lot polygon 和 footprint。规则系统让错误定位优先停留在结构层,减少直接翻查顶点数组的成本。
15.4 在引擎中集成 Procedural 模块
程序生成模块进入引擎后,需要和任务系统、资源系统、渲染系统、编辑器和缓存系统协作。生成算法本身只回答“如何产生结构”,引擎集成还要回答“何时生成、在哪个线程生成、生成结果如何上传、什么时候释放、怎么调试、如何保持版本一致”。这些问题直接决定程序化内容能否在真实 frame 中稳定运行。
引擎侧可以把生成模块拆成四个阶段:生成请求、CPU 任务、资源提交、运行时消费。生成请求由相机位置、编辑器修改或 streaming 系统触发;CPU 任务生成中间表示、mesh 数据、instance 列表和碰撞代理;资源提交把数据写入 mesh buffer、texture、instance buffer 或引擎对象;运行时消费阶段由 renderer、physics、navigation 和 editor overlay 使用这些资源。
| 阶段 | 输入 | 输出 | 主要风险 | 调试证据 |
|---|---|---|---|---|
| 请求 | chunk key、参数、seed、版本 | 生成任务 | 重复请求、版本错配 | 任务队列与 key 日志 |
| CPU 任务 | 参数、规则、资源库 | 中间表示与几何数组 | 非确定性、越界、无解 | debug mesh、错误码 |
| 提交 | 顶点、索引、实例、碰撞 | 引擎资源句柄 | 主线程阻塞、上传峰值 | frame time、upload bytes |
| 消费 | 资源句柄、bounds、LOD | draw、physics、navigation | bounds 错误、LOD 抖动 | capture、overlay、profiler |
这个表的重点是把程序生成纳入 frame 资源路径。很多生成器在离线预览中效果正常,进入运行时后出现卡顿,原因通常落在任务粒度、资源上传、缓存失效或主线程对象创建成本上。生成器要给渲染管线提供稳定资源,也要给 engine scheduler 提供可预算任务。
Chunk 是程序化大场景的常用组织单位。城市例子可以把世界划分成固定大小的街区 chunk,每个 chunk 的 key 包含坐标、全局 seed、参数 profile 和生成器版本。相机靠近时请求生成,离开一定距离后保留缓存或释放资源。chunk 边界上的道路、地形高度和实例密度必须使用世界坐标和稳定 seed 派生,才能让相邻 chunk 结果连续。
缓存 key 应覆盖所有影响输出的输入。道路规则、噪声参数、模块库版本、材质 ID、LOD 规则和碰撞生成策略都会改变输出。如果缓存 key 只包含 chunk 坐标和 seed,修改立面模块后可能继续读取旧缓存。稳定的做法是把生成器版本、参数 profile hash 和资产库 hash 写入 key,并在调试界面显示当前 key。
并行任务需要保护确定性。常见做法是每个 chunk 独立生成,每个对象使用独立随机流,任务之间不共享可变随机状态。任务完成顺序可以变化,但输出内容不能依赖完成顺序。城市生成中,建筑 ID 可以来自地块排序、地块 polygon hash 或稳定 GUID,不能来自“任务完成时追加到全局数组的顺序”。
资源提交要区分高频变更和低频变更。道路、建筑体块和碰撞代理通常属于低频数据,可以生成后缓存较久;灯光闪烁、门牌颜色和局部装饰可以作为 instance 属性或材质参数变化。把低频拓扑和高频外观拆开,可以减少 mesh 重建和 GPU buffer 上传。Unity 的 Mesh API 与 Unreal Engine 的 Procedural Content Generation Framework 都体现了同一类工程边界:生成系统最终要把结构落到引擎资源、编辑器工具和运行时消费路径上。
编辑器控制应保留人工干预入口。程序生成不等于完全自动生成。设计者需要锁定某些建筑、手动调整道路、覆盖某个地块的高度、禁用某个模块或把结果 bake 成普通资产。工程上可以让每个生成对象拥有 source 标记:来自规则、来自手工 override、来自缓存、来自 bake 文件。这样重新生成时就能保留人工修改,同时让调试工具显示对象来源。
运行时调试至少需要四类视图。第一类是结构视图,用不同颜色显示道路、地块、建筑体块和立面 cell。第二类是资源视图,显示 vertex count、index count、instance count、material count 和 buffer 大小。第三类是流式视图,显示 chunk 状态、任务耗时、缓存命中和上传字节数。第四类是错误视图,标出无解地块、越界 footprint、失败模块和碰撞穿插区域。这些视图让生成器问题能被定位到阶段和数据对象。
15.5 实例化 Procedural 城市场景生成
城市街区生成可以把本章所有概念串成一个可复查流程。输入是一条穿过 chunk 的道路中心线、若干规则参数和全局 seed。输出要在一个 frame 中可见:道路 mesh、地块边界、建筑 mesh、窗户和阳台实例、LOD、碰撞代理、调试 overlay。这个流程的目标是把程序生成转成渲染管线可以稳定处理的数据,并把城市细节控制在可预算范围内。
第一步是生成道路和地块。道路中心线先扩展为带宽度的道路 polygon,再计算道路两侧可用区域。可用区域根据最小临街宽度、最小深度和道路等级切分成 lot polygon。这个阶段的输出是 road graph、road polygon 和 lot polygon。后续建筑、实例和碰撞都依赖这些结构,因此这里要保存每条边的朝向、道路等级和相邻关系。
第二步是从地块生成建筑 footprint 和高度。每个地块根据 setback 参数向内收缩,得到建筑 footprint。高度可以由道路等级、地块面积、区域 profile 和低频噪声共同决定。高度生成后要经过约束夹取,例如商业主路两侧高度范围更大,窄地块高度上限更低,靠近保留区域的高度下降。这个阶段输出 building volume,它由 footprint、多层高度、屋顶类型和材质 profile 构成。
第三步是立面规则展开。每个 building volume 的外侧面被拆成 façade side,再按楼层高度和模块宽度切分成 façade cell。入口层优先放置门和商铺窗,中间层重复窗户模块,屋顶层放置檐口或设备。每个 cell 生成语义、尺寸、局部坐标和候选模块 ID。窗户、阳台、遮阳板等重复元素适合实例化;建筑主体、屋顶轮廓和特殊转角适合生成 mesh。
第四步是生成渲染资源。道路面片、建筑主体和屋顶可以合并成按材质分组的 mesh;窗户、阳台、路灯、树木和空调外机写入 instance buffer。每个 instance 至少包含 transform、module ID、材质变体和可选颜色参数。mesh 输出要包含 position、normal、tangent、UV、material ID 和 bounds。instance 输出要包含 per-instance bounds 或可推导 bounds,方便后续 culling 和 LOD。
第五步是生成 LOD 和碰撞代理。近距离 LOD 使用完整建筑立面和实例;中距离 LOD 合并窗户为简化面片或贴图;远距离 LOD 只保留建筑体块和主色块。碰撞代理使用 footprint 和高度生成简化盒体或凸包,导航系统使用道路 polygon 和建筑占用区域生成可通行区域。渲染 LOD 和碰撞代理可以来自同一中间表示,但输出精度不同。
下面的简化伪代码展示 chunk 生成的阶段边界。它刻意保留中间表示,方便调试和缓存。
CityChunk generate_city_chunk(CityChunkKey key, CityProfile profile) {
RandomStream rng = make_stream(key.world_seed, key.chunk_coord, key.generator_version);
RoadGraph roads = build_roads(profile.road_input, key.chunk_bounds, rng.derive("roads"));
LotSet lots = split_lots(roads, profile.lot_rules, rng.derive("lots"));
BuildingSet buildings = create_buildings(lots, profile.building_rules, rng.derive("buildings"));
FacadeSet facades = expand_facades(buildings, profile.facade_rules, rng.derive("facades"));
MeshData static_mesh = build_static_mesh(roads, buildings, facades);
InstanceData instances = build_instances(facades, profile.module_library);
CollisionData collision = build_collision(roads, buildings);
LodData lod = build_lod(static_mesh, instances, profile.lod_rules);
return CityChunk(static_mesh, instances, collision, lod, roads.debug(), lots.debug(), facades.debug());
}
这段代码的判断点有三个。第一,rng.derive 把随机流按层级拆开,使道路、地块、建筑和立面的变化互相隔离。第二,中间表示没有在生成后立即丢弃,它们进入 debug 输出和缓存。第三,渲染资源、碰撞资源和 LOD 同时生成,保证同一栋建筑在可见画面、物理阻挡和远距离表示上使用同一个结构来源。
实例化是城市生成的主要性能手段。重复窗户如果全部烘成唯一 mesh,顶点和内存会快速膨胀。把窗户模块做成少量 mesh,并用 instance transform 表示位置和尺寸,可以把大量重复对象转成 instance buffer。这样 CPU 侧提交和 GPU 侧顶点数据都更稳定。实例化收益取决于材质合并、模块数量、可见实例数量和 culling 粒度。
程序化城市的性能判断需要同时覆盖生成耗时和运行时资源成本。一个 chunk 生成很快,但如果输出了大量材质、过多小 mesh、过细碰撞体或频繁上传 buffer,frame 仍然会抖动。应同时记录 CPU 生成时间、主线程提交时间、GPU buffer 上传字节数、draw count、instance count、vertex count、material count、collision shape count 和缓存命中率。每个指标都对应不同瓶颈。
城市生成的视觉错误也要按数据阶段排查。道路断裂先看 road graph 和 chunk 边界 key;建筑越界先看 lot polygon 与 footprint;窗户浮空先看 façade cell 的局部坐标和模块 anchor;远处闪烁先看 LOD 切换阈值和材质高频细节;碰撞阻挡错误先看 collision proxy 是否来自同一 building volume。最终画面只是症状,结构数据才是诊断入口。
把这条流程收束成工程原则:程序生成几何要先生成可解释结构,再生成渲染资源;先保障可复现和可验证,再扩大规模;先明确瓶颈类型,再决定使用 mesh 合并、实例化、LOD、缓存或流式加载。城市街区只是一个例子,同样的判断顺序可以迁移到地形、洞穴、植被、建筑、道具摆放和科幻场景生成。
最小自检任务
给定一个 256m × 256m 的城市 chunk,输入包括全局 seed、道路中心线、建筑高度范围、窗户模块库和 LOD 规则。请设计一个最小程序化生成流程,要求输出道路 mesh、建筑主体、窗户实例、碰撞代理和调试数据。然后回答:如果相邻 chunk 的道路边界出现断裂,应该按什么顺序排查?如果窗户位置偶尔浮在墙外,应该检查哪些中间数据?
答案要点
一个合格流程应从 chunk key 开始,key 至少包含 chunk 坐标、全局 seed、参数 profile hash 和生成器版本。生成阶段应依次产生 road graph、road polygon、lot polygon、building volume、facade cell、static mesh、instance data、collision proxy 和 LOD data。road graph、lot polygon、building volume 和 façade cell 应保留为调试数据,因为它们能解释最终 mesh 和 instance 的来源。
道路边界断裂应先检查相邻 chunk 是否使用同一世界坐标采样、同一全局 seed、同一道路规则版本和兼容的边界输入;再检查道路 polygon 是否在 chunk 边缘做了稳定裁剪;最后检查 mesh 构建是否遗漏边界顶点或使用了不同精度。这个顺序从输入一致性开始,再进入中间表示,最后进入顶点生成。
窗户浮在墙外应先检查 façade side 的法线方向、局部坐标系和尺寸;再检查 façade cell 是否正确映射到模块 anchor;然后检查 instance transform 的旋转、缩放和 pivot;最后检查模块 mesh 自身原点是否符合约定。这个问题通常发生在立面中间表示到 instance transform 的转换阶段,直接检查最终 draw call 难以定位根因。
本章知识点总结
- 生成路径:程序生成几何把参数、seed、规则和约束转换为 mesh、instance、LOD、碰撞和调试数据。
- 确定性:相同输入和相同生成版本应得到相同输出,确定性支撑缓存、复现、回滚和联机同步。
- 中间表示:道路 graph、地块 polygon、建筑 volume 和 façade cell 能解释最终资源的结构来源。
- 约束优先:规则负责展开结构,约束负责保证结果满足边界、朝向、连通性和尺寸要求。
- 噪声作用:Perlin、Simplex、Worley 和 FBM 提供连续随机性,适合控制高度、密度、外观和细节变化。
- 采样边界:噪声使用前要确定采样空间、数值范围和影响对象,chunk seam 与高频闪烁都能从这里排查。
- Grammar 价值:L-system、shape grammar 和 tile rule 把生成过程组织成可解释层级,便于定位结构错误。
- Seed 分层:道路、地块、建筑和立面应使用独立派生随机流,局部变化才不会改写上游结构。
- 引擎集成:生成模块要覆盖任务调度、资源提交、运行时消费、编辑器控制和缓存失效策略。
- Chunk key:chunk 坐标、全局 seed、参数 profile 和生成器版本共同决定缓存命中和输出一致性。
- 实例化收益:重复窗户、阳台、路灯和植被适合写入 instance buffer,以降低重复顶点和提交成本。
- LOD 来源:近中远距离表示和碰撞代理应来自同一中间结构,保证可见结果与物理阻挡一致。
- 性能证据:程序化场景要同时记录生成耗时、提交耗时、上传字节、draw count、instance count 和缓存命中率。
- 调试顺序:视觉症状应先回查输入和中间表示,再检查 mesh、instance、LOD、碰撞和渲染状态。