UnityShader(5)

This time I’m going to use a surface shader to create a water effect. Scroll to the bottom first to read the code. If you don’t understand, then read the explanation below.

The first step is to judge the depth of water and distinguish between deep water areas and shallow water areas.

The concept of depth map needs to be used here. Without going into too many concepts, let’s just talk about how to implement it. First of all, our water surface is on the ground. We can use the depth map to calculate the distance depth from the human eye to the ground. The distance from the human eye to the water surface is recorded as IN. Proj.z

It can be seen that when we look at the deep water area, the difference in depth-IN.Proj.z will be larger than that in the shallow water area, so we can judge the deep water area and the shallow water area by the size of this difference. This IN is the input structure value of the surface shader of one of the colored patches on my water surface. Depth can be obtained by adjusting a series of APIs, which involves a lot of spatial transformations. I will not go into details here, but just give a general idea.

Then, after judging the depth of the water, we need to map our depth-IN.Proj.z to [0,1] by setting a dark water color and a light water color, so that by setting a lerp difference, lerp(_WaterShallowColor,_WaterDeepColor, min(_DepthRange, deltaDepth)/_DepthRange), you can get the color depth of the entire water area, and this mapping process can be achieved by setting another variable _DepthRange.

min(_DepthRange, deltaDepth)/_DepthRange, this means that when the water is very deep, min(_DepthRange, deltaDepth)=_DepthRange, the overall result is 1, so the dark water color is taken in the lerp function, when the water is very shallow When, min(_DepthRange, deltaDepth)=deltaDepth, the third parameter of lerp becomes deltaDepth/_DepthRange. Because the water is very shallow, this number is close to 0, and the color is close to light water. If the depth of the water is between deep water and Between shallow water, the color is also between deep water color and diving color. However, although deep water and shallow water are distinguished in this way, they are both solid colors and have no three-dimensional effect.

The second step is to achieve the three-dimensional effect of water by sampling the normal map.

The following code means that the normal map was originally assumed to be the i, jth patch sampled to the patch of my current model, but now it is not sampled to my current patch, but _WaterSpeed * _Time is added to the x direction For patches with the value .x, regardless of whether _Time. Line 0-1 corresponds to model 0-1, which is now equivalent to a larger sampling distance for the same point. Assume that normal line 0-1 corresponds to model 0-1.4. Model 1-1.4 will not be displayed, so it is equivalent to 0.6 times the original .

float4 bumpOffset1 = tex2D(_NormalTex, IN.uv_NormalTex + float2(_WaterSpeed * _Time.x, 0));

The remaining three calculations are the same, but it needs to be noted that _Time.x will change. In this way, the original normal map is assumed to be the i, jth patch sampled from the m, n patches of the model. After a while, it corresponds to the m + k, nth patch of the model. All the patches are mapped in this way, which is equivalent to the whole moving, that is, there is a big wave effect, surging.
float4 bumpOffset2 = tex2D(_NormalTex, float2(1 – IN.uv_NormalTex.y, IN.uv_NormalTex.x) + float2(_WaterSpeed * _Time.x, 0));
float4 offsetColor = (bumpOffset1 + bumpOffset2)/2;
float2 offset = UnpackNormal(offsetColor).xy * _Refract;
float4 bumpColor1 = tex2D(_NormalTex, IN.uv_NormalTex + offset + float2(_WaterSpeed * _Time.x, 0));
float4 bumpColor2 = tex2D(_NormalTex, float2(1 – IN.uv_NormalTex.y, IN.uv_NormalTex.x) + offset + float2(_WaterSpeed * _Time.x, 0));

The third step is to implement ripples, mainly for shallow water areas. The implementation of ripples requires sampling the WaveTex texture.

The WaveTex map is an alternating black and white picture. White means there are ripples, black means there are no ripples, but it is 0-1, corresponding to the model 0-1, but in fact we need to map a larger range of x to a smaller range. On the WaveTex map, you can use the sine wave mentioned in GPUGem to simulate water surface ripples.

