Using Fractal Noise To Create An Island Map

Hello, I am @MightyPart and this is a tutorial on how to use Fractal Noise to create an island map.

Please tell me below if you get stuck or are confused. English is my native language but is currently 3am.

What is Perlin Noise?

Perlin noise is a type of visual noise that consists of a smoothly-varying signal with an organic feel than pure random noise.

What is Fractal Noise?

Fractal noise is multiple layers of perlin noise that are combined together to form a more intricate version of perlin noise.

What The Finished Project Will Look Like

Summary





Section 1: Settings Up The Project

Create a new Roblox Studio Project and remove everything from the workspace. Then add a Script called GenTerrain into ServerScriptService. Now add an empty folder called Settings into the newly created GenTerrain script.

Inside of the Settings folder add 2 IntValue’s called SIZE_XZ and PART_SCALE. Set the value of SIZE_XZ to “150” and set the value of PART_SCALE to “5”.

Inside of the GenTerrain Script add these variables

SIZE_XZ = script.Settings.SIZE_XZ.Value

PART_SCALE = script.Settings.PART_SCALE.Value
NOISE_SCALE = 100
HEIGHT_SCALE = 30

OCTAVES = 4
LACUNARITY = 3
PERSISTENCE = .35
SEED = 69

WATER_HEIGHT = 0
TREES_AMOUNT = 350

Section 2: Creating The Grid

To start off we need to create a grid of parts. We can achieve this by adding this code to the GenTerrain Script:

for x=0,SIZE_XZ*PART_SCALE,PART_SCALE do
	for z=0,SIZE_XZ*PART_SCALE,PART_SCALE do
		local part = Instance.new("Part")
		part.Anchored = true
		part.Size = Vector3.new(PART_SCALE,NOISE_SCALE,PART_SCALE)
		part.Position = Vector3.new(x,0,z)
		part.Parent = workspace.Map
	end
end

When you press Run, this grid that consists of many parts should be the result.


Section 3: Creating The Fractal Noise

Add a new ModuleScript called FractalNoise into the GenTerrain Script.

Add the code below into the FractalNoise ModuleScript:

return function(x, y, octaves, lacunarity, persistence, scale, seed)
	local value = 0 
	local x1 = x 
	local y1 = y
	local amplitude = 1
	for i = 1, octaves, 1 do
		value += math.noise(x1 / scale, y1 / scale, seed) * amplitude
		y1 *= lacunarity
		x1 *= lacunarity
		amplitude *= persistence
	end
	return math.clamp(value, -1, 1)
end

The function above creates layered noise based on the arguments put into it. For more info about this function you should check out BuiltToWreck’s post where they explain more about it.

Now import the FractalNoise ModuleScript function into the GenTerrain Script by adding in this line of code (ideally near the top of the Script):

FractalNoise = require(script.FractalNoise)

The nested for loops from Section 2 can now be changed to include the Fractal Noise:

for x=0,SIZE_XZ*PART_SCALE,PART_SCALE do
	for z=0,SIZE_XZ*PART_SCALE,PART_SCALE do
		local height = FractalNoise(x, z,
			OCTAVES, -- Integer that is >1
			LACUNARITY, -- Number that is >1
			PERSISTENCE, -- Number that is >0 and <1
			NOISE_SCALE,
			SEED
		) * HEIGHT_SCALE
		height = math.floor(height / PART_SCALE + 0.5) * PART_SCALE -- rounds to nearest 5
		
		local part = Instance.new("Part")
		part.Anchored = true
		part.Size = Vector3.new(PART_SCALE,NOISE_SCALE,PART_SCALE)
		part.Position = Vector3.new(x,height,z)
		part.Parent = workspace.Map
	end
end

When you press Run, your grid should now be reminiscent of terrain.


Section 4: Creating The Falloff Map

What is a falloff map?
A falloff map, for the context of this tutorial, is a nested array of positions that raises or lowers a specific part of the terrain. It will be used to lower the terrain close to the edge of the map/island.

Add a new ModuleScript called FalloffMap into the GenTerrain Script.

