WGSL 入门:着色器语言的类型系统与语法
英文原作者:webgpufundamentals Contributors
原始链接:https://webgpufundamentals.org/webgpu/lessons/webgpu-wgsl.html
本文并非逐字翻译,而是基于原文的深度编译与解读。文中保留了核心 WGSL 代码示例(未翻译,以保真),并对关键技术点进行中文阐述。同时提炼出 3 个与性能优化相关的核心要点,并检查 GLSL/WebGL 专业术语的对应关系。
核心优化要点提炼
-
利用强类型与编译期常量优化
WGSL 的强类型系统和const(编译期常量)允许编译器在着色器编译阶段完成大量求值。例如const PI = radians(180.0);会在编译期计算出结果,避免运行时计算。合理使用const和抽象类型(AbstractInt/AbstractFloat)可减少 GPU 指令开销。 -
善用向量与矩阵运算的硬件加速
WGSL 原生支持vec2/vec3/vec4及mat类型,并且向量/矩阵的加减乘除和内置函数(如mix)被映射到 GPU 的 SIMD 指令。在着色器中尽量使用向量化运算,避免标量循环,可以显著提升并行执行效率。 -
入口点裁剪与绑定资源控制
WGSL 编译器会自动剔除未被入口点直接或间接访问的绑定资源(uniform/storage 等)。如果因调试或兼容性需要强制包含某个资源,可使用伪赋值_ = uni;。这有助于减小管线布局体积,降低驱动开销,同时避免未使用的绑定导致校验错误。
正文深度解读
WGSL 是强类型语言
与 JavaScript 不同,WGSL 要求每个变量、结构体字段、函数参数和返回值都明确类型。如果你用过 TypeScript、Rust、C++、C#、Java 等,不会感到陌生。
基础类型:
i3232位有符号整数u3232位无符号整数f3232位浮点数bool布尔值f1616位浮点数(需要请求可选特性)
变量声明对比:
JavaScript:
var a = 1;
let c = 3;
function d(e) { return e * 2; }
WGSL:
var a: f32 = 1;
let c: f32 = 3;
fn d(e: f32) -> f32 { return e * 2; }
注意 WGSL 中变量声明需要 : <type>,函数返回值需要 -> <type>。
自动类型推断:类似 TypeScript,若不显式声明类型,则根据右侧表达式自动推断。
fn foo() -> bool { return false; }
var a = 1; // i32
let b = 2.0; // f32
var c = 3u; // u32
var d = foo(); // bool
类型转换:强类型意味着经常需要转换类型。
let a = 1; // i32
let b = 2.0; // f32
let c = a + b; // 错误:不能将 i32 与 f32 相加
修正:
let c = f32(a) + b; // 正确
但 WGSL 引入了“抽象整数”(AbstractInt)和“抽象浮点数”(AbstractFloat),它们是编译期未决定具体类型的数字。
let a = 1; // i32
let b = 2.0; // f32
let c = a + b; // 错误:i32 + f32
let d = 1 + 2.0; // d 是 f32,因为抽象数字自动匹配
数值后缀:
2i // i32
3u // u32
4f // f32
4.5f // f32
5h // f16
5.6h // f16
6 // AbstractInt
7.0 // AbstractFloat
深度解读:抽象类型是 WGSL 相比 GLSL 的一大改进,可以减少显式类型转换的繁琐,同时保持类型安全。
let var const 与 JavaScript 的差异
在 JavaScript 中:var 函数作用域,let 块作用域,const 常量(不可变)。
在 WGSL 中:所有变量都是块作用域。var 是可变的存储变量;let 是常量值(不可变);const 是编译期常量,不能用于运行时表达式。
fn foo() {
let a = 1;
a = a + 1; // 错误:a 是常量表达式
var b = 2;
b = b + 1; // 正确
}
const one = 1; // 正确
const two = one * 2; // 正确
const PI = radians(180.0); // 正确
fn add(a: f32, b: f32) -> f32 {
const result = a + b; // 错误!const 只能用于编译期表达式
return result;
}
术语对照:WGSL 的
var类似 GLSL 中的普通局部变量;let类似const(但作用域不同);const更像 GLSL 中的#define或编译期常量。
向量类型
WGSL 有三种向量类型:vec2、vec3、vec4。写法为 vec?<type>,例如 vec2<i32>、vec3<f32>、vec4<u32>、vec3<bool>。
示例:
let a = vec2<i32>(1, -2);
let b = vec3<f32>(3.4, 5.6, 7.8);
let c = vec4<u32>(9, 10, 11, 12);
访问器(Accessors):可通过 x,y,z,w、r,g,b,a 或数组下标访问。
let a = vec4<f32>(1, 2, 3, 4);
let b = a.z;
let c = a.b;
let d = a[2];
// b、c、d 都是 3
Swizzle:可访问多个分量并重组。
let a = vec4<f32>(1, 2, 3, 4);
let b = a.zx; // vec2(3,1)
let c = a.br; // vec2(3,1)
let d = vec2<f32>(a[2], a[0]);
可重复分量:
let a = vec4<f32>(1, 2, 3, 4);
let b = vec3<f32>(a.z, a.z, a.y); // 3,3,2
let c = a.zzy; // 3,3,2
向量快捷写法:省略 <type> 中的 i32/f32/u32/f16,写成 i/f/u/h 后缀。
let a = vec4<f32>(1, 2, 3, 4);
let b = vec4f(1, 2, 3, 4); // 等价
向量构造:可用较小向量组合成更大向量。
let a = vec4f(1, 2, 3, 4);
let b = vec2f(2, 3);
let c = vec4f(1, b, 4); // (1,2,3,4)
let d = vec4f(1, a.yz, 4); // (1,2,3,4)
let e = vec4f(a.xyz, 4); // (1,2,3,4)
let f = vec4f(1, a.yzw); // (1,2,3,4)
向量运算:逐分量执行。
let a = vec4f(1, 2, 3, 4);
let b = vec4f(5, 6, 7, 8);
let c = a + b; // vec4f(6, 8, 10, 12)
let d = a * b; // vec4f(5, 12, 21, 32)
let e = a - b; // vec4f(-4, -4, -4, -4)
内置函数也支持向量:
let a = vec4f(1, 2, 3, 4);
let b = vec4f(5, 6, 7, 8);
let c = mix(a, b, 0.5); // vec4f(3, 4, 5, 6)
let d = mix(a, b, vec4f(0, 0.5, 0.5, 1)); // vec4f(1, 4, 5, 8)
术语检查:Swizzle 在 GLSL 中同样存在,语法一致;
mix对应 GLSL 的mix函数;但在 WebGL/GLSL 中向量类型写作vec4,而 WGSL 中更常见vec4f写法。
矩阵
矩阵是向量的数组,格式为 mat<向量数>x<向量尺寸><类型>。例如 mat3x4<f32> 是 3 个 vec4<f32> 组成的数组。与向量一样有快捷写法:
let a: mat4x4<f32> = ...
let b: mat4x4f = ...
矩阵向量访问:用数组下标获取某一列向量。
let a = mat4x4f(...);
let b = a[2]; // b 是第3个列向量 vec4f
最常见的 3D 计算中,mat4x4f 与 vec4f 直接相乘得到新 vec4f。
let a = mat4x4f(....);
let b = vec4f(1, 2, 3, 4);
let c = a * b; // c 是 vec4f,结果
数组
声明固定大小数组使用 array<type, numElements>。
let a = array<f32, 5>; // 5 个 f32
let b = array<vec4f, 6>; // 6 个 vec4f
也有 array 构造函数,参数必须同类型:
let arrOf3Vec3fsA = array(vec3f(1,2,3), vec3f(4,5,6), vec3f(7,8,9));
let arrOf3Vec3fsB = array<vec3f, 3>(vec3f(1,2,3), vec3f(4,5,6), vec3f(7,8,9));
WGSL 1.0 无法获取固定数组的长度。
运行时大小数组:仅允许用于根作用域的存储声明或根作用域结构体的最后一个字段。
struct Stuff {
color: vec4f,
size: f32,
verts: array<vec3f>,
};
@group(0) @binding(0) var<storage> foo: array<mat4x4f>;
@group(0) @binding(1) var<storage> bar: Stuff;
运行时数组的元素数量由绑定组设置决定,WGSL 中可用 arrayLength 查询。
@group(0) @binding(0) var<storage> foo: array<mat4x4f>;
@group(0) @binding(1) var<storage> bar: Stuff;
...
let numMatrices = arrayLength(&foo);
let numVerts = arrayLength(&bar.verts);
函数
WGSL 函数格式:fn name(参数) -> 返回类型 { ... }
fn add(a: f32, b: f32) -> f32 {
return a + b;
}
入口点
入口点由 @vertex、@fragment 或 @compute 标记。
@vertex fn myFunc(a: f32, b: f32) -> @builtin(position): vec4f {
return vec4f(0, 0, 0, 0);
}
着色器只使用入口点访问的资源
@group(0) @binding(0) var<uniforms> uni: vec4f;
vec4f fn foo() {
return uni;
}
@vertex fn vs1(): @builtin(position) vec4f {
return vec4f(0);
}
@vertex fn vs2(): @builtin(position) vec4f {
return foo();
}
vs1 未引用 uni,因此若管线只使用 vs1,则 uni 不会成为必需绑定。vs2 通过 foo() 间接引用 uni,所以会被要求绑定。
优化解读:编译器自动裁剪未使用的资源,这有助于减少绑定布局的体积,提升管线创建效率。
属性(Attributes)
“属性”一词在 WebGPU 中有两个含义:一是顶点属性(见顶点缓冲区文章),二是 WGSL 中 @ 开头的标记。
@location(number)
用于定义着色器的输入输出位置。
顶点着色器输入:
@vertex vs1(@location(0) foo: f32, @location(1) bar: vec4f) ...
struct Stuff {
@location(0) foo: f32,
@location(1) bar: vec4f,
};
@vertex vs2(s: Stuff) ...
阶段间变量(inter-stage variables):
struct VSOut {
@builtin(position) pos: vec4f,
@location(0) color: vec4f,
@location(1) texcoords: vec2f,
};
struct FSIn {
@location(1) uv: vec2f,
@location(0) diffuse: vec4f,
};
@vertex fn foo(...) -> VSOut { ... }
@fragment fn bar(moo: FSIn) ...
顶点着色器输出 color 到 location 0,texcoords 到 location 1;片段着色器按 location 匹配接收为 uv 和 diffuse(名称可以不同,位置必须一致)。
片段着色器输出:@location 指定写入哪个 GPURenderPassDescriptor.colorAttachment。
struct FSOut {
@location(0) albedo: vec4f;
@location(1) normal: vec4f;
}
@fragment fn bar(...) -> FSOut { ... }
@builtin(name)
用于指定变量值来自 WebGPU 内建功能。
@vertex fn vs1(@builtin(vertex_index) foo: u32, @builtin(instance_index) bar: u32) ...
也可以放在结构体中:
struct Foo {
@builtin(vertex_index) vNdx: u32,
@builtin(instance_index) iNdx: u32,
}
@vertex fn vs1(blap: Foo) ...
内建变量表(已编译为中文):
计算着色器内建变量
| 内建名称 | IO | 类型 | 描述 |
|---|---|---|---|
local_invocation_id | 输入 | vec3<u32> | 当前调用在工作组网格内的局部调用 ID |
local_invocation_index | 输入 | u32 | 当前调用的线性化局部调用索引 |
global_invocation_id | 输入 | vec3<u32> | 当前调用在计算着色器网格中的全局调用 ID |
workgroup_id | 输入 | vec3<u32> | 当前工作组在计算着色器网格中的位置 |
num_workgroups | 输入 | vec3<u32> | 调度尺寸 (group_count_x, group_count_y, group_count_z) |
片段着色器内建变量
| 内建名称 | IO | 类型 | 描述 |
|---|---|---|---|
position | 输入 | vec4<f32> | 当前片段在帧缓冲空间中的位置(x、y、z 已除以 w) |
front_facing | 输入 | bool | 当前片段是否位于正面 |
frag_depth | 输出 | f32 | 片段的更新深度值 |
sample_index | 输入 | u32 | 当前片段的采样索引(0 到 sampleCount-1) |
sample_mask | 输入/输出 | u32 | 采样覆盖掩码,可输入获取或输出控制 |
顶点着色器内建变量
| 内建名称 | IO | 类型 | 描述 |
|---|---|---|---|
vertex_index | 输入 | u32 | 当前顶点在当前绘制命令中的索引 |
instance_index | 输入 | u32 | 当前实例索引 |
position | 输出 | vec4<f32> | 输出顶点位置(齐次坐标,NDC 空间) |
重要说明:
position内建变量在顶点着色器和片段着色器中是两个独立的概念:顶点着色器输出position,片段着色器输入position。它们可以出现在同一个结构体中,但含义不同。如下例:
struct VOut {
@builtin(position) p: vec4f;
};
@vertex fn vs() -> VOut {
return VOut(vec4f(0, 0, 0, 1));
}
@fragment fn fs(v: VOut) {
return v.p;
}
从 WGSL 角度看,VOut 被声明了两次:一次用于 vs 输出,一次用于 fs 输入。也可拆分成两个模块分别声明,效果等同。
流程控制
WGSL 支持 for、if、while、loop、break、break if、continue、continuing、discard、switch。
for (var i = 0; i < 10; i++) { ... }
if (i < 5) { ... } else if (i > 7) { ... } else { ... }
var j = 0;
while (j < 5) { j++; }
var k = 0;
loop {
k++;
if (k >= 5) { break; }
}
var k = 0;
loop {
k++;
break if (k >= 5);
}
for (var i = 0; i < 10; ++i) {
if (i % 2 == 1) { continue; }
...
continuing {
// continue 跳到这里
...
}
}
if (v < 0.5) { discard; }
var a : i32;
let x : i32 = generateValue();
switch x {
case 0: { a = 1; }
default { a = 2; }
case 1, 2, { a = 3; }
case 3, { a = 4; }
case 4 { a = 5; }
}
switch 只支持 u32 或 i32,且 case 值必须是常量。
运算符
| 名称 | 运算符 | 结合性 |
|---|---|---|
| 括号 | (...) | |
| 基本 | a(),a[],a.b | 左到右 |
| 一元 | -a,!a,~a,*a,&a | 右到左 |
| 乘法 | a * b,a / b,a % b | 左到右 |
| 加法 | a + b,a - b | 左到右 |
| 移位 | a << b,a >> b | 需要括号 |
| 关系 | <,>,<=,>=,==,!= | 需要括号 |
| 二进制与 | a & b | 左到右 |
| 二进制异或 | a ^ b | 左到右 |
| 二进制或 | `a | b` |
| 短路与 | a && b | 左到右 |
| 短路或 | `a |
内置函数
参见 WGSL 函数参考。
与其他语言的差异
-
if、while、switch、break-if条件不需要括号if a < 5 { doTheThing(); } -
没有三元运算符,使用
select替代:let a = select(falseExpression, trueExpression, condition); -
++和--是语句,不是表达式,且没有前置/后置区别。var a = 5; a++; // 现在为 6 ++a; // 错误:没有前置递增 let b = a++; // 错误:a++ 不是表达式,是语句 -
+=、-=等是赋值语句,不是表达式。let a = 5; a += 2; // a 为 7 let b = a += 2; // 错误:a += 2 不是表达式 -
Swizzle 不能出现在赋值左侧。
var color = vec4f(0.25, 0.5, 0.75, 1); color.rgb = color.bgr; // 错误 color = vec4(color.bgr, color.a); // 正确(注意:有提案计划添加此功能。)
-
伪赋值
_:将值赋给_表示“假装使用但不实际使用”,常用于强制包含绑定资源。@group(0) @binding(0) var<uniforms> uni1: vec4f; @group(0) @binding(1) var<uniforms> uni2: mat4x4f; @vertex fn vs1(): @builtin(position) vec4f { return vec4f(0); } @vertex fn vs2(): @builtin(position) vec4f { _ = uni1; _ = uni2; return vec4f(0); }vs1不会要求绑定uni1和uni2,而vs2会强制要求在管线中提供这两个绑定。
术语检查(GLSL/WebGL 对照)
| WGSL 术语 | 说明 | GLSL/WebGL 对应 |
|---|---|---|
var<uniform> | 只读 uniform 缓冲 | uniform 变量 |
var<storage> | 可读写存储缓冲 | SSBO / buffer 变量 |
@location | 着色器输入/输出位置 | attribute、varying、out 位置 |
@builtin(position) | 顶点输出/片段输入位置 | gl_Position、gl_FragCoord |
@builtin(vertex_index) | 顶点索引 | gl_VertexID |
@builtin(instance_index) | 实例索引 | gl_InstanceID |
@builtin(front_facing) | 正面标志 | gl_FrontFacing |
@builtin(frag_depth) | 深度输出 | gl_FragDepth |
| swizzle | 分量重组 | 语法一致 |
let | 块级不可变量 | 局部 const |
const | 编译期常量 | #define 或编译期常量表达式 |
| 抽象整数/浮点 | 编译期未定类型数字 | GLSL 无对应,类似字面量自动提升 |
本文编译自 webgpufundamentals 的 WGSL 教程,旨在帮助读者快速理解 WGSL 语法及与 GLSL/WebGL 的差异,并提炼出可应用于实际项目中的优化要点。
