Dissolving 3D objects

ssolve3d_example.png

A spaceship uses a ray to flash burn a roof open. A wizard teleports away in a swirl of flames. The reactor core gets shut down and fizzles out. Dissolving an object is a solid tool to make an object disappearing a lot more interesting than fading away or popping out of existence.

You will learn to:

  • Make an object disappear gradually using a texture as a mask.
  • Use smoothstep interpolation to produce a soft transition between the opaque and transparent parts.
  • Make the burning or dissolving edges glow.

In short

We create a mask from a texture for the shader to sample from. Wherever the texture is below a certain threshold, the pixel there should be transparent, or on its way to becoming transparent. The darkest parts will disappear first, and the brightest parts will disappear last. The shader uses that mask value to blend the burning edge with the material’s color.

Hint: It’s best to go from a dark gray to light gray color rather than pure black or pure white, otherwise 0% and 100% will still give you a partial dissolve.

Two examples of dissolve masks

Setting up the scene

To get started, we need a 3D object to dissolve and a material to hold our shader.

  1. Create a new MeshInstance node.
  2. Add a new Sphere Mesh to the Mesh property.
  3. Assign a new ShaderMaterial to the material property.
  4. Click on the material you created to edit it.
  5. Assign a new Shader to the Shader property.
  6. Right-click on the new shader and select Save to store the shader to disk. I’ll name it dissolve.shader.

Coding the dissolve shader

Double-click on dissolve.shader to open it, and let’s get coding.

Our shader is going to need some information to draw appropriately.

  • albedo: the color of our object.
  • texture_albedo: if we want the object to have a texture.
  • emission_color: the color the edges will glow.
  • emission_amount: the intensity the edges will glow.
  • dissolve_texture: the black and white mask we’ll sample to test for burn edges.
  • burn_size: the distance on the object by which the glow will travel.
  • dissolve_amount: a percentage from 0 to 1 to mark the progress from not-at-all dissolved to fully dissolved.
shader_type spatial;

uniform vec4 albedo : hint_color;
uniform sampler2D texture_albedo : hint_albedo;

uniform vec4 emission_color : hint_color = vec4(1);
uniform float emission_amount;

uniform sampler2D dissolve_texture;
uniform float burn_size : hint_range(0,2);
uniform float dissolve_amount : hint_range(0,1);

The hint_color statement gives us access to a color picker in the inspector. hint_range clamps the value for us between two values inclusively. Burning more than 100% or less than 0% could cause unusual glitches.

Hint: albedo and texture_albedo is how the default Spatial Material applies color. You can even convert a Spatial Material to a Shader Material to save time if you need more than the albedo and texture albedo (like normal map, metallic, roughness, etc) by right clicking on a Spatial Material and selecting Convert to Shader Material.

Configuring the mask

While any black and white texture will do, we’re going to use the open simplex noise generator built into Godot to create the dissolve texture. This makes it easy to test our shader with different transition masks and get the final desired effect. A noisy texture also gives great random looking burn patterns!

  1. Open the ShaderMaterial of your object in the inspector
  2. Assign a new NoiseTexture to the shader parameter Dissolve Texture.
  3. Click on the new texture to edit it.
  4. Pick an appropriate size. It doesn’t have to be large to be effective; if you’re targeting mobile, going smaller would be best. I’ll use 512x512.
  5. Be sure to check Seamless so we don’t get strange artifacts if we try to repeat the texture.
  6. Assign a new OpenSimplexNoise to the Noise property.
  7. By clicking on it you can play with the Period, Persistence, Lacunarity, Octaves and Seed properties to get a result you like.

Hint: See the official docs to know what each property does, but nothing beats experimentation to get a feel for how each one changes the result.

You can right click on the noise texture and save it to your project to re-use it if you want each dissolve to look the same.

Configured simplex noise

The fragment shader

Let’s get the color and texture color squared out of the way:

void fragment() {
    vec4 albedo_tex = texture(texture_albedo, UV);
    ALBEDO = albedo.rgb * albedo_tex.rgb;
}

Now sample out of the dissolve texture to get the level of dissolution at that point in the texture. You only need a single float because it’s a black and white image and all three of the colors are the same:

float sample = texture(dissolve_texture, UV).r;

The emission value at that point is the percentage of dissolution traveling outwards onto the object’s opaque pixels. To put it simply, this allows the glow to travel onto the parts of the object that are visible. You get that by adding the burn size to the upper edge of smoothstep.

