12 minute read

Have you seen liquids inside some small containers like bottles, vases, or glasses in a video games? Since they are not very conspicuous, usually games tend to ignore their simulation, however games like Half-Life Alyx have a very advanced liquid simulation inside a bottle that makes a lot of gamers astonished to how accurate they are.

In this tutorial, we are going to attempt to create liquid shader however in a simplified manner. This GIF below shows the final result, but I’m going to give you some tips on how to improve it in the last part of the tutorial if you are interested.

This is a considerably long tutorial so you don’t have to finish it in one go. Make sure you understand the concepts, take a break, and come back later if necessary. If you encounter a problem when following the tutorial, you can check out the full source code from SteveImmanuel/unity-liquid-wobble.


Before we start, this tutorial assumes you have the following requirements:

  • Basic understanding of Unity
  • Basic understanding of Shader
  • Basic knowledge of linear algebra

Project Setup

Let’s create a new Unity project. We are going to use Universal Render Pipeline (URP) for this project, so you can choose the URP template or upgrade it later on once you are inside Unity.


Next, we need to download the Shader Graph package from the Package Manager (Window->Package Manager). If you can’t find it, make sure to choose the Unity Registry from the drop down menu.

Make sure these two packages are installed.


Scene Setup

Now, let’s create a new basic 3D object. I am going to use a capsule for this tutorial.


Note: Every object that is rendered will have a shader. If you don’t specify them, Unity will use their default shader according to the render pipeline.

In this case, we want to make our own custom shader. Usually artist will write their own shader in codes (HLSL or GLSL), however it is very complicated, hard to debug, and requires very high learning curve for a newbie. Luckily, there is also node-based approach that enables us to create our own shader easier, visually in a node-based manner. Unity provides their node-based shader called Shader Graph and we are going to use it.

Create a new shader from the URP as shown in the picture below. Let’s name it LiquidWobbleShader. Then, create a new material from that shader and name it LiquidWobbleMat. Assign the material to the capsule.



If you have a pink-colored capsule like I did in the picture above, that means you incorrectly setup the URP.

Note: The “Pink” shader is a Unity fallback shader in case it cannot render the objects based on the shader assigned to them.

If you don’t encounter this issue, you can skip ahead to the next section. To fix this issue, we need to create a new pipeline asset as shown in the picture below.


Open the project settings (Edit->Project Settings), navigate to Graphics and assign the new pipeline asset to the Scriptable Render Pipeline Settings


If you get it correctly, the capsule will look like this.


Liquid Shader

This is the core, most crucial part of this tutorial. Let’s open the shader graph editor by double clicking LiquidWobbleShader from the project hierarchy.

In the graph inspector, turn on Alpha Clip and Two Sided as we are going to need them.

  • Alpha Clip makes the renderer ignore parts where the alpha is below a certain threshold.
  • Two Sided makes the renderer renders both the front and back face of an object.


Before jumping to deep, you will notice that there are already two nodes in the editor, Vertex and Fragment.


To put it simply, Vertex Shader is the shader part that does all the calculation of each vertices in an object, while Fragment Shader is the shader part that does all the calculation of each fragments/pixels that is being rendered in the scene.

Shader is difficult because you have to create series of operation that operates on every single pixel of an object and you have to do it all at once not iteratively. Therefore the operations must handle all kinds of condition for each pixel.

This shader consists of 3 main modules which are liquid fill, liquid color, and wobble effect.

Note: If you’re having a hard time understanding what a node does, you can always refer to the Official Documentation.

Liquid Fill

This module will take care of how much of a container is filled. We are going to use position attributes from the object to achieve this effect.

First, create a Position node. Press space and type on the name of the node you want and select it. Make sure that the position node is set to World Space.


Next, create an Object node. Subtract the output of Position node to the Position output of the Object node using Subtract node. The result will look like the picture below.


If you save the graph, you will get a result that look like this in your scene.


Note: You have to save the graph by pressing Save Asset in order for the shader to be recompiled and for you to see the result.

