Gerstner Waves 简介

Gerstner Waves 最初是作为流体力学的一个精确解析解(严格推导)被提出,假设每一个水质点都在做圆周运动,那么在忽略粘性和假设流体不可压缩的情况下,这组方程可以完美满足流体力学的基本方程。

Gerstner Waves 原理
尽管 Gerstner Waves 是严格的数学精确解,但因违背理想流体无旋假设而存在物理缺陷,故在真实海洋建模中远不如符合开尔文环流定理的Stokes波主流。 到了 1980 年代,图形学家们发现 Gerstner Waves 非常适合模拟海洋,为了适应实时渲染的需求和艺术家的视觉控制,图形学界对原始物理公式进行了参数化改造和数学上的“违规”操作,所以以下讨论的 Gerstner Waves 公式并非原始的物理公式,其逻辑也并不完全符合现实物理。

数学公式

单个波的位移公式

单个波的位移:

$$ P' = \left\{ \begin{aligned} x' &= Q A D_x \cos(\theta) \\ y' &= A \sin(\theta) \\ z' &= Q A D_z \cos(\theta) \end{aligned} \right. $$

其中相位 $\theta$ :

$$ \begin{aligned} \theta &= k(D \cdot P_{xz}) - \omega t \\ &= k(D_x x + D_z z) - \omega t \end{aligned} $$

其中 $\omega$ 由色散公式得:

