Is this intended behavior for my terrain generator?

I made a 3D Perlin noise terrain generator a while back, and I decided to add ‘Octaves’ to it. Which didn’t do what I thought it would do.

Here is a picture of the terrain without the octaves:

But then this happens when I add 20 octaves:

Is this intended behavior? Or am I not doing some math equation / algorithm correctly?

Here is the code:

local ChunkSize = 64
--math.randomseed(tick())
local Seed = math.random(-1000000000, 1000000000) % math.random(-1000000000, 1000000000)
math.randomseed(Seed)
local Seed = -406931380
local NoiseScale = 34
local Amp = 20
local BlockSize = 4
local MinDensity = -20
local Octaves = 20

print(Seed.."/"..NoiseScale.."/"..Amp)

--357444159/34/40

-- -969535336 Is a cool seed.

spawn(function()
	for x = 0, ChunkSize do
		for z = 0, ChunkSize do
			for y = 0, ChunkSize do
				local Xnoise = 0
				local Ynoise = 0
				local Znoise = 0
				Ynoise = math.noise(x / NoiseScale, z / NoiseScale, Seed) * Amp
				for i = 0, Octaves do
					Xnoise = Xnoise + math.noise(y / NoiseScale, z / NoiseScale, Seed) * Amp
					
					Znoise = Znoise + math.noise(x / NoiseScale, y / NoiseScale, Seed) * Amp
				end
				local Density = Xnoise + Ynoise + Znoise - y
				
				if Density > MinDensity then
					local Part = Instance.new("Part")
					Part.Parent = workspace
					Part.Anchored = true
					
					Part.Size = Vector3.new(BlockSize, BlockSize, BlockSize)
					
					Part.CFrame = CFrame.new((x - ChunkSize / 2)*BlockSize, y*BlockSize, (z - ChunkSize / 2)*BlockSize)
					
					
					
					--workspace.Terrain:FillBlock(Part.CFrame, Vector3.new(BlockSize, BlockSize, BlockSize), Enum.Material.Grass)
					
					--Part:Destroy()
				end
			end
		end
		wait()
		
	end
end)

Ignore the vague title for this topic.

I have attempted something like this before and I believe the behavior is correct for your current settings. If you are trying to get close to normal cliff overhangs like you would see in minecraft, then perhaps testing various amp and density values. If I recall correctly the reason you are getting such an unusual outcome is because you are using too high of values. It’s been a while since ive messed with perlin noise so I could be slightly wrong on this.

All in all, just keep experimenting with random values until you get close to achieving desired result :smile:

3 Likes

Thanks for letting me know. I was just making sure that I was doing an algorithm correctly or something.

Here’s a really great source for lots of things noise related, including a glossary over common terms: libnoise: Glossary

Basically, having several “octaves” of noise means that you’ll get details at different scales. If you have only one octave, you’ll get rolling hills all with the same distance between hilltops and valleys. Multiple octaves is great if you want noise at one scale for mountain ranges, and noise at a much smaller scale for little bumps on the mountain surface.

What you’re doing is adding several noise functions together, but all at the same scale. You’ll want to scale (multiply) the input coordinates to the noise functions to get several “octaves”.

One small distinction between just adding functions at different scales is that when using octaves, each noise function is a fixed size relative to the previous. That’s called the “lacunarity”, which you can also read up at the link I provided.

EDIT: here’s a link to a previous thread comment with a script/module that generates proper “Perlin noise” (i.e. with several octaves of noise).

1 Like

I know how to do perlin noise with frequency, amp, gain, lacunarity, octaves, etc…

I learned from @Crazyman32’s video talk at RDC here:

2 Likes

At this timestamp you can can see at lines 11 and 14 that the frequency of the noise is changing with each octave. Unfortunately the frikken code example is cut off, so you can’t see the frequency being applied to the input coordinates to math.noise.

In the code you posted, the NoiseScale variable is equivalent to the reciprocal of the frequency (i.e. NoiseScale=(1/frequency). But you never change the frequency of the noise, so having “octaves” is meaningless. You’re just adding the same noise to itself over and over again.

	local Xnoise = 0
	for i = 0, Octaves do
		Xnoise = Xnoise + math.noise(y / NoiseScale, z / NoiseScale, Seed) * Amp
	end

is exactly equivalent to

	local Xnoise = 0
	Xnoise = math.noise(y / NoiseScale, z / NoiseScale, Seed) * Amp* Octaves

because math.noise always gives the same output if you give it the same input.

To make your code work, you could change the frequency for every octave like this:

	local Xnoise = 0
	local NoiseScale = NoiseScale
	for i = 0, Octaves do
		Xnoise = Xnoise + math.noise(y / NoiseScale, z / NoiseScale, Seed) * Amp
		NoiseScale = NoiseScale / 1.2 --1.2 is the lacunarity
	end

You can also refer to the book for a third explanation.

Wouldn’t you want to put it in parenthesis like so: (amp * octaves)?

Correct me if i’m wrong but i’m pretty sure frequency is applied to the noise like so:

f * (x / n)

where n is noisescale, f is frequency, and x is the coordinate

I don’t know what you mean or what part of what I commented you’re quoting.

Not if NoiseScale determines how far each “hilltop” is from the next one, and frequency determines how often “hilltops” occur, then they’re really just each other’s inverse (opposite). You’re expressing one concept with two variables. Your NoiseScale is equivalent to the period of the noise.

Here’s a graph of the function sin(x):

If you multiply the x coordinate with 2, then the input to sin changes twice as fast, and so does the output.

If you multiply x with 0.5, then it changes half as fast.

In other words, the number that we multiply the x variable with is a multiplier of the function’s frequency. This is also true of math.noise, so we can do

local frequency = 1/16
for x = 0, 1, 1/100 do
	local value = math.noise(x*frequency)
end	

… to control the frequency of the noise. A high frequency variable means we get features that change fast. A low frequency means features that change slowly.

Instead of thinking about how fast features change, you can think of how big they are. Instead of multiplying the input to the function by some number, you can divide it by the period that you want:

The number that you divide x by is then a multiplier of the period of the function. Notice how sin(x/2) is exactly equivalent to sin(x*0.5), since 1/2=0.5. Dividing by 2 is the same as multiplying by 0.5. Because the concepts of frequency and period are so closely intertwined, it doesn’t make sense to express them as two different variables. It doesn’t make sense to multiply the input to math.noise by a number, and then divide it by another number, unless there’s a good use case for it in your specific case.

1 Like