The player travels through a heated desert in the noon sun. An explosion goes off with a blast of heat. A blackhole opens and starts pulling in light and objects. You control these effects with a distortion shader, sampling the colors according to a mask.
Just like previous post-processing scenes, we start with a main scene that’s a child of a Viewport
in a ViewportContainer
. See the post-processing tutorial earlier in this series if you need a refresher.
We’re using a 3D scene this time around, so there’s one more thing to keep in mind: anti-aliasing. The viewport may have an anti-aliasing setting that’s different from your default rendering settings, so you’ll need to control that as required.
You control it inside the Viewport
node’s Msaa property.
The shader is on the ViewportContainer
’s material as a ShaderMaterial
.
A large part of the shader is already covered: we just want to use the code to build the torus mask from the previous section. Add the torus thickness, hardness, radius, invert, center and size properties. For the displacement itself, we add one more uniform: the amount of displacement. The higher the value, the more warped it will make things look.
shader_type canvas_item; uniform float torus_thickness : hint_range(0.001, 1.0) = 0.5; uniform float torus_hardness = 1.0; uniform float torus_radius = 1.0; uniform float torus_invert : hint_range(-1.0, 1.0) = 1.0; uniform vec2 torus_center = vec2(0.5, 0.5); uniform vec2 torus_size = vec2(1.0, 1.0); uniform float displacement_amount;
The fragment shader is the same, but we change how we use the mask.
void fragment() { // Build mask float torus_distance = length((UV - torus_center) * torus_size); float radius_distance = torus_thickness / 2.0; float inner_radius = torus_radius - radius_distance; float circle_value = clamp(abs(torus_distance - inner_radius) / torus_thickness, 0.0, 1.0); float circle_alpha = pow(circle_value, pow(torus_hardness, 2.0)); float mask = abs(clamp(abs(sign(torus_invert)) - sign(torus_invert), 0.0, 1.0) - circle_alpha) * abs(torus_invert);
Instead of putting it in the texture, we’ll instead use it to add an offset to the UV
s of the texture. That way, when the shader samples pixels, it will find the color from further in or out from the original position.
vec2 displacement_uv = UV + mask * displacement_amount;
Which we can use to find the color.
vec4 distorted_color = texture(TEXTURE, displacement_uv);
Which we can output.
COLOR = distorted_color;
For a final shader code of
shader_type canvas_item; uniform float torus_thickness : hint_range(0.001, 1.0) = 0.5; uniform float torus_hardness = 1.0; uniform float torus_radius = 1.0; uniform float torus_invert : hint_range(-1.0, 1.0) = 1.0; uniform vec2 torus_center = vec2(0.5, 0.5); uniform vec2 torus_size = vec2(1.0, 1.0); uniform float displacement_amount; void fragment() { // Mask float torus_distance = length((UV - torus_center) * torus_size); float radius_distance = torus_thickness / 2.0; float inner_radius = torus_radius - radius_distance; float circle_value = clamp(abs(torus_distance - inner_radius) / torus_thickness, 0.0, 1.0); float circle_alpha = pow(circle_value, pow(torus_hardness, 2.0)); float mask = abs(clamp(abs(sign(torus_invert)) - sign(torus_invert), 0.0, 1.0) - circle_alpha) * abs(torus_invert); // Displace vec2 displacement_uv = UV + mask * displacement_amount; vec4 distorted_color = texture(TEXTURE, displacement_uv); COLOR = distorted_color; }
Play around with the displacement_amount
to get a feel as to how the main scene can be warped.
You can substitute the torus for different masks, or substitute in a texture instead that you can sample from. For example, you could have a viewport that contains a particle emitter that emits white fuzzy particles on a black background and use that to trail behind a projectile of hot plasma, as in Harvester.