Lens Flare for Godot 4
This is a fork/improvement of https://godotshaders.com/shader/lens-flare-shader/ which is in turn a port of https://www.shadertoy.com/view/4sX3Rs
The main change is switching from blend_mix to blend_add. This fixes the shader affecting the entire screen and also makes it so that the SCREEN_TEXTURE doesn’t need to be looked at. I also cleaned up the formatting quite a bit 🫡
To use:
- Create a new
TextureRectnode and set its anchor to fill the screen- Probably also good to set it to expand horizontally and vertically
- Give it a
PlaceholderTexture2D - Under
CanvasItem->Material, create a newShaderMaterialand assign it to this shader - Give the shader a noise texture (just the default
NoiseTexture2Dis fine) - Attach a script that updates the sun position each frame
Here’s an example script that, when given a DirectionalLight3D, sets the lens flare to track it. As a bonus, this also does a raycast to see if the sun is visible and turns off the lens flare if it isn’t (for example, if you are indoors). The _can_see_sun ray cast can be disabled if it’s not necessary for your game.
extends TextureRect
## How far to ray cast to check if the sun is visible
const SUN_RAYCAST_LENGTH: float = 750.0
## The sun that the lens flare follows
@export var sun: DirectionalLight3D
var camera: Camera3D
func _ready() -> void:
camera = get_viewport().get_camera_3d()
func _physics_process(_delta: float) -> void:
if not sun or not camera:
visible = false
return
var effective_sun_direction: Vector3 = (
(sun.global_transform.basis.z * maxf(camera.near, 1.0)) + camera.global_position
)
# turn off the lens flare the the sun isn't visible at all
visible = not camera.is_position_behind(effective_sun_direction)
if visible:
# OPTIONAL: hide the lens flare if the sun is blocked
if not _can_see_sun():
visible = false
return
var sun_screen_position := camera.unproject_position(effective_sun_direction)
(material as ShaderMaterial).set_shader_parameter("sun_position", sun_screen_position)
## Uses a ray cast to see if anything is blocking the sun
func _can_see_sun() -> bool:
if not sun or not camera:
return false
var origin := camera.global_position
var end := origin + sun.global_basis.z * SUN_RAYCAST_LENGTH
var space_state: PhysicsDirectSpaceState3D = camera.get_world_3d().direct_space_state
var query := PhysicsRayQueryParameters3D.create(origin, end)
var result := space_state.intersect_ray(query)
return result.is_empty()
Note that, if you have multiple cameras, you will also need to update which camera this script is using (or use something like Phantom Camera, which makes it so you only ever have one camera but can control it easily).
This pairs nicely with Sky3D, if you pass in a Sky3D node you can get the sun via sky_3d.sun.
Shader code
// TRANSLATED & MODIFIED FROM: https://www.shadertoy.com/view/4sX3Rs
shader_type canvas_item;
render_mode blend_add;
uniform vec2 sun_position = vec2(0.0);
uniform vec3 tint = vec3(1.4, 1.2, 1.0);
uniform sampler2D noise_texture;
float noise_float(float t, vec2 texResolution)
{
return texture(noise_texture, vec2(t, 0.0) / texResolution).x;
}
float noise_vec2(vec2 t, vec2 texResolution)
{
return texture(noise_texture, t / texResolution).x;
}
vec3 lens_flare(vec2 uv, vec2 pos, vec2 texResolution)
{
vec2 main = uv - pos;
vec2 uvd = uv * length(uv);
float ang = atan(main.x, main.y);
float dist = length(main);
dist = pow(dist, 0.1);
float n = noise_vec2(vec2(ang * 16.0, dist * 32.0), texResolution);
float f1 = max(0.01 - pow(length(uv + 1.2 * pos), 1.9), 0.0) * 7.0;
float f2 = max(1.0 / (1.0 + 32.0 * pow(length(uvd + 0.8 * pos), 2.0)), 0.0) * 0.25;
float f22 = max(1.0 / (1.0 + 32.0 * pow(length(uvd + 0.85 * pos),2.0)), 0.0) * 0.23;
float f23 = max(1.0 / (1.0 + 32.0 * pow(length(uvd + 0.9 * pos), 2.0)), 0.0) * 0.21;
vec2 uvx = mix(uv, uvd, -0.5);
float f4 = max(0.01 - pow(length(uvx + 0.4 * pos), 2.4), 0.0) * 6.0;
float f42 = max(0.01 - pow(length(uvx + 0.45 * pos), 2.4), 0.0) * 5.0;
float f43 = max(0.01 - pow(length(uvx + 0.5 * pos), 2.4), 0.0) * 3.0;
uvx = mix(uv, uvd, -0.4);
float f5 = max(0.01 - pow(length(uvx + 0.2 * pos), 5.5), 0.0) * 2.0;
float f52 = max(0.01 - pow(length(uvx + 0.4 * pos), 5.5), 0.0) * 2.0;
float f53 = max(0.01 - pow(length(uvx + 0.6 * pos), 5.5), 0.0) * 2.0;
uvx = mix(uv, uvd, -0.5);
float f6 = max(0.01 - pow(length(uvx - 0.3 * pos), 1.6), 0.0) * 6.0;
float f62 = max(0.01 - pow(length(uvx - 0.325 * pos), 1.6), 0.0) * 3.0;
float f63 = max(0.01 - pow(length(uvx - 0.35 * pos), 1.6), 0.0) * 5.0;
vec3 c = vec3(0.0);
c.r += f2 + f4 + f5 + f6;
c.g += f22 + f42 + f52 + f62;
c.b += f23 + f43 + f53 + f63;
c = c * 1.3 - vec3(length(uvd) * 0.05);
return c;
}
vec3 cc(vec3 color, float factor,float factor2)
{
float w = color.x + color.y + color.z;
return mix(color, vec3(w) * factor, w * factor2);
}
void fragment()
{
vec2 texResolution = 1.0 / TEXTURE_PIXEL_SIZE;
vec2 resolution = 1.0 / SCREEN_PIXEL_SIZE;
vec2 uv = FRAGCOORD.xy / resolution.xy - 0.5;
uv.x *= resolution.x/resolution.y; //fix aspect ratio
vec2 sun = (sun_position.xy / resolution.xy) - vec2(0.5, 0.5);
sun.x *= resolution.x / resolution.y; //fix aspect ratio
vec3 flare_color = tint * lens_flare(uv, sun.xy, texResolution);
COLOR = vec4(flare_color, 1.0);
}

Thanks! Copying over my comment on the original shader since it’s also relevant here:
Instead of doing a ray-cast on the CPU to check whether anything is covering the sun, you can use the depth buffer for this directly in the shader. That way, you’re not dependent on 3D collision, but directly use what’s visible (which should be much more accurate).
To do this, sample the depth texture at UV coordinates derived from sun_position and, if the value is greater than 0.0, don’t render a lens flare (use the existing screen texture color or, if you’re using
blend_add, don’t add anything).Note that to access the depth buffer, you need to turn this into a 3D post-processing effect rather than a canvas item. It’s very easy to change this, just follow the docs for setting it up and you can re-use the shader almost 1:1: https://docs.godotengine.org/en/stable/tutorials/shaders/advanced_postprocessing.html