目录

webgpufundamentals.org

Fix, Fork, Contribute

WebGPU着色器常量

我不确定这个主题是否值得被视为着色器的一种输入。但从某种角度来看,它确实算是,所以我们还是来探讨一下。

常量,或者更正式地称为管线可覆盖常量 (pipeline-overridable constants),是一种你在着色器中声明,但在使用该着色器创建管线时可以更改其值的常量。

一个简单的例子如下:

override red = 0.0;
override green = 0.0;
override blue = 0.0;

@fragment fn fs() -> @location(0) vec4f {
  return vec4f(red, green, blue, 1.0);
}

将此片元着色器(fragment shader)与基础文章中的顶点着色器(vertex shader)结合使用。

@vertex fn vs(
  @builtin(vertex_index) vertexIndex : u32
) -> @builtin(position) vec4f {
  let pos = array(
    vec2f( 0.0,  0.5),  // 顶部中心
    vec2f(-0.5, -0.5),  // 左下
    vec2f( 0.5, -0.5)   // 右下
  );

  return vec4f(pos[vertexIndex], 0.0, 1.0);
}

如果我们直接使用这个着色器,会得到一个黑色的三角形。

但是,我们可以在指定管线(pipeline)时更改这些常量,或者说“覆盖(override)”它们。

  const pipeline = device.createRenderPipeline({
    label: 'our hardcoded triangle pipeline',
    layout: 'auto',
    vertex: {
      module,
    },
    fragment: {
      module,
      targets: [{ format: presentationFormat }],
+      constants: {
+        red: 1,
+        green: 0.5,
+        blue: 1,
+      },
    },
  });

现在我们得到了一个有点粉的颜色。

管线可覆盖常量只能是标量值,即布尔值(true/false)、整数、浮点数。它们不能是向量或矩阵。

如果你没有在着色器中指定常量的值,那么你必须在管线中提供一个。你还可以为它们分配一个数字 ID,然后通过 ID 来引用它们。

例如:

override red: f32;             // 必须在管线中指定
@id(123) override green = 0.0; // 可以通过 'green' 或 123 来指定
override blue = 0.0;

@fragment fn fs() -> @location(0) vec4f {
  return vec4f(red, green, blue, 1.0);
}

你可能会问,这样做有什么意义?我在创建 WGSL 时也能轻而易举地做到这一点。例如:

const red = 0.5;
const blue = 0.7;
const green = 1.0;

const code = `
const red = ${red};
const green = ${green};
const blue = ${blue};

@fragment fn fs() -> @location(0) vec4f {
  return vec4f(red, green, blue, 1.0);
}
`;

或者更直接一点:

const red = 0.5;
const blue = 0.7;
const green = 1.0;

const code = `
@fragment fn fs() -> @location(0) vec4f {
  return vec4f(${red}, ${green}, ${blue}, 1.0);
}
`;

区别在于,管线可覆盖常量可以在着色器模块创建之后再应用,从技术上讲,这比重新创建一个新的着色器模块要快。不过,创建管线本身并不是一个快速的操作,所以目前还不清楚在创建管线的整个过程中,这种方式到底能节省多少时间。但有一种可能性是,WebGPU 的实现可以利用你第一次使用特定常量创建管线时产生的信息,以便下次你使用不同常量创建管线时,能够大幅减少所需工作量。

无论如何,这是向着色器传入少量数据的一种方法。

入口点是独立评估的

同样重要的一点是,请记住入口点是孤立评估的,正如在关于阶段间变量的文章中部分讨论过的那样。

这就好比传递给 createShaderModule 的代码中,所有与当前入口点无关的内容都被剔除了。管线覆盖常量会被先应用,然后才会创建该入口点的着色器。

让我们扩展一下上面的示例。我们将修改着色器,使顶点阶段(vertex stage)和片元阶段(fragment stage)都使用这些常量。我们将把顶点阶段的值传递给片元阶段,然后每隔 50 像素宽的垂直条纹交替使用不同的值进行绘制。

