卡通风格的水面效果—ToonWater

卡通风格的水面效果—ToonWater

今天我们来实现一个卡通风格的水面效果,我们游戏是上帝视角的,玩家只能平移不能旋转,所以水面的Shader计算会有很多简化的地方,毕竟是要用在移动端上面的。先看看最终的效果视频:

卡通风格的水面效果—ToonWaterhttps://www.zhihu.com/video/1240653770318274560

我们先来拆分一下要实现的效果:

1.浅滩到深水区的颜色渐变

2.水波纹往岸边推进

3.水底纹理的扭曲

4.白色浪花泡沫往岸边推进消失

大概的实现思路:浅滩到深水区的颜色渐变以及水底想要显示的内容,全部绘制在贴图上。通过UV动画采样一张法线贴图,来实现水波的推进。再用一张Noise来扭曲主纹理,以及浪花泡沫的效果。

首先我们来制作水面的模型,岸边的形状是弯曲不规则的,而我们需要模型的UV又一个正规的矩形,这样看起来水波纹往岸边推进才会显示正确。

水面Mesh
水面UV
水面纹理贴图
Shader渲染的结果

接下来我们用一张Noise扭曲来模拟下水底部折射的效果

fixed4 frag (v2f i) : SV_Target
{
    fixed2 samplerNoise = fixed2(tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.xy + float2(_WaveControl.w * _Time.y, 0)).r * _WaveControl.z, 0);
    fixed4 col = tex2D(_MainTex, i.uv + samplerNoise.x);
    return col;
}

_WaveControl.w是控制扭曲的速度,_WaveControl.z是控制扭曲强度的

https://www.zhihu.com/video/1240688003850924032

现在再来给水面加上水波纹的效果,这里需要用到法线贴图,计算也是在切线空间下的

v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv.xy = v.uv;

                TANGENT_SPACE_ROTATION;
                o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));
                return o;
            }
fixed4 frag (v2f i) : SV_Target
            {
                float2 samplerUV = float2(_Time.y * _WaveControl.x, 0);
                float3 normalDir = normalize(UnpackNormal(tex2D(_NormalTex, TRANSFORM_TEX(i.uv, _NormalTex) + samplerUV * 0.25)));
               
                float vdn = saturate(pow(dot(i.viewDir, normalDir), _WaveControl.y));
                return vdn;
            }

_WaveControl.x是控制波纹移动的速度

先看下法线计算出来的运动效果

https://www.zhihu.com/video/1240692532767232000

是不是觉得怪怪的不好看,我们在采样的时候再加上之前的Noise扭曲来看下效果

v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv.xy = v.uv;

                TANGENT_SPACE_ROTATION;
                o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));
                return o;
            }
fixed4 frag (v2f i) : SV_Target
            {
                fixed2 samplerNoise = fixed2(tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.xy + float2(_WaveControl.w * _Time.y, 0)).r * _WaveControl.z, 0);
                float2 samplerUV = float2(_Time.y * _WaveControl.x, 0);
                float3 normalDir = normalize(UnpackNormal(tex2D(_NormalTex, TRANSFORM_TEX(i.uv, _NormalTex) + samplerUV * 0.25 + samplerNoise)));
               
                float vdn = saturate(pow(dot(i.viewDir, normalDir), _WaveControl.y));
                return vdn;
            }
https://www.zhihu.com/video/1240693540822650880

这样看起来是不是就有点意思了

注意观察下,我们水面实际上要用的效果,其实是把这个计算得到的法线结果反向一下,作为水面的高光部分

v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv.xy = v.uv;

                TANGENT_SPACE_ROTATION;
                o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));
                return o;
            }
fixed4 frag (v2f i) : SV_Target
            {
                fixed2 samplerNoise = fixed2(tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.xy + float2(_WaveControl.w * _Time.y, 0)).r * _WaveControl.z, 0);
                float2 samplerUV = float2(_Time.y * _WaveControl.x, 0);
                float3 normalDir = normalize(UnpackNormal(tex2D(_NormalTex, TRANSFORM_TEX(i.uv, _NormalTex) + samplerUV * 0.25 + samplerNoise)));
               
                float vdn = saturate(pow(dot(i.viewDir, normalDir), _WaveControl.y));
                fixed4 col = tex2D(_MainTex, i.uv + samplerNoise.x) * (2 - vdn);
                return col;
            }

_WaveControl.y是控制法线高光的强度

https://www.zhihu.com/video/1240732368681717760

最后我们来实现这个白色的浪花泡沫效果,刚开始的时候也不好想这个效果要怎么实现出来。后来就是把它当做一个简单的溶解消失来做,然后再反向思考下,只要得到一个动态的白色遮罩区域就可以了。这里我是把这个区域写在了模型的顶点颜色里面,所以我们先来看下怎么写入顶点色。

R通道写入的是半透明区域,也就是岸边的一个过度。

G通道写入的是白色浪花出现的区域

Mesh Vertex Color
R通道,半透明区域
G通道,白色浪花区域

在Shader中用G通道的区域个和一个移动的Noise,计算可以得到动态的消散遮罩区域

