返回知识库

渲染

着色器语言核心

着色器语言核心 封面
WebGPU/WebGLShaderGLSL/WGSL语法对比资源绑定

直觉问题

  • 在 JavaScript 中写 const a = 5,到了 GLSL 中怎么就变成了 uniform float a = 5.0?WGSL 中又是什么样?
  • 同一个矩阵运算,GLSL 写 mat4 * vec4,WGSL 怎么写?类型到底要不要带尖括号 <f32>
  • 为什么 GLSL 可以用全局的 in/out 变量传递数据,而 WGSL 非要用 struct 包起来再通过 @location 传递?

这篇笔记将建立 GLSL 与 WGSL 的”语法对照表”,让你在两种语言间切换时不再晕头转向。


核心概念白话讲:为什么两种着色器语言的语法如此不同?

GLSL:C 风格的”自由派”

GLSL(OpenGL Shading Language)诞生于 2004 年,设计理念接近 C/C++

  • 灵活自由:隐式类型转换、全局变量、宏定义一应俱全
  • 硬件友好:直接映射到 GPU 寄存器和内存布局
  • 历史包袱:不同版本的 GLSL(1.0/3.0/4.5)语法差异巨大

WGSL:TS 风格的”严格派”

WGSL(WebGPU Shading Language)诞生于 2020 年,设计理念接近 TypeScript/Rust

  • 严格类型:无隐式转换,类型必须显式标注(vec4<f32>
  • 现代设计:统一的资源绑定模型(@group + @binding)、显式内存布局
  • 安全第一:编译时强制检查,避免运行时崩溃

语言特性对比全景

graph LR
    A[GLSL vs WGSL] --> B[类型系统]
    A --> C[变量声明]
    A --> D[函数定义]
    A --> E[资源绑定]
    
    B --> B1[GLSL: 隐式转换<br/>vec4(x, y, z, w)]
    B --> B2[WGSL: 显式类型<br/>vec4<f32>(x, y, z, w)]
    
    C --> C1[GLSL: 全局变量<br/>uniform mat4 uMVP;]
    C --> C2[WGSL: var + 存储类别<br/>var<uniform> uMVP: mat4x4<f32>;]
    
    D --> D1[GLSL: void main() {...}]
    D --> D2[WGSL: @vertex fn main(...) -> Output {...}]
    
    E --> E1[GLSL: layout(location=0)<br/>layout(binding=0)]
    E --> E2[WGSL: @location(0)<br/>@group(0) @binding(0)]

原理与机制:语法差异背后的设计哲学

1. 类型系统:隐式 vs 显式

GLSL 的隐式转换

GLSL 允许在数值类型之间自动转换:

float f = 5;        // int → float(自动)
vec3 v = vec3(1.0, 2, 3.0);  // mix int and float(自动)
mat4 m = mat4(1.0);  // scalar → matrix(自动)

WGSL 的严格类型检查

WGSL 要求类型完全匹配,必须显式转换:

let f: f32 = f32(5);                  // int → f32(显式)
let v: vec3<f32> = vec3<f32>(1.0, f32(2), 3.0);  // 显式类型标注
let m: mat4x4<f32> = mat4x4<f32>(1.0);          // 显式构造

来源说明:通过 WebSearch 检索 WGSL W3C 规范(2024 年 7 月)确认:“WGSL does not have implicit conversions or promotions from concrete types”(WGSL 对具体类型没有隐式转换或提升)[来源:https://www.w3.org/TR/WGSL/]

2. 变量存储模型:隐式内存 vs 显式地址空间

GLSL 的存储限定符

GLSL 通过限定符隐式指定变量所在的内存空间:

uniform mat4 uMVP;        // Uniform Buffer(常量数据)
in vec3 aPos;             // Vertex Attribute(顶点输入)
out vec3 vNormal;         // Varying(插值输出)
buffer LightBuffer {      // Shader Storage Buffer(读写数据)
    vec4 lights[];
};

WGSL 的显式地址空间

WGSL 通过 var<T> 的泛型参数显式指定存储类别:

@group(0) @binding(0)
var<uniform> uMVP: mat4x4<f32>;         // uniform:只读常量

@group(0) @binding(1)
var<storage, read> lights: array<vec4<f32>>;  // storage, read:只读存储

@group(0) @binding(2)
var<storage, read_write> particles: array<Particle>;  // storage, read_write:可读写

WGSL 地址空间映射

WGSL 地址空间GLSL 对应典型用途访问权限
uniformuniformMVP 矩阵、光照参数只读
storagebuffer / SSBO大型数据数组、粒子系统可配置 read / read_write
function函数局部变量临时计算结果读写
private无直接对应线程私有变量读写
workgroupshared (Compute Shader)Workgroup 内共享数据读写

3. 资源绑定模型:单一布局 vs 双层索引

GLSL 的 layout 限定符

GLSL 使用 layout(location=X)layout(binding=Y) 两种独立系统:

// Vertex Shader 输入
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aUV;

// 资源绑定
layout(binding = 0) uniform sampler2D uTexture;
layout(binding = 1) uniform LightBlock {
    vec4 position;
    vec4 color;
} uLights;

WGSL 的双层绑定系统

WGSL 引入了 @group@binding 双层索引:

// Vertex Shader 输入(仅 @location)
struct VertexInput {
    @location(0) aPos: vec3<f32>,
    @location(1) aUV: vec2<f32>,
};

// 资源绑定(@group + @binding)
@group(0) @binding(0)
var uTexture: texture_2d<f32>;

@group(0) @binding(1)
var uSampler: sampler;

@group(1) @binding(0)
struct Light {
    position: vec3<f32>,
    color: vec3<f32>,
}
var<uniform> uLights: array<Light, 16>;

来源说明:通过 WebSearch 检索 WebGPU Fundamentals(2024 年)确认:“WebGPU uses a two-level binding system: bind groups and bindings within groups”(WebGPU 使用双层绑定系统:绑定组和组内的绑定)[来源:https://webgpufundamentals.org/webgpu/lessons/webgpu-wgsl.html]

绑定模型对比流程图

graph TB
    subgraph GLSL["GLSL 资源绑定"]
        A1[layout binding=0<br/>uniform sampler2D tex]
        A2[layout binding=1<br/>uniform LightBlock lights]
        A3[单层索引系统<br/>直接映射到 GPU 资源槽]
    end
    
    subgraph WGSL["WGSL 资源绑定"]
        B1[@group 0 @binding 0<br/>var texture]
        B2[@group 0 @binding 1<br/>var sampler]
        B3[@group 1 @binding 0<br/>var uniform lights]
        B4[双层索引系统<br/>group → binding → GPU 资源]
    end
    
    A3 --> C[CPU 端绑定资源<br/>glBindBufferBase GL_UNIFORM_BUFFER 0]
    B4 --> D[CPU 端创建 BindGroup<br/>device.createBindGroup bindings: [0, 1, 2]]
    
    C --> E[Draw Call 执行]
    D --> E

4. 内存布局:隐式 std140 vs 显式 align/size

GLSL 的 std140 布局

GLSL 的 Uniform Buffer 默认使用 std140 布局,编译器自动计算对齐:

layout(std140, binding = 0) uniform UniformBlock {
    vec3 position;      // offset 0, size 12, padding to 16
    float radius;       // offset 16, size 4
    mat4 matrix;        // offset 32 (16 的倍数), size 64
} uBlock;

WGSL 的显式布局控制

WGSL 允许使用 @align@size 显式指定内存布局:

struct UniformBlock {
    @align(16) position: vec3<f32>,  // 强制 16 字节对齐
    radius: f32,                      // offset 12-15, size 4
    @align(16) matrix: mat4x4<f32>,   // 强制 16 字节对齐
}

@group(0) @binding(0)
var<uniform> uBlock: UniformBlock;

来源说明:通过 WebSearch 检索 “Memory Layout in WGSL | Learn Wgpu”(2024 年)确认:“WGSL requires explicit alignment and size attributes for struct members to match GPU memory layout”(WGSL 需要为结构体成员显式指定对齐和大小属性以匹配 GPU 内存布局)[来源:https://sotrh.github.io/learn-wgpu/showcase/alignment/]


GLSL vs WGSL 代码对照:让差异一目了然

1. 数据类型对照

概念GLSLWGSL核心差异
布尔boolbool相同
整数int / uinti32 / u32WGSL 显式位数
浮点floatf32 / f16WGSL 显式精度
向量vec2 / vec3 / vec4vec2<f32> / vec3<f32>WGSL 泛型带类型参数
矩阵mat2 / mat3 / mat4mat2x2<f32> / mat3x3<f32>WGSL 显式行列数
原子atomic_uintatomic<i32> / atomic<u32>WGSL 原子类型更丰富

向量构造示例

// GLSL
vec3 v1 = vec3(1.0, 2.0, 3.0);
vec4 v2 = vec4(v1, 1.0);
// WGSL
let v1: vec3<f32> = vec3<f32>(1.0, 2.0, 3.0);
let v2: vec4<f32> = vec4<f32>(v1, 1.0);

2. 变量声明对照

概念GLSLWGSL核心差异
常量const float PI = 3.14159;const PI: f32 = 3.14159;WGSL 类型显式
全局变量uniform mat4 uMVP;@group(0) @binding(0) var<uniform> uMVP: mat4x4<f32>;WGSL 需绑定 + 存储类别
局部变量float x = 5.0;var x: f32 = 5.0;WGSL 使用 var 关键字
只读变量const float x = compute();let x: f32 = compute();WGSL 用 let 表示不可变

完整示例对比

// GLSL Vertex Shader
#version 330 core

layout(location = 0) in vec3 aPos;
layout(location = 1) in vec2 aUV;

uniform mat4 uMVPMatrix;
uniform float uTime;

out vec2 vUV;
out float vTime;

void main() {
    gl_Position = uMVPMatrix * vec4(aPos, 1.0);
    vUV = aUV;
    vTime = uTime;
}
// WGSL Vertex Shader
struct VertexInput {
    @location(0) aPos: vec3<f32>,
    @location(1) aUV: vec2<f32>,
};

struct VertexOutput {
    @builtin(position) clipPosition: vec4<f32>,
    @location(0) vUV: vec2<f32>,
    @location(1) vTime: f32,
};

@group(0) @binding(0)
var<uniform> uMVPMatrix: mat4x4<f32>;

@group(0) @binding(1)
var<uniform> uTime: f32;

@vertex
fn main(input: VertexInput) -> VertexOutput {
    var output: VertexOutput;
    output.clipPosition = uMVPMatrix * vec4<f32>(input.aPos, 1.0);
    output.vUV = input.aUV;
    output.vTime = uTime;
    return output;
}

3. 结构体与数组对照

概念GLSLWGSL核心差异
结构体定义struct Name { ... };struct Name { ... };相同
结构体成员float value;value: f32,WGSL 用冒号分隔
数组声明float arr[10];var arr: array<f32, 10>;WGSL 用泛型语法
结构体数组Light lights[16];var lights: array<Light, 16>;WGSL 显式泛型
动态数组buffer { float data[]; } ssbo;var<storage> data: array<f32>;WGSL 无需显式长度

结构体与数组示例

// GLSL Fragment Shader
#version 330 core

struct Light {
    vec3 position;
    vec3 color;
    float intensity;
};

layout(std140, binding = 0) uniform LightBlock {
    Light lights[16];
    int lightCount;
};

uniform sampler2D uTexture;

in vec2 vUV;
out vec4 FragColor;

void main() {
    vec4 texColor = texture(uTexture, vUV);
    vec3 totalLight = vec3(0.0);
    
    for (int i = 0; i < lightCount; i++) {
        vec3 lightDir = normalize(lights[i].position - vUV);
        totalLight += lights[i].color * lights[i].intensity * max(0.0, lightDir.z);
    }
    
    FragColor = texColor * vec4(totalLight, 1.0);
}
// WGSL Fragment Shader
struct Light {
    position: vec3<f32>,
    @size(16)  // 显式对齐到 16 字节
    color: vec3<f32>,
    intensity: f32,
}

struct LightBlock {
    lights: array<Light, 16>,
    lightCount: i32,
}

@group(0) @binding(0)
var<uniform> uLights: LightBlock;

@group(1) @binding(0)
var uTexture: texture_2d<f32>;

@group(1) @binding(1)
var uSampler: sampler;

struct FragmentInput {
    @location(0) vUV: vec2<f32>,
};

@fragment
fn main(input: FragmentInput) -> @location(0) vec4<f32> {
    let texColor = textureSample(uTexture, uSampler, input.vUV);
    var totalLight: vec3<f32> = vec3<f32>(0.0);
    
    for (var i: i32 = 0; i < uLights.lightCount; i++) {
        let lightDir: vec3<f32> = normalize(uLights.lights[i].position - vec3<f32>(input.vUV, 0.0));
        totalLight += uLights.lights[i].color * uLights.lights[i].intensity * max(0.0, lightDir.z);
    }
    
    return texColor * vec4<f32>(totalLight, 1.0);
}

4. 内置变量对照

GLSL 内置变量WGSL 内置值用途
gl_Position@builtin(position)Vertex Shader 输出:裁剪空间坐标
gl_FragCoord@builtin(position)Fragment Shader 输入:像素坐标
gl_VertexID@builtin(vertex_index)Vertex Shader 输入:顶点索引
gl_InstanceID@builtin(instance_index)Vertex Shader 输入:实例索引
gl_FrontFacing@builtin(front_facing)Fragment Shader 输入:是否正面朝向
gl_FragDepth@builtin(frag_depth)Fragment Shader 输出:自定义深度值

内置变量使用示例

// GLSL
void main() {
    gl_Position = uMVP * vec4(aPos, 1.0);
}
// WGSL
@vertex
fn main(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
    var output: VertexOutput;
    output.clipPosition = uMVP * vec4<f32>(aPos, 1.0);
    return output;
}

5. 函数定义对照

概念GLSLWGSL核心差异
函数声明float compute(vec3 v);fn compute(v: vec3<f32>) -> f32;WGSL 参数类型显式
函数定义float compute(vec3 v) { return length(v); }fn compute(v: vec3<f32>) -> f32 { return length(v); }WGSL 返回类型显式
入口点void main() { ... }@vertex fn main(...) -> Output { ... }WGSL 需阶段属性
重载float max(float a, float b);fn max(a: f32, b: f32) -> f32;WGSL 函数重载更严格

函数定义示例

// GLSL
vec3 calculateLighting(vec3 normal, vec3 lightDir, vec3 lightColor) {
    float diff = max(dot(normal, lightDir), 0.0);
    return lightColor * diff;
}

void main() {
    vec3 light = calculateLighting(vNormal, uLightDir, uLightColor);
    FragColor = vec4(light, 1.0);
}
// WGSL
fn calculateLighting(normal: vec3<f32>, lightDir: vec3<f32>, lightColor: vec3<f32>) -> vec3<f32> {
    let diff: f32 = max(dot(normal, lightDir), 0.0);
    return lightColor * diff;
}

@fragment
fn main(input: FragmentInput) -> @location(0) vec4<f32> {
    let light: vec3<f32> = calculateLighting(input.vNormal, uLightDir, uLightColor);
    return vec4<f32>(light, 1.0);
}

常见误区与陷阱

1. 以为 WGSL 的 vec3 和 GLSL 的 vec3 完全等价

误区:在 WGSL 中写 vec3 而不加 <f32> 类型参数,以为和 GLSL 一样可以省略。

真相:WGSL 的向量类型是泛型类型,必须显式指定元素类型。vec3 在 WGSL 中是未完整的类型声明,编译器会报错。

正确写法

// 错误
let v: vec3 = vec3(1.0, 2.0, 3.0);  // 编译错误

// 正确
let v: vec3<f32> = vec3<f32>(1.0, 2.0, 3.0);  // 必须显式类型参数

2. 混淆 varlet 的语义

误区:在 WGSL 中到处用 var,以为和 GLSL 的变量声明一样简单。

真相

  • var:表示可变变量,对应 GLSL 的普通变量
  • let:表示不可变变量(类似 JS 的 const),对应 GLSL 的 const
  • const:WGSL 中用于编译时常量(类似 C++ 的 constexpr

正确写法

// 局部可变变量
var counter: i32 = 0;
counter += 1;

// 局部不可变变量
let position: vec3<f32> = vec3<f32>(1.0, 2.0, 3.0);
// position += vec3<f32>(1.0, 0.0, 0.0);  // 编译错误:let 不可变

// 编译时常量
const PI: f32 = 3.14159;

3. 忽视结构体成员的内存对齐

误区:在 WGSL 中定义结构体时直接复制 GLSL 的布局,以为内存布局会自动对齐。

真相:WGSL 要求结构体成员必须显式满足内存对齐规则,否则会导致数据错位。GLSL 的 std140 布局规则与 WGSL 的 @align 规则略有差异。

正确写法

// 错误:可能导致数据错位
struct WrongStruct {
    position: vec3<f32>,  // offset 0, size 12
    radius: f32,          // offset 12, size 4
    color: vec4<f32>,     // offset 16, size 16
}

// 正确:显式对齐
struct CorrectStruct {
    @align(16) position: vec3<f32>,  // 强制 16 字节对齐
    radius: f32,                      // offset 12-15, size 4
    @align(16) color: vec4<f32>,      // 强制 16 字节对齐
}

来源说明:通过 WebSearch 检索 “WebGPU Storage Buffers”(2024 年)确认:“WGSL requires explicit alignment for struct members to avoid data misalignment”(WGSL 需要为结构体成员显式对齐以避免数据错位)[来源:https://webgpufundamentals.org/webgpu/lessons/webgpu-storage-buffers.html]

4. 以为 @group(0) @binding(0) 可以省略

误区:在 WGSL 中省略 @group@binding,以为像 GLSL 一样可以自动推断。

真相:WGSL 要求所有资源变量(uniform、storage、texture、sampler)必须显式指定 @group@binding,这是编译时的强制要求,无法省略。

正确写法

// 错误:缺少 @group 和 @binding
var<uniform> uMVP: mat4x4<f32>;  // 编译错误

// 正确:必须显式指定
@group(0) @binding(0)
var<uniform> uMVP: mat4x4<f32>;

5. 混淆 in/out@location 的作用域

误区:在 WGSL 中试图用 in/out 代替 @location,以为 GLSL 的写法可以直接迁移。

真相

  • GLSL 的 in/out 是阶段间的数据流标识,配合 layout(location=X) 使用
  • WGSL 取消了 in/out 语法,统一使用 struct + @location 传递数据

正确写法

// GLSL
layout(location = 0) in vec3 aPos;
layout(location = 0) out vec3 vNormal;
// WGSL
struct VertexInput {
    @location(0) aPos: vec3<f32>,
};

struct VertexOutput {
    @location(0) vNormal: vec3<f32>,
};

延伸阅读与自测

权威索引

  1. WebGPU Shading Language Specification (W3C): https://www.w3.org/TR/WGSL/ - WGSL 官方规范,包含完整的语法、类型系统和内存模型定义(通过 WebSearch 检索确认,2024 年 7 月)。

  2. OpenGL Shading Language Specification (Khronos): https://registry.khronos.org/OpenGL/specs/gl/GLSLangSpec.4.60.pdf - GLSL 4.6 官方规范,包含所有内置函数和类型定义(通过 WebSearch 检索确认,2024 年 1 月)。

  3. WebGPU Fundamentals - WGSL Tutorial: https://webgpufundamentals.org/webgpu/lessons/webgpu-wgsl.html - 实用的 WGSL 入门教程,包含与 GLSL 的对比示例(通过 WebSearch 检索确认,2024 年 5 月)。

  4. Memory Layout in WGSL | Learn Wgpu: https://sotrh.github.io/learn-wgpu/showcase/alignment/ - WGSL 内存布局和对齐规则的深入讲解(通过 WebSearch 检索确认,2024 年 3 月)。

自测题

  1. 在 WGSL 中,如何声明一个包含 16 个 vec4<f32> 元素的数组,并将其绑定到 @group(1) @binding(2) 的 uniform 缓冲区? 请写出完整的 WGSL 代码。

  2. GLSL 的 layout(std140, binding=0) 与 WGSL 的 @group(0) @binding(0) 在内存布局上有何本质差异? 请结合结构体对齐规则说明。

  3. 为什么 WGSL 不支持隐式类型转换,而 GLSL 支持? 这种设计差异对开发者有何实际影响?

参考答案

  1. WGSL 数组声明与绑定代码
@group(1) @binding(2)
var<uniform> dataArray: array<vec4<f32>, 16>;
  1. 内存布局差异
  • GLSL 的 std140 是隐式布局规则,编译器自动计算对齐,开发者无法手动干预
  • WGSL 要求显式使用 @align@size 指定对齐规则,开发者需要手动确保 CPU 端数据布局与 GPU 端一致
  • 例如:GLSL 的 vec3 后面跟 float 会自动填充到 16 字节边界,而 WGSL 需要显式写 @align(16)
  1. 隐式转换设计差异的影响
  • GLSL 支持隐式转换是为了方便开发者,但容易导致精度丢失和编译器行为不一致
  • WGSL 禁止隐式转换是为了保证类型安全和性能可预测性,避免隐式转换导致的性能开销
  • 实际影响:WGSL 开发者需要显式调用构造函数(如 f32(5)),代码略显冗余但更安全

总结

GLSL 与 WGSL 的语法差异反映了两种语言设计哲学的根本不同:

  • GLSL:C 风格的自由灵活,隐式类型转换、全局变量、宏定义,适合快速开发但容易踩坑
  • WGSL:TS 风格的严格规范,显式类型标注、双层绑定、内存对齐,适合大型项目但学习曲线陡峭

掌握这两种语言的对照表,是理解现代 GPU 渲染技术的基础。在后续章节中,我们将基于这些语法知识深入探讨渲染管线、光照模型和后处理技术。