Unity3D教程:水面渲染之Gerstner波的原理及实现

发表于2016-05-11
评论3 1.38w浏览

      水面渲染经常在很多场景中会应用到,本文主要讲述了在Unity3D中水面渲染Gerstner波的原理以及实现,一起来看看吧。


1、前言

本文旨在与大家一起探讨学习新知识,如有疏漏或者谬误,请大家不吝指出。

以下内容参考了GPU精粹1中第一章关于水波模拟的部分知识。


2、概述

水的渲染经常用于游戏开发以及各种虚拟现实中,其中关于水波的模拟书籍以及网上也有了很多的文章描述,那么其中主要使用的其实就是正弦波或者与其相关的变种公式。




 3、原理

首先,我们来回顾一下我们的初中知识,关于正弦函数公式:



那么我们下面对公式中的各个参数进行说明。如图所示A是指波的振幅;影响波的周期L(或者叫波长),相位决定了波在X轴向上的移动距离,速度S与相常数的关系可以写成公式。当我们设置好速度S、波长L以及振幅A,然后我们带入点坐标的x值,就可以在2D世界中得到一个正弦波了。

当然,我们的目标是3D世界,所以我们需要加入一个z值,我们这里使用左手坐标系,并假定水平面是y=0,那么xz平面就是指的水平面,而y值则表示高度。我们需要通过输入x、z值,求出点p(x, 0, z)的高度值y。我们对公式做一点修改:

我们新加入了一个参数D,D指明了这个波在xz平面上的运动方向。D点乘(x,z)后得到的是一个标量,剩下的就和上面2D世界的波没什么区别了。

通过叠加多个不同方向、波长、振幅以及速度的波,我们就可以模拟出水波的大致效果了。但是很显然,正弦波太圆滑了,它可以模拟水池的波,但是我们观察到海水的波,其波峰比较尖锐,而波谷比较宽,如下图所示:



这时候就轮到我们的主角Gerstner波出场了。我们直接给出公式:



上述三个公式分别对应x、z、y三个分量。我们可以看到,y值的计算是完全没有变化的,我们对x和z的位置做了一些偏移(实际上点P是在做一个圆圈运动),在x、z的公式中,加入了一个新的参数Q,Q可以用来控制波的陡度,其值越大,则波越陡,当然这里要注意,如果Q值太大了,就会造成环,如下图所示:



经过先辈们研究发现,如果的值等于1,则会形成最尖锐的波,超过1则会造成环,等于0则是最平缓的波。

 

4、实现

有了上述公式,剩下的实现其实非常的简单了,在顶点着色器中代入公式,计算出新的位置就ok了,我们来看下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
float4 _A;
float4 _S;
float4 _Dx;
float4 _Dz;
float4 _L;                                     
float3 CalculateWavesDisplacement(float3 vert)
{
    float PI = 3.141592f;
    float3 pos = float3(0,0,0);
    float4 w = 2*PI/_L;
    float4 psi = _S*2*PI/_L;
    float4 phase = w*_Dx*vert.x+w*_Dz*vert.z+psi*_Time.x;
    float4 sinp=float4(0,0,0,0), cosp=float4(0,0,0,0);
    sincos(phase, sinp, cosp)
    pos.x = dot(_Q*_A*_Dx, cosp);
    pos.z = dot(_Q*_A*_Dz, cosp);
    pos.y = dot(_A, sinp);
    return pos;
}
v2f vert (appdata v)
{
    v2f o;
    float3 worldPos = mul(_Object2World, v.vertex);
    float3 disPos = CalculateWavesDisplacement(worldPos);
    v.vertex.xyz = mul(_World2Object, float4(worldPos+disPos, 1));
    o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
    o.uv = TRANSFORM_TEX(v.uv, _MainTex);
    UNITY_TRANSFER_FOG(o,o.vertex);
    return o;
}


有几点需要大家注意:

1、这里我是做了四个波的叠加。四个波的参数存放在float4类型的变量里。例如_A.x表示波a的振幅,_A.y表示波b的振幅,依次类推。其中_Dx以及_Dz分别存放了方向参数D的x值和z值,即波a的参数D为(_Dx.x, _Dz.x)。

2、四个波的叠加操作是通过点乘函数完成的,这样可以少写几行代码^^。

3、上述函数CalculateWavesDisplacement中其实可以做优化的。例如可以在外面将2*PI/_L计算好再传入shader中进行计算,在正式完整的代码中我就是这么做的,另外需要注意除零的问题,显然我没有做处理。

4、函数CalculateWavesDisplacement的参数,我传入的是顶点的世界坐标,这样当多个水的面片拼接时就可以避免边缘撕裂的情况了。当然这里将点转换到世界坐标系,又转换回物体坐标系显得有点不雅,可以考虑在外部脚本中算好ViewProj的矩阵,传入shader中使用。

    以下是完整的示例代码:

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引