ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

three.js BatchedMesh 深度指南:用多绘制批次渲染减少 Draw Call

three.js BatchedMesh 深度指南:用多绘制批次渲染减少 Draw Call three.js BatchedMesh 深度指南用多绘制批次渲染减少 Draw Call【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js本篇基于 three.js 官方 API 文档与源码实现系统讲解BatchedMesh类的使用方式、容量参数、几何体与实例管理 API以及其底层的间接绘制multi-draw / indirect draw渲染管线。读完之后你将能够在同一材质、不同几何体或不同变换的大规模场景中用BatchedMesh替代成百上千个独立Mesh掌握addGeometry/addInstance/setMatrixAt等完整 API 的正确用法理解perObjectFrustumCulled、sortObjects、customSort等渲染属性背后的裁剪与排序机制以及optimize()的缓冲区重排原理。BatchedMesh 是什么BatchedMesh是一种带多绘制批次渲染multi draw batch rendering支持的特殊Mesh继承链为EventDispatcher → Object3D → Mesh → BatchedMesh实现位于 src/objects/BatchedMesh.js。它的官方定位是当你需要渲染大量使用相同材质、但拥有不同几何体或不同世界变换的对象时请使用这个类。使用BatchedMesh可以帮助减少 draw call 数量从而提升应用的整体渲染性能。它与InstancedMesh的关键区别在于InstancedMesh要求所有实例共享同一个几何体而BatchedMesh允许一个批次内混入多个不同的几何体每个几何体可以有多份实例并且每个实例还可以拥有独立的颜色通过setColorAt与可见性通过setVisibleAt。从源码结构看BatchedMesh把所有子几何体的顶点/索引数据合并进一个大BufferGeometry再为每个实例维护一份绘制区间start/count元数据渲染时借助渲染器的间接绘制能力一次性提交。官方在 examples/webgl_mesh_batch.html 和 examples/webgpu_mesh_batch.html 中提供了 WebGL 与 WebGPU 两个可运行的完整示例可作为本文所有代码的参考实现。快速上手完整示例以下是官方文档给出的标准用法覆盖初始化 → 添加几何体 → 创建实例 → 设置矩阵 → 加入场景的完整流程const box new THREE.BoxGeometry( 1, 1, 1 ); const sphere new THREE.SphereGeometry( 1, 12, 12 ); const material new THREE.MeshBasicMaterial( { color: 0x00ff00 } ); // 初始化 BatchedMesh 并添加几何体 const batchedMesh new BatchedMesh( 10, 5000, 10000, material ); const boxGeometryId batchedMesh.addGeometry( box ); const sphereGeometryId batchedMesh.addGeometry( sphere ); // 为这些几何体创建实例 const boxInstancedId1 batchedMesh.addInstance( boxGeometryId ); const boxInstancedId2 batchedMesh.addInstance( boxGeometryId ); const sphereInstancedId1 batchedMesh.addInstance( sphereGeometryId ); const sphereInstancedId2 batchedMesh.addInstance( sphereGeometryId ); // 设置每个实例的局部变换 batchedMesh.setMatrixAt( boxInstancedId1, boxMatrix1 ); batchedMesh.setMatrixAt( boxInstancedId2, boxMatrix2 ); batchedMesh.setMatrixAt( sphereInstancedId1, sphereMatrix1 ); batchedMesh.setMatrixAt( sphereInstancedId2, sphereMatrix2 ); scene.add( batchedMesh );流程可以归纳为三条主线几何体线addGeometry返回geometryId之后可用setGeometryAt换装、deleteGeometry删除实例线addInstance( geometryId )返回instanceId之后用setMatrixAt/setColorAt/setVisibleAt/deleteInstance操作渲染线把batchedMesh像普通Mesh一样加入场景即可裁剪、排序、间接绘制都发生在渲染器的onBeforeRender钩子里无需手动干预。构造函数与容量参数new BatchedMesh( maxInstanceCount, maxVertexCount, maxIndexCount, material )参数说明默认值maxInstanceCount计划添加并渲染的实例数量上限必填maxVertexCount所有唯一几何体合计占用的顶点数上限必填maxIndexCount所有唯一几何体合计占用的索引数上限maxVertexCount * 2material网格材质单个Material或材质数组必填注意容量参数的语义maxVertexCount/maxIndexCount约束的是去重后的几何体集合相同几何体添加多份实例不会重复占用空间而maxInstanceCount约束的是实例总数。从源码看这些容量值在构造时就直接决定了三张内部DataTexture的布局_initMatricesTexture、_initIndirectTexture、_initColorsTexture矩阵纹理_matricesTextureRGBAFormat FloatType1 个Matrix4恰好占用 4 个像素RGBA RGBA RGBA RGBA 对应矩阵的 4 列纹理边长取ceil(sqrt(maxInstanceCount * 4) / 4) * 4且不小于 4。也就是说 16×16 的纹理最多容纳 64 个矩阵64×64 容纳 1024 个间接纹理_indirectTextureRedIntegerFormat UnsignedIntType边长ceil(sqrt(maxInstanceCount))每个像素存一个实例索引供渲染器把第 i 次绘制使用第几个实例的矩阵传入顶点着色器颜色纹理_colorsTexture惰性创建仅在首次调用setColorAt时才分配初始全白避免无需求时浪费显存。maxIndexCount省略时的默认行为在构造函数签名中直接体现constructor( maxInstanceCount, maxVertexCount, maxIndexCount maxVertexCount * 2, material )见 src/objects/BatchedMesh.js#L192。容量不足时addInstance会抛出Maximum item count reached错误预留空间超限的addGeometry会抛出Reserved space request exceeds the maximum buffer size错误。两者都可以通过事后扩容/缩容 API见下文调整。属性总览属性类型默认值说明boundingBoxBox3null批次的包围盒需通过computeBoundingBox()显式计算boundingSphereSpherenull批次的包围球需通过computeBoundingSphere()显式计算customSortFunctionnull渲染前执行的自定义排序函数接收待排序实例列表每项含z深度字段和相机instanceCountnumber只读—当前实例数量isBatchedMeshboolean只读true类型测试标志maxInstanceCountnumber只读—批次可存储的最大实例数perObjectFrustumCulledbooleantrue是否对批次内的单个对象做视锥剔除sortObjectsbooleantrue是否对批次内的对象排序以改善 overdraw 相关瑕疵材质标记为transparent时按远到近渲染否则按近到远渲染unusedIndexCountnumber只读—未使用的索引数量unusedVertexCountnumber只读—未使用的顶点数量其中perObjectFrustumCulled与sortObjects直接对应渲染管线中的两个分支两者都为true时每帧渲染前都会遍历实例做视锥测试和深度排序若可见性未变化且两者均为false则整个onBeforeRender直接短路返回见 onBeforeRender。对于静态大批次关闭这两项可以省掉每帧的遍历开销。几何体管理addGeometry / setGeometryAt / optimize.addGeometry( geometry, reservedVertexCount, reservedIndexCount ) : number把几何体加入批次并返回geometryId供其他函数使用。geometry要添加的BufferGeometryreservedVertexCount可选默认-1为该几何体预留的顶点缓冲区空间。如果计划稍后用比原几何体更大的几何体替换这个槽位就必须在这里预留足够的空间默认取所给几何体顶点缓冲区的长度reservedIndexCount可选默认-1索引缓冲区预留空间规则同上默认取所给几何体索引缓冲区的长度。源码中的三条硬约束_validateGeometry索引必须一致——批次内所有几何体要么都有index要么都没有否则抛出All geometries must consistently have index属性必须一致——后加入的几何体必须包含首批次几何体已有的全部属性且各属性的itemSize与normalized必须一致否则抛出All attributes must have a consistent itemSize and normalized value预留空间不能越界——indexStart reservedIndexCount maxIndexCount或vertexStart reservedVertexCount maxVertexCount时抛出Reserved space request exceeds the maximum buffer size。值得注意的实现细节addGeometry内部会把索引值平移为该几何体在合并顶点缓冲区中的偏移量vertexStart srcIndex.getX(i)见 setGeometryAt预留区中未被实际几何体使用的索引会填充为指向vertexStart的退化三角形保证空闲区间渲染出来不产生任何可见像素。.setGeometryAt( geometryId, geometry ) : number用新几何体替换指定 ID 的几何体。若预留空间不足则抛出错误调用它会改变所有正在渲染该几何体的实例——这是几何体去重设计的直接推论同一个geometryId被多少份实例引用就会有多少份实例的外观一起更新例如可用来做动画帧序列。.deleteGeometry( geometryId ) : BatchedMesh删除该几何体引用它的所有实例也会被一并删除副作用见 deleteGeometry。删除是软删除geometryId进入空闲池之后再次addGeometry时会按 ID 升序优先复用最小的空闲 ID。.optimize() : BatchedMesh重排批次内的子几何体回收已删除几何体留下的空隙从而腾出空间添加新几何体。从 optimize 实现 看它按vertexStart排序所有活跃的几何体区间用array.copyWithin把顶点/索引数据向前压实并同步修正索引指针的偏移量elementDelta。如果批次生命周期内会频繁增删几何体建议定期调用optimize()防止碎片化导致unusedVertexCount偏小而实际空间不足。扩容与缩容setInstanceCount / setGeometrySize.setInstanceCount( maxInstanceCount )调整实例容量。会先压缩末尾已释放的实例槽位若目标值小于仍有实例占用的 ID 范围抛出Instance ids outside the range ... are being used. Cannot shrink instance count。调整后矩阵/间接/颜色三张纹理都会按新容量重建并拷贝旧数据setInstanceCount。仓库单元测试 test/unit/src/objects/BatchedMesh.tests.js 正是验证了这个删除实例后收缩容量的场景4 个实例删 2 个再setInstanceCount( 2 )断言instanceCount 2.setGeometrySize( maxVertexCount, maxIndexCount )调整顶点/索引缓冲区容量。若存在活跃区间的vertexStart reservedVertexCount或索引对应值超出新上限抛出Cannot shrink further类错误。内部会 dispose 旧几何体、按旧几何体的属性布局重建并整体拷贝数据setGeometrySize。实例管理addInstance 与各类 set/get.addInstance( geometryId ) : number用已注册的几何体创建新实例返回instanceId。实现上addInstance达到maxInstanceCount且没有可回收的已删除 ID 时抛出Maximum item count reached有空闲 ID 时按升序优先复用。新建实例的矩阵在矩阵纹理中初始化为单位矩阵颜色初始化为白色。矩阵.setMatrixAt( instanceId, matrix ) / .getMatrixAt( instanceId, matrix )设置/读取单个实例的局部变换相对batchedMesh本身。注意文档明确声明不支持负缩放的矩阵negatively scaled matrices are not supported因为这会翻转绕序导致背面剔除逻辑异常。实现上就是把 16 个 float 写入矩阵纹理对应 4 个像素并标记needsUpdatesetMatrixAt。颜色.setColorAt( instanceId, color ) / .getColorAt( instanceId, color )color既可以是ColorRGB也可以是Vector4RGBA附带 alpha。首次调用setColorAt时惰性创建颜色纹理未设置过颜色时getColorAt返回全白1,1,1[,1]。可见性与几何绑定.setVisibleAt( instanceId, visible )/.getVisibleAt( instanceId )控制单个实例的可见性等价于InstancedMesh中隐藏某个实例的能力且不会把已删除实例误标为可见.setGeometryIdAt( instanceId, geometryId )/.getGeometryIdAt( instanceId )在实例级别动态换绑几何体无需销毁重建实例.validateInstanceId( instanceId )/.validateGeometryId( geometryId )对越界或已删除的 ID 抛出明确错误各get/set方法内部都会先调用它帮助你在开发期尽早发现 ID 管理错误。删除与区间查询.deleteInstance( instanceId ) : BatchedMesh软删除ID 进入可复用池.getGeometryRangeAt( geometryId, target ) : Object返回该几何体在合并缓冲区中的区间数据字段包括vertexStart / vertexCount / reservedVertexCount / indexStart / indexCount / reservedIndexCount / start / count见 getGeometryRangeAt可用于自定义渲染、LOD 或与其他系统交换元数据。包围盒与包围球.computeBoundingBox()/.computeBoundingSphere()默认均为null需显式计算。实现是把每个活跃实例的几何体包围体经实例矩阵变换后求并集computeBoundingBox可用于整体视锥剔除或拾取粗筛.getBoundingBoxAt( geometryId, target )/.getBoundingSphereAt( geometryId, target )返回单个几何体的包围体未找到对应 ID 时返回null首次调用时会按该几何体的索引/顶点区间惰性计算并缓存此后直接复用。渲染管线深入裁剪、排序与间接绘制BatchedMesh的核心价值体现在每次绘制前的 onBeforeRender 钩子中它完成了从实例元数据到多绘制指令的转换短路优化若可见性自上次以来没有变化且perObjectFrustumCulled与sortObjects均为false直接返回不遍历任何实例视锥剔除可选当perObjectFrustumCulled为true时在批次局部坐标系下构造视锥数组相机会用FrustumArray处理 XR 多视图对每个实例的世界空间包围球做intersectsSphere测试剔除视野外的实例深度排序可选当sortObjects为true时把通过剔除的实例压入带对象池的MultiDrawRenderList每项记录(start, count, z, index)其中z是包围球中心沿相机前向的投影距离。默认排序函数是不透明材质sortOpaquea.z - b.z近到远——提前写入深度减少后续片元的 overdraw透明材质sortTransparentb.z - a.z远到近——保证 alpha 混合的正确叠加顺序若设置了customSort则由你的函数(list, camera) ...接管排序列表中每项的z字段可用于深度排序或自定义规则如按材质批次分组写出多绘制指令把排序结果写入_multiDrawStartsInt32Array索引偏移索引模式下换算为字节偏移与_multiDrawCountsInt32Array索引数量同时把第 i 次绘制对应第几个实例写入间接纹理的indirectArray供顶点着色器采样该实例的矩阵线框兼容当material.wireframe为true时渲染器会把三角形隐式转换为每三角形 3 条线段的线属性因此multiDrawMultiplier 2顶点数×2 的索引空间换算并按顶点数是否超过 65535 选择 2 字节或 4 字节的字节步长。此外onBeforeShadow会直接委托给onBeforeRender使用阴影相机意味着投影中的阴影渲染同样享受批量化的裁剪与排序而不会回退到逐实例绘制。射线检测raycast 的批量支持BatchedMesh重写了 raycast遍历所有可见且活跃的实例用setDrawRange( geometryInfo.start, geometryInfo.count )让一个共享的临时Mesh逐实例复用父类的三角形求交逻辑命中结果的intersect.object指向batchedMesh本身并附带intersect.batchId字段标识命中了哪个实例。因此点击拾取的用法是raycaster.intersectObject( batchedMesh ); // 命中后 intersect.batchId 即为实例 ID注意这是 CPU 端逐实例检测实例数量极大时可先利用computeBoundingSphere的结果做粗筛。生命周期管理copy 与 disposecopy( source )深拷贝几何体、包围体、几何/实例信息表、空闲 ID 池并克隆矩阵/间接/颜色三张纹理且拷贝其像素数据copy可安全用于场景快照或对象克隆dispose()释放几何体与三张内部纹理的 GPU 资源。文档明确要求当该实例不再被应用使用时调用此方法。若几何体与外部Mesh共享需自行确认geometry.dispose()的副作用。实战注意事项清单结合源码与文档以下是使用BatchedMesh时的关键约束与最佳实践容量按峰值规划构造时按最大实例数、最大唯一几何体集合的顶点/索引总量预留运行中不够用再setInstanceCount/setGeometrySize扩容会重建纹理并拷贝数据有额外开销几何体属性要统一首批次加入的几何体定义了整批的属性布局后续几何体必须提供相同的属性集合与itemSize索引有无必须全批一致预留空间是换装的前提计划运行时用setGeometryAt换更大几何体如骨骼动画的顶点膨胀、LOD 高模替换时addGeometry阶段就必须传入足够的reservedVertexCount/reservedIndexCount不要使用负缩放setMatrixAt的文档明确声明不支持负缩放矩阵删除后记得重排deleteGeometry只回收 ID 不回收缓冲区空间unusedVertexCount统计的是尾部连续空闲量长时间运行的场景建议周期性optimize()静态场景关闭裁剪与排序perObjectFrustumCulled false; sortObjects false;且可见性不再变化时onBeforeRender直接短路零遍历开销透明批次注意排序方向transparent材质自动改为远到近渲染如需特殊混合策略可用setCustomSort接管类型检测用object.isBatchedMesh做instanceof之外的轻量类型判断与 InstancedMesh 的选型几何体单一、实例海量且需要 GPU 端实例属性动画时优先InstancedMesh几何体种类多每种几何体 2~N 份实例、需要逐实例颜色/可见性/射线batchId拾取时优先BatchedMesh完整示例参考examples/webgl_mesh_batch.html 演示了带混合几何体与动态增删实例的完整场景examples/webgpu_mesh_batch.html 展示 WebGPU 后端下的等价行为另有 examples/webgl_batch_lod_bvh.html 展示与 LOD、BVH 加速结构配合的用法。参考API 文档源文件docs/pages/BatchedMesh.html.md核心实现src/objects/BatchedMesh.js单元测试test/unit/src/objects/BatchedMesh.tests.js运行示例examples/webgl_mesh_batch.html、examples/webgpu_mesh_batch.html【免费下载链接】three.jsJavaScript 3D Library.项目地址: https://gitcode.com/GitHub_Trending/th/three.js创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表