$$ \omega^2 = g k \tanh(kh) \implies \left\{ \begin{aligned} &\text{深水: } &\lim_{h \to 0} w^2 &= gk &\implies w &= \sqrt{gk} \\ &\text{浅水: } &\lim_{h \to \infty} w^2 &= gk^2h &\implies w &= k\sqrt{gh} \end{aligned} \right. $$
参数 含义
$Q$ 锐利度(波陡度)
$A$ 波高(振幅)
$D=(D_x,D_z)$ 波传播方向
$k$ 波数(由 $k = \frac{2\pi}{\lambda}$ 得,$\lambda$ 为波长)
$h$ 水深
$\omega$ 角频率(可通过色散公式计算)
$t$ 时间
$g$ 重力加速度( $g \approx 9.81$ )

写成绝对位置公式:

$$ P(x,z) = \begin{pmatrix} x &+& Q A D_x \cos(\theta) \\ y &+& A \sin(\theta) \\ z &+& Q A D_z \cos(\theta) \end{pmatrix} $$

多波叠加

把多个波的位移累加即可, 写成绝对位置公式:

$$ P(x,z) = \begin{pmatrix} x &+& \sum_{i=1}^{N} Q_i A_i D_{x_i} \cos(\theta_i) \\ y &+& \sum_{i=1}^{N} A_i \sin(\theta_i) \\ z &+& \sum_{i=1}^{N} Q_i A_i D_{z_i} \cos(\theta_i) \end{pmatrix} $$

在实际工程中,更多的是多波叠加。

重新计算法线

单波

顶点移动后,为了正确光照,需要重新计算法线。原理:

计算偏导、构建切线:

$$ \begin{aligned} T_x &= \frac{\partial P'}{\partial x} = \begin{pmatrix} 1 - k Q A D_x^2 sin(\theta) \\ k A D_x \cos(\theta) \\ - k Q A D_x D_z \sin(\theta) \end{pmatrix} \\ T_z &= \frac{\partial P'}{\partial z} = \begin{pmatrix} - k Q A D_x D_z \sin(\theta) \\ k A D_z \cos(\theta) \\ 1 - k Q A D_z^2 \sin(\theta) \end{pmatrix} \end{aligned} $$

叉乘并归一化得到法线:

$$ N = \text{normalize}(T_x \times T_z) $$

根据向量叉乘公式:

$$ \begin{aligned} N_x &= (T_z)_y (T_x)_z - (T_z)_z (T_x)_y \\ N_y &= (T_z)_z (T_x)_x - (T_z)_x (T_x)_z \\ N_z &= (T_z)_x (T_x)_y - (T_z)_y (T_x)_x \end{aligned} $$

带入计算 X 分量 和 Z 分量,复杂项都会被抵消:

$$ \begin{aligned} N_x &= - k A D_x \cos(\theta) \\ N_z &= - k A D_z \cos(\theta) \end{aligned} $$

带入计算 Y 分量:

$$ N_y = 1 - k Q A \sin(\theta) \cdot (D_x^2 + D_z^2) $$

由于 $D$ 是波的传播方向单位向量,所以 $D_x^2 + D_z^2 = 1$,代入后得到:

$$ N_y = 1 - k Q A \sin(\theta) $$

对于单个波,它的未归一化法线:

$$ N = \begin{pmatrix} - k D_x A \cos(\theta) \\ 1 - k Q A \sin(\theta) \\ - k D_z A \cos(\theta) \end{pmatrix} $$

多波

对于多波叠加的法线,由于微积分的线性性质,只需要将每个波求出的偏导数项(即偏移量)累加,然后再执行一次最终的计算即可。

根据和的导数等于导数的和,计算偏导、构建切线:

$$ \begin{aligned} T_x &= \frac{\partial P'}{\partial x} = \begin{pmatrix} 1 - \sum_{i=1}^{N} k_i Q_i A_i D_{x_i}^2 sin(\theta_i) \\ \sum_{i=1}^{N} k_i A_i D_{x_i} \cos(\theta_i) \\ - \sum_{i=1}^{N} k_i Q_i A_i D_{x_i} D_{z_i} \sin(\theta_i) \end{pmatrix} \\ T_z &= \frac{\partial P'}{\partial z} = \begin{pmatrix} - \sum_{i=1}^{N} k_i Q_i A_i D_{x_i} D_{z_i} \sin(\theta_i) \\ \sum_{i=1}^{N} k_i A_i D_{z_i} \cos(\theta_i) \\ 1 - \sum_{i=1}^{N} k_i Q_i A_i D_{z_i}^2 \sin(\theta_i) \end{pmatrix} \end{aligned} $$

叉乘并归一化得到法线:

$$ N = \text{normalize}(T_x \times T_z) $$

代码示例

波数据结构

HLSL
// 波属性
struct Wave
{
	float amplitude; // 振幅 A
	float wavenumber; // 波数 k
	float2 direction; // 方向 D
	float steepness; // 锐利度 Q
	float speed; // 角频率 ω
};
// 计算的结果
struct WaveResult
{
    float3 position; // 偏移后的位置
    float3 normal; // 偏移后的法线
};
点击展开查看更多

单波函数

HLSL
WaveResult ComputeGerstnerWave(Wave wave, float3 gridPosition, float time)
{
    WaveResult result;

    // 1. 预计算基础参数
    /// 相位 Phase/θ
    float phase = wave.wavenumber * dot(wave.direction, gridPosition.xz) - wave.speed * time;
    float cosP = cos(phase);
    float sinP = sin(phase);
    /// Q * A
    float QA = wave.steepness * wave.amplitude;

    // 2. 计算顶点偏移
    result.position.x = gridPosition.x + QA * wave.direction.x * cosP;
    result.position.z = gridPosition.z + QA * wave.direction.y * cosP;
    result.position.y = wave.amplitude * sinP;

    // 3. 计算法线
    result.normal.x = -wave.wavenumber * wave.direction.x * wave.amplitude * cosP;
    result.normal.z = -wave.wavenumber * wave.direction.y * wave.amplitude * cosP;
    result.normal.y = 1.0 - wave.wavenumber * wave.steepness * wave.amplitude * sinP;
    result.normal = normalize(result.normal);

    return result;
}
点击展开查看更多

多波叠加

HLSL
#define N 8;
WaveResult ComputeMultGerstnerWaves(Wave waves[N], float3 gridPosition, float time)
{
    // 1. 初始化累加用于累加的变量
    /// 累积偏导(注意有初始值)
    float3 dPdx = float3(1.0, 0.0, 0.0);
    float3 dPdz = float3(0.0, 0.0, 1.0);
    /// 累积位置偏移
    float3 totalOffset = float3(0.0, 0.0, 0.0);
    
    // 2. 遍历波
    for (int i = 0; i< N; i++)
    {
	    Wave wave = waves[i];
	    // 1. 预计算基础参数
	    /// 相位 Phase/θ
	    float phase = wave.wavenumber * dot(wave.direction, gridPosition.xz) - wave.speed * time;
	    float cosP = cos(phase);
	    float sinP = sin(phase);
	    /// Q * A
	    float QA = wave.steepness * wave.amplitude;
	
	    // 2. 计算顶点偏移
	    totalOffset.x += QA * wave.direction.x * cosP;
	    totalOffset.z += QA * wave.direction.y * cosP;
	    totalOffset.y += wave.amplitude * sinP;
	    
	    // 3. 计算法线,累积偏导数
	    /// 预计算部分数据
	    float a_k_cos = wave.amplitude * wave.wavenumber * cosP;
        float qa_k_sin = QA * wave.wavenumber * sinP;
        /// 累积 X 方向的偏导
        dPdx.x -= wave.direction.x * wave.direction.x * qa_k_sin;
        dPdx.y += wave.direction.x * a_k_cos;
        dPdx.z -= wave.direction.x * wave.direction.y * qa_k_sin;
        /// 累积 Z 方向的偏导
        dPdz.x -= wave.direction.x * wave.direction.y * qa_k_sin;
        dPdz.y += wave.direction.y * a_k_cos;
        dPdz.z -= wave.direction.y * wave.direction.y * qa_k_sin;
    }
    
    // 3. 计算最终结果
    WaveResult result;
    result.normal = normalize(cross(dPdz, dPdx));
    result.position = gridPosition + totalOffset;
    return result;
}
点击展开查看更多

精简优化法线计算

直接按单波的方法计算并累积,最后归一化。

HLSL
float3 normal = float3(0, 1.0, 0);
for (...) {
	...
	normal.x -= wave.wavenumber * wave.direction.x * wave.amplitude * cosP;
	normal.z -= wave.wavenumber * wave.direction.y * wave.amplitude * cosP;
}
normal = normalize(normal);
点击展开查看更多

程序化构建波信息

经典 Gerstner Wave 实现里,每个 wave 的参数都需要手动定义。为避免麻烦,多数项目会随机生成 wave 参数,但遵循海洋统计规律。例如: 方向:随机 ±30° (模拟风向) 波长:指数分布 振幅:与波长相关 速度:由深水波公式

HLSL
for (int i = 0; i < N; i++)
{
    wave.direction = normalize(windDir + random2(-0.3,0.3));
    wave.wavelength = lerp(minWave, maxWave, random());
    wave.amplitude = wave.wavelength * 0.03;
    wave.speed = sqrt(9.81 * (2 * PI / wave.wavelength));
    wave.steepness = 0.7 / (wave.amplitude * N);
    waves[i] = wave;
}
点击展开查看更多

这样得到的海浪:

  • 方向相似(风向)
  • 尺度不同
  • 不会整齐排列 视觉上就自然很多。

波破碎

真实海浪在陡度过高时会破。

$$ Q k A > \text{某个阈值} $$

可以据此判断是否发生了波破碎,可以加一些 平化处理产生白沫

版权声明

作者: Cheyne Xie

链接: https://chaim.eu.org/posts/7240359d/

许可证: CC BY-NC-SA 4.0

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License. Please attribute the source, use non-commercially, and maintain the same license.

开始搜索

输入关键词搜索文章内容

↑↓
ESC
⌘K 快捷键