Brainstorm designing an occlusion culling system?

So not so long ago I learned that Roblox may or may not do occlusion culling.

For context:

Three-types-of-visibility-culling-techniques-1-View-Frustum-Culling-2-back-face

Here is occlusion culling with an image.
It’s basically just discarding objects that are hidden behind other objects and not rendering them.

Roblox does frustum and backface culling which are pretty much basic in nearly every modern game engine.
Roblox as far as I know however, does not cull hidden objects.

In most cases this is not an issue, Roblox has been doing fine for years without occlusion culling and it also has potential draw backs such as the added overhead for having to check visible objects every frame.

The big but,

for a game project I might want to design and play around with an experimental Lua implementation of a culling algorithm to see how much it affects performance and perhaps my project will run a lot better on old/low-end computers.

My current idea so far:

My current idea is to give models a smaller “visibility” hitbox and put them in chunks so I don’t have to cycle through the entire workspace.

Perform a simple raycast and it it does not reach the camera without intersecting it’s marked as “hidden” and I can make it invisible to improve performance.

Now… making the object SHOW UP again is a different challenge, because for that I would technically have to raycast every single frame for every single object.

So while graphical performance would increase, CPU performance would decrease for every hidden object that gets added to the list of hidden objects.

That’s not good.

My method for making objects appear again was to have an inflated volume/hitbox that the player’s camera has to look and raycast into for it to appear again.
But I wonder if that’s truly the most convenient way to do it, I feel like it might not.

Game engines actually implement a lot of these culling optimizations with buffers, textures and whatnot but Roblox ain’t some engine where you can do low-level programming so I have to think of a simpler, more naive approach that makes use of higher-level functions like the physics engine and raycasts or region checks.

It might actually not be worth it to implement a custom culling system but I honestly want to give it a shot.
It might actually prove to be very useful for games that are open-world with massive mountains or indoors and have many rooms full of objects and whatnot.

All your feedback, comments and thoughts will be greatly appreciated!

11 Likes

They are probably going to introduce culling, but if you want to play around with it, go ahead and do so. The only drawback is that Lua is much slower than C++, so it would be a lot slower.

In addition, I would like to see how you will handle it, so I will be watching this topic frequently.

5 Likes

After reading this post from 2014 I kind of doubt they’ll ever really consider adding it.

Besides that, I think I do have an idea for how this could work, at least for simple shapes like boxes and spheres.

1 Like

Last time I checked, Lua has gotten quite fast.
It’s not as fast as C++, not by a long shot.

But Lua on JIT can be almost as fast as C++, which is really impressive.
Roblox’ current Luau VM despite not using JIT at the moment, still gets pretty comparable performance to Lua on JIT, it’s fast.

If I had to guess/estimate, it’s somewhere between 50 - 60-ish % the speed of C++?
A good algorithm should perform well.

Also @BurningEuphoria.

Roblox has been using the “because mobile doesn’t support it” excuse for ages for not implementing features.

Roblox has no reason not to implement it for PC and console, which DO support it.
I don’t even make mobile games so I quite frankly do not care either if mobile doesn’t support something.

BUT before I go off-topic.
Please do share what implementations you have in mind! :slight_smile:

3 Likes

Why not detect if it’s visible on the camera from it’s corners, < probably fast?
and then if visible raycast all part’s corners from the camera? < needs optimising

  • We can’t check from the center because else users would report ‘parts dissapearing randomly from certain angles’
2 Likes

I realized this might happen actually.
I think I will mostly use occlusion culling for rooms or things like trees and buildings hidden behind big mountains but still inside the render distance.

So here’s 2 potential problems with occlusion culling.

The 2nd problem miiight have a solution that works maybe more than half the time?
I’ve considered only counting the object as “occluded” if the raycast for instance, only hits the wall on one of it’s longest sides at an angle that is more than 45 degrees for instance.

Not perfect but might solve some edge cases like this.

2 Likes

I don’t know how the script would be for the 2nd solution, but yeah, it would also work (sometimes).

1 Like

Scripting no one need to worry about, I can script about anything on Roblox.

The real challenge here though is finding the right algorithm / method to implement and preferably one that’s not computationally heavy.
I think I’ll most likely reserve occlusion culling for indoor areas and large objects outdoors though.

I might know a way to implement occlusion culling for procedurally generated rooms and dungeons but that would unfortunately limit the ways that I could use it.

