ChunkLoader Module To Improve Game Render Performance

:open_book: Download | :file_folder: GitHub | :globe_with_meridians: Place


“ChunkLoader, a way to add more detail to your games
without sacrificing performance.”

:information_source: About

ChunkLoader is a efficient performance optimization system for all roblox games. Effectively increasing performance across all games no matter the genre, build or age.


:placard: Description

Performant:

ChunkLoader was originally made as a way to reduce lag within my games, but
it worked so well I decided to share it to the public!

Useful features:

ChunkLoader is only a single module, which means you can customize
anything without looking through a dozen dependencies.

Easily Integrated:

ChunkLoader has customizable render distance, customizable render origin,
and customizable filter list for items you dont want to unrender.

It also doesnt interfere with anything server-side, so it’s only purpose is really to just improve performance, making it easily integrated into already existing games.


:information_source: How does it work?

The way ChunkLoader works is by using asset pooling, i.e all the chunks are prefilled at the start, and while the game runs the module decides what chunks to render or unrender depending on the distance from the render origin, and if it is in the dontUnrender list or not.


:placard: How To Import

Simply paste the code below into a ModuleScript and require it inside of a LocalScript within StarterPlayerScripts to use the module

local ReplicatedStorage = game:GetService "ReplicatedStorage"
local RunService = game:GetService "RunService"
local Players = game:GetService "Players"

local player = Players.LocalPlayer

local Constructor = {}

local Renderer = {}

local chunkLoaders = {}

type RBXScriptSignal = NetSignal.RBXScriptSignal

-- Creates a new Renderer object for this client.
function Constructor.new(name: string, renderDistance: number, chunkSize: number, renderOrigin: BasePart|Camera, dontUnrender: {Instance})
	local self = setmetatable({}, {
		__index = Renderer
	})
	self.renderDistance = renderDistance or 4
	self.chunkSize = chunkSize or 64

	self.chunks = {}
	self.bound = {}
	
	self.filteredTags = {}
	self.dontUnrender = dontUnrender or {}
	
	self.renderOrigin = renderOrigin or workspace.CurrentCamera
	
	chunkLoaders[name] = self
	
	return self
end

-- Gets a created Renderer from name
function Constructor.GetLoader(name: string): typeof(Constructor.new())
	return chunkLoaders[name]
end

-- Returns true if position is within renderDistance
function Renderer:IsPositionWithinRenderDistance(position: Vector3)
	local distance = player:DistanceFromCharacter(position)
	
	return distance < self.renderDistance*self.chunkSize
end

-- Add more instances to the dontUnrender list
function Renderer:DontUnrender(instances: Instance | {Instance})
	if typeof(instances) ~= "table" then
		table.insert(self.dontUnrender, instances)
	else
		for _, inst in instances do
			table.insert(self.dontUnrender, inst)
		end
	end
end

-- Sets a new renderOrigin
function Renderer:SetRenderOrigin(newOrigin: BasePart|Camera)
	self.renderOrigin = newOrigin
end

-- Filters anything with the specified tag from being unrendered
function Renderer:FilterTag(tag: string, value)
	self.filteredTags[tag] = value
end

-- Binds a function to either the Unrendered or the Rendered event
function Renderer:BindFunction(event: "Unrendered"|"Rendered", func: any)
	self.bound[event] = self.bound[event] or {}
	self.bound[event][func] = true
end

-- Gets the position of a chunk using the specified position
function Renderer:GetChunkPosition(position: Vector3)
	local x = math.floor(position.X / self.chunkSize)
	local z = math.floor(position.Z / self.chunkSize)

	-- Calculate the corner position
	local cornerX = x * self.chunkSize
	local cornerZ = z * self.chunkSize

	return Vector2.new(cornerX, cornerZ)
end

local function FireFunctions(self, name: string, ...)
	for func, _ in self.bound[name] do
		func(...)
	end
end

local function Activate(self, chunkInfo)
	if not chunkInfo then return end
	for part: BasePart, _ in chunkInfo.parts do
		chunkInfo.status = 1
		part.LocalTransparencyModifier = 0
		FireFunctions(self, "Rendered", part)
	end
end

local function Deactivate(self)
	for x, zTable in self.chunks do
		for z, chunkInfo in zTable do
			chunkInfo.status = 0
		end
	end
end

local function hasFilteredTags(self, tags)
	for _, tag in tags do
		if self.filteredTags[tag] then return true end
	end
	return false
end

local function Reload(self)
	for x, zTable in self.chunks do
		for z, chunkInfo in zTable do
			if chunkInfo.status ~= 0 then continue end
			for part: BasePart, _ in chunkInfo.parts do
				if table.find(self.dontUnrender, part) or hasFilteredTags(self, part:GetTags()) then continue end
				part.LocalTransparencyModifier = 1
				FireFunctions(self, "Unrendered", part)
			end
		end
	end
end

-- Updates all chunks
function Renderer:UpdateChunks()
	Deactivate(self)
	local originChunk = self:GetChunkPosition(self.renderOrigin.CFrame.Position)
	local renderDistance = self.renderDistance
	
	for dz = -renderDistance, renderDistance do
		for dx = -renderDistance, renderDistance do
			local x = originChunk.X + (dx * self.chunkSize)
			local z = originChunk.Y + (dz * self.chunkSize)
			local chunkInfo = self.chunks[x][z]
			
			Activate(self, chunkInfo)
			
			RunService.RenderStepped:Wait()
		end
		RunService.RenderStepped:Wait()
	end
	Reload(self)
