第 25 章 性能优化指南摘要本章系统介绍了 VSG 性能优化的核心策略包括视锥剔除、细节层次LOD/PagedLOD、Draw Call 优化、流式加载与多线程、数据格式选择等关键技术。通过组合应用这些手段可有效解决大场景卡顿、显存占用激增、启动慢等常见性能问题。优化前应先测量定位瓶颈再针对性实施。本章定位VSG 已经很快但大场景仍会卡。本章汇总「视锥剔除 / LOD / 批处理 / 流式加载 / 多线程」的组合拳以及常见问题。25.1 本章目标理解 VSG 自动做的视锥剔除与包围球用LOD/PagedLOD做细节层次减少 Draw Call 与状态切换批处理、共享状态用DatabasePager限量流式加载、多线程录制。25.2 准备工作第 6 章LOD / Switch、第 12 章命令图、第 18 章DatabasePager已读。25.3 视锥剔除自动 包围球VSG 在RecordTraversal第 12 章中自动按包围球做视锥剔除——节点若不在视锥内则不被记录。前提节点/几何体的包围球正确。// 若加载的模型没有包围体或你手动修改了几何体记得重新计算vsg::computeBounds(node);// 为子树计算/刷新包围球另外可用CullNode/CullGroup第 17 章提过用于文字等显式包裹强制先做包围球裁剪再记录子树。25.4 细节层次LOD与PagedLODLOD第 6 章按屏幕占比在「同精度子节点」间切换适合单文件多精度模型PagedLOD第 18 章子节点按需从磁盘加载适合大世界/地形。autolodvsg::LOD::create();lod-boundvsg::dsphere(vsg::dvec3(0.0),1.0);// 包围球lod-addChild({0.0,highRes});// 近处高精度lod-addChild({0.2,lowRes});// 远处低精度25.5 减少 Draw Call 与状态切换手段说明合并几何体把多个小Geometry合并成一个共享顶点布局时减少 Draw Call实例化同一网格多次出现用DrawIndexed的instanceCount per-instance 数据而非复制节点共享 StateGroup相同管线的子图复用同一个StateGroup减少BindGraphicsPipeline/BindDescriptorSet批处理排序VSG 用Bin对绘制命令排序以减少状态切换复杂场景可使用自定义Bin策略纹理图集/Array多张小纹理合成图集或纹理数组减少描述符切换经验Draw Call 数量的下降往往比单网格优化更立竿见影。下面是一个将两个简单立方体几何体合并为一个Geometry对象的 C 示例// 假设我们有两个独立的立方体几何体简化示例顶点/索引数据已略去细节autocube1createCubeGeometry(vsg::vec3(-1.0f,0.0f,0.0f),0.5f);// 左边立方体autocube2createCubeGeometry(vsg::vec3(1.0f,0.0f,0.0f),0.5f);// 右边立方体// 合并前两个独立的 Geometry 节点至少需要 2 次 Draw CallautosceneBeforeMergevsg::Group::create();sceneBeforeMerge-addChild(cube1);sceneBeforeMerge-addChild(cube2);// 合并步骤将两个几何体的顶点、索引等数据合并到同一个 Geometry 中// 1. 收集所有顶点属性位置、法线、纹理坐标等automergedVerticesvsg::vec3Array::create();automergedIndicesvsg::ushortArray::create();// 假设 cube1 和 cube2 各自有独立的顶点/索引数据// 这里仅示意合并逻辑实际需要根据具体数据结构编写appendGeometryData(cube1,mergedVertices,mergedIndices);appendGeometryData(cube2,mergedVertices,mergedIndices,mergedVertices-size());// 偏移索引值// 2. 创建合并后的单一 Geometry 对象automergedGeometryvsg::Geometry::create();mergedGeometry-arraysvsg::DataList{mergedVertices};mergedGeometry-indicesmergedIndices;mergedGeometry-commands.push_back(vsg::DrawIndexed::create(mergedIndices-size(),1,0,0,0));// 3. 包装成 StateGroup 或直接加入场景automergedNodevsg::StateGroup::create();mergedNode-add(mergedGeometry);// 合并后只有一个 Geometry 节点只需 1 次 Draw CallautosceneAfterMergevsg::Group::create();sceneAfterMerge-addChild(mergedNode);// 说明// - 合并前2 个 Geometry → 至少 2 次 Draw Call可能更多取决于状态切换// - 合并后1 个 Geometry → 1 次 Draw Call// 注意合并要求几何体共享相同的顶点布局、材质和管线状态否则仍需分开绘制。// 补充完整的 createCubeGeometry 和 appendGeometryData 实现// 创建一个简单的立方体几何体中心位置为 center边长为 sizevsg::ref_ptrvsg::GeometrycreateCubeGeometry(constvsg::vec3center,floatsize){// 立方体有 8 个顶点autoverticesvsg::vec3Array::create(8);floathalfsize*0.5f;// 定义立方体的 8 个顶点局部坐标(*vertices)[0]vsg::vec3(-half,-half,-half)center;(*vertices)[1]vsg::vec3(half,-half,-half)center;(*vertices)[2]vsg::vec3(half,half,-half)center;(*vertices)[3]vsg::vec3(-half,half,-half)center;(*vertices)[4]vsg::vec3(-half,-half,half)center;(*vertices)[5]vsg::vec3(half,-half,half)center;(*vertices)[6]vsg::vec3(half,half,half)center;(*vertices)[7]vsg::vec3(-half,half,half)center;// 立方体有 6 个面每个面 2 个三角形共 12 个三角形36 个索引autoindicesvsg::ushortArray::create(36);// 前、后、左、右、上、下 6 个面的三角形索引// 前(*indices)[0]0;(*indices)[1]1;(*indices)[2]2;(*indices)[3]2;(*indices)[4]3;(*indices)[5]0;// 后(*indices)[6]4;(*indices)[7]5;(*indices)[8]6;(*indices)[9]6;(*indices)[10]7;(*indices)[11]4;// 左(*indices)[12]0;(*indices)[13]3;(*indices)[14]7;(*indices)[15]7;(*indices)[16]4;(*indices)[17]0;// 右(*indices)[18]1;(*indices)[19]5;(*indices)[20]6;(*indices)[21]6;(*indices)[22]2;(*indices)[23]1;// 上(*indices)[24]3;(*indices)[25]2;(*indices)[26]6;(*indices)[27]6;(*indices)[28]7;(*indices)[29]3;// 下(*indices)[30]0;(*indices)[31]4;(*indices)[32]5;(*indices)[33]5;(*indices)[34]1;(*indices)[35]0;// 创建几何体对象autogeometryvsg::Geometry::create();geometry-arraysvsg::DataList{vertices};geometry-indicesindices;geometry-commands.push_back(vsg::DrawIndexed::create(indices-size(),1,0,0,0));returngeometry;}// 将单个几何体的顶点和索引数据追加到合并的数组中voidappendGeometryData(vsg::ref_ptrvsg::Geometrygeometry,vsg::ref_ptrvsg::vec3ArraymergedVertices,vsg::ref_ptrvsg::ushortArraymergedIndices,uint32_tvertexOffset0){// 获取几何体的顶点数据假设只有位置属性autoverticesgeometry-arrays[0].castvsg::vec3Array();if(!vertices)return;// 获取几何体的索引数据autoindicesgeometry-indices.castvsg::ushortArray();if(!indices)return;// 1. 追加顶点数据size_t originalVertexCountmergedVertices-size();mergedVertices-reserve(originalVertexCountvertices-size());for(size_t i0;ivertices-size();i){mergedVertices-push_back((*vertices)[i]);}// 2. 追加索引数据并应用顶点偏移size_t originalIndexCountmergedIndices-size();mergedIndices-reserve(originalIndexCountindices-size());for(size_t i0;iindices-size();i){// 注意这里假设原始索引是从 0 开始的局部索引// 合并时需要加上之前已合并的顶点总数作为偏移mergedIndices-push_back((*indices)[i]static_castuint16_t(vertexOffset));}}// 数据变化说明// 合并前// - cube1: 8 个顶点36 个索引索引范围 0-7// - cube2: 8 个顶点36 个索引索引范围 0-7// 两个几何体独立共 16 个顶点72 个索引需要 2 次 Draw Call//// 合并后// - mergedGeometry: 16 个顶点72 个索引// - 顶点 0-7: cube1 的顶点// - 顶点 8-15: cube2 的顶点索引已偏移 8// - 索引 0-35: cube1 的原始索引0-7// - 索引 36-71: cube2 的原始索引8-15已偏移// 合并为一个几何体只需 1 次 Draw Call25.6 流式加载与多线程DatabasePager第 18 章后台线程读/编译PagedLOD限定targetMaxNumPagedLODWithHighResSubgraphs控制显存占用多线程录制第 18 章多窗口/多CommandGraph并行recordAndSubmit避免每帧分配复用vsg::Value/BufferInfo依赖vsg::Allocator内存池第 23 章少 new/delete。25.7 加载与格式发布/加载用.vsgb二进制而非.vsgt启动更快第 21 章大纹理用KTX2/Basis等 GPU 压缩格式vsgXchange支持第 15 章节省带宽与显存占用用Options::fileCache共享已加载对象避免重复读盘。25.8 测量而不是猜用vsg::Instrumentation给Viewer与AnimationManager做性能剖析第 12/14 章提过assignInstrumentation用 RenderDoc第 26 章看每帧 Draw Call 数、绑定切换、纹理内存先量化瓶颈CPU 录制GPU 填充带宽再针对性优化。25.9 优化清单速查25.9 优化手段前后性能对比优化手段优化前指标示例优化后指标示例适用场景合并几何体100 个独立小几何体 → 约 100 次 Draw Call显存占用分散合并为 1 个几何体 → 1 次 Draw Call显存连续大量相同材质/顶点布局的小物体如草丛、碎石、建筑部件实例化1000 个相同树木每棵独立 Geometry → 1000 次 Draw Call1 个 Geometry 实例化数据 → 1 次 Draw Call大量重复的物体树木、路灯、士兵等共享 StateGroup10 个不同材质球每个独立 StateGroup → 10 次管线/描述符绑定共享 1 个 StateGroup → 1 次绑定多个子图使用相同管线、纹理、Uniform 时纹理图集/Array10 张小纹理各自绑定 → 10 次纹理切换合并为 1 张图集 → 1 次纹理绑定UI 图标、字体图集、地形贴图等小纹理集合LOD/PagedLOD远处高模仍渲染 → 每帧 50 万三角形显存占用 500 MB远处切低模 → 每帧 10 万三角形显存 200 MB大场景、地形、复杂角色模型视锥剔除全场景 1000 个节点全部录制 → CPU 录制负担重仅视锥内 200 个节点录制 → CPU 负担降低 80%开放世界、室内外大场景纹理压缩KTX24096×4096 RGBA8 未压缩 → 64 MB 纹理内存KTX2 ASTC 8×8 压缩 → 8 MB 纹理内存大尺寸纹理地形、天空盒、角色贴图二进制格式.vsgb加载 …vsgt 文本格式模型 → 解析耗时 2 秒加载 .vsgb 二进制 → 解析耗时 0.3 秒任何需要快速启动的场景症状优先尝试远处渲染卡顿加LOD/PagedLOD确认包围球正确Draw Call 过多合并几何、实例化、共享StateGroup显存占用激增DatabasePager限量、纹理压缩、复用缓冲主线程阻塞后台分页、避免主线程vsg::read大文件启动慢用.vsgb、开fileCache25.10 小结视锥剔除自动发生但依赖正确的包围球computeBoundsLOD/PagedLOD做层次DatabasePager做流式与限量减少 Draw Call合并/实例化/共享状态通常收益最大多线程录制 内存池兜底先测量再优化。25.11 延伸阅读与下一章预告第 18 章《多线程与数据加载》分页与线程细节第 26 章《常见问题与调试技巧》使用工具定位性能瓶颈第 23 章《内存管理》分配器与泄漏排查。25.12 实战练习练习 1路灯场景优化场景描述一个城市街道场景包含 100 个相同的路灯模型每个路灯都是一个独立的Geometry节点使用相同的材质和纹理。问题当前渲染每帧需要约 100 次 Draw CallCPU 录制开销较大。优化任务请设计一个优化方案将 Draw Call 降低到最低并说明具体实现步骤。参考答案思路使用实例化Instancing将路灯几何体改为一个Geometry配合DrawIndexed的instanceCount参数设为 100并通过 per-instance 数据如vsg::mat4Array传递每个路灯的位置、旋转等变换矩阵。共享 StateGroup所有路灯共享同一个StateGroup包含相同的管线、描述符集材质、纹理。合并几何体备选如果路灯结构简单且顶点布局一致也可考虑将 100 个路灯的顶点数据合并到一个Geometry中但实例化更灵活支持独立变换、剔除。效果预估优化后 Draw Call 从 100 次降为 1 次CPU 录制开销显著降低GPU 可通过一次绘制调用渲染全部实例。// 练习1路灯场景优化 - 实例化关键代码示例// 1. 创建单个路灯几何体简化立方体autolampGeometrycreateCubeGeometry(vsg::vec3(0.0f),1.0f);// 中心在原点边长1// 2. 创建实例变换矩阵数组100个路灯的位置autoinstanceTransformsvsg::mat4Array::create(100);for(inti0;i100;i){floatx(i%10)*5.0f;// 每行10个间距5米floatz(i/10)*5.0f;(*instanceTransforms)[i]vsg::translate(x,0.0f,z);// 平移到街道位置}// 3. 创建描述符集将实例数据传递给着色器autoinstanceDescriptorvsg::DescriptorBuffer::create(instanceTransforms,0,0);autodescriptorSetvsg::DescriptorSet::create(descriptorSetLayout,vsg::Descriptors{instanceDescriptor});// 4. 配置 DrawIndexed 使用实例化autodrawCommandvsg::DrawIndexed::create(lampGeometry-indices-size(),// 索引数量100,// instanceCount实例数量关键参数0,0,0// 其他参数);// 5. 创建 StateGroup 并添加几何体与描述符集autolampStateGroupvsg::StateGroup::create();lampStateGroup-add(lampGeometry);lampStateGroup-add(descriptorSet);// 效果从100次Draw Call降为1次CPU录制开销大幅降低// 注意着色器中需使用 gl_InstanceIndex 访问 per-instance 数据练习 2地形场景卡顿分析场景描述一个开放世界地形场景在视角拉远时能看到大片地形帧率明显下降但近处特写时流畅。问题分析可能的原因并提出解决步骤。参考答案思路可能原因分析缺少 LOD远处仍使用高精度网格三角形数量过多。视锥剔除失效地形节点包围球过大或未正确计算导致大量不可见面片仍被录制。纹理内存过大远处地形仍加载高分辨率纹理显存带宽压力大。分页加载未生效整个地形一次性加载而非按需流式加载。解决步骤步骤 1测量定位使用vsg::Instrumentation或 RenderDoc 确认瓶颈是 GPU 填充三角形过多还是 CPU 录制节点过多。步骤 2应用 LOD/PagedLOD为地形创建多个细节层次远处切换为低模。使用PagedLOD实现按需分块加载。步骤 3验证包围球对地形节点调用vsg::computeBounds()确保视锥剔除能正确剔除不可见区域。步骤 4纹理优化将地形纹理转换为 KTX2/Basis 压缩格式并考虑使用纹理池或虚拟纹理技术。步骤 5启用 DatabasePager配置DatabasePager的targetMaxNumPagedLODWithHighResSubgraphs限制同时加载的高精度块数量避免显存占用激增。步骤 6测试验证优化后再次测量帧率与显存占用确认卡顿缓解。// 练习2地形场景卡顿分析 - PagedLOD 配置代码示例// 1. 创建 PagedLOD 节点用于地形分块autoterrainPagedLODvsg::PagedLOD::create();// 2. 设置包围球重要确保视锥剔除正确工作terrainPagedLOD-boundvsg::dsphere(vsg::dvec3(0.0,0.0,0.0),500.0);// 半径500米// 3. 添加不同细节层次的子节点// 高精度模型近处加载terrainPagedLOD-addChild(vsg::PagedLOD::Child{0.0,highResTile},// 屏幕占比0-20%时显示high_res_tile.vsgb// 高精度模型文件路径);// 中精度模型中距离加载terrainPagedLOD-addChild(vsg::PagedLOD::Child{0.2,mediumResTile},// 屏幕占比20-50%时显示medium_res_tile.vsgb// 中精度模型文件路径);// 低精度模型远处加载或作为占位符terrainPagedLOD-addChild(vsg::PagedLOD::Child{0.5,lowResTile},// 屏幕占比50%时显示low_res_tile.vsgb// 低精度模型文件路径);// 4. 配置 DatabasePager 控制流式加载autodatabasePagervsg::DatabasePager::create();databasePager-targetMaxNumPagedLODWithHighResSubgraphs10;// 关键参数限制同时加载的高精度块数量databasePager-minimumTimeAvailableForDatabasePager0.001;// 每帧分配给分页线程的最小时间秒// 5. 将 DatabasePager 附加到 Viewerviewer-addUpdateOperation(databasePager);// 效果远处自动切换为低模按需加载地形块显存占用可控// 注意需要预先准备不同精度的地形分块文件.vsgb格式