WGSL 除了一些常规的表达式外,还有一些自己独特的表达式。
我们先回顾一下之前学习的一个知识点:着色器的4个生命周期
详细的可查阅之前的文章:WGSL基础之着色器的4个生命周期节点
-
着色器模块创建阶段(Shader module creation):
当 WebGPU createShaderModule 方法被调用后发生,此时将开始解析 WGSL 的源码。
-
管线创建阶段(Pipeline creation):
当 WebGPU createComputePipeline 或 createRenderPipeline 方法被调用后发生,他们分别创建 计算管线 和 渲染管线。
特别补充:官方更加推荐使用上述 2 个方法的异步版函数,即 createComputePipelineAsync 和 createRenderPipelineAsync,这样可以避免阻塞。
-
着色器开始执行阶段(Shader execution start):
当 GPUQueue 通过 submit() 将命令缓冲区 数据提交给 GPU ,开始执行管线,并调用相对应的入口函数后发生。
特别补充:在 submit() 之前发生的事情是——渲染通道编码器(GPURenderPassEncoder) 调用 draw() 或 end(),然后 命令编码器(GPUCommandEncoder) 调用了 finish() 方法,得到了命令缓冲区(GPUCommandBuffer)。
-
着色器执行完成阶段(Shader execution end):
当所有着色器中的工作都被完成时发生。
我们在 WGSL 中所使用、编写的各种表达式,都会在上述第 1 个生命周期中被解析。
下面按照 WGSL 官方文档,我们快速得过一遍各种表达式。
千万不要被某些表达式的名字给吓到,实际上这些表达式都很好理解。
创建时表达式
额~,英文是
create-time expressions
,直译是创建时间表达式
,我感觉非常别扭,这里我姑且暂时将它翻译为创建时表达式
名字很绕口,但实际就是指 表达式中那些具体的数字。
可能我理解的不对,但目前我就是这样理解的。
在 WGSL 中定义了 2 个 “抽象数值类型”:
AbstractInt:整数集,即 -2 的 63 次方 到 2 的 63 次方 之间的整数
abstract 是抽象的意思,而 int 是整数的意思
AbstractFloat:有限浮点数,即 IEEE-754 binary64
IEEE-754 是二进制浮点数算术标准,而 binary64 是 64 位浮点数
文字表达式
就是用文字来表达某些标量,例如 true、false、AbstractInt、AbstractFloat、i32、u32、f32、f16
括号表达式
就是在运算中,括号的用法。
我都不知道该说点什么
类型构造函数表达式
在声明向量、矩阵、数组时用到的 套路形式
,例如:vec2<f32>
、mat2x3<i32>
、array<i32,3>
所谓 类型构造函数表达式
是指在上面那些 套路形式中 如何去设置初始值。
例如:vec3<i32>(0,1,0)
可以把上面的 (0,1,0) 看作是 “构造函数(实例对象)初始化时所传的参数”。
假设我并不填写参数,那么 WGSL 会用 “零” 来作为参数的默认值。
不传参数 | 对应的 “零” 值 |
---|---|
bool() | false |
i32() | 0 |
u32() | 0 |
f32 | 0.0 |
f16 | 0.0 |
如果是 向量、数组、矩阵 ,它们的元素为上面表格中的某一种类型,那么也遵循这套原则。
例如:
vec2<f32>()
等同于vec2<f32>(0.0,0.0,0.0)
vec3<i32>()
等同于vec3<0,0,0>
array<bool,2>()
等同于array<bool,2>(false,false)
我个人建议 无论在什么情况下都使用
明确的值
来填充到表达式中,这样有利于代码阅读。
在 WGSL 中没有 JS 中的那种 “隐式转换”,不同类型的值如果需要转换,则必须使用目标类型的构造函数形式来强制转换。
例如:把 某类型的值强制转换为 布尔值 ,其代码形式为 bool(xxx)
。
类型值的类型转化(重新解释)
在 WGSL 中可以通过 bitcast
关键词,按照 bitcast<T>(e):T
这种形式,将某个 表达式类型 转化(解释) 成另外一种类型。
例如:bitcast<vec2<f32>>(e):vec2<i32>
,将 vec2 每个元素的类型由 f32 转化为 i32。
复合值分解表达式
就是使用 xxx.xx
的形式去获取 xxx 中的某些元素(分量)值以及类型。
某个元素的分量有 N 种表达形式:
- a,b,c,d,e...
- r,g,b,a
- x,y,z,w
- IJKL
- 数组索引形式,即
[x]
请注意:上述分量的顺序是固定的,例如 b 是指
rgba
中的第 3 个分量。
假设,我们先定义一个数组:var a: vec3<f32> = <vec3><f32>(1.0, 2.0, 3.0)
此时,我们就可以利用上面 4 种形式去 表达 a 的某些元素的值和类型。
var a: vec3<f32> = <vec3><f32>(1.0, 2.0, 3.0);
var b:f32 = a.y; // b = 2.0
var c:vec2<f32> = a.bb // c = (3.0, 3.0)
var d: vec3<f32> = a.zyx // d = (3.0, 2.0, 1.0)
var e:f32 = a[1] // e = 2.0
对于矩阵、数组、结构对象,他们也有自己对应的分量代表。
套路基本相同,本文就不多讲了,实际遇到代码时看一样就明白了。
逻辑表达式
逻辑表达式几个关键词用法和 JS 中的一模一样,例如:
-
逻辑否定(取反):假设 e:bool,那么
!e
表示 e 的相反布尔值上面的属于 一元逻辑运算符
下面的都属于 二元逻辑运算符
-
短路 或:e1 || e2
-
短路 与:e1 && e2
所谓 短路 是指 根据前面 e1 的值来取决定是否有必要去计算 e2
-
逻辑 或:e1 | e2
-
逻辑 与:e1 & e2
算术表达式
一类是比较基础的数据加减乘除(+
、-
、*
、/
)、取余运算(%
),与 JS 中的一模一样,就不再多说了。
另外一类就是 矩阵 相关的几个计算:矩阵相加、相减、相乘、缩放、转置等。
比较表达式
就是 ==
、!=
、<
、<=
、>
、>=
这几个比较运算符。
请注意,由于 WGSL 本身属于强类型语言,没有自动的隐式转换,所以不会,也不需要出现 JS 中的 ===
。
位表达式
按位运算平时在 JS 中并不常用,不过 WGSL 中的 位表达式用法和一般的程序语言没有什么区别。
~
:|
:&
:^
:<<
:>>
:
这些位运算符的含义自己百度吧。
函数调用表达式
暂时先不讲这个,因为在后面会专门有一篇用来讲解如何声明函数。
实际也没有啥,就是跟 TypeScript 中定义函数的形式相似,即 参数类型、返回值类型等等
变量标识符表达式
我没看懂官方文档关于这个的描述,暂时先忽略。
形式参数表达式
就是参数的类型,例如 a:T
寻址表达式
寻址(address-of) 操作符将一个引用转换为其对应的指针。
r.ref<S,T,A>
转化为 &r.ptr<S,T,A>
间接寻址表达式
间接寻址运算符(indirection) 将指针转化为相应的引用。
和 寻址表达式 刚好是相反的操作
p:ptr<S,T,A>
转化为 *p:ref<S,T,A>
我相信对于绝大多数前端而言,不知道什么是 指针,什么是 引用。
我在查阅了一些 C++ 文章后,简单说一下:
- 所谓 “指针” 是指 指向创建真实值(内存)的“变量”
- 所谓 “引用” 是指对于 指针 的再次声明变量,换句话说 可以把 引用 当做成 指针的别名
以 JS 举个示例:
let num = 2 let mynum = num // num 就是指针(这只是个比喻) // mynum 就是引用 (换句话说:mynum 是指针 num 的别名)
常量标识符表达式
这里的常量实际就是指 let
声明变量时给变量设定的类型。
关于表达式,本文大概过了一遍,如果有不理解的地方,暂时不用去想,等后期 WGSL 代码看的多了,自然就明白了。
下一篇,我们将学习 控制语句,说直白点就是 if、wsitch、while 等。