如何做一個水面流動效果
本文參考自教程,並加上自己的一些心得體會。所謂的水面流動,就是對給定的紋理貼圖進行取樣,使其隨著時間流動,呈現有規則地沿著某一方向連續,但是又不完全有規律的效果。
首先,我們用到的貼圖是一張高度圖,ag通道分別代表高度在x和y方向上的導數,b通道儲存真實的高度資料。先讓我們直接對貼圖進行取樣,我們用高度資料作為最後的顏色輸出:
float3 UnpackDerivativeHeight (float4 textureData) { float3 dh = textureData.agb; dh.xy = 2 * dh.xy - 1; return dh; } float3 dh = UnpackDerivativeHeight(tex2D(_MainTex, i.uv)); float4 texColor = dh.z * dh.z * _Color; float3 tangentNormal = normalize(float3(-dh.x, -dh.y, 1));
接下來,我們希望它能沿著某一個方向動起來,為了實現這一效果,需要旋轉整張貼圖的uv,例如我們嘗試著旋轉45度:
float2 RotateUV(float2 uv, float2 rotVec) { rotVec = normalize(rotVec); float2x2 mat = float2x2(rotVec.y, -rotVec.x, rotVec.x, rotVec.y); return mul(mat, uv); }
這裡我們傳入的引數並非是一個旋轉的角度,而是一個向量,但實際上兩者是等價的。已知旋轉矩陣為:
\[\begin{bmatrix} cos\theta & -sin\theta \\ sin\theta & cos\theta \end{bmatrix} \]又知道\(sin^2\theta + cos^2\theta = 1\),那麼我們可以用一個歸一化的向量\(\vec{v} = (sin\theta, cos\theta)\)來表示旋轉。效果如下:
接下來我們讓它動起來,設定旋轉向量為一個隨時間變化的向量:
float2 uv = RotateUV(i.uv, float2(cos(_Time.y), sin(_Time.y)));
仔細觀察,會發現高光的效果不太對,高光並不是跟隨著紋理旋轉而旋轉的,原因是我們在旋轉高度貼圖時,沒有相應地對貼圖描述的法線也進行旋轉,那麼我們把計算得到的旋轉矩陣暴露出來:
float2 RotateUV(float2 uv, float2 rotVec, out float2x2 mat)
{
rotVec = normalize(rotVec);
mat = float2x2(rotVec.y, -rotVec.x, rotVec.x, rotVec.y);
return mul(mat, uv);
}
dh.xy = mul(rotMat, dh.xy);
我們希望把旋轉向量記錄在一張單獨的貼圖中,這樣就可以根據uv取樣得到不同的旋轉向量,然後讓uv隨著時間不斷位移,從而獲得沿著某個方向移動的效果。甚至,我們可以利用這張貼圖附加一個速度資訊,控制uv隨著時間位移的速度:
float3 flowVec = tex2D(_FlowMap, i.uv).rgb;
flowVec.xy = 2 * flowVec.xy - 1;
flowVec.z *= _FlowStrength;
float2 uv = RotateUV(i.uv, flowVec.xy, rotMat);
uv.y = uv.y - flowVec.z * _Time.y;
不過結果卻是各種噪點在閃爍,令人失望。仔細分析下原因,是我們對flow map的取樣太精細了,每個點都對應不同的旋轉向量,導致這種不連續的效果。為了解決這個問題,首先我們將整個map進行分塊處理,每塊對應一個旋轉向量:
float2 flowUV = floor(i.uv * _GridResolution) / _GridResolution;
float3 flowVec = tex2D(_FlowMap, flowUV).rgb;
然後,讓我們對塊與塊之間進行融合:
float3 FlowCell(float2 uv, float2 offset)
{
float2x2 rotMat;
float2 flowUV = floor(uv * _GridResolution + offset) / _GridResolution;
float3 flowVec = tex2D(_FlowMap, flowUV).rgb;
flowVec.xy = 2 * flowVec.xy - 1;
flowVec.z *= _FlowStrength;
float2 mainUV = RotateUV(uv, flowVec.xy, rotMat);
mainUV.y = mainUV.y - flowVec.z * _Time.y;
float3 dh = UnpackDerivativeHeight(tex2D(_MainTex, mainUV));
dh.xy = mul(rotMat, dh.xy);
return dh;
}
float3 dhA = FlowCell(i.uv, float2(0, 0));
float3 dhB = FlowCell(i.uv, float2(1, 0));
float wA = frac(i.uv.x * _GridResolution);
float wB = 1 - wA;
float3 dh = dhA * wA + dhB * wB;
雖然整體看上去看上去平滑了很多,但是塊和塊之間的間隙依舊十分明顯。這些間隙的形成是由於不同塊取樣的flow map位置存在跳變,而發生跳變時融合的權重wA和wB總有一個是1。為了消除間隙,我們希望flow map取樣發生跳變時,對應的融合權重為0。對於wA來說,我們希望當取樣點到達塊中央時,wA到達最大值1,而在邊緣部分,則減小到最小值0。對於wB我們希望也滿足這樣的條件,但是這樣就不滿足融合的效果,因此我們需要將第二次取樣的塊偏移0.5個單位而不是1個單位:
float3 FlowCell(float2 uv, float2 offset)
{
offset *= 0.5;
...
}
float wB = abs(2 * frac(i.uv.x * _GridResolution) - 1);
float wA = 1 - wB;
可以看到,u方向的間隙已經消失了,如法炮製,再消除掉y方向上的間隙:
float3 dhA = FlowCell(i.uv, float2(0, 0));
float3 dhB = FlowCell(i.uv, float2(1, 0));
float3 dhC = FlowCell(i.uv, float2(0, 1));
float3 dhD = FlowCell(i.uv, float2(1, 1));
float2 t = abs(2 * frac(i.uv * _GridResolution) - 1);
float wA = (1 - t.x) * (1 - t.y);
float wB = t.x * (1 - t.y);
float wC = (1 - t.x) * t.y;
float wD = t.x * t.y;
float3 dh = dhA * wA + dhB * wB + dhC * wC + dhD * wD;
經過調整之後,取樣的權重最大時實際上是在塊的中心,那麼我們也希望取樣的flow map位置也是每個塊的中心,而現在是,帶有偏移量的塊取樣的是其中心位置,但沒有偏移的塊取樣的是其左下位置,因此我們需要額外進行一點處理:
float2 shift = 1 - offset;
shift *= 0.5;
float2 flowUV = (floor(uv * _GridResolution + offset) + shift) / _GridResolution;
這裡放shift放外面是因為我們只是想平移取樣的位置,而不是整個塊的位置。
接下來,我們對流動的效果進行微調,例如根據flow map中b通道,即流動速度來調製高度資訊和tiling資訊,這也比較符合直觀印象,流動越快之處,高度越高,tiling越大,流動紋理越密集。
float2 mainUV = RotateUV(uv, flowVec.xy, rotMat);
mainUV.y = mainUV.y - flowVec.z * _Time.y;
float tiling = flowVec.z * _TilingModulated + _Tiling;
mainUV *= tiling;
float3 dh = UnpackDerivativeHeight(tex2D(_MainTex, mainUV));
dh.xy = mul(rotMat, dh.xy);
dh *= flowVec.z * _HeightScaleModulated + _HeightScale;
同時,為了避免取樣出flow map的資料差異過小,我們可以手動為mainUV增加一些偏移來增加差異性:
float2 mainUV = RotateUV(uv + offset, flowVec.xy, rotMat);
如果你覺得我的文章有幫助,歡迎關注我的微信公眾號(大齡社畜的遊戲開發之路)^ - ^