Add the code below into the FalloffMap ModuleScript:

SIZE_XZ = script.Parent.Settings.SIZE_XZ.Value
PART_SCALE = script.Parent.Settings.PART_SCALE.Value

VALUES = {}

for x=0,SIZE_XZ*PART_SCALE,PART_SCALE do
	VALUES[x] = {}
	for z=0,SIZE_XZ*PART_SCALE,PART_SCALE do
		
		local valueX = math.abs(((SIZE_XZ*PART_SCALE)/2 - x) / (SIZE_XZ*PART_SCALE))
		local valueZ = math.abs(((SIZE_XZ*PART_SCALE)/2 - z) / (SIZE_XZ*PART_SCALE))
		local value = math.max(valueX, valueZ) 

		VALUES[x][z] = value

	end
end

return function(x,z)
	return VALUES[x][z]
end

import the FalloffMap ModuleScript function into thee GenTerrain Script, preferably underneath where you required the Fractal Noise script:

FalloffMap = require(script.FalloffMap)

Now lets visualise the Falloff Map on the terrain. To do this alter the nested for loops seen in Section 2 & 3 to this:

for x=0,SIZE_XZ*PART_SCALE,PART_SCALE do
	for z=0,SIZE_XZ*PART_SCALE,PART_SCALE do
		local height = FractalNoise(x, z,
			OCTAVES, -- Integer that is >1
			LACUNARITY, -- Number that is >1
			PERSISTENCE, -- Number that is >0 and <1
			NOISE_SCALE,
			SEED
		) * HEIGHT_SCALE
		height = math.floor(height / PART_SCALE + 0.5) * PART_SCALE -- rounds to nearest 5
		
		local part = Instance.new("Part")
		part.Anchored = true
		part.Size = Vector3.new(PART_SCALE,NOISE_SCALE,PART_SCALE)
		part.Position = Vector3.new(x,height,z)
		
		local value = FalloffMap(x,z)
		part.Color = Color3.new(.5-value, .5-value, .5-value)
		
		part.Parent = workspace.Map
	end
end

When you press Run, you will see the falloff map visualised:


The blackest parts represents water and the lightest parts represent grass. As you can currently tell there isn’t a very big area for the grass, most of the map is taken up by water. We can fix this issue by making the gradient from the darkest color to the lightest color be a curve instead of a straight line.

To do this create a new function inside of the FalloffMap ModuleScript called “evaluate()”:

function evaluate(value)
	local a = 2.8
	local b = 2.2
	
	return math.pow(value, a) / (math.pow(value,a) + math.pow(b-b*value, a))
end

Now change the nested for loop inside of the FalloffMap ModuleScript to include the “evaluate()” function:

for x=0,SIZE_XZ*PART_SCALE,PART_SCALE do
	VALUES[x] = {}
	for z=0,SIZE_XZ*PART_SCALE,PART_SCALE do

		local valueX = math.abs(((SIZE_XZ*PART_SCALE)/2 - x) / (SIZE_XZ*PART_SCALE))
		local valueZ = math.abs(((SIZE_XZ*PART_SCALE)/2 - z) / (SIZE_XZ*PART_SCALE))
		local value = math.max(valueX, valueZ) 
		
		local evalValue = math.clamp(evaluate(value)*10, -.5, .5)

		VALUES[x][z] = evalValue

	end
end

When you press Run, you will now see that there is now more lighter parts than darker parts.


Section 5: Applying The Falloff Map Onto The Terrain.

Now that we have visualised the falloff map we now need to make the falloff map affected the height of the terrain. To do this we need to edit the nested for loops of the GenTerrain Script:

for x=0,SIZE_XZ*PART_SCALE,PART_SCALE do
	for z=0,SIZE_XZ*PART_SCALE,PART_SCALE do
		local height = FractalNoise(x, z,
			OCTAVES,
			LACUNARITY,
			PERSISTENCE,
			NOISE_SCALE,
			SEED
		) * HEIGHT_SCALE

		local value = FalloffMap(x,z)
		height = height - (value*(PART_SCALE*20))
		height = math.floor(height / PART_SCALE + 0.5) * PART_SCALE 
		
		local part = Instance.new("Part")
		part.Anchored = true
		part.Size = Vector3.new(PART_SCALE,NOISE_SCALE,PART_SCALE)
		part.Position = Vector3.new(x,height,z)

		part.Color = Color3.new(.5-value,.5-value,.5-value)
		part.Parent = workspace.Map

	end
end

When you press Run, you will see that the darker a part is the lower down it is.


Section 6: Adding Color To The Terrain

To add color to the terrain add this if statement to the nested for loop inside of the GenTerrain Script:

if value >= .15 then
	part.Color = Color3.fromRGB(143, 126, 95)
	part.Material = Enum.Material.Sand
elseif value >= 0 then
	part.Color = Color3.fromRGB(115, 142, 112)
	part.Material = Enum.Material.Grass
end
The nested for loop should now look like this
for x=0,SIZE_XZ*PART_SCALE,PART_SCALE do
	for z=0,SIZE_XZ*PART_SCALE,PART_SCALE do
		local height = FractalNoise(x, z,
			OCTAVES,
			LACUNARITY,
			PERSISTENCE,
			NOISE_SCALE,
			SEED
		) * HEIGHT_SCALE

		local value = FalloffMap(x,z)
		height = height - (value*(PART_SCALE*20))
		height = math.floor(height / PART_SCALE + 0.5) * PART_SCALE 
		
		local part = Instance.new("Part")
		part.Anchored = true
		part.Size = Vector3.new(PART_SCALE,NOISE_SCALE,PART_SCALE)
		part.Position = Vector3.new(x,height,z)
		
		if value >= .15 then
			part.Color = Color3.fromRGB(143, 126, 95)
			part.Material = Enum.Material.Sand
		elseif value >= 0 then
			part.Color = Color3.fromRGB(115, 142, 112)
			part.Material = Enum.Material.Grass
		end

		part.Parent = workspace.Map

	end
end

When you press Run, Your terrain should look like this:


Section 7: Refining The Terrain Colors

In the image above (in Section 6) the grass section of the terrain is a square, this is quite unnatural. To fix this we are going to utilise math.noise to make the sides of the green square wavy.

To achieve this we need to add a table at the top of the GenTerrain Script:

conversionTable = {
	["-0.5"] = .10,
	["-0.25"] = .13,
	["0"] = .16,
	["0.25"] = .19,
	["0.5"] = .22
}

You also need to add this code in just above where you inserted the if statement in Section 6 into the nested for loop inside the GenTerrain Script:

local noise = math.noise(x/NOISE_SCALE, z/NOISE_SCALE)
noise = Round(noise, .25)
noise = math.clamp(noise, -0.5, .5)
noise = conversionTable[tostring(noise)]

You also need to change the if statement slightly:

if value >= noise then
	part.Color = Color3.fromRGB(143, 126, 95)
	part.Material = Enum.Material.Sand
elseif value >= 0 then
	part.Color = Color3.fromRGB(115, 142, 112)
	part.Material = Enum.Material.Grass
end
The nested for loop should now look like this
for x=0,SIZE_XZ*PART_SCALE,PART_SCALE do
	for z=0,SIZE_XZ*PART_SCALE,PART_SCALE do
		local height = FractalNoise(x, z,
			OCTAVES,
			LACUNARITY,
			PERSISTENCE,
			NOISE_SCALE,
			SEED
		) * HEIGHT_SCALE

		local value = FalloffMap(x,z)
		height = height - (value*(PART_SCALE*20))
		height = math.floor(height / PART_SCALE + 0.5) * PART_SCALE 
		
		local part = Instance.new("Part")
		part.Anchored = true
		part.Size = Vector3.new(PART_SCALE,NOISE_SCALE,PART_SCALE)
		part.Position = Vector3.new(x,height,z)
		
		local noise = math.noise(x/NOISE_SCALE, z/NOISE_SCALE)
		noise = math.floor(noise / .25 + 0.5) * .25 
		noise = math.clamp(noise, -0.5, .5)
		noise = conversionTable[tostring(noise)]
		
		if value >= noise then
			part.Color = Color3.fromRGB(143, 126, 95)
			part.Material = Enum.Material.Sand
		elseif value >= 0 then
			part.Color = Color3.fromRGB(115, 142, 112)
			part.Material = Enum.Material.Grass
		end

		part.Parent = workspace.Map

	end
