Perlin noise map generator is extemely slow

Howdy developers!

I am working on a procedurally generated game (kind of like minecraft), in which I use perlin noise to generate the chunks.

With a normal script, it takes ~1000ms to generate one chunk. I’ve tried using actors, but it seems they create more lag than they reduce (It takes 2 seconds per chunk, and lags the entire server).

This is the script:

function Terrain:GenerateChunk(chunkPosition, onFinished)
	local chunkNeighbors = {}

	for neightbor_x = -1, 1 do
		for neightbor_z = -1, 1 do
			for neightbor_y = -1, 1 do
				if neightbor_x ~= 0 or not (neightbor_x == neightbor_y and neightbor_x == neightbor_z and neightbor_z == neightbor_y) then
					local neighbor_position = Vector3.new(chunkPosition.X + neightbor_x, chunkPosition.Y + neightbor_y, chunkPosition.Z + neightbor_z)
					table.insert(chunkNeighbors, neighbor_position)
				end
			end
		end
	end

	local chunk = {
		position = chunkPosition,
		blocks = {},
		neighbors = chunkNeighbors,
	}

	local start_x = Terrain.chunk_size.X * chunkPosition.X
	local end_x = (Terrain.chunk_size.X) * (chunkPosition.X + 1)
	local start_z = Terrain.chunk_size.Z * chunkPosition.Z
	local end_z = (Terrain.chunk_size.Z ) * (chunkPosition.Z + 1)

	for x = start_x, end_x do		-- I suspect this is the reason for the lag
		for z = start_z, end_z do			
			if chunkPosition.Y == 0 then
				local max_y = math.floor((math.noise(x/self.scale, z/self.scale, self.seed) * self.amplitude) + 100)
				local max_block_y = chunkPosition.Y * Terrain.chunk_size.Y + max_y

				for y = chunkPosition.Y * Terrain.chunk_size.Y, max_block_y do
					local material = "unknown"

					if y < max_block_y - 5 then
						material = "stone"
					else
						material = "grass"
					end

					local blockPosition = Vector3.new(x, y, z)
					local block = {
						position = blockPosition,
						visible = false,
						rendered = false,
						render_part = nil,
						neighbors = {},
						material = material,
					}

					chunk.blocks[blockPosition] = block
				end

				for y = max_block_y, ((chunkPosition.Y + 1) * Terrain.chunk_size.Y) do					
					local blockPosition = Vector3.new(x, y, z)
					local block = {
						position = blockPosition,
						visible = false,
						rendered = false,
						render_part = nil,
						neighbors = {},
						material = "air",
					}


					chunk.blocks[blockPosition] = block
				end
			else

			end
		end

		if x % 5 == 0 then
			task.wait()
		end
	end

	for x = start_x, end_x do		
		for z = start_z, end_z do			
			for y = (chunkPosition.Y * Terrain.chunk_size.Y), ((chunkPosition.Y + 1) * Terrain.chunk_size.Y) do
				local block = chunk.blocks[Vector3.new(x, y, z)]

				if x == start_x or x == end_x or z == start_z or z == end_z then
					block.visible = true
				end

				for neightbor_x = -1, 1 do
					for neightbor_z = -1, 1 do
						for neightbor_y = -1, 1 do
							if neightbor_x ~= 0 or not (neightbor_x == neightbor_y and neightbor_x == neightbor_z and neightbor_z == neightbor_y) then
								local neighbor_position = Vector3.new(x + neightbor_x, y + neightbor_y, z + neightbor_z)
								table.insert(block.neighbors, neighbor_position)

								if block.material ~= "air" then
									local real_block = chunk.blocks[neighbor_position]
									if real_block and real_block.material == "air" then
										block.visible = true
									end
								end
							end
						end
					end
				end
			end
		end

		if x % 5 == 0 then
			task.wait()
		end
	end

	--task.synchronize()
	self.chunks[chunkPosition] = chunk
	onFinished()
end

Benchmark your code, try removing the task.waits first. if it’s timing out then reduce your terrain size.
Insert the benchmarking code between each set of loops to get a finer grain.

Here’s the sample for benchmarking with os.clock() docs

-- Record the initial time:
local startTime = os.clock()
-- Do something you want to measure the performance of:
local a, b = 0, 1
for i = 1, 5000000 do
    a, b = b, a
end
-- Measure amount of time this took:
local deltaTime = os.clock() - startTime
print("Elapsed time: " .. deltaTime)
-->  Elapsed time: 0.044425600033719 (actual number may vary)

I have a feeling it is because of your neighbors table, that grow quite large, 8x what ever size you pick. Try removing that all together and addressing neighbors by indecies.

2 Likes

Okay, it seems the issue is with checking the neighbours.

With checking the neighbors it takes 53 seconds, and without checking them it takes around 19 to generate 100 chunks. However neighbors are pretty important, since I use them to detect if a block should be visible, and to find the correct places to build the triangle map. I will try to make this faster, Ill respond if I get anything better done

Change this to:

x % 1024

decrease the 1024 if its lagging.

At 1024 the loop just errors (exhausted time), at 512 it takes 0.19 milliseconds per chunk, at 25 it takes 0.18 milliseconds per chunk, and at 10 it takes 0.192 milliseconds per chunk. It seems like 10 is the best value, with decent speeds and the best server CPU utilization.

I’ve also found out that the code slowing everything down is:

if block.material ~= "air" then
	local real_block = chunk.blocks[neighbor_position]
	if real_block and real_block.material == "air" then
		block.visible = true
	end
end

It looks like a simple if check, so I’m not sure what’s the problem.

This is a very fast process.

The performance might be hindered because you’re using a dictionary instead of an array.

I meant 0.19 seconds instead of 0.19 milliseconds, and thats on my machine, on roblox servers its 0.6 seconds, pretty unusable since I need to generate up to 6 chunks per second

If it’s really not the massive table inserting I’ve quoted above then I’d say try to change your chunk.blocks keys to numbers instead of Vectors. If your tables are indexed from 1 sequentially they are arrays under the hood, which can be read from faster than other keyed tables.

you could also insert a break into your neighbor loop so it stops checking after the block is visible.

local block = chunk.blocks[Vector3.new(x, y, z)]

if x == start_x or x == end_x or z == start_z or z == end_z then
	block.visible = true
	continue -- skips over all neighbor checks
end

local skip_neighbor = false
for neightbor_x = -1, 1 do
	if skip_neighbor then
		break
	end

	for neightbor_z = -1, 1 do
		for neightbor_y = -1, 1 do
			if neightbor_x ~= 0 or not (neightbor_x == neightbor_y and neightbor_x == neightbor_z and neightbor_z == neightbor_y) then
				local neighbor_position = Vector3.new(x + neightbor_x, y + neightbor_y, z + neightbor_z)
				table.insert(block.neighbors, neighbor_position)

				if block.material ~= "air" then
					local real_block = chunk.blocks[neighbor_position]
					if real_block and real_block.material == "air" then
						block.visible = true
						skip_neighbor = true
					end
				end
			end
		end
	end
end
1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.