Hello, I am @MightyPart and this is a tutorial on how to use Fractal Noise to create 3D Terrain with caves.
Please tell me below if you get stuck or are confused. English is my native language but its 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.
Section 1: Setting Up The Project
Create a new Roblox Studio Project and remove everything from the workspace. Then add a Script called GenTerrain
into ServerScriptService
.
Add these variables into the GenTerrain
Script:
WIDTH, DEPTH, HEIGHT = 50, 50, 30
PART_SCALE = 5
NOISE_SCALE = 100
HEIGHT_SCALE = 30
OCTAVES = 4
LACUNARITY = 3
PERSISTENCE = .35
SEED = 69
MAX_TREES = 50
ValidTreePositions = {}
CollectionService = game:GetService("CollectionService")
Add this function near the top of the GenTerrain
Script:
local function Round(num, mult)
return math.floor(num / mult + 0.5) * mult
end
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,WIDTH*PART_SCALE,PART_SCALE do
for z=0,DEPTH*PART_SCALE,PART_SCALE do
for y=0,HEIGHT*PART_SCALE,PART_SCALE do
local part = Instance.new("Part")
part.Anchored = true
part.Size = Vector3.new(PART_SCALE, PART_SCALE, PART_SCALE)
part.Position = Vector3.new(x,y,z)
part.Parent = workspace
end
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:
FractalNoise = {}
FractalNoise["2D"] = 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
return FractalNoise
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,WIDTH*PART_SCALE,PART_SCALE do
for z=0,DEPTH*PART_SCALE,PART_SCALE do
local TwoD_height = FractalNoise["2D"](x, z,
OCTAVES,
LACUNARITY,
PERSISTENCE,
NOISE_SCALE,
SEED
) * HEIGHT_SCALE
TwoD_height = Round(TwoD_height, PART_SCALE)
TwoD_height += (HEIGHT*PART_SCALE) - HEIGHT_SCALE
for y=0,HEIGHT*PART_SCALE,PART_SCALE do
if y > TwoD_height then continue end
local part = Instance.new("Part")
part.Anchored = true
part.Size = Vector3.new(PART_SCALE, PART_SCALE, PART_SCALE)
part.Position = Vector3.new(x,y,z)
part.Parent = workspace
end
end
end
When you press Run
, the top of the grid should now be reminiscent of terrain:
Section 4: Making The Caves
To make the caves we need to add another function into the FractalNoise
ModuleScript. This function should be situated above return FractalNoise
:
FractalNoise["3D"] = function(x, y, z, octaves, lacunarity, persistence, scale, seed)
local value = 0
local x1 = x
local y1 = y
local z1 = z
local amplitude = 1
for i = 1, octaves, 1 do
value += math.noise(x1 / scale, y1 / scale, z1 / scale, seed) * amplitude
y1 *= lacunarity
x1 *= lacunarity
z1 *= lacunarity
amplitude *= persistence
end
return math.clamp(value, -1, 1)
end
Now edit the nested for loops inside of the GenTerrain
Script:
for x=0,WIDTH*PART_SCALE,PART_SCALE do
for z=0,DEPTH*PART_SCALE,PART_SCALE do
local TwoD_height = FractalNoise["2D"](x, z,
OCTAVES,
LACUNARITY,
PERSISTENCE,
NOISE_SCALE,
SEED
) * HEIGHT_SCALE
TwoD_height = Round(TwoD_height, PART_SCALE)
TwoD_height += (HEIGHT*PART_SCALE) - HEIGHT_SCALE
for y=0,HEIGHT*PART_SCALE,PART_SCALE do
if y > TwoD_height then continue end
local ThreeD_height = FractalNoise["3D"](x, y, z,
OCTAVES,
LACUNARITY,
PERSISTENCE,
20,
SEED
) * HEIGHT_SCALE
ThreeD_height = Round(ThreeD_height, PART_SCALE)
if ThreeD_height < 0 and y ~= TwoD_height then continue end
local part = Instance.new("Part")
part.Anchored = true
part.Size = Vector3.new(PART_SCALE, PART_SCALE, PART_SCALE)
part.Position = Vector3.new(x,y,z)
part.Parent = workspace
end
end
end
When you press Run
, you will see that the terrain now has caves underneath it.
Section 5: Adding Color
To add color to the terrain add this if statement to the nested for loop inside of the GenTerrain
Script:
if y == TwoD_height then
part.Color = Color3.fromRGB(115, 142, 112)
part.Material = Enum.Material.Grass
table.insert(ValidTreePositions, Vector3.new(x,y,z))
elseif y <= TwoD_height-PART_SCALE and y >= (TwoD_height-(PART_SCALE*3)) then
part.Color = Color3.fromRGB(140, 95, 56)
part.Material = Enum.Material.Slate
else
part.Color = Color3.fromRGB(143, 146, 146)
part.Material = Enum.Material.Rock
end
The nested for loop should now look like this
for x=0,WIDTH*PART_SCALE,PART_SCALE do
for z=0,DEPTH*PART_SCALE,PART_SCALE do
local TwoD_height = FractalNoise["2D"](x, z,
OCTAVES,
LACUNARITY,
PERSISTENCE,
NOISE_SCALE,
SEED
) * HEIGHT_SCALE
TwoD_height = Round(TwoD_height, PART_SCALE)
TwoD_height += (HEIGHT*PART_SCALE) - HEIGHT_SCALE
for y=0,HEIGHT*PART_SCALE,PART_SCALE do
if y > TwoD_height then continue end
local ThreeD_height = FractalNoise["3D"](x, y, z,
OCTAVES,
LACUNARITY,
PERSISTENCE,
20,
SEED
) * HEIGHT_SCALE
ThreeD_height = Round(ThreeD_height, PART_SCALE)
if ThreeD_height < 0 and y ~= TwoD_height then continue end
local part = Instance.new("Part")
part.Anchored = true
part.Size = Vector3.new(PART_SCALE, PART_SCALE, PART_SCALE)
part.Position = Vector3.new(x,y,z)
if y == TwoD_height then
part.Color = Color3.fromRGB(115, 142, 112)
part.Material = Enum.Material.Grass
table.insert(ValidTreePositions, Vector3.new(x,y,z))
elseif y <= TwoD_height-PART_SCALE and y >= (TwoD_height-(PART_SCALE*3)) then
part.Color = Color3.fromRGB(140, 95, 56)
part.Material = Enum.Material.Slate
else
part.Color = Color3.fromRGB(143, 146, 146)
part.Material = Enum.Material.Cobblestone
end
part.Parent = workspace
end
end
end
When you press Run
, Your terrain should now have some color on it.
Section 6: 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 this for loop to the bottom of the GenTerrain
Script.
for count=1,MAX_TREES do
local tree = game.ReplicatedStorage.Tree:Clone()
local pos = ValidTreePositions[math.random(1, #ValidTreePositions)]
local cframe = CFrame.new(
pos.X, pos.Y+(tree:GetExtentsSize().Y/2)-2, pos.Z
) * CFrame.Angles(0, math.rad(math.random(1, 360)), 0)
tree:SetPrimaryPartCFrame(cframe)
tree.Parent = workspace
end
When you press Run
, your terrain will now have Trees on top of it.
Section 7: Making Blocks Breakable
Add the code below into the nested for loops inside of the GenTerrain
function right above the part.Parent = workspace
line:
clickDetector = Instance.new("ClickDetector")
clickDetector.Parent = part
CollectionService:AddTag(part, "Blocks")
Add the for loop below to the end of the GenTerrain
script:
for _,block in pairs(CollectionService:GetTagged("Blocks")) do
block.ClickDetector.MouseClick:Connect(function()
block:Destroy()
end)
end
When you press Run
, you will now be able to remove blocks.
Section 8: Finishing Touches
Currently the player can fall through the bottom of the map, to fix this we can add a part at the bottom of the terrain to make sure players can’t fall through holes in the bottom of the map.
To do this add this code to the bottom of the GenTerrain
Script.
local bottomRock = Instance.new("Part")
bottomRock.Anchored = true
bottomRock.Size = Vector3.new((WIDTH*PART_SCALE)+PART_SCALE, PART_SCALE, (DEPTH*PART_SCALE)+PART_SCALE)
bottomRock.Position = Vector3.new((WIDTH*PART_SCALE)/2, -PART_SCALE, (DEPTH*PART_SCALE)/2)
bottomRock.Parent = workspace
bottomRock.Color = Color3.fromRGB(116, 114, 115)
bottomRock.Material = Enum.Material.Pebble
When you press Run
, there will now be a layer of rock preventing the player from falling through holes in the bottom of the map.