Perlin Noise-Based Terrain Generation

Hello everyone! I’m new to Roblox Dev forum, and I’d like to share a terrain generation system based on Perlin noise that I’ve been working on for my game, Airlift.


In essence, 3D PN creates a series of numbers that flow into each other. If you assign these numbers to a place on a grid, it can create entire mountains and valleys for you! I chose to make it so when a value was high enough and it was considered a mountain, the number would be multiplied in order to create those steep cliffs that you see. After all of the terrain tiles were in place, I added another layer of Perlin noise to decide where trees would spawn, creating a forest kind of look.

When doing research on this topic, it was difficult for me to find resources relevant to what I wanted to create on Roblox. So if any of you are looking to make a similar system and would like my help, please reach out! Perlin noise isn’t as difficult as it sounds, but is a really powerful tool for realistic terrain and I’d love to help you get there.

Feedback/questions are very welcome : )

17 Likes
looks a bit like the game

minecraft

4 Likes

Haha yeah, Minecraft uses Perlin noise as well. I’ll just have to make a better game than Minecraft so people will say they ripped off of me.

7 Likes

It’s really cool! I always love to see procedurally generated terrain, especially with different types of gradient noise. Also love the terrain colors, they really flow well. Definitely interested in doing something like this for myself.

1 Like

Very nice. Back in the day I also tried doing something similar but somewhere along the lines I messed up and it ended up making a ton of holes in the ground at higher elevations. Would be nice to see the source of this :smile:

1 Like

The game looks very interesting but, too bad it’s not unlimited. But it’s still very good!

1 Like

It can be made infinite with a load/unload chunk system (like minecraft)

@iMajesticMuffins to the OP:

Looks pretty good. I kinda wonder how did u make it so trees doesn’t spawn in top of each other

2 Likes

That’s interesting, I had similar problems whenever I used a seed above 100,000. I solved this by setting the seed to math.random(1, 20000)/1000. Hope this helps, and I appreciate the kind words!

local step = .05 --Jump between to noise values (lower number, smoother)
local size = 50 --Length and width of the tile grid
local scaleFactor = 160 --Multiplier for the base noise (higher number, more violent peaks and valleys)
local gridSize = 7.5 --Grid regular tiles are locked to
local secondGrid = 14 --Grid the mountains are locked to
local heightClassifier = 75 --Lower the number, higher the terrain (more mountains)
local seed = math.random(1, 20000)/1000
local populationSeed = seed+1 --Seeding for trees, bushes, etc

local tileStorage = {}

local highestTile = nil

function populateWorld()
	for i = 1, math.random(2, 4) do
		local selectedTile = tileStorage[math.random(1, #tileStorage)]
		selectedTile.TileScript.CreateObject:Fire("Ruins")
	end
	for z = 1, size do
		for x = 1, size do
			--TREES and BUSHES
			local ColorNum = (1 + math.noise(x*.1,z*.1,populationSeed))
			local griddedNumb = (math.floor(ColorNum/gridSize)*gridSize)
			local selectedTile =  tileStorage[(size*(z-1))+(x-1)]
			if ColorNum > 1 and #selectedTile.Objects:GetChildren() == 0 then
				if math.random(1,30) < 13 then --Probability of a tree spawning on an eligible tile
					if selectedTile.Name == "Grass" then
						selectedTile.TileScript.CreateObject:Fire("Tree")
					end
				else
					if math.random(1,30) < 5 then --Probability of a bush spawning instead of a tree
						if selectedTile.Name == "Grass" then
							selectedTile.TileScript.CreateObject:Fire("Bush")
						end
					end
				end
			end
		end
	end
	for z = 1, size do
		for x = 1, size do
			--ROCKS
			local selectedTile =  tileStorage[(size*(z-1))+(x-1)]
			if selectedTile.Name == "Rock" then
				if math.random(1,290) < 17 then
					selectedTile.TileScript.CreateObject:Fire("Rock")
				end
			end
		end
	end
	game.ReplicatedStorage.Events.WorldGenerated:Fire()
end

game.ReplicatedStorage.Events.GenerateWorld.Event:Connect(function()
	print("Generating on seed "..seed)
	
	local blockHeights = {
		[0] = "Sand",
		[1] =  "Grass",
		[2] =  "Rock",
		[3] = "SnowRock"
	}

	for z = 1, size do
		for x = 1, size do
			local ColorNum = (1 + math.noise(x*step,z*step,seed))*scaleFactor - 20 --First we take the number we get from the noise when we input our X and Z coordinate, multiply it by the scale factor, and then subtract 20
			local griddedNumb = (math.floor(ColorNum/gridSize)*gridSize) --Put that number into a grid
			local heightType = (math.floor(ColorNum/heightClassifier)) --Based on how high the tile is, assign it a height type (Sand, Grass, Rock, or SnowRock)
			if heightType < 0 then heightType = 0 end --If heightType was somehow out of bounds, correct it
			if heightType > 3 then heightType = 3 end
			
			local clone = game.ServerStorage.Tiles:FindFirstChild(blockHeights[heightType]):Clone()
			if heightType == 2 then --This block multiplies the height of the mountains to give the steep cliff effect
				griddedNumb = griddedNumb * (ColorNum*.0073)
				griddedNumb = (math.floor(griddedNumb/secondGrid)*secondGrid)
			end
			if heightType == 3 then  --This block multiplies the height of the mountains to give the steep cliff effect
				griddedNumb = griddedNumb * (ColorNum*.0073)
				griddedNumb = (math.floor(griddedNumb/secondGrid)*secondGrid)
			end
			clone:SetPrimaryPartCFrame(CFrame.new(Vector3.new(z*20, griddedNumb, x*20))) --Set the tile's position to the final product
			if highestTile == nil then
				highestTile = clone
			else
				if clone.PrimaryPart.Position.Y > highestTile.PrimaryPart.Position.Y then
					highestTile = clone
				else
					if clone.PrimaryPart.Position.Y == highestTile.PrimaryPart.Position.Y then
						if math.random(1, 2) == 2 then
							highestTile = clone
						end
					end
				end
			end
			clone.Parent = game.Workspace.Tiles
			table.insert(tileStorage, #tileStorage, clone)
		end
	end
	print("Playfield generated successfully. Populating...")
	populateWorld()
end)
2 Likes

Thank you! Like @DavidNet22 said, it’s totally possible to make it infinite. The game I’m making right now actually depends on having a limited amount of space so players have to fight over resources, so the grid is smaller (but will scale depending on how many players are connected for each round).

1 Like

For all the trees, rocks, points of interests and loot chests, I’m actually having that handled by the terrain generator rather than each tile individually. Once the world has been created, it loops through every tile and decides whether to place an object on that tile or not. This way, I can be in complete control over where and how the trees spawn : )

1 Like

oh god the lag…

Nevertheless, thats pretty
cool.

Not to be the nerd, but Minecraft uses 3D peril noise.

Also really good job, it’s pleasing for the eye. (unlike my projects)

Ohh and if you want the map to be centred here’s something:

Position = Vector3.new(
    -MapSizeX/2 + x,
    y,
    -MapSizeY/2 + z
)

Assuming you have a 2d array.

2 Likes

It used to use perlin noise but not anymore. Now it uses a copyrighted noise as strange as that may sound.

You’re right! And thank you so much, I was deciding on whether or not to have it be an island or just have walls around it, I’ll try this out!