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
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.