Home Simple water ripple (without RenderTexture and Particle System)
Post
Cancel

Simple water ripple (without RenderTexture and Particle System)

The generation of simple water ripples only needs the center position information, and the rest of the artistic effects can be achieved in the shader.

Since the shader cannot temporarily store the information, the script needs to pass the character position information to the shader.

(Can the geometry shader store information?)

Method

  1. Store character positions in an array at intervals Vector4 waterRipples[]

  2. Store individual water ripple lifetimes in waterRipples.w

    (Time preferably needs to take into account whether the character moves, jumps, etc.)

  3. Pass waterRipples to Shader

  4. Calculate all water ripples in the shader

Custom Fuction:

customfunction

  • A single water ripple code is as follows:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    
    float RipplesTransform(float time, float3 positionWS, float4 center, float radius = 3, float speed = 0.5, float noise = 0)
    {
        float dist = distance((center.xyz + float3(0, 0.5, 0)), (positionWS + noise) * float3(1, 1, 2));
        float changingRadius = center.w;
        changingRadius = 1 - changingRadius;
        changingRadius = 1 - pow(changingRadius,3);
        float opacity = sin(changingRadius * 3.1415926);
      
        float rings = abs(frac(dist - (time * speed)) - 0.5) * 2.0;
        rings  = pow(rings,4);
        float mask = RemapFloat01(dist, float2(0, radius * changingRadius));
        mask = 1.0 - mask;
        mask *= opacity;
      
        return rings * mask;
    }
    
  • dist is the distance to the center point, where float3(0, .5, 0) is the height offset of the center point, used to adjust the height of the center position of the water ripple.

    float3(1, 1, 2) is It is used to scale the distance in the z direction, so that an elliptical water ripple is obtained (if the character is not on the plane with the z axis of 0, it cannot be used directly)

  • RemapFloat01() maps a range to 01 (custom)

  • changingRadius is a value from 0 to 1 that controls the radius change and transparency.

    The change of radius needs to be fast and then slow (blue curve), and the change of transparency also needs to be fast, then slow and finally disappear (orange curve).

  • rings is to loop through frac() and abs().

  • void SetWaterRipples() is used to perform calculations and output results to shadergraph.

    1
    2
    3
    4
    5
    6
    7
    8
    
    void SetWaterRipple_float(
        float3 posWS, float time, float noise, out float WaterRipples)
    {
        for (int i = 0; i < _ripplesParticlesSize; i++)
        {
            WaterRipples = lerp(RipplesTransform(time, posWS, _waterRipples[i], 7, 0.36, noise), 1, WaterRipples);
        }
    }
    

The Custom Function reference code is as follows:

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
//-----Unity ShaderGraph Custom Function-----

uniform float4 _waterRipples[10];
uniform int _ripplesParticlesSize;

float RemapFloat01(float In, float2 InMinMax)
{
    return clamp((In - InMinMax.x) / (InMinMax.y - InMinMax.x), 0, 1);
}

//generate according to center pos
float RipplesTransform(float time, float3 positionWS, float4 center, float radius = 3, float speed = 0.5, float noise = 0)
{
    float dist = distance((center.xyz + float3(0, 0.5, 0)), (positionWS + noise * .7) * float3(1, 1, 2));
    float changingRadius = center.w;
    changingRadius = 1 - changingRadius;
    changingRadius = 1 - pow(changingRadius,3);
    float opacity = sin(changingRadius * 3.1415926);

    float rings = abs(frac(dist - (time * speed)) - 0.5) * 2.0;
    rings  = pow(rings,4);
    float mask = RemapFloat01(dist, float2(0, radius * changingRadius));
    mask = 1.0 - mask;
    mask *= opacity;

    return rings * mask;
}

void SetWaterRipple_float( float3 posWS, float time, float noise,

                    out float WaterRipples)
{
    for (int i = 0; i < _ripplesParticlesSize; i++)
    {
        WaterRipples = lerp(RipplesTransform(time, posWS, _waterRipples[i], 7, 0.36, noise), 1, WaterRipples);
    }
}

