WebGPU Shader Constants(管线可覆盖常量)
原文作者:webgpufundamentals Contributors
原文链接:https://webgpufundamentals.org/webgpu/lessons/webgpu-constants.html
本文是 WebGPU 系列教程之一,讨论向着色器提供数据的各种方式。本课聚焦于 pipeline-overridable constants——一种在着色器中声明,但可在创建渲染管线时覆盖的常量。
什么是管线可覆盖常量?
在 WGSL 中,你可以使用 override 关键字声明一个常量,并在创建管线时指定新值。一个最简单的例子:
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);
}
配合基础教程中的顶点着色器:
@vertex fn vs(
@builtin(vertex_index) vertexIndex : u32
) -> @builtin(position) vec4f {
let pos = array(
vec2f( 0.0, 0.5), // top center
vec2f(-0.5, -0.5), // bottom left
vec2f( 0.5, -0.5) // bottom right
);
return vec4f(pos[vertexIndex], 0.0, 1.0);
}
如果直接使用这个着色器,会得到一个黑色三角形。但我们可以在创建管线时覆盖这些常量:
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,
},
},
});
这样就会得到粉紫色。管线可覆盖常量只能是标量:布尔值、整数或浮点数,不能是向量或矩阵。
如果在着色器中没有给常量默认值,则必须在管线中提供。也可以使用 @id 指定数字 ID:
override red: f32; // 必须在管线中指定
@id(123) override green = 0.0; // 可以通过名称 "green" 或 ID 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 实现可以在首次使用某组常量创建管线后缓存部分信息,后续使用不同常量创建时能减少重复工作。无论如何,这是一种将少量数据传入着色器的便捷方式。
入口点独立求值
一个容易忽略的重点:每个 entry point(入口点)会被独立评估。可以理解成,传给 createShaderModule 的代码会被“剥离”,只保留与当前入口点相关的部分;管线覆盖常量先应用,之后才为该入口点生成着色器。
因此,同一个 WGSL 模块里的顶点阶段和片段阶段可以各自使用不同的常量值。我们扩展示例,让两个阶段都使用 red/green/blue,并让顶点阶段把颜色通过 inter-stage 变量传给片段阶段,每 50 像素切换一种颜色:
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), // top center
vec2f(-0.5, -0.5), // bottom left
vec2f( 0.5, -0.5) // bottom right
);
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(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 模块拆分成两个独立的着色器模块:
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), // top center
vec2f(-0.5, -0.5), // bottom left
vec2f( 0.5, -0.5) // bottom right
);
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,
},
},
});
两者的行为完全一致。共享一个着色器模块只是省去了重复维护结构体声明等代码。
注意:实际开发中很少使用管线可覆盖常量来传入颜色。这里选择颜色作为例子,只是因为结果直观、便于理解。
3 个核心优化要点
-
避免重复编译着色器模块
管线可覆盖常量允许在createRenderPipeline阶段修改常量值,而无需重新创建GPUShaderModule。对于需要频繁调整少量参数的场景,可能减少着色器模块的重新编译开销。虽然创建管线本身仍有成本,但 WebGPU 实现可以复用之前创建管线时的部分缓存信息。 -
利用入口点独立求值,共享模块并隔离常量
同一个 WGSL 模块中的顶点和片段入口点可以分别应用不同的常量覆盖值,效果等同于为每个入口点单独创建着色器模块。这种设计既能共用结构体、函数等共享代码,又能在不同渲染阶段使用不同配置,避免冗余的代码维护。 -
使用
@id提升常量的可编程管理性
为可覆盖常量指定显式数字 ID(@id(123))后,可以在管线中通过 ID 而非名称来覆盖常量。这便于在程序化生成着色器或动态调整管线参数时,减少因名称变化导致的兼容性问题,也方便集中管理常量映射。
GLSL / WebGL 专业术语检查备注
- 本文使用的是 WGSL(WebGPU Shading Language),不是 GLSL。WebGPU 使用 WGSL 编写着色器,WebGL 使用 GLSL,两者语法和关键字不同。
- 常见术语对照:
pipeline-overridable constants:管线可覆盖常量render pipeline:渲染管线entry point:入口点shader module:着色器模块stage(vertex / fragment):阶段(顶点 / 片段)inter-stage variables:阶段间变量builtin(position):内置变量(位置)location:位置(用于绑定输入/输出)override:覆盖(WGSL 关键字,对应概念类似 GLSL 中的const替换,但语义不同)
- WGSL 中的
@builtin(position)、@location(0)、@id(123)是 attribute 语法,不要与 GLSL 的layout(location=0)混淆。
