Shockwave Distortion Shader (2D Space Wrap)

2d shockwave shader

This tutorial is based on this work & is not my original work.

I had to add a distortion zone in my game, which can act as a portal. The place must have shockwave, distortion or space wrap like effect. I found a solution on godotshaders.com, so I used it. However, I am writing a tutorial on it here so people learning shaders can understand it. This shader also has chromatic aberration effect with it.

2d distortion shader

I made a 3D fast travel distortion shader as well. Something I think is related to it.

Defining Uniform Variables

Uniforms are variables that are sent from CPU to GPU. Most of them have unchanged values, but we will control center from GDScript in real-time:

shader_type canvas_item;

// Core distortion parameters
uniform float strength: hint_range(0.0, 0.1, 0.001) = 0.08;
uniform vec2 center = vec2(0.5, 0.5);
uniform float radius: hint_range(0.0, 1.0, 0.001) = 0.25;

// Visual effect parameters
uniform float aberration: hint_range(0.0, 1.0, 0.001) = 0.425;
uniform float width: hint_range(0.0, 0.1, 0.0001) = 0.04;
uniform float feather: hint_range(0.0, 1.0, 0.001) = 0.135;
  • strength: How powerful the distortion is (like how much it bends the space)
  • center: Where the effect happens on the screen (0.5, 0.5 is the middle)
  • radius: How big the effect is
  • aberration: Creates that cool rainbow effect on the edges
  • width: How thick the distortion ring is
  • feather: How smooth the edges are

Getting Access to the Screen

We need to be able to see what’s on the screen to distort it:

uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap;

This line gives us access to whatever is being displayed on the screen.

Fragment Shader

This is where the actual distortion happens. We’ll break it down into steps:

void fragment() {
    // 1. Basic Setup
    vec2 screen_uv = SCREEN_UV;
    
    // 2. Fix the Stretching
    float aspect_ratio = SCREEN_PIXEL_SIZE.y/SCREEN_PIXEL_SIZE.x;
    vec2 aspect_corrected_uv = (screen_uv - vec2(0.0, 0.5)) / vec2(1.0, aspect_ratio) + vec2(0.0, 0.5);

This first part sets up our coordinates. SCREEN_UV gives us the position we’re working with, and we fix it so the effect looks circular instead of oval.

    // 3. Find Distance from Center
    vec2 distance_from_center = aspect_corrected_uv - center;
    
    // 4. Create the Ring Shape
    float distance_length = length(distance_from_center);
    float outer_edge = smoothstep(radius - feather, radius, distance_length);
    float inner_edge = smoothstep(radius - width - feather, radius - width, distance_length);
    float distortion_mask = (1.0 - outer_edge) * inner_edge;

Here we’re creating the ring shape where our effect will appear. The smoothstep makes the edges nice and smooth instead of harsh.

    // 5. Calculate How Much to Bend
    vec2 distortion_direction = normalize(distance_from_center);
    vec2 distortion_offset = distortion_direction * strength * distortion_mask;
    
    // 6. Apply the Bend
    vec2 distorted_uv = aspect_corrected_uv - distortion_offset;

This part figures out which way and how much to bend the image.

    // 7. Add Rainbow Effect
    vec2 color_separation = distortion_offset * aberration * distortion_mask;
    
    // 8. Put It All Together
    vec2 final_uv = mix(screen_uv, distorted_uv, distortion_mask);
    
    // 9. Sample Different Colors
    vec4 red_channel = texture(SCREEN_TEXTURE, final_uv + color_separation);
    vec4 base_color = texture(SCREEN_TEXTURE, final_uv);
    vec4 blue_channel = texture(SCREEN_TEXTURE, final_uv - color_separation);
    
    // 10. Final Color
    COLOR = vec4(
        red_channel.r,
        base_color.g,
        blue_channel.b,
        1.0
    );
}

The final part creates that cool chromatic aberration effect (the rainbow edges) by slightly offsetting the red and blue colors.

Using the Shader

Apply this shader to a ColorRect node or TextureRect node or any canvas_item in general by creating a ShaderMaterial in inspector and applying this shader on it. And attach a script to it. For testing, I will pass an object as @export variable. And in code, we will display the effect at the global position of that object.

Attach this script on the ColorRect node where this shader is applied:

@tool # To view this in editor. Otherwise remove @tool keyword

extends ColorRect  # or whatever node type you're using

@export var node_2d: Node2D # Which node to apply this shader to

@onready var camera_2d := get_viewport().get_camera_2d()

func set_distortion_center(world_position: Vector2) -> void:
	if camera_2d == null: camera_2d = Camera2D.new()
	
	# Get the current viewport size
	var viewport_size: Vector2 = get_viewport_rect().size
	
	# Get the camera's center position (accounts for smoothing and limits)
	var camera_center: Vector2 = camera_2d.get_screen_center_position()
	
	# Calculate screen position (normalize to 0.0 - 1.0 range)
	var screen_position: Vector2 = (
		# First convert world position to screen coordinates
		(world_position - camera_center) * camera_2d.zoom +  # Apply camera zoom
		viewport_size / 2  # Center offset
	)
	
	# Convert to normalized coordinates (0-1 range)
	var normalized_position = screen_position / viewport_size
	
	# make sure material is ShaderMaterial
	material.set_shader_parameter("center", normalized_position)


# Example: Update center when mouse is clicked
func _process(delta):
	var world_pos = node_2d.global_position
	set_distortion_center(world_pos)

And thats it! Thank you for reading <3

space wrap shader

Leave a Reply

Your email address will not be published. Required fields are marked *