# Using Fractal Noise To Create 3D Terrain With Caves

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

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.

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.

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

``````

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.

19 Likes

I really liked this tutorial! I used it to make minecraft terrain. However, could you please add a part about Greedy meshing as I can’t wrap my head around how I could implement it

(Also I added ore, I could update the post on how to do that)

3 Likes

Add to the tutorial a system that makes it so only blocks surrounded by air get rendered. You will get at most a 10x performance increase.

4 Likes

That is a good idea. Maybe I should add that to my edit. (Although, I don’t think im smart enough)

3 Likes

Heyo, don’t know if you where still working on this but here is a code snipped that im using that uses greedy meshing for the y axis. You could use it for the x & the z axis if you wanted to but im doing a noise graident color so doing greedy meshing on the x & z messes it up. Hope this helps ya!

``````local function OptimizeTerrain()
for X = 1, GRID_SIZE_2D do
for Y = 2, GRID_SIZE_3D do
for Z = 1, GRID_SIZE_2D do
if not TerrainGrid[X][Y][Z] then
continue
end

if not TerrainGrid[X][Y - 1][Z] then
continue
end

TerrainGrid[X][Y][Z]["Size"] += Vector3.new(
0,
TerrainGrid[X][Y - 1][Z]["Size"].Y,
0
)
TerrainGrid[X][Y - 1][Z] = nil
end

end
end)

print(string.format("Optimizing Terrain: %d%s", X / GRID_SIZE_2D * 100, "%"))
end
end
``````
1 Like

If you don’t mind me asking, have you tested how much performance this has successfully saved?

No I have not done exact tests however it will take longer to generate as you are adding another step but it reduces part count by a large amount (~300% less parts)

Here is a video of it in action: (Note: this is FBM + 3D Noise)

This seems rather good, if you were to make this in parallel luau, you can effectively make seamless chunks generate with little to no lag.

Yeah I was just making some testing, if I where to create this with a chunk system then I would for sure use that!