☰
Tidewater的WGSL着色器组合系统:ShaderModule如何优雅管理数百个着色器
2026/10/13 22:43:22 网站建设 项目流程

【免费下载链接】tidewater

Coastal town built with Opus 5.5

项目地址:https://gitcode.com/gh_mirrors/ti/tidewater
点击查看免费下载

Tidewater 是一款基于 WebGPU 原生构建的浏览器海岛钓鱼游戏,它的整个渲染引擎不使用任何框架,直接以 WGSL 编写着色器。全项目共有近百个ShaderModule实例,海洋、地形、云层、鲸鱼等每个系统都导出可复用的 WGSL 代码块。本文深入讲解 Tidewater 的 WGSL 着色器组合系统,看它如何用"模块拼装"的方式优雅管理数百个着色器,避免绑定声明重复和冲突。

痛点:手写数百个 WebGPU 着色器有多痛苦

在 WebGPU 中,每写一个着色器都要手工完成三件枯燥的事:声明@group(n) @binding(m)变量、创建BindGroupLayout、把纹理/缓冲区塞进BindGroup。更麻烦的是共享代码——"采样高度图求海拔"这类函数,地形材质需要、水面着色器需要、后处理 GTAO 也需要。如果每处复制一份,改一处就要改几十处。

Tidewater 的解法只有一个文件:src/engine/gpu/Shader.js,其中ShaderModule类是整个组合系统的地基。

ShaderModule:一块自带"接口清单"的 WGSL 乐高