end

-- Fills cache to be rendered or unrendered, can be called multiple times
function Renderer:FillCache(radius: number)
	local originChunk = self:GetChunkPosition(self.renderOrigin.CFrame.Position)
	local chunkHeight = 2000

	local overlapParams = OverlapParams.new()
	overlapParams.FilterType = Enum.RaycastFilterType.Exclude
	overlapParams:AddToFilter(self.dontUnrender)
	
	local playerCharacterFilter = {}

	-- Gather all player characters once and add to the filter
	for _, player in pairs(Players:GetPlayers()) do
		if player.Character then
			table.insert(playerCharacterFilter, player.Character:GetDescendants())
		end
	end
	overlapParams:AddToFilter(playerCharacterFilter)

	local size = Vector3.new(self.chunkSize, chunkHeight, self.chunkSize)
	
	local function setValues(x, z)
		self.chunks[x] = self.chunks[x] or {}
		self.chunks[x][z] = self.chunks[x][z] or {}
		self.chunks[x][z].status = self.chunks[x][z].status or 0
		self.chunks[x][z].parts = self.chunks[x][z].parts or {}
	end

	for dz = -radius, radius do
		for dx = -radius, radius do
			local x = originChunk.X + (dx * self.chunkSize)
			local z = originChunk.Y + (dz * self.chunkSize)
			
			setValues(x, z)
			
			local cframe = CFrame.new(x + self.chunkSize, 0, z + self.chunkSize)
			
			local query = workspace:GetPartBoundsInBox(cframe, size, overlapParams)
			if #query < 2 then continue end
			
			for _, part: BasePart in query do
				if self.chunks[x][z].parts[part] then continue end
				
				self.chunks[x][z].parts[part] = true
			end
			
			RunService.RenderStepped:Wait()
		end
		RunService.RenderStepped:Wait()  -- Extra wait after completing each row
	end
end

-- Autoupdates chunks every renderstep
function Renderer:BindToRenderStepped()
	local doing = false
	self.loop = RunService.RenderStepped:Connect(function(dt)
		local fps = 1/dt
		if doing then return end
		doing = true
		self:UpdateChunks()
		task.wait(10*(2-(fps/100)))
		doing = false
	end)
end

-- Autoadds any instances to the cache
function Renderer:CacheAddedDescendants()
	game.DescendantAdded:Connect(function(descendant: BasePart|Model)
		
	end)
end

-- Ends the Renderer process
function Renderer:EndProcess()
	self:UnbindFromRenderStepped()
	
end

-- Unbind
function Renderer:UnbindFromRenderStepped()
	self.loop:Disconnect()
end

return Constructor

:question: Still confused?

The :FillCache() function fills the asset pool with chunks of a specified amount
around a specific Vector3 position. This means that anything outside of your :FillCache() range will not be considered by the ChunkLoader.


Hope this helps!

(Side note: If you want the chunk loader to keep working when the player resets, just set the ChunkLoader.renderOrigin to a new renderOrigin. Doesnt apply if you use a Camera object as the renderOrigin)

(Credits to SaintImmor for the improved post!)

31 Likes

Also, if you can, please post a reply or two if you like the post. It increases engagement and makes the post more popular :slight_smile:

1 Like

Pretty interesting I’ll have to try it out!

How does this compare to StreamingEnabled? Or is it completely different?

1 Like

Honestly, I haven’t benchmarked it to StreamingEnabled, would be worth a shot. What I do know though is that rendering with this module is alot more customizable, and maybe combining it with good use of StreamingEnabled could get you even better performance.

1 Like

If you could benchmark it though, that’d be greatly appreciated because I am currently outside.

image
image
image

just wanted to let you know that there’s a syntax error and an issue with the variable. By the way, can you elaborate on how to use it?

Oh yes my bad, the unrendered variable definition should be ReplicatedStorage:FindFirstChild(“Unrendered”) instead of .Unrendered. Also, just define Players as game:GetService(“Players”).

An example on how to use it is within the sample code I sent in the main post. Will be fixing the variable issues in the main code

Issues fixed, should work now

±–==-_+__

Can I redesign this post for you? It’s quite unwelcoming.

Of course it would be free of charge, this is such a good resource that I want to make sure it’s more popular.

1 Like

Sure! I dont really know how to design posts, so itd be very appreciated. If you want you can just send the redesigned post in my PMs and ill post a new post with the new design

Ive been searching around, and I think I will add some more features to the chunk loader (such as part rendering occlusion, raycasts being handled in a seperate thread using ParallelScheduler.)

I will push the features to the main posts if I get them to work well, however for now theyre just theoretical updates :slight_smile:

Im @AccursedHeaven. You need to make a roblox place, rbxm and github repository.

Thats all I need from you, ill do the rest

I already have a github repository for this, let me just get the rbx place and rbxm

Github Repo: GitHub - silldraanm/ChunkLoader: A simple Roblox ChunkLoader module to improve render performance
rbxm:
ChunkLoader.lua (6.4 KB)
place:
Place1.rbxl (238.9 KB)

1 Like

Thank you, give me around 20 minutes to get it to you

2 Likes

Sorry for the delay, fell asleep

I’ll have it done in around 2-3 hours.

Its alright, I was playing games anyway

dude im sorry to keep delaying but urgent things keep coming up. Ill get it to you soon though :sweat_smile:

Working on it right now

Sorry the HUGE delays, I was very busy for the past couple of days

1 Like