How to make a basic 2D ripple shader effect.

How can I make a shader that makes expanding circles that gradually fade away of time on a given UV location?
I know some basic things about shaders but I can’t wrap my head around this. Looking online I can only really find explanations on how to do in 3D with a vertex shader, but not fragment shader which is what I think I need for this.

Something like this but with a clear background:

Anyone any idea’s how I could make something like that?

There are three main questions here.
How do you draw a circle in a fragment shader.
How do you animate something in a shader.
And possibly, how do you have multiple “things” in one shader.

For the “how do you draw a circle” question, that’s actually pretty easy.

The short version is take a position (like the UV) and get the distance from that position to the circle center position. Then use the circle radius you want and fwidth() to sharpen. In the example I linked to, it’s using a hard coded center of 0.5, 0.5 and hard coded radius of 0.5, since by default a quad mesh has a UV range of 0.0 to 1.0, so that’ll draw a filled circle that covers the entire quad. But you can replace those with whatever you want.

You also have to make sure the UVs you’re using have a square aspect ratio on whatever you’re rendering. If you’re trying to use screen space UVs, you’ll want to use the screen resolution to modify the UVs before you do the above so you don’t get ellipses.

To make it a 1 pixel ring instead of a filled circle, you can do two circles, or subtract the radius from the distance and use the absolute value of that.

For the “how do you animate something” question, you either have to do this from c#, or bake the animation into a texture you can sample, or write a function that takes a time and outputs the relevant values. It’s pretty common to animate something in a shader using nothing but the global time (_Time.y) and a few math functions. Something like frac(_Time.y) will get you a value that repeats going from 0.0 to just below 1.0 every second. Multiply the time value to adjust the speed. Add some value to the time to offset it if you want more than one thing animated, or apply a smoothstep or power to the value to change the curve. And then use a lerp(a,b,t) to blend between two values with the still 0.0 to 1.0 value.

For the last part of “how do you have multiple things”, well, you do the above multiple times, with different values. If you want 10 rings to appear, then for every pixel you calculate all 10 rings all the time.

1 Like

Thanks! I think I figured most of it out. Though I still don’t understand how to do multiple rings. (And I my alpha fading still has issues if you change the speed, as the timing value for it is hard coded) Beyond that it seems to work great!

Here is what I have:

Shader "Unlit/Ripple"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        _Size ("Size", float) = 1
        _XPos ("X Postion", float) = .5
        _YPos ("Y Postion", float) = .5
    }
    SubShader
    {
        Tags { "Queue"="Transparent" "IgnoreProjector"="True" "RenderType"="Transparent" }
        Blend SrcAlpha OneMinusSrcAlpha
        ColorMask RGB
        ZWrite Off
        Cull Off
        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
        
            #include "UnityCG.cginc"
            struct appdata
            {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
            };
            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            float _XPos;
            float _YPos;
        
            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                //change location
                o.uv = v.texcoord - float2(_XPos, _YPos);
                return o;
            }
            fixed4 _Color;
            float _Size;
            fixed4 frag (v2f i) : SV_Target
            {
                //create ring
                float time = frac(_Time.y);
                float dist = length(i.uv)/time/_Size;
                float pwidth = length(float2(ddx(dist), ddy(dist)));
                float alpha = smoothstep(0.5, 0.5 - pwidth * 1.5, dist) * (1-smoothstep(0.5, 0.5 - pwidth * 1.5, dist*1.05));

                return fixed4(_Color.rgb, _Color.a * abs(alpha/(tan(time/1.23*2))));
            }
            ENDCG
        }
    }
}

Using arrays to control multiple points in a shader from script:

(and important update on how to assign arrays to shaders - Arrays & Shaders in Unity 5.4+ - Alan Zucconi)

And here’s an example of doing multiple points in the shader alone using dynamically computed positions.

Shader "Unlit/AnimatedRings"
{
    Properties
    {
        _Color ("Color", Color) = (1,1,1,1)
        [IntRange] _NumRings ("Number of Rings", Range(1,64)) = 4
    }
    SubShader
    {
        Tags { "Queue"="Transparent" "RenderType"="Transparent" }
        LOD 100

        Pass
        {
            Blend One OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float4 pos : SV_POSITION;
                float2 uv : TEXCOORD0;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos(v.vertex);
                o.uv = v.uv;
                return o;
            }

            float pixelRing(float2 pos, float2 center, float radius)
            {
                float dist = length(pos - center);
                float deriv = length(float2(ddx(dist), ddy(dist)));
                float ring = smoothstep(deriv, 0.0, abs(dist - radius));
                return ring;
            }

            half4 _Color;
            int _NumRings;

            half4 frag (v2f i) : SV_Target
            {
                float2 uv = i.uv - 0.5;

                half tau = UNITY_PI * 2.0;

                half totalAlpha = 0;
                for (int r=0; r<_NumRings; r++)
                {
                    float a = (float)r / (float)_NumRings;

                    half radians = tau * a;
                    half s, c;
                    sincos(radians, s, c);

                    float2 center = float2(s, c) * 0.166;

                    float t = frac(_Time.y * 0.25 + a);

                    float radius = sqrt(t) * 0.33;

                    half ring = pixelRing(uv, center, radius);

                    float alpha = pow(1.0 - t, 2.0);

                    totalAlpha = lerp(totalAlpha, 1.0, ring * alpha);
                }

                half4 col = _Color * totalAlpha;
                return col;
            }
            ENDCG
        }
    }
}

That makes sense, so if I understand your code properly basically recalculating an array of rings and to generate their location in a ring based on their position in the array by changing their timing. Correct? So how would you change their location based on on hit position?

I got multiple rings to work but moving them around moves all of them. And when I hit the plane they all move to that position to the contact point of the collision. But looking at your code I don’t understand how you would be able to move just the newly generated rings to the new collision point while keeping those that still haven’t fully faded at their old location. I also don’t understand what the S and C variables are, are they just 0? Using your code I can change it’s location by changing the center variable.

Do I need to send a whole list with existing collision points from a C# script with their position and time since the collision as an array to the shader to be able to do that? That seems rather inefficient, but that’s the only way I can imagine it and I doubt that would work. Even if it did I think I would require to either get back info on when the ring has fully faded or just hardcode the timing to remove the ring from the list.

The sincos function is setting the s and c values. It’s effectively:

float s = sin(radians);
float c = cos(radians);

With the sine and cosine of an angle, you have position on a circle that I’m then scaling by 0.166 . That’s the ring’s center position on the UVs (which I offset so 0,0 is centered earlier in the code for convenience). And the angle I get from dividing the index of the current ring by the total rings.

Basically I’m computing a unique position for each ring as a position around a circle. I could use something else, like a Vector2 value from c#, or a pseudo random offset derived from that index. If you need to have the positions be directly from code, then you need to use an array of vectors that you set from c# and use as the center positions. Like that tutorial I linked to.

You can go about that two ways. You can have an array of positions, radii, and alphas that you animate from c# directly, which while “inefficient”, isn’t slow. Unity is passing way, way more data than that every frame. A few dozen float4s isn’t anything significant.

Alternatively, you can pass in positions and a start time when things change, and be done. _Time.y in the shader should match the Time.timeSinceLevelLoad in script, so you just need an array Vector4s with UV position as xy and a start time as the z if you want to have multiple rings. Just initialize the array with the time at some large negative value. And use _Time.y - ringArray[index].z in the shader. You can use the w component for anything else you might want, like ring size or starting opacity, etc. Just have a fixed size array and keep track of the last index you updated so you know which one to change next. When the index >= array size, set it back to 0 and overwrite the previous value.