2 Likes

True, my idea was using multiple raycasts from part corners (not middle), yours used only one raycast but its less precise since if you overthink it, you find out second solution doesn’t work.

1 Like

Every method has it’s drawbacks, the corner method too unfortunately.
If the corners were hidden and part of the middle as seen in the image is visible, the game would still think it’s occluded.

Now technically I COULD solve this issue with even more raycasts but at that point the performance impact on the CPU starts to outweigh the performance gains on the GPU.

2 Likes

I understood your problem, probably this might need a special type of casting that is like shapecasting but detects where it doesnt hit instead of where it hits. That’d be really useful acually, since it doesnt need infinite amount of raycasts.

1 Like

I personally think that this is impossible to do feasibly, but you might want to take a look at Camera | Documentation - Roblox Creator Hub although this is bound to the same issues you listed.

1 Like

The problem you are going to have with this approach is that you are brute forcing your way into seeing if an object is visible. It’d be more efficient to instead project each corner /vertex of your model to the camera. Roblox already does this and they are able to do it really fast as they go and do the projections on each instance in parallel on the GPU. You can’t do that and hence any effort to try and make it efficient will be sacrificed by your loss in performance. Here’s the thing, when it comes to optimizing games usually the CPU/GPU isn’t the bottleneck, but rather what it is is when new resources are introduced into the scene. The introduction of new information/memory slows down the computer program. Given that, a way to optimize that is to simply reduce the number of parts you have. You could do that by having an occlusion culling system as you said, but honestly if it’s just one instance at a time it wouldn’t be worth it.

By the way, there’s a reason why roblox doesn’t just occlude those objects when rendering, it’s cause if they didn’t do that they’d have to use a much more computationally expensive way to render stuff. It’s just a principle with rasterizers that you need to render each polygon at least as a collection of lines, see if they get occluded, and then “render” then in the sense that we think of rendering as plotting pixels on the screen.

EDIT: if you want to just have x amount of trees in the scene, you could create an algorithm that plots them on the screen and then if a tree is not visible by the camera, it repositions that tree to a new acceptable area defined by the area that’s visible on the screen. Though, at that point it’s probably better to take a different approach like a level of detail system where if you can see more than 50 trees, then the trees will get clumped into simplified instances so it gives the illusion of having a lot more trees

1 Like

So the thing here is, the point of occlusion culling here is that I could make more complex scenes without having to reduce the detail.

Removing parts or using less models is generally the more “simple” and “naive” way of optimizing a game.
But, as you probably would have guessed, this sacrifices details and you essentially limit yourself with the amount of objects you could place down.

The whole point of occlusion culling here is so I can have MORE objects and detail.
I want to push the engine to the limits by having the absolute highest amount of objects as possible.

There is one project of mine where I want the player to be able to completely wreck up a room, defeat the enemies etc.
And I want ragdolls, bullet holes, destroyed objects and debris, etc to stay around forever until the level is unloaded so removing them is no option.

The issue with Roblox currently is though that if you’re inside a huge building for instance, EVERY ROOM within the frustum of the camera will get rendered regardless of whether it’s visible or not.

Every piece of furniture, all the meshes and parts it’s made out of.
Any NPCs, interactable objects, the walls themselves, the doors, the lights and shadows.

EVERYTHING gets rendered regardless of visibility.

Most developers here on the platform would just tell you “Just reduce the amount of parts and models you use, simple”.
Which yes, that does work, but it’s also very generic advice that almost anyone should know at this point.

But as simple as that sounds, removing objects from the scene and reducing complexity is not always what you want.
Sometimes complexity is intentional game design and it might be there for a reason.

In such cases I do not think simply reducing scene complexity is the go-to solution.
It would not achieve the results that I’d want for sure.

What if I WANT to render thousands of trees? What if I wanted a entire city with thousands of large buildings full of windows and whatnot and looooots of cars driving around?

So I have to come around with a work around such as implementing my own Lua software occlusion culling system.

Now, fortunately, I’m a little familiar with multi-threading.
So even IF the algorithm ends up being a little expensive, most computers (and phones) have about 4 cores, some 6 or 8.
And I think that’s enough for multi-threaded game logic.

2 Likes
-- Services
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
-- Constants
local MIN_RENDER_DISTANCE: number = 128 -- Just loading objects near the camera, so shadows don't look weird
local MAX_RENDER_DISTANCE: number = 2048 -- The maximum distance you'll see objects before they never render

