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:
smoothstep
interpolation to produce a soft transition between the opaque and transparent parts.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.
To get started, we need a 3D object to dissolve and a material to hold our shader.
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.
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!
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.
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);
}
If you cast your object in darkness, you’ll find that the edges are bright but aren’t glowing.
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.
There’s a couple of issues you may encounter.
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;
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;