WGSL 入门:着色器语言的类型系统与语法

英文原作者:webgpufundamentals Contributors
原始链接:https://webgpufundamentals.org/webgpu/lessons/webgpu-wgsl.html

本文并非逐字翻译,而是基于原文的深度编译与解读。文中保留了核心 WGSL 代码示例(未翻译,以保真),并对关键技术点进行中文阐述。同时提炼出 3 个与性能优化相关的核心要点,并检查 GLSL/WebGL 专业术语的对应关系。


核心优化要点提炼

  1. 利用强类型与编译期常量优化
    WGSL 的强类型系统和 const(编译期常量)允许编译器在着色器编译阶段完成大量求值。例如 const PI = radians(180.0); 会在编译期计算出结果,避免运行时计算。合理使用 const 和抽象类型(AbstractInt/AbstractFloat)可减少 GPU 指令开销。

  2. 善用向量与矩阵运算的硬件加速
    WGSL 原生支持 vec2/vec3/vec4 及 mat 类型,并且向量/矩阵的加减乘除和内置函数(如 mix)被映射到 GPU 的 SIMD 指令。在着色器中尽量使用向量化运算,避免标量循环,可以显著提升并行执行效率。

  3. 入口点裁剪与绑定资源控制
    WGSL 编译器会自动剔除未被入口点直接或间接访问的绑定资源(uniform/storage 等)。如果因调试或兼容性需要强制包含某个资源,可使用伪赋值 _ = uni;。这有助于减小管线布局体积,降低驱动开销,同时避免未使用的绑定导致校验错误。


正文深度解读

WGSL 是强类型语言

与 JavaScript 不同,WGSL 要求每个变量、结构体字段、函数参数和返回值都明确类型。如果你用过 TypeScript、Rust、C++、C#、Java 等,不会感到陌生。

基础类型:

  • i32 32位有符号整数
  • u32 32位无符号整数
  • f32 32位浮点数
  • bool 布尔值
  • f16 16位浮点数(需要请求可选特性)

变量声明对比:

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左到右
二进制或`ab`
短路与a && b左到右
短路或`a

内置函数

参见 WGSL 函数参考。

与其他语言的差异

  1. if、while、switch、break-if 条件不需要括号

    if a < 5 { doTheThing(); }
    
  2. 没有三元运算符,使用 select 替代:

    let a = select(falseExpression, trueExpression, condition);
    
  3. ++ 和 -- 是语句,不是表达式,且没有前置/后置区别。

    var a = 5;
    a++;          // 现在为 6
    ++a;          // 错误:没有前置递增
    let b = a++;  // 错误:a++ 不是表达式,是语句
    
  4. +=、-= 等是赋值语句,不是表达式。

    let a = 5;
    a += 2;           // a 为 7
    let b = a += 2;   // 错误:a += 2 不是表达式
    
  5. Swizzle 不能出现在赋值左侧。

    var color = vec4f(0.25, 0.5, 0.75, 1);
    color.rgb = color.bgr; // 错误
    color = vec4(color.bgr, color.a);  // 正确
    

    (注意:有提案计划添加此功能。)

  6. 伪赋值 _:将值赋给 _ 表示“假装使用但不实际使用”,常用于强制包含绑定资源。

    @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 的差异,并提炼出可应用于实际项目中的优化要点。