Hi there, Perren here.
Welcome to my first post and quite an exciting one at that!

As of this post, 2Ginge will be in the process of submitting a nifty little asset that incorporates a little bit of trickery and approximated physics. I found the approach we took with the liquid shader included in this asset pack quite interesting and I 'd love to share the process with you on the chance that you will also find it interesting or at least informative. The following will breakdown the trickery and show exactly what is happening.

Present in this asset pack is a 'liquid' inside a 'glass' bottle with simulated liquid physics - animated in relation to overall bottle movement - and a variable liquid fill level, great for interactive bottle props in VR for example. We have a created a series of scripts and shaders that allow users to simulate the liquid physics (faked!) easily using their own meshes, or those provided in the asset pack.
 

Concepts covered in this breakdown:

  • Cutoff, level (or % of fill) of the liquid within the bottle.
  • Rendering an arbitrary fill surface of 'liquid'.

Not Covered:

  • Animation of the liquid (though it will be apparent how and to what variables it is applied, it will be covered in a follow up post). 
  • Glass Shader.
  • Volume of liquid 

 

Cutoff:

The first image is the final result (minus the emission as it lessens the impact of lighting). As you can see, there is a surface to the liquid that responds to the directional light in the scene.

The image to the left shows the 'world' up as the vector in Red.
The dotted and (blue/) black line is the termination line (in local space) that the shader will start discarding fragments (not rendered).

(Note: if the up vector is rotated then the liquid itself can be rotated).

Illusionary Surface rendering:

Note: the image to the left is for reference for normal directions.

This is an example of the liquid mesh cut across the middle, which is a common occurrence when using discard shaders. Notice that the geometry facing away is lit darkly, we can use this to our advantage.

Let us suppose we render this mesh in two ways, one normally for ALL geometry that is facing us (basically a standard back face culled shader) and another for the geometry that is facing away from us.

For the geometry that is facing away we know that it will resemble the shell of the mesh, thus if instead of using the normals provided by the mesh we create some of our own that will represent our surface. Realistically, a surface is defined by light, not necessarily geometry.

Using the 'world up' vector as a normalized vector for the normal of the liquid surface, due to the front facing geometry rendering over most of this surface, it gives the illusion of a surface at the top of the mesh.


Surface Normal (inside vertex shader):

...
//Surface Normal
//_point is the middle of the surface
//_lowerBound is the bottom of the liquid,
    //hint when this rotates around the _point
    //then the surface changes angles!!!
    
out.normal = normalize(_point - _lowerBound);
..

Cutoff (inside fragment shader):

...
//Surface Level
//_point is the middle of the surface
//_localPos is the position of the pixel in local space
//_lowerBound is the bottom of the liquid,
    //hint when this rotates around the _point
    //then the surface changes angles!!!
    
fixed3 pixelDir = normalize(_localPos - _point);
fixed3 volumeDir = normalize(_point - _lowerBound);

//essentially if the angle is greater than 90 degrees, dont render.
if(dot(pixelDir,volumeDir) > 0)
{
    //dont render the fragment
    discard;
}
...

Thank you for reading and supporting 2Ginge, we appreciate your interest and hope we are able to help you create your next great game with the tools we are providing.

If you have any questions, do get in contact with us at contact@2ginge.com.

- Perren Spence-Pearse