Screen Space God Rays (Godot.4.3)
This shader re-creates the effect described in the following article:
volumetric-light-scattering-as-a-custom-renderer-feature-in-urp
You can effectively mimic the behaviours of volumetric light by adding a radial blur to a white circle that is positioned on a light source and multiplied by an object occlusion mask. This can be achieved in Godot using a SubViewport that renders the scene with the background missing, which is then passed to a ColorRect containing a shader which calculates the light source circle and radial blur.
Instructions:
1. Add the following nodes to your scene :
- Control
- ColorRect_LightRays
- SubViewportContainer
- SubViewport
- Camera3D_Viewport
2. Add a RemoteTransform3D node to your current camera, or the node that controls your main camera.
- Player
- Head
- Camera3D [current]
- RemoteTransform3D
- Head
Note: See screenshots for final node structure.
3. Adjust node settings accordingly:
RemoteTransform3D
Remote Path: Camera3D_Viewport
Use Global Coordinates: true
Update -> Position: true
Update -> Rotation: true
Update -> Scale: false
ColorRect_LightRays
Layout -> Anchor Preset: Full Rect
Material -> Resource -> Local to Scene: true
SubviewportContainer
Layout -> Anchor Preset: Full Rect
Visibility -> Visibility Layer: null // make sure no layers are selected
Subviewport
Size: [ set to your screen resolution ]
RenderTarget -> Clear Mode: NextFrame
RenderTarget -> Update Mode: Always
Viewport -> Transparent BG: true
4. Create a new script, copy/paste the ‘GD Script’ section from the Shader code below and then add the script to the ColorRect_LightRays node in your scene.
5. Set the Light, Camera node paths for the ColorRect node:
- The ‘Light’ path should point to a DirectionalLight3D node in your scene.
- The Camera path should point to your main/current camera.
6. Add a new Shader Material to the ColorRect_LightRays node, create a new shader script and then copy/paste the ‘Shader’ section from the Shader code below.
7. Open the shader material parameters section and click on the ‘Subviewport Tex’ paramater.
8. Select ‘New ViewportTexture’ and then select the Subviewport node in your scene.
9. Adjust the parameters below to change the behaviour of the light rays:
- Ray Length
- Ray Intensity
- Light Source Scale
- Light Source Feather
Limitations:
- Objects that are not within the camera’s view will not occlude light and if the light source is behind the camera no light rays will be drawn.
- Light may not be occluded correctly on transparent materials. Addative shading appears to work best when dealing with shader materials -> render_mode blend_add;
- This will only work with DirectionalLight3D nodes.
Omni lights can work with the same method but creating the occlusion mask requires finding objects that are in-front of and behind a given light source.
Shader code
// GD Script
// Add to ColorRect_LightRays
extends ColorRect
@export var light_path : NodePath
@onready var light : DirectionalLight3D = get_node(light_path)
@export var camera_path : NodePath
@onready var camera : Camera3D = get_node(camera_path)
func _process(_delta: float) -> void:
var pos = camera.unproject_position(camera.global_position - (-light.global_basis.z.normalized()))
material.set_shader_parameter("light_source_pos", pos)
material.set_shader_parameter("light_source_dir", -light.global_basis.z)
material.set_shader_parameter("camera_dir", -camera.global_basis.z)
// Shader
// Add to ColorRect_LightRays -> Materials -> ShaderMaterial
shader_type canvas_item;
render_mode unshaded, blend_add;
uniform sampler2D subviewport_tex : filter_linear;
uniform sampler2D light_color : hint_default_white;
uniform float ray_length : hint_range(0.0, 1.0) = 1.0;
uniform float ray_intensity : hint_range(0.0, 1.0) = 1.0;
uniform float light_source_scale = 1.0;
uniform float light_source_feather = 2.0;
uniform float noise_strength : hint_range(0.0, 1.0) = 0.2;
uniform vec2 light_source_pos = vec2(0.0, 0.0);
uniform vec3 light_source_dir = vec3(0.5, -1.0, 0.25);
uniform vec3 camera_dir = vec3(-0.5, 1.0, -0.25);
const int SAMPLE_COUNT = 200;
float random (vec2 uv) {
return fract(sin(dot(uv.xy, vec2(12.9898,78.233))) * 43758.5453123);
}
void fragment() {
// divide light source pos by screen size for correct UV positioning
vec2 light_pos = light_source_pos / (1.0 / SCREEN_PIXEL_SIZE);
vec2 dir = UV - light_pos;
// light source uv coords with correct aspect ratio for drawing circle
vec2 ratio = vec2(SCREEN_PIXEL_SIZE.x / SCREEN_PIXEL_SIZE.y, 1.0);
vec2 dir2 = UV / ratio - light_pos / ratio;
float light_rays = 0.0;
vec2 uv2;
float scale, l;
for (int i = 0; i < SAMPLE_COUNT; i++){
scale = 1.0f - ray_length * (float(i) / float(SAMPLE_COUNT - 1));
l = (1.0 - texture(subviewport_tex, dir * scale + light_pos).a);
uv2 = dir2 * scale * pow(light_source_scale, 2.0);
l *= smoothstep(1.0, 0.999 - light_source_feather, dot(uv2, uv2) * 4.0);
light_rays += l / float(SAMPLE_COUNT);
}
// multiply with noise to reduce color banding
float n = 1.0 - random(UV) * noise_strength * smoothstep(0.999 - 1.25, 1.0, dot(dir2, dir2) * 2.0);
light_rays *= n;
// multiply with camera/light dot to fade based on view angle
float d = clamp(-dot(normalize(camera_dir), normalize(light_source_dir)), 0.0, 1.0);
light_rays *= d;
COLOR.rgb = vec3(light_rays * ray_intensity) * texture(light_color, UV).rgb;
}
These are really good results! Very detailed instructions as well, thanks for making this shader!