跳到主要内容

多重绘制

多重绘制使引擎能够通过单次 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() 完成绘制命令配置。