当前位置: 首页 > news >正文

Unity URP 下的流体模拟 深入解析 Navier-Stokes 方程与浅水方程的数学原理

引言

流体模拟是计算机图形学中最具挑战性的领域之一。从海洋的波涛汹涌到杯中咖啡的涟漪,流体的行为遵循着复杂的物理规律。在游戏开发和实时渲染中,我们需要在物理准确性和计算效率之间找到平衡。

本文将深入探讨两种核心的流体模拟方法:Navier-Stokes 方程(NS方程)用于描述完整的流体动力学,以及浅水方程(Shallow Water Equations)用于高效的表面流体模拟。我们将从数学原理出发,逐步深入到 Unity URP 中的实际实现。

Navier-Stokes 方程

Navier-Stokes 方程是描述粘性流体运动的基本方程组,由法国工程师 Navier 和英国数学家 Stokes 在19世纪提出。这组方程基于质量守恒、动量守恒和能量守恒三大物理定律。

数学原理

NS方程包含两个核心方程:连续性方程(质量守恒)和动量方程。

连续性方程(不可压缩流体)

∇ · u = 0

表示流体微元的体积保持不变

动量方程

∂u/∂t + (u · ∇)u = -∇p/ρ + ν∇²u + f

描述流体速度随时间的变化

符号说明:
u= 速度场向量 (u, v, w)
p= 压力场
ρ= 流体密度
ν= 运动粘度系数
f= 外力项(如重力)

方程组成解析

1. 对流项 (u · ∇)u

描述流体微元随流动而产生的速度变化。这是 NS 方程中非线性的来源,也是数值求解的主要难点。

2. 压力项 -∇p/ρ

压力梯度驱动流体从高压区流向低压区。在不可压缩流体中,压力起到拉格朗日乘子的作用,确保速度场无散度。

3. 粘性项 ν∇²u

描述流体内部的摩擦效应,使速度趋于平滑。粘度系数 ν 越大,流体越"粘稠"。

4. 外力项 f

通常包含重力、浮力、表面张力等外部作用力。在游戏开发中,也常用于添加交互效果。

数值求解方法

在实时渲染中,我们使用有限差分法在网格上离散化 NS 方程。以下是基于 Jos Stam 的 Stable Fluids 算法的简化实现:

// 第1步:添加外力(如鼠标交互、重力) AddForces(velocityField, dt); // 第2步:对流(使用半拉格朗日方法) Advect(velocityField, velocityField, dt); // 第3步:粘性扩散 Diffuse(velocityField, viscosity, dt); // 第4步:投影步骤(确保无散度) Project(velocityField, pressureField); // 第5步:密度/颜色平流 Advect(densityField, velocityField, dt);

浅水方程

浅水方程是 NS 方程在特定条件下的简化形式。当流体的水平尺度远大于垂直尺度时(如海洋、湖泊、河流),可以假设流体在垂直方向上的压力分布是静水压力,从而将三维问题简化为二维问题。

数学推导

连续性方程

∂h/∂t + ∇ · (hu) = 0

动量方程

∂(hu)/∂t + ∇ · (hu⊗u) = -gh∇h + νh∇²u + hf

浅水方程特有符号:
h= 水深(水面高度)
u= 水平速度场 (u, v)
g= 重力加速度
= 张量积

优势与应用场景

计算效率
  • • 2D网格 vs 3D网格,内存占用大幅减少
  • • 时间步长可以更大(受CFL条件限制更宽松)
  • • 适合大规模水体(海洋、湖泊)
适用场景
  • • 开放世界游戏中的海洋系统
  • • 河流、瀑布等表面水体
  • • 需要与地形交互的洪水模拟
局限性
  • • 无法模拟垂直方向的运动(如漩涡、水花飞溅)
  • • 不适用于深水区域或快速变化的流体
  • • 需要配合粒子系统处理破碎波和泡沫

波动方程形式

在小振幅假设下,浅水方程可以进一步简化为经典的波动方程:

∂²h/∂t² = c²∇²h

其中 c = √(gh) 是波速

这种形式特别适合使用频谱方法(如FFT)求解,可以高效模拟大面积水面的波动效果。

Unity URP 实现

