Introduction
Perlin Noise can be extremely confusing, I’ve read and watched countless tutorials on how to use it but I always got stuck on simple things. Have you ever gotten stuck on a problem where you were generating the same world every time no matter the seed? Well, I’ve decided to write a comprehensive guide on how to use Perlin Noise and why it’s useful. This will go in depth on how to use it and how you can implement it into creating procedural terrain. It will teach you chunks, Perlin noise, some math, and just learning how it works. At the end of each block of text, the code for that segment will be shown in a picture format! This can generate in under 2 seconds for HUGE maps too!
This guide can be used for any game development platform, as it is mostly just math. Sebastian Lague has a really good video on how Perlin noise works, but it’s a little different in Roblox.
This guide is meant for more advanced scripters, feel free to take a look at it though!
Table of Contents
1. Introduction
2. What is Perlin noise?
3. Getting started with Perlin noise!
4. Scripting
5. How does Perlin noise work?
6. How do I generate a random seed?
7. How do we color our terrain?
8. How do we make chunks?
9. Smoothing out and removing water bumps!
10. Making the terrain solid and fixing holes.
11. Usage
12. Source Code
13. Ending
What is Perlin noise?
Perlin noise was made by Ken Perlin in 1983, it has a ton of use cases. If you seem it before, then you know it looks like static on a TV. Here is me making a model of it in Roblox vs the real thing.
As you can see in the photo, it looks kind of blurry. This is actually what makes Perlin noise useful! Perlin noise is a 2 dimensional image, but you can apply it to 3D too by using multiple noise functions. Perlin noise is usually a number between 0 - 1, -0.5, 0.5, or -1, 1. In Roblox it is -0.5, 0.,5, and we will talk more about this soon. A lot of major coding languages today will actually have the noise function, so don’t worry about not being able to apply this to any other game development platform! It has many use cases, such as for us in the terms of procedural terrain. Here is a virtual landscape generated with procedural terrain, and also a gif of changing amplitude and frequency.
Getting started with Perlin noise!
Roblox has a built in noise function called math.noise(x, y, z)
. Since we are making the top of the world, we only need the first 2 arguments, since the top of a world can be “2D”. Below I’ll teach you how to make something like this, but I’ll also teach you how perlin noise works.
This map here is huge generated under 2 seconds, with the info below you can create this too!
Now this terrain here is colored, you could make this actual terrain, or you could make a Minecraft voxel world! It’s amazing how much cool things you can do with Perlin noise.
Scripting
Anyway let’s make a new ModuleScript called “PerlinGenerator” inside of ServerScriptService. What I’m going to do is make a new Part, and call it “PerlinBlock”, and make a variable for it in the script. I’ll make it 1x1x1 and anchored, and put it inside of the script. We can also make a new function called Generate().
Great! Now let's start off by defining some variables. Lets define a variable called "MapSize", and set it equal to 100. Also lets make a variable to a new folder you can create in workspace called, "TerrainParts".
Now we can create a 2 nested loops, these will go to a max of the MapSize, and start from 0. The reason we have 2, is because we have to generate the world out of “Chunks”, each chunk could be any size you want, but for now we will keep it at 1. Since we are making a 2D noise map, we can define the variables to be named X and Z. I will also add a task.wait()
in the first loop so we don’t crash studio!
Ok, so now we are looping over each chunk in the world, 1 by 1. Hm… we don’t seem to actually be doing anything yet. Let’s make our sample X and Z variables! These variables are the “base” variables that the Perlin noise function will use. We will also have to add 2 new parameters to the function, Scale and Frequency.
Scale:
What is Scale you may ask? Scale is basically in short terms, how zoomed in the map is. The higher the number, the more zoomed in you are!
Scale of 10:
Scale of 3:
Frequency:
Oh, a new one! Frequency is how often the hills and stuff will show up. Not too hard to understand I hope
Frequency of 0.4:
Frequency of 0.1:
I’ll be honest, frequency and scale are basically the same thing. Feel free to only use one if you want, but I would recommend using both just to follow how other people do it! Lets get back to our 2 sample variables. For Scale and Frequency to affect these, we have to divide both of them by Scale, and multiply by Frequency. This is how it should look right now:
Great now we have a cool part, placing down blocks!!! Let us make a new variable called y, and set it equal to 1. Let us also clone our part, and set the CFrame to the x, y, and z variables we now have! Make sure you parent it to the folder you made!
Hit run and in the command bar or from a script, call the Generate() function and pass in a Scale of 10 and a Frequency of 0.1. You can paste this code below into the command bar to generate a plane!
local PerlinGenerator = require(game:GetService("ServerScriptService").PerlinGenerator)
PerlinGenerator.Generate(10, 0.1)
If we hit run now, we see a flat plane of blocks! We are definitely getting somewhere.
Let us do some cleaning up. Each time we call the Generate function lets remove all previous blocks from the TerrainParts folder.
Now we can get into actually making perlin noise! First though, we need to understand how it works…
How does Perlin noise work?
Perlin noise actually isn’t too complicated, basically what it does is interpolating between a dot product of multiple gradient vectors and their offsets. You don’t need to know this but I’ll include it for those tech geeks . Let’s get into how it works in general.
We can use Perlin noise by doing math.noise(x,y,z)
. Since we won’t be using integers, it will return a value between -0.5, and 0.5. We will scale this up to 0 - 1 however later. So, for X all you have to do is input your X variable, this is just a number, same for Y, input your Y variable. Since this is a 2D map however, you might be wondering what Z is for. For us we can use Z as a seed. A seed is a randomly generated number that defines how the world looks, if you input the same seed the world will look the same.
You can think of it as Minecraft’s seed system, since Minecraft does actually use Perlin noise for it’s world generation. (It uses many more things but I won’t go into that.)
How do I generate a random seed?
So now that we know how Perlin noise works, we should first generate a random seed. This is where many people get stuck, and end up generating the same world every time. What we will do is create a new pseudorandom generator, and pass in the Seed. Then we can use :NextNumber(), on it to get a completely random seed! Let’s do this now, remember that Seed is actually a number in Roblox’s case.
First define a variable called random inside the generate function, set it equal to Random.new(Seed)
. You will also have to create a new parameter in the generate function called Seed for this to work.
Nice, but now we have to get our random offsets from this pseudorandom generator. Now what are offsets you may ask? Well basically we offset the Perlin noise map by a certain amount, this way it’s completely new each time.
Contrary to popular beliefs, we don’t need to put in something for Z, this is because Roblox actually clamps it to 0 - 1. So all those times people say “Just put tick() inside of the Z argument”, that’s wrong sadly. This is why many people end up with the problem of generating the same world every time, as we know tick() returns a huge number now, increasing every second now, this means it will distort your world years from now even.
So let’s get back to work and make our offsets. If we use a offset too high or too low then the world will be generated wrong. This is so important to know, a offset too high or low is very dangerous for your world.
Look at this world, I set the max random number to be 1 million, seems ok right? We want more random seeds, but this is the wrong way to go, never EVER do this please!
So, let’s finally make our offsets, make 2 new variables called xOffset and zOffset, we will set them equal to random:NextNumber(-100000, 100000)
, we use only 100,000 because that’s pretty much the highest you can go before it distorts.
You may be thinking “Only 100,000 different seeds? No!!!”, this isn’t true, since we made the psuedorandom generator WHILE passing in the seed, it will be up to 4,294,967,296 * 40,000,400,001 different worlds, (I think). Long story short, don’t worry about repeating worlds.
Now let’s just add these offsets to their respective variables,
FINALLY! Let’s generate our Perlin noise! Let’s use math.noise()
, and pass in our sampleX, and sampleZ. Let’s also add 0.5 to the end of this. We will set this equal to our Y variable in the loop.
Perfect, let’s hit play and see what it looks like-
Ouch! It looks like hot garbage, let’s fix that. Right now it is generating a number between 0 - 1, which means the max height is 1. We can multiply this by an Amplitude to increase the height! Let’s create a new parameter called Amplitude and multiply our noise function by it.
Ok, for now when calling the Generate function, set Scale to 10, Frequency to 0.4, Amplitude to 15, and Seed to what ever number you want.
Wow that looks like real terrain! Let’s be careful though and clamp it between 0 - 1, because Roblox says it sometimes goes out of this range. Make sure not to clamp the noise function WITH the amplitude, multiply the end result by Amplitude otherwise you will not have any hills.
If you want you could stop here! You have everything you need to know, all you have to do now is instead of placing a part, put your terrain there or put a different kind of part! You can make your maps bigger by changing the MapSize variable. I would recommend continuing on for colors though!
How do we color our terrain?
Coloring our terrain really isn’t too hard. First we need a dictionary of colors, the key has to be a number 0 - 1, and the value has to be a Color3 value. The reason the key has to be 0 - 1 is because our noise function returned a value 0 - 1. Anyway, put in a number for the key, anything GREATER than this number will have that color. So, you could put 0 as a blue color for water, 0.2 as a green color for land, and 0.8 as a white color for mountain crests.
So make a variable for the dictionary at the top of your script, feel free to copy mine for now!
local Colors = {
[0.8] = Color3.fromRGB(255, 255, 255), -- Mountain Crests
[0.75] = Color3.fromRGB(66, 66, 66), -- Dark Rock
[0.7] = Color3.fromRGB(88, 88, 88), -- Rock
[0.5] = Color3.fromRGB(44, 93, 40), -- Dark Grass
[0.3] = Color3.fromRGB(52, 111, 48), -- Grass
[0.25] = Color3.fromRGB(110, 168, 255), -- Foamy Edges
[0.10] = Color3.fromRGB(70, 130, 214), -- Ocean
[0] = Color3.fromRGB(48, 89, 149), -- Deep Ocean
}
This is how the top of your script should look:
Ok, perfect. Let us actually separate our noise function from the clamp function so we can read the non scaled y value.
Great, now lets make a new function to get the color, I won’t go over how this works because it’s pretty simple and it’s not the point of this post. Feel free to copy it here and put it in your script.
local function GetColor(y : number) : Color3
local closetKey = -1
-- Loop through colors and find the one that it is greater than but also less than the next one.
for key, color in pairs(Colors) do
if y >= key then
if key > closetKey then
closetKey = key
end
end
end
return Colors[closetKey]
end
Awesome! Let’s now set our part’s color each to this function and pass in the baseY value.
Ok, now let’s generate our terrain again… you can use this code in the command bar.
local PerlinGenerator = require(game:GetService("ServerScriptService").PerlinGenerator)
PerlinGenerator.Generate(10, 0.4, 15, tick())
Wow, this is basically perfect terrain! This is really the end of this, you can continue if you want.
How do we make chunks?
Chunks actually should pretty easy, and luckily it is. Create a new parameter called ChunkSize in the generator function, and when setting the CFrame of the cloned part, create a new variable called BlockCFrame, and set this equal to a new CFrame with all the x, y, z variables all multiplied by the chunk size. Also then set the cloned part’s CFrame equal to the BlockCFrame variable. Finally we have to change the size of our part to Vector3.new(ChunkSize, ChunkSize, ChunkSize)
Now this helps up scale up our world if it’s too small for the player.
Smoothing out and removing water bumps.
Wow, this may get complicated for you guys! Now you may notice that the water is bumpy, what if we want it to be flat? What if we want to influence the land around us? This is where a inverse lerp comes in.
We can use this to map a number between 2 values. We want the map the y position starting from the threshold, to 1, since that is the max height. If we mapped the threshold from 0, it would jut out of the ground. I made 2 simple functions for these, and you can put them in your code:
local function InverseLerp(value : number, start : number, endingValue : number) : number
return (value - start) / (endingValue - start)
end
local function SmoothNumber(baseY : number, threshold : number) : number
if baseY <= threshold then
return 0
else
local lerpedValue = InverseLerp(baseY, threshold, 1)
return lerpedValue
end
end
All we have to do is input baseY into the SmoothNumber function. Let us also create a new parameter called SmoothingFactor. I also made a variable that will store the baseY since we will be changing it a lot. We are just saving it for later. Now if we hit play, and input a smoothing factor of around 0.4, it would suddenly almost all turn to water, this is because we set the baseY to 0. We want to keep the colors, that is why I made the preservedBaseY variable.
As you see, we input the smoothing factor into the smooth number function. So, it will take that and inverse lerp baseY to between the smoothing factor and 1.
Keep smoothing factor between 0 - 1, or 0.25 for the best results!
So now, let us change the color of the parts to be GetColor(preservedBaseY), since we don’t want it all to be water.
Making the terrain solid and fixing holes.
Is this you right now? With holes and a flat plane? Let’s fix it!
This could be very hard… but no it’s literally changing a number. Let us set the size of the parts on the Y to be y * 2 + 1
. We added a + 1 so that it would be a little extra thick.
Wow! This terrain is amazing!
Usage
So now, you can build your worlds! Below I will answer questions on how to fix certain bugs, and just how to use the generator.
Q: How do I make my map scale bigger?
A: Change the chunk size.
Q: How do I make my map wider and longer, not scaled more?
A: Change the MapSize variable.
Q: I’m getting the same world every time?
A: Make sure you are using a random seed every time, use you can pass in tick() into the Seed for the generator function. Make sure you make the pseudorandom generator correctly!
Q: It’s completely flat!
A: Make sure scale is around 10, frequency around 0.4, and amplitude around 10, and chunksize around 2. Then play around with it.
Q: Too many hills?
A: Change the frequency to be less.
Source Code
You can download the Perlin Generator script here. Make sure you made a folder in workspace called TerrainParts though!
Script: Perlin.rbxm (4.5 KB)
Place: Perlin.rbxl (50.1 KB)
Github: Roblox Perlin Generator Repo
Copy and Paste:
-- Written by @LxckyDev, 8/8/2024
local Perlin = {}
local PerlinBlock = script.PerlinBlock
local MapSize = 100
local TerrainParts = workspace.TerrainParts
local Colors = {
[0.8] = Color3.fromRGB(255, 255, 255), -- Mountain Crests
[0.75] = Color3.fromRGB(66, 66, 66), -- Dark Rock
[0.7] = Color3.fromRGB(88, 88, 88), -- Rock
[0.5] = Color3.fromRGB(44, 93, 40), -- Dark Grass
[0.3] = Color3.fromRGB(52, 111, 48), -- Grass
[0.25] = Color3.fromRGB(110, 168, 255), -- Foamy Edges
[0.10] = Color3.fromRGB(70, 130, 214), -- Ocean
[0] = Color3.fromRGB(48, 89, 149), -- Deep Ocean
}
local function GetColor(y : number) : Color3
local closetKey = -1
-- Loop through colors and find the one that it is greater than but also less than the next one.
for key, color in pairs(Colors) do
if y >= key then
if key > closetKey then
closetKey = key
end
end
end
return Colors[closetKey]
end
local function InverseLerp(value : number, start : number, endingValue : number) : number
return (value - start) / (endingValue - start)
end
local function SmoothNumber(baseY : number, threshold : number) : number
if baseY <= threshold then
return 0
else
local lerpedValue = InverseLerp(baseY, threshold, 1)
return lerpedValue
end
end
function Perlin.Generate(Scale : number, Frequency : number, Amplitude : number, ChunkSize : number, SmoothingFactor : number, Seed : number)
-- Remove all previous blocks.
for _, object in pairs(TerrainParts:GetChildren()) do
object:Destroy()
end
-- Generate random offsets.
local random = Random.new(Seed)
local xOffset = random:NextNumber(-100000, 100000)
local zOffset = random:NextNumber(-100000, 100000)
-- Loop through each chunk.
for x = 0, MapSize do
for z = 0, MapSize do
-- Change sizes of sample X and Z.
local sampleX = x / Scale * Frequency + xOffset
local sampleZ = z / Scale * Frequency + zOffset
-- Generate the perlin noise?!?!
local baseY = math.clamp((math.noise(sampleX, sampleZ) + 0.5), 0, 1)
local preservedBaseY = baseY
baseY = SmoothNumber(baseY, SmoothingFactor)
local y = baseY * Amplitude
-- Get the correct CFrame.
local BlockCFrame = CFrame.new(x * ChunkSize, y * ChunkSize, z * ChunkSize)
-- Create a new part for the terrain.
local clone = PerlinBlock:Clone()
clone.CFrame = BlockCFrame
clone.Color = GetColor(preservedBaseY)
clone.Size = Vector3.new(ChunkSize, y * 2 + 1, ChunkSize)
clone.Parent = TerrainParts
end
task.wait()
end
end
return Perlin
Ending
Thank you so much for reading until the end! I really hope this helped you make your perfect terrain generation system. I hope people realize that it is not that hard to do, and just requires some time and learning. If this tutorial taught you something, feel free to give feedback on it or share it with others. I spent a lot of time writing this post.