Sunflares - Screenspace particle rendering and sun glow effect

Ever wanted to make 90’s retro style sun-flares for your game? This effect was popular in games like Zelda ocarina of time, and basically every early 3D-accelerated game. It’s cheap and can easily be customized.

This code does a couple of noteworthy tricks:

  1. It does its rendering by emitting particles that only last for a single frame. This is done by calling :Clear() and then :Emit(1) on a disabled emitter each render stepped. This is also very useful if you want to put stable glows on things…

  2. The particles are drawn “on top of everything”. The emitters are set to draw at the screen near plane depth every frame; basically right up against the camera. This is achieved by using a Zoffset that is the same as the transformed screen depth. This can be used for making particles that render over everything else in the world, great for highlighting objects or doing map markers etc.

Update: Added an animated geometry occlusion test.

Video:

Uncopylocked place:

The sourcecode:

local sunDistance = 500
local objectDistance = 10
local brightScale = 0.15
local rim = 0.1 --fraction of screen near border to fade out at
local occludeLevel = 0
local occludeSpeed = 10

local list = {
	script.SunFlare,
	script.Bit0,
	script.Bit1,
	script.Bit2,
	script.Bit3,
	script.Bit4,
	script.Bit5,
}

local brightness = 1
local rotation =0

game["Run Service"].RenderStepped:Connect(function(dt)
	
	--Calculate the sun position
	local sunPosition = workspace.CurrentCamera.CFrame.Position + game.Lighting:GetSunDirection() * sunDistance
	
	local screenSpacePosition,onScreen = workspace.CurrentCamera:WorldToViewportPoint(sunPosition)
	
	--Make a rod to place the other parts along
	local vector = (Vector2.new(workspace.CurrentCamera.ViewportSize.x/2,workspace.CurrentCamera.ViewportSize.Y/2) - Vector2.new(screenSpacePosition.x, screenSpacePosition.y))
	
	local dist = vector.Magnitude * 3 --length of the "rod"
	local dir = vector.Unit
	
	rotation = math.deg(math.atan2(-vector.y, vector.x)) + 180
	
	local step = dist / #list
	
	for j = 1, #list do	
		
		local dx = j - 1
		local ray = workspace.CurrentCamera:ViewportPointToRay(screenSpacePosition.x + dir.x * dx * step, screenSpacePosition.y + dir.y * dx * step, 1)	

		list[j].Position = ray.Origin + ray.Direction.Unit * objectDistance
	end
	
	--calculate the falloff
	local normalizedPos = Vector2.new(screenSpacePosition.X / game.Workspace.CurrentCamera.ViewportSize.X,screenSpacePosition.Y / game.Workspace.CurrentCamera.ViewportSize.Y)
	
	brightness = 1
	
	--Not visible
	if (onScreen == false) then
		brightness = 0
	end
	
	if (brightness > 0) then

		--left
		if (normalizedPos.X < rim) then
			local dx = math.clamp(1-(rim - normalizedPos.X) / rim,0,1)
			brightness = math.min(brightness,dx)
		end
		--right
		if (normalizedPos.X > 1-rim) then
			local dx = math.clamp(1-( normalizedPos.X-(1-rim)) / rim,0,1)
			brightness = math.min(brightness,dx)
		end
		
		--top
		if (normalizedPos.Y < rim) then
			local dx = math.clamp(1-(rim - normalizedPos.Y) / rim,0,1)
			brightness = math.min(brightness,dx)
		end
		--bottom
		if (normalizedPos.Y > 1-rim) then
			local dx = math.clamp(1-( normalizedPos.Y-(1-rim)) / rim,0,1)
			brightness = math.min(brightness,dx)
	 		
		end
	end

		
	--Do ray occlusion - just one + some animation seems fine
	if (brightness > 0) then
				
		local params = RaycastParams.new()
		params.RespectCanCollide = true
		local vec = (sunPosition - game.Workspace.CurrentCamera.CFrame.Position).Unit
		local res = game.Workspace:Raycast(game.Workspace.CurrentCamera.CFrame.Position, vec * sunDistance, params)
		
		
		if (res) then
			occludeLevel -= dt * occludeSpeed
		else
			occludeLevel += dt * occludeSpeed
		end
		
		occludeLevel = math.clamp(occludeLevel,0,1)
 		brightness = math.min(occludeLevel, brightness)
	end
	
	
end)


game["Run Service"]:BindToRenderStep("PreCamera",5000,function()
	
	for key,value in list do
		value.Parent = game.Workspace
		for _,part in value:GetDescendants() do
			if (part:IsA("ParticleEmitter")) then
				
				part.Brightness = brightness * brightScale
				part.Rotation = NumberRange.new(rotation)				
				
				--Super hackery, we can abuse the zoffset to make these appear flush with the camera in Z space
				local screenSpacePosition,onScreen = workspace.CurrentCamera:WorldToViewportPoint(part.Parent.WorldPosition)
				part.ZOffset = screenSpacePosition.Z-1
				
				--Emit a particle for just 1 frame
				part.Enabled = false
				part:Clear()
				part:Emit(1)
			end
		end
	end	
end)
66 Likes

uhh did you forget to add a github repository?? because you did it with chickynoid

also one issue, It can go through parts, please make it so it can’t go through parts

The source code is literally a single script though…

4 Likes

Updated so it won’t go through parts

2 Likes

i love this effect so much, great job on it

Looks a bit janky to me but I think this could become a better resource with some extra work put into it, good work though.

Is that Dusty Dune Galaxy I hear?

This looks awesome, I was waiting for someone to make something like this.

1 Like

Love this, gonna use this in the future for a ultra realistic game…