top of page
Writer's picturesantosh nalla

Vertex Animations using Shaders

Updated: May 29, 2022

Vertex animations allows you to animate individual vertices of a mesh. This is dealt by vertex shader.


Vertex animations are an efficient and powerful solution to animate a large number of objects because it uses GPU(which is much much faster than CPU) to achieve the visuals.


Let's say you have a thousand birds/fish flocking in your game. Rigging and animating every bird/fish in your game can increase the load on the CPU and drastically reduce the performance. Transferring this load to GPU by animating them at vertex shader level in the render pipeline, we can completely avoid rigs and increase the performance.

v2f vert (appdata v) //Vertex Shader
  {

  }

This is where you animate vertices of a mesh.


Let's first animate the crowd in the stadium.

Animating and rigging each character in the stadium will put a lot of load on the CPU and reduce the performance of the game as mentioned. Let's animate their vertices and make them look like they are cheering.


Let's keep it simple by making them move up and down on their feet.

If you look at the above gif closely, it is nothing but a sin wave. Vertices are moving up and down in a sin wave with respect to time.

In Unity shaders, we can use a build-in shader variable _Time.y which gives us time since level load. So using this, let's animate the vertices in a sin wave.

 v.vertex.z += abs(sin(( _Time.y * _Frequency))) * _Amplitude;

abs() make sure the value is always positive. Since sin() can go from -1 to 1 and since the vertices of the character cannot go below its feet, we always have to make sure they stay in positive range(0 to 1).


_Amplitude variable controls the character jump height.


_Frequency controls how fast the character jumps.


These are similar to controlling amplitude and frequency of sin wave.


So at _Time.y * Frequency = 0,180,360,720.... degrees, sin(_Time.y * Frequency) = 0 so the vertices stay at the feet.


And at _Time.y * Frequency = 90, 270, 450 degrees, abs(sin(_Time.y * Frequency)) = 1, so the vertices are at max height from the ground.


Wanna put more crowd in to the stadium?

Here you go!!


Full code:

Shader "ShaderLearning/Vertex Animations/CrowdSimulator"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _Amplitude("Amplitude", Range(0,200)) = 0
        _Frequency("Frequency", Range(0,100)) = 0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

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

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;
            float _Amplitude;
            float _Frequency;

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

                v.vertex.z -= abs(sin(( _Time.y * _Frequency))) * _Amplitude;

                o.vertex = UnityObjectToClipPos(v.vertex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}

Now let's animate the flag.


Flag animation is a bit different from the crowd.


Take a default plane and apply the flag texture to it.

Note: the default plane in unity is subdivided. It has 100 vertices to play with.


Now the UV of this default plane runs between 0 to 1 with 0 being the near end and 1 being the far end. We can of course get this UV data from appData struct which is an input to vertex shader( float2 uv : TEXCOORD0;) .


So uv.x runs from 0 to 1 as mentioned.


Let's first generate a static sin wave for our flag.

v.vertex.y += sin((v.uv.x ) * _WingsSpeed) * _WingsAmplitude;

This generates a static sin wave.


Now how to move the flag with time?

Mathematically adding a constant to sin() shifts the wave by 'constant' amount.

v.vertex.y += sin((v.uv.x + constant) * _WingsSpeed) * _WingsAmplitude;

Since we want to keep animating the flag wrt time, lets replace constant with _Time.y.

v.vertex.y += sin((v.uv.x - _Time.y) * _WingsSpeed) * _WingsAmplitude;

This animates our flag generating sin wave. But wait? shouldn't our flag stay fixed at the pole(uv.x = 0)?

v.vertex.y += sin((v.uv.x - _Time.y) * _WingsSpeed) * _WingsAmplitude * v.uv.x;

so at v.uv.x = 0, v.vertex.y = 0 so the flag doesn't move at the pole to which it is fixed.

Code:


