静态效果

项目文件组成结构如下:
├── LICENSE.md
├── makePG.py
├── OceanDemo-main.zip
├── package.json
├── README.md
├── src
│ ├── assets
│ │ ├── environment
│ │ │ └── qwantani_1k.hdr
│ │ └── ocean
│ │ ├── 00_noise0.exr
│ │ ├── babylonBuoy.glb
│ │ ├── buoy.glb
│ │ ├── fftInverseFFT.wgsl
│ │ ├── fftInverseFFT2.wgsl
│ │ ├── fftInverseFFT3.wgsl
│ │ ├── fftPrecompute.wgsl
│ │ ├── fisher_boat.glb
│ │ ├── foam1.jpg
│ │ ├── initialSpectrum.wgsl
│ │ ├── initialSpectrum2.wgsl
│ │ ├── ocean0.jpg
│ │ ├── ocean1.jpg
│ │ ├── ocean2.jpg
│ │ ├── ocean3.jpg
│ │ ├── timeDependentSpectrum.wgsl
│ │ └── wavesTexturesMerger.wgsl
│ ├── createScene.ts
│ ├── externalFileTypes.d.ts
│ ├── index.html
│ ├── index.ts
│ └── scenes
│ ├── buoyancy.ts
│ ├── fft.ts
│ ├── initialSpectrum.ts
│ ├── ocean.ts
│ ├── oceanGeometry.ts
│ ├── oceanGui.ts
│ ├── oceanMaterial.ts
│ ├── skyBox.ts
│ ├── tools
│ │ ├── computeHelper.ts
│ │ ├── exrSerializer.ts
│ │ ├── math.vector.float32.ts
│ │ ├── RTTDebug.ts
│ │ └── skyMaterialExt.ts
│ ├── wavesCascade.ts
│ ├── wavesGenerator.ts
│ └── wavesSettings.ts
├── tsconfig.json
└── webpack.config.js
第一步:入口与架构(搞清楚它是怎么跑起来的)
先看外壳,弄明白这些 TypeScript 文件是怎么组织到一起的。
-
src/index.ts:这是整个应用的"点火装置"。你已经看过了,它负责初始化WebGPUEngine。 -
src/createScene.ts:这是一个分发器。这个 Demo 显然支持不同的场景(比如纯海洋、浮力模拟等),它通过URL参数决定加载哪个模块。 -
src/scenes/ocean.ts:这是你最该仔细读的文件。 它把天空盒、船只模型、海洋几何体以及最重要的"波动生成器"串联在一起。
第二步:核心逻辑(学习物理是怎么变成画面的)
这是项目的灵魂,建议按以下逻辑链路去读:
-
src/scenes/wavesSettings.ts:看这里。了解风速、海浪强度等参数是如何定义的。 -
src/scenes/wavesGenerator.ts:重头戏! 这里负责调度 GPU 计算。它会调用computeHelper.ts来运行那些.wgsl(计算着色器)。 -
src/scenes/oceanMaterial.ts:这里定义了你看到的海洋"长什么样"。它负责处理光照、折射、反射以及读取 GPU 计算出来的位移贴图。
第三步:硬核算法(进阶深水区)
如果你想弄明白为什么这个海浪这么真实,就得看 src/assets/ocean/ 下的 .wgsl 文件。这是 Web 开发者最高阶的技能之一。
-
initialSpectrum.wgsl:生成初始的海浪频谱(通常基于 Phillips 频谱公式)。 -
fftInverseFFT.wgsl:核心算法。利用快速傅里叶逆变换,把频率域的数据转回我们肉眼能看到的波动(高度场)。 -
wavesTexturesMerger.wgsl:把计算好的位移、法线、泡沫数据合并成最终的贴图。
第四步: 学习路径
| 阶段 | 文件 | 目标 |
|---|---|---|
| 1. 跑通流程 | index.ts -> ocean.ts |
搞清楚一个 WebGPU 场景从创建到渲染的生命周期。 |
| 2. 操控 UI | oceanGui.ts |
修改代码,看看调节风速(Wind Speed)时,哪些变量被传给了着色器。 |
| 3. 材质魔法 | oceanMaterial.ts |
尝试修改 oceanMaterial 里的颜色或反射率,理解着色器是如何渲染水面的。 |
| 4. 物理模拟 | wavesGenerator.ts |
理解 ComputeShader 是如何每一帧都在后台更新海浪形状的。 |
第四步:程序运行路径
从你双击 npm run dev 到屏幕上出现第一朵浪花,拆解出一条清晰的运行路径。
(1)初始化阶段:点火与引擎挂载
一切的起点在 src/index.ts。
-
浏览器握手 :程序首先通过
WebGPUEngine.IsSupportedAsync检查你的浏览器(Canary 版本)是否支持 WebGPU。 -
引擎启动 :创建一个
WebGPUEngine实例,并配置deviceDescriptor。这里非常关键,它申请了timestamp-query和indirect-first-instance等高级特性,这是跑计算着色器(Compute Shader)的前提。 -
模块加载 :通过
getSceneModuleWithName动态加载src/scenes/ocean.ts(默认场景)。
(2)场景构建阶段:资源与逻辑组装
当程序进入 src/scenes/ocean.ts 的 createScene 函数时,它开始疯狂"搭积木":
A. 环境配置
-
调用
skyBox.ts生成天空盒。 -
加载
src/assets/environment/qwantani_1k.hdr。这不只是背景,它为海洋提供了 IBL (基于图像的光照),决定了海水的环境反射。
B. 核心组件实例化(最关键的路径)
程序在这里初始化了两个重量级对象:
-
WavesGenerator(src/scenes/wavesGenerator.ts):这是"大脑",负责所有物理计算。 -
OceanMaterial(src/scenes/oceanMaterial.ts):这是"皮肤",负责最终的视觉呈现。
(3)渲染循环阶段:数据的"生死时速"
一旦执行 engine.runRenderLoop,每一帧(约 16ms)都会经历以下闭环:
步骤一:频谱生成(Initial & Time-Dependent)
-
文件路径 :
initialSpectrum.ts->initialSpectrum.wgsl -
逻辑:GPU 根据风速、海浪强度(Settings),在频率域生成初始的海浪分布。它不是生成形状,而是生成"波的能量密度"。
步骤二:逆傅里叶变换(IFFT)
-
文件路径 :
wavesGenerator.ts->fftInverseFFT.wgsl -
逻辑 :这是项目最硬核的地方。GPU 将频率域的数据通过 IFFT 转换回空间域。
-
产出 :生成几张关键的 RTT (渲染目标贴图)。
-
Displacement Map:告诉顶点网格往哪里跳(高度和水平位移)。
-
Normal Map:决定光的折射和反射角度。
-
Foam Map:计算波浪破碎处的泡沫。
-
步骤三:材质着色(Fragment Shading)
-
文件路径 :
oceanMaterial.ts -
逻辑:
-
顶点偏移 :读取 Displacement Map,把平整的
oceanGeometry顶起来,变成起伏的海浪。 -
像素着色 :结合天空盒反射、HDR 环境光,以及
mix-blend-mode: screen等逻辑,计算出深蓝到浅绿的渐变。
-
(4) 交互阶段:反馈与浮力
-
buoyancy.ts:如果你在场景里放了那只鸭子或小船(.glb模型),程序会实时采样海浪的高度数据。 -
逻辑 :它读取当前时刻坐标下的海浪高度,通过
buoyancy.ts计算浮力,实时修正模型的position和rotation,让船看起来真的漂在水上。