Miscellaneous Shader Input(杂项着色器输入)

原文作者:webgpufundamentals Contributors
原文链接:https://webgpufundamentals.org/webgpu/lessons/webgpu-miscellaneous-input.html


编译说明

本文是 WebGPU Fundamentals 系列中「向着色器传递数据」的其中一篇。前面的文章已经依次介绍了:

阶段间变量(Inter-stage Variables)→ Uniforms → Storage Buffers → Vertex Buffers → Textures → Loading Images → Using Video → Cube Maps → Storage Textures → Multisampling / MSAA → Immediates → Constants,本篇文章为最后一篇:Miscellaneous Shader Input。

本解读聚焦于一种不太常见、但非常实用的着色器输入方式:firstInstance。


正文解读:用 firstInstance 给着色器传递数据

在调用 draw 或 drawIndexed 时,我们可以传入一个 firstInstance 值:

pass.draw(numVertices,
          numInstances = 1,
          firstVertex = 0,
          firstInstance = 0); // <== 这里

pass.drawIndexed(indexCount,
                 instanceCount = 1,
                 firstIndex = 0,
                 baseVertex = 0,
                 firstInstance = 0); // <== 这里

这个值在着色器中对应 @builtin(instance_index)。也就是说,即使我们不进行真正的实例化渲染,也可以用它来传递一个索引,从而选择不同的数据。例如,在“第一篇文章”的简单三角形例子上,我们可以做如下修改。

示例 1:用 instanceIndex 选择偏移量

+struct VertexOut {
+  @builtin(position) pos: vec4f,
+  @location(0) @interpolate(flat, either) colorNdx: u32,
+};

@vertex fn vs(
*  @builtin(vertex_index) vertexIndex : u32,
+  @builtin(instance_index) instanceIndex: u32,
) -> VertexOut {
  let pos = array(
    vec2f( 0.0,  0.5),  // top center
    vec2f(-0.5, -0.5),  // bottom left
    vec2f( 0.5, -0.5)   // bottom right
  );
+  let offsets = array(
+    vec2f( 0.0,  0.5),  // top middle
+    vec2f(-0.5, -0.5),  // left bottom
+    vec2f( 0.5, -0.5),  // right bottom
+  );

-  return vec4f(pos[vertexIndex], 0.0, 1.0);
+  return VertexOut(
+    vec4f(pos[vertexIndex] + offsets[instanceIndex], 0.0, 1.0),
+    instanceIndex,
+  );
}

-@fragment fn fs() -> @location(0) vec4f {
-  return vec4f(1, 0, 0, 1);
-}
+@fragment fn fs(in: VertexOut) -> @location(0) vec4f {
+  let colors = array(
+    vec4f(1, 1, 0, 1),  // yellow
+    vec4f(0, 1, 1, 1),  // cyan
+    vec4f(1, 0, 1, 1),  // magenta
+  );
+  return colors[in.colorNdx];
+}

上面的代码使用 instanceIndex 来选择偏移量,并通过一个带有 @interpolate(flat, either) 的阶段间变量,把实例索引传递给片元着色器。
flat 表示“不要插值”;either 表示从三角形的第一个或最后一个顶点取值。由于我们给所有顶点传递的都是同一个值,因此取第一个还是最后一个并不重要。不过 either 在兼容模式(compatibility mode)下也能工作,所以是更好的选择。

最后,只需更新调用 draw 的次数,画出 3 个三角形:

function render() {
  // 获取 canvas 当前纹理并设为渲染目标
  renderPassDescriptor.colorAttachments[0].view =
    context.getCurrentTexture().createView();

  const encoder = device.createCommandEncoder({ label: 'our encoder' });
  const pass = encoder.beginRenderPass(renderPassDescriptor);
  pass.setPipeline(pipeline);
  pass.draw(3);  // 调用顶点着色器 3 次
+  pass.draw(3, 1, 0, 1); // 为 instance_index 传递 1
+  pass.draw(3, 1, 0, 2); // 为 instance_index 传递 2
  pass.end();

  const commandBuffer = encoder.finish();
  device.queue.submit([commandBuffer]);
}

渲染结果就是三个不同颜色的三角形(在线示例参见原文链接)。


