Implementation of Unity3D simple water shader

Implementation ideas

Today we will implement a simple water surface effect. The water surface is mainly the superposition of the following effects
1. The dynamic wave effect of the water surface, we use the simplest sine wave plus normal map to simulate here. If you want a more accurate wave effect, you can consider using Gerstner wave
2. Different water depths have different colors, use the difference between the vertex depth and vertex coordinates to calculate the depth, and then map to the color
3. Surge effect, or according to the vertex depth The difference with the vertex coordinates is used to calculate the depth, and then superimpose a layer of surge
4. Refraction effect, use grabpass, offset after sampling, and finally superimpose it.

Code

The first is the vertex displacement function, which is calculated by substituting the sine function into the x-coordinate and z-coordinate of the vertex, because the water surface effect written here is a plane effect rather than a volumetric water effect. So no need to calculate the y axis. _WaveSpeed ​​and _WaveGap are used to control wave speed and spacing. Finally, each vertex is offset towards the vertex's normal direction.

        void vertexDataFunc(inout appdata_full v, out Input o)
        {
    
    
            UNITY_INITIALIZE_OUTPUT(Input, o);
            fixed height = sin(_Time.y * _WaveSpeed + v.vertex.z * _WaveGap + v.vertex.x) * _WaveHeight;
            v.vertex.xyz += v.normal * height;
        }

For the water surface depth effect, first declare the screenPos variable in the input to obtain the coordinate value between [0,w], and divide xyz by w to obtain the uv coordinate value of [0,1].
Use the uv coordinate value to sample the _CameraDepthTexture depth texture to obtain the depth value of the point, and use LinearEyeDepth to convert it into a linear depth value. Then use LinearEyeDepth(IN.screenPos.z) to get the actual depth of the vertex. LinearEyeDepth actually converts the nonlinear depth value between [0,1] into a linear depth value.
One doubt is why the two depth values ​​are different. This is because transparent objects will not be written into the depth texture, so reading the depth value of a certain screen coordinate is actually finding the first opaque point. Get his depth value, and if this point is transparent, then the depth value read in the depth texture is not his depth value. But in any case, the actual linear depth value of the vertex is obtained directly by using LinearEyeDepth(IN.screenPos.z).
So the difference between these two depths is actually the difference in the distance between the first opaque point after passing through a transparent point and the transparent point.

			IN.screenPos.xyz /= IN.screenPos.w;
            //深度纹理中的深度
            float depth1 = LinearEyeDepth(tex2D(_CameraDepthTexture, IN.screenPos.xy).r);
            //当前顶点的深度
            float depth2 = LinearEyeDepth(IN.screenPos.z);
            float distance = abs(depth1 - depth2);
            //深度插值颜色
            fixed4 waterColor = lerp(_EdgeColor, _Color, saturate(distance * _EdgeThreshold));

Set the tag, "RenderType" = "Transparent" is to not write the depth value, "Queue" = "Transparent" is to make the shader render after all opaque objects are rendered, so that the grabpass can get the correct image, two The purpose of the Transparent setting is different

 Tags
        {
    
    
            "RenderType" = "Transparent" "Queue" = "Transparent" 
        }

The surge effect is still calculated based on the depth difference. If it is judged that the depth difference is less than the threshold of _FoamThreshold, the surge map is superimposed with a layer of effect.

			//form
            float4 foamColor = tex2D(_FoamTex, (IN.uv_FoamTex + fixed2(1, 1) * _Time.x * _FoamSpeed) * 20);
            fixed formDegree = clamp(distance, 0, _FoamThreshold);
            //把0~_FoamThreshold 缩放到0~1
            formDegree = formDegree / _FoamThreshold;
            fixed4 formColor = lerp(foamColor,fixed4(0, 0, 0, 0), formDegree) * _FoamColorScale;

To achieve the sparkling effect of the water surface also needs the help of the normal map. Here, the normal map is sampled twice. When sampling, the uv is offset in different directions according to the time. Finally, the two normals are superimposed, and there will be a shimmering effect. Um. . But I don't know what the principle is. Anyway, it is enough to write the shader that looks right. I have seen several water shaders that also have this kind of treatment.

 			//法线
            float2 speed = _Time.x * float2(_WaveSpeed, _WaveSpeed) * _NormalSpeed;
            fixed3 bump1 = UnpackNormal(tex2D(_NormalTex, IN.uv_FoamTex.xy + speed)).rgb;
            fixed3 bump2 = UnpackNormal(tex2D(_NormalTex, IN.uv_FoamTex.xy - speed)).rgb;
            fixed3 bump = normalize(bump1 + bump2);
            bump.xy *= _NormalScale;
            bump = normalize(bump);
            o.Normal = bump;

For refraction, it is the _RefractionTex declared by grabpass before sampling, and then offset according to the normal to get the refraction effect

 			//折射
            float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
            fixed3 refrCol = tex2D(_RefractionTex, IN.screenPos.xy + offset).rgb;

Finally, superimpose the calculated colors

            o.Albedo = waterColor.rgb * refrCol + formColor.rgb;
            o.Alpha = waterColor.a;

final effect.
Please add a picture description
the gif is a bit blurry
Please add a picture description

parameter panel
Please add a picture description

full code

Shader "LX/simpleWater"
{
    
    
    Properties
    {
    
    
        _Color ("Color", Color) = (0,0,1,1)
        _EdgeColor ("EdgeColor", Color) = (0,1,1,1)
        _EdgeThreshold ("EdgeThreshold", float) =0.1
        _WaveHeight ("WaveHeight", float) = 1
        _WaveSpeed ("WaveSpeed", float) = 1
        _WaveGap ("WaveGap", float) = 1
        _FoamTex ("FoamTex", 2D) = "white"
        _FoamThreshold ("FoamThreshold", float) =0.1
        _FoamColorScale ("FoamColorScale", float) =0.2
        _FoamSpeed ("FoamSpeed", float) =0.1
        _NormalTex ("NormalTex", 2D) = "white"
        _NormalScale ("NormalScale", float) = 1
        _NormalSpeed ("NormalSpeed", float) = 1
        _Distortion("Distortion",float) = 1


    }
    SubShader
    {
    
    
        Tags
        {
    
    
            "RenderType" = "Transparent" "Queue" = "Transparent" "IgnoreProjector" = "True"
        }
        LOD 200

        GrabPass
        {
    
    
            "_RefractionTex"
        }
        CGPROGRAM
        #pragma surface surf Standard alpha:fade keepalpha fullforwardshadows vertex:vertexDataFunc
        #pragma target 3.0

        sampler2D _MainTex;

        struct Input
        {
    
    
            float2 uv_FoamTex;
            float4 screenPos;
        };

        sampler2D _CameraDepthTexture;
        fixed4 _Color;
        fixed4 _EdgeColor;
        float _WaveHeight;
        float _WaveSpeed;
        float _WaveGap;
        float _EdgeThreshold;
        float _FoamThreshold;
        sampler2D _FoamTex;
        float _FoamColorScale;
        sampler2D _NormalTex;
        float _NormalScale;
        float _NormalSpeed;
        float _FoamSpeed;
        float _Distortion;

        sampler2D _RefractionTex;
        float4 _RefractionTex_TexelSize;

        void vertexDataFunc(inout appdata_full v, out Input o)
        {
    
    
            UNITY_INITIALIZE_OUTPUT(Input, o);
            fixed height = sin(_Time.y * _WaveSpeed + v.vertex.z * _WaveGap + v.vertex.x) * _WaveHeight;
            v.vertex.xyz += v.normal * height;
        }

        void surf(Input IN, inout SurfaceOutputStandard o)
        {
    
    
            IN.screenPos.xyz /= IN.screenPos.w;
            //深度纹理中的深度
            float depth1 = LinearEyeDepth(tex2D(_CameraDepthTexture, IN.screenPos.xy).r);
            //当前顶点的深度
            float depth2 = LinearEyeDepth(IN.screenPos.z);
            float distance = abs(depth1 - depth2);
            //深度插值颜色
            fixed4 waterColor = lerp(_EdgeColor, _Color, saturate(distance * _EdgeThreshold));

            //form
            float4 foamColor = tex2D(_FoamTex, (IN.uv_FoamTex + fixed2(1, 1) * _Time.x * _FoamSpeed) * 20);
            fixed formDegree = clamp(distance, 0, _FoamThreshold);
            //把0~_FoamThreshold 缩放到0~1
            formDegree = formDegree / _FoamThreshold;
            fixed4 formColor = lerp(foamColor,fixed4(0, 0, 0, 0), formDegree) * _FoamColorScale;

            //法线
            float2 speed = _Time.x * float2(_WaveSpeed, _WaveSpeed) * _NormalSpeed;
            fixed3 bump1 = UnpackNormal(tex2D(_NormalTex, IN.uv_FoamTex.xy + speed)).rgb;
            fixed3 bump2 = UnpackNormal(tex2D(_NormalTex, IN.uv_FoamTex.xy - speed)).rgb;
            fixed3 bump = normalize(bump1 + bump2);
            bump.xy *= _NormalScale;
            bump = normalize(bump);
            o.Normal = bump;

            //折射
            float2 offset = bump.xy * _Distortion * _RefractionTex_TexelSize.xy;
            fixed3 refrCol = tex2D(_RefractionTex, IN.screenPos.xy + offset).rgb;

            o.Albedo = waterColor.rgb * refrCol + formColor.rgb;
            o.Alpha = waterColor.a;
        }
        ENDCG
    }
    FallBack "Diffuse"
}

In addition, the code has also been passed to the github warehouse, you can also pay attention to it~
my github

Guess you like

Origin blog.csdn.net/o83290102o5/article/details/120296344