潮湿效果之水纹

1,102 阅读4分钟

水纹

疫情肆掠,在家坐月子坐到傻了,开始写点文章找回状态。

本文是关于我们游戏 水纹 效果的实现细节。

这里的 水纹 效果是 天气系统 的一部分,主要表现 雨水流动 的效果,如下图:

本文会先介绍一下我们的实现方式,再对比一下其他实现。

我们的实现

常见的水波纹效果,一般都是依靠 法线贴图 + UV动画 来完成的。

考虑到 水平方向垂直方向 水的流动方式不一样,我们提供了两套水纹贴图。

水平方向的法帖如下:

垂直方向的法帖如下:

针对 水平的地表垂直的墙壁 我们会选择不同的法帖。

除去 水平法帖垂直法帖 外,我们还提供了如下水纹参数:

Wet Bump Speed HWet Bump Spped V 分别代表 水平流速垂直流速,这里的流速指是 UV动画 的速度。

只有流速还不够,我们还需要 流动方向

  • 对于 垂直流动,其 UV动画的方向 永远是 (0, 1),这和 垂直法帖 是一致的。

  • 对于 水平流动,我们固定死了方向为 (0.5, 0.5),当然这里也可以开放出来给美术调整。

最终的水流方向是 水平方向垂直方向 插值而成,权重由 法线的倾斜度 决定。

此外,为了保证水纹的连续性,场景中的渲染单元通过其 世界坐标的xz分量 来映射 水纹贴图 的纹理坐标,Wet Bump World Scale 通过对 坐标的缩放 来控制 波纹的大小

下图是不同波纹大小的对比:

最后,我们还可以通过调整 Wet Bump Height Scale 来调整 波纹的强弱Wet Bump Height Scale 的作用类似于 UnpackScaleNormalBumpScale 参数。

好了,实现细节讲的差不多了,下面贴代码:

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 函数,传入 WorldNormalWorldPosition.xz 即可。

下图是替换 楚留香 的水纹算法后,我们房顶的水流效果:

其他参考

最后贴一个Unity插件,我们的实现,当初也参考了这个插件: wet-animation-shaders,截图如下:

原理大同小异,就不罗嗦了。

个人主页

本文的个人主页链接:baddogzz.github.io/2020/02/05/…

好了,拜拜。