+struct VOut {
+  @builtin(position) pos: vec4f,
+  @location(0) color: vec4f,
+}

@vertex fn vs(
  @builtin(vertex_index) vertexIndex : u32
-) -> @builtin(position) vec4f {
+) -> VOut {
  let pos = array(
    vec2f( 0.0,  0.5),  // 顶部中心
    vec2f(-0.5, -0.5),  // 左下
    vec2f( 0.5, -0.5)   // 右下
  );

-  return vec4f(pos[vertexIndex], 0.0, 1.0);
+  return VOut(
+    vec4f(pos[vertexIndex], 0.0, 1.0),
+    vec4f(red, green, blue, 1),
+  );
}

override red = 0.0;
override green = 0.0;
override blue = 0.0;

-@fragment fn fs() -> @location(0) vec4f {
-  return vec4f(red, green, blue, 1.0);
+@fragment fn fs(v: VOut) -> @location(0) vec4f {
+  let colorFromVertexShader = v.color;
+  let colorFromFragmentShader = vec4f(red, green, blue, 1.0);
+  // 每隔 50 个像素在两种颜色之间切换
+  return select(
+    colorFromVertexShader,
+    colorFromFragmentShader,
+    v.pos.x % 100.0 > 50.0);
}

现在我们将向每个入口点传入不同的常量。

  const pipeline = device.createRenderPipeline({
    label: 'our hardcoded triangle pipeline',
    layout: 'auto',
    vertex: {
      module,
+      constants: {
+        red: 1,
+        green: 1,
+        blue: 0,
+      },
    },
    fragment: {
      module,
      targets: [{ format: presentationFormat }],
      constants: {
        red: 1,
        green: 0.5,
        blue: 1,
      },
    },
  });

结果显示,每个阶段中的常量值是不同的。

同样,从功能上讲,我们为了方便,只使用了一个包含一段 WGSL code的着色器模块。上面的代码在功能上等同于:

  const vertexModule = device.createShaderModule({
    code: /* wgsl */ `
      struct VOut {
        @builtin(position) pos: vec4f,
        @location(0) color: vec4f,
      }

      @vertex fn vs(
        @builtin(vertex_index) vertexIndex : u32
      ) -> VOut {
        let pos = array(
          vec2f( 0.0,  0.5),  // 顶部中心
          vec2f(-0.5, -0.5),  // 左下
          vec2f( 0.5, -0.5)   // 右下
        );

        return VOut(
          vec4f(pos[vertexIndex], 0.0, 1.0),
          vec4f(red, green, blue, 1),
        );
      }

      override red = 0.0;
      override green = 0.0;
      override blue = 0.0;
    `,
  });

  const fragmentModule = device.createShaderModule({
    code: /* wgsl */ `
      struct VOut {
        @builtin(position) pos: vec4f,
        @location(0) color: vec4f,
      }

      override red = 0.0;
      override green = 0.0;
      override blue = 0.0;

      @fragment fn fs(v: VOut) -> @location(0) vec4f {
        let colorFromVertexShader = v.color;
        let colorFromFragmentShader = vec4f(red, green, blue, 1.0);
        // 每隔 50 个像素在两种颜色之间切换
        return select(
          colorFromVertexShader,
          colorFromFragmentShader,
          v.pos.x % 100.0 > 50.0);
      }
    `,
  });

  const pipeline = device.createRenderPipeline({
    label: 'our hardcoded triangle pipeline',
    layout: 'auto',
    vertex: {
*      module: vertexModule,
      constants: {
        red: 1,
        green: 1,
        blue: 0,
      },
    },
    fragment: {
*      module: fragmentModule,
      targets: [{ format: presentationFormat }],
      constants: {
        red: 1,
        green: 0.5,
        blue: 1,
      },
    },
  });

注意:使用管线可覆盖常量来传递颜色并常见。我们之所以使用颜色,是因为它易于理解且方便展示结果。

有疑问? 在stackoverflow上提问.
Issue/Bug? 在GitHub上提issue.
comments powered by Disqus