Shader wizard – Melting Shader Part 2

Shader Wizard Header - Melt Shader Part 2

By Hugo Scott-Slade, Technical Director

In this series of posts I will go through the stages necessary to build a shader than can ‘melt’ any mesh into the ground. There are a lot of tutorials out there which cover the basics of writing shaders but these posts will aim to cover some good techniques for taking your game to the next level. I will assume you have a basic knowledge of shaders and we will build on each step until you have a cool shader to play with.

Part 1 covered

  • Object Space vs World Space
  • Moving Verts in the shader
  • Supporting Unity’s PBR rendering with the moving verts, including updating the shadows to match your new shape
  • Reorienting Normals based on new shape.

Parts 2 will cover

  • Creating the melt ‘shape’
  • Changing the object’s material properties for the melted area

Parts 3 will cover next week

  • Tessellating the mesh to create new verts for a smooth melt shape
  • Optimising your tessellation
  • Combining tessellation, vertex and surface shaders
  • Circumventing current Unity limitations for the above.
  • General bug tips and things you will probably come across in your workflow and how to fix them!

All shaders and scripts have been written in Unity 5.4 but should work in older/newer versions too. Some shader keywords have changed in 5.4 such as _Object2World to unity_ObjectToWorld. If that doesn’t mean anything to you don’t worry – it will.

Additionally, the source code for all shaders is commented outlining the steps but this post will give a greater depth of information (it has pretty pictures) so be sure to read both. Also I won’t be using any of those fancy greek letters or reducing things to simple letter notation. I find that kinda stuff just bounces off my brain so I will aim for clarity over brevity.

Melting a Mesh

If you search online you can find a lot of GIFs and renders of objects melting. There are a few different ways we could implement it but this post will cover how to make an object melt into the surface below it.

For simplicity we will assume our object will only ever melt into a flat opaque surface although it could be easily adjusted to work on any surface. There are two main values we will use to displace our objects verts to make it melt and they are:-

  • _MeltY – Anything below this is considered fully melted.
  • _MeltDistance – Anything within this distance of _MeltY will have a melt value between 0 and 1.

We will be building on top of the last shader from part 1, the lit shader with reoriented normals. The only thing that will change is the function that displaces the verts, rather than using a sine wave to oscillate the verts we will melt them in accordance with the two values above.

Enter the Melt-rix

Our melting will push the verts along their normals as the object lowers across the melt threshold. _MeltY is a y value in the world not within our object so we will need to calculate our vert position in world space as well as the vert normal. You can convert normals to world space just the same as positions.

float4 worldSpacePosition = mul( unity_ObjectToWorld, objectSpacePosition );
// need to make normal a float4. opengl/metal wont build with mul(float4x4, float3)
float4 worldSpaceNormal   = mul( unity_ObjectToWorld, float4(objectSpaceNormal,0) );

Calculating how much to push the verts out is easy. Any position below _MeltY is fully melted and anything above _MeltY + _MeltDistance is not at all melted. Here is the code that does that. The second line uses saturate which is the CG equivalent of Clamp01 in C#, without that any positions above _MeltY + _MeltDistance would start to be sucked inwards which is not what we want at all. We are subtracting the value from 1 to make the value range correspond with what we want, that 0 is not melted and 1 is fully melted.

float melt = ( worldSpacePosition.y - _MeltY ) / _MeltDistance;
melt = 1 - saturate( melt );

I also use another value, _MeltCurve that takes the linear 0 – 1 range and gives it a nice curve. It just uses pow and a value above 1 to control the exponent. I use it because it gives a nicer shape like the object is melting down and a pool spreading outwards. Here is a graph showing the curve using an _MeltCurve of 2 versus a purely linear input.

Curve Graph

Pushing the verts out by the normal multiplied by this new curved melt value gives us the shape we want. Next step is to modify the surface shader to change the properties of our object once it has melted.

Melt Verts shader

Creamy Pixels

In order for the surface shader to know which parts to modify we’re going to need to pass some data from the vertex shader. Adding a new value to the Input structure along with the uv_MainTex means we can read it in the surface shader. We also need to modify our vertex function’s parameters so Unity knows we’re modifying the Input struct as well was adding a line of code to initialize the Input struct which Unity was doing automatically before. Your vertex shader should start now looking like this:

void vert( inout appdata_full v, out Input o )
{
    UNITY_INITIALIZE_OUTPUT( Input, o );
    ...

The next step is to get the melt value out of the vertex displacement function and into our vertex shader to set on Input. I added an out parameter to the getNewVertPosition function which takes a value I pass in and sets its value to the melt value. After I find the melt value for the vertex I set it in the Input struct so I can reuse the same holding variable when I call the displacement function for the other 2 vertices used for normal recalculation. Additionally I have added 3 new properties to the shader to control the new ‘melted’ material:

  • _MeltColor
  • _MeltMetallic
  • _MeltGlossiness

I will cover the initial 3 steps I took when creating the melt look for Cone Wars. They are smooth interpolation (left), hard edge (center) and wave edge (right).

Melt Pixels shader

The smooth interpolation was the first version I created and simply uses lerp and the melt value from the vertex shader to smoothly blend between the properties. This style wasn’t what I was after for Cone Wars so I started to move towards something more toon like. The hard edge also uses lerp to blend the two values but I modify the melt value in the surface shader to be either 0 or 1 using the step function so either full object material or full cream material. This was more in line with how I wanted it to look but it needed a nicer join between the two materials.

I use a sine wave to modify the threshold in the step function that is based on the object space position on each vertex. To access the object space position I had to add the value to the Input struct and set it in the vertex shader. With some tweaking this gave me a look I was happy with.

What’s next for the shader

After I created this version I wanted to give the splat more thickness where it joined the surface and when it joined the object to create the illusion of surface tension. I started down the road of reorienting the normals at these points within the surface shader and had some success but there was a clear issue. Not enough vertices. This is clear on some of the gifs in this post, particularly some shadowing artefacts around where the melted object crosses the melt threshold.

When moving vertices you are limited by the vert count of your objects, so creating these smooth surfaces based on the geometry of your object is tough. Without wanting to add verts just for the purpose of this shader in the game I started work on adding tessellation – to add new vertices dynamically within the shader. I will cover this in the next post in this series but here is a sneak peak.

Tessellation Final shader

Download the unity package for this post here

For more information about Cone Wars be sure to subscribe to the mailing list and follow @Glitchers on twitter.