Screenspace Godray

Screen-space godray based on Chapter13 of GPU Gems 3

Attach shader to quad size (2,2) meters. Make sure quad is near and in front of camera, to avoid frustrum culling.
To pass a directional light direction to shader, attach a script and pass the -global_basis.z of the DirectionalLight3D to the instance paramter “light_world_dir”

For example:

@tool
extends MeshInstance3D
@export var light_source: DirectionalLight3D
func _process(delta: float) -> void:
    if light_source:
        set_instance_shader_parameter("light_world_dir", -light_source.global_transform.basis.z)
Shader code
//based on GPU Gems 3: Chapter 13. Volumetric Light Scattering as a Post-Process
shader_type spatial;
render_mode unshaded, cull_front, fog_disabled, blend_add;

uniform sampler2D depth_tex : hint_depth_texture,filter_nearest;

//attach a script that pass the -global_basis.z of your directional light
instance uniform vec3 light_world_dir = vec3(0.0, -1.0, 0.0);

uniform int num_samples = 32;
uniform float density = 0.75;
uniform float weight = 0.02;
uniform float decay = 0.95;
uniform float exposure = 0.5;

uniform float light_fade_start:hint_range(0.0, 1.0, 0.1) = 0.8;
uniform float light_fade_end:hint_range(0.0, 1.0, 0.1) = 1.0;

uniform vec3 ambient_col : source_color = vec3(1.167, 1.182, 0.725);

float bayer4x4(vec2 uv, vec2 viewport_size) {
	ivec2 p = ivec2(mod(uv * viewport_size, 4.0));
	int index = p.x + p.y * 4;

	float matrix[16] = float[](
		0.0/16.0,  8.0/16.0,  2.0/16.0, 10.0/16.0,
		12.0/16.0, 4.0/16.0, 14.0/16.0,  6.0/16.0,
		3.0/16.0, 11.0/16.0,  1.0/16.0,  9.0/16.0,
		15.0/16.0, 7.0/16.0, 13.0/16.0,  5.0/16.0
	);

	return matrix[index];
}

float sample_depth_value(sampler2D depth_texture, vec2 uv, float default_val)
{
	if (uv.x < 0.0 || uv.x > 1.0 ||
		uv.y < 0.0 || uv.y > 1.0) {
		return default_val;
	}
	return texture(depth_texture,uv).r;
}
void vertex() {
	POSITION = vec4(VERTEX.xy, 1.0, 1.0);	
}

void fragment() {
	vec3 light_dir = normalize(light_world_dir);

	// Project directional light to screen
	vec3 world_light_pos = CAMERA_POSITION_WORLD + light_dir * 1000.0;
	vec4 clip = PROJECTION_MATRIX * VIEW_MATRIX * vec4(world_light_pos, 1.0);
	vec2 ndc = clip.xy / clip.w;
	vec2 light_screen_pos = ndc * 0.5 + 0.5;

	// Prevent extreme stretching when off-screen
	light_screen_pos = clamp(light_screen_pos, -0.5, 1.5);

	// dither noise using bayer matrix to smooth out the banding
	// could use blue noise too
	float noise = bayer4x4(SCREEN_UV, VIEWPORT_SIZE);

	vec2 delta = (SCREEN_UV - light_screen_pos) / float(num_samples) * density;
	vec2 tex_coord = SCREEN_UV + delta * noise;

	vec3 color = vec3(0.0);
	float illumination_decay = 1.0;

	for (int i = 0; i < num_samples; i++) {
		tex_coord -= delta;

		//builtin function return any value outside screen as 0.0 meaning sky
		//for this we want any value outside screen as being occluded, so revert to 1.0
		float scene_depth = sample_depth_value(depth_tex,tex_coord,1.0);

		// Godot reversed Z:
		// near = 1.0
		// far / sky = 0.0
		//occlusion = 0 when depth>1e-10 using invert step function
		//otherwise occlusion = 1
		float occlusion = 1.0 - step(1e-10, scene_depth);

		float t = float(i) / float(num_samples);
		float sample_fade = 1.0-smoothstep(light_fade_start, light_fade_end, t);

		color += ambient_col *
				occlusion *
				illumination_decay *
				weight *
				sample_fade;
					
		illumination_decay *= decay;
	}

	float sun_facing =
		smoothstep(0.0, 0.3, dot(CAMERA_DIRECTION_WORLD, light_dir));

	ALBEDO = color * exposure* sun_facing;
}
Live Preview
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.

Related shaders

guest

0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments