├── imgs └── memory.jpg ├── README.md ├── 03 WGSL基础之文本(代码)结构规范.md ├── 09 WGSL基础之函数和入口函数.md ├── 02 WGSL基础之着色器的4个生命周期节点.md ├── 08 WGSL基础之控制语句.md ├── 05 WGSL基础之内存.md ├── 06 WGSL基础之变量声明.md ├── 04 WGSL基础之普通类型.md ├── 07 WGSL基础之表达式.md ├── 01 WGSL基础之概念.md └── 10 WGSL基础之内置值和内置函数.md /imgs/memory.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/puxiao/wgsl-tutorial/HEAD/imgs/memory.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wgsl-tutorial 2 | 从今天 2022年05月22日 开始学习和探索 WGSL。 3 | 4 | 5 | 6 |
7 | 8 | 与本教程对应的,是我之前写的 WebGPU 入门系列教程。 9 | 10 | 仓库地址:https://github.com/puxiao/webgpu-tutorial 11 | 12 | 13 | 14 |
15 | 16 | **为什么要学习 WGSL ?** 17 | 18 | 因为 WGSL 是 WebGPU 的着色器语言,想成为 WebGPU 高手 WGSL 必须学。 19 | 20 | > 如果你不会 WGSL,那么对于 WebGPU 中动态更改着色器、资源纹理绑定、更改相机视图等等操作,你会很难以理解。毕竟 WGSL 和 WebGPU 是相辅相成的关系。 21 | 22 | 23 | 24 |
25 | 26 | **关于本教程的几点说明:** 27 | 28 | * 本教程主要讲解 WebGPU 的着色器语言 WGSL 29 | * 本教程实际上是 [webgpu-tutorial](https://github.com/puxiao/webgpu-tutorial) 的一部分,只不过是为了方便归类,所以才单独创建的仓库 30 | * 如果你是刚入门想学习 WebGPU,那么我并不推荐你上来就学习 WGSL,我建议你还是先去看 WebGPU,对 WebGPU 有一定了解后,再开始学习 WGSL 31 | * 我也是刚开始学习 WGSL,本教程实际上是我的学习笔记,若发现我说的不对的地方,还请指正,不胜感激。 32 | 33 | 34 | 35 |
36 | 37 | **WGSL 官方文档:** 38 | 39 | * 英文版:https://www.w3.org/TR/WGSL/ 40 | * 中文翻译版:https://www.orillusion.com/zh/wgsl.html 41 | 42 | 43 | 44 |
45 | 46 | **欢迎与我交流:** 47 | 48 | Github:https://github.com/puxiao 49 | 50 | 邮箱:yangpuxiao@gmail.com 51 | 52 |

53 | wechat.jpg 54 |

55 | 56 | 57 |
58 | 59 | **教程同步:** 60 | 61 | 本教程文章也会同步发布到我申请的个人微信公众号里,公众号名称就叫:WebGPU 62 | 63 | ![](https://raw.githubusercontent.com/puxiao/webgpu-tutorial/main/imgs/me_qrcode02.jpg) 64 | 65 | 66 | 67 |
68 | 69 | **关于版权:** 70 | 71 | 1. 若你发现我引用了你的文章、图片、PPT 或视频截图、或者其他你认侵犯到你权利的地方,请通知我。 72 | 2. 若你想转发我的文章,请先告诉我,我希望至少你能注明文章来源和作者。 73 | 74 | 75 | 76 |
77 | 78 | **前端同仁,加油,一起学习 WGSL !** 79 | 80 | -------------------------------------------------------------------------------- /03 WGSL基础之文本(代码)结构规范.md: -------------------------------------------------------------------------------- 1 | # 03 WGSL基础之文本(代码)结构规范 2 | 3 | **WGSL中代码文本结构规范主要由以下几种组成:编码格式、注释、空格与换行、数字标记、关键字、标识符、属性、指令、声明和作用域。** 4 | 5 | 6 | 7 |
8 | 9 | > 由于 WGLS 还处于草案阶段,所以本文所说的规范仅仅适用于当下,不排除未来有发生变化。 10 | 11 | 12 | 13 |
14 | 15 | **规范1:推荐使用 UTF-8 作为 WGSL 的代码文本编码格式** 16 | 17 | 如同 JS 或 HTML,UTF-8 是作为代码编码格式的首选。 18 | 19 | 当然并不是说不可以使用其他编码格式,只是官方推荐选择使用 UTF-8。 20 | 21 | 22 | 23 |
24 | 25 | **规范2:一些可以接受的空格与换行符** 26 | 27 | 在 WGSL 代码文本中,以下内容都会被视作为符合规范的 空格或换行: 28 | 29 | 1. 空格 30 | 31 | 2. 水平制表符,也就是按 Tab 键 32 | 33 | 3. 垂直制表符 34 | 35 | 4. 换行 36 | 37 | 5. 回车 38 | 39 | 6. 下一行 40 | 41 | 7. 换页 42 | 43 | 8. 行分隔符 44 | 45 | 9. 段落分隔符 46 | 47 | 10. 从左到右的标记 48 | 49 | 11. 从右到左的标记 50 | 51 | 52 | 53 |
54 | 55 | **规范3:注释方式和 JS 完全相同** 56 | 57 | 在 WGSL 中的添加注释方式和在 JS 中添加注释的方式完全相同。 58 | 59 | 1. 行尾注释:在单行代码后面添加 `//xxxx` 形式的注释 60 | 2. 块级注释:使用 `/* xxx */` 这种形式 61 | 62 | 63 | 64 |
65 | 66 | **规范4:数字(数值)的一些特殊标记** 67 | 68 | 在 WGSL 中数值的一些特殊标记含义和用法与 JS 是相同的,例如: 69 | 70 | 1. 使用 `0x` 开头可以表示这是一个 十六进制数字 71 | 72 | > 在有些语言中,若以 `0` 开头的数字则表示为 八进制数字,但是在 WGSL 中并没有这个规范 73 | 74 | 2. 使用 `e` 来表示科学计数法(十进制数字的指数) 75 | 76 | 77 | 78 |
79 | 80 | **WGSL 吸纳了 C 语言中的一些特殊数字标记符号。** 81 | 82 | 在一些数字的后面,可以添加使用以下特殊标记符号: 83 | 84 | 1. u:是单词 unsigned 的简写,表明这是一个无符号的整数 85 | 86 | 2. i:是单词 int 的简写,表明这是一个有符号的整数 87 | 88 | 3. f:是单词 float 的简写,表明这是一个 浮点数 89 | 90 | 4. h:是单词 hex 的简写,表明这是一个 十六进制数 91 | 92 | 5. l(L):是单词 long int 的简写,表明这是一个 长整型 93 | 94 | 6. p:表明这是一个 十六进制数的指数 95 | 96 | > e 是 十进制数的指数 97 | 98 | 99 | 100 |
101 | 102 | 数字的特殊符号还可以结合使用。 103 | 104 | 如果你在 WGSL 代码中看到这样一个数值 `0x1p0f`,不要惊讶,慢慢会习惯的。 105 | 106 | 107 | 108 |
109 | 110 | **强调一下:WGSL 中是不存在隐式转换的** 111 | 112 | 例如数字 1 表示为一个整数,而数字 1.0 则表示为一个浮点数,因此 1 和 1.0 是不相同的两种数值。 113 | 114 | 115 | 116 |
117 | 118 | **规范5:规定的一些关键字** 119 | 120 | 在 WGSL 中的关键字大致可以分为以下 3 种: 121 | 122 | 1. 表明某种类型的关键字,例如 f16、mat2x3、vec2、vec3 等 123 | 2. 一些语法对应的关键字,例如 let、if、for、flase、return 等 124 | 3. 一些保留的关键字,例如 Buffer、Self、as、new、void、yield 等 125 | 126 | 127 | 128 |
129 | 130 | **规范6:标识符、属性、指令** 131 | 132 | 这 3 项本文不做讲解,因为这一块是我们比较靠后时候才需要学习的。 133 | 134 | 135 | 136 |
137 | 138 | **规范7:声明和作用域** 139 | 140 | 声明在 WGSL 中非常重要,主要用来表明: 141 | 142 | 1. 某些变量的类型 143 | 2. 声明定义函数 144 | 3. 明确某些函数的返回值类型 145 | 146 | 147 | 148 |
149 | 150 | > 如果你会 TypeScript 或 Rust,那么对于这种类型声明会比较容易接受 151 | 152 | 153 | 154 |
155 | 156 | 至于声明的变量对应的作用域,这点和 JS 没有什么区别。 157 | 158 | 159 | 160 |
161 | 162 | **规范8:特殊关键词 `@stage`** 163 | 164 | 在 WGSL 代码的开头第一行,可以添加: 165 | 166 | 1. `@stage(vertex)`:表明接下来的代码是应用于 顶点阶段 167 | 2. `@stage(fragment)`:表明接下来的代码是应用于 片元阶段 168 | 169 | 170 | 171 |
172 | 173 | **总结:** 174 | 175 | 实际上本文上面讲解的规范中,只有 数字结尾特殊标记 稍微难以理解(主要是不习惯),其他方面和 JS 或 TypeScript 没有太大的差别。 176 | 177 | 178 | 179 |
180 | 181 | 关于 WGSL 代码文本的一些结构规范就先学习到这里。 182 | 183 | 下一节,我们将过一遍 WGSL 中存在的各种类型。 184 | 185 | -------------------------------------------------------------------------------- /09 WGSL基础之函数和入口函数.md: -------------------------------------------------------------------------------- 1 | # 09 WGSL基础之函数和入口函数 2 | 3 | **在 WGSL 中函数分为内置函数和用户自定义函数,入口函数属于一种特殊的用户自定义函数,入口函数会被着色器调用。** 4 | 5 | 6 | 7 |
8 | 9 | #### 用户自定义函数 10 | 11 | > 用户自定义函数英文是:user-defined function 12 | 13 | 我们先学习一下用户自定义函数相关知识点,和 JS 中的 函数在定义和使用方面还是有一些差异的。 14 | 15 | 16 | 17 |
18 | 19 | **如何定义函数?** 20 | 21 | 在 WGSL 中使用 `fn` 来作为创建函数的关键词 22 | 23 | > 相当于 JS 中的 function 24 | 25 | 26 | 27 |
28 | 29 | 一个典型的函数应该包含以下几部分: 30 | 31 | 1. 函数名 32 | 33 | 2. 函数的参数以及参数类型 34 | 35 | 3. 使用 `->` 来表示此函数有返回值 36 | 37 | > 注意并不是 JS 中的箭头函数 `=>` 38 | 39 | 4. 函数返回值的类型 40 | 41 | 42 | 43 |
44 | 45 | 函数有返回值的简单示例: 46 | 47 | ``` 48 | fn addTwo( a: i32, b:f32) -> i32 { 49 | return a + b; 50 | } 51 | ``` 52 | 53 | 54 | 55 |
56 | 57 | 假设函数没有返回值,那么可以写成: 58 | 59 | ``` 60 | fn myFun() { 61 | ... 62 | } 63 | ``` 64 | 65 | 66 | 67 |
68 | 69 | **停止执行函数:** 70 | 71 | 当执行语句中出现了 `discard` ,则会立即停止函数的调用和执行,并且无法恢复。 72 | 73 | > discard 关键词在上一节 “WGSL基础之控制语句” 中学习过。 74 | > 75 | > discard 只能用于 片元 着色器阶段,表明立即结束当前片元着色器的执行,并丢弃当前的片元数据。 76 | 77 | 78 | 79 |
80 | 81 | **特别注意:在WGSL 中不允许出现 递归函数** 82 | 83 | 也就是说不允许函数之间循环调用。 84 | 85 | 86 | 87 |
88 | 89 | #### 入口函数(entry point) 90 | 91 | 入口函数属于一种特殊的用户自定义函数。 92 | 93 | 入口函数是指在管线的 计算(compute)、顶点(vertex)、片元(fragment) 这 3 个阶段中,WGSL 会去调度执行的函数。 94 | 95 | 96 | 97 |
98 | 99 | **指明入口函数属于哪个阶段的关键词分别是:@vertex、@fragment、@compute** 100 | 101 | 简单示例: 102 | 103 | ``` 104 | @vertex 105 | fn vert_main() -> ... 106 | 107 | @fragment 108 | fn frag_main() -> ... 109 | 110 | @compute 111 | fn comp_main() { ... } 112 | ``` 113 | 114 | > 通常情况下 顶点入口函数会返回和空间顶点坐标相关的值,而片元入口函数则返回和颜色相关的值。 115 | 116 | 117 | 118 |
119 | 120 | **补充说明:已被废弃的声明方式** 121 | 122 | 在早些时候,使用的是 `@stage(vertex)`、`@stage(fragment)`、`@stage(compute)` 这种形式来定义入口函数属于哪个阶段。 123 | 124 | 不过这种方式已被废弃,现在使用刚才提到的 `@vertex`、`@fragment`、`@compute` 。 125 | 126 | > WebGPU 和 WGSL 目前还处于草案当中,很多语法都在不断变化中。 127 | 128 | 129 | 130 |
131 | 132 | 实际上本文到此就应该结束了。但是在入口函数中,还有一个非常重要的概念:插值 133 | 134 | 可是关于插值的相关知识,我也不是特别懂。 135 | 136 | 以下内容为我在查阅一些文章后 有限的理解,请勿完全相信,因为我也是处于学习中,如果讲错了还请理解。 137 | 138 | 139 | 140 |
141 | 142 | #### 插值 143 | 144 | 插值,即如何控制用户定义的输入和输出数据。 145 | 146 | 插值主要分为 2 个方面: 147 | 148 | 1. 插值类型 149 | 2. 插值的采样方式 150 | 151 | 152 | 153 |
154 | 155 | **插值类型** 156 | 157 | 1. perspective(默认):值以透视的方式进行插值计算 158 | 159 | 2. linear:值以线性、非透视的方式进行插值计算 160 | 161 | 3. flat:值不是内插的。 162 | 163 | > 以展开(扁平化) 2D 平面的方式进行插值计算 164 | 165 | 166 | 167 |
168 | 169 | **插值的一些名词:内插、外插、间插** 170 | 171 | > 我也不是很懂,百度了一些文章之后,大概明白了一些 172 | 173 | 1. 图像内插、外插 是 2 种不同的图像插值计算方式,其作用是将 1 张低分辨率的图像转化为 1 张较高分辨率的图像。 174 | 175 | > 也就是说将 1 张小图 拉大变成 1 张大图 176 | 177 | 2. 图像间插:就是将 2 张不同的图像通过计算得到处于它们 2 者中间过渡的那 1 张新的图像。 178 | 179 | > 也就是说计算出 2 张图片中间的 补间 图像 180 | 181 | 182 | 183 |
184 | 185 | 在 CSS 中有一个类似的样式:transform-style 186 | 187 | > 我只是说有些相似,但和本文所学习的并没有任何关联 188 | 189 | 它有 2 个配置值: 190 | 191 | 1. preserve-3d:指示元素的子元素位于该 元素的 3D 空间中 192 | 193 | > 元素的子元素将会有 3D 遮挡关系 194 | 195 | 2. flat(默认值):指示元素的子元素位于该 元素的 2D 平面中 196 | 197 | > 元素的子元素将不会有 3D 遮挡关系 198 | 199 | 200 | 201 |
202 | 203 | **插值的采样方式:** 204 | 205 | > 在有些图形学文章中会将 “插值的采样方式” 称呼为 “插值修饰符” 206 | 207 | 1. center(默认):在像素的中心执行 208 | 209 | 2. centroid:(实在是没有理解官方文档中关于 centroid 的介绍) 210 | 211 | > centroid 单词本意为 “质心” 212 | 213 | 3. sample:对每一个样本执行内插,每个样本调用一次 片元着色器 214 | 215 | 216 | 217 |
218 | 219 | 如果插值类型设置为 flat,则不得指定插值采样方式。 220 | 221 | > 换句话说,只有将插值类型设置为 perspective 或 linear 时才需要设置 插值的采样方式 222 | 223 | 224 | 225 |
226 | 227 | 1. 如果未指定插值类型,则默认为 perspective 228 | 2. 如果未指定插值的采样类型,则默认为 center。 229 | 230 | 231 | 232 |
233 | 234 | 关于插值就先讲到这里,等以后更加深入学习,再来填坑。 235 | 236 | 237 | 238 |
239 | 240 | 本文到此结束,下一节我们学习 WGSL 中的内置值和内置函数,学完之后就完成了对 WGSL 基础部分的学习。 -------------------------------------------------------------------------------- /02 WGSL基础之着色器的4个生命周期节点.md: -------------------------------------------------------------------------------- 1 | # 02 WGSL基础之着色器的4个生命周期节点 2 | 3 | **WGSL中着色器(shader)的4个生命周期节点分别是:着色器模块创建、管线创建、着色器开始执行、着色器执行完成。** 4 | 5 | 6 | 7 |
8 | 9 | 上面提到的前 2 个生命周期节点可以看做是 “着色器前期准备阶段”,而后 2 个生命周期节点可以看做是 “着色器开始和执行完成阶段”。 10 | 11 | 12 | 13 |
14 | 15 | > 本文提到的 "生命周期" 和 React/Vue 中的生命周期概念是相同。 16 | 17 | 我们把 “生命周期” 中几个关键的时间点称之为 “**节点**”,而节点与节点之间我们称之为 “**阶段**”。 18 | 19 | 20 | 21 |
22 | 23 | 下面就这 4 个生命周期节点详细了解一下。 24 | 25 | 26 | 27 | #### 着色器的4个生命周期节点 28 | 29 |
30 | 31 | > 以下会使用 “节点” 这个词来指代 “生命周期节点” 32 | 33 | 34 | 35 |
36 | 37 | **第1个节点:着色器模块创建(Shader module creation)** 38 | 39 | 当在 WebGPU 中执行 GPUDevice 实例的 createShaderModule() 方法后会开始 着色器模块创建节点。 40 | 41 | 在渲染管线中,目前只有 顶点阶段 和 片元阶段 是可以供我们进行编程的,所以正常情况我们需要执行 2 次 createShaderModule() 方法,分别用于创建 顶点着色器模块 和 片元着色器模块。 42 | 43 | 44 | 45 |
46 | 47 | 请记住:着色器模块创建节点实际上包含 2 个内容,分别是 顶点着色器模块创建 和 片元着色器模块创建。 48 | 49 | 50 | 51 |
52 | 53 | **第2个节点:管线创建(Pipeline creation)** 54 | 55 | 当在 WebGPU 中执行 GPUDevice 实例的 createComputePipeline() 或 createRenderPipeline() 方法后会开始 管线创建。 56 | 57 | 这两个方法会分别创建 计算管线 和 渲染管线。 58 | 59 | 60 | 61 |
62 | 63 | > 补充:以上 2 个方法分别对应有他们的异步形式,并且 WebGPU 官方推荐使用他们的异步函数,因为这样可以避免一些阻塞。 64 | > 65 | > createComputePipeline() 对应的异步函数为 createComputePipelineAsync() 66 | > 67 | > createRenderPipeline() 对应的异步函数为 createRenderPipelineAsync() 68 | 69 | 70 | 71 |
72 | 73 | 请记住:管线创建节点实际上包含 2 个内容,分别是 计算管线创建 和 渲染管线创建。 74 | 75 | 76 | 77 |
78 | 79 | > WebGPU 相关知识点回顾: 80 | > 81 | > * [WebGPU基础之着色器模块(GPUShaderModule)](https://github.com/puxiao/webgpu-tutorial/blob/main/10%20WebGPU%E5%9F%BA%E7%A1%80%E4%B9%8B%E7%9D%80%E8%89%B2%E5%99%A8%E6%A8%A1%E5%9D%97(GPUShaderModule).md) 82 | > * [WebGPU基础之管线(GPUPipelineBase、GPUComputePipeline、GPURenderPipeline)](https://github.com/puxiao/webgpu-tutorial/blob/main/11%20WebGPU%E5%9F%BA%E7%A1%80%E4%B9%8B%E7%AE%A1%E7%BA%BF(GPUPipelineBase%E3%80%81GPUComputePipeline%E3%80%81GPURenderPipeline).md) 83 | 84 | 85 | 86 |
87 | 88 | **第3个节点:着色器开始执行(Shader execution start)** 89 | 90 | 当在 WebGPU 中依次执行了下面操作: 91 | 92 | 1. 渲染通道编码器(GPURenderPassEncoder) 实例调用了 draw() 和 end() 方法 93 | 94 | 2. 命令编码器(GPUCommandEncoder) 实例调用了 finish() 方法,得到了 命令缓冲区(GPUCommandBuffer) 95 | 96 | 3. 最终 命令队列(GPUQueue) 通过 .submit() 将 命令缓冲区 提交给 GPU 97 | 98 | 4. 此时进入 着色器开始执行,进入该阶段的第一件事情就是去执行着色器模块中的入口函数 99 | 100 | > 着色器模块中的入口函数名是由 顶点/片元 着色器模块配置项中 `entryPoint` 属性值决定的。 101 | 102 | 103 | 104 |
105 | 106 | **第4个节点:着色器执行完成(Shader execution end)** 107 | 108 | 当所有着色器中的工作都被完成时发生。 109 | 110 | 1. 所有的调用都完成 111 | 2. 所有对资源的访问都完成 112 | 3. 若有需要,输入已传递给下游管线阶段 113 | 114 | 115 | 116 |
117 | 118 | #### 着色器可能发生的错误 119 | 120 | 在以上 4 个节点的发生过程中,着色器程序可能会发生错误。 121 | 122 | 这些错误可以简单归纳为 2 类。 123 | 124 | 125 | 126 |
127 | 128 | **程序错误(program error)**: 129 | 130 | 着色器(.wgsl) 中的代码不符合 WGSL 或 WebGPU 规范时发生。 131 | 132 | 这里推荐在 VSCode 中安装一个名为 “wgsl-analyzer” 的插件,它可以帮助我们对 .wgsl 文件中的代码进行格式化,以及少量的语法检查。 133 | 134 | 135 | 136 |
137 | 138 | **未分类错误(uncategorized erro)**: 139 | 140 | 着色器(.wgsl) 中的代码虽然符合 WGSL 或 WebGPU 规范,但或许因为 着色器太过复杂,超出了当前设备可实现的能力,这些错误就会被归纳为 "未分类错误"。 141 | 142 | > 若真的发生这种错误,那么只能通过简化着色器代码来解决。 143 | 144 | 145 | 146 |
147 | 148 | 当然还有一种可能发生未分类错误的原因,那就是 WebGPU 本身存在的缺陷。 149 | 150 | > 毕竟 WebGPU 目前还处于草案当中,并未完全成熟。 151 | 152 | 153 | 154 |
155 | 156 | 以上错误可能会发生在以下 3 个着色器生命周期阶段内: 157 | 158 | 1. 着色器模块创建阶段 159 | 2. 管线创建阶段 160 | 3. 着色器执行期间 161 | 162 | 163 | 164 |
165 | 166 | **编译消息:** 167 | 168 | 在编译执行 WGSL 的过程中,除了错误(error)信息外,还会有 警告(warning) 和 信息(info)。 169 | 170 | 在这 3 种信息中还会包含 错误/警告 相对应的代码行号、具体字符位置 等其他信息。 171 | 172 | > 这些信息和平时使用 React/Vue 给我们提供的编译信息是相通、相似的。 173 | 174 | 175 | 176 |
177 | 178 | 本文我们学习了 WGSL 着色器的 4 个生命周期,以及在这期间可能抛出的错误、警告、信息。 179 | 180 | 下一节,我们将开始学习 WGSL 代码中的一些规范和约定,例如 如何添加注释、数字的一些特殊标记、常见关键字、如何添加声明等等。 181 | 182 | -------------------------------------------------------------------------------- /08 WGSL基础之控制语句.md: -------------------------------------------------------------------------------- 1 | # 08 WGSL基础之控制语句 2 | 3 | **和任何一门编程语言类似,WGSL 中支持常见的各种控制执行顺序的控制语句(statements),例如 if/switch/loop/for/while 等等。** 4 | 5 | 6 | 7 |
8 | 9 | 在学习 控制语句 前,先学习一下: 10 | 11 | 1. 赋值语句 12 | 2. 递增和递减 13 | 14 | 15 | 16 |
17 | 18 | > 尽管简单,但是我们还是过一遍。 19 | 20 | 21 | 22 |
23 | 24 | **简单赋值:** 25 | 26 | 将右侧的运算结果赋值给左侧的变量(引用)。 27 | 28 | ``` 29 | struct Person { 30 | name: string, 31 | age: i32, 32 | weight: f32 33 | } 34 | 35 | var me: Person; 36 | 37 | me.name = 'puxiao'; 38 | me.age = 36; 39 | me.weight = 76.5 40 | ``` 41 | 42 | > `struct` 是定义结构类型的关键词,相当于 TypeScript 中的 `interface` 43 | 44 | 45 | 46 |
47 | 48 | **虚假赋值:** 49 | 50 | 所谓虚假赋值,就是赋值语句中 用 `_` 来充当左侧,简单来说 `_` 就是一个无意义的占位符。 51 | 52 | ``` 53 | _ = someFun() 54 | ``` 55 | 56 | > `_` 在 TypeScript 中有时很有用,假设 TS 配置有 “若有未使用的变量则发出警告”,当有些函数中的某个参数确确实实不需要使用的时候,就可以使用 `_` 来做占位符,从而避免 TS 的警告。 57 | 58 | 59 | 60 |
61 | 62 | **复合赋值:** 63 | 64 | 将右侧的运算符移动到 等号 的左侧。 65 | 66 | ``` 67 | a = a + b 68 | 69 | //等同于 70 | 71 | a += b 72 | ``` 73 | 74 | 在 WGSL 中类似的运算符有:`+、-、*、/、%、&、|、^、>>、<<` 75 | 76 | 77 | 78 |
79 | 80 | **递增或递减** 81 | 82 | 对于变量自身的 递增(+1) 或 递减(-1)。 83 | 84 | ``` 85 | var a: i32 = 38; 86 | a++ // 此时 a 的值为 39 87 | a-- // 此时 a 的值又重新为 38 88 | ``` 89 | 90 | 91 | 92 |
93 | 94 | 接下来,我们过一遍控制语句。 95 | 96 | 请注意,控制语句在极个别地方的用法和 JS 是不相同的。 97 | 98 | 99 | 100 |
101 | 102 | **if 语句:** 103 | 104 | 和 JS 用法完全相同 105 | 106 | 107 | 108 |
109 | 110 | **switch 语句:** 111 | 112 | switch 的用法和 JS 相同,只不过在 WGSL 中多了一个关键词 `fallthrough`,可以将 判断控制器 移交给 下一个 case。 113 | 114 | > fallthrough 单词原意为:失败、落空 115 | > 116 | > 在 JS 的 switch 中是不存在 fallthrough 的,但是在其他的一些编程语言也是有 fallthrough 关键词的。 117 | > 118 | > 在 JS 中类似作用的是 continue ,只不过 continue 不允许出现在 switch 中。 119 | 120 | 121 | 122 |
123 | 124 | 几点需要注意: 125 | 126 | 1. switch 查询的值 不需要使用 括号 包裹 127 | 2. switch 查询的值 不必在乎拼写形式,而是在乎表达的值,例如 `0` 和 `0x0000` 是相等的 128 | 3. default 不需要必须位于结尾处,但它还是会作为 最终默认 的匹配选项 129 | 4. fallthrough 关键词不能使用在最后一项中(case 或 default) 130 | 131 | 132 | 133 |
134 | 135 | 示例: 136 | 137 | ``` 138 | switch x { 139 | case 0: { 140 | ... 141 | } 142 | default { 143 | ... 144 | } 145 | case 1,2 { 146 | .... 147 | fallthrough 148 | } 149 | case 3 { 150 | ... 151 | } 152 | } 153 | ``` 154 | 155 | 156 | 157 |
158 | 159 | **for 与 loop 循环:** 160 | 161 | for 循环 和 JS 中的用法完全相同。 162 | 163 | 而 loop 循环可以看做是 for 循环形式上的一种 “变体”。 164 | 165 | ``` 166 | let a: i32 = 2; 167 | var i: i32 = 0; 168 | loop { 169 | if(i>=4) { break; } 170 | a *= 2 171 | i = i++ 172 | } 173 | ``` 174 | 175 | > 从代码可读性角度来说,for 循环是 loop 循环的简写形式(语法糖)。 176 | 177 | 178 | 179 |
180 | 181 | **while 循环:** 182 | 183 | 和 JS 中的用法完全相同。 184 | 185 | 186 | 187 |
188 | 189 | **break if 语法:** 190 | 191 | break 可以跳出 switch、loop、for 循环。 192 | 193 | 而 break if 的用法是指:break + 条件判断 来决定是否跳出。 194 | 195 | ```diff 196 | //假设原本我们只写了一个 break ,那么意味着一定是跳出循环 197 | - break 198 | 199 | //而 break if xxx 是指只有当 xxx 为 true 时才会选择跳出循环 200 | + break if i>= 4; 201 | ``` 202 | 203 | 204 | 205 |
206 | 207 | **continue 与 continuing:** 208 | 209 | continue 是跳出本次循环的剩余代码,进入下一次循环。 210 | 211 | 那 WGSL 中的 continuing 是什么意思呢? 212 | 213 | > 我看了 WGSL 官方文档,也没太看明白。 214 | 215 | 216 | 217 |
218 | 219 | 大体上来讲 continuing 位于 loop 循环体的结尾处,你可以简单把它理解为 “本次循环体中最后一项执行的内容”,即使在前面已经发生了 continue(跳出本次循环体),也依然会执行 continuing 中的代码。 220 | 221 | > 我也不确定这样理解对不对。 222 | 223 | ``` 224 | var i: i32 = 0; 225 | loop { 226 | if(i >=4 ) { break; } 227 | if(i % 2 == 0) { continue; } 228 | let step: i32 = 2; 229 | 230 | continuing { 231 | i = i + step; 232 | } 233 | } 234 | ``` 235 | 236 | 237 | 238 |
239 | 240 | 在 continuing 中不允许出现以下 2 个关键词: 241 | 242 | 1. return 243 | 2. discard 244 | 245 | 246 | 247 |
248 | 249 | **discard 关键词:** 250 | 251 | 只有在 片元 着色器阶段才可以使用 `discard` ,表明立即结束当前片元着色器的执行,并丢弃当前的片元数据。 252 | 253 | ``` 254 | discard; 255 | ``` 256 | 257 | 258 | 259 |
260 | 261 | **本文小结:** 262 | 263 | 在 WGSL 的赋值语句、控制语句 绝大多数用法和 JS 都相同。 264 | 265 | 只不过 WGSL 中有以下几个 JS 中不存在的关键词和用法: 266 | 267 | 1. switch 中的 fallthrough 268 | 2. loop 循环:for 循环的另外一种形式 269 | 3. break if xxx:只有 xxx 为 true 时才执行 break 跳出 270 | 4. continuing:当前循环体中最后执行的内容 271 | 5. discard:立即停止当前片元着色器的执行,并丢弃当前片元数据 272 | 273 | 274 | 275 |
276 | 277 | 本文到此结束,下一节我们学习 函数(function) 相关知识点。 -------------------------------------------------------------------------------- /05 WGSL基础之内存.md: -------------------------------------------------------------------------------- 1 | # 05 WGSL基础之内存 2 | 3 | **本文学习一下在 WGSL 中内存相关的知识点。** 4 | 5 | 6 | 7 |
8 | 9 | 提前说一下,本文所说的 “内存” 你可以把它理解为: 10 | 11 | 1. 供 CPU 使用的内存 12 | 2. 供 GPU 使用的显存 13 | 14 | 无论是以上哪种 “存”,这些知识点都是相同的。 15 | 16 | 17 | 18 |
19 | 20 | **内存地址:** 21 | 22 | > 当然你也可以称呼它为 “内存位置” 23 | 24 | 1. 每一个内存地址可以存 8 个字节的内容 25 | 2. 多个内存地址组成一组内存 26 | 3. 每一个变量都对应一组内存地址 27 | 4. 每个变量所占的内存地址都不会与其他变量所占用的内存地址重叠 28 | 29 | 30 | 31 |
32 | 33 | **内存的3种访问模式:** 34 | 35 | 1. 只读(read):读取内存地址的内容 36 | 2. 只写(write):设置内存地址的内容 37 | 3. 读写(read_write):既可以读取,也可以设置 内存地址的内容 38 | 39 | 40 | 41 |
42 | 43 | **可存储类型:** 44 | 45 | WGSL 中可存储的类型有: 46 | 47 | 1. 标量(scalar):bool、u32、i32、f32、f16 48 | 2. 向量(vector):vector2(f32)、vector3(f32)、vector4(f32)... 49 | 3. 矩阵(matrix):mat2x2(f32)、mat3x3(f32)... 50 | 4. 原子(atomic):`atomic`、`atomic` 51 | 5. 数组:`array`、`array` 52 | 6. 结构(structure):struct Data { ... } 53 | 7. 纹理(texture):texture 54 | 8. 采样器(sampler):sampler 55 | 56 | 57 | 58 |
59 | **IO可共享类型:** 60 | 61 | 管线输入和输出的值必须为 IO 可共享类型。 62 | 63 | > 所谓 “IO”,实际是 输入(Input) 和 输出(Out) 首字母的缩写 64 | > 65 | > 当然你粗浅的把 输入 理解为 写,把 输出 理解为 读,IO 也就是 读写 66 | 67 | 在 WGSL 中只有以下 3 种类型属于 IO 可共享类型: 68 | 69 | 1. 标量类型 70 | 2. 数值向量类型 71 | 3. 成员全部为 标量 或 数值向量 的结构类型 72 | 73 | 74 | 75 |
76 | 77 | > 只有内置管线输入可能为 布尔类型,而用户输入或输出的数据属性不能 或 包含布尔类型。 78 | 79 | 80 | 81 |
82 | 83 | **主机可共享类型:** 84 | 85 | > 这里的 “主机” 在 WGSL 英文文档中使用的单词是 host 86 | 87 | 所谓 主机可共享类型 包含 2 种含义: 88 | 89 | 1. 主机与 GPU 之间共享的缓冲区内容 90 | 2. 主机与 GPU 之间可以复制 且 无需格式转换的内容 91 | 92 | 93 | 94 |
95 | 96 | **名词解释:内存布局(memory layout)** 97 | 98 | 假定我们现在得到了一大块连续的内存,为了更好的管理这块内存,我们需要对这块内存进行不同功能的划分(分割),通常把这种对内存的划分称之为 “内存布局”。 99 | 100 | 那究竟都怎么划分、布局了呢? 101 | 102 | 答:通常情况下,我们将系统内存划分成 2 + 5 段。 103 | 104 | 105 | 106 |
107 | 108 | 2 + 5 段? 109 | 110 | 111 | 112 |
113 | 114 | 假设系统内存是一个竖直的长条形,那么这 2 + 5 段 他们的分布顺序为: 115 | 116 | 1. 最上面的内核区:用于系统本身运行所需的内存 117 | 118 | 2. 最下面的保留区:用于系统预留出的一些内存 119 | 120 | > 最上面 和 最下面 这 2 段 就是 `2 + 5 段` 中的 2 121 | 122 | 123 | 124 |
125 | 126 | ![](https://raw.githubusercontent.com/puxiao/wgsl-tutorial/main/imgs/memory.jpg) 127 | 128 | 129 | 130 |
131 | 132 | 除了最上面和最下面,中间部分会被划分为 5 段,从上往下,他们依次是: 133 | 134 | 1. 栈区(stack):存放函数、方法、指针、局部变量、函数中参数等 135 | 136 | 2. 堆区(heap):存放那些我们通过 new 创建的变量 137 | 138 | 3. 未初始化数据(uninitialized data):也被称为 BSS 段,就是在代码中还未被初始化的变量 139 | 140 | 4. 已初始化数据(initialized data):通常简称为 数据段,就是在代码中那些已经被我们初始化过的变量、全局变量、全局静态变量 141 | 142 | 5. 文本段(text):也被称为 代码段,就是我们程序的代码文本 143 | 144 | > 代码段的内容发生变化意味着上面几个分段的内容也会相应发生变化(重新计算) 145 | 146 | 147 | 148 |
149 | 150 | 在有些计算机文章中,会将上面提到的 “段” 按照上下关系,划分为 高地址 和 低地址。 151 | 152 | 例如 文本段 属于 低地址,而 栈区 位于 高地址。 153 | 154 | 155 | 156 |
157 | 158 | **思考一下:假设我们通过 WebGPU+WGSL 更改绘制三角形的颜色,实现方式是?** 159 | 160 | 1. 第1种方式:每次通过修改 WGSL 中的代码,然后重新执行一遍 .wgsl 内容 161 | 162 | > 对应的是修改 “文本段(代码段)” 中的内容 163 | 164 | 2. 第2种方式:WGSL 代码内容只执行一次,但是我们通过某个方法动态修改内存中的变量(内存)值 来实现颜色更换 165 | 166 | > 对应的是修改 “栈区/堆区” 中的内容 167 | 168 | 想象一下,上面哪种方式性能最佳? 169 | 170 | 171 | 172 |
173 | 174 | 上面讲解的 内存布局 是指 通常意义上程序运行的机制。 175 | 176 | 但是对于 WGSL 而言,它的内存布局有着更加细致、严格 的相关知识点,例如。 177 | 178 | 1. 对齐和大小 179 | 2. 结构成员布局 180 | 3. 数组布局 181 | 4. 值的内部布局 182 | 5. 地址空间布局约束 183 | 184 | 我暂时也没有完全理解这些概念,不过,有 2 个词一定要提一下。 185 | 186 | **统一缓冲区(Uniform buffer) 与 存储缓冲区(storage buffer)** 187 | 188 | > 在 WSGL 官方文档中很多地方都使用单词 uniform 和 storage 来指上述 2 个缓冲区。 189 | 190 | 191 | 192 |
193 | 194 | 先翻篇,接着看下一个概念。 195 | 196 | 197 | 198 |
199 | 200 | **内存视图类型** 201 | 202 | 如同读写一个纹理数据需要使用到 纹理视图 一样,对于内存的读写 也需要通过 内存视图 才可以。 203 | 204 | > 内存视图 的英文为 memory view 205 | 206 | 在 WGSL 中有 2 种内存视图类型: 207 | 208 | 1. 引用类型:reference types,简写为 ref 209 | 210 | > 引用类型 并不会出现在 WGSL 源码中,它是用来分析 WGSL 代码的 211 | 212 | 2. 指针类型:pointer types,简写为 ptr 213 | 214 | > 指针类型 可能会出现在 WGSL 源码中 215 | 216 | 217 | 218 |
219 | 220 | > 特别提醒:上面说的是 引用类型和指针类型,并不是 引用和指针。 221 | 222 | 223 | 224 |
225 | 226 | 无论哪种视图类型,他们的格式规范都相似,分别是: 227 | 228 | 1. ref 229 | 2. ptr 230 | 231 | 232 | 233 |
234 | 235 | > S:存储类 236 | > 237 | > T:可存储类型 238 | > 239 | > A:访问模式 240 | 241 | 242 | 243 |
244 | 245 | 某种程度上来讲: 246 | 247 | 1. 引用:暗含 全局变量 的意思 248 | 2. 指针:暗含 局部变量 的意思 249 | 250 | 比如: 251 | 252 | 1. 用 var 声明的是 引用类型 253 | 2. 用 let 声明的是 指针类型 254 | 255 | > 有点像 JS 中的 var 和 let 256 | 257 | 再比如:函数中的参数就是 指针类型,而不是 引用类型 258 | 259 | 260 | 261 |
262 | 263 | **在WGSL中可以像TypeScript那样定义某种类型。** 264 | 265 | 这种形式被称为 “类型别名”。 266 | 267 | 举个例子: 268 | 269 | ``` 270 | type Arr = array 271 | ``` 272 | 273 | ``` 274 | type RTArray = array> 275 | ``` 276 | 277 | 278 | 279 |
280 | 281 | **在WGSL中,另外一个非常重要的知识点是:纹理与采样器类型** 282 | 283 | 在 WGSL 定义了和它们 2 个相关的很多字面类型。 284 | 285 | 例如 纹素(颜色)相关的有 rgba8unorm、rgba8uint、rgba16uint、r32float 等等 286 | 287 | 纹理深度相关的有 texture_depth_2d、texture_depth_cube 等等 288 | 289 | 我们先不去深究这些。 290 | 291 | 292 | 293 |
294 | 295 | 本文我们只是最粗略、初级得了解了一下 WGSL 中 内存的一些概念。 296 | 297 | 水平有限,目前能够讲解的也就这些了。 298 | 299 | WGSL 最核心的事情就是和 内存 打交道,只有慢慢完全了解 内存 相关知识点,才能够清晰理解 WGSL 的核心运行原理。 300 | 301 | 302 | 303 |
304 | 305 | 本文到此结束,接下来要学习一下在 WGSL 中如何声明变量。 306 | 307 | -------------------------------------------------------------------------------- /06 WGSL基础之变量声明.md: -------------------------------------------------------------------------------- 1 | # 06 WGSL基础之变量声明 2 | 3 | **在WGSL中,变量声明相关的 4 个关键词为:var、let、const、override** 4 | 5 | **变量修饰符相关的 4 个关键词为:private、workgroup、uniform、storage** 6 | 7 | **和全局变量相关的 2 个关键词为:group、binding** 8 | 9 | 10 | 11 |
12 | 13 | #### 变量声明相关:var、let、const、override 14 | 15 | 能够想象这 4 个关键词的用法应该和 TypeScript 中的用法差不多。 16 | 17 | > 由于 JS 并非面对对象语言,所以在 JS 中没有 override(重写/覆盖) 的概念,但是 TypeScript 中是可以使用 override 的。 18 | 19 | 20 | 21 |
22 | 23 | 实际情况是 var/let/const/override 他们的用法和 JS 中的用法确实非常相似,只是在某些细节上略微不同。 24 | 25 | 26 | 27 |
28 | 29 | **let** 30 | 31 | ``` 32 | let mysize: i32 = 1024 33 | ``` 34 | 35 | let 只能出现在函数内部中。 36 | 37 | let 声明变量时,其对应的 `类型` 就会固定下来,且以后也无法改变。 38 | 39 | 这句话隐含了 2 个事情: 40 | 41 | 1. let 声明变量时其值为必填项 42 | 43 | 2. let 声明变量时,假设没有明确定义其类型,WGSL 则会根据其值来推导出该变量是什么类型。 44 | 45 | 因此 `let mynum = 1` 与 `let mynum = 1.0` 推导出 mynum 的类型是不一样的 46 | 47 | 48 | 49 |
50 | 51 | **override** 52 | 53 | 1. WGSL 并不是面对对象语言,也没有 类(class) 的概念,所以本文提到的 override(覆盖声明) 并不是指去覆盖父类中的某个变量。 54 | 55 | 2. WGSL 中的 override 是指 `覆盖管道创建方法指定的值` 56 | 57 | 3. 假设管道中并未指定有该被覆盖的值,此时 override 相当于初始化该变量的值 58 | 59 | > 换句话说,override 至少需要指明类型 或者 具体的值,(具体的值可以反向推理出它是什么类型) 60 | 61 | 4. override 只能出现在 模块作用域 中 62 | 63 | > 换句话说,override 不应该出现在 函数中 64 | 65 | 66 | 67 |
68 | 69 | **const** 70 | 71 | 1. const 的用法 和 在 JS 中相同,用于声明类型和值都不能再改变的常量。 72 | 73 | 74 | 75 |
76 | 77 | **var** 78 | 79 | 1. var 的用法 和 在 JS 中相同 80 | 81 | 2. var 声明变量时允许先不赋值,例如 `var i: i32;`,此时默认其值为 0 82 | 83 | > 尽管默认可以不赋值,但是这样做法并不被推荐,最好还是在声明时明确其初始值 84 | 85 | 86 | 87 |
88 | 89 | #### 变量修饰符:private、workgroup、uniform、storage 90 | 91 | 接下来讲解一下变量修饰符,如果你只是会 JS ,那么这个变量修饰符似乎对你略微有点难以理解。 92 | 93 | 因为在 JS 中某个对象的所有属性都是可访问的,根本不存在 可访问 类型定义。 94 | 95 | > 当然目前 JS 类(Class)的定义中,声明变量时可以通过添加 `#` 的形式来将该属性变为私有。 96 | 97 | 98 | 99 |
100 | 101 | 如果你本身会 TypeScript ,那么也得认真看一下,因为 WGSL 中的变量修饰符和传统面对对象中的修饰符不太一样。 102 | 103 | 104 | 105 |
106 | 107 | **变量修饰符,就是在声明变量之前明确该变量的作用域以及可访问性。** 108 | 109 | **WGSL中的变量修饰符关键词有:private、workgroup、uniform、storage** 110 | 111 | 112 | 113 |
114 | 115 | 在了解这些变量修饰符之前,有必要先回顾一下在之前 `WGSL基础之普通类型` 那篇文章中的 `地址空间` 知识点。 116 | 117 | | 存储地址 | 可访问性 | 释义 | 118 | | --------- | ------------------ | ---------------- | 119 | | function | 读、写 | 同一函数内 | 120 | | private | 读、写 | 同一模块内 | 121 | | workgroup | 读、写 | 着色器工作组内 | 122 | | uniform | 读 | 相同的着色器阶段 | 123 | | storage | 读(默认仅为读)、写 | 相同的着色器阶段 | 124 | | handle | 读 | 相同的着色器阶段 | 125 | 126 | > 注意:上述中的 handle 目前为 WGSL 的保留关键词,实际中还未被真正投入使用。 127 | 128 | 129 | 130 |
131 | 132 | **private:** 133 | 134 | 明确该变量仅在当前模块下可以访问。 135 | 136 | 示例:`var mynum: f32;` 137 | 138 | 139 | 140 |
141 | 142 | **workgroup:** 143 | 144 | 表明该变量在相同的 workgroup(着色器工作组) 中可以访问。 145 | 146 | 示例:`var worklist: array;` 147 | 148 | 149 | 150 |
151 | 152 | **uniform 与 storage:** 153 | 154 | 他们都表明该变量在相同的 着色器阶段(shader stage) 下可以访问。 155 | 156 | 不同点在于: 157 | 158 | 1. uniform:字面意思为 “统一”,即不可修改,其值为固定值 159 | 2. storage:字面意思为 “存储”,即可读也可写,其值可以修改 160 | 161 | 162 | 163 |
164 | 165 | **storage的特别补充:** 166 | 167 | 1. 假设我们的代码为 `var`,它相当于 `var` 此时仅为只读模式 168 | 2. 若想变为可读可写,则代码需要修改为 `var`,此时 storage 声明的变量才是可读可写。 169 | 170 | 171 | 172 |
173 | 174 | 接下来的内容并不是换了个话题,而是对变量修饰符做了一个知识延展。 175 | 176 | 177 | 178 |
179 | 180 | 在 WGSL 中 “资源接口” 包含:统一缓冲区、存储缓冲区、纹理、采样器 181 | 182 | 183 | 184 |
185 | 186 | **统一缓冲区 和 存储缓冲区** 187 | 188 | 1. 多个使用 `var` 生成的变量汇聚到一起,形成了 统一缓冲区(uniform buffers) 189 | 2. 多个使用 `var` 生成的变量汇聚到一起,形成了 存储缓冲区(storage buffers) 190 | 191 | 192 | 193 |
194 | 195 | WGSL 中的纹理和采样器相关知识我们还没学习,不过因为学习过 WebGPU,所以 纹理和采样器 我假定你都知道他们是做什么的。 196 | 197 | > 如果你是纯小白,对 纹理和采样器 的概念都不清楚,那么你可以去看我之前写的 WebGPU 系列教程,里面有相关的介绍。 198 | 199 | 200 | 201 |
202 | 203 | **强调一下:在 WGSL 中对于纹理和采样器资源,无需使用变量修饰符** 204 | 205 | 为什么无需使用? 206 | 207 | 这里我猜测是因为 纹理 和 采样器 属于 WGSL 核心资源,WGSL 本身对它们 2 个有自己特定的访问、读写、存储规范,因此无需(也没必要) 我们像对待普通变量那样给它们增加变量修饰符。 208 | 209 | 210 | 211 |
212 | 213 | #### 和全局变量相关的关键词:group、binding 214 | 215 | WGSL 中定义了可应用于全局变量的 2 个关键词:group、binding 216 | 217 | 这 2 个关键词牵扯到 资源接口 中的一些概念,我们此刻只是知道有这 2 个关键词即可,不做深究。 218 | 219 | 我们只需要有个大体印象, 220 | 221 | 1. group:对应着色器工作组, 222 | 2. binding:对应管线布局 223 | 224 | 在使用它们 2 个时,还需要明确它们对应的 编号(序列号)数值: 225 | 226 | 1. 例如 `group(0)` 227 | 2. 例如 `binding(1)` 或 `binding(2)` 228 | 229 | 230 | 231 |
232 | 233 | >关于 编号(序列号) 的含义,需要查看 WebGPUPipelineLayout(管线布局) 相关知识才可以容易理解。 234 | 235 | 236 | 237 |
238 | 239 | **示例代码:** 240 | 241 | ``` 242 | struct Params { 243 | specular: f32, 244 | count: i32 245 | } 246 | @group(0) @binding(2) 247 | var param: Params 248 | 249 | 250 | @group(0) @binding(0) 251 | var pbuf: array> 252 | 253 | 254 | @group(0) @binding(1) 255 | var filter_params: sampler 256 | ``` 257 | 258 | 259 | 260 |
261 | 262 | **本文知识点回顾:** 263 | 264 | 1. WGSL 中声明变量的关键词有:let、const、var、override 265 | 266 | 2. WGSL 中变量修饰符关键词有:private、workgroup、uniform、storage 267 | 268 | > 纹理和采样器 无需使用修饰符 269 | 270 | 3. WGSL 中全局变量相关的关键词有:group、binding 271 | 272 | 273 | 274 |
275 | 276 | 本文到此结束,接下来要学习 表达式。 -------------------------------------------------------------------------------- /04 WGSL基础之普通类型.md: -------------------------------------------------------------------------------- 1 | # 04 WGSL基础之普通类型 2 | 3 | **WGSL属于静态类型语言,换言之在 WGSL 中每一个数据(变量/表达式)都需要明确指定其类型。WGSL 中类型大致可以划分为 3类:普通类型、内存视图类型、纹理和采样器类型** 4 | 5 | 6 | 7 |
8 | 9 | 本文我们只学习 普通类型。 10 | 11 | 12 | 13 |
14 | 15 | 我们先来看一下在 WGSL 中 数字 1 究竟可以是哪种类型,以及他们不同的表达形式。 16 | 17 | | 表现形式 | 对应类型 | 18 | | -------- | --------------- | 19 | | 1i | 32 位有符号整数 | 20 | | 1u | 32 位无符号整数 | 21 | | 1.0f | 32 位浮点数 | 22 | | 1.0h | 16 进制浮点数 | 23 | 24 | 25 | 26 |
27 | 28 | > 假设你会某一种面对对象编程语言,或者你会 TypeScript,那么对于数据类型并不会感到陌生,接受起来会非常容易。 29 | 30 | > 因为我最早从事 Flash AS3 开发,as3 就属于面对对象语言,后来 flash 没落了,我转行做过销售、UI 设计,3 年前转行做前端开发(原定的是全栈开发),刚开始学习 JS 发现这怎么跟 as3 那么像,后来发现 TypeScript 更像。 31 | 32 | > 如果你是只用 JS 的前端人员,且不会 TS,那么第一次接触 静态类型语言,你会非常不适应,觉得处处受限,但是这种不适感会很快消失的,因为你会发现所谓 “静态类型” 无非就是定义变量之前提前告诉编译器这个变量是什么类型,而编译器会根据你定义的类型来自动做代码检查,并且会根据类型申请创建更加精准的连续内存而已。 33 | 34 | > 补充:上面提到的 “变量” 不仅仅是指变量本身,还包括 函数、表达式 等。 35 | 36 | 37 | 38 |
39 | 40 | 接下来我们开始逐个学习 WGSL 中都有哪些类型概念。 41 | 42 | 特别说明:这些类型并不是孤立存在的,一些简单基础的类型可以构建出复杂、复合的类型。 43 | 44 | 45 | 46 |
47 | 48 | #### 布尔与数字相关的类型 49 | 50 | | 类型名 | 类型形式 | 对应值说明 | 51 | | -------------- | -------- | ----------------- | 52 | | 布尔类型 | bool | true 或 false | 53 | | 无符号整数类型 | u32 | 0 和 正整数 | 54 | | 有符号整数类型 | i32 | 负整数、0、正整数 | 55 | | 32位浮点数类型 | f32 | -- | 56 | | 16位浮点数类型 | f16 | -- | 57 | 58 | 59 | 60 |
61 | 62 | > 下面的类型,前提是假设你掌握 图形学基础 知识。如果你连 向量、矩阵 都不明白它们是什么,那么你一定要先去学图形学基础,再来学习 WGSL。 63 | 64 | 65 | 66 |
67 | 68 | #### 向量相关的类型 69 | 70 | 向量的英文单词为 vector,在 WGSL 中使用缩写 `vec` 来表示向量。 71 | 72 | 向量的书写模板为:`vecN` 73 | 74 | 1. N:指向量的维度,例如 2、3、4 分别表示二维向量、三维向量、四维向量 75 | 2. T:指向量每一个分量的类型 76 | 77 | 例如 `vec2(f32)` 就是指: 78 | 79 | 1. 一个二维向量 80 | 2. 且每一个分量的类型为 f32 81 | 82 | 照着这个模板套路你自然很容易看明白 `vec3`、`vec4` 是指什么了。 83 | 84 | 85 | 86 |
87 | 88 | > 补充一下为什么向量中通常使用 f32,而不是 u32 或 i32 ? 89 | > 90 | > 因为我们 99.99% 时间使用的向量都是 `归一化后的向量`,其每一个分量的取值范围都是 `0 ~ 1`,或者是 `-1 ~ 1` 91 | 92 | 93 | 94 |
95 | 96 | #### 矩阵类型 97 | 98 | 矩阵的英文单词为 matrix,在 WGSL 中使用缩写 `mat` 来表示矩阵。 99 | 100 | 矩阵的书写模板为:`matCxR` 101 | 102 | 1. C:是单词 col 的缩写,指明该矩阵的 列数 103 | 2. R:是单词 rol 的缩写,指明该矩阵的 行数 104 | 3. T:是单词 type 的缩写,指明该矩阵每个分量的类型 105 | 106 | 例如 `mat2x3` 就是指: 107 | 108 | 1. 该矩阵为 2 列 109 | 2. 该矩阵为 3 行 110 | 3. 该矩阵每一个分量的类型为 f32 111 | 112 | 113 | 114 |
115 | 116 | #### 原子类型 117 | 118 | 原子类型的英文单词为 atomic type,在 WGSL 中使用原子的单词 `atomic` 来表示原子类型。 119 | 120 | 在 WGSL 中原子类型是用于封装 整数标量(u32 或 i32) 类型的。 121 | 122 | 原子类型的书写模板为:`atomic`,其中 T 只能是 u32 或 i32。 123 | 124 | 125 | 126 |
127 | 128 | 向量类型、矩阵类型都在理解范围内,但是这个 原子类型 是用来做什么的? 129 | 130 | “原子类型” 这个词并不是 WGSL 发明的,像 C++ 中就有原子类型。 131 | 132 | 133 | 134 |
135 | 136 | > 又到了我胡说八道的时间了,关于原子类型,下面是我查阅相关资料后的理解。 137 | 138 | 先来看一个名词:**原子操作** 139 | 140 | 在上一节我们简单讲解了 并发 和 并行,并发可以简单理解为 “可以在不同任务之间自由切换”。 141 | 142 | 1. “不同任务” 准确来说是指 “不同的线程” 143 | 2. “自由切换” 是指 “线程的调度机制” 144 | 145 | 百度百科中关于 原子操作 的解释为: 146 | 147 | 原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始就会一直运行到结束,中间不会发生线程之间的切换。 148 | 149 | 150 | 151 |
152 | 153 | 但是在高并发程序中,往往是多个线程要同时访问和操作某一个数据,这就引申出了一个需要解决的事情:如何保护和同步共享数据? 154 | 155 | 既然是 “保护”,于是就出现了 2 个名词概念:**自旋锁、互斥锁** 156 | 157 | 关于 自旋锁和互斥锁 我也不太理解。 158 | 159 | 总之我们只需要知道: 160 | 161 | 1. 原子操作 与 高并发 之间存在 数据同步共享 的问题矛盾 162 | 2. 通过某种策略(自旋锁、互斥锁) 可以解决这种矛盾 163 | 164 | **而 WGSL 中的 原子类型 就是解决这种矛盾中的一个重要关键点。** 165 | 166 |
167 | 168 | 至于 原子类型 实际使用方式和示例,等到以后再慢慢学习吧,本文只是有一个模糊印象即可。 169 | 170 | 171 | 172 |
173 | 174 | #### 数组类型 175 | 176 | 实际上我更习惯使用 “元祖” 这个词。 177 | 178 | 数组类型的书写模板为:`array` 或 `array` 179 | 180 | 1. E:是单词 element 的缩写,指明数组元素的类型 181 | 182 | 2. N:是单词 number 的缩写,指明数组元素的总个数(也就是数组的长度) 183 | 184 | > 这里的 N 必须是大于 0 的整数 185 | 186 | 例如 `array` 就是指: 187 | 188 | 1. 一个数组,该数组中每个元素类型都是 f32 189 | 2. 该数组一共有 8 个元素,数组长度为 8 190 | 191 | 我们也可以通过常量形式来表达 N : 192 | 193 | ``` 194 | const arrSize = 8; 195 | ... 196 | array 197 | ``` 198 | 199 | > 注意必须是使用 const 声明的常量 200 | 201 | 202 | 203 |
204 | 205 | 特别注意: 206 | 207 | 1. 平时定义变量时我们只能使用 `array` 这种形式,也就是说在定义一个数组变量时一定设定好该数组的元素总个数。 208 | 209 | > 一个老生常谈的话题:JS 中的数组 array 实际上并不是真的数组,它只不过是 通过 object 模拟出来的 "数组",JS 中的数组元素之间是可以不连续的。 210 | > 211 | > 但是在 WGSL 中,数组元素必须是连续的。 212 | 213 | 2. 而 `array` 这种形式,缺少元素总个数,它不能应用于具体变量,它只能应用在某些特定上下文中,例如下面即将讲到的 结构类型 中。 214 | 215 | 216 | 217 |
218 | 219 | #### 结构类型 220 | 221 | 结构类型实际就相当于 TypeScript 中的 interface 所定义的类型,即 表明某个对象所拥有的属性以及属性值类型。 222 | 223 | “结构” 对应的英文单词为 struct,在 WGSL 中就是使用 struct 关键词来定义 结构类型的。 224 | 225 | 例如: 226 | 227 | ``` 228 | struct Data { 229 | a:i32, 230 | b:vec2, 231 | c:array, 232 | d:array 233 | } 234 | ``` 235 | 236 | > 在上面的示例代码中,最后一项 d 这一行结尾处加不加 英文逗号 "," 都是可以的 237 | 238 | 239 | 240 |
241 | 242 | 对于结构类型中的成员(属性),WGSL 定义了以下几个标识符: 243 | 244 | 1. builtin 245 | 2. location 246 | 3. align 247 | 4. size 248 | 249 | 我们暂时先不去学习这几个标识符的含义。 250 | 251 | 252 | 253 |
254 | 255 | #### 复合类型 256 | 257 | 假设某个结构类型中的成员类型为:vector、matrix、array 或 structure(结构类型),那么就把这种结构类型称之为 复合类型。 258 | 259 | 复合类型的深度定义约定为 1 + N,而 N 的值为: 260 | 261 | 1. 若为一维数组,则 N 为 1 262 | 2. 若为向量,则 N 为 1 263 | 3. 若为矩阵,则 N 为 2 264 | 4. 若一个复合类型中的成员存在 多个不同 类型值,则 N 取它们之间 N 为最大的那个值 265 | 266 | 换句话说,一个复合类型的深度为:1 + max(n1, n2, n3...) 267 | 268 | 269 | 270 |
271 | 272 | #### 可构造类型 273 | 274 | 如果某种类型的变量可以被创建、加载、存储、作为函数的参数或返回值,那么这些类型都可以称之为 可构造类型。 275 | 276 | 平时我们使用的绝大多数类型都是属于可构造类型。 277 | 278 | 那么问题来了:哪些类型不可构造? 279 | 280 | 1. 原子类型 281 | 282 | 2. runtime-sized 数组类型 283 | 284 | > 额~,runtime-sized 数组类型又是什么?我在看 WGSL 官方文档时也没太理解。 285 | 286 | 以上 2 种类型都不属于可构造类型。 287 | 288 | 289 | 290 |
291 | 292 | #### 固定占用类型 293 | 294 | 这里的 “占用” 是指 “内存占用”。 295 | 296 | 所谓 “固定占用类型” 就是指 占用固定内存大小的那些变量类型。它们在定义(创建) 时所占用的内存大小就已经固定下来,并且不会随着 WGSL 不同生命周期阶段而发生变化。 297 | 298 | 299 | 300 |
301 | 302 | 关于普通类型,我们就讲解到这里。 303 | 304 | 下一节我们将讲解 内存视图类型。 305 | 306 | -------------------------------------------------------------------------------- /07 WGSL基础之表达式.md: -------------------------------------------------------------------------------- 1 | # 07 WGSL基础之表达式 2 | 3 | **WGSL 除了一些常规的表达式外,还有一些自己独特的表达式。** 4 | 5 | 6 | 7 |
8 | 9 | **我们先回顾一下之前学习的一个知识点:着色器的4个生命周期** 10 | 11 | > 详细的可查阅之前的文章:WGSL基础之着色器的4个生命周期节点 12 | 13 | 1. **着色器模块创建阶段**(Shader module creation): 14 | 15 | 当 WebGPU createShaderModule 方法被调用后发生,此时将开始解析 WGSL 的源码。 16 | 17 | 2. **管线创建阶段**(Pipeline creation): 18 | 19 | 当 WebGPU createComputePipeline 或 createRenderPipeline 方法被调用后发生,他们分别创建 计算管线 和 渲染管线。 20 | 21 | > 特别补充:官方更加推荐使用上述 2 个方法的异步版函数,即 createComputePipelineAsync 和 createRenderPipelineAsync,这样可以避免阻塞。 22 | 23 | 3. **着色器开始执行阶段**(Shader execution start): 24 | 25 | 当 GPUQueue 通过 submit() 将命令缓冲区 数据提交给 GPU ,开始执行管线,并调用相对应的入口函数后发生。 26 | 27 | > 特别补充:在 submit() 之前发生的事情是——渲染通道编码器(GPURenderPassEncoder) 调用 draw() 或 end(),然后 命令编码器(GPUCommandEncoder) 调用了 finish() 方法,得到了命令缓冲区(GPUCommandBuffer)。 28 | 29 | 4. **着色器执行完成阶段**(Shader execution end): 30 | 31 | 当所有着色器中的工作都被完成时发生。 32 | 33 | 34 | 35 |
36 | 37 | 我们在 WGSL 中所使用、编写的各种表达式,都会在上述第 1 个生命周期中被解析。 38 | 39 | 40 | 41 |
42 | 43 | 下面按照 WGSL 官方文档,我们快速得过一遍各种表达式。 44 | 45 | > 千万不要被某些表达式的名字给吓到,实际上这些表达式都很好理解。 46 | 47 | 48 | 49 |
50 | 51 | **创建时表达式** 52 | 53 | > 额~,英文是 `create-time expressions`,直译是 `创建时间表达式`,我感觉非常别扭,这里我姑且暂时将它翻译为 `创建时表达式` 54 | 55 | 名字很绕口,但实际就是指 表达式中那些具体的数字。 56 | 57 | > 可能我理解的不对,但目前我就是这样理解的。 58 | 59 | 60 | 61 |
62 | 63 | > 在 WGSL 中定义了 2 个 “抽象数值类型”: 64 | > 65 | > 1. AbstractInt:整数集,即 -2 的 63 次方 到 2 的 63 次方 之间的整数 66 | > 67 | > abstract 是抽象的意思,而 int 是整数的意思 68 | > 69 | > 2. AbstractFloat:有限浮点数,即 IEEE-754 binary64 70 | > 71 | > IEEE-754 是二进制浮点数算术标准,而 binary64 是 64 位浮点数 72 | 73 | 74 | 75 |
76 | 77 | **文字表达式** 78 | 79 | 就是用文字来表达某些标量,例如 true、false、AbstractInt、AbstractFloat、i32、u32、f32、f16 80 | 81 | 82 | 83 |
84 | 85 | **括号表达式** 86 | 87 | 就是在运算中,括号的用法。 88 | 89 | > 我都不知道该说点什么 90 | 91 | 92 | 93 |
94 | 95 | **类型构造函数表达式** 96 | 97 | 在声明向量、矩阵、数组时用到的 `套路形式`,例如:`vec2`、`mat2x3`、`array` 98 | 99 | 所谓 `类型构造函数表达式` 是指在上面那些 套路形式中 如何去设置初始值。 100 | 101 | 例如:`vec3(0,1,0)` 102 | 103 | 可以把上面的 (0,1,0) 看作是 “构造函数(实例对象)初始化时所传的参数”。 104 | 105 | 106 | 107 |
108 | 109 | 假设我并不填写参数,那么 WGSL 会用 “零” 来作为参数的默认值。 110 | 111 | | 不传参数 | 对应的 “零” 值 | 112 | | -------- | -------------- | 113 | | bool() | false | 114 | | i32() | 0 | 115 | | u32() | 0 | 116 | | f32 | 0.0 | 117 | | f16 | 0.0 | 118 | 119 | 如果是 向量、数组、矩阵 ,它们的元素为上面表格中的某一种类型,那么也遵循这套原则。 120 | 121 | 例如: 122 | 123 | 1. `vec2()` 等同于 `vec2(0.0,0.0,0.0)` 124 | 2. `vec3()` 等同于 `vec3<0,0,0>` 125 | 3. `array()` 等同于 `array(false,false)` 126 | 127 | 128 | 129 |
130 | 131 | > 我个人建议 无论在什么情况下都使用`明确的值` 来填充到表达式中,这样有利于代码阅读。 132 | 133 | 134 | 135 | 136 | 137 | **在 WGSL 中没有 JS 中的那种 “隐式转换”,不同类型的值如果需要转换,则必须使用目标类型的构造函数形式来强制转换。** 138 | 139 | 例如:把 某类型的值强制转换为 布尔值 ,其代码形式为 `bool(xxx)` 。 140 | 141 | 142 | 143 |
144 | 145 | **类型值的类型转化(重新解释)** 146 | 147 | 在 WGSL 中可以通过 `bitcast` 关键词,按照 `bitcast(e):T` 这种形式,将某个 表达式类型 转化(解释) 成另外一种类型。 148 | 149 | 例如:`bitcast>(e):vec2`,将 vec2 每个元素的类型由 f32 转化为 i32。 150 | 151 | 152 | 153 |
154 | 155 | **复合值分解表达式** 156 | 157 | 就是使用 `xxx.xx` 的形式去获取 xxx 中的某些元素(分量)值以及类型。 158 | 159 | 160 | 161 |
162 | 163 | 某个元素的分量有 N 种表达形式: 164 | 165 | 1. a,b,c,d,e... 166 | 2. r,g,b,a 167 | 3. x,y,z,w 168 | 4. IJKL 169 | 5. 数组索引形式,即 `[x]` 170 | 171 | > 请注意:上述分量的顺序是固定的,例如 b 是指 `rgba` 中的第 3 个分量。 172 | 173 | 174 | 175 |
176 | 177 | 假设,我们先定义一个数组:`var a: vec3 = (1.0, 2.0, 3.0)` 178 | 179 | 此时,我们就可以利用上面 4 种形式去 表达 a 的某些元素的值和类型。 180 | 181 | ``` 182 | var a: vec3 = (1.0, 2.0, 3.0); 183 | var b:f32 = a.y; // b = 2.0 184 | var c:vec2 = a.bb // c = (3.0, 3.0) 185 | var d: vec3 = a.zyx // d = (3.0, 2.0, 1.0) 186 | var e:f32 = a[1] // e = 2.0 187 | ``` 188 | 189 | 190 | 191 |
192 | 193 | 对于矩阵、数组、结构对象,他们也有自己对应的分量代表。 194 | 195 | > 套路基本相同,本文就不多讲了,实际遇到代码时看一样就明白了。 196 | 197 | 198 | 199 |
200 | 201 | **逻辑表达式** 202 | 203 | 逻辑表达式几个关键词用法和 JS 中的一模一样,例如: 204 | 205 | 1. 逻辑否定(取反):假设 e:bool,那么 `!e` 表示 e 的相反布尔值 206 | 207 | > 上面的属于 一元逻辑运算符 208 | 209 | > 下面的都属于 二元逻辑运算符 210 | 211 | 2. 短路 或:e1 || e2 212 | 213 | 3. 短路 与:e1 && e2 214 | 215 | > 所谓 短路 是指 根据前面 e1 的值来取决定是否有必要去计算 e2 216 | 217 | 4. 逻辑 或:e1 | e2 218 | 219 | 5. 逻辑 与:e1 & e2 220 | 221 | 222 | 223 |
224 | 225 | **算术表达式** 226 | 227 | 一类是比较基础的数据加减乘除(`+`、`-`、`*`、`/`)、取余运算(`%`),与 JS 中的一模一样,就不再多说了。 228 | 229 | 另外一类就是 矩阵 相关的几个计算:矩阵相加、相减、相乘、缩放、转置等。 230 | 231 | 232 | 233 |
234 | 235 | **比较表达式** 236 | 237 | 就是 `==`、`!=`、`<`、`<=`、`>`、`>=` 这几个比较运算符。 238 | 239 | 请注意,由于 WGSL 本身属于强类型语言,没有自动的隐式转换,所以不会,也不需要出现 JS 中的 `===`。 240 | 241 | 242 | 243 |
244 | 245 | **位表达式** 246 | 247 | 按位运算平时在 JS 中并不常用,不过 WGSL 中的 位表达式用法和一般的程序语言没有什么区别。 248 | 249 | 1. `~`: 250 | 2. `|`: 251 | 3. `&`: 252 | 4. `^`: 253 | 5. `<<`: 254 | 6. `>>`: 255 | 256 | > 这些位运算符的含义自己百度吧。 257 | 258 | 259 | 260 |
261 | 262 | **函数调用表达式** 263 | 264 | 暂时先不讲这个,因为在后面会专门有一篇用来讲解如何声明函数。 265 | 266 | > 实际也没有啥,就是跟 TypeScript 中定义函数的形式相似,即 参数类型、返回值类型等等 267 | 268 | 269 | 270 |
271 | 272 | **变量标识符表达式** 273 | 274 | 我没看懂官方文档关于这个的描述,暂时先忽略。 275 | 276 | 277 | 278 |
279 | 280 | **形式参数表达式** 281 | 282 | 就是参数的类型,例如 `a:T` 283 | 284 | 285 | 286 |
287 | 288 | **寻址表达式** 289 | 290 | 寻址(address-of) 操作符将一个引用转换为其对应的指针。 291 | 292 | `r.ref` 转化为 `&r.ptr` 293 | 294 | 295 | 296 |
297 | 298 | **间接寻址表达式** 299 | 300 | 间接寻址运算符(indirection) 将指针转化为相应的引用。 301 | 302 | > 和 寻址表达式 刚好是相反的操作 303 | 304 | `p:ptr` 转化为 `*p:ref` 305 | 306 | 307 | 308 |
309 | 310 | > 我相信对于绝大多数前端而言,不知道什么是 指针,什么是 引用。 311 | > 312 | > 我在查阅了一些 C++ 文章后,简单说一下: 313 | > 314 | > 1. 所谓 “指针” 是指 指向创建真实值(内存)的“变量” 315 | > 2. 所谓 “引用” 是指对于 指针 的再次声明变量,换句话说 **可以把 引用 当做成 指针的别名** 316 | > 317 | > 以 JS 举个示例: 318 | > 319 | > ``` 320 | > let num = 2 321 | > let mynum = num 322 | > 323 | > // num 就是指针(这只是个比喻) 324 | > // mynum 就是引用 (换句话说:mynum 是指针 num 的别名) 325 | > ``` 326 | 327 | 328 | 329 |
330 | 331 | **常量标识符表达式** 332 | 333 | 这里的常量实际就是指 `let` 声明变量时给变量设定的类型。 334 | 335 | 336 | 337 |
338 | 339 | 关于表达式,本文大概过了一遍,如果有不理解的地方,暂时不用去想,等后期 WGSL 代码看的多了,自然就明白了。 340 | 341 | 下一篇,我们将学习 控制语句,说直白点就是 if、wsitch、while 等。 -------------------------------------------------------------------------------- /01 WGSL基础之概念.md: -------------------------------------------------------------------------------- 1 | # 01 WGSL基础之概念 2 | 3 | **WGSL 是 WebGPU Shading Language 的缩写,它是 WebGPU 的着色器语言。** 4 | 5 | 6 | 7 |
8 | 9 | > 本文部分知识点来源于 10 | > 11 | > * Orillusion (国产WebGPU引擎) 翻译的 WGSL 文档: 12 | > 13 | > https://www.orillusion.com/zh/wgsl.html 14 | > 15 | > * 刘子瑛(阿里大牛,《TensorFlow+PyTorch深度学习从算法到实战》作者)所写的一篇文章, 16 | > 17 | > 欢迎来到 WebGPU 世界:https://mp.weixin.qq.com/s/SZvqpGJKGNeQfIa9fcp8XA 18 | 19 | 20 | 21 |
22 | 23 | > 我也是刚开始学习 WGSL,若有讲解错误的地方还请多多担待,更加欢迎留言指正。 24 | 25 | 26 | 27 |
28 | 29 | 开始吧。 30 | 31 | 32 | 33 |
34 | 35 | #### 概念1:WGSL与GLSL的关系 36 | 37 | 1. WGSL 是 WebGPU 配套的着色器语言,其语法更像 Rust 38 | 2. GLSL 是 WebGL 配套的着色器语言,其语法更像 C 39 | 40 | 41 | 42 | **补充:** 43 | 44 | 1. HLSL 是微软 Direct3D 配套的着色器语言 45 | 46 | > HLSL 是 High Level Shader Languase 的简写 47 | > 48 | > DXD12 是 Direct3D 第 12 版本的简写,从 DXD12 版本开始支持 WebGPU 49 | > 50 | > 有些文章会使用 DX12 来代指 DXD12 51 | 52 | 2. MSL 是苹果 Metal 配套的着色器语言 53 | 54 | > MSL 是 Metal Shading Language 的简写 55 | 56 | 3. SPIR-V 是 Vulkan 配套的着色器语言 57 | 58 | > SPIR 是 Standard Portable Intermediate Representation 的简写 59 | 60 | 61 | 62 |
63 | 64 | #### 概念2:WGSL与SPIR-V的关系 65 | 66 | 1. SPIR-V 是一种二进制格式 67 | 2. WGSL、GLSL、HLSL 都可以转换成 SPIR-V 68 | 3. SPIR-V 可以转化成 HLSL 和 MSL 69 | 70 | 71 | 72 |
73 | 74 | #### 概念3:为什么可以横跨3大框架(Vulkan/Metal/DXD12) 75 | 76 | 1. 由于 WGSL 可以转化为 SPIR-V 77 | 2. 而 SPIR-V 除 Vulkan外,还可转化运行在 Metal 和 DXD12 78 | 3. 因此 WebGPU、WGSL 可以实现兼容、横跨这 3 种框架与设备 79 | 80 | 81 | 82 |
83 | 84 | #### 概念4:CPU与GPU参与程度 85 | 86 | 1. 在 WebGL/GLSL 中,CPU 几乎参与每一步渲染控制中 87 | 2. 在 WebGPU/WGSL 中,CPU 通过命令编码器可以将多条(所有条)命令一起打包,并交给 GPU 去执行,然后 CPU 就可以空闲了 88 | 3. 由于 WebGPU/WGSL 减轻了 CPU 负担,将图形渲染工作交给了 GPU,因此 WebGPU 相对 WebGL 性能有大幅度提升 89 | 90 | 91 | 92 |
93 | 94 | #### 概念5:图形渲染与通用计算 95 | 96 | 1. WebGPU/WGSL 除了用于 3D 图形渲染,还可以用作 通用计算 97 | 2. 目前深度学习框架 TensorFlow.js 已经支持 WebGPU 98 | 3. 未来会有更多地方 或 框架会用到 WebGPU/WGSL,例如 BIM、CIM、WebXR 等等 99 | 100 | 101 | 102 |
103 | 104 | #### 概念6:命令式编程、声明式编程、函数式编程 105 | 106 | 1. 命令式编程:强调过程,一步一步告诉计算机应该做什么,专注于 “如何去做” 107 | 108 | 2. 声明式编程:专注于 “做什么”,而不是 “如何去做” 109 | 110 | > 比较明显的例子就是 数据库查询,通常只需要输入查询命令就可以得到结果,而无需编写具体的查询过程细节 111 | 112 | 3. 函数式编程:把运算过程尽量写成一系列嵌套的函数调用 113 | 114 | 115 | 116 |
117 | 118 | 命令式编程 与 函数式编程 对比示例,假设现在要计算 (1+2)*3/4 的结果。 119 | 120 | **命令式:** 121 | 122 | ``` 123 | const a = 1 + 2 124 | const b = a * 3 125 | const c = b / 4 126 | ``` 127 | 128 | > 强调具体的每一步计算过程,并且会记录过程中的值,也就是说:强调的是具体过程 129 | 130 | 131 | 132 |
133 | 134 | **函数式:** 135 | 136 | ``` 137 | const add = (num1, num2) => { 138 | return num1 + num 2 139 | } 140 | 141 | const multiply = (num1, num2) { 142 | return num1 * num2 143 | } 144 | 145 | const divide = (num1, num2) { 146 | return num1 / num2 147 | } 148 | 149 | const res = divide(multiply(add(1,2),3),4) 150 | ``` 151 | 152 | > 将具体过程改造成 不同嵌套函数 之间的调用,计算过程中的某些值并未被记录,只是将某个函数结果当做另外一个函数的参数而已 153 | 154 | 155 | 156 |
157 | 158 | > 函数式编程中还有另外一种特殊形式:柯里化,就是把接收多个参数的函数变换为接收单个参数的函数。例如将 fun(a,b,c) 转换为 fun(a)(b)(c) 这种形式。 159 | 160 | 161 | 162 |
163 | 164 | **思考题:** 165 | 166 | 假设有一个数组 let arr = [1,2,3] ,现在想得到该数组中每一项都+1 后的结果,于是代码写成: 167 | arr.map(item => item + 1) 168 | 169 | 请问,这种写法属于以下哪种编程: 170 | A:命令式 171 | B:声明式 172 | C:函数式 173 | 174 | 目前,我的答案是:B 175 | 176 | 1. 首先 `arr.map()` 并没有强调具体的每一步过程,也没有记录过程中每一步的计算结果,因此可以排除 A 177 | 2. 同时,`arr.map( item => item + 1)` 中也不是将上一次函数结果作为下一次函数的某个参数,不符合函数互相嵌套的形式,因此可以排除掉 C 178 | 3. 那它符合 B 声明式吗?符合,因为 `item => item + 1` “强调结果,而不强调过程” 179 | 180 | 181 | 182 |
183 | 184 | > 以上纯粹我个人理解和胡说八道,实际上把 `arr.map( item => item + 1 )` 说成是 函数式也是说得通的。 185 | 186 | 187 | 188 |
189 | 190 | > 网上还有一种说法:函数式属于声明式,是声明式的一种形式 191 | 192 | 193 | 194 |
195 | 196 | 之所以要讲解 命令式、声明式、函数式 的原因是:**WGSL 属于 命令式语言**,“强调每一步着色过程,一步步详细告诉 GPU 应该做什么”。 197 | 198 | > 额,这一句话不够严谨,实际上着色器(shader)仅仅是 WGSL 的一部分而已 199 | 200 | 所以通常情况下 .wgsl 中的代码会出现很多 if、for、switch 等代码。 201 | 202 | 它和我们前端使用的 JavaScript (侧重函数式) 在编写代码时,需要思维习惯上的一些转变。 203 | 204 | > 有条件的,也可以学习一下 Rust、WebAssembly 205 | 206 | 207 | 208 |
209 | 210 | #### 概念7:静态类型 VS 动态类型 211 | 212 | 1. JavaScript 属于动态类型,例如 let a =2; a='hello',变量 a 既可以是数字,也可以是字符串 213 | 214 | 2. TypeScript 属于(伪)静态类型 215 | 216 | > 为什么我说 TS 属于 伪静态类型呢?因为 TS 只是声明变量类型,但不强制变量为某一种固定类型(例如 TS 中允许出现 联合类型、any 等),TS 只是无限模拟 静态类型。 217 | 218 | 3. WGSL 属于静态类型,每一个变量、表达式计算结果 都需要明确定义其具体的特定类型 219 | 220 | 4. WGSL 不支持类型隐式转换,例如将数字转换为布尔值需要显式转换 或 重新构造 221 | 222 | 5. WGSL 仅支持同一类型的元祖,不像 TS 支持不同元素类型的元祖 223 | 224 | 225 | 226 |
227 | 228 | #### 概念8:并行 & 并发 229 | 230 | > 下面这段知识点来源于 郝伟博士 的一篇文章。示例说明多线程的两组概念:串行VS并行 和 并发VS顺发 231 | > 232 | > https://blog.csdn.net/weixin_43145361/article/details/124701832 233 | 234 | 235 | 236 |
237 | 238 | **并行 VS 串行:** 239 | 240 | 1. 串行(serial):通常一个大的任务会被拆分成多个小任务(环节、步骤),这些小任务按照顺序依次串联在一起,就像糖葫芦那样。在执行的过程中,各个小任务不能同时进行。他们只能按照顺序依次、逐个执行,直至全部完成, 241 | 2. 并行(parallel):尽管每一个小任务都是串行,但是假设有多个处理单元,每个单元都负责处理某一个小任务,也就是说 同时可以有 N 个小任务在 N 个处理单元进行,就被称为 并行。 242 | 243 | 小总结:串行 与 并行 强调的是就近使用 几个 处理单元来处理任务。 244 | 245 | 246 | 247 |
248 | 249 | > 换句话说: 250 | > 251 | > 1. 假设只能使用 1 个处理单元,那么就是 串行 252 | > 2. 假设能够使用 多个 处理单元,那么就是 并行 253 | 254 | 255 | 256 |
257 | 258 | **并发 VS 顺发:** 259 | 260 | 1. 顺发(sequential):按照顺序一件一件处理(调用),当前任务完成后才会开始触发(处理)下一个任务 261 | 262 | > 会发生阻塞 263 | 264 | 2. 并发(concurrent):当前任务进行中时,就可以随时切换(触发/处理)到另一个任务。 265 | 266 | > 不会发生阻塞 267 | 268 | 请注意,并发只是看上去好像 “可以同时进行多项任务”,但实际并发只是强调 “可以在多个任务中来回切换”。 269 | 270 | 因为无论并发还是顺发,他们都是在单个处理器中进行的。 271 | 272 | 273 | 274 |
275 | 276 | > 并行、串行 中的 “行” 背后暗藏着 “执行某件事” 的含义 277 | > 278 | > 并发、顺发 中的 “发” 背后暗藏着 “发出任务调度” 的含义 279 | 280 | 281 | 282 |
283 | 284 | **举一个例子:** 285 | 286 | 假设有一个人一边嚼口香糖,一边打电话。 287 | 288 | 1. 并行:对于 耳朵 和 嘴巴而言,他们属于两个不同的处理单元,且可以同时进行,因此 耳朵和嘴巴 属于 并行关系 289 | 290 | 2. 并发:对于 嚼口香糖 和 说话 而言,这 2 个行为都发生在同一个单元(嘴巴)中,嚼口香糖时不能说话,说话时不能嚼口香糖,但是这 2 个行为之间可以无缝切换,因此这两个行为属于 并发 关系。 291 | 292 | > 尽管看上去 嚼口香糖 和 说话 好像是同时发生的,但实际上这两个行为只是可以无缝切换而已。 293 | 294 | 3. 顺发:整个接听电话的过程为 摁下接听键 > 开始通话 > 自己或对方摁下挂电话键,这几个环节是严格按照顺序发生(执行)的,因此这些行为属于 顺发 关系。 295 | 296 | 297 | 298 |
299 | 300 | **实际关联:** 301 | 302 | 在 WGSL 官方文档 https://www.w3.org/TR/WGSL/ 第 1 章介绍中有这样一句话 303 | 304 | “Invocations within a shader stage execute concurrently, and may often execute in parallel. ” 305 | 306 | 翻译过来就是:着色器阶段中的调用会并发执行,并且通常会并行执行。 307 | 308 | 额~,如果不了解什么是 并发、并行,那么就完全无法理解上面这句话。 309 | 310 | 311 | 312 |
313 | 314 | 好了,关于 WGSL 的一些基础概念知识点就讲解到这里。 315 | 316 | 下一节,我们讲解着色器的生命周期。 317 | 318 | -------------------------------------------------------------------------------- /10 WGSL基础之内置值和内置函数.md: -------------------------------------------------------------------------------- 1 | # 10 WGSL基础之内置值和内置函数 2 | 3 | **在 WGSL 中存在一些内置值和内置函数,这些内置值和内置函数是我们着色器编程中最为重要的核心。** 4 | 5 | 6 | 7 |
8 | 9 | #### 内置值 10 | 11 | 内置值按照名称、所处管线阶段、属于输入还是输出,以及该内置值的类型,统计如下: 12 | 13 | | 名称 | 阶段 | 输入或输出 | 类型 | 14 | | ---------------------- | ---- | ---------- | ----------- | 15 | | vertex_index | 顶点 | 输入 | u32 | 16 | | instance_index | 顶点 | 输入 | u32 | 17 | | position | 顶点 | 输出 | `vec4` | 18 | | position | 片元 | 输入 | `vec4` | 19 | | front_facing | 片元 | 输入 | bool | 20 | | frag_depth | 片元 | 输出 | f32 | 21 | | sample_index | 片元 | 输入 | u32 | 22 | | sample_mask | 片元 | 输入 | u32 | 23 | | sample_mask | 片元 | 输出 | u32 | 24 | | local_invocation_id | 计算 | 输入 | `vec3` | 25 | | local_invocation_index | 计算 | 输入 | u32 | 26 | | global_invocation_id | 计算 | 输入 | `vec3` | 27 | | workgroup_id | 计算 | 输入 | `vec3` | 28 | | num_workgroups | 计算 | 输入 | `vec3` | 29 | 30 | 31 | 32 |
33 | 34 | 目前我们仅仅处于 WGSL 基础理论知识学习过程中,上述内置值的具体使用,以后都会慢慢使用到的。 35 | 36 | 此刻我们只是简单对它们有一个初步印象即可。 37 | 38 | 39 | 40 |
41 | 42 | 上述内置值对应的含义为: 43 | 44 | > 顺序和上面那个表格相同 45 | 46 | | 名称 | 描述 | 47 | | ---------------------- | --------------------------------------------- | 48 | | vertex_index | 当前顶点的索引 | 49 | | instance_index | 当前顶点的实例索引 | 50 | | position | 当前顶点的输出位置 | 51 | | position | 当前片元在帧缓冲区中的位置 | 52 | | front_facing | 当前片段位于正面图元上时为 true,否则为 false | 53 | | frag_depth | 片段的深度 | 54 | | sample_index | 当前片段的样本索引 | 55 | | sample_mask | 当前片元(输入)的样本覆盖掩码 | 56 | | sample_mask | 当前片元(输出)的样本覆盖掩码 | 57 | | local_invocation_id | 当前执行单元在本地工作组中的索引 | 58 | | local_invocation_index | 当前执行单元在本地工作组中的偏平形式的索引 | 59 | | global_invocation_id | 当前执行单元在全局工作组中的索引 | 60 | | workgroup_id | 当前调用的工作组 ID | 61 | | num_workgroups | 工作组的数量 | 62 | 63 | > 以上内置值需要经过大量实际代码练习使用,才会真正理解其含义,目前大致有个初步印象即可。 64 | 65 | 66 | 67 |
68 | 69 | #### 内置函数 70 | 71 | 首先强调一点,WGSL 的内置函数都有一个特性:重载(overload)。 72 | 73 | 简单来说就是 **同一个函数名,参数的数据类型不同,其对应的含义相似却又略微不同。** 74 | 75 | > 等后面看几个例子就明白 重载 的含义了。 76 | 77 | 78 | 79 |
80 | 81 | **内置函数的类型:** 82 | 83 | WGSL 中的内置函数分为以下 12 个分类: 84 | 85 | 1. 逻辑相关的内置函数 86 | 2. 数组相关的内置函数 87 | 3. 浮动数相关的内置函数 88 | 4. 整数相关的内置函数 89 | 5. 矩阵相关的内置函数 90 | 6. 向量相关的内置函数 91 | 7. 导数相关的内置函数 92 | 8. 纹理相关的内置函数 93 | 9. 原子相关的内置函数 94 | 10. 数据打包相关的内置函数 95 | 11. 数据解包相关的内置函数 96 | 12. 同步相关的内置函数 97 | 98 | 99 | 100 |
101 | 102 | **第1种:逻辑相关的内置函数:** 103 | 104 | 和逻辑相关的内置函数一共有 3 个,它们分别是:all、any、select 105 | 106 | > 简单来说可以把这 3 个看作是某些 if 条件语句的简写形式 107 | > 108 | > 在 Python 中也有这些函数的用法 109 | 110 | 111 | 112 |
113 | 114 | 首先说一下,这 3 个内置函数的参数都可以是以下 2 种形式: 115 | 116 | 1. `e: vecN`:某维度下的向量,且每个分量都为 bool 类型 117 | 2. `e`:某个可迭代对象 e 的所有元素都为 bool 类型 118 | 119 | 120 | 121 |
122 | 123 | 那么,以上 3 个逻辑内置函数的用法为: 124 | 125 | 1. `all(e:vecN)`:当 e 的所有分量都为 true 时,其运行结果才为 true 126 | 127 | 2. `all(e)`:当 e 的所有可迭代属性值都为 true 时,其运行结果才为 true 128 | 129 | 3. `any(e:vecN)`:当 e 的任何一个分量为 true 时,其运行结果就为 true 130 | 131 | 4. `any(e)`:当 e 的任意一个可迭代属性值为 true 时,其运行结果就为 true 132 | 133 | 5. `select(f:T, t:T, cond:bool)`:当 cond 参数为 true 时,其运行结果返回 t,否则返回 f 134 | 135 | > select() 和 三元运算 有些类似,只不过是先写 flase 结果值,后写 true 结果值 136 | 137 | 6. `select(f:vecN, t:vecN, cond:vecN)`:将 cond 的每一个分量都进行一次 select() 计算,并返回最终的结果 138 | 139 | 140 | 141 |
142 | 143 | **第2种:数组相关的内置函数** 144 | 145 | 数组相关的只有一个内置函数:arrayLength 146 | 147 | 其含义也非常简单,就是返回数组中元素的数量。 148 | 149 | 150 | 151 |
152 | 153 | **第3种:浮动数相关的内置函数** 154 | 155 | 浮动数相关的内置函数,简单来说相当于 JS 中 Math 提供的很多计算相关的函数。 156 | 157 | 例如: 158 | 159 | 1. abs():求绝对值 160 | 2. cos():求余弦 161 | 3. acos():求反余弦值 162 | 4. ceil():求上限(整数) 163 | 5. max():求参数中的最大值 164 | 6. ... 165 | 166 | 167 | 168 |
169 | 170 | 除了像 JS 中 Math 所能提供的二维几何相关的计算函数外,WGSL 还提供了浮动数其他的一些函数。 171 | 172 | 例如: 173 | 174 | 1. faceForward(e1, e2, e3):如果 e2 与 e3 点乘 结果为负则返回 e1,否则返回 -e1 175 | 176 | > 点乘也叫 内积,是线性代数 中的知识点,用于计算 2 个向量之间的夹角 177 | 178 | 2. distance(e1, e2):计算两个空间坐标的距离 179 | 180 | 3. fract(e):返回 e 的小数点后的数值 181 | 182 | 4. cross(e1,e2):返回 3维向量 e1 和 e2 的叉乘 183 | 184 | 5. ... 185 | 186 | > 函数太多,就不逐一介绍了,更多的请查阅 WGSL 官方文档。 187 | 188 | 189 | 190 |
191 | 192 | **第4种:整数相关的内置函数** 193 | 194 | 整数相关的内置函数,不少都与 向量的分量 有关。 195 | 196 | 例如: 197 | 198 | 1. clamp():限制分量的取值范围 199 | 200 | > 有些文章会把 clamp 翻译为 “收窄”,个别文章会翻译为 “夹紧” 201 | 202 | 2. shiftRight():逻辑右移 203 | 204 | 3. reverseBits():反转参数中的位 205 | 206 | 4. ... 207 | 208 | 209 | 210 |
211 | 212 | > 同样,因为函数众多,就不逐一介绍了。 213 | 214 | 215 | 216 |
217 | 218 | **第5种:矩阵相关的内置函数** 219 | 220 | 一共有 2 个: 221 | 222 | 1. determinant(e):返回 e 的行列式 223 | 2. transpose(e):返回 e 矩阵转置后的结果 224 | 225 | 226 | 227 |
228 | 229 | **第6个:向量相关的内置函数** 230 | 231 | 只有 1 个:dot(),用于计算参数的 点乘。 232 | 233 | > 点乘又被称为 点积、内积,其运算结果为一个向量在另外一个向量上的投影。 234 | 235 | 236 | 237 |
238 | 239 | > 有点乘(dot),但为什么没有 叉乘(cross)? 240 | > 241 | > 实际上 叉乘(cross) 被归类到了 `第3种:浮动数相关的内置函数` 242 | 243 | 244 | 245 |
246 | 247 | **第7个:导数相关的内置函数** 248 | 249 | 先解释一下什么叫导数和偏导数。 250 | 251 | 252 | 253 |
254 | 255 | > 以下内容来源于百度百科 256 | 257 |
258 | 259 | **导数**(derivative)也叫 导函数值,又名 微商,是微积分中的重要基础概念。 260 | 261 | > 这里的 微商 可不是卖货的那个微商 262 | 263 | 导数是函数的局部性质。一个函数在某一点的导数描述了这个函数在这一点附近的变化率。 264 | 265 | 266 | 267 |
268 | 269 | **偏导数**(partial derivative):在数学中,一个多变量的函数的偏导数,就是它关于其中一个变量的导数而保持其他变量恒定。偏导数在向量分析和微分几何中是很有用的。 270 | 271 | > 我大学也学过微积分,可是忘得一干二净了,微积分对我来说是天书。 272 | 273 | 274 | 275 |
276 | 277 | 有偏导数,那自然有对应的 全导数。 278 | 279 | **全导数:......** 280 | 281 | > 不想听 282 | 283 | 284 | 285 |
286 | 287 | 总之记住以下 2 点: 288 | 289 | 1. 在 WGSL 中 偏导数 只能应用在 片元 着色器阶段 290 | 291 | > 用于相邻片元操作的片段着色器调用协作中 292 | 293 | 2. 只能在 uniform control flow 中被调用 294 | 295 | > uniform control flow 从字面上应该翻译为:统一控制流 296 | 297 | 298 | 299 |
300 | 301 | 在 WGSL 中一共存在以下几种 导数 相关的内置函数: 302 | 303 | 1. dpdx(e:T) -> T:e 的偏导数对应窗口的 x 坐标 304 | 2. dpdxCoarse(e:T) -> T:使用局部差异,返回 e 的偏导数相对于窗口 x 的坐标 305 | 3. dpdxFine(e:T) -> T:返回 e 的偏导数相对于窗口 x 坐标 306 | 4. dpdy(e:T) -> T:e 的偏导数对应窗口的 y 坐标 307 | 5. dpdyCoarse(e:T) -> T:使用局部差异,返回 e 的偏导数相对于窗口 y 的坐标 308 | 6. dpdyFine(e:T) -> T:返回 e 的偏导数相对于窗口 y 坐标 309 | 7. fwidth(e:T) -> T:相当于返回 abs(dpdx(e) + abs(dpdy(e))) 310 | 8. fwidthCoarse(e:T) -> T:相当于返回 abs(dpdxCoarse(e) + abs(dpdyCoarse(e))) 311 | 9. fwidthFine(e:T) -> T:相当于返回 abs(dpdxFine(e) + abs(dpdyFine(e))) 312 | 313 | 314 | 315 |
316 | 317 | **第8个:纹理相关的内置函数** 318 | 319 | 纹理可能是我们打交道最多的对象,但是本文不过多介绍,因为 纹理 太重要了,以后我们再慢慢展开。 320 | 321 | 1. textureDimensions:返回纹理的尺寸,或以 纹素(texels) 为单位的纹理的 mipmap 级别 322 | 323 | > dimensions 单词本意为:规模、大小 324 | 325 | 2. textureGather:返回一个四元素向量,其分量是从选定的纹素指定的通道中提取的 326 | 327 | > gather 单词本意为:收集 328 | 329 | 3. textureGatherComparse:对深度纹理中的四个纹素进行深度比较,并返回比较结果的四元素向量 330 | 331 | > comparse 单词本意为:比较、对比 332 | 333 | 4. textureLoad:从纹理中读取单个纹素,无需采样或过滤 334 | 335 | 5. textureNumLayers:返回数组纹理的层数 336 | 337 | 6. textureNumLevels:返回纹理的 mipmap 级别数 338 | 339 | 7. textureNumSamples:返回多重采样纹理中每个纹素的样本数 340 | 341 | 8. textureSample:采样纹理,只能在 片元 着色器阶段使用、只能在 统一控制流(uniform control flow) 中调用 342 | 343 | 9. textureSampleBias:对具有 mipmap 级别偏差的纹理进行采样,只能在 片元着色器阶段和 统一控制流中调用 344 | 345 | 10. textureSampleCompare:对深度纹理进行采样,并将采样的深度值与参考值进行比较,只能在 片元着色器阶段和 统一控制流中调用 346 | 347 | 11. textureSampleCompareLevel:对深度纹理进行采样,并将采样的深度值与参考值进行比较 348 | 349 | > 注意:textureSampleCompare 和 textureSampleCompareLevel 函数几乎相同,只不过 textureSampleCompareLevel 总是从 mipmap 0 级别开始采样纹理,并且可以在任何着色器阶段调用。 350 | 351 | 12. textureSampleGrad:使用显式渐变对纹理进行采样 352 | 353 | 13. textureSampleLevel:使用显式 mipmap 级别 或 mipmap 级别 0 对纹理进行采样 354 | 355 | 14. textureStore:将单个纹素写入到纹理中 356 | 357 | 358 | 359 |
360 | 361 | **第9个:原子相关的内置函数** 362 | 363 | 先复习一下什么叫 原子类型。 364 | 365 | > 关于 原子类型 在之前的 `WGSL基础之普通类型` 中简单讲解过。 366 | 367 | 368 | 369 |
370 | 371 | 在高并发程序中往往是多个线程要同时访问和操作某一个数据,这就需要解决一个问题:如何保护和同步共享数据。一般情况下都是通过 自旋锁、互斥锁 来解决的。 372 | 373 | 374 | 375 |
376 | 377 | 原子操作:多个共享内存的线程能够同时读写同一位置上的数据。原子操作会确保正在读或写的数据的值是符合预期的,简单来说就是下一个原子操作一定会在上一个原子操作结束后才开始。 378 | 379 | 380 | 381 |
382 | 383 | 在这些解决方案中,就存在一种 “永远无法再分割” 的数据类型概念,这种类型就被称之为 “原子类型”。 384 | 385 | 386 | 387 |
388 | 389 | > 在化学中,原子是指化学反应不可再分的基本微粒。 390 | > 391 | > 在物理学中,原子由原子核和绕核运动的电子组成,尽管原子核与电子可以再次分割,但分割所需要的力量是非常巨大的。 392 | 393 | 394 | 395 |
396 | 397 | 原子的英文单词是 atomic,所以在 WGSL 中也是用 atomic 这个关键词来定义原子类型。 398 | 399 | 原子类型的书写模板为:`atomic`,其中 T 的类型只能是 u32 或 i32 400 | 401 | 402 | 403 |
404 | 405 | **并不是只有在 WGSL 中才有原子类型,在原生 JS 中也有:atomics** 406 | 407 | 具体可以查看:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Atomics 408 | 409 | 410 | 411 |
412 | 413 | 原子内置函数的几个规定: 414 | 415 | 1. 不能在 顶点(vertex) 着色阶段使用 416 | 2. 内置函数的参数 atomic_ptr 的存储类 SC 必须是 storage(相同着色阶段) 或 workgroup(同一工作组) 417 | 3. 内置函数的参数 A (访问模式) 必须是 读与写(read_write) 418 | 419 | 420 | 421 |
422 | 423 | 下面是 原子 相关的 2 个内置函数: 424 | 425 | 1. `atomicLoad(atomic_ptr: ptr,A>) -> T`:原子负载 426 | 2. `atomicStore(atomic_ptr:ptr,A>,v:T)`:原子商店 427 | 428 | 429 | 430 |
431 | 432 | 下面是和 原子 读-修改-写 相关的内置函数: 433 | 434 | 1. atomicAdd():相加 435 | 2. atomicSub():相减 436 | 3. atomicMin():取小 437 | 4. atomicAnd():相与 438 | 5. atomicOr():相或 439 | 6. atomicXor():异或 440 | 7. atomicExchange():替换并返回旧值 441 | 442 | > 以上几个原子内置函数的参数和返回值都相同,都是:`(atomic_ptr:ptr,A>,v:T) -> T` 443 | 444 | 445 | 446 |
447 | 448 | 以上几个原子内置函数,在 JS 的 atomics 中也有类似的函数。 449 | 450 | 451 | 452 |
453 | 454 | `atomicCompareExchangeWeak(atomic_ptr: ptr, A>,cmp:T,v:T) -> __atomic_compare_exchange_result_` 455 | 456 | > 求放过 457 | 458 | 459 | 460 |
461 | 462 | **第10个:数据打包相关的内置函数** 463 | 464 | 这里的 “数据打包” 是指针对 WGSL 中某类型的数据格式进行编码,以便程序可以将打包后的数据密集写入内存,从而减少着色器的内存带宽需求。 465 | 466 | 1. `pack4x8snorm(e:vec4) -> u32`:将4个标准化浮点值转化为8位有符号的整数,然后将他们组合成一个 u32 值 467 | 2. `pack4x8unorm(e:vec4) -> u32`:将4个标准化浮点值转化为8位无符号的整数,然后将他们组合成一个 u32 值 468 | 3. `pack2x16snorm(e:vec2) -> u32`:将2个标准化浮点值转化为16位有符号的整数,然后将他们组合成一个 u32 值 469 | 4. `pack2x16unorm(e:vec2) -> u32`:将2个标准化浮点值转化为16位无符号的整数,然后将他们组合成一个 u32 值 470 | 5. `pack2x16float(e:vec2) -> u32`:将2个浮点值转化为半精度浮点数,然后将他们组合成一个 u32 值 471 | 472 | 473 | 474 |
475 | 476 | **第11个:数据解包相关的内置函数** 477 | 478 | 数据解包 是和 数据打包 刚好相反的一个过程,即程序从内存中读取许多密集打包的值,从而减少着色器的内存带宽需求。 479 | 480 | 数据解包相关的内置函数 和 数据打包内置函数相似,只不过: 481 | 482 | 1. 数据打包内置函数名都是以 `pack...` 为开头 483 | 2. 而数据解包内置函数名是以 `unpack...` 为开头 484 | 485 | 他们分别是:unpack4x8snorm()、unpack4x8unorm()、unpack2x16snorm()、unpack2x16unorm、unpack2x16float() 486 | 487 | 488 | 489 |
490 | 491 | **第12个:同步相关的内置函数** 492 | 493 | 所有同步函数只能在 计算(compute) 着色器阶段使用。 494 | 495 | WGSL 中有 2 个同步相关的内置函数: 496 | 497 | 1. storageBarrier():存储屏障,影响 存储类中的内存和原子操作 498 | 2. workgroupBarrier():工作组屏障,影响 工作组 存储类中的内存和原子操作 499 | 500 | > barrier 单词本意为:障碍、屏障、阻力、隔阂、关卡、分界线 501 | 502 | 503 | 504 |
505 | 506 | 至此,关于 WGSL 中的 12 个方面的内置函数讲解完毕。 507 | 508 | 只需有一些大概印象即可。 509 | 510 | 511 | 512 |
513 | 514 | 本文学习了 WGSL 中的 内置值 和 内置函数。 515 | 516 | 同时,本文也是 WGSL 基础系列教程中最后一篇文章了。 517 | 518 | 519 | 520 |
521 | 522 | **总结:** 523 | 524 | 通过一系列 WGSL 基础知识的学习,对 WGSL 有了一些底层的基础印象,虽然我们现在还不会熟练编写 WGSL 代码,但是有了这些基础后,再往下深入学习和实际应用就比较容易了。 525 | 526 | 至少从心里上对 WGSL 不再感到那么神秘和难易理解,也更容易看懂别人写的 WGSL 代码了。 527 | 528 | 529 | 530 |
531 | 532 | 加上之前对于 WebGPU 的学习,我们已经入门了 WebGPU + WGSL。 533 | 534 | **WebGPU 和 WGSL 基础系列教程的结束,恰恰是全新的、深入的实际开发之路的开始。** 535 | 536 | 537 | 538 |
539 | 540 | 接下来,我将筹划下一阶段的学习目标和教程。 541 | 542 | **加油,自己。** --------------------------------------------------------------------------------