Shader "ShaderLearning/Vertex Animations/WavingFlag"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _WingsSpeed("WingsSpeed", Range(0,40)) = 0
        _WingsAmplitude("WingsAmplitude", Range(0,10)) = 0
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" }
        LOD 100

        Pass
        {
            Cull Off
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

            sampler2D _MainTex;
            float _WingsSpeed;
            float _WingsAmplitude;

            v2f vert (appdata v)
            {
                v2f o;

                o.uv = v.uv;

                v.vertex.y += sin((v.uv.x - _Time.y) * _WingsSpeed) * _WingsAmplitude * v.uv.x;

                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

Moving on to animate a butterfly.


Animating a butterfly is quite tricky.

Similar to a flag, apply butterfly texture to a default plane.


The butter fly texture when applied to a plane look similar to this.


The middle part of the butterfly shouldn't move, where as the left and right wings of the butterfly should animate.

float movementAmount = 1 - sin(UNITY_PI * v.uv.x);

Lets first calculate the movementAmount.

When v.uv.x == 0.5, sin(pi * 0.5) = 1, thus movementAmount = 0.

when v.uv.x == 0 or 1 , sin(0) or sin(pi) = 0, thus movementAmount = 1, which are both extremes of the wings.


Now,

float movementAmount = 1 - sin(UNITY_PI * v.uv.x);
v.vertex.y += sin(_Time.y * _WingsSpeed) * _WingsAmplitude * movementAmount;

since movementAmount = 0 in middle part of our butterfly, v.vertex.y = 0, hence there is no movement.

This animates our 2 wings of our butterfly with Time.


Code:

Shader "ShaderLearning/Vertex Animations/Butterfly Animations"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
        _WingsSpeed("WingsSpeed", Range(0,40)) = 0
        _WingsAmplitude("WingsAmplitude", Range(0,10)) = 0
    }
    SubShader
    {
        Tags { "RenderType"="Transparent" }
        LOD 100

        Pass
        {
            Cull Off
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

            sampler2D _MainTex;
            float _WingsSpeed;
            float _WingsAmplitude;

            v2f vert (appdata v)
            {
                v2f o;

                o.uv = v.uv;

                float movementAmount = 1 - sin(UNITY_PI * v.uv.x);
                v.vertex.y += sin(_Time.y * _WingsSpeed) * _WingsAmplitude * movementAmount;

                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}

A Similar logic can be applied to animate a fish.


Code:

Shader "ShaderLearning/Vertex Animations/FishAnimations"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}

        //Fish Translate Properties
        [Header(Translate Inputs)]
        [Space(10)]
        _FishTranslateAmplitude("FishTranslateAmplitude", Range(0,10)) = 0
        _FishTranslateSpeed("FishTranslateSpeed", Range(0,10)) = 0

        //Fish Yaw Properties
        [Space(10)]
        [Header(Yaw Inputs)]
        [Space(10)]
        _FishYawAmplitude("FishYawAmplitude", Range(0,10)) = 0
        _FishYawSpeed("FishYawSpeed", Range(0,10)) = 0

        //Tail Yaw Properties
        [Space(10)]
        [Header(Tail Yaw Inputs)]
        [Space(10)]
        _FishTailYawAmplitude("FishTailYawAmplitude", Range(0,10)) = 0
        _FishTailYawSpeed("FishTailYawSpeed", Range(0,10)) = 0
        
        //Tail Roll Properties
        [Space(10)]
        [Header(Tail Roll Inputs)]
        [Space(10)]
        _FishTailRollAmplitude("FishTailRollAmplitude", Range(0,10)) = 0
        _FishTailRollSpeed("FishTailRollSpeed", Range(0,10)) = 0
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            Cull Off
            Blend SrcAlpha OneMinusSrcAlpha

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

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

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

            sampler2D _MainTex;
            float _WingsSpeed;
            float _FishTranslateAmplitude;
            float _FishTranslateSpeed;
            float _FishYawAmplitude;
            float _FishYawSpeed;
            float _FishTailYawAmplitude;
            float _FishTailYawSpeed;
            float _FishTailRollAmplitude;
            float _FishTailRollSpeed;

            v2f vert (appdata v)
            {
                v2f o;

                o.uv = v.uv;

                //Fish Translate Amplitude
                v.vertex.z += sin(( _Time.y) * _FishTranslateSpeed) * _FishTranslateAmplitude;

                //Fish Yaw Amplitude
                v.vertex.z += sin(( _Time.y) * _FishYawSpeed) * _FishYawAmplitude * (0.5 - v.uv.x);

                //Fish Tail Yaw
                v.vertex.z += sin((v.uv.x * UNITY_PI - _Time.y) * _FishTailYawSpeed) * _FishTailYawAmplitude /** (1 - v.uv.x)*/;

                //Fish Tail Roll
                v.vertex.z += sin((v.uv.y - _Time.y) * _FishTailRollSpeed) * _FishTailRollAmplitude /** (1 - v.uv.x)*/;
                
                o.vertex = UnityObjectToClipPos(v.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                return col;
            }
            ENDCG
        }
    }
}
86 views0 comments

Recent Posts

See All

Comments


bottom of page