Note: We explained smoothstep back in the 2D outline tutorial if you need a refresher.

float emission_value = 1.0 - smoothstep(dissolve_amount, dissolve_amount + burn_size, sample);

We need the difference from 1.0 to invert the value. Without inverting, the visible parts would glow instead of the disappearing parts. With that emission value, your final EMISSION is the product between that value, the color, and the emission amount.

EMISSION = vec3(emission_value * emission_amount * emission_color.rgb);

The ALPHA is almost identical to the emission value’s smoothstep, but instead of traveling onto the object, we want to clip the alpha up to the edge and no further. You get that by subtracting the burn size from the lower edge of smoothstep instead of adding it to the top.

ALPHA = smoothstep(dissolve_amount - burn_size, dissolve_amount, sample);

Which gives you the final shader code:

shader_type spatial;

uniform vec4 albedo : hint_color;
uniform sampler2D texture_albedo : hint_albedo;

uniform vec4 emission_color : hint_color = vec4(1);
uniform float emission_amount;

uniform sampler2D dissolve_texture;
uniform float burn_size : hint_range(0, 2);
uniform float dissolve_amount : hint_range(0, 1);


void fragment() {
    vec4 albedo_tex = texture(texture_albedo, UV);
    ALBEDO = albedo.rgb * albedo_tex.rgb;

    float sample = texture(dissolve_texture, UV).r;
    float emission_value = 1.0 - smoothstep(dissolve_amount, dissolve_amount + burn_size, sample);
    EMISSION = vec3(emission_value * emission_amount * emission_color.rgb);
    ALPHA = smoothstep(dissolve_amount - burn_size, dissolve_amount, sample);
}

Make the edges glow in 3D

If you cast your object in darkness, you’ll find that the edges are bright but aren’t glowing.

Bright edges

We want a nice, bright highlight that bleeds outwards from the brightest spots. Godot, thankfully, has the ability to do that for us using a WorldEnvironment node. Add a new one to your scene.

In its Environment property, assign a new Environment and click on it to edit its properties.

Enable the Glow property. The scene will look a little bit brighter but that’s because we’re taking the brightest color and overlaying it on our image by default. Instead, change the Blend Mode to Additive. It’s possible, depending on how bright you made your emission amount, that your object went radioactively and blindingly bright. We can refine the glow settings to make a more appealing glow.

To keep it from being too much, lower the Strength until the glow is around the brightest edges, or go back to your original color and lower the emissive strength to a more reasonable level. Changing the strength in the world environment will change it for all objects.

The other setting of interest are the Levels. The way glowing works is that any color value that is brighter than the Hdr Threshold (1.0 by default) gets rendered on a texture, then blurred more with each level. With Screen or Overlay blending, the brightest color in this image gets shown and the rest clamped down. With Additive blending, we add those colors together, layering each one on top of the other to cause a satisfying glow effect. The lower the level, the less blurred and the closer to the edge it is. I find having 2 through 5 turned on and the others turned off give a pretty good result; experiment to find your ideal result.

The last setting you need to know about is Bicubic Upscaling. The blurring happens internally in Godot using mipmaps: progressively smaller versions of the screen texture that, once upscaled and filtered, looks blurrier. Bilinear filtering filters to the left, right, up and down which can cause you to have square looking glows, or glows with unusually vertical or horizontal edges. Bicubic Upscaling uses a different algorithm to resize the textures. It’s more expensive but gives a more accurate result that gives rounder and softer glows. See the exaggerated examples below. The second image has a smoother gradient.

Exaggerated effect with bicubic upscaling off


Exaggerated effect with bicubic upscaling on

Troubleshooting

There’s a couple of issues you may encounter.

Dissolving objects have no shadows

3D objects that use an alpha channel don’t get shadows by default; the engine draws opaque pixels to the depth buffer which in turn controls what object are in front or behind others.

We can fix this by adding the depth_draw_alpha_prepass as a render mode. The engine will run through the shader once before drawing to the depth buffer to test for which pixels should cast shadows.

render_mode depth_draw_alpha_prepass;

Cannot see the opposite side of the sphere through the burn holes

To save on performance, 3D objects draw polygons that the camera can see; the engine culls out the ones facing away so they don’t get drawn. The fix is to add cull_disabled to draw all polygons, regardless of whether they point away or towards the camera.

render_mode depth_draw_alpha_prepass, cull_disabled;
Community Discussion