I recently worked on the respawn animation and special effects of the protagonist of my game “King, Witch and Dragon” . For this special effect, I needed a couple of hundred animated rats.


Creating two hundred skinning meshes with keyframe animation for just one special effect is a waste of resources. So I decided to use a particle system, but for this I had to choose a different approach to animation.

In this post I’ll explain how to animate simple creatures using the vertex shader . As an example, I use a rat, but the same method is applicable for animating fish, birds, flying mice and other creatures that are not the main goal of player interactions.
Most tutorials about shader animation for beginners talk about how to animate a flag using a sine wave. I will show a slightly more complex version in which we will break the model into different parts of the body and animate them separately.

I will describe in detail the creation of this animation here:

Rat animation

Preparation


To start, I created a low poly rat model in Blender.

Rat model

To separate the model into different parts (body, tail, legs), I use UV coordinates . For everything to work as it should, the model needs to be deployed in a special way.

Rat UV

The main movement of the rat is jumping. In order for all the vertices to be animated smoothly and synchronously, we need to place the entire body of the rat along one of the axes. For convenience, I placed it along the horizontal axis of U.

The eyes, the inner part of the ear and the nose should have a different color on the texture, so I placed them separately, but performed the offset only along the V axis , without changing the horizontal coordinates. If you move them along the horizontal axis, their movement will not be synchronized with the movement of the head.

The tail of the rat occupies the left half of the sweep (U coordinates from 0.0 to 0.5). This will be our tail mask , which we use to animate the tail.

The paws are located in the lower half of the sweep (V coordinates from 0.0 to 0.4). This is our paw mask . In addition, the legs are compressed along the horizontal axis U to prevent unwanted deformation when moving back and forth. Since in my project I use cel-shading without a detailed texture, compressed UVs will not cause problems.

Based on this UV scan, I created a diffuse texture . Now we can start working on the shader.

If you want to repeat all the steps, you can download the model in FBX format with a diffuse texture from here .

Creating a Shader


First I will create this shader in the Shader Graph of the Unity engine, and then I will show the text version.

Let's create a Unlit Graph , select Preview as the Custom Mesh , and then select the rat model.

ITKarma picture

Overlay the texture


Create a new Texture 3D parameter, this will be our main diffuse texture.Create a Sample Texture 2D node, connect our new parameter to its texture field, and then connect the node to the Color field of the Master node.

ITKarma picture

In the future, we will work only with vertices.

The main movement


We will create it using the sine wave . To adjust the waveform, we need three parameters:

  • Jump Amplitude - Rat Jump Height
  • Jump Frequency - Rat jumping frequency
  • Jump Speed ​​ - speed along the vertical axis

ITKarma picture

The two main nodes that allow us to create an animated motion are Time and UV . In the UV node, we will need to use each axis separately, so we will connect it to the Split node, which will give us access to each channel.

We can control the scroll speed of the sine wave by multiplying the Time node by the Jump Speed ​​ parameter.

If we multiply the horizontal UV component by Jump Frequency , then we can control the compression and stretching of the sine wave.

The sum of these products will give us a sinusoid of the desired shape. By default, a sine wave returns values ​​between -1.0 and 1.0. If we multiply them by Jump Amplitude , we get the jump path.

Building sive wave

Now we need to apply the result to the positions of the vertices, but only to their vertical component (local Y axis). We use the Position node and connect it to the Split node. Then we add the value of the sine wave to component Y and put it all together using the Combine node. Connect the output of this node to the Vertex Position field of the Master node.

Apply Y-offset

The rat began to move, but not quite as we needed. She looks more like a dolphin than a rat.

Rat sine wave

As mentioned above, a sinusoid can return both positive and negative values. If we now place the rat on the surface, it will “dive” into it. In addition, the sinusoid has very smooth extrema, which is why the animation is like swimming.

To fix this, we use the Absolute node. This function mirrors all negative values ​​into positive and the output of this function is always positive .

Absolute values ​​of the sine wave

Normal values ​​above, absolute values ​​below.

Let's add this node to our graph.

Graph absolute node

Now the animation has become more downloadable.

Rat absolute sine wave

This is more like jumping or racing, but still far from desired. The problem now is that as soon as the paws touch the ground, they bounce back into the air. This is logical in terms of the shape of a sinusoid, but unnatural in terms of rat movement. Let's keep the rat's paws on the ground for a while before the next jump.

To do this, we again modify the sine wave. Let's move it down the vertical axis and then take the maximum value between sine and zero.

Sine wave vertical offset

Above is the absolute value, in the middle is offset, below is the maximum between sine and zero.

To realize this in the graph, we need a new parameter Jump Vertical Offset , which will allow us to adjust the amount of shift of the sine wave.

Graph abs sine offset

Now the rat can stay on the ground for a while.

Rat jump with vertical offset

Additional tail swinging


In general, everything looks good anyway, but the tail always hangs near the ground. Let's add energy to it.

We use UV coordinates to mask the tail and animate it separately from the body.

