Liquid Shader Breakdown

I’ve moved to a new site!
An updated version of this post can be found here, with no ads :
https://www.cyanilux.com/tutorials/liquid-shader-breakdown/


Intro

This liquid shader is based on discarding pixels that are above a certain Y position and rendering both front and back faces of the mesh in order to fake the top surface of the liquid.

Based on Minions Art’s liquid shader, also see their shadergraph version here.

A few things to note before I explain the effect:

  • Models that use this shader should have a pivot which is roughly at the center of mass.
  • This is an Unlit shader – it won’t be affected by lighting. If we used a PBR shader (which includes lighting), since the top surface is actually the back faces it wouldn’t look right. It might still be possible to use a PBR shader but with Emission instead of Albedo for the backfaces, or you could use the unlit master node but handle lighting calculations yourself (by using a Dot Product with the light direction and mesh normals). For now though I will stick to unlit.
  • I’m using the LWRP (Lightweight Render Pipeline), but it should work with the HDRP (High Definition Render Pipeline) too.

Breakdown

First we use a Position node to obtain the pixel/fragment’s world space position. Unity’s Shadergraph shows a (3) on the output of the node and the lines that connect the nodes are yellow to show that this value is a Vector3. This means it contains 3 numbers, representing the XYZ axis in world space.

The preview on the position node shows a 2D slice of a sphere, showing how the position is visualised. The red shows the X component, while green shows Y and blue shows Z. Negative numbers don’t show in the preview, which is why the bottom left shows as black – and there’s no blue at all, as forwards into the image is the negative Z axis.

With this position we could discard all the pixels that are in the positive Y direction (aka upwards, or where Y > 0). However, doing this with the world position means the liquid height will not move if the object is moved, and it will become completely submerged or invisible if it’s moved too far below or above the liquid height level. To fix this, we need to Subtract the object’s world space position which can be obtained via the position output on the Object node.

LiquidGraph1

This can then be plugged into a Split node. It always has 4 outputs on the node but it will only fill 3 of them since it’s a Vector3. The outputs are labelled as RGBA, each being a single value shown by the (1) and cyan connections, however since this is a world space position it might be useful to think of them terms of XYZ (Red=X, Green=Y, Blue=Z as mentioned before).

For now, we will take the Y/G value, and plug it into the edge input of a Step node. This has two inputs and returns 1 if the In input is greater than or equal to the edge input, otherwise it returns 0. I will leave the other input as 0, but you could also connect a Vector1 property here (or offset the other value with an Add/Subtract node), so you can change the liquid height from outside of the shader. The output of this step node will go into the Alpha input on the Master node.

By connecting a Vector1 (or Slider) node with a value of 0.5 to the AlphaClipThreshold input on the Master node, it will tell the shader to discard pixels that are below this threshold. (Note that a node must be connected, entering a value directly into the master node’s input wont work.)

We should now see that pixels above the pivot of our object are discarded, but there is no top surface yet. In order to get this we need to render back faces. To do this, click the cog icon on the top right of the Master node and tick the Two Sided box.

LiquidGraph2

Now that there is a top surface we should change the colours so we can see the effect better. To do this use a Branch node with the Predicate set to use the Is Front Face node. This is a Boolean value as shown by the (B) and purple connections. It should be fairly self-explanatory what these nodes do – it’s basically an if statement based on whether it is a front face or not. If it is a front face, it will use the True input as the output and if it’s a back face it will use the False value instead. (Although it is worth noting that the HLSL code generated by a branch node is a lerp (linear interpolation) between two values, with the predicate being 0 or 1, rather than actual conditional branching).

We’ll plug two Color nodes into the True and False inputs. The true one being the liquid colour, and the false the top surface or froth/foam colour. These should probably be Color properties, so it is possible to access them outside of the shader and create multiple materials with different colours. To do this we can either place a Color node and then right-click on it and select Convert To Property, or click the plus icon in the blackboard (usually in the top left corner) and select Color and then drag the newly added property on that list into the graph.

The output of this branch will then connect to the Color input on the Master node.

To improve the effect, I added a level of foam onto the side of the front faces. We can do this by adding another Step node (using the same input as the other one), but offset the edge input by a small amount. I’m using 0.05 for this, but it might also be a good idea to use a Vector1 property here so you can change the foam height from outside of the shader.

LiquidGraph3

Multiply the lower step output with the liquid colour and put this into an Add node, leaving one of the inputs empty for now. Then we can Subtract the lower step from the higher step to obtain our foam level, and Multiply the output by 0.9 so it isn’t exactly the same colour as the top surface. Then Multiply it again by the foam colour. This can then be used as the other input on that Add node mentioned before, and the output of that replaces the True input of the Branch node we set up earlier.

And that’s pretty much the effect. I also added a little Sine movement to the liquid height by offsetting the Y/G value from the Split node using the R/X component, a Time node and a few maths based nodes like this:

LiquidGraph4

The Frequency and Time Scale properties are set to 1 but it is useful to have them there if you want to adjust the effect slightly, and the Amplitude is set to 0.05. The output of the Add on the right replaces the input into the Step nodes from earlier.

Here’s a image showing the entire graph:

Liquid Shader Graph
(click to open a larger version in a new tab)

Thanks for reading this!

If you have any comments, questions or suggestions please drop me a tweet @Cyanilux, and if you enjoyed reading please consider sharing a link with others! 🙂