Unity3D教程:水面渲染之Gerstner波的原理及实现
水面渲染经常在很多场景中会应用到,本文主要讲述了在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中使用。
以下是完整的示例代码: