多重绘制
多重绘制使引擎能够通过单次 API 调用提交多个子绘制。多个几何体合并到单个 Mesh,通过单个 MeshInstance 渲染,每个子绘制针对合并几何体的不同部分。这降低 CPU 开销,改善大量共享材质和渲染状态的子网格的渲染性能。
用途
多重绘制尤其适用于:
- 地形渲染:将地形划分为块,在一次调用中渲染所有可见块,动态剔除隐藏块。
- 合并几何体:将多个几何体合并为单个网格,以不同实例数渲染不同部分。
- LOD 系统:无需改变绘制调用即可切换子网格。
- 剔除系统:无需修改材质即可隐藏或显示部分几何体。
平台支持
使用 GraphicsDevice.supportsMultiDraw 检查多重绘制支持:
if (app.graphicsDevice.supportsMultiDraw) {
// Use multi-draw
} else {
// Fallback to standard rendering
}
- WebGPU:始终支持,使用间接绘制命令。
- WebGL2:多数设备通过
WEBGL_multi_draw扩展支持。
当 supportsMultiDraw 为 false 时,引擎会使用多重绘制数据,在内部回退到单次绘制调用的循环。虽然无法获得完整的多重绘制性能收益,但材质和渲染状态只配置一次,因此仍显著快于使用独立 MeshInstance 渲染各个几何体。
gl_DrawID 由 WEBGL_multi_draw 扩展本身提供,因此回退路径中不可用。引擎只在扩展存在时定义 CAPS_MULTI_DRAW,所以读取 gl_DrawID 的着色器代码必须以它为条件。
如果子绘制仅在渲染的几何体部分上不同,两种路径行为相同,因为回退会将每部分作为独立绘制提交。如果子绘制需要在着色器中识别自身,则行为不同:材质读取 gl_DrawID 时,应检查 supportsMultiDraw,并在其为 false 时提供其他路径,参见下文跨平台方式。
实时示例
- 多重绘制:带动态块剔除的地形渲染。
- 实例化多重绘制:对不同几何体进行实例化,仅 WebGPU。
- 多平台实例化多重绘制:跨平台的实例化多重绘制。
基础多重绘制
最简单的用法是渲染合并网格中的子网格。
第 1 步:创建合并网格
将多个几何体合并为单个网格,记录每个子网格的索引偏移量和数量:
// Track sub-draw info
const subDraws = [];
let indexOffset = 0;
// Combine geometries
const positions = [];
const indices = [];
let vertexBase = 0;
for (const geometry of geometries) {
// Store sub-draw info
subDraws.push({
firstIndex: indexOffset,
indexCount: geometry.indices.length
});
// Append positions
positions.push(...geometry.positions);
// Append indices with offset
for (const idx of geometry.indices) {
indices.push(vertexBase + idx);
}
vertexBase += geometry.positions.length / 3;
indexOffset += geometry.indices.length;
}
// Create the combined mesh
const mesh = new pc.Mesh(app.graphicsDevice);
mesh.setPositions(positions);
mesh.setIndices(indices);
mesh.update();
第 2 步:配置多重绘制
使用 MeshInstance.setMultiDraw() 分配并填充绘制命令:
const meshInst = new pc.MeshInstance(mesh, material);
// Allocate multi-draw with max number of sub-draws
const cmd = meshInst.setMultiDraw(null, subDraws.length);
// Add each sub-draw
for (let i = 0; i < subDraws.length; i++) {
const sub = subDraws[i];
// add(index, indexCount, instanceCount, firstIndex, baseVertex, firstInstance)
cmd.add(i, sub.indexCount, 1, sub.firstIndex, 0, 0);
}
// Finalize with the number of active draws
cmd.update(subDraws.length);
第 3 步:动态更新
可以逐帧更新绘制命令,显示或隐藏子绘制:
app.on('update', (dt) => {
let activeCount = 0;
for (let i = 0; i < subDraws.length; i++) {
// Check if this sub-draw should be visible (e.g., frustum culling)
if (isVisible(subDraws[i])) {
cmd.add(activeCount, subDraws[i].indexCount, 1, subDraws[i].firstIndex, 0, 0);
activeCount++;
}
}
// Update with actual number of visible draws
cmd.update(activeCount);
});
结合实例化的多重绘制
多重绘制可与硬件实例化结合,在一次调用中以不同实例数渲染不同几何体。
WebGPU 方式
在 WebGPU 中,可使用 firstInstance 参数指定各子绘制从何处读取实例数据:
// Three geometries with different instance counts
const instanceCounts = [8, 15, 25];
const totalInstances = instanceCounts.reduce((a, b) => a + b, 0);
// Create instance data for all instances
const matrices = new Float32Array(totalInstances * 16);
// ... populate matrices ...
// Create instancing vertex buffer
const vbFormat = pc.VertexFormat.getDefaultInstancingFormat(app.graphicsDevice);
const vb = new pc.VertexBuffer(app.graphicsDevice, vbFormat, totalInstances, {
data: matrices
});
meshInst.setInstancing(vb);
// Set up multi-draw with firstInstance offsets
const firstInstance = [0, instanceCounts[0], instanceCounts[0] + instanceCounts[1]];
const cmd = meshInst.setMultiDraw(null, 3);
cmd.add(0, indexCounts[0], instanceCounts[0], firstIndex[0], 0, firstInstance[0]);
cmd.add(1, indexCounts[1], instanceCounts[1], firstIndex[1], 0, firstInstance[1]);
cmd.add(2, indexCounts[2], instanceCounts[2], firstIndex[2], 0, firstInstance[2]);
cmd.update(3);
跨平台方式
WebGL2 不支持 firstInstance。要在两个平台上实现相同结果,可将实例数据存入纹理,并在自定义着色器代码块中使用 gl_DrawID:
if (app.graphicsDevice.isWebGL2 && app.graphicsDevice.supportsMultiDraw) {
// Store matrices in a texture
const matricesTexture = new pc.Texture(app.graphicsDevice, {
width: totalInstances * 4, // 4 vec4s per matrix
height: 1,
format: pc.PIXELFORMAT_RGBA32F,
minFilter: pc.FILTER_NEAREST,
magFilter: pc.FILTER_NEAREST,
mipmaps: false,
levels: [matrices]
});
// Use custom vertex format with instance ID
const vbFormat = new pc.VertexFormat(app.graphicsDevice, [{
semantic: pc.SEMANTIC_ATTR11,
components: 1,
type: pc.TYPE_INT32,
asInt: true
}]);
// Override the instancing shader chunk
material.setAttribute('aInstanceId', pc.SEMANTIC_ATTR11);
material.setParameter('uDrawOffsets[0]', drawOffsets);
material.setParameter('uInstanceMatrices', matricesTexture);
material.shaderChunks.glsl.set('transformInstancingVS', customShaderCode);
}
自定义着色器使用 gl_DrawID 确定基础偏移量,再从纹理读取矩阵:
#ifdef CAPS_MULTI_DRAW
attribute int aInstanceId;
uniform float uDrawOffsets[10];
uniform sampler2D uInstanceMatrices;
mat4 getInstancedMatrix(int index) {
int size = textureSize(uInstanceMatrices, 0).x;
int j = index * 4;
int x = j % size;
int y = j / size;
vec4 v1 = texelFetch(uInstanceMatrices, ivec2(x, y), 0);
vec4 v2 = texelFetch(uInstanceMatrices, ivec2(x + 1, y), 0);
vec4 v3 = texelFetch(uInstanceMatrices, ivec2(x + 2, y), 0);
vec4 v4 = texelFetch(uInstanceMatrices, ivec2(x + 3, y), 0);
return mat4(v1, v2, v3, v4);
}
mat4 getModelMatrix() {
int drawOffset = int(uDrawOffsets[gl_DrawID]);
int instanceIndex = drawOffset + aInstanceId;
return matrix_model * getInstancedMatrix(instanceIndex);
}
#endif
DrawCommands API 参考
MeshInstance.setMultiDraw() 返回 DrawCommands 类:
setMultiDraw
const cmd = meshInstance.setMultiDraw(camera, maxCount);
camera:绑定命令的 CameraComponent,null表示所有相机。maxCount:分配的最大子绘制数量。传入0禁用多重绘制。- 返回:
DrawCommands实例。
add
cmd.add(i, indexOrVertexCount, instanceCount, firstIndexOrVertex, baseVertex, firstInstance);
i:绘制索引,范围为 0 到 maxCount-1。indexOrVertexCount:要绘制的索引或顶点数量。instanceCount:实例数量,非实例化时使用 1。firstIndexOrVertex:起始索引或顶点偏移。baseVertex:基础顶点偏移,仅 WebGPU,默认为 0。firstInstance:首个实例偏移,仅 WebGPU,默认为 0。
update
cmd.update(count);
count:实际执行的绘制数量,可以小于 maxCount。
所有 add() 调用完成后,调用 update() 完成绘制命令配置。