4、着色器基础WGSL:WGSL语法入门、数据类型、函数定义、与GLSL的对比

好,咱们今天来聊聊WGSL。说实话,我第一次接触WGSL的时候,心里是有点抵触的——毕竟GLSL用了那么多年,凭什么要换?但真正上手写了几个Shader之后,我发现WGSL的设计其实挺讲究的。它不像GLSL那样“野路子”,而是更像一门正经的编程语言。

嗯,咱们今天的目标很明确:让你能看懂WGSL代码,能自己写简单的Shader,并且知道它和GLSL到底差在哪。

4.1 WGSL语法入门:先跑起来再说

WGSL的全称是WebGPU Shading Language。它是WebGPU的专属着色器语言。你想想看,WebGPU要跨平台、要安全、要高效,那它的着色器语言肯定不能太随意。

先看一个最简单的例子——一个啥也不干的片段着色器:

@fragment
fn main() -> @location(0) vec4<f32> {
    return vec4<f32>(1.0, 0.0, 0.0, 1.0);
}

这段代码干了三件事:

  • @fragment 是一个装饰器,告诉编译器这是个片段着色器入口
  • fn main() 定义了一个函数,名字叫main
  • -> @location(0) vec4<f32> 表示返回值是4维浮点向量,绑定到location 0

我个人习惯把这种结构叫做“装饰器+函数签名+函数体”三段式。你记住这个模式,后面所有Shader都长这样。

小提示:WGSL里没有void类型。如果函数不返回任何值,就写 -> void。但片段着色器必须返回颜色值,所以这里不能省略。

4.2 数据类型:比GLSL严谨多了

WGSL的数据类型,说白了就是“把GLSL的隐式转换全砍了”。我在项目中遇到过好几次因为类型不匹配导致的编译错误,后来学乖了——所有数字都老老实实写类型。

常用的标量类型:

类型 说明 GLSL对应
i32 32位有符号整数 int
u32 32位无符号整数 uint
f32 32位浮点数 float
bool 布尔值 bool

向量类型:

  • vec2<f32>vec3<f32>vec4<f32>
  • 也支持 vec2<i32>vec4<u32>

矩阵类型:

  • mat2x2<f32>mat3x3<f32>mat4x4<f32>
  • 注意:WGSL里矩阵是列主序的,和GLSL一样
注意:WGSL没有 double 类型!所有浮点数都是f32。我曾经试图传一个double进去,结果编译直接报错——嗯,从那以后我再也不在WGSL里用双精度了。

4.3 函数定义:有模有样

WGSL的函数定义比GLSL规范得多。它借鉴了Rust的语法风格,每个参数都要写类型,返回值也要写类型。

基本语法:

fn 函数名(参数名: 参数类型) -> 返回值类型 {
    // 函数体
    return 值;
}

举个例子,一个计算光照强度的函数:

fn calculateLight(normal: vec3<f32>, lightDir: vec3<f32>) -> f32 {
    let dotProduct = dot(normal, lightDir);
    return max(dotProduct, 0.0);
}

你想想看,GLSL里写 float calculateLight(vec3 normal, vec3 lightDir) 虽然也行,但类型检查没那么严格。WGSL是强类型,参数类型写错了直接编译不过。

函数可以嵌套调用,也可以递归——但我不建议你在Shader里写递归,性能太差了。

个人经验:我习惯把常用的数学运算封装成函数,比如颜色混合、法线变换、阴影计算。这样主函数看起来清爽很多,调试也方便。

4.4 与GLSL的对比:到底差在哪?

好,咱们来做个对比。我整理了一张表,把最核心的区别列出来:

特性 WGSL GLSL
类型声明 var x: f32 = 1.0; float x = 1.0;
常量声明 const PI: f32 = 3.14; const float PI = 3.14;
向量构造 vec3<f32>(1.0, 0.0, 0.0) vec3(1.0, 0.0, 0.0)
内置变量 @builtin(position) gl_Position
输入输出 @location(0) 装饰器 layout(location=0)
类型转换 显式转换,如 f32(1) 隐式转换较多

说白了,WGSL就是GLSL的“严格版”。它砍掉了所有模糊不清的地方,让你写代码的时候必须想清楚每个变量的类型和用途。

举个例子,GLSL里你可以写 vec3 color = 1.0;,编译器会自动帮你展开成 vec3(1.0, 1.0, 1.0)。但在WGSL里,你必须写 let color: vec3<f32> = vec3<f32>(1.0, 1.0, 1.0);。刚开始觉得麻烦,但习惯了之后发现——嗯,确实少了很多莫名其妙的bug。

4.5 避坑指南:我踩过的那些坑

最后分享几个我实际项目中遇到的坑:

  • 类型转换要显式写let a: f32 = 1; 会报错,必须写成 let a: f32 = f32(1);
  • 数组索引从0开始:这个和GLSL一样,但WGSL的数组越界会直接导致Shader崩溃
  • 不要用 ++--:WGSL不支持自增自减运算符,要用 i = i + 1;
  • 注意 letvar 的区别let 声明的是不可变变量,var 才是可变的
我曾经... 在项目里用 let 声明了一个变量,然后试图修改它,结果编译报错。找了半天才发现是 let 的问题。从那以后,我所有可变变量都用 var,不可变的才用 let

好了,这一章的内容就到这里。WGSL的语法其实不难,关键是适应它的“严谨风格”。下一章咱们会深入讲讲WGSL的缓冲区绑定和资源管理,那才是真正体现WebGPU威力的地方。