Unity 的 Universal Render Pipeline (URP) 提供了现代化的渲染架构,结合 Compute Shader 的强大计算能力,可以实现高效的流体模拟。

Compute Shader 实现

Compute Shader 允许我们在 GPU 上进行通用计算,非常适合流体模拟这类并行计算密集型任务。

FluidCompute.compute - 浅水方程求解

#pragma kernel UpdateHeight #pragma kernel UpdateVelocity RWTexture2D<float> _HeightField; RWTexture2D<float2> _VelocityField; float _DeltaTime; float _GridSize; float _Gravity; [numthreads(8, 8, 1)] void UpdateHeight(uint3 id : SV_DispatchThreadID) { uint2 pos = id.xy; uint2 size; _HeightField.GetDimensions(size.x, size.y); // 边界检查 if (pos.x >= size.x || pos.y >= size.y) return; // 获取当前格子的速度 float2 vel = _VelocityField[pos]; // 计算高度梯度(连续性方程) float hL = _HeightField[clamp(pos - int2(1, 0), 0, size - 1)]; float hR = _HeightField[clamp(pos + int2(1, 0), 0, size - 1)]; float hB = _HeightField[clamp(pos - int2(0, 1), 0, size - 1)]; float hT = _HeightField[clamp(pos + int2(0, 1), 0, size - 1)]; // 更新高度 float divergence = ((hR - hL) * vel.x + (hT - hB) * vel.y) / (2.0 * _GridSize); float newHeight = _HeightField[pos] - _DeltaTime * divergence; _HeightField[pos] = max(newHeight, 0.0); }

FluidCompute.compute - 速度更新