We know that the tail is located in the left half of the UV scan. Create a smooth gradient from 0.0 to 0.5 (or even to 0.6, so that the effect is smoother) along the horizontal axis U. At 0.0, we will have white color, 0.6 and then black. The brighter the gradient pixel, the more additional movement is applied to the vertex. In fact, it will affect the tip of the tail most, and closer to the body, the effect will weaken.

To create this gradient, we use the Smooth Step node.

We also need a new Tail Extra Swing parameter to set the amount of additional movement.

Multiplying this new parameter by the output of the Smooth Step node, we get the distribution of movement along the tail. Then we will add it to the Jump Amplitude parameter to get the final body movement, taking into account the additional tail swinging.

Graph tail mask

Graph tail mask full

Tail movement is now more noticeable ( Tail Extra Swing =0.3).

Rat extra tail swing

Paw movement


For the movement of the paws, we use approximately the same structure as for the body. We will need a couple more new parameters:

  • Legs Amplitude - the amount of paw movement relative to their default positions
  • Legs Frequency - paw speed

We don’t need the Legs Speed ​​ parameter, because the movement of the paws must be synchronized with the movement of the body, so we again use the Jump Speed ​​ parameter. The only thing worth considering here: since we use the absolute value of the sine wave, two jumps are obtained in one cycle. Therefore, to compensate for this, we use Jump Speed ​​* 2 .

Paws should move forward and backward (both positive and negative offset), so in this case we don’t need the Absolute node.

Graph legs sine wave

Now you need to create a paw mask to animate them separately.

We will use the Smooth Step node again, but this time select the UV vertical axis as the input parameter. Let's set the gradient from 0.1 to 0.4.

Why is 0.1, not 0.0? To avoid deformation of the paws. All vertices below level 0.1 will have the same offset.

We need to adjust the Legs Frequency value so that when the front legs go forward, the rear legs move backward, and vice versa. In my case, I set the value to 10.

Graph legs mask

Let's add the result to the local position of the vertices in Z. I temporarily disabled the body movement to separately look at the paw animation.

Graph isolated legs movement

Rat isolated legs movement

Now combine everything together and see how it looks. I deliberately slowed down to make it easier to find possible problems.

Rat slomo

We see that the front legs touch the ground when they are in the extreme left position. This is not true; they should land when they are in approximately the rightmost position. This means that the phases of the body and paw animations do not match.

To solve this problem, we’ll create a new Legs Phase Offset parameter that allows us to compensate for this phase difference and match animations.

For this phase shift to work, you need to add it to the Time node already after multiplied by Jump Speed ​​ (so as not to change the speed), but before all other manipulations.

Graph phase offset

After setting the value (I specified -1.0), the animation became correct.

Rat with phase offset

Here's what it looks like at normal speed.

Rat final

Ready Count:

Complete graph

Text version of the shader


For those who have not yet switched to URP/HDRP or simply prefer to write shaders manually, here is the version in the code:

Shader "Unlit/Rat" { Properties { _JumpSpeed("Jump Speed", float)=10 _JumpAmplitude("Jump Amplitude", float)=0.18 _JumpFrequency("Jump Frequency", float)=2 _JumpVerticalOffset("Jump Vertical Offset", float)=0.33 _TailExtraSwing("Tail Extra Swing", float)=0.15 _LegsAmplitude("Legs Amplitude", float)=0.10 _LegsFrequency("Legs Frequency", float)=10 _LegsPhaseOffset("Legs Phase Offset", float)=-1 [NoScaleOffset] _MainTex ("Texture", 2D)="white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { 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; float4 _MainTex_ST; half _JumpSpeed; half _JumpAmplitude; half _JumpFrequency; half _JumpVerticalOffset; half _TailExtraSwing; half _LegsAmplitude; half _LegsFrequency; half _LegsPhaseOffset; v2f vert (appdata v) { float bodyPos=max((abs(sin(_Time.y * _JumpSpeed + v.uv.x * _JumpFrequency)) - _JumpVerticalOffset), 0); float tailMask=smoothstep(0.6, 0.0, v.uv.x) * _TailExtraSwing + _JumpAmplitude; bodyPos *= tailMask; v.vertex.y += bodyPos; float legsPos=sin(_Time.y * _JumpSpeed * 2 + _LegsPhaseOffset + v.uv.x * _LegsFrequency) * _LegsAmplitude; float legsMask=smoothstep(0.4, 0.1, v.uv.y); legsPos *= legsMask; v.vertex.z += legsPos; v2f o; o.vertex=UnityObjectToClipPos(v.vertex); o.uv=TRANSFORM_TEX(v.uv, _MainTex); return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col=tex2D(_MainTex, i.uv); return col; } ENDCG } } } 

Conclusion


Congratulations! We created a vertex shader that can revive a simple creature without using skeletal rigs and key frame animations.

This method is suitable for background objects that are not in the main focus of the player’s attention. Also, compared to skinned mesh renderers (Skinned Mesh Renderers) , it provides better performance.

I hope it was interesting and/or useful for you.

Source