示例 2:结合 Storage Buffer 数组

我们不一定要在着色器中硬编码数组,也可以使用 instance_index 从 Storage Buffer 的数组中选取数据:

struct VertexOut {
  @builtin(position) pos: vec4f,
  @location(0) @interpolate(flat, either) instanceIndex: u32,
};

struct PerInstanceInfo {
  matrix: mat4x4f,
  color: vec4f,
};

@group(0) @binding(0) `var<storage, read>` perInst: `array<PerInstanceInfo>`;

@vertex fn vs(
  @location(0) position: vec4f,
  @builtin(instance_index) instanceIndex: u32,
) -> VertexOut {
  let info = perInst[instanceIndex];

  return VertexOut(
    info.matrix * position,
    instanceIndex,
  );
}

@fragment fn fs(in: VertexOut) -> @location(0) vec4f {
  let info = perInst[in.instanceIndex];
  return info.color;
}

何时使用该方法

是否采用这种方式由你自己决定。对于 draw 之间的小幅变化,你也可以使用 immediates。

使用 firstInstance 的优势在于:

  1. 减少 WebGPU API 调用次数;
  2. 不用仅仅为了传递一个数字而创建一个 ArrayBufferView。

本系列文章在生成 mipmap、并使其兼容 compatibility mode 时也会使用该方法。它帮助我们轻松选择 2D 数组纹理或立方体贴图的某一层,而不必额外创建 uniform buffer。

另外注意:vertex_index 也可以通过 draw 调用参数来控制,但由于我们通常需要它来选择顶点,所以较少用它来传递自定义数据。


三个核心优化要点

  1. 使用 firstInstance 传递轻量整数索引,避免额外的 Uniform Buffer / ArrayBufferView
    对于单数字或少量变化,直接传入 draw/drawIndexed 的 firstInstance,在着色器里通过 @builtin(instance_index) 读取。这样可以减少 WebGPU 调用次数,避免为一个小值创建 JS 类型数组视图或绑定 uniform 资源。

  2. 结合 Storage Buffer 数组,用 instanceIndex 直接索引 per-instance 数据
    将多个实例的矩阵、颜色等数据一次性放入 var<storage, read> array<PerInstanceInfo>,然后通过 instanceIndex 在着色器里索引,而不是为每个实例单独设置 uniform 或多次 draw。这样可以批量传参,减少绑定和调用的复杂度。

  3. 使用 @interpolate(flat, either) 传递常量阶段间变量,提升兼容性并避免插值开销
    当从顶点着色器向片元着色器传递不应当插值的值(例如实例索引)时,用 flat 禁用插值;选择 either 可在 compatibility mode 下正常工作。这比默认的插值方式更高效且更可移植。


GLSL/WebGL 专业术语检查

经核查,本文大量使用的是 WebGPU / WGSL 术语,而非 GLSL/WebGL 代码。以下是核心术语及与 GLSL/WebGL 的对应关系:

WebGPU/WGSL 术语说明GLSL/WebGL 类似术语或概念
@builtin(instance_index)当前实例索引gl_InstanceID / gl_InstanceID(WebGL2)
@builtin(vertex_index)当前顶点索引gl_VertexID / gl_VertexIndex
@builtin(position)顶点输出裁剪坐标gl_Position
@interpolate(flat, either)阶段间变量插值修饰符GLSL flat 插值限定符
Storage Buffer可读可写的缓冲区Shader Storage Buffer Object(SSBO)
Uniform Buffer只读统一缓冲区Uniform Buffer Object(UBO)
Vertex Buffer顶点数据缓冲区Vertex Buffer Object(VBO)
Inter-stage Variables顶点到片元之间的传递变量varying / in / out
firstInstancedraw 系列调用参数,指定实例索引起始值baseInstance(部分 GLES/GL 版本)
compatibility modeWebGPU 兼容模式WebGL 模式相关,非 GLSL 原生术语
no interpolation禁用插值flat

结论:本文没有混用 GLSL/WebGL 专业术语;术语使用符合 WebGPU/WGSL 规范。either 是 WGSL 特有的插值采样选项,在 GLSL 中并不存在直接对应。