end

When you press Run, you will see that the green square has wavy sides now, which looks more natural.


Section 8: Adding Water

As you can probably tell the terrain currently looks very dry, we can remedy this by adding water.

To create the water add this code to the bottom of the GenTerrain Script.

-- adds water to the terrain
WD_SIZE = (2048)-0.01
XY_POS = (2048/2) - (NOISE_SCALE*PART_SCALE)/2
local water = Instance.new("Part")
water.Anchored = true
water.Size = Vector3.new(WD_SIZE, HEIGHT_SCALE, WD_SIZE)
water.Position = Vector3.new(XY_POS, WATER_HEIGHT, XY_POS)
water.Color = Color3.fromRGB(12, 84, 92)
water.Parent = workspace
water.Transparency = .5
water.CanCollide = false

When you press Run, the terrain will have water around it. Now it actually looks like an island.


Section 9: Adding Trees

To give your island a sense of live you could add some trees. First get my tree model here. Add the tree to ReplicatedStorage

Add a variable to the top of the GenTerrain Script called “validTreePositions”

validTreePositions = {}

Add this for loop to the bottom of the GenTerrain Script.

for count=1,TREES_AMOUNT do
	local tree = game.ReplicatedStorage.Tree:Clone()
	local pos = validTreePositions[math.random(1, #validTreePositions)]
	
	local cframe = CFrame.new(
		pos.X, pos.Y+(NOISE_SCALE/2)+(tree:GetExtentsSize().Y/2)-5, pos.Z
	) * CFrame.Angles(0, math.rad(math.random(1, 360)), 0)
	
	tree:SetPrimaryPartCFrame(cframe)
	tree.Parent = workspace	
end

Now change the if statement in the nested for loops inside of the GenTerrain Script:

if value >= noise then
	part.Color = Color3.fromRGB(143, 126, 95)
	part.Material = Enum.Material.Sand
elseif value >= 0 then
	part.Color = Color3.fromRGB(115, 142, 112)
	part.Material = Enum.Material.Grass
	table.insert(validTreePositions, Vector3.new(x,height,z))
end
The nested for loop should now look like this
for x=0,SIZE_XZ*PART_SCALE,PART_SCALE do
	for z=0,SIZE_XZ*PART_SCALE,PART_SCALE do
		local height = FractalNoise(x, z,
			OCTAVES,
			LACUNARITY,
			PERSISTENCE,
			NOISE_SCALE,
			SEED
		) * HEIGHT_SCALE

		local value = FalloffMap(x,z)
		height = height - (value*(PART_SCALE*20))
		height = math.floor(height / PART_SCALE + 0.5) * PART_SCALE 
		
		local part = Instance.new("Part")
		part.Anchored = true
		part.Size = Vector3.new(PART_SCALE,NOISE_SCALE,PART_SCALE)
		part.Position = Vector3.new(x,height,z)
		
		local noise = math.noise(x/NOISE_SCALE, z/NOISE_SCALE)
		noise = math.floor(noise / .25 + 0.5) * .25 
		noise = math.clamp(noise, -0.5, .5)
		noise = conversionTable[tostring(noise)]
		
		if value >= noise then
			part.Color = Color3.fromRGB(143, 126, 95)
			part.Material = Enum.Material.Sand
		elseif value >= 0 then
			part.Color = Color3.fromRGB(115, 142, 112)
			part.Material = Enum.Material.Grass
			table.insert(validTreePositions, Vector3.new(x,height,z))
		end

		part.Parent = workspace.Map

	end
end

When you press Run you should see your completed island.


Section 9: Finishing Touches

Change the value of SIZE_NZ (located in GenTerrain > Settings) to be “300”.

Your island should now be 3 times as big.

3 Likes