v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv.xy = v.uv;
                o.color = v.color;

                TANGENT_SPACE_ROTATION;
                o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));
                return o;
            }
fixed4 frag (v2f i) : SV_Target
            {
                fixed2 samplerNoise = fixed2(tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.xy + float2(_WaveControl.w * _Time.y, 0)).r * _WaveControl.z, 0);
                float2 samplerUV = float2(_Time.y * _WaveControl.x, 0);
                fixed distortNoise = tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.xy + samplerUV + samplerNoise).r;
                fixed waveMask = saturate((i.color.g - distortNoise) * 30);
                return fixed4(waveMask, waveMask, waveMask, 1);
            }
https://www.zhihu.com/video/1240726937079021568

然后再用采样的浪花贴图和得到的遮罩区域做一个乘法就可以得到最终的浪花效果了

v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv.xy = v.uv;
                o.color = v.color;

                TANGENT_SPACE_ROTATION;
                o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));
                return o;
            }
fixed4 frag (v2f i) : SV_Target
            {
                fixed2 samplerNoise = fixed2(tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.xy + float2(_WaveControl.w * _Time.y, 0)).r * _WaveControl.z, 0);
                float2 samplerUV = float2(_Time.y * _WaveControl.x, 0);
                fixed distortNoise = tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.xy + samplerUV + samplerNoise).r;
                fixed waveMask = saturate((i.color.g - distortNoise) * 30);
                fixed waveTex = tex2D(_WaveTex, i.uv * _WaveTex_ST.xy + samplerUV + samplerNoise).r * waveMask;
                return fixed4(waveTex , waveTex , waveTex , 1);
            }
https://www.zhihu.com/video/1240729121438986240

最后把上面的效果都合起来,用顶点颜色的R通道作为水面的透明度,完整代码如下:

Shader "Custom/LX_ToonWater"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _NormalTex ("Normal", 2D) = "bump" {}
        _SamplerNoise ("SamplerNoise", 2D) = "white" {}
        //x:WaveSpeed y:Reflection z:NoiseStrength w:DistortSpeed
        _WaveControl ("WaveControl", vector) = (0,0,0,0)
        _WaveTex ("WaveTexture", 2D) = "black" {}
    }
    SubShader
    {
        Tags 
        { 
            "RenderType" = "Transparent"
            "Queue" = "Transparent"
            "IgnoreProjector" = "True" 
        }
        LOD 100

        Pass
        {
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma fragmentoption ARB_precision_hint_fastest

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
                float3 normal : NORMAL;
                float4 tangent : TANGENT;
                float4 color : COLOR;
            };

            struct v2f
            {    
                float4 vertex : SV_POSITION;
                float2 uv : TEXCOORD0;
                float3 viewDir : TEXCOORD1;
                float4 color : TEXCOORD2;
            };

            sampler2D _MainTex;
            sampler2D _NormalTex; float4 _NormalTex_ST;;
            sampler2D _SamplerNoise; float4 _SamplerNoise_ST;
            sampler2D _WaveTex; float4 _WaveTex_ST;
            fixed4 _WaveControl;;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                o.color = v.color;

                TANGENT_SPACE_ROTATION;
                o.viewDir = normalize(mul(rotation, ObjSpaceViewDir(v.vertex)));

                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed2 samplerNoise = fixed2(tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.xy + float2(_WaveControl.w * _Time.y, 0)).r * _WaveControl.z, 0);
                float2 samplerUV = float2(_Time.y * _WaveControl.x, 0);

                float3 normalDir = normalize(UnpackNormal(tex2D(_NormalTex, TRANSFORM_TEX(i.uv, _NormalTex) + samplerUV * 0.25 + samplerNoise)));
                float vdn = saturate(pow(dot(i.viewDir, normalDir), _WaveControl.y));

                fixed distortNoise = tex2D(_SamplerNoise, i.uv * _SamplerNoise_ST.zw + samplerUV + samplerNoise).r;
                fixed waveMask = saturate((i.color.g - distortNoise) * 30);
                fixed waveTex = tex2D(_WaveTex, i.uv * _WaveTex_ST.xy + samplerUV + samplerNoise).r * waveMask;

                fixed4 col = tex2D(_MainTex, i.uv + samplerNoise.x) * (2 - vdn);
                col.rgb += waveTex;
                col.a = i.color.r;
                return col;
            }
            ENDCG
        }
    }
}

写在最后,无论是之前做影视还是现在做游戏,水的效果都是最难搞的,动态的物理模拟和渲染都非常复杂的。这个效果是因为游戏应用场景比较简单,所以很多地方都简化了,没有那些复杂的计算。每种效果都要根据实际项目的应用场景,来制定实现效果的解决方案,没有那么多直接拿来就能用的。其实最后就是想说,别在后台私信问我要Demo工程什么的,甚至是连贴图都要的,已经写的很详细了吧,要是连合适的贴图都不去找,不愿意直接动手做一下的话,也太那个啥了吧......(伸手党!)

发布于 2020-05-04 20:27