对于WGSL的深入概述,请参见 Tour of WGSL。 我们还有 实际的WGSL规范 ,尽管它是为 语言律师们 编写的,可能难以理解 😂
本文假设您已经知道如何编程。它可能过于简略,但希望它能帮助您理解和编写WGSL着色程序。
与 JavaScript 不同,WGSL 要求了解每个变量、结构字段、函数参数和函数返回类型的具体类型。如果您使用过TypeScript、Rust、C++、C#、Java、Swift、Kotlin 等,那么您对此应该很熟悉。
WGSL 中包括这些基本类型
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; }
需要注意的是,我们在变量声明中添加了 : <类型>
,例如 : f32
,以及在函数声明中添加了 -> <类型>
。
WGSL为变量提供了一个快捷方式。类似于 TypeScript,如果您没有声明变量的类型,则它将自动成为右侧表达式的类型。
fn foo() -> bool { return false; } var a = 1; // a 是 i32 类型 let b = 2.0; // b 是 f32 类型 var c = 3u; // c 是 u32 类型 var d = foo(); // d 是 bool 类型
此外,严格的类型意味着您经常需要转换类型。
let a = 1; // a 是 i32 类型 let b = 2.0; // b 是 f32 类型 *let c = a + b; // 错误:不能将一个 i32 的值添加到 f32 的值上
为了修复该错误,我们将其中一个变量的类型转换为另一个的类型:
let a = 1; // a 是 i32 类型 let b = 2.0; // b 是 f32 类型 let c = f32(a) + b; // 这样就行了
但是!WGSL有所谓的 “抽象整数(AbstractInt)” 和 “抽象浮点数(AbstractFloat)” 。您可以将它们视为尚未决定其类型的数字。这些都是仅在编译时有效的特性。
let a = 1; // a 是 i32 类型 let b = 2.0; // b 是 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 // 抽象整数 7.0 // 抽象浮点数
let
var
和 const
在 WGSL 和 Javascript 中的含义不同在JavaScript中,var
是一个具有函数作用域的变量。let
是一个具有块作用域的变量。const
是一个常量变量(不能改变)[1],具有块作用域。
在WGSL中,所有变量都具有块作用域。var
是一个具有存储空间的变量,因此是可变的。let
是一个常量值。
fn foo() { let a = 1; * a = a + 1; // 错误:a 是常量表达式 var b = 2; b = b + 1; // 彳亍 }
const
不是一个变量,而是一个编译时常量。您不能将 const
用于运行时。
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有三种向量类型 vec2
, vec3
, 和 vec4
。它们的基本样式是 vec?<type>
例如 vec2<i32>
(两个i32的向量), vec3<f32>
(三个f32的向量), vec4<u32>
(四个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);
您可以使用各种选择器来访问向量内的值。
let a = vec4<f32>(1, 2, 3, 4); let b = a.z; // 通过 x,y,z,w 访问 let c = a.b; // 通过 r,g,b,a 访问 let d = a[2]; // 通过数组元素选择器访问
在上面的例子中,b
, c
和 d
都是相同的。它们都在访问 a 的第三个元素,而它们都是’3’。
您也可以同时访问多个元素。
let a = vec4<f32>(1, 2, 3, 4); let b = a.zx; // 通过 x,y,z,w 访问 let c = a.br; // 通过 r,g,b,a 访问 let d = vec2<f32>(a[2], a[0]);
在上面的例子中,b
, c
和 d
都是相同的,它们都是 vec2<f32>(3, 1)
。
您还可以重复元素。
let a = vec4<f32>(1, 2, 3, 4); let b = vec3<f32>(a.z, a.z, a.y); let c = a.zzy;
在上面的例子中,b
和 c
是一样的。它们都是 vec3<f32>
,其内容是 3, 3, 2。
基本类型有快捷方式。您可以将 <i32>
改为 i
, <f32>
改为 f
, <u32>
改为 u
, <f16>
改为 h
,如:
let a = vec4<f32>(1, 2, 3, 4); let b = vec4f(1, 2, 3, 4);
a
和 b
是相同的类型。
可以用更小的类型构建向量。
let a = vec4f(1, 2, 3, 4); let b = vec2f(2, 3); let c = vec4f(1, b, 4); let d = vec4f(1, a.yz, 4); let e = vec4f(a.xyz, 4); let f = vec4f(1, a.yzw);
a
, c
, d
, e
和 f
是相同的。
您可以在向量上进行数学运算。
let a = vec4f(1, 2, 3, 4); let b = vec4f(5, 6, 7, 8); let c = a + b; // c 是 vec4f(6, 8, 10, 12) let d = a * b; // d 是 vec4f(5, 12, 21, 32) let e = a - b; // e 是 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); // c is vec4f(3, 4, 5, 6) let d = mix(a, b, vec4f(0, 0.5, 0.5, 1)); // d is vec4f(1, 4, 5, 8)
WGSL有许多矩阵类型。矩阵是向量的数组,格式是 mat<向量数量>x<向量大小><类型>
,例如 mat3x4<f32>
是一个包含3个 vec4<f32>
的数组。就像向量一样,矩阵也有相同的快捷方式:
let a: mat4x4<f32> = ... let b: mat4x4f = ...
a
和 b
是相同的类型。
您可以使用数组语法引用矩阵中的向量。
let a = mat4x4f(...); let b = a[2]; // b 是 a 中第3个向量的一个 vec4f
最常用的3D计算矩阵类型是 mat4x4f
,可以直接与vec4f
相乘产生另一个 vec4f
。
let a = mat4x4f(....); let b = vec4f(1, 2, 3, 4); let c = a * b; // c 是一个 vec4f,是 a * b 的结果
WGSL中的数组使用 array<type, numElements>
语法声明。
let a = array<f32, 5>; // 一个包含五个 f32 的数组 let b = array<vec4f, 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));
在上面的例子中,arrOf3Vec3fsA
和 arrOf3Vec3fsB
是相同的类型。
不幸的是,在WGSL版本1中,没有方法获取固定大小数组的大小。
只有根作用域存储声明或作为根作用域结构体最后一个字段的数组才能指定为没有大小。
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;
foo
和 bar.verts
中的元素数量由运行时使用的绑定组设置定义。您可以在 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 函数名(参数) -> 返回类型 { ..函数体... }
的模式。
fn add(a: f32, b: f32) -> f32 { return a + b; }
WGSL 的程序需要一个入口点。入口点由 @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(); }
上面 uni
没有被 vs1
访问,因此如果在管道中使用 vs1
,它不会显示为必需的绑定。vs2
通过调用 foo
间接引用了 uni
,所以在管道中使用 vs2
时,它会显示为必需的绑定。
属性(attributes) 这个词在WebGPU有双重含义,一个是 顶点属性(vertex attributes) 这在顶点缓冲区的文章有过介绍。
另一个是在WGSL中,属性以 @
开头。
@location(number)
@location(number)
用于定义着色器的输入和输出。
对于顶点着色器,输入由顶点着色器入口点函数的 @location
属性定义。
@vertex vs1(@location(0) foo: f32, @location(1) bar: vec4f) ... struct Stuff { @location(0) foo: f32, @location(1) bar: vec4f, }; @vertex vs2(s: Stuff) ...
vs1
和 vs2
定义了在地址0和1的输入,它们需要由顶点着色器提供。
对于 Inter-stage 变量, @location
属性定义了变量在着色器之间传递的位置。
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) ...
上面的例子中, 顶点着色器 foo
将 color
作为 location(0)
上的 vec4f
,texcoords
作为 location(1)
上的 vec2f
。
而片段着色器 bar
将他们以 uv
和 diffuse
接收,因为它们的位置是匹配的。
对于片段着色器,@location
指定了将结果存储在哪个GPURenderPassDescriptor.colorAttachment
中。
struct FSOut { @location(0) albedo: vec4f; @location(1) normal: vec4f; } @fragment fn bar(...) -> FSOut { ... }
@builtin(name)
@builtin
属性用于指定某个特定变量的值来自WebGPU的内置功能。
@vertex fn vs1(@builtin(vertex_index) foo: u32, @builtin(instance_index) bar: u32) ... { ... }
在上面的例子中,foo
的值来自内置的 vertex_index
而 bar
的值来自内置的 instance_index
.
struct Foo { @builtin(vertex_index) vNdx: u32, @builtin(instance_index) iNdx: u32, } @vertex fn vs1(blap: Foo) ... { ... }
在这个例子中,blap.vNdx
的值来自内置的vertex_index
而 blap.iNdx
的值来自内置的 instance_index
.
内部名称 | 阶段 | IO | 类型 | 描述 |
---|---|---|---|---|
vertex_index | vertex | input | u32 |
当前顶点在当前API级绘制命令中的索引, 不依赖于绘制实例化。
对于非索引绘制,第一个顶点的索引等于绘制的 对于索引绘制,索引等于顶点的索引缓冲条目, 加上绘制的 |
instance_index | vertex | input | u32 |
当前顶点在当前API级绘制命令中的实例索引。
第一个实例的索引等于绘制的 |
position | vertex | output | vec4<f32> | 当前顶点的输出位置,使用齐次坐标。 齐次归一化(也就是所有的 x, y, 和 z 分量都除以 w 分量)后, 位置处于WebGPU标准化设备坐标空间。参见WebGPU § 3.3 Coordinate Systems。 |
fragment | input | vec4<f32> | 当前片段在帧缓冲(framebuffer) 空间的位置。 (x, y和z 分量都已经被缩放过所以 w 现在是1。) 参见 WebGPU § 3.3 Coordinate Systems. | |
front_facing | fragment | input | bool | 当当前片段位于面向前方的 图元上时为真,否则为假。 |
frag_depth | fragment | output | f32 | 视口深度范围内的片段更新后的深度。参见WebGPU § 3.3 Coordinate Systems。 |
local_invocation_id | compute | input | vec3<u32> | 当前调用的局部调用ID(local invocation ID), 即其在工作组网格(workgroup grid)中的位置。 |
local_invocation_index | compute | input | u32 | 当前调用的局部调用索引(local invocation index), 即调用在工作组网格(workgroup grid)中的线性索引。 |
global_invocation_id | compute | input | vec3<u32> | 当前调用的全局调用ID(global invocation ID), 也就是它在计算着色器(compute shader grid)中的位置。 |
workgroup_id | compute | input | vec3<u32> | 当前调用的工作组ID(workgroup ID), 也就是该工作组在工作组网格(workgroup grid)中的位置。 |
num_workgroups | compute | input | vec3<u32> | 通过API调度的计算着色器的调度大小(dispatch size),即vec<u32>(group_count_x, group_count_y, group_count_z) |
sample_index | fragment | input | u32 | 当前片段的样本索引。 该值至少为0且至多为sampleCount -1, 其中sampleCount 是为GPU渲染管线指定的MSAA样本数量 参见WebGPU § 10.3 GPURenderPipeline. |
sample_mask | fragment | input | u32 | 当前片段的样本覆盖率掩码。它包含一个位掩码,指示此片段中哪些样本被正在渲染的图元覆盖。 参见WebGPU § 23.3.11 Sample Masking. |
fragment | output | u32 | 控制当前片段的样本覆盖率的掩码。写入此变量的最后一个值成为着色器输出掩码.
写入值中的零位将导致颜色附件(color attachments)中相应的样本被丢弃。 See WebGPU § 23.3.11 Sample Masking. |
像大多数计算机语言一样,WGSL具有流程控制语句。
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++; 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; } ... }
for (var i = 0; i < 10; ++i) { if (i % 2 == 1) { continue; } ... continuing { // continue goes here ... } }
if (v < 0.5) { discard; }
使用 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
类型的变量工作,并且各分支的匹配值必须是常量。
名称 | 操作符 | Associativity | Binding |
---|---|---|---|
Parenthesized | (...) |
||
Primary | a() , a[] , a.b |
Left-to-right | |
Unary | -a , !a , ~a , *a , &a |
Right-to-left | All above |
Multiplicative | a * b , a / b , a % b |
Left-to-right | All above |
Additive | a + b , a - b |
Left-to-right | All above |
Shift | a << b , a >> b |
Requires parentheses | Unary |
Relational | a < b , a > b , a <= b , a >= b , a == b , a != b |
Requires parentheses | All above |
Binary AND | a & b |
Left-to-right | Unary |
Binary XOR | a ^ b |
Left-to-right | Unary |
Binary OR | a | b |
Left-to-right | Unary |
Short-circuit AND | a && b |
Left-to-right | Relational |
Short-circuit OR | a || b |
Left-to-right | Relational |
请见 the WGSL Function reference.
if
, while
, switch
, break-if
表达式不需要括号。if a < 5 { doTheThing(); }
许多语言有一个三元运算符 condition ? trueExpression : falseExpression
但WGSL没有。WGSL有 select
.
let a = select(falseExpression, trueExpression, condition);
++
and --
are statements, not expressions.许多语言有 *前置递增 * 和 后置递增 运算符。
// JavaScript let a = 5; let b = a++; // b = 5, a = 6 (前置递增) let c = ++a; // c = 7, a = 7 (后置递增)
WGSL没有这些。它只有递增和递减语句。
// WGSL var a = 5; a++; // a 现在是 6 *++a; // 错误:没有前置递增这种东西 *let b = a++; // 错误:a++ 不是一个表达式,而是一个语句(译者注:语句(statement)不返回值)
+=
, -=
不是表达式,它们是赋值语句// JavaScript let a = 5; a += 2; // a = 7 let b = a += 2; // a = 9, b = 9
// WGSL let a = 5; a += 2; // a 是 7 *let b = a += 2; // 错误:a += 2 不是一个表达式
在某些语言中可以这样做,但在WGSL中不可以。
var color = vec4f(0.25, 0.5, 0.75, 1); *color.rgb = color.bgr; // 错误 color = vec4(color.bgr, color.a); // 彳亍
Note:有一个提议是增加这个功能。
_
_
是一个特殊的变量,你可以赋值给它,来让某些东西看起来被使用了,但实际上并不使用它。
@group(0) @binding(0) var<uniforms> uni1: vec4f; @group(0) @binding(0) var<uniforms> uni2: mat4x4f; @vertex fn vs1(): @builtin(position) vec4f { return vec4f(0); } @vertex fn vs2(): @builtin(position) vec4f { _ = uni1; _ = uni2; return vec4f(0); }
上面的例子中,无论是 uni1
还是 uni2
都没有被 vs1
访问到,因此如果在管线中使用 vs1
,它们都不会作为必需的绑定出现。而 vs2
则引用了 uni1
和 uni2
,所以当使用 vs2
在管线中时,它们都会作为必需的绑定出现。
Copyright © 2023 World Wide Web Consortium. W3C® liability, trademark and permissive document license rules apply.
JavaScript中的变量有基础类型 undefined
, null
, boolean
, number
, string
, reference-to-object
。
新手程序员们可能会因 const o = {name: 'foo'}; o.name = 'bar';
能够工作而困惑,因为 o
已经被声明为了 const
。
事实上 o
确实是常量,它是对一个对象的常量引用。你不能再次设置 o
引用哪个对象,但你可以改变对象本身。 ↩︎