ShaderModule的构造参数非常简洁(见 src/engine/gpu/Shader.js#L45-L59):

  • name:模块名,用于日志和调试
  • deps:依赖的其他模块(形成依赖树)
  • code:一段纯 WGSL——函数、结构体、常量
  • bindings:这段代码需要读取的资源(纹理、缓冲区、uniform 块)
  • uniforms:可选,自动声明为var<uniform>的 uniform 块

以地形系统为例,它把高度图、法线图、splat 图全部打包成一个模块(src/world/TerrainGPU.js#L101-L114):

this.module = new ShaderModule( { name: 'terrain', deps: [ commonModule ], uniforms: this.uniforms, bindings: { terrainHeightTex: { texture: this.heightTexture }, terrainNormalTex: { texture: this.normalTexture }, // ... }, code: TERRAIN_WGSL, } );

之后任何着色器只要把terrainModule加进自己的modules列表,就能直接调用terrainHeightAt( xz )这样的函数——代码和它读取纹理的绑定一起被带过去,一次声明,处处可用。

🧩 共享基础工具也这样组织:噪声、哈希、深度重建函数全部收在 src/engine/render/wgsl/common.js 的commonModule里,被几乎所有系统依赖。

依赖树自动去重与排序

真正巧妙的地方在collectModules函数(src/engine/gpu/Shader.js#L68-L84):它做深度优先遍历,先访问依赖,再访问自身,并用Set去重。也就是说,即使十个系统都(直接或间接)依赖commonModule,它生成的 WGSL 代码在最终着色器里也只出现一次。模块之间无需关心"谁先谁后",依赖顺序由系统自动推导。

三组 Bind Group:组合系统的隐藏骨架

Tidewater 给所有着色器规定了统一的绑定分组(见 src/engine/gpu/Shader.js#L23-L29 的注释):

分组内容特点
group 0frame帧 uniform(相机、时间、太阳方向)+ 9 个共享采样器全局唯一,所有着色器共用同一套
group 1各模块 + 材质自己的资源自动组合,本系统的核心
group 2每次绘制(per-draw)数据仅渲染管线使用

group0按渲染阶段懒加载并缓存(src/engine/gpu/Shader.js#L480-L493),而 9 个共享采样器(线性/各向异性/最近邻/阴影比较)让所有纹理共享少量 sampler 句柄,绕开了"每纹理一个采样器"的经典浪费。

BindingSet:绑定组的自动缓存

BindingSet(src/engine/gpu/Shader.js#L323-L458)把"声明 WGSL 变量"和"创建运行时 BindGroup"两件事合并了:

  • describe()根据 spec 自动生成 WGSL 声明——uniform 块、storage 缓冲区、采样纹理、存储纹理、sampler 五种类型全部覆盖
  • declarations()直接吐出@group(1) @binding(i)声明拼进源码
  • getBindGroup()带一个 4 槽小缓存:帧间资源没变(对比的是对象引用 + 版本号,而非字符串)就直接复用旧的BindGroup,避免了逐帧重建的开销

对于 ping-pong 交替的历史纹理(时域抗锯齿、TAA 那一类),这个缓存正好覆盖两种组合,非常务实。

迷你预处理器:让 WGSL 也能写条件分支

WGSL 本身没有 C 那样的#ifdef。Tidewater 在 src/engine/gpu/Shader.js#L88-L173 实现了preprocess(),支持#if、#ifdef、#ifndef、#elif、#else,还能比较数值(#if NAME == value)和&&/||逻辑组合。

define 从材质或核的defines字段传入,这让同一份着色器源码可以在"带/不带水下光照""不同 LOD 等级"等配置间切换,而不必维护多份拷贝——materialSurface这类大模板里的分支全靠它。

自动应对 GPU 的"预算"限制

WebGPU 对每个着色阶段有硬性预算(如每阶段 12 个 uniform 缓冲区)。组合系统的危险在于:模块堆多了,uniform 块可能悄悄超预算、编译直接失败。

composeShader在拼装时自动兜底(src/engine/gpu/Shader.js#L533-L576):

  1. 按阶段划分绑定——用reachableIdentifiers做函数调用图分析(src/engine/gpu/Shader.js#L598-L642),找出顶点着色器根本用不到的绑定,把可见性收窄到 fragment 阶段,省下 uniform 名额
  2. 降级(demote)——如果 uniform 块仍超预算,把超出的块自动改绑为只读 storage 缓冲区(结构布局不变,WGSL 声明同步改写)

作者基本无需为"这个组合会不会超限"操心,组合系统替他们管了。

编译错误也能"对号入座"

createShaderModule(src/engine/gpu/Shader.js#L654-L676)做了两件贴心事:按源码文本缓存GPUShaderModule(同源不重复创建);异步拉取getCompilationInfo,一旦有错误,就打印行号 + 带>标记的源码片段:

WGSL error in terrain ( 87:3 ): expected ';', found identifier > let x = terrainHeightAt( xz )

调试几百行拼装出来的着色器时,这比一坨原始日志救命多了。

三个入口:Material、ComputeKernel、FullscreenPass

组合系统最终通过三个入口被使用,写法高度一致:

入口文件场景
Materialsrc/engine/render/Material.js网格材质:modules+vertex/surface片段
ComputeKernelsrc/engine/gpu/Compute.js#L21-L41FFT 海洋、泡沫、尾迹等 compute 核
FullscreenPasssrc/engine/render/FullscreenPass.js后处理全屏 pass

三者都调用同一个composeShader(src/engine/gpu/Shader.js#L519-L595):收集模块 → 合并绑定 → 拼上结构体与 group 0/1 声明 → 预处理器求值 → 产出最终 WGSL 源码和两个BindingSet。渲染端还会自动按 pass 类型决定要不要插入光照模块与钩子模块(src/engine/render/MeshShader.js#L371-L381),比如深度 pass 就不带光照,省得白白占用 uniform 预算。

项目还有一套严格的模块命名规范(terrainHeightAt、oceanParams、cloudsShadow……统一前缀),见 docs/PORTING.md,保证跨系统调用不重名、可读性好。

小结:组合优于继承

Tidewater 管理数百个着色器的思路可以概括为三句话:

  1. 系统即模块:每个子系统导出一个ShaderModule,函数、资源、依赖一起打包
  2. 声明即绑定:describe()从同一份 spec 同时生成 WGSL 声明和运行时 BindGroup,两边永不脱节
  3. 预算交给系统:依赖去重、阶段划分、uniform 降级、BindGroup 缓存全自动

这套纯 WebGPU 原生的设计没有引入一行框架代码,却拿到了引擎级的复用能力和稳定性——对想在浏览器里写大型渲染项目的开发者来说,src/engine/ 目录值得一读。

【免费下载链接】tidewater

Coastal town built with Opus 5.5

项目地址:https://gitcode.com/gh_mirrors/ti/tidewater
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询