三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

游戏开发中的物理模拟:如何用梯度、散度和拉普拉斯算子模拟水流与烟雾?

游戏开发中的物理模拟:如何用梯度、散度和拉普拉斯算子模拟水流与烟雾?

游戏物理模拟实战:用梯度、散度与拉普拉斯算子打造动态流体效果

在《塞尔达传说:王国之泪》的瀑布飞溅与《原神》的晨雾弥漫背后,隐藏着三个改变游戏视觉效果的关键数学工具——梯度、散度和拉普拉斯算子。这些来自矢量分析的算子,正以差分形式活跃在现代游戏引擎的着色器代码中,将枯燥的偏微分方程转化为令人屏息的动态画面。本文将用WebGL着色器代码和Unity示例,揭示如何用这些工具构建实时2D流体系统。

1. 从数学到像素:理解基础算子

1.1 梯度场:流体的动力之源

在Shader中,梯度表现为相邻像素的亮度差值。以下GLSL代码计算高度场的梯度:

vec2 gradient(sampler2D heightMap, vec2 uv, float pixelSize) { float hL = texture(heightMap, uv - vec2(pixelSize, 0.0)).r; float hR = texture(heightMap, uv + vec2(pixelSize, 0.0)).r; float hD = texture(heightMap, uv - vec2(0.0, pixelSize)).r; float hU = texture(heightMap, uv + vec2(0.0, pixelSize)).r; return vec2(hR - hL, hU - hD) / (2.0 * pixelSize); }

这个简单的中心差分实现,可以驱动水面波纹传播。在《深海迷航》中,类似的梯度计算被用于模拟海底洋流对植被的影响。

1.2 散度检测:控制流体进出

散度计算是流体模拟的"守门人"。以下是在Unity ComputeShader中实现的散度检测:

[numthreads(8,8,1)] void ComputeDivergence (uint3 id : SV_DispatchThreadID) { float2 velocityL = VelocityBuffer[id.xy - uint2(1,0)]; float2 velocityR = VelocityBuffer[id.xy + uint2(1,0)]; float2 velocityD = VelocityBuffer[id.xy - uint2(0,1)]; float2 velocityU = VelocityBuffer[id.xy + uint2(0,1)]; DivergenceBuffer[id.xy] = 0.5 * ( (velocityR.x - velocityL.x) + (velocityU.y - velocityD.y) ); }

《城市:天际线》的水循环系统就依赖这类计算来模拟排水管和蒸发效应。

1.3 拉普拉斯算子:自然的平滑之手

烟雾扩散的核心是拉普拉斯算子。这个片段着色器代码展示了染料扩散:

vec4 diffuse(sampler2D tex, vec2 uv, vec2 pixelSize, float dt) { vec4 center = texture(tex, uv); vec4 up = texture(tex, uv + vec2(0, pixelSize.y)); vec4 down = texture(tex, uv - vec2(0, pixelSize.y)); vec4 left = texture(tex, uv - vec2(pixelSize.x, 0)); vec4 right = texture(tex, uv + vec2(pixelSize.x, 0)); return center + dt * (up + down + left + right - 4.0 * center); }

《战地》系列的爆炸烟雾效果正是基于这种原理的增强版本。

2. 构建2D流体模拟系统

2.1 速度场与压力场的舞蹈

完整的流体模拟需要解Navier-Stokes方程。以下是简化后的计算步骤:

  1. 平流阶段:用半拉格朗日法搬运速度
  2. 施加外力:添加用户交互或重力
  3. 计算散度:检测速度场的压缩区域
  4. 压力求解:用Jacobi迭代解泊松方程
  5. 速度修正:用压力梯度更新速度
// 简化版模拟循环示例 for (int iter = 0; iter < steps; ++iter) { advectVelocity(); applyForces(); computeDivergence(); solvePressure(20); // 20次迭代 subtractPressureGradient(); }

2.2 性能优化技巧

实时模拟需要权衡精度与性能:

优化技术性能提升视觉质量影响
多重网格法5-10x几乎无损
时间步长自适应2-3x轻微失真
16位浮点纹理1.5x可忽略
降低分辨率+升采样3-4x边缘模糊

《DOTA2》的水面特效就采用了16位浮点纹理加时间重映射的组合方案。

3. 视觉效果的魔法加工

3.1 从数据到画面

将物理量转化为视觉效果需要创意加工:

  • 速度场可视化:流线粒子或条纹图
  • 密度场渲染:光线步进体积雾
  • 涡度增强:用旋度计算添加细节
// 涡度限制计算示例 float3 curl = float3( (velocityU.z - velocityD.z) - (velocityF.y - velocityB.y), (velocityF.x - velocityB.x) - (velocityR.z - velocityL.z), (velocityR.y - velocityL.y) - (velocityU.x - velocityD.x) ); vorticityForce = 0.5 * length(curl) * normalize(cross(float3(0,1,0), curl));

《死亡搁浅》的时雨效果就运用了涡度增强技术。

3.2 美术可控参数

给美术师提供这些调节参数:

  • 粘度:控制流体"粘稠度"
  • 扩散率:影响烟雾散开速度
  • 外力衰减:决定交互持续时间
  • 颜色梯度:映射密度到颜色
{ "fluid_params": { "viscosity": 0.001, "diffusion": 0.005, "force_falloff": 0.95, "color_gradient": "fire" } }

4. 现代引擎中的实现路径

4.1 Unity实现方案

使用Compute Shader构建模拟:

  1. 创建RWTexture2D作为模拟缓冲区
  2. 分派ComputeShader线程组
  3. 用MaterialPropertyBlock传递参数
  4. 在后期处理中渲染结果
void UpdateSimulation() { int threadGroupsX = Mathf.CeilToInt(simWidth / 8.0f); int threadGroupsY = Mathf.CeilToInt(simHeight / 8.0f); computeShader.Dispatch(kernelAdvect, threadGroupsX, threadGroupsY, 1); // 其他计算阶段... }

4.2 Unreal引擎方案

利用RenderTarget和Material:

  1. 设置RenderTarget链实现乒乓缓冲
  2. 通过Custom节点编写HLSL代码
  3. 使用蓝图控制模拟流程
  4. 结合Niagara系统驱动粒子

4.3 WebGL轻量级方案

基于Three.js的实现要点:

const fluidShader = { uniforms: { "velocityTexture": { value: null }, "deltaTime": { value: 1.0/60.0 } }, fragmentShader: ` uniform sampler2D velocityTexture; uniform float deltaTime; void main() { // 平流计算代码... } ` };

5. 实战案例:交互式烟雾模拟

在Houdini中构建原型后,移植到游戏引擎的步骤:

  1. 场数据准备:将模拟烘焙为纹理序列
  2. 实时参数映射:连接风力、温度等输入
  3. 交互响应:处理玩家输入位置
  4. LOD策略:根据距离切换模拟精度
# Houdini场数据导出示例 node = hou.pwd() geo = node.geometry() # 将速度场写入纹理 for pt in geo.points(): vel = pt.attribValue("v") write_to_texture(vel, frame=hou.frame())

《幽灵线:东京》的邪气效果就采用了类似的离线+实时混合方案。

← 返回列表