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 TextureRect node 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 new ShaderMaterial and assign it to this shader
  • Give the shader a noise texture (just the default NoiseTexture2D is 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);
}
Tags
3d, lens-flare, Sun
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 TranquilMarmot

Ray-traced sphere for particle effects

Related shaders

Screen Space Lens Flare – On Emissive Material

Lens Flare Shader

Screen Space Lens Flare with rainbow colored effect

guest

1 Comment
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Karl
2 months ago

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