Baked "Global Illumination"

Hello, I was (as it seems many others too) enthralled when I saw Elttob’s recent work with Global Illumination. I decided to take a crack at it. While I haven’t yet gotten it to the point he got it, I have gotten what I think to be a good spot for baking the effect. I have created a test place in which you can see the effect in action:

Note: Your performance may vary from mine. Also, leaving the game results in the client locking up (I’m unsure why)

My Specs:

CPU: Intel i7 9700
GPU: AMD Radeon RX 6600 XT

image

The best part is, it runs at 60 FPS:
image

I am using the same method which Elttob and many others have used, which is to create a grid of probes, and using Ray-Tracing, determine an approximate color for that point. While I am unsure of the limitations of other’s system, I know that the system which I have created can do a lot. In the test game, there are 57K+ probes.

How I did it

The major difference between my method and Elttob’s is that instead of using individual parts, I used a single part, and the probes in the grid consist of attachments.

Picture of the place in studio:

Within each attachment, I have a pointlight, and an Actor containing a Ray Tracing script.
image

The Ray Tracing script is taking advantage of Parallel Lua to more quickly and efficiently determine the color of each node. To prevent script-timeouts, I have the baking process happen over a series of steps with a buffer to store information.

Buffer Module
local RayBuffer = {}
local bufferFuncs = {}

function RayBuffer.new()
	local self = {}
	self.Color3s = {}
	
	setmetatable(self, {__index = bufferFuncs, __newindex = nil})
	return self
end

function bufferFuncs:GetAvgColor3()
	if self then
		local col = Color3.new()
		local b = 0
		for _,v in ipairs(self.Color3s) do
			local c = v.Color
			col = Color3.new(col.R + c.R, col.G + c.G, col.B + c.B)
			b += v.Brightness
		end
		local l = #self.Color3s
		return Color3.new(col.R / l, col.G / l, col.B / l), b / l
	end
end

function bufferFuncs:AddColor3(C:Color3, brightness:number, frame:number?)
	if self then
		local ColorVal = {}
		ColorVal.Color = C
		ColorVal.Brightness = brightness
		ColorVal.Frame = frame
		table.insert(self.Color3s, ColorVal)
	end
end

function bufferFuncs:ClearFrame(frame:number)
	if self then
		for i,v in ipairs(self.Color3s) do
			if v.Frame == frame then
				table.remove(self.Color3s, i)
			end
		end
	end
end

return RayBuffer
Ray Tracing Script
local a = script.Parent.Parent
local l = a:WaitForChild("PointLight")
local RS = game:GetService("RunService")
local WS = workspace
local LS = game:GetService("Lighting")
local R = Random.new()
local RB = require(game:GetService("ServerScriptService"):WaitForChild("RayBuffer"))

local rayCount = 8
local numRays = 0
local skyColor = Color3.new(1,1,1)
local sunDir = game:GetService("ServerScriptService"):WaitForChild("SunDirection")
local rayBuffer = RB.new()
local frame = -1
local bufferLength = 16
local updateTime = 0.5

local function canPointSeeSun(point:Vector3)
	local result = WS:Raycast(point, sunDir.Value * 500)
	if result then
		return false
	end
	return true
end

local function calculateFalloff(lightPower:number, dist)
	return lightPower / (1 + (dist))
end

local function updateNodeColor()
	frame = (frame + 1) % bufferLength
	rayBuffer:ClearFrame(frame) --Clearing the frame before adding anything to it
	task.desynchronize()
	for i = 0, rayCount, 1 do
		local rayResult = WS:Raycast(a.WorldPosition, R:NextUnitVector() * 500)
		if rayResult then
			if canPointSeeSun(rayResult.Position) then
				rayBuffer:AddColor3(rayResult.Instance.Color, calculateFalloff(10, rayResult.Distance), frame)
			else
			end
		end
	end
	task.synchronize()
	l.Color, l.Brightness = rayBuffer:GetAvgColor3()
	return
end

for i = 0,bufferLength,1 do
	updateNodeColor()
	task.wait(updateTime)
end

I would love for people to try out the place and let me know of any performance issues, or inaccuracies. I’ve never created a Ray Tracing script before, and I don’t think I calculated the probe colors exactly correctly, it does look good though.

21 Likes

The game is public now, my apologies for forgetting to set it to public before!

Some photos of a Happy Home in Robloxia:

3 Likes

dang this is actually way more performant than i expected

max graphics
16 gb ddr4 ram
core i5 11400h
rtx 3060 (desktop)

So I’ve got a few questions about your GI:

  • You mentioned that it was baked, though do you have any plans to make it real-time like Elttob’s?
  • Do you think you could increase probe density (not amount)?
  • Is the lighting baked in at runtime or is the information stored and then loaded in?
  • Are you taking into account the atmosphere/skybox as a light source?
  • Any idea as to why the dark mass behind the house is present?

Really great stuff man. Looks really good.

3 Likes

I’ve tried making it real-time, depending on the scene and the number of probes. In his video, Elttob mentioned 60 rays from each probe per frame, my system is no where near performant enough to do that for one simple reason: the buffer. When running in real time, and when baking, the framerate decreases at a linear rate as the number of color values in the buffer increases. This is because each time the probes color is updated, it is getting the average of all of the colors and brightness values. Even though the buffer only stores at most 16 color values, when that is expanded to 50K+ probes, its a lot of math operations.

When creating the grid of attachments, I have total control over the number of probes, and the distance between them. In the treehouse example, there are 12 studs between each attachment, and 48x48x48 attachments.

The lighting was calculated at runtime in another place. It took about 15 seconds of mostly 1 FPS to calculate. I then copied all of the attachments and just pasted them into a the map of the place shown above.

In its current state, the only rays which are actually used are the ones which hit a point which is also being directly hit by sunlight. If no RaycastResult is returned by the Raycast then it is ignored. If there is a result, but the point it hits isn’t in direct sunlight (calculated by shooting a ray in the direction of the sun and checking if there is a result; no means it is in direct sunlight) then it is also ignored.

The “dark mass behind the house” is its shadow. There is nothing for light to bounce on into the shadow to illuminate it, therefore it is dark.

3 Likes

Maybe you can give it an ambient value (like non-raytraced games do) so that it isn’t completely pitch-black?

1 Like

This is interesting because it’s a complex lighting script that I actually understand, idk if that’s a good thing or a bad thing. Maybe I’m just getting better at my job.

anyways It looks interesting I’d love to see it working with something with a higher poly count or more reflections so I’ll probably be trying it sometime soon.

I NEEDED THIS, I JUST FORGOR WHAT ITS CALLED

Good job, it looks rly nice

The Ambient value is exactly what this method is avoiding. If it is pitch black, that is how it physically would be (At least approximately). Adding an ambient lighting color would just defeat the point of doing any of this.

2 Likes

Then turn up the EnvironmentalDiffuseScale to simulate atmospheric scattering. indoor areas darken somewhat with that so it should work fine to some extent shouldn’t it?

1 Like

Looks great and runs great!

Are you thinking of making this open source some time? Would be really useful to test with
My bad, I didn’t see you showed the script!

This is a dumb question but where do i put the scripts so it works?