[numthreads(8, 8, 1)] void UpdateVelocity(uint3 id : SV_DispatchThreadID) { uint2 pos = id.xy; uint2 size; _VelocityField.GetDimensions(size.x, size.y); if (pos.x >= size.x || pos.y >= size.y) return; float h = _HeightField[pos]; if (h < 0.01) return; // 避免除零 float2 vel = _VelocityField[pos]; // 计算压力梯度(重力驱动) float hL = _HeightField[clamp(pos - int2(1, 0), 0, size - 1)]; float hR = _HeightField[clamp(pos + int2(1, 0), 0, size - 1)]; float hB = _HeightField[clamp(pos - int2(0, 1), 0, size - 1)]; float hT = _HeightField[clamp(pos + int2(0, 1), 0, size - 1)]; float2 pressureGrad; pressureGrad.x = -(hR - hL) * _Gravity / (2.0 * _GridSize); pressureGrad.y = -(hT - hB) * _Gravity / (2.0 * _GridSize); // 更新速度(简化欧拉积分) float2 newVel = vel + _DeltaTime * pressureGrad; // 阻尼 newVel *= 0.995; _VelocityField[pos] = newVel; }

可视化渲染

模拟完成后,我们需要将高度场和速度场转换为可视化的水面。URP 提供了灵活的 Shader Graph 和 HLSL Shader 选项。

WaterSurface.shader - URP Shader

Shader "Custom/WaterSurface" { Properties { _HeightMap ("Height Map", 2D) = "black" {} _NormalStrength ("Normal Strength", Float) = 1.0 _BaseColor ("Base Color", Color) = (0.0, 0.3, 0.5, 1.0) _FoamColor ("Foam Color", Color) = (1.0, 1.0, 1.0, 1.0) _FoamThreshold ("Foam Threshold", Float) = 0.8 } SubShader { Tags { "RenderType"="Transparent" "Queue"="Transparent" } Pass { Name "ForwardLit" Tags { "LightMode"="UniversalForward" } HLSLPROGRAM #pragma vertex vert #pragma fragment frag #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl" struct Attributes { float4 positionOS : POSITION; float2 uv : TEXCOORD0; }; struct Varyings { float4 positionHCS : SV_POSITION; float3 positionWS : TEXCOORD0; float2 uv : TEXCOORD1; float3 normalWS : TEXCOORD2; }; TEXTURE2D(_HeightMap); SAMPLER(sampler_HeightMap); float4 _HeightMap_TexelSize; float _NormalStrength; float4 _BaseColor; float4 _FoamColor; float _FoamThreshold; Varyings vert(Attributes input) { Varyings output; float2 uv = input.uv; float height = SAMPLE_TEXTURE2D_LOD(_HeightMap, sampler_HeightMap, uv, 0).r; float3 positionOS = input.positionOS.xyz; positionOS.y += height; output.positionHCS = TransformObjectToHClip(positionOS); output.positionWS = TransformObjectToWorld(positionOS); output.uv = uv; // 计算法线 float hL = SAMPLE_TEXTURE2D_LOD(_HeightMap, sampler_HeightMap, uv - float2(_HeightMap_TexelSize.x, 0), 0).r; float hR = SAMPLE_TEXTURE2D_LOD(_HeightMap, sampler_HeightMap, uv + float2(_HeightMap_TexelSize.x, 0), 0).r; float hB = SAMPLE_TEXTURE2D_LOD(_HeightMap, sampler_HeightMap, uv - float2(0, _HeightMap_TexelSize.y), 0).r; float hT = SAMPLE_TEXTURE2D_LOD(_HeightMap, sampler_HeightMap, uv + float2(0, _HeightMap_TexelSize.y), 0).r; float3 normal; normal.x = (hL - hR) * _NormalStrength; normal.y = 2.0 * _HeightMap_TexelSize.x; normal.z = (hB - hT) * _NormalStrength; normal = normalize(normal); output.normalWS = TransformObjectToWorldNormal(normal); return output; } float4 frag(Varyings input) : SV_Target { float height = SAMPLE_TEXTURE2D(_HeightMap, sampler_HeightMap, input.uv).r; // 基础颜色 float4 color = _BaseColor; // 简单的光照计算 float3 normal = normalize(input.normalWS); float3 lightDir = normalize(_MainLightPosition.xyz); float NdotL = saturate(dot(normal, lightDir)); color.rgb *= (0.3 + 0.7 * NdotL); // 泡沫效果(波峰处) float foam = saturate((height - _FoamThreshold) / 0.2); color = lerp(color, _FoamColor, foam); return color; } ENDHLSL } } }

C# 驱动脚本

FluidSimulator.cs - CPU端控制

using UnityEngine; using UnityEngine.Rendering; public class FluidSimulator : MonoBehaviour { [Header("Simulation Settings")] [SerializeField] private int gridSize = 256; [SerializeField] private float gridSpacing = 0.1f; [SerializeField] private float timeStep = 0.016f; [SerializeField] private float gravity = 9.81f; [SerializeField] private float damping = 0.995f; [Header("Compute Shader")] [SerializeField] private ComputeShader fluidCompute; [Header("Interaction")] [SerializeField] private float brushRadius = 5f; [SerializeField] private float brushStrength = 1f; // 渲染纹理 private RenderTexture heightField; private RenderTexture heightFieldTemp; private RenderTexture velocityField; private RenderTexture velocityFieldTemp; // Compute Shader 内核索引 private int updateHeightKernel; private int updateVelocityKernel; private int addDisturbanceKernel; void Start() { InitializeTextures(); InitializeComputeShader(); } void InitializeTextures() { // 创建双缓冲渲染纹理 RenderTextureDescriptor desc = new RenderTextureDescriptor( gridSize, gridSize, RenderTextureFormat.RFloat, 0 ); desc.enableRandomWrite = true; heightField = new RenderTexture(desc); heightField.Create(); heightFieldTemp = new RenderTexture(desc); heightFieldTemp.Create(); // 速度场使用 RGFloat 格式(存储 x, y 分量) RenderTextureDescriptor velocityDesc = new RenderTextureDescriptor( gridSize, gridSize, RenderTextureFormat.RGFloat, 0 ); velocityDesc.enableRandomWrite = true; velocityField = new RenderTexture(velocityDesc); velocityField.Create(); velocityFieldTemp = new RenderTexture(velocityDesc); velocityFieldTemp.Create(); } void InitializeComputeShader() { updateHeightKernel = fluidCompute.FindKernel("UpdateHeight"); updateVelocityKernel = fluidCompute.FindKernel("UpdateVelocity"); addDisturbanceKernel = fluidCompute.FindKernel("AddDisturbance"); } void Update() { HandleInput(); // 执行模拟步骤 DispatchComputeShaders(); // 交换缓冲区 SwapBuffers(); } void DispatchComputeShaders() { int threadGroups = Mathf.CeilToInt(gridSize / 8f); // 设置共享参数 fluidCompute.SetFloat("_DeltaTime", timeStep); fluidCompute.SetFloat("_GridSize", gridSpacing); fluidCompute.SetFloat("_Gravity", gravity); fluidCompute.SetFloat("_Damping", damping); // 更新速度 fluidCompute.SetTexture(updateVelocityKernel, "_HeightField", heightField); fluidCompute.SetTexture(updateVelocityKernel, "_VelocityField", velocityField); fluidCompute.SetTexture(updateVelocityKernel, "_VelocityFieldOut", velocityFieldTemp); fluidCompute.Dispatch(updateVelocityKernel, threadGroups, threadGroups, 1); // 更新高度 fluidCompute.SetTexture(updateHeightKernel, "_HeightField", heightField); fluidCompute.SetTexture(updateHeightKernel, "_VelocityField", velocityFieldTemp); fluidCompute.SetTexture(updateHeightKernel, "_HeightFieldOut", heightFieldTemp); fluidCompute.Dispatch(updateHeightKernel, threadGroups, threadGroups, 1); } void SwapBuffers() { // 交换高度场 RenderTexture temp = heightField; heightField = heightFieldTemp; heightFieldTemp = temp; // 交换速度场 temp = velocityField; velocityField = velocityFieldTemp; velocityFieldTemp = temp; } void HandleInput() { if (Input.GetMouseButton(0)) { Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); RaycastHit hit; if (Physics.Raycast(ray, out hit)) { Vector2 uv = hit.textureCoord; AddDisturbance(uv, brushStrength); } } } void AddDisturbance(Vector2 uv, float strength) { fluidCompute.SetFloat("_BrushPosX", uv.x); fluidCompute.SetFloat("_BrushPosY", uv.y); fluidCompute.SetFloat("_BrushRadius", brushRadius / gridSize); fluidCompute.SetFloat("_BrushStrength", strength); fluidCompute.SetTexture(addDisturbanceKernel, "_HeightField", heightField); int threadGroups = Mathf.CeilToInt(gridSize / 8f); fluidCompute.Dispatch(addDisturbanceKernel, threadGroups, threadGroups, 1); } void OnDestroy() { // 清理资源 heightField?.Release(); heightFieldTemp?.Release(); velocityField?.Release(); velocityFieldTemp?.Release(); } }

应用案例

开放世界海洋系统

在大型开放世界游戏中,使用浅水方程配合 FFT 频谱方法可以实时渲染数千平方公里的海洋表面。Gerstner 波叠加可以模拟不同风速和方向下的海面状态。

// Gerstner 波叠加 float3 GerstnerWave(float2 pos, float2 direction, float steepness, float wavelength, float speed, float time) { float k = 2 * PI / wavelength; float c = sqrt(9.8 / k); float2 d = normalize(direction); float f = k * (dot(d, pos) - c * time); float a = steepness / k; return float3( d.x * a * cos(f), a * sin(f), d.y * a * cos(f) ); }

交互式水体

玩家与水体交互时,通过 Compute Shader 在交互位置添加高度扰动,可以产生逼真的波纹扩散效果。这种方法常用于水池、喷泉等场景。

实现要点:使用 RenderTexture 作为高度场,每帧执行一次 Jacobi 迭代求解波动方程,配合法线贴图实现光照效果。

雨景与水坑

在雨天场景中,可以使用粒子系统生成雨滴接触水面的位置,然后在对应位置触发波纹。多个波纹的叠加可以产生复杂的水面效果。

性能优化

LOD 策略

  • •根据距离使用不同精度的模拟网格(近处256x256,远处64x64)
  • •远处使用预计算的波纹法线贴图替代实时模拟
  • •视锥体外的水体完全跳过模拟

GPU 优化

  • •使用 half 精度(fp16)存储高度场,减少带宽占用
  • •合并多个计算步骤到单个 Compute Shader
  • •使用 GPU Instancing 渲染水面网格

优化后的多分辨率模拟

public class MultiResolutionFluid : MonoBehaviour { [System.Serializable] public class LODLevel { public int resolution; public float worldSize; public float maxDistance; public RenderTexture heightTexture; } public LODLevel[] lodLevels; void Update() { Vector3 cameraPos = Camera.main.transform.position; foreach (var lod in lodLevels) { // 根据距离决定是否更新该 LOD 层级 float distance = Vector3.Distance(cameraPos, transform.position); if (distance < lod.maxDistance) { // 以较低频率更新远处的 LOD int updateInterval = Mathf.RoundToInt(distance / lod.worldSize); if (Time.frameCount % (updateInterval + 1) == 0) { SimulateLOD(lod); } } } } void SimulateLOD(LODLevel lod) { // 执行该 LOD 层级的模拟 // ... } }

总结

流体模拟是游戏开发中极具挑战性但也极具表现力的技术领域。通过理解 Navier-Stokes 方程和浅水方程的数学原理,我们可以在 Unity URP 中实现从简单的交互式水坑到广阔海洋的各种水体效果。

关键要点:

  • NS 方程适合需要完整3D流体行为的场景,但计算成本较高
  • 浅水方程是大面积水体模拟的最佳选择,兼顾效率和视觉效果
  • Compute Shader是实现高性能流体模拟的核心技术
  • LOD 和多分辨率策略对于大规模场景至关重要

随着 GPU 计算能力的不断提升,实时流体模拟的效果将越来越接近离线渲染。结合机器学习技术(如神经辐射场、物理信息神经网络),未来的流体模拟将更加高效和逼真。

http://www.jsqmd.com/news/653521/

相关文章:

  • UUV Simulator水下机器人仿真实战指南:构建高保真水下环境与机器人系统
  • 从10bit到16bit:MIPI RAW数据转换的C++与Python实现对比
  • 现代智能汽车中的无线技术11.6——TCU之远程诊断与运行监控
  • 被AGI逼疯的硅谷天才,正在集体逃亡
  • PowerDMIS最佳拟合法
  • 从零部署SITS2026邮件AI模块:3个Power Automate连接器+1个Outlook插件,IT管理员15分钟完成上线
  • 使用 Claude Code 将 Google Stitch 设计稿转换为代码
  • Unity弓箭轨迹别再硬算了!一个脚本搞定抛物线运动(附完整C#代码)
  • Playwright和Robot Framework 哪个好
  • 用Lisp写回测(K线篇)—— 从“玩具”到工程
  • 深度解析:OpenIPC固件在君正T31ZX平台烧录故障排查与修复指南
  • Unity URP 热更新兼容性:Shader 在 IL2CPP 打包下的注意事项
  • 如何监控集群 interconnect_ping与traceroute验证心跳通畅.txt
  • OpenAI惨遭反超,Anthropic狂吞70%新客户,Claude已开启「灵魂校准」
  • 别再只聊天了!用Python调用Gemini API,5分钟搞定图片识别和表格数据提取
  • 告别网络性能盲猜:手把手教你将iperf3交叉编译到ARM设备,实测WiFi/有线带宽
  • 【Ubuntu2404】Ubuntu24.04下Docker引擎的安装与配置全攻略
  • 装好Hermes只是第一步:四步调教,让AI“越用越聪明”
  • 红黑榜 | 以为吃了70碗水煮菜,其实换了个形式吃咸菜?
  • Unity中PICO手柄按键返回值的高级应用与实战解析
  • 黑群晖转白群晖DS920+数据迁移全记录(含避坑指南)
  • 太空算力:下一个万亿蓝海赛道
  • 【RAG】【vector_stores053】Milvus全文搜索向量存储示例分析
  • ICLR 2025 | HiPRAG:不是让 Agent RAG 搜得更多,而是让它学会什么时候不该搜
  • 数据结构之双端队列
  • 5大核心功能打造极致Markdown预览体验:Markdown Viewer全面解析
  • “程序包io.swagger.annotations不存在”终极解决方案:从原理到实战的万字深度剖析(2026年最全最新解决方案)
  • 2026年超长论文分章节降AI率的正确方法:多章节处理完整攻略
  • while(1);的top-down分析
  • 第3讲——并查集