Chapter 11: Mesh Topology and Data Structures
本章讨论一个三角网格从 DCC 工具进入实时引擎时,拓扑关系如何被检查、改写、压缩并交给 GPU 渲染。读完本章,读者应能定位一个网格资产中的邻接关系、边界、孔洞、非流形边、渲染顶点拆分和 submesh 切分,并能判断它们分别影响编辑算法、导入管线、GPU buffer 上传和 draw call 组织。
贯穿材料是一块名为 GuardRailPanel 的护栏板网格:主体是带螺栓孔的金属板,边缘有倒角,四个孔洞周围需要硬法线,表面使用一张金属材质,螺栓区域使用另一张材质。它在建模软件中看起来像一个连续物体,进入引擎后会拆成多个 vertex buffer、一个或多个 index buffer、若干 submesh 和实例化数据。这个拆分过程由拓扑关系和渲染状态共同决定。
网格数据有两层含义。几何层回答“哪些点、边、面构成一个曲面”,渲染层回答“GPU 每次顶点读取需要哪些连续字节”。Half-edge、邻接表、边面关系主要服务几何层;vertex buffer、index buffer、attribute layout、material split 主要服务渲染层。稳定的导入管线需要先在几何层识别拓扑问题,再把结果转成渲染层的连续数据。
下面这条路径贯穿本章。图中的拓扑审查属于 CPU 侧导入阶段,buffer 上传和 draw call 属于运行时渲染阶段。两者使用同一个资产,但服务的查询完全不同。
这条路径给本章一个可复用判断顺序:先检查网格是否形成可处理曲面,再判断顶点属性是否需要拆分,然后选择 CPU 编辑结构和 GPU 渲染结构,最后用画面接缝、法线错误、draw call 数量、顶点数量和上传成本验证结果。
11.1 多边形网格的拓扑基础
多边形网格的拓扑描述“元素之间如何相连”。一个最小网格由顶点、边和面构成。顶点提供位置或属性挂载点,边连接两个顶点,面引用一组有序顶点形成可渲染或可编辑的局部片面。三角网格把面固定为三个顶点引用,因此 GPU 管线能直接把 index buffer 每三个索引解释为一个 triangle primitive。
GuardRailPanel 的螺栓孔说明了拓扑和外观的关系。孔洞周围的边界环决定洞的轮廓,倒角面决定高光过渡,硬法线决定孔边缘是否显得锋利。渲染时 GPU 只看到三角形和属性,导入阶段必须先判断这些三角形是否共同组成符合预期的曲面。
邻接关系(adjacency)指顶点、边、面之间的相邻引用。例如,一个面有哪些相邻面,一条边被哪些面使用,一个顶点周围按顺序连接了哪些面。邻接关系通常不会直接存进最终的 vertex buffer,因为 GPU 顶点读取主要按索引顺序进行。导入器、简化器、法线生成、UV 展开和碰撞代理生成需要邻接关系,所以它们会在 CPU 侧临时构建边表或 half-edge 结构。
流形(manifold)在网格处理中表示局部曲面关系可被稳定解释。对三角网格而言,一条内部边通常连接两个面,一条边界边连接一个面;围绕一个顶点的面应形成一个连续扇区或少数可明确分离的扇区。这个定义服务编辑和几何算法:法线平滑、边折叠、细分曲面和碰撞网格都依赖局部邻域可遍历。
边界(boundary)是一组只被一个面使用的边形成的环或链。护栏板外轮廓是边界,螺栓孔内圈也是边界。孔洞(hole)是边界环围出的缺面区域;它可以是设计结果,也可以是导入损坏。判断孔洞时需要结合资产语义:护栏板的螺栓孔是有效孔洞,扫描模型上意外出现的裂口通常是缺面。
非流形边(non-manifold edge)是局部曲面无法按普通表面解释的边或顶点。最典型情况是一条边同时被三个或更多面使用,或者两个独立表面只在一个顶点相接。它会破坏许多几何操作:边折叠时无法确定折叠影响范围,法线平滑时邻域路径混乱,封闭体积计算时内外侧判断失效。
下面的 Python 片段展示一个导入器如何用索引三角形统计边使用次数。它只验证边的面数关系,目的是定位 boundary 和 non-manifold edge;它还没有处理顶点周围的扇区顺序。
from collections import defaultdict
triangles = [
(0, 1, 2),
(2, 1, 3),
(1, 4, 3),
]
edge_faces = defaultdict(list)
for face_id, tri in enumerate(triangles):
for a, b in ((tri[0], tri[1]), (tri[1], tri[2]), (tri[2], tri[0])):
edge = tuple(sorted((a, b)))
edge_faces[edge].append(face_id)
boundary_edges = [edge for edge, faces in edge_faces.items() if len(faces) == 1]
non_manifold_edges = [edge for edge, faces in edge_faces.items() if len(faces) > 2]
print(boundary_edges)
print(non_manifold_edges)
这段代码的关键点是把有向三角边归一化为无向边。渲染管线关心的是三角形顺序和 winding,拓扑审查先关心同一条几何边被多少面引用。对 GuardRailPanel,外边缘和孔洞边缘应该出现在 boundary_edges 中;如果某个孔边缘出现在 non_manifold_edges 中,导入器应把它标记为需要修复或拆分的拓扑错误。
一个稳定判断顺序是:先统计边引用次数,再追踪边界环是否闭合,然后检查顶点一环邻域是否连续,最后结合材质、法线和 UV 接缝判断拆分是否属于渲染需求。这样能把“几何损坏”和“渲染顶点拆分”分开处理。
| 现象 | 拓扑解释 | 渲染侧信号 |
|---|---|---|
| 孔洞边缘形成闭合边界环 | 设计型 hole | 轮廓正确,边缘法线可能需要硬化 |
| 一条边被三个面引用 | non-manifold edge | 法线、碰撞、简化结果容易异常 |
| 相同位置出现多个顶点 | 属性拆分或导出重复 | 顶点数上升,接缝处属性可能分离 |
| 面的 winding 混乱 | 局部面方向不一致 | 背面剔除、法线方向、阴影出现翻转 |
拓扑基础最终服务一个结论:网格质量先由元素连接关系决定,再由顶点属性和渲染状态决定。渲染阶段可以把很多数据压成连续数组,但导入阶段仍要保留足够的拓扑信息来判断资产是否可处理。
11.2 顶点/边/面的数据组织方式
实时渲染中的网格数据通常会转换为 vertex buffer 和 index buffer。vertex buffer 存储顶点属性字节,index buffer 存储组成图元的顶点索引。Khronos 的 glTF 2.0 specification 将 mesh 定义为 primitive 数组,primitive 包含属性、索引、材质和 topology mode;这个定义接近实时引擎的 draw call 输入。
顶点在渲染层不是单纯的空间点。一个 render vertex 是一组同时被顶点着色器读取的属性:position、normal、tangent、UV、color、joint、weight 或自定义属性。glTF 规范中的 POSITION、NORMAL、TANGENT、TEXCOORD_n、COLOR_n、JOINTS_n 和 WEIGHTS_n 正是这种属性语义的标准化表达。相同 position 在 UV 接缝、硬法线、材质边界或 skin weight 不同的位置会拆成多个 render vertex。
GuardRailPanel 的孔洞边缘提供了一个直接例子。几何上,孔边缘上的一点属于同一个曲面位置;渲染上,孔边缘的正面和倒角面可能需要不同 normal,不同 tangent,甚至不同 UV island。导入器会复制这个 position,生成多个 render vertex,并让不同三角形索引引用不同副本。这样做会提高顶点数量,但能让 shader 获得正确属性。
数据布局常见两类:AoS(Array of Structures)把一个顶点的所有属性连续放在一起,SoA(Structure of Arrays)把 position、normal、UV 等属性分别放在不同数组中。interleaved layout 是 AoS 的常见形式,separate streams 是 SoA 的常见运行时形式。选择布局时要看 shader 读取哪些属性、pass 是否复用 position、平台 vertex fetch 行为和上传更新频率。
| 布局 | 数据形态 | 适用判断 |
|---|---|---|
| Interleaved vertex | pos normal uv tangent 连续排列 | 一个 pass 同时读取大部分属性,缓存局部性清晰 |
| Separate streams | position buffer、normal buffer、UV buffer 分离 | depth pass 只读取 position,动画或 morph 只更新部分属性 |
| Index buffer | 三角形引用 render vertex | 顶点复用率高,post-transform cache 有收益 |
| Non-indexed geometry | 属性按图元顺序线性展开 | 小型临时几何或完全无复用的 procedural 输出 |
图形 API 会把 shader 输入和 buffer 字节布局绑定在一起。Vulkan 规范的 Fixed-Function Vertex Processing 描述了 vertex shader input attribute、vertex input binding 和 vkCmdBindVertexBuffers 之间的关系:attribute 指向 binding,binding 在 draw 时关联具体 buffer,格式信息决定如何从内存取出并转换数据。这个规则说明 vertex buffer layout 是 shader 契约和 API 状态共同定义的结果。
下面的 C++ 结构只展示一种 interleaved layout。它把 GuardRailPanel 的主要静态属性放在一个顶点结构里,适合普通 forward 或 deferred geometry pass。
struct MeshVertex {
float position[3];
float normal[3];
float tangent[4];
float texcoord0[2];
};
struct MeshBuffers {
MeshVertex* vertices;
uint32_t vertex_count;
uint32_t* indices;
uint32_t index_count;
};
这段代码表达的是 CPU 侧资产构建结果,真实引擎还会把数据上传到 GPU buffer,并用 pipeline state 描述 attribute offset、format 和 stride。tangent[4] 中的第四个分量常用于记录 bitangent 方向符号。选择 uint16_t 或 uint32_t index 要看 render vertex 数量;顶点数量低于 65536 时,16-bit index 能降低 index buffer 带宽。
边和面在最终渲染结构中通常以索引隐含存在。一个 triangle list 的 index buffer 每三个索引构成一个面;边则由三角形内的三对索引推导出来。这个表示对 GPU 高效,但邻接查询成本高。导入器如果要做法线生成、切线生成、简化和拓扑审查,应在 CPU 侧建立额外结构,然后在导出 runtime mesh 时丢弃或压缩这些辅助结构。
一个实用判断是把网格数据分成三份:authoring data、processing data 和 runtime data。authoring data 保留 DCC 语义,processing data 保留拓扑和邻接,runtime data 只保留渲染、碰撞、光照和 streaming 需要的最小数据。混淆这三份数据会让运行时背负导入期结构,也会让导入器缺少几何处理所需的邻接信息。
11.3 半边结构(Half-Edge)与环状数据结构
半边结构(Half-Edge)把一条无向边拆成两条方向相反的 halfedge,并在每条 halfedge 上保存 next、twin、face、vertex 等引用。它解决的问题是稳定遍历网格邻域:沿着一个面的边界前进,跳到相邻面,围绕一个顶点访问一圈面,沿边界环找到孔洞。
CGAL 的 Halfedge Data Structures manual 将 halfedge 描述为组合数据结构,并说明 face 和 vertex 可存储一个 incident halfedge,halfedge 至少提供 next 和 opposite 引用。这类结构的重点是关系查询;几何坐标、法线、材质和自定义属性由上层结构挂载。
GuardRailPanel 的孔洞边缘可以用 half-edge 直接遍历。选中孔边缘上的一条 halfedge,沿 next 前进可以得到该面的边界,沿 twin 可以跳到相邻面;如果某条边没有相邻内部面,导入器可以为它创建 boundary halfedge 或使用 sentinel 标记。这样,孔洞边界环、外轮廓边界环和裂缝都能用同一套遍历逻辑处理。
下面是一个简化 half-edge 结构。它省略内存池和句柄系统,只保留核心引用,适合说明关系。
struct HalfEdge {
uint32_t vertex; // 当前 halfedge 指向的终点
uint32_t face; // 所属面
uint32_t next; // 同一面内下一条 halfedge
uint32_t twin; // 反方向 halfedge
};
struct Face {
uint32_t halfedge;
};
struct VertexTopo {
uint32_t halfedge;
};
这组结构的遍历方式和 index buffer 不同。index buffer 擅长按三角形顺序提交给 GPU;half-edge 擅长回答“从这个元素能到达哪些邻居”。围绕顶点的一环遍历通常从一个 incident halfedge 出发,反复执行 twin 后接 next,直到回到起点或遇到边界。这个环状遍历能支持平滑法线、平均曲率、顶点选择扩展和局部编辑。
边折叠(edge collapse)会把一条边的两个端点合并,并删除或改写周围面。执行前要检查折叠是否产生翻面、退化三角形、材质边界破坏或非流形连接。half-edge 让导入器能在局部邻域内枚举受影响面,更新 next、twin 和 incident 引用。LOD 简化、碰撞代理生成和自动修复都依赖这种局部更新能力。
边分裂(edge split)会在一条边上插入新顶点,并把相关面拆成更多面。对护栏板孔洞,如果倒角需要更圆滑的轮廓,导入器或建模工具会沿边界插入新顶点。half-edge 能明确找到边两侧的面和边界状态,因此能把新增 halfedge 与旧环正确连接。
边界处理是 half-edge 实现中的核心细节。常见做法有两种:一种为边界创建虚拟 boundary face,让边界也形成可遍历环;另一种使用无效 twin 或特殊标记。虚拟 boundary face 使“沿环遍历”逻辑统一,但会增加结构管理成本;特殊标记更轻量,但遍历代码要显式处理边界分支。
半边结构和 GPU 运行时结构的边界要清楚。half-edge 对编辑、导入、简化和拓扑验证很有价值,直接作为每帧渲染输入会增加随机访问和指针跳转成本。运行时通常把 half-edge 的处理结果烘焙为连续的 vertex/index buffer、cluster、meshlet 或碰撞结构。CPU 编辑结构回答“邻居是谁”,GPU 渲染结构回答“下一批连续字节在哪里”。
11.4 网格加载与存储优化
网格加载的目标是把作者数据转换成运行时可直接使用的数据。对 GuardRailPanel,导入器要完成单位和坐标转换、属性校验、顶点去重、索引生成、法线与切线处理、材质切分、量化压缩、索引重排和 streaming 分块。每一步都应保留可追踪证据,例如输入顶点数、输出 render vertex 数、submesh 数量、index 类型、压缩误差和缓存优化前后指标。
去重(deduplication)先要明确“相同”的定义。几何 position 相同只说明两个点处在同一空间位置;render vertex 去重需要 position、normal、tangent、UV、color、joint、weight 和自定义属性全部满足合并条件。孔洞边缘的硬法线会阻止合并,UV seam 会阻止合并,材质边界通常会把三角形拆到不同 submesh 中。
索引重排(index reorder)服务 post-transform cache。GPU 顶点着色器处理一个 render vertex 后,硬件通常能在短时间内复用变换结果;三角形顺序越能重用相邻顶点,重复 vertex shader 执行越少。meshoptimizer 文档中的 vertex cache optimization 描述了通过重排三角形序列提升局部性的方法,并指出不同 GPU 架构的复用细节存在差异。
切线生成(tangent generation)依赖 position、normal 和 UV。法线贴图在 fragment shader 中通过 tangent space 解释扰动方向,导入器必须让 tangent 与 UV island 和 normal split 对齐。对护栏板孔洞,倒角面和正面如果使用不同 UV island,切线也应随 render vertex 拆分。切线错误的画面信号通常是法线贴图在接缝处翻转或高光方向突变。
量化压缩(quantization)把 float 属性转换为更小的整数表示。例如 position 可以按 mesh bounds 转成 16-bit 或 10-bit 分量,normal 和 tangent 可以用归一化整数或专用编码。量化的判断依据是屏幕误差和 shader 解码成本:静态远景物体更适合压缩,近景硬表面需要控制 position 和 normal 误差。压缩后应记录最大误差和包围盒,方便资产审查。
cache reorder 与 overdraw reorder 要分开判断。前者面向顶点复用,后者面向像素遮挡。meshoptimizer 文档也说明 overdraw 优化可能牺牲部分 vertex cache 效率,并且在某些 tile-based GPU 上收益受限。导入器应把这类优化作为可测量策略:在目标场景里观察 vertex shader 时间、fragment shader 时间、overdraw heatmap 或 GPU counter,再决定是否启用。
Streaming 关注大资产的加载粒度。一个巨型网格如果一次性上传,会增加启动等待和显存峰值;按 chunk、LOD、material 或空间 cluster 切分后,可以按相机距离和可见性逐步加载。切分时要保留边界属性一致性,尤其是法线、切线、lightmap UV 和碰撞代理。否则 chunk 接缝会在光照或阴影中显现。
一个可执行加载顺序如下:先解析格式并建立原始属性视图,再执行拓扑审查,然后生成 render vertex 和 index buffer,随后做 tangent、bounds、LOD 和压缩,最后按 material、LOD、streaming chunk 写入运行时资产。glTF 的 buffer、bufferView 和 accessor 分层也体现了这种数据视图思想:buffer 是二进制 blob,bufferView 是连续片段,accessor 定义类型化读取方式;规范还要求 vertex indices 和 vertex attributes 使用的 buffer view 只包含同一种数据。
下面的伪代码展示导入器的主路径。它强调阶段顺序,省略具体格式解析和 API 上传细节。
ImportedMesh import_mesh(const SourceMesh& source) {
TopologyGraph topo = build_topology_graph(source.positions, source.polygons);
TopologyReport report = audit_topology(topo);
AttributeSet attributes = split_render_vertices(source, report);
TangentSet tangents = generate_tangents(attributes);
IndexBuffer indices = build_indices(attributes);
reorder_for_vertex_cache(indices, attributes.positions);
QuantizedMesh packed = quantize_attributes(attributes, tangents, indices);
return build_runtime_mesh(packed, source.material_slots);
}
这段伪代码的关键判断是先拓扑审查,再渲染顶点拆分。顺序反过来会让导入器把 UV seam、硬法线 split 和非流形问题混在一起,后续很难判断顶点数量上升来自有效属性拆分还是来自脏数据。
11.5 实时引擎中的网格数据组织策略
实时引擎不会用一种结构覆盖所有网格。static mesh、skinned mesh、submesh、material split 和 instance data 分别服务不同运行时约束。组织策略的核心是把更新频率、渲染状态、内存布局和 draw call 边界对齐。
Static mesh 的几何属性在运行时基本固定。GuardRailPanel 的主体适合做 static mesh:position、normal、tangent 和 UV 一次上传到 GPU,帧间只更新 object transform 或 instance data。它的优化重点是压缩、索引重排、LOD、bounds、streaming 和 material split。
Skinned mesh 的顶点位置会随骨骼或形变变化。它需要 joint index、joint weight、bind pose、animation buffer 或 compute skinning 输出。skinned mesh 的拓扑在大多数运行时保持固定,但 vertex shader 或 compute pass 会根据骨骼矩阵计算变形后位置和法线。它的瓶颈经常出现在属性带宽、骨骼矩阵读取和重复 skinning。
Submesh 是一次 mesh 内部按材质、pipeline state、LOD 或索引范围划分出的渲染单元。glTF primitive 与引擎 submesh 很接近:每个 primitive 绑定一组属性、可选索引、材质和 topology mode。护栏板主体和螺栓区域使用不同材质时,应拆成两个 submesh;这样会增加 draw call 或 multi-draw 项,但能让材质状态清晰。
Material split 的代价来自状态切换和批次碎片化。一个 mesh 如果被过度切成很多材质区域,CPU 提交、descriptor 绑定和 GPU pipeline 切换成本会升高。合并材质可以减少提交,但可能增加纹理 atlas、shader 分支或无效像素成本。判断时要看材质数量、可见实例数、pass 数量和平台提交开销。
Instance data 让多个对象共享同一份 mesh buffer,只为每个实例提供 transform、颜色、材质索引或自定义参数。护栏板在道路场景中可能重复出现数百次,实例化能把几何数据复用到多个 draw。instance data 通常放在单独 buffer,更新频率高于静态 vertex buffer;shader 根据 instance id 读取每个实例的 transform 和参数。
下面的结构展示一个简化运行时 mesh。它把几何 buffer、submesh 范围和 instance buffer 分开,便于不同更新频率的数据走不同上传路径。
struct SubmeshRange {
uint32_t first_index;
uint32_t index_count;
uint32_t material_id;
};
struct RuntimeMesh {
GpuBuffer vertex_buffer;
GpuBuffer index_buffer;
SubmeshRange* submeshes;
uint32_t submesh_count;
Bounds local_bounds;
};
struct InstanceData {
float object_to_world[16];
uint32_t material_variant;
};
这个结构把 RuntimeMesh 作为共享资源,把 InstanceData 作为每帧或按需更新的数据。渲染器构建 draw list 时,先按 mesh、material、pipeline 和 pass 组织,再填充 instance range。这样能减少重复上传,保持可见性剔除、LOD 和材质排序的操作空间。
实时引擎的网格组织可以按以下顺序判断。先看顶点是否随帧变化,决定 static mesh 或 skinned mesh。再看材质和 pipeline state,决定 submesh 切分。然后看对象是否大量重复,决定 instance data。最后看资产规模和场景可见性,决定 streaming chunk、LOD 和 cluster。这个顺序从更新频率开始,因为更新频率决定 buffer 生命周期和同步成本。
| 决策点 | 观察证据 | 常见组织方式 |
|---|---|---|
| 顶点是否变形 | 骨骼、morph、布料、procedural update | skinned mesh 或 dynamic vertex stream |
| 材质是否不同 | shader、texture、blend、double-sided 状态 | submesh 或 material range |
| 对象是否重复 | 同一 mesh 多次出现 | instance buffer |
| 是否需要分批加载 | 资产大、远景多、开放世界 | chunk、LOD、streaming page |
| 是否需要 GPU-driven | draw 数量大、剔除复杂 | cluster、meshlet、indirect draw |
本章的最终结论是:网格数据结构要按问题分层。拓扑结构服务编辑、验证和转换;连续 buffer 服务 GPU 读取;submesh 和 instance data 服务渲染状态与批处理;LOD、chunk 和压缩服务规模控制。把这些层次拆清楚,才能解释同一个模型为什么在 DCC 工具里是一个物体,在引擎里会变成多份 buffer、多个 submesh 和多条 draw 命令。
最小自检任务
给定一个护栏板网格:它有一个矩形外轮廓、四个螺栓孔、主体金属材质和螺栓边缘材质。导入后发现 render vertex 数量远大于几何 position 数量,并且有一条孔边缘边被三个三角形引用。请判断导入器应如何区分有效属性拆分和拓扑错误,并说明最终运行时应保留哪些结构。
答案要点
先用几何 position 和 polygon index 构建边表或 half-edge,统计每条无向边的 incident face 数量。外轮廓和螺栓孔边缘形成只被一个面使用的 boundary loop,属于可解释边界;一条孔边缘边被三个三角形引用时,应标记为 non-manifold edge,并在导入阶段修复、拆分或拒绝该资产。
再检查 render vertex 拆分来源。硬法线、UV island、tangent split、skin weight 差异和材质边界会让相同 position 生成多个 render vertex,这属于有效属性拆分;它们应被记录为 vertex count 增长原因。非流形边产生的重复点或异常三角形属于拓扑错误,处理前不应直接进入压缩和 cache reorder。
最终运行时通常保留 vertex buffer、index buffer、submesh range、material id、bounds、LOD 或 streaming chunk。half-edge、边表和拓扑报告主要保留在导入产物、调试数据或编辑器缓存中。运行时渲染路径需要连续读取和稳定 draw list;拓扑结构用于资产处理和诊断。
本章知识点总结
- 拓扑关系:网格拓扑描述顶点、边和面如何相连,决定导入器能否稳定执行法线、切线、简化和修复。
- 邻接查询:邻接关系服务 CPU 侧几何处理,最终 vertex buffer 通常用连续字节和索引隐含这些关系。
- 流形边界:内部边通常连接两个面,边界边连接一个面,孔洞由闭合边界环表达。
- 非流形边:一条边被三个或更多面使用会破坏局部曲面遍历,并影响简化、碰撞和法线生成。
- 渲染顶点:render vertex 是同一组 shader 输入属性的集合,相同 position 会因法线、UV、切线或材质边界拆分。
- Buffer 布局:interleaved layout 适合同时读取多数属性的 pass,separate streams 适合不同更新频率或不同 pass 读取需求。
- Half-edge:半边结构通过
next、twin、face和vertex引用支持面环、顶点一环和边界环遍历。 - 编辑结构:half-edge 适合导入、编辑、简化和拓扑验证,运行时渲染通常使用压缩后的连续 buffer。
- 去重条件:顶点去重应比较完整属性集合,position 相同不足以推出 render vertex 可以合并。
- 索引重排:index reorder 通过改善顶点复用局部性降低重复 vertex shader 执行。
- 切线生成:tangent 必须和 UV island、normal split 以及 render vertex 拆分保持一致,接缝错误会表现为高光或法线贴图异常。
- 量化压缩:属性量化用可接受屏幕误差换取更低带宽和存储成本,近景硬表面需要更严格误差控制。
- Submesh:submesh 按材质、pipeline state 或索引范围组织一次 mesh 内部的 draw 单元。
- 实例数据:instance data 让多个对象共享同一 mesh buffer,并把 transform 和变体参数放入独立更新路径。
- 组织顺序:实时引擎先按顶点更新频率区分 static 和 skinned,再按材质、重复实例和规模控制选择 submesh、instance、LOD 和 streaming。