目录

webgpufundamentals.org

Fix, Fork, Contribute

WebGPU WGSL

对于WGSL的深入概述,请参见 Tour of WGSL。 我们还有 实际的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 varconst 在 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);

选择器(accessors)

您可以使用各种选择器来访问向量内的值。

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, cd都是相同的。它们都在访问 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, cd 都是相同的,它们都是 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;

在上面的例子中,bc 是一样的。它们都是 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);

ab 是相同的类型。

向量构造

可以用更小的类型构建向量。

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, ef 是相同的。

向量数学

您可以在向量上进行数学运算。

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 = ...

ab 是相同的类型。

矩阵向量访问

您可以使用数组语法引用矩阵中的向量。

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));

在上面的例子中,arrOf3Vec3fsAarrOf3Vec3fsB 是相同的类型。

不幸的是,在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;

foobar.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)

属性(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) ...

vs1vs2 定义了在地址0和1的输入,它们需要由顶点着色器提供。

Inter-stage 变量

对于 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) ... 

上面的例子中, 顶点着色器 foocolor 作为 location(0) 上的 vec4ftexcoords 作为 location(1)上的 vec2f。 而片段着色器 bar 将他们以 uvdiffuse 接收,因为它们的位置是匹配的。

片段着色器输出

对于片段着色器,@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_indexbar 的值来自内置的 instance_index.

struct Foo {
  @builtin(vertex_index) vNdx: u32,
  @builtin(instance_index) iNdx: u32,
}
@vertex fn vs1(blap: Foo) ... {
  ...
}

在这个例子中,blap.vNdx 的值来自内置的vertex_indexblap.iNdx的值来自内置的 instance_index.

内部名称 阶段 IO 类型 描述
vertex_index vertex input u32 当前顶点在当前API级绘制命令中的索引, 不依赖于绘制实例化。

对于非索引绘制,第一个顶点的索引等于绘制的firstVertex 参数的值,无论是直接还是间接提供的。 绘制实例中每个额外的顶点,索引递增一。

对于索引绘制,索引等于顶点的索引缓冲条目, 加上绘制的baseVertex 参数的值,无论是直接还是间接提供的。

instance_index vertex input u32 当前顶点在当前API级绘制命令中的实例索引。

第一个实例的索引等于绘制的firstInstance 参数的值, 无论是直接还是间接提供的。 绘制中每个额外的实例,索引递增一。

position vertex output vec4<f32> 当前顶点的输出位置,使用齐次坐标。 齐次归一化(也就是所有的 x, y, 和 z 分量都除以 w 分量)后, 位置处于WebGPU标准化设备坐标空间。参见WebGPU § 3.3 Coordinate Systems
fragment input vec4<f32> 当前片段在帧缓冲(framebuffer) 空间的位置。 (x, yz 分量都已经被缩放过所以 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

  for (var i = 0; i < 10; i++) { ... }

if

    if (i < 5) {
      ...
    } else if (i > 7) {
      ..
    } else {
      ...
    }

while

  var j = 0;
  while (j < 5) {
    ...
    j++;
  }

loop

  var k = 0;
  loop {
    k++;
    if (k >= 5) {
      break;
    }
  }

break

  var k = 0;
  loop {
    k++;
    if (k >= 5) {
      break;
    }
  }

break if

  var k = 0;
  loop {
    k++;
    break if (k >= 5);
  }

continue

  for (var i = 0; i < 10; ++i) {
    if (i % 2 == 1) {
      continue;
    }
    ...
  }

continuing

  for (var i = 0; i < 10; ++i) {
    if (i % 2 == 1) {
      continue;
    }
    ...

    continuing {
      // continue goes here
      ...
    }
  }

discard

   if (v < 0.5) {
     discard;
   }

使用 discard 会退出当前着色器。它只能在片段着色器中使用。

switch

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 仅与 u32i32 类型的变量工作,并且各分支的匹配值必须是常量。

操作符

名称 操作符 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 不是一个表达式

调制(swizzles)不能出现在左边

在某些语言中可以这样做,但在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 则引用了 uni1uni2,所以当使用 vs2 在管线中时,它们都会作为必需的绑定出现。


  1. JavaScript中的变量有基础类型 undefined, null, boolean, number, string, reference-to-object。 新手程序员们可能会因 const o = {name: 'foo'}; o.name = 'bar'; 能够工作而困惑,因为 o 已经被声明为了 const。 事实上 o 确实是常量,它是对一个对象的常量引用。你不能再次设置 o 引用哪个对象,但你可以改变对象本身。 ↩︎

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