【免费下载链接】tidewater
Coastal town built with Opus 5.5
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 0 | frame帧 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):
- 按阶段划分绑定——用
reachableIdentifiers做函数调用图分析(src/engine/gpu/Shader.js#L598-L642),找出顶点着色器根本用不到的绑定,把可见性收窄到 fragment 阶段,省下 uniform 名额 - 降级(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
组合系统最终通过三个入口被使用,写法高度一致:
| 入口 | 文件 | 场景 |
|---|---|---|
Material | src/engine/render/Material.js | 网格材质:modules+vertex/surface片段 |
ComputeKernel | src/engine/gpu/Compute.js#L21-L41 | FFT 海洋、泡沫、尾迹等 compute 核 |
FullscreenPass | src/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 管理数百个着色器的思路可以概括为三句话:
- 系统即模块:每个子系统导出一个
ShaderModule,函数、资源、依赖一起打包 - 声明即绑定:
describe()从同一份 spec 同时生成 WGSL 声明和运行时 BindGroup,两边永不脱节 - 预算交给系统:依赖去重、阶段划分、uniform 降级、BindGroup 缓存全自动
这套纯 WebGPU 原生的设计没有引入一行框架代码,却拿到了引擎级的复用能力和稳定性——对想在浏览器里写大型渲染项目的开发者来说,src/engine/ 目录值得一读。
【免费下载链接】tidewater
Coastal town built with Opus 5.5
相关推荐
react-native-skia 混合着色器指南:用 Blend 与 ColorShader 组合着色效果
react native skia 混合着色器指南:用 Blend 与 ColorShader 组合着色效果 本篇指南基于 react native skia
图形学移动开发跨平台UI组件ants源码解析(三):worker队列双实现——栈与环形队列、PreAlloc预分配的性能秘密
ants源码解析(三):worker队列双实现——栈与环形队列、PreAlloc预分配的性能秘密 🐜 作为 Go 语言最流行的 goroutine 池开源库,
并发编程后端WebGL 着色器与 GLSL 完全指南:顶点着色器、片段着色器与着色器语言详解
WebGL 着色器与 GLSL 完全指南:顶点着色器、片段着色器与着色器语言详解 本文是 WebGL 基础 https://link.gitcode.com/i
图形学
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考