之前學習了一些Gerstner Waves的知識,看了一些教程,但發現說的略不夠通俗,爲了便於自己理解,我自己來寫一個給自己的教程吧。
該教程會有三個步驟,一闡述原理,二給出源碼和源碼解釋,三提供demo下載。這樣一來就算文字看不懂,
也可以下載demo,運行demo,結合源碼一起看,總有一種方式能讓你看懂(^_^)。
原理解釋
參考:GPU Gems的官方文檔
https://developer.nvidia.com/gpugems/GPUGems/gpugems_ch01.html
以下是它的原理的翻譯:
Gerstner波函數最初是在計算機圖形學很久之前就開發出來的,用於物理模擬海水。
我們選擇Gerstner Waves這了因爲他們有一種容易被忽視的( often-overlooked )特質:
他們通過向每個波峯移動頂點,形成更清晰、尖銳(sharper)的波峯。
我的理解就是:x、z座標向波峯靠攏,y軸座標做sin波計算。所以看起來波峯更尖銳,波谷更廣闊(因爲頂點向波峯靠攏了,
波谷的頂點距離變更大了,看起來就更寬闊。)
方程式爲:
溫馨提示:不要被公式嚇到了,看似複雜,但它僅僅是個公式而已,我們不一定要完全理解它,我們只需要知道每個參數是幹啥的即可。然後代入到我們代碼裏運行即可。最終都只是加減乘除等基礎的運算。
方程式參數解釋:
其中Qi是控制波浪陡度的參數,i是第i個波浪(也可以只有一個波浪,更好理解)。
如果Qi是0,則是一般的正弦波。
當Qi = 1/(wi*Ai)時,波峯最尖銳。
其他參數解釋請看下面:
爲了便於理解,我們必須要結合正弦波的圖來看,因爲Gerstner Waves也是正弦波的基礎上演變過來的。
下圖中的參數和Gerstner Waves方程式裏的參數是 一致的。
正弦波:
正弦波方程式:
L(WaveLength)是世界座標上波峯與波峯之間的距離。
參數wi是頻率,是一個常數,wi = 2 π /L(GUP Gems網頁版有錯誤,寫少了個π)。根據百度百科,頻率的意思是單位時間中完成週期性變化的次數。
根據正弦方程的標準函數(也叫正弦型函數)f(x)=Asin(wx+β),最小正週期L就是L=2π/w。所以 w = 2π/L。
具體可見百度百科正弦型函數的解釋:
https://baike.baidu.com/item/%E6%AD%A3%E5%BC%A6%E5%9E%8B%E5%87%BD%E6%95%B0/898409?fr=aladdin
Ai是水平面到波峯的高度。
Di是波峯移動的水平向量。
t x φi 是初相,是一個常數。根據正弦型函數,ωx+φ叫做相位,φ叫做初相(即當x=0時的相位 )。t x φi 決定了所有的點是否向左(t x φi >0)或向右(t x φi <0)平行移動|t x φi |個單位。
t是時間。
(x,y)是水平位置(horizontal position)
因此Di點乘(x,y)是可知兩者的夾角方向,等於0爲90度,大於0夾角是銳角。小於0夾角是鈍角。
因此全部正弦波疊加的公式爲:
由於Gerstner Wave沒有嚴格的高度公式,所以套上上面的公式,我們的Gerstner Wave方程式爲:
這樣就更好理解了吧?H(x,y,t)是波的高度,是正弦波。
x,y對應着unity 裏vector3裏的x,z。
理解上面的參數是幹嘛的,我們就可以寫代碼啦!
計算頂點的代碼在C#裏爲:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;
public class WaterWave : MonoBehaviour
{
Mesh mesh;
public float height;
//尖銳
[Range(0, 1f)]
public float sharp = 0.5f;
[Range(0.5f, 10f)]
public float speed = 2f;//初相
[Range(1, 50)]
public int waveT;//週期
public Vector2 WaveDir = Vector2.left;
private Vector3[] baseVertices;
private void OnEnable()
{
mesh = GetComponent<MeshFilter>().mesh;
baseVertices = mesh.vertices;
}
void Update()
{
Vector3[] vertices = this.mesh.vertices;
for (int i = 0; i < vertices.Length; i++)
{
Vector3 vertice = this.baseVertices[i];
float A = this.height;
float w = (float)(2 * Math.PI / waveT);
float Qi = sharp / w * A;
float cosNum = Mathf.Cos(Time.time * speed + w * Vector2.Dot(WaveDir, new Vector2(vertices[i].x, vertices[i].z)));
float sinNum = Mathf.Sin(Time.time * speed + w * Vector2.Dot(WaveDir, new Vector2(vertices[i].x, vertices[i].z)));
vertice.x += Qi * A * WaveDir.x * cosNum;
vertice.z += Qi * A * WaveDir.y * cosNum;
vertice.y = sinNum * A;
vertices[i] = vertice;
}
this.mesh.vertices = vertices;
this.mesh.RecalculateNormals();//重新計算法線
}
}
代碼很少吧?這只是一個波的計算。
在以上代碼裏,this.mesh.RecalculateNormals();幫我們重新計算法線了。
法線公式及解釋
如果要手動計算法線,官方也給出了公式,並且指出說明計算效率非常高:
其中:
P是P(x,y,t)
其他的參數和計算頂點的一樣。具體代碼請下載demo查看。
上圖是多個波疊加起來的效果,疊加的源碼在demo裏可以查看。
Demo下載
如果以上內容看起來有點吃力,是因爲沒有活生生的例子。所以我們提供了demo哦!
裏面有源碼。在編輯器調整參數,運行看看效果,結合本文看看源碼,相信你很快就能理解並開始運用啦!
demo裏分別有shader和C#的例子。我發現C#裏寫GerstnerWave操作頂點和法線,會可能出現閃爍等異常效果。
但是同樣的公式在shader裏寫就不會。非常費解,所以建議在shader裏寫GerstnerWave。
上圖是shader寫的GerstnerWave效果。
但是不在C#裏寫海波浪,如果是多種LOD的多個mesh拼接成的大海效果,如何計算波浪呢?目前我還沒答案。
等我研究出了,我會寫第二篇的。
更新:
shader代碼裏
float w = (float)(2 * UNITY_PI / waveT);和
float w = 1 / waveT;的問題不用糾結,都是初相的大小不同而已,大小不同,效果也不同,大家可以註釋其中一個看效果。
最後:
因爲教程是基於我自己的理解來寫的,如果有紕漏和錯誤,請讀者指出,共同學習,共同進步,謝謝。