Sci-Fi Scanner Pulse (Godot 4 update)

A basic scanner “pulse” effect that’s often found in sci-fi games. This is a version of my previous shader that has been updated for Godot 4. It you’re following the video, there are a couple of extra steps to set up the Quad Mesh that I’ve outlined below.

It needs to be added to a Quad mesh that is size (2,2) and rendered in the frame. I would suggest making the mesh instance a child of the camera to keep track of it.

You also want to turn on “Flip Faces” in the mesh itself, and under the “Geometry Node” heading, you’ll probably want to increase the “Extra Cull Margin” as high as it will go.

There also needs to be an associated script on the quad that keeps track of starting the effect, speed and distance travelled. Example script included below.

extends MeshInstance3D

# Internal Variables.
var distance := 15.0
var is_running := false

# Play with these variables.
@export var max_distance := 20.0
@export var speed := 2.0

# Get the reference to the material to pass data to shader parameters.
@onready var SHADER: ShaderMaterial = self.get_active_material(0)

# Don't forget to assign the start point node to this variable.
@export_node_path("Node3D") var origin_point


func _ready():
	# Set the start point of the effect in the shader to the world position of
	# of the origin_point.
	SHADER.set_shader_parameter("start_point", get_node(origin_point).get_global_transform())


func _process(delta):
	if is_running:
		distance += delta * speed
		if distance > max_distance:
			is_running = false
			# Set distance to 0 to stop shader from rendering the effect.
			distance = 0.0
		SHADER.set_shader_parameter("radius", distance)
	
	if Input.is_action_just_pressed("ui_accept"):
		is_running = true
		distance = 0.0
Shader code
/* 
Basic sci-fi pulse post-processing effect.
Required to be put on a quad mesh that is rendered in the scene.
Required associated script to increase the radius over time.
Video tutorial on YouTube: https://youtu.be/x1dIJdz8Uj8
Written by Michael Watt

Thanks to Inigo Quilez for the SDF (https://iquilezles.org/articles/distfunctions/)
Thanks to nonunknown for the conditional statement replacements (https://godotshaders.com/shader/optimize-your-shaders/)
*/

shader_type spatial;
render_mode unshaded;

// Settings to play with
uniform mat4 start_point = mat4(1.0);
uniform float pulse_width = 2.0;
uniform vec4 color : source_color = vec4(1.0);

// Updated by Script
uniform float radius = 5.0;

// Access the screen and depth buffers
uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap;
uniform sampler2D DEPTH_TEXTURE : hint_depth_texture, filter_linear_mipmap;

// Necessary for rebuilding the world coordinates
varying mat4 CAMERA;

// Function from Inigo Quilez https://iquilezles.org/articles/distfunctions/
float sdSphere( vec3 p, float s ) {
	return length(p)-s;
}

// Replacements for < and > because math on GPU is fast. They return 1 or 0
float when_lt(float left_side, float right_side) {
	return max(sign(right_side - left_side), 0.0);
}
float when_gt(float left_side, float right_side) {
	return max(sign(left_side - right_side), 0.0);
}

void vertex() {
	POSITION = vec4(VERTEX, 1.0);
	
	CAMERA = INV_VIEW_MATRIX;
}

void fragment() {
	// Get the original screen rendered texture at the screen uv coordinates.
	vec4 original = textureLod(SCREEN_TEXTURE, SCREEN_UV, 0.0);
	
	// Get the depth value form the depth buffer.
	float depth = textureLod(DEPTH_TEXTURE, SCREEN_UV, 0.0).x;
	vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, depth);
	
	// Unecessary for this effect, but to get the linear depth value,
	// use the following code.
	// vec4 view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
	// view.xyz /= view.w;
	// float linear_depth = -view.z;
	
	// Calculate the fragment's world position
	vec4 world = CAMERA * INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
	vec3 world_position = world.xyz / world.w;
	
	// Use the provided start transform and shift the world position to match
	// for the SDF to work as expected.
	vec3 adjusted_position = (inverse(start_point) * vec4(world_position, 1.0)).xyz;
	float dist = sdSphere(adjusted_position, radius);
	
	// Extra calculations to get the correct gradient direction.
	// Using calculation functions in place of if statements.
	float mix_ratio = 0.0;
	float check = when_lt(dist, 0.0) * when_gt(dist, -pulse_width);
	float percentage = abs(dist) / abs(pulse_width);
	mix_ratio = 1.0 * check - percentage;
	mix_ratio = clamp(mix_ratio, 0.0, 1.0);
	
	// Set the albedo to the mix between the original screen and the added
	// pulse color.
	ALBEDO = mix(original.rgb, color.rgb, mix_ratio);
}
Tags
Depth buffer, depth texture, Post processing, Science fiction, scifi
The shader code and all code snippets in this post are under CC0 license and can be used freely without the author's permission. Images and videos, and assets depicted in those, do not fall under this license. For more info, see our License terms.

More from Watt Interactive

Quick Example Fresnel Outline

Sci-Fi Scanner Pulse (Godot 3.5)

Shaped Glow Post-Processing Effect

Related shaders

Sci-Fi Scanner Pulse (Godot 3.5)

SciFi Hologram

Godot 4.x Color Swap for 3D Mesh Models

Subscribe
Notify of
guest

6 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
XenithStar
XenithStar
11 months ago

There appears to be a problem with the pulse completely obscuring planes behind it. This appears to affect Sprite3Ds as well as MeshInstance3Ds with QuadMesh geometries. I believe it has something to do with the ‘flip_faces` flag, since turning it off unobscures those planes. You can easily see this in the editor, as it’ll obscure things like the ground plane grid and the DirectionalLight3D icon.

Fab
Fab
5 months ago

Hi Mike,
many thanks for this shader plus video description. I tested it within my code and it worked very well with Godot 4. I have one question concerning GPUParticles: Whenever this shader is running I dont see any particles anymore. What could be the reason and how to fix it? Many thanks for answers in advance.
Best regards
Fab

Last edited 5 months ago by Fab
Britwaldo
Britwaldo
5 months ago
Reply to  Fab

Try adding this in the render_mode line: ‘render_mode unshaded, blend_add; ‘ worked for me ๐Ÿ™‚

Fab
Fab
5 months ago
Reply to  Britwaldo

Hi Britwaldo,
many thanks. That is the solution. ๐Ÿ™‚
Fab

No One
No One
1 month ago

doesnt seem to work in godot 4.3 :/

Sky
Sky
1 month ago
Reply to  No One

Due to the reverse-z change in 4.3 (https://godotengine.org/article/introducing-reverse-z/), you need to change the write to position line in vertex to POSITION = vec4(VERTEX.xy, 1.0, 1.0)