ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

WebGPU 计算着色器驱动流体仿真:基于 Navier-Stokes 方程手写网格烟雾效果

WebGPU 计算着色器驱动流体仿真:基于 Navier-Stokes 方程手写网格烟雾效果 WebGPU 计算着色器驱动流体仿真基于 Navier-Stokes 方程手写网格烟雾效果在 Web 交互设计中流体动力学Fluid Simulation——如随鼠标轨迹翻滚的数字烟雾、流光水波与液体水墨——向来是打造顶级视觉沉浸感的终极圣杯。然而在过去的 WebGL 时代要在浏览器端跑通一套稳定的流体模拟前端开发者必须编写极其扭曲的代码建立五六个独立的浮点数帧缓冲Float FBO在顶点着色器和片元着色器之间反复切换纹理附件还要在 CPU 侧处理复杂的 Ping-Pong 状态机性能稍有不慎就会导致页面瞬间掉帧卡死。WebGPU 的横空出世彻底重写了物理可视化的底层逻辑。通过 WGSLWebGPU Shading Language的计算着色器Compute Shader与存储纹理Storage Texture我们能够直接把流体力学中最核心的偏微分方程——不可压缩纳维-斯托克斯方程Incompressible Navier-Stokes Equations以并行的网格计算流水线完整搬进 GPU 片上。让用户每一次轻微的鼠标滑过都能在屏幕上掀起如丝绸般细腻翻滚的物理级烟雾漩涡。一、不可压缩流体力学的四大离散求解步骤流体的宏观运动遵循著名的纳维-斯托克斯动量守恒方程$$\frac{\partial \vec{u}}{\partial t} -(\vec{u} \cdot \nabla)\vec{u} - \frac{1}{\rho}\nabla p \nu \nabla^2 \vec{u} \vec{f}$$配合不可压缩性约束速度场散度为零$$\nabla \cdot \vec{u} 0$$在计算机欧拉网格Eulerian Grid上实时求解该方程图形学大师 Jos Stam 提出的“稳定流体算法Stable Fluids”将每一帧的推演严格拆分为四个阶段外力注入Add Forces, $\vec{f}$将用户的鼠标拖拽位移向量注入当前网格单元作为外力速度同时注入对应颜色的烟雾染料浓度。平流对流步Advection流体自身的速度场推动染料和速度场自身前进。为了防止数值发散采用半拉格朗日法Semi-Lagrangian Method从当前网格点沿速度反方向回溯Backtrace时间 $\Delta t$ 找到粒子出发点通过双线性插值获得新值。粘性扩散步Diffusion, $\nu \nabla^2 \vec{u}$流体分子间的粘滞摩擦力。通过雅可比迭代Jacobi Iteration求解扩散泊松方程。对于烟雾这类低粘度流体该步可适当简化。压力投影步Projection / Divergence-free整个流体模拟最关键的核心经过对流后的速度场会产生散度Divergence即某些网格流进的水比流出的多破坏了“不可压缩”法则。我们必须先求解速度散度场通过 20 次以上的雅可比迭代解出压力标量场 $p$最后从速度场中减去压力梯度 $\nabla p$强行将速度场投影到完全无散度$\nabla \cdot \vec{u} 0$的物理稳定状态。二、手写 WGSL 压力泊松求解与对流计算着色器在 WebGPU 中速度场用 RG 16 位浮点存储纹理表征染料密度用 RGBA 纹理表征。看核心的压力泊松方程雅可比迭代 WGSL 计算着色器实现// WGSL 压力泊松求解器jacobi_pressure.wgsl group(0) binding(0) var pressureRead : texture_2df32; group(0) binding(1) var divergenceRead : texture_2df32; group(0) binding(2) var pressureWrite : texture_storage_2dr16float, write; compute workgroup_size(16, 16) fn main(builtin(global_invocation_id) id : vec3u32) { let coords vec2i32(id.xy); let dims vec2i32(textureDimensions(pressureRead)); if (coords.x dims.x || coords.y dims.y) { return; } // 1. 采样上下左右四个邻域网格的压力标量 (边界夹取) let pL textureLoad(pressureRead, clamp(coords vec2i32(-1, 0), vec2i32(0), dims - vec2i32(1)), 0).r; let pR textureLoad(pressureRead, clamp(coords vec2i32(1, 0), vec2i32(0), dims - vec2i32(1)), 0).r; let pB textureLoad(pressureRead, clamp(coords vec2i32(0, -1), vec2i32(0), dims - vec2i32(1)), 0).r; let pT textureLoad(pressureRead, clamp(coords vec2i32(0, 1), vec2i32(0), dims - vec2i32(1)), 0).r; // 2. 采样当前网格散度 (Divergence) let div textureLoad(divergenceRead, coords, 0).r; // 3. 雅可比迭代离散公式中心压力等于周围邻域压力之和减去散度后求均值 let newPressure (pL pR pB pT - div) * 0.25; // 4. 就地写入下一轮的压力存储纹理 textureStore(pressureWrite, coords, vec4f32(newPressure, 0.0, 0.0, 0.0)); }紧接着是半拉格朗日对流通道Advection Pass// WGSL 半拉格朗日对流通道advect.wgsl group(0) binding(0) var velocityTex : texture_2df32; group(0) binding(1) var sourceQuantity : texture_2df32; // 染料或速度场 group(0) binding(2) var texSampler : sampler; group(0) binding(3) var outputTex : texture_storage_2drgba16float, write; struct Uniforms { deltaTime : f32, gridResolution : vec2f32, decayRate : f32, }; group(0) binding(4) varuniform u : Uniforms; compute workgroup_size(16, 16) fn main(builtin(global_invocation_id) id : vec3u32) { let coords vec2i32(id.xy); if (coords.x i32(u.gridResolution.x) || coords.y i32(u.gridResolution.y)) { return; } // 1. 提取当前单元速度 let vel textureLoad(velocityTex, coords, 0).xy; // 2. 反向追溯粒子出发位置 let pos vec2f32(coords) vec2f32(0.5); let backtrackedPos pos - vel * u.deltaTime; // 3. 归一化至 UV 坐标 [0, 1] 并利用硬件双线性采样器获得高精度平滑色彩 let uv backtrackedPos / u.gridResolution; let advectedValue textureSampleLevel(sourceQuantity, texSampler, uv, 0.0); // 4. 叠加轻微时间耗散写入输出 textureStore(outputTex, coords, advectedValue * u.decayRate); }三、WebGPU 管线编排与鼠标力场交互集成在 TypeScript 宿主代码中我们监听用户的鼠标移动并在后台连续调度 Compute Pass 执行物理步进export class FluidSimulationEngine { private device: GPUDevice; private jacobiPipeline!: GPUComputePipeline; private advectPipeline!: GPUComputePipeline; private gridWidth 256; // 256x256 的物理网格足以呈现极其惊艳的涡流细节 private gridHeight 256; constructor(device: GPUDevice) { this.device device; } public step(commandEncoder: GPUCommandEncoder, dt: number): void { // 1. 调度平流通道 (Advect Velocity Density) // 2. 调度散度计算通道 (Compute Divergence) // 3. 循环 20 次调度雅可比压力迭代通道强制压力收敛 for (let i 0; i 20; i) { const jacobiPass commandEncoder.beginComputePass({ label: Jacobi Iteration ${i} }); jacobiPass.setPipeline(this.jacobiPipeline); // 切换绑定组 (Ping-Pong) jacobiPass.dispatchWorkgroups(this.gridWidth / 16, this.gridHeight / 16); jacobiPass.end(); } // 4. 调度投影减速梯度通道 (Subtract Gradient) } }四、生产避坑与性能压榨铁律浮点纹理线性滤波支持Float32 Filterable Limit在对流通道中我们使用了textureSampleLevel对浮点纹理进行双线性插值。WebGPU 规范中r32float或rgba32float默认并不保证支持硬件线性滤波必须在请求适配器时显式检查并请求特性requiredFeatures: [float32-filterable]或者在生产中安全选用全平台标配的半精度浮点格式rgba16float。数值耗散Numerical Dissipation与网格分辨率平衡半拉格朗日方法天生带有数值耗散网格分辨率如果太低如低于 64×64漩涡在转动半圈后就会迅速抹平成一团浑浊。在移动端将流体模拟网格锁定在128×128 到 256×256之间而在最终渲染呈现时配合双线性放大到全屏是在算力与流体涡流锐度之间的黄金平衡点。把偏微分方程交给着色器在数字屏幕上复现大自然的流体律动这正是 Web 极客探索视觉极限的最高浪漫。
返回列表