local THREAD_CHUNKS: number = 8 -- How many threads should handle the objects

local THREADS_PER_DELAY: number = math.floor(THREAD_CHUNKS / 4) -- How many threads to spawn before waiting
local THREAD_DELAY: number = 1 / 120 -- The delay between spawning threads
-- Variables
local Unloaded_Directory = ReplicatedStorage:WaitForChild("Unloadedobjects")
local Loaded_Directory = workspace:WaitForChild("objects")

local objects: { Model } = Loaded_Directory:GetChildren()

local OBJ_Count: number = #objects
local Chunk_Count: number = math.floor(OBJ_Count / THREAD_CHUNKS) -- How many objects one thread will handle

local Rendering: boolean = false
-- Main
RunService:BindToRenderStep("TREE_RENDERING", Enum.RenderPriority.Camera.Value + 1, function()
	if not Rendering then
		local Camera: Camera? = workspace.CurrentCamera
		if Camera then
			Rendering = true
			-- Where the player's camera is
			local Viewpoint: Vector3 = Camera.CFrame.Position
			-- Spawn all 8 threads
			for Thread: number = 1, THREAD_CHUNKS, 1 do
				-- The tree index that the thread will be starting at
				local Start: number = ((Thread - 1) * Chunk_Count) + 1
				-- And, where it should end
				-- If this is the last chunk, just iterate through the rest of the tree table
				-- Otherwise, just handle the default chunk amount
				local End: number = if Thread >= THREAD_CHUNKS then OBJ_Count else Thread * Chunk_Count
				task.spawn(function()
					-- Forgive me if anything here is off; I'm a bit rusty at parallel Luau
					task.synchronize()
					for Index: number = Start, End, 1 do
						local OBJ: Model = objects[Index]
						local OBJ_Origin: Vector3 = OBJ:GetPivot().Position
						-- First, compare distances to see if it can be loaded without the WorldToViewportPoint calculation
						local Distance: number = (OBJ_Origin - Viewpoint).Magnitude
						if Distance > MAX_RENDER_DISTANCE then
							OBJ.Parent = Unloaded_Directory
							continue
						elseif Distance <= MIN_RENDER_DISTANCE then
							OBJ.Parent = Loaded_Directory
							continue
						end
						-- Then, check to see if it's in the point
						local Position: Vector2, Visible: boolean = Camera:WorldToViewportPoint(OBJ_Origin)
						OBJ.Parent = if Visible then Loaded_Directory else Unloaded_Directory
					end
				end)
				-- If this isn't the last thread, yield before spawning more
				if ((Thread % THREADS_PER_DELAY) == 0) and (Thread < THREAD_CHUNKS) then
					task.wait(THREAD_DELAY)
				end
			end
			Rendering = false
		end
	end
end)

I had this script from an old game, it might work fine for you…

A local script parented to an Actor, and two folders, one with your current objects in the workspace called objects and the other in the ReplicatedStorage called Unloadedobjects

Try it out and lmk if it works.

1 Like

Oh this is cool! I’ll definitely look into this later!

I heard Roblox might implement occlusion culling eventually but in case some devices do not support it I could use this as a fallback method where necessary.

3 Likes

Don’t expect a lot of goodies from ROBLOX mate…

1 Like

If most of your environment takes place indoors, you could take a page out of the Source engine. Split your environments into multiple rooms, and only load them in when a player can see anything in that room.

That said, this is a really bad solution for any open world games…

I realise this is basically what everyone has been saying but on a less precise level. Oh well.

Try to find out how he does it here:

https://twitter.com/MrChickenRocket/status/1504449539071410182

https://twitter.com/MrChickenRocket/status/1665097567133405185

2 Likes

This will most likely be used for procedurally generated dungeons and enormous indoor areas.
I’m not even sure if I could make this work properly for open-world-esque situations.

Though, I suppose if an object is blocked by a mountain or some other large structure then it might be a little easier to determine whether it should be culled or not.

But I hope Roblox comes with occlusion culling so I don’t have to write my own for open-world situations.

For indoor areas and dungeon crawlers it’s definitely useful and also a lot easier and less resource intensive to implement.

Having extremely large and complex indoor areas, especially with lots of furniture and whatnot can cause a LOT of unnecessary computations so a simple culling algorithm is gonna make those type of games a lot more playable on old devices.

2 Likes