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

引言

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

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

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 算法的简化实现:

cs 复制代码
// 第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 - 浅水方程求解

cs 复制代码
#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 - 速度更新

cs 复制代码
[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

cs 复制代码
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端控制

cs 复制代码
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 波叠加可以模拟不同风速和方向下的海面状态。

cs 复制代码
// 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 渲染水面网格

优化后的多分辨率模拟

cs 复制代码
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 计算能力的不断提升,实时流体模拟的效果将越来越接近离线渲染。结合机器学习技术(如神经辐射场、物理信息神经网络),未来的流体模拟将更加高效和逼真。

相关推荐
mxwin5 小时前
Unity Shader 深度重建世界坐标
unity·游戏引擎·shader
雪儿waii6 小时前
Unity 中继承(父类子类)用法详解
unity·游戏引擎
总写bug的程序员6 小时前
用 AI 蒸馏球员的思维操作系统:qiuyuan-skill 技术解析
人工智能·unity·游戏引擎
mxwin9 小时前
Unity Shader 预乘 Alpha 完全指南 解决半透明纹理边缘黑边问题,让你的 UI 渲染更干净
unity·游戏引擎
mxwin9 小时前
Unity URP 软粒子(Soft Particles)完全指南
unity·游戏引擎·shader
mxwin9 小时前
Unity Shader 深度偏移Depth Bias / Offset 完全指南
unity·游戏引擎·shader
星河耀银海10 小时前
Unity基础:UI组件详解:Button按钮的点击事件绑定
ui·unity·lucene
RReality11 小时前
【Unity Shader URP】平面反射(Planar Reflection)实战教程
ui·平面·unity·游戏引擎·图形渲染·材质
风酥糖11 小时前
Godot游戏练习01-第30节-教程结束我继续
游戏·游戏引擎·godot