Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
Featured Blog | This community-written post highlights the best of what the game industry has to offer. Read more like it on the Game Developer Blogs or learn how to Submit Your Own Blog Post
In this post, we'll look at how to create dissolution VFX using custom surface shaders in Unity. In doing so, I'll explore the use of noise textures, alpha fading, geometric parameters, and emission colours to create various dematerialization effects.
Note: This post was originally published on our studio's development blog. We're a small student team based in Ontario, Canada working on our first commercial project.
A few weeks ago, my team and I found ourselves crunching towards an expo deadline, prioritizing various polish items and gameplay tweaks. Perhaps our largest chunk of work centred around visual effects - there were quite a few so-called “delighters” that we wanted to add in, and we had little more than a week to put the finishing touches on our demo. But there was one effect in particular that we wanted to implement, and we started out with absolutely no idea of how to handle it - trying to animate a “possession” effect for our main character, a little poltergeist. We wanted to give the impression that the character dissolved into ghostly energy, which we could then animate on a curve to “enter” different objects. But how could we make a mesh appear as if it was disintegrating into energy, or gradually breaking apart into the aether?
Our artist pulled up a few effects from different games that were similar to what we envisioned, giving us a point of reference for what we wanted to achieve:
Top: Simple but functional transformation of Mario into coloured particles in Super Mario Sunshine (source), Bottom: Beautiful and envy-inspiring dematerialization of Link in The Legend of Zelda: Breath of the Wild (source).
A bit more digging online revealed that the effect we were looking for was probably based on a dissolve shader, which we could combine with a particle system to create that suave torn-to-pieces-by-supernatural-forces look. The particle system would be the easy part, tech-wise, and so our big challenge was tackling the dissolve shader.
We wanted something that was flashy, customizable, and portable, so that we could use it on different objects - a custom Unity surface shader with support for fancy materialization and dematerialization effects. The finished product will let us create something like this:
Here’s a quick breakdown of the steps we’ll take to create this effect:
Use a grayscale noise texture to fade mesh alpha based on an interpolation factor.
Use model-space fragment position to control dissolution based on a specified direction vector.
Combine texture- and geometry-based alpha/clipping control to create a hybrid dissolve effect.
Add in a glow effect by “predicting” the next areas to dissolve and adjusting model emission accordingly.
The great thing about these features is that they can be easily configured to work in tandem with one another, without interfering with any other shader features you might want to support, such as specular/normal/emission mapping, rimlighting, and so on. For simplicity’s sake, let’s assume we’re starting with Unity’s standard surface shader template, and a humble cube destined for greatness. The first thing you’ll want to do, assuming you want to support a gradual alpha fade, is adjust your shader tags and #pragma declaration accordingly:
Tags { "Queue" = "Transparent" "RenderType"="Fade" } //...// #pragma surface surf Standard /*...any additional features you want...*/ alpha:fade
If you’d prefer something that eats away at the mesh without fading the alpha gradually, feel free to skip this step. However, when writing to the output of your fragment routine, just make sure to use clip() to cull any dissolved fragments, rather than setting the output alpha value directly (as I’ll be doing here).
The first item on the agenda is to control our dissolution based on a noise texture. This will let us create different effects reminiscent of burning, cracking, slicing, and so on. Here, I’ve used Photoshop’s clouds and difference clouds filters to create some high-contrast Perlin-type noise:
For our object to fade away based on this pattern, we’ll just add it as a texture map to our shader, along with a floating-point parameter, _DissolveScale, on the range of [0, 1] to control the progression of the effect. For convenience, I’ve set zero to mean “fully intact” and one to mean “completely dematerialized”, so that as we move the slider from left to right in the Inspector, the object will gradually disappear.
If we think of the texture as a map to control our object’s dissolution, we want areas of different values (light/dark) to dissolve at different times. Let’s say that we want the black/dark parts of our texture to dissolve first, giving the appearance that the mesh cracks into pieces which then fade away. To accomplish this, for each fragment, we’ll add the luminance value of the dissolution map to our interpolation factor and use the result as our output alpha value:
//Convert dissolve progression to -1 to 1 scale. half dBase = -2.0f * _DissolveScale + 1.0f; //Read from noise texture. fixed4 dTex = tex2D(_DissolveTex, IN.uv_MainTex); //Convert dissolve texture sample based on dissolve progression. half dTexRead = dTex.r + dBase; //Set output alpha value. half alpha = clamp(dTexRead, 0.0f, 1.0f); o.Alpha = alpha;
Note that we’ve converted the interpolation factor to the space of [-1, 1] for this operation - don’t worry if this doesn’t make sense at first. All we’ve done is effectively ensure that our global alpha value will be 1 (fully opaque) at the very start of the effect, and 0 (fully transparent) at the very end. (If you happen to be unfamiliar with this sort of operation, it’s a little trick commonly called range remapping or range conversion, and it’s useful for all sorts of things).
We’re left with an effect that looks like this - not too shabby for a single noise texture and a few lines of code:
The next order of business is controlling this effect based on geometry - what if we want to dissolve the object from top to bottom, for example? There’s two straightforward ways that we might accomplish this. If you’re looking to create a particularly complicated progression (such as dissolving a character’s hands, bow tie, and eyes before the rest of them, for example) - you might just want to create your texture with this in mind, using your object’s UVs as a guide and hand-painting a dissolve texture to your liking (remember, with the code above, darker dissolves first).
A more interesting challenge is to control the effect based on a direction vector. You’ll need three new parameters for this:
A Vector for the starting point of the effect in model space.
A Vector for the ending point of the effect in model space.
A floating-point control representing the width of the “gradient” or “edge” along which the object is dissolving - I call this the “band size”.
You can visualize the effect as a gradient sweeping across the object, controlling the alpha and “wiping” the mesh from your starting point to your endpoint as it vanishes. Achieving this is pretty simple, but you’ll first want to add a vertex routine to your shader program, since you’ll be needing some geometry data that isn’t carried through to the fragment function by default. Outside of any of our shader functions, we’ll calculate a few global values based on our new parameters:
//Precompute dissolve direction. static float3 dDir = normalize(_DissolveEnd - _DissolveStart); //Precompute gradient start position. static float3 dissolveStartConverted = _DissolveStart - _DissolveBand * dDir; //Precompute reciprocal of band size. static float dBandFactor = 1.0f / _DissolveBand;
Then, we’ll write our vertex routine to calculate an “alpha value” for the current vertex based on the effect progression. Note that we’ve modified the shader’s fragment Input struct to have an additional parameter for this (dGeometry) - we’ll let Unity handle the interpolation for each individual fragment to help reduce artifacts. Here’s what the complete calculation looks like:
//Don't forget to specify your vertex routine. #pragma surface surf Standard /*...your other #pragma tags...*/ vertex:vert //... void vert (inout appdata_full v, out Input o) { UNITY_INITIALIZE_OUTPUT(Input,o); //Calculate geometry-based dissolve coefficient. //Compute top of dissolution gradient according to dissolve progression. float3 dPoint = lerp(dissolveStartConverted, _DissolveEnd, _DissolveScale); //Project vector between current vertex and top of gradient onto dissolve direction. //Scale coefficient by band (gradient) size. o.dGeometry = dot(v.vertex - dPoint, dDir) * dBandFactor; }
Then, in our fragment shader, we simply use the interpolated dGeometry value (clamped to the range of [0, 1]) to set our alpha and we’re left with an effect that progresses like this:
Combining this with our texture-based dissolve to create a hybrid effect is dead simple - just add the raw value of dGeometry to the luminance of the noise texture, clamp to [0, 1] as per usual, and use that as your alpha value:
//Combine texture factor with geometry coefficient from vertex routine. half dFinal = dTexRead + IN.dGeometry; //Clamp and set alpha. half alpha = clamp(dFinal, 0.0f, 1.0f); o.Alpha = alpha;
Our last task is adding in some emissivity, so that the edges of pieces about to dissolve can glow before fading away. There’s quite a lot of ways to handle this, and the one that works best for you will vary depending on the approach you’ve taken. You can use offset versions of the interpolation parameter to calculate the glow strength, you can shift a “band” of emission down your mesh as it fades away, you can apply thresholding logic to your final alpha value to have a fragment “emit” at low values before clipping itself from view, and so on.
For our purposes here, I’ve chosen an approach which supports the “hybrid” texture/geometry dissolve fairly intuitively, by defining the size of the glow region in accordance with the “band size” specified for the rest of the effect. I use this factor to offset the alpha value calculated previously, using this shifted value to control the glow strength. I’ve also included a couple of additional parameters which control the sharpness of the glow’s edge (an intensity multiplier) and create a gradient to calculate the glow’s colour (start/end colours, and a parameter to shift the boundary between them):
//Shift the computed raw alpha value based on the scale factor of the glow. //Scale the shifted value based on effect intensity. half dPredict = (_GlowScale - dFinal) * _GlowIntensity; //Change colour interpolation by adding in another factor controlling the gradient. half dPredictCol = (_GlowScale * _GlowColFac - dFinal) * _GlowIntensity; //Calculate and clamp glow colour. fixed4 glowCol = dPredict * lerp(_Glow, _GlowEnd, clamp(dPredictCol, 0.0f, 1.0f)); glowCol = clamp(glowCol, 0.0f, 1.0f);
By outputting the computed colour as the emissive colour (o.Emission) of the fragment, the mesh will now glow in anticipation of its disappearance. In the following examples, the albedo tint is adjusted according to the glow factor to boost the colour even more.) You can play with different noise textures, glow colours, and effect parameters to create quite a few different dematerialization effects:
Top: "Magma" effect using Perlin-type noise, high-intensity red-yellow glow, and top-to-bottom effect direction. Middle: "Boules" effect using pin-light radial gradients, purple glow, and bottom-to-top effect direction. Bottom: "Glitch" effect using offset barcode pattern, cyan-green glow, and corner-to-corner effect direction.
For reference, here’s the final property list and Inspector panel for the shader used to create the above effects:
Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _DissolveScale ("Dissolve Progression", Range(0.0, 1.0)) = 0.0 _DissolveTex("Dissolve Texture", 2D) = "white" {} _GlowIntensity("Glow Intensity", Range(0.0, 5.0)) = 0.05 _GlowScale("Glow Size", Range(0.0, 5.0)) = 1.0 _Glow("Glow Color", Color) = (1, 1, 1, 1) _GlowEnd("Glow End Color", Color) = (1, 1, 1, 1) _GlowColFac("Glow Colorshift", Range(0.01, 2.0)) = 0.75 _DissolveStart("Dissolve Start Point", Vector) = (1, 1, 1, 1) _DissolveEnd("Dissolve End Point", Vector) = (0, 0, 0, 1) _DissolveBand("Dissolve Band Size", Float) = 0.25 }
Finally, here’s a look at the effect in action on our little poltergeist fellow, synchronized with a particle system which we’ve animated on a curve to give that extra little bit of spooky panache:
And voilà, now we’ve created a nice, customizable shader perfect for teleportation, burning, dissolving, or any other bit of dematerialization magic.
Read more about:
Featured BlogsYou May Also Like