水紋
疫情肆掠,在家坐月子坐到傻了,開始寫點文章找回狀態。
本文是關于我們游戲 水紋 效果的實現細節。
這里的 水紋 效果是 天氣系統 的一部分,主要表現 雨水流動 的效果,如下圖:
本文會先介紹一下我們的實現方式,再對比一下其他實現。
我們的實現
常見的水波紋效果,一般都是依靠 法線貼圖 + UV動畫 來完成的。
考慮到 水平方向 和 垂直方向 水的流動方式不一樣,我們提供了兩套水紋貼圖。
水平方向的法帖如下:
垂直方向的法帖如下:
針對 水平的地表 和 垂直的墻壁 我們會選擇不同的法帖。
除去 水平法帖 和 垂直法帖 外,我們還提供了如下水紋參數:
Wet Bump Speed H 和 Wet Bump Spped V 分別代表 水平流速 和 垂直流速,這里的流速指是 UV動畫 的速度。
只有流速還不夠,我們還需要 流動方向:
對于 垂直流動,其 UV動畫的方向 永遠是 (0, 1),這和 垂直法帖 是一致的。
對于 水平流動,我們固定死了方向為 (0.5, 0.5),當然這里也可以開放出來給美術調整。
最終的水流方向是 水平方向 和 垂直方向 插值而成,權重由 法線的傾斜度 決定。
此外,為了保證水紋的連續性,場景中的渲染單元通過其 世界坐標的xz分量 來映射 水紋貼圖 的紋理坐標,Wet Bump World Scale 通過對 坐標的縮放 來控制 波紋的大小。
下圖是不同波紋大小的對比:
最后,我們還可以通過調整 Wet Bump Height Scale 來調整 波紋的強弱,Wet Bump Height Scale 的作用類似于 UnpackScaleNormal 的 BumpScale 參數。
好了,實現細節講的差不多了,下面貼代碼:
half3 SGameWetBump(half2 wetFactors, float3 worldPos, half3 worldNormal)
{
half worldNormalY = worldNormal.y;
float isVertical = (worldNormalY * worldNormalY) < 0.9;
float flowTime = _Time.y;
//豎直方向
float2 flowDir1 = float2(0, 1) * _WetBumpSpeedV;
float uvX1 = lerp(worldPos.x, worldPos.z, abs(worldNormal.x) > abs(worldNormal.z));
float uvY1 = worldPos.y;
float2 uv1 = float2(uvX1, uvY1);
float2 flowUV1 = float2(uv1 + flowTime * flowDir1) * _WetBumpWorldScale;
//xz平面方向
float2 flowDir2 = float2(0.5,0.5) * _WetBumpSpeedH;
float uvX2 = worldPos.x;
float uvY2 = worldPos.z;
float2 uv2 = float2(uvX2, uvY2);
float2 flowUV2 = float2(uv2 + flowTime * flowDir2) * _WetBumpWorldScale;
//根據是否垂直方向插值
float2 flowUV = lerp(flowUV1, flowUV2, 1 - isVertical);
#if defined(SGAME_WET_BUMP_MAP_H)
half3 flowBump = UnpackNormal(tex2D(_WetBumpMapH, flowUV));
#else
half3 flowBump = UnpackNormal(tex2D(_WetBumpMapV, flowUV));
#endif
half3 bumpNormal = lerp(half3(0,0,1), flowBump, wetFactors.x);
return bumpNormal;
}
注意,SGameWetBump 的返回值是 切線空間 下波紋的 法線偏移,我們把 法線偏移 和 原切線空間法線 相加,再轉到 世界空間 來計算光照,就可以表現出 水紋 的流動。
// 潮濕法線的計算
float3 wetNormal.xy = tangentNormal + wetBumpNormal.xy * _WetBumpHeightScale;
wetNormal.z = sqrt(1.0 - saturate(dot(wetNormal.xy, wetNormal.xy)));
我們的做法就介紹到這里,下面來看一下 楚留香 的做法。
楚留香的做法
通過分析代碼,我發現 楚留香 的 水紋 并不依賴 法線貼圖,而是通過 噪聲 生成。
這里的 噪聲算法 留待后續研究,先附上代碼:
float Hash(in float2 p)
{
return frac(((sin(dot(p,float2(127.1,311.7))))*(43758.5453)));
}
float Noise(float2 p)
{
float2 i = floor(p);
float2 f = frac(p);
float2 u = (f * f) * (3.0 - 2.0 * f);
return -1.0 + (2.0 * lerp(lerp(Hash(i + float2(0.0,0.0)), Hash(i + float2(1.0,0.0)),u.x), lerp(Hash(i + float2(0.0,1.0)), Hash(i+ float2(1.0,1.0)),u.x),u.y));
}
float SeaOctave(float2 uv)
{
uv += Noise(uv);
float2 wv = 1.0 - abs(sin(uv));
float2 swv = abs(cos(uv));
wv = lerp(wv,swv,wv);
return 1.0 - pow(wv.x * wv.y, 0.65);
}
float3 RippleNormal(in float3 N,in float2 uv)
{
float4 jitterUV;
half worldscale = 5;
worldscale = 1;
jitterUV = uv.xyxy * float4(1.5,5,5,1.5) * worldscale;
// 這里的CameraPosPS.w我用_Time.y代替,就有水流運動了,大家可以自行調節速度。
//float4 seed = clamp(N.xzxz * 10000,-1,1) * float4(20,20,6,6) * CameraPosPS.w;
float4 seed = clamp(N.xzxz * 10000,-1,1) * float4(20,20,6,6) * _Time.y;
float R1 = SeaOctave(jitterUV.yx*10 - seed.x) + SeaOctave(float2(jitterUV.z * 3 - seed.z, jitterUV.w * 3));
float R3 = SeaOctave(float2(jitterUV.xy*4 - seed.w)) + SeaOctave(jitterUV.zw * 8 - seed.y);
R3 *= 0.5;
float R_D = (R1 * N.x * N.x + R3 * N.z * N.z)* 5 + (R1+R3) * 0.1 - 0.212;
// 這里的 EnvInfo.x 可以控制水紋的強度
float EnvInfo = 0.6;
R_D *= (step(0.5,EnvInfo.x) * EnvInfo.x * 1.3);
return normalize(lerp((N + float3(0,0,R_D)),N,(1 - 0.2 * saturate(N.y))));
}
上述代碼我已經改到可以在Unity下運行了,如果需要水流效果,直接調用 RippleNormal 函數,傳入 WorldNormal 和 WorldPosition.xz 即可。
下圖是替換 楚留香 的水紋算法后,我們房頂的水流效果:
其他參考
最后貼一個Unity插件,我們的實現,當初也參考了這個插件: wet-animation-shaders,截圖如下:
原理大同小異,就不羅嗦了。
個人主頁
本文的個人主頁鏈接:https://baddogzz.github.io/2020/02/05/Wet-Waves/。
好了,拜拜。