When a body collides with another body, the impact often causes some displacement due to changes in volume. This is seen when a rock is thrown into a lake and causes ripples to move outwards; or when you press jelly with a spoon. When the meteor that killed the dinosaurs crashed into the Earth’s surface, it probably caused a shockwave as well.
The shockwave, or ripple, effect simulates this effect. We can use it for explosions, stomps, earth bending skills, ripples in a water body, and much more.
In this tutorial, you’re going to learn some essential visual effects skills that you can apply to other shaders arts:
For our effect we’re going to use an unusual scene setup. We’ll take the game screen, process it, apply the effect, and finally render the game screen with the effect.
We have our game world where the actual game happens, and we have our “post process” layer which takes the game world and applies our effect on top of it. This is a common layout when you work with screen space shaders.
Let’s add a new Node as the root of our scene. This is the node that holds the necessary setup for our effect to work. Let’s call it “Game”.
Add a new Texture Rect as a child. Here’s where the magic happens. Rename it as PostProcessedScreen and in the Layout menu, select “Full Rect” so it takes up the whole screen. We’ll use this node to display the game’s world after we have applied our effect.
We’ll pass snapshots of the raw game screen to the Texture property and then apply the shader to it. The final result will be our post processed screen so this node holds our shader.
In the Shockwave/ folder, drag the displacement_material.tres to the PostProcessedScreen > material slot. It’s a simple CanvasMaterial that uses our displacement_shader which we prepared beforehand.
Ensure that the displacement_material.tres is marked as local_to_scene to avoid changing anything outside of this scene which shares this resource.
Let’s look at how the displacement_shader works.
Distortion shaders, also know as displacement shaders, work by remapping a texture by changing its UV coordinates.
If you want to know how this UV mapping works, you can watch Part 2 of our video series introducing shaders, where Nathan talks about manipulating UV maps.
To get in depth knowledge and learn the secrets of the shaders arts check out our Shader Secrets course, where François guides you through the ins and outs of this shady art.
In our distortion shader, we use a texture to dictate how the UV coordinates will be displaced.
shader_type canvas_item; uniform sampler2D displacement_mask; uniform float displacement_amount = 5.0;
Then, we take the object’s current texture and remap it using the new displaced UV.
void fragment() { float uv_displace_amount = displacement_amount * 1.0 / float(textureSize(TEXTURE, 0).x); vec4 displacement = texture(displacement_mask, UV); vec2 displacement_uv = UV + displacement.xy * uv_displace_amount; COLOR = texture(TEXTURE, displacement_uv); }
Let’s see how we can feed the shader with these two textures so it can do its magic.
Our shader is pretty agnostic with the textures it uses, so we can pass anything to it and it’ll do its job. But since we’re aiming for a shockwave effect, we need these textures to be dynamic since the wave will move and dissipate.
Instead of passing pre-rendered images to the shader, we’ll pass snapshots of how the game is at a given moment using Viewport Textures.
In Godot, a Viewport processes all its children and renders a texture which represents our game. For instance, if you add two Sprites as children of a Viewport, it renders a canvas with a background and those two Sprites.
If you run a scene and check the “Remote” view of the SceneTree, you’ll notice there’s a node of type Viewport called root. This is the ultimate Viewport that Godot uses to render everything else.
For our shader we’ll add two Viewports to our scene. The idea is that each Viewport renders two different worlds that we’ll mix later with our shader.
The first Viewport is going to render our game world, so let’s call it “World”. Everything that is part of our game boils down to this Viewport.
Let’s call the second Viewport Mask. We’ll use everything it renders as a mask which is used in the shader.
We’ve set up a basic scene that you can use to test this effect. In the Shockwave/ folder drag the ShockwaveWorld.tscn to the SceneTree as a child of the World node.
Notice that as soon as we add the ShockwaveWorld as a child of the World Viewport, it disappears. This is because a custom Viewport needs a render target to display what it rendered. Godot uses the game’s window as a render target for the root Viewport, but for custom Viewports we have to create render targets manually. Here is where our PostProcessedScreen comes in. We’ll use it to render our World.
Remember, since Godot’s SceneTree processes nodes from top to bottom and, since our Viewports are dependencies of the PostProcessedScreen, they need to be processed before the PostProcessedScreen can use them. So we need to move the PostProcessedScreen to the bottom of the hierarchy.
Before we can render our custom Viewports, we need to define their capture rectangle.
Hold shift to select both the World and the Mask nodes and in the Inspector. In our case, both Viewports are going to take the full screen. So in the Size property let’s make the x
and y
values match our game’s window width and height respectively. Set x
to 1024
and y
to 600
.
Let’s also ensure that they render with transparent backgrounds so we can properly mix them in the shader. Mark the Transparent BG property as true.
If there isn’t an update in a Viewport or if the Viewport is not visible, Godot doesn’t process it for optimization reasons. In our case they won’t be visible directly, but we still want to process them. So let’s ensure that both our Viewports are always processing by forcing them to always update. In the Render Target > Update Mode left-click where it says “When Visible” and from the dropdown menu select “Always”.
Since we’re using custom Viewports, we have to manually tell them to flip their rendered texture vertically. Otherwise we get a mirrored version of our game in our PostProcessedScreen. Turn the Render Target > V Flip option as true.
With that we can render them to our render target! Select the PostProcessedScreen and in the Texture property left-click the “Empty” slot. From the dropdown menu select “New Viewport Texture” and from the popup menu, select the World Viewport. Now we can see our ShockwaveWorld!
If you look closely you can notice that our World looks sharp, which may not reflect how the ShockwaveWorld looks. This is because it became a texture and it we didn’t tell Godot to apply the proper filters to this new texture. Let’s fix this.
Note that this step isn’t necessary if you want sharp edges in your game. For instance, if you’re going with a pixel art aesthetic you can skip this fix.
Left-click on the PostProcessedScreen > Texture slot to edit this resource. In the Flags category check the Mipmaps and the Filter options and voila, our World is fixed.
Next, let’s setup our Mask render target which is the displacement_shader > Displacement Mask parameter. Left-click the PostProcessedScreen > Material > Material to display the Shader Param category. Left-click the Displacement Mask slot and create a new Viewport Texture. This time, select the Mask Viewport from the popup menu.
If you get the following popup instead, right-click the displacement_material slot and select “Make Unique”. This happens due to Viewport Textures having a dependency on the hierarchy of the scene so they can only be saved as built-in resources of a PackedScene.
You can repeat the filtering fix that we used in the World Viewport Texture to get a smoother result for the Mask Viewport Texture as well.
Now, anything the Mask draws results in a distortion. As a test, you can drag the white_ring.png image from the Assets/ folder and drop it on top of our little planet. Make sure it instances as a child of the Mask Viewport.
In this chapter we’ve talked a lot about Viewports and Viewport Textures. If you’re still unsure, the official documentation is highly recommended so you can better understand them.
We have our mask working! It’s time to design our shockwave.
Let’s create a new scene to focus on the effect itself.
We’re going to use a Particles2D for the shockwave to procedurally animate the wave, but you can use a Sprite instead if you want more control. Add a new Particles2D to the scene and rename it to Shockwave.
Drag the white_ring.png image from the Assets/ folder to the Shockwave > Texture slot. This represents our shockwave.
The Amount property dictates how many shockwaves the effect produces along its Lifetime. For instance, if we want an effect that resembles ripples on the water, we can increase the Amount to 8
or 10
.
Our shockwave is caused by a meteor and, since the Earth is very solid, we’ll set the Amount to 3
. To slowdown the particles, increase the Lifetime to 2.0
seconds.
If you want to use this effect in moving objects, you’ll also need to uncheck the Drawing > Local Coords property. This is useful if you have a ship moving on water that creates ripples, or a spaceship that activates a boost. This is what we did in our game, Harvester.
In the Process Material > Material property create a new Particles Material. We’ll use this to animate the shockwave.
To prevent the rings from falling, disable the gravity of the Particles Material by setting all three values to 0
in Gravity > Gravity.
A shockwave grows in size quickly then suddenly stops. Check out Riot’s Game video about explosions timing for more details and examples.
To mimic this behavior we’ll use scale curves. In the Scale > Scale Curve property, create a new CurveTexture. Left-click the CurveTexture and the Curve slot to edit the curve. Right-click on the graph and from the dropdown menu go to Load Preset > Ease Out.
We need to tweak the preset a bit to achieve the result we want.
Left-click on the point on the left and adjust its handle to increase the curvature to make it look like a hill. Fine tune the point on the right as well. Depending on your final composition you may also want to tweak the final scale of the effect as well. In our case, we’ll scale it up. Set the Scale > Scale property to 5.0
.
The displacement should also gradually lose its strength as the shockwave grows in size. We can achieve this by fading out.
In the Color > Color Ramp property create a new Gradient Texture and make a Gradient which starts with full opaque white and ends with full transparent white. The fading should only happen towards the end of the Gradient.
At this point we can save our Shockwave scene to make it a reusable asset.
Now that we have our scene ready and our effect done, we can apply it to our final composition.
Create an instance of the Shockwave in our Game scene and position it accordingly. With the effect in place, we’ll reparent it as a child of the Mask Viewport.
And there we have it! You can use an AnimationPlayer to trigger the Shockwave > Emitting property, or even a Timer.
In the ShockwaveDemo, we’ve played a bit with the Shockwave > Speed Scale by decreasing it to 0.8
to give more weight to the comet’s impact, but that’s optional.
You can also play with the PostProcessedScreen > Material > Displacement Amount to change the strength of the effect. We’ve set it to 8
in the ShockwaveDemo.
Note that this is a static implementation of this effect. Ideally, the Mask node would dynamically instance the Shockwave triggered by some event. You can apply our EventBus design pattern to make the communication between the trigger and the Mask. For example, we could make the Explosion emit a signal that would prompt the Mask to add a Shockwave at the Explosion position.