First, we need to sample the waveform graph and the noise graph. The waveform graph is sampled twice to produce two ripples. The noise graph is used when sampling to produce a random effect. The above sine wave method is used for sampling the waveform graph. Pick it in such a way that it will have a wave effect.

fixed4 waveColor = tex2D(_WaveTex,float2(waveA + _WaveRange * sin(_Time.x * _WaveSpeed + noiseColor.r) , 1) + offset);
fixed4 waveColor2 = tex2D(_WaveTex,float2(waveA + _WaveRange * sin(_Time.x * _WaveSpeed + _WaveDelta + noiseColor.r) , 1) + offset);

Shader "Custom/MyWater2"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _MainTex ("Albedo (RGB)", 2D) = "white" {}
        _WaterShallowColor("WaterShallowColor", Color) = (1,1,1,1)
        _WaterDeepColor("WaterDeepColor", Color) = (1,1,1,1)
        _TranAmount("TranAmount", Range(0,100)) = 0.5
        _DepthRange("DepthRange",float) = 1
        _NormalTex("Normal",2D) = "bump"{}
        _WaterSpeed("WaterSpeed",float) = 5
        //Control the density of normals
        _Refract("Refract",float) = 0.5
        _Specular("Specular",float) = 1
        _Gloss("Gloss",float) = 0.5
        _SpecularColor("_SpecularColor", Color) = (1,1,1,1)
        //The wave effect requires sampling the wave image first, and the noise image is to produce random effects.
        _WaveTex("WaveTex",2D) = "white"{}
        _NoiseTex("NoiseTex",2D) = "white"{}
        _WaveSpeed("WaveSpeed",float) = 1
        _WaveRange("WaveRange",float) = 0.5
        _WaveRangeA("WaveRangeA",float) = 1
        _WaveDelta("WaveDelta", float) = 0.5
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" "Queue"="Transparent" }
        LOD 200
        //Water is transparent, so close ZWrite
        ZWrite off
        CGPROGRAM
        // Physically based Standard lighting model, and enable shadows on all light types
        #pragma surface surf WaterLight vertex:vert alpha noshadow

        // Use shader model 3.0 target, to get nicer looking lighting
        #pragma target 3.0
        //Adding float is to increase precision
        sampler2D_float _CameraDepthTexture;
        sampler2D _MainTex;
        fixed4 _WaterShallowColor;
        fixed4 _WaterDeepColor;
        half _TranAmount;
        half _DepthRange;
        sampler2D _NormalTex;
        half _WaterSpeed;
        sampler2D _WaveTex;
        sampler2D _NoiseTex;
        float _WaveSpeed;
        float _WaveRange;
        //These two control the range of the wave
        float _WaveRangeA;
        float _WaveDelta;

        struct input
        {
            float2 uv_MainTex;
            float4 proj;
            float2 uv_NormalTex;
            float2 uv_WaveTex;
            float2 uv_NoiseTex;
        };

        half _Glossiness;
        half _Metallic;
        fixed4 _Color;
        half _Refract;
        half _Specular;
        half _Gloss;
        fixed4 _SpecularColor;

        // Add instancing support for this shader. You need to check 'Enable Instancing' on materials that use the shader.
        // See https://docs.unity3d.com/Manual/GPUInstancing.html for more information about instancing.
        // #pragma instancing_options assumeuniformscaling
        UNITY_INSTANCING_BUFFER_START(Props)
            // put more per-instance properties here
        UNITY_INSTANCING_BUFFER_END(Props)

        fixed4 LightingWaterLight(SurfaceOutput s, fixed3 lightDir, half3 viewDir, fixed atten)
        {
            float diffuseFactor = max(0,dot(normalize(lightDir),s.Normal));
            half3 halfDir = normalize(lightDir + viewDir);
            float nh = max(0,dot(halfDir,s.Normal));
            float spec = pow(nh, s.Specular * 128) * s.Gloss;
            fixed4 c;
            c.rgb = (s.Albedo * _LightColor0.rgb * diffuseFactor + _SpecularColor.rgb * spec * _LightColor0.rgb) * atten;
            c.a = s.Alpha + spec * _SpecularColor.a;
            return c;
        }

        void vert(inout appdata_full v,out Input i)
        {
            UNITY_INITIALIZE_OUTPUT(Input, i);
            i.proj = ComputeScreenPos(UnityObjectToClipPos(v.vertex));
            COMPUTE_EYEDEPTH(i.proj.z);
        }

        void surf (Input IN, inout SurfaceOutput o)
        {
        //The key point is the depth map to determine deep water and shallow water, and the normal line movement to achieve the wave effect
            //tex2Dproj(_CameraDepthTexture, IN.proj)=tex2D(_CameraDepthTexture, IN.proj.xy/IN.proj.w)
            //Sample the depth map and sample screen space
            // Its uv represents the coordinates of the current model after the vertex shader draws it to the screen.
            //Each variable after sampling is the same, just take r
            //The closer the corresponding area on the depth map is, the redder it is (the depth value is close to 1), and the further away it is, the darker it is (the depth value is close to 0). That is to say, the depth value from 0 to 1 represents from far to near.
            half depth = LinearEyeDepth(tex2Dproj(_CameraDepthTexture, UNITY_PROJ_COORD(IN.proj)).r);
            //depth is equivalent to calculated like this
            //SAMPLE_DEPTH_TEXTURE_PROJ(_CameraDepthTexture, UNITY_PROJ_COORD(IN.proj));
            //UNITY_PROJ_COORD(IN.proj) is the returned texture coordinate after sampling. Most platforms are the returned IN.proj, so there is not much difference between writing it or not.
            //The depth value of the camera depth map minus the depth value of the ground model patch
            half deltaDepth = depth - IN.proj.z;
            
            fixed4 c = lerp(_WaterShallowColor,_WaterDeepColor, min(_DepthRange, deltaDepth)/_DepthRange);



            //Sample the normal map
            float4 bumpOffset1 = tex2D(_NormalTex, IN.uv_NormalTex + float2(_WaterSpeed * _Time.x, 0));
            float4 bumpOffset2 = tex2D(_NormalTex, float2(1 - IN.uv_NormalTex.y, IN.uv_NormalTex.x) + float2(_WaterSpeed * _Time.x, 0));
            float4 offsetColor = (bumpOffset1 + bumpOffset2)/2;
            float2 offset = UnpackNormal(offsetColor).xy * _Refract;
            float4 bumpColor1 = tex2D(_NormalTex, IN.uv_NormalTex + offset + float2(_WaterSpeed * _Time.x, 0));
            float4 bumpColor2 = tex2D(_NormalTex, float2(1 - IN.uv_NormalTex.y, IN.uv_NormalTex.x) + offset + float2(_WaterSpeed * _Time.x, 0));
            
            //The waves can be opposite to the depth of the water. min(_DepthRange, deltaDepth)/Where _DepthRange is close to 1, there should be no waves.
            //Because waves will appear at the edge of the water, min(_DepthRange, deltaDepth)/_DepthRange is close to 1, which means that deltadepth is very large and the water is very deep.
            half waveA = 1 - min(_WaveRangeA,deltaDepth)/_WaveRangeA;
            fixed4 noiseColor = tex2D(_NoiseTex,IN.uv_NoiseTex);
            //sin function to sample the ripple image to produce a flowing effect
            fixed4 waveColor = tex2D(_WaveTex,float2(waveA + _WaveRange * sin(_Time.x * _WaveSpeed + noiseColor.r) , 1) + offset);
            fixed4 waveColor2 = tex2D(_WaveTex,float2(waveA + _WaveRange * sin(_Time.x * _WaveSpeed + _WaveDelta + noiseColor.r) , 1) + offset);
            o.Albedo = c.rgb + (waveColor.rgb + waveColor2.rgb) * waveA;
            o.Normal = UnpackNormal((bumpColor1 + bumpColor2)/2).xyz;
            o.Gloss = _Gloss;
            o.Specular = _Specular;


            o.Alpha = min(_TranAmount,deltaDepth) / _TranAmount;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

Normal map, noise map, ripple map network disk link

Link: https://pan.baidu.com/s/1Qdm3ly2YW9vq9lNqYvZnbA?pwd=jk3l
Extraction code: jk3l