How to optimize terrain loading?

My terrain generation is chunk-based and uses the Terrain:WriteVoxel method. It is used for generating a map for my game.

The problem is that it loads rather slowly. It takes about 1 minute to generate the map. This makes it very annoying to debug.

I thought of maybe multi-threading the chunk generation, but I have not tried that approach.

Let me know if you need more info.

I have a open source plugin Infinite Terrain Plugin

That might help you load the terrain faster if you look at how I have done the localscript

Also if you want some help on how to do multi thread you can check out this video

I’ll soon be updating my plugin to also use multiple threads

2 Likes

The dream of parallel terrain generation seems to be going up in smoke…

Function Terrain.WriteVoxels is not safe to call in parallel

You would have to generate the material and occupancy list in multiple threads then call Terrain.WriteVoxels in the synchronized part

-- main script

-- get the chunk loading actor
local actors = {game.ReplicatedFirst.Actor}

-- make 5 more clones
for i = 1, 5 do
	local actor = actors[1]:Clone()
	actor.Parent = actors[1].Parent
	table.insert(actors, actor)
end

-- fire a BindableEvent to each actor telling it what chunk it must load
game:GetService("RunService").RenderStepped:Connect(function(deltaTime)
	for i, actor in ipairs(actors) do
		actor.Event:Fire(math.random(-50, 50), math.random(-50, 50))
	end
end)
-- actor script

local chunkSize = 16

script.Parent.Event.Event:ConnectParallel(function(chunkX, chunkZ)
	local startX = chunkX * chunkSize
	local startZ = chunkZ * chunkSize
	local endX = startX + chunkSize - 1
	local endZ = startZ + chunkSize - 1
	
	local region = Region3.new(Vector3.new(startX * 4, -8 * 4, startZ * 4), Vector3.new(endX * 4 + 4, 0, endZ * 4 + 4))
	local materials, occupancys = game.Workspace.Terrain:ReadVoxels(region, 4)
	
	for x = 1, materials.Size.X do
		for y = 1, materials.Size.Y do
			for z = 1, materials.Size.Z do
				if y > 4 then continue end
				materials[x][y][z] = Enum.Material.Grass
				occupancys[x][y][z] = 1
			end
		end
	end
	
	task.synchronize()
	
	workspace.Terrain:WriteVoxels(region, 4, materials, occupancys)
end)

this is what the code above looks like


as you can see the synchronized workspace.Terrain:WriteVoxels part is supper fast and nothing you need to worry about

2 Likes

I tried it, but it did not really result in a significant improvement in performance. My terrain generation went from 70 to 68 seconds. Maybe I did something wrong? The code is in TypeScript, but I think you get the idea.

task.desynchronize();

const materials: TerrainMaterials = Chunk.create3DArray(Enum.Material.Air);
const occupancy: TerrainOccupancy = Chunk.create3DArray(0);

processVoxels(materials, occupancy, this.x, this.y);
task.synchronize();
Chunk.writeVoxels(this.getRegion(), materials, occupancy);

I’m seeing a large boost in performance here is a demo project

multi thread terrain.rbxl (33.5 KB)

and this is the code in the demo project

if game:IsLoaded() == false then game.Loaded:Wait() end

local chunkSize = 16
local actorAmount = 8 -- change this to have more actors more actors will allow more chunks to load per frame
local selectedActor = nil
local positionX = math.huge
local positionZ = math.huge
local actors = {game.ReplicatedFirst.Actor}
local loaded = {}

for i = 2, actorAmount do
	local actor = actors[1]:Clone()
	actor.Parent = actors[1].Parent
	table.insert(actors, actor)
end

local function LoadChunk(chunkX, chunkZ)
	if loaded[chunkX] == nil then loaded[chunkX] = {} end
	if loaded[chunkX][chunkZ] ~= nil then return end
	loaded[chunkX][chunkZ] = true
	actors[selectedActor].Event:Fire(chunkX, chunkZ)
	selectedActor += 1
	if selectedActor > actorAmount then task.wait() selectedActor = 1 end
end

while true do
	task.wait()
	local chunkX = math.floor(workspace.CurrentCamera.Focus.Position.X / 4 / chunkSize)
	local chunkZ = math.floor(workspace.CurrentCamera.Focus.Position.Z / 4 / chunkSize)
	if positionX == chunkX and positionZ == chunkZ then continue end
	positionX, positionZ, selectedActor = chunkX, chunkZ, 1
	local directionX, directionZ, count, length = 1, 0, 0, 1
	LoadChunk(chunkX, chunkZ)
	for i = 2, 900 do -- this loop can be optimized
		chunkX += directionX chunkZ += directionZ count += 1
		if count == length then count = 0 directionX, directionZ = -directionZ, directionX if directionZ == 0 then length += 1 end end
		LoadChunk(chunkX, chunkZ)
	end
end
-- this is not optimized just some demo code

local chunkSize = 16

script.Parent.Event.Event:ConnectParallel(function(chunkX, chunkZ)
	local startX = chunkX * chunkSize
	local startZ = chunkZ * chunkSize
	local endX = startX + chunkSize - 1
	local endZ = startZ + chunkSize - 1
	
	local region = Region3.new(Vector3.new(startX * 4, -16 * 4, startZ * 4), Vector3.new(endX * 4 + 4, 0, endZ * 4 + 4))
	local materials, occupancys = game.Workspace.Terrain:ReadVoxels(region, 4)
	for x = 1, materials.Size.X do
		for z = 1, materials.Size.Z do
			local height = 8 + math.noise((startX + x) * 0.04, 0, (startZ + z) * 0.04) * 6
			for y = 1, materials.Size.Y do
				if y > height then continue end
				materials[x][y][z] = Enum.Material.Grass
				occupancys[x][y][z] = 1
			end
		end
	end
	
	task.synchronize()
	
	game.Workspace.Terrain:WriteVoxels(region, 4, materials, occupancys)
end)

you can change the actorAmount to adjust how many chunks to load per frame
the default value of 8 means it will load a max of 8 16 by 16 chunks per a single frame its safe to try larger values if you like for instance if you set actorAmount to 32 that will make it load a total of 32 16 by 16 chunks per a single frame

for me even just having 2 actors is fast enough

with 6 actors I can load 30 16 by 16 chunks in just 5 frames and leaving plenty of CPU for remaining game logic before it would of been imposable to load 6 16x16 chunks of terrain in a single frame without frame spikes

1 Like