Next, we need to use a Step node to make the shader only renders part of the object to get the fill effect. The fill effect is based on object’s height so we are going to use the y component from the result before. To get only the y component, use Split node. Connect the G output from Split node to the Edge input of the Step node.

Note: In Shader Graph, the x, y, z, w components of a vector is equivalent to r, g, b, a components of a color, vice versa.

Now, let’s create a properties. Properties are something that we can tweak later on in the inspector or via C# script. Create a Float property and name it Fill. Drag that property and connect it to the In input of the Step node.


Save the graph and check your scene. If you tweak the Fill property from the inspector you will have a working fill effect on your object like below.


Now we can tidy things up a little bit by grouping all the nodes we made so far.


Liquid Color

This module will handle the color of the liquid we want to show. You might notice that currently our liquid is looking hollow without lid. We are going to fix that first by rendering the backside of our object. This is why we enabled the two-sided option earlier.


By default, the renderer will ignore the backside/back face of an object to save computation time as usually this part is not visible. As you can see the in the picture above, front face is the side that has normal vector pointing towards the camera, while back face is the opposite.

In our shader the color of the object is controlled by Base Color in the Fragment node. Let’s create new properties and name it SideColor and SurfaceColor and don’t forget to use HDR mode in the property setting because we will use it to add bloom effect later on. We will use a predicate node called Is Front Face to determine whether it is front face or back face.


We are also going to use Branch node to handle if logic. Connect the output of Is Front Face node to the Predicate input of Branch node, and connect SideColor and SurfaceColor properties to the Branch node accordingly like in the picture above. What we are doing is basically telling the shader to use SideColor when rendering front face and SurfaceColor when rendering back face.


If you do everything correctly, you will get result like above.

Note: If your capsule color is black, that means you just haven’t set the actual color on the SideColor and SurfaceColor properties. You can do that by going to the material property in the inspector.

Now, let’s spice things up a bit. We are going to integrate a new effect called Fresnel to our color. Fresnel is basically the reflectance you get based on a surface depending from the viewing angle.

Create new properties and name it FresnelStrength and FresnelColor. Next, add Fresnel Effect node to our graph, connect FresnelStrength to the Power input and multiply the output with FresnelColor property. Last, add the result with the SideColor property.


Do the same thing for the SurfaceColor, but use different Fresnel Effect node. This is important because our surface is actually faked by rendering the back face. Therefore, we can’t use the normal vectors from our back face for our surface as they are completely different. We will fix this problem in later section.

You might be wondering when to use Add or Multiply node. A nice intuition, though might not apply in all situation, is use Add node when you want to add something on top of the other (i.e. stack), and use Multiply node when you want to combine something together, like color for example.


By now, you should see our capsule looking like the picture below. The gradient outline that you see is the fresnel effect. This is already looking much better than before!


Wobble Effect

This is the most important and hardest module to implement. This module basically takes care of the wobbling of our surface when our object moves. This will create the illusion that our object is a liquid and not just a static mesh.

First, create new properties XRotation and ZRotation. Under the reference in the property setting, set it to _XRotation and _ZRotation respectively. This has to be exactly the same because we are going to use it to control it from C# script later. Also, set the mode into Slider and clamp the value between -1 and 1.


Next we are going to use 2 Rotate About Axis node. For input we need to use Position node but this time in Object Space. For the axis, use X axis and Z axis accordingly and set the rotation to 90. Multiply the output of each node with XRotation and ZRotation based on the axis and add the result together using Add node.


The graph should look like the picture above. Next add the output to our position from the first module as shown in the picture below. This will enable us to control the rotation of our surface in X and Z axis independently.


Save the graph and try playing around with the XRotation and ZRotation value from the inspector. You should see something similar like this.


Controlling the Shader

We are basically done implementing the basic functionality of our shader. What’s left is to control the wobble effect programmatically.

In the capsule gameobject, add new C# Script component and name it Wobble. The name has to be Wobble if you want to use the code below otherwise you have to change the class name.

using UnityEngine;

