水纹
疫情肆掠,在家坐月子坐到傻了,开始写点文章找回状态。
本文是关于我们游戏 水纹 效果的实现细节。
这里的 水纹 效果是 天气系统 的一部分,主要表现 雨水流动 的效果,如下图:
本文会先介绍一下我们的实现方式,再对比一下其他实现。
我们的实现
常见的水波纹效果,一般都是依靠 法线贴图 + 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 参数。
好了,实现细节讲的差不多了,下面贴代码:
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
31
32
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 的返回值是 切线空间 下波纹的 法线偏移,我们把 法线偏移 和 原切线空间法线 相加,再转到 世界空间 来计算光照,就可以表现出 水纹 的流动。
1
2
3
// 潮湿法线的计算
float3 wetNormal.xy = tangentNormal + wetBumpNormal.xy * _WetBumpHeightScale;
wetNormal.z = sqrt(1.0 - saturate(dot(wetNormal.xy, wetNormal.xy)));
我们的做法就介绍到这里,下面来看一下 楚留香 的做法。
楚留香的做法
通过分析代码,我发现 楚留香 的 水纹 并不依赖 法线贴图,而是通过 噪声 生成。
这里的 噪声算法 留待后续研究,先附上代码:
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
31
32
33
34
35
36
37
38
39
40
41
42
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,截图如下:
原理大同小异,就不罗嗦了。
好了,拜拜。