Script:

  • ripplesParticlesSize and waterRipples[] need to be passed to the shader. Where waterRipples[].xyz represents the center position of the water ripple, and waterRipples[].w represents the survival time of the water ripple;

  • waterRipples[].w can be affected by time_dissolving and time_jumping, time_jumping is determined by time_grounded and time_air.

    In this way, the value of waterRipples[].w can be roughly used to represent the different states of the character when moving, when taking off, when in the air and when landing.

reference code:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
//-----C# Script-----

public class SetWaterRipples : MonoBehaviour
{
    private static int ripplesParticlesSize = 9;
    private int count;
    private Vector4[] waterRipples = new Vector4[ripplesParticlesSize];
    private bool[] startSpreading = new bool[ripplesParticlesSize];
    private float[] time_ripple = new float[ripplesParticlesSize];

    [SerializeField] private float interval = .2f;
    private float duration;
    private float time;
    private float time_dissolving;
    private Transform playerTr;
    private Vector3 playerPos;
    private CharacterMovement _characterMovement;
    private Controller _controller;
    void Start()
    {
        playerTr = GameObject.FindWithTag("Player").transform;
        _characterMovement = playerTr.gameObject.GetComponent<CharacterMovement>();
        _controller = playerTr.gameObject.GetComponent<Controller>();
        
        duration = (ripplesParticlesSize) * interval;
        Shader.SetGlobalInt("_ripplesParticlesSize", ripplesParticlesSize);
        
        for (int i = 0; i < ripplesParticlesSize; i++)
        {
            startSpreading[i] = false;
        }
    }
    
    void Update()
    {
        playerPos = playerTr.position;
        Timer();

        if (time >= interval)
        {
            waterRipples[count] = playerPos + new Vector3(Random.Range(-.3f, .3f), 0f, Random.Range(-1f, 1f));
            startSpreading[count] = true;
            
            count++;
            if (count > ripplesParticlesSize-1)
            {
                count = 0;
            }
            time = 0f;
        }
        Shader.SetGlobalVectorArray("_waterRipples", waterRipples);
    }

    private bool isGrounded = false;
    private float time_grounded;
    private float time_air;
    private float time_jumping;
    void Timer()
    {
        time += Time.deltaTime;
        for (int i = 0; i < ripplesParticlesSize; i++)
        {
            if (startSpreading[i])
            {
                time_ripple[i] += Time.deltaTime;
              	//combination of ripple appreance
                waterRipples[i].w = Mathf.Clamp01(Mathf.InverseLerp(0f, duration, time_ripple[i])) * Mathf.Lerp(time_dissolving, 1f,
                    (1f - time_jump) * Mathf.Sin(Mathf.PI * (1f - Mathf.Pow((1f - time_jump), 3f)) ));
                if (time_ripple[i] >= duration)
                {
                    startSpreading[i] = false;
                    time_ripple[i] = 0f;
                }
            }
        }
        
        //appear when jumping
        isGrounded = _controller.State.IsGrounded;
        if (isGrounded)
        {
            time_air = 0f;
            
            time_grounded += Time.deltaTime * .6f;
            if (time_grounded >= 1f)
            {
                time_grounded = 1f;
            }
        }
        else
        {
            time_grounded = 0f;
            time_air += Time.deltaTime * 1f;
            if (time_air >= 1f)
            {
                time_air = 1f;
            }
        }
        time_jumping = Mathf.Lerp(time_grounded, 1f, time_air);
        
        //appear when moving
        if (_characterMovement._horizontalMovement != 0f)
        {
            time_dissolving += Time.deltaTime * 1f;
            if (time_dissolving >= 1f)
            {
                time_dissolving = 1f;
            }
        }
        else
        {
            time_dissolving -= Time.deltaTime * 0.3f;
            if (time_dissolving <= 0f)
            {
                time_dissolving = 0f;
            }
        }
    }
}

Extends

Add sparkling effect to water surface

The effect is more noticeable at a distance.

This can be done using positionWS.z combined with Screen Position to add noise at the end

(Below is the reference of screen space near and far)

Demo:

Final Shadergraph:

This post is licensed under CC BY 4.0 by the author.

2D crowd animation using ShaderGraph

-