public class Wobble : MonoBehaviour
{
    Renderer rend;
    Vector3 lastPos;
    Vector3 velocity;
    Vector3 lastRot;
    Vector3 angularVelocity;
    public float maxWobble = 0.03f;
    public float wobbleSpeed = 1f;
    public float recovery = 1f;
    float wobbleAmountX;
    float wobbleAmountZ;
    float wobbleAmountToAddX;
    float wobbleAmountToAddZ;
    float pulse;
    float time = 0.5f;

    void Start()
    {
        rend = GetComponent<Renderer>();
    }

    private void Update()
    {
        time += Time.deltaTime;
        // decrease wobble over time
        wobbleAmountToAddX = Mathf.Lerp(wobbleAmountToAddX, 0, Time.deltaTime * (recovery));
        wobbleAmountToAddZ = Mathf.Lerp(wobbleAmountToAddZ, 0, Time.deltaTime * (recovery));

        // make a sine wave of the decreasing wobble
        pulse = 2 * Mathf.PI * wobbleSpeed;
        wobbleAmountX = wobbleAmountToAddX * Mathf.Sin(pulse * time);
        wobbleAmountZ = wobbleAmountToAddZ * Mathf.Sin(pulse * time);

        // velocity
        velocity = (lastPos - transform.position) / Time.deltaTime;
        angularVelocity = transform.rotation.eulerAngles - lastRot;

        // add clamped velocity to wobble
        wobbleAmountToAddX += Mathf.Clamp((velocity.z + (angularVelocity.x * 0.2f)) * maxWobble, -maxWobble, maxWobble);
        wobbleAmountToAddZ += Mathf.Clamp((velocity.x + (angularVelocity.z * 0.2f)) * maxWobble, -maxWobble, maxWobble);

        // keep last position
        lastPos = transform.position;
        lastRot = transform.rotation.eulerAngles;

        // send it to the shader
        rend.material.SetFloat("_XRotation", wobbleAmountX);
        rend.material.SetFloat("_ZRotation", wobbleAmountZ);
    }
}

Copy the code above to the script. That script will calculates what the surface should be pointing towards and contraint it to point towards upward over time by sine wave. The calculation result will then passed on to the shader and the shader will display the object accordingly.

And we are done! Play the game, move the object from the scene, and you should see something like this.


Polishing Up

This section is optional. If you think that you already have what you are looking for, then feel free not to follow this section.

Fixing Surface Normal

As I pointed out earlier, we can’t use back face normal vectors as our normal vectors for the surface. We need to calculate it based on where the surface is pointing at.

To do that, I’m going to recycle the logic from our second module in the liquid shader. We will rotate an upward vector (0, 1, 0) by the amount of XRotation and ZRotation.


Then, connect the output to the Normal input of the Fresnel Effect node. Now, we will have a correct fresnel effect on the surface. The difference is quite subtle but is still noticable.

Post Processing

Let’s go crazier by adding post processing. One of the most used post processing is called bloom. It is an effect to make bright objects glow.


Create a new Global Volume, then create new profile. Add component called Bloom and enable the Threshold and Intensity. The Threshold controls how much intensity of an object must have in order for this effect to be applied, while Intensity controls the amount of glow effect.


Next, bump up the color intensity of the shader material. I used a custom mesh that I made in Blender and here is the result.


Note: If your only see the glow effect on the Scene but not in Game, you need to enable the post processing property on the Main Camera in the inspector.

Bubbles

You might realize that in real life, when we shake water in a bottle, it produces bubbles that floats up and then pops. We can also implement that in our shader.

I’m not going to explain this one in detail but I will give you my idea implementation because it is quite complicated and long, so I might revisit it for another tutorial.

To create the bubbles, I am using Particle System, Render Texture to capture the Particle System and apply the texture on top of the base color in our shader.

The result is quite nice. In the GIF below, I used a constant spawning bubbles but you could always control to spawn the bubbles only when bottle shakes for example via script.


Conclusion

In this tutorial we learn how to make simple liquid wobble shader in Unity using Shader Graph. We also learn about setting up URP, basic shader operations, rendering, and also customisation on how to extend and improve this shader further.

Tags:

Updated: