Help with Perlin Noise

You can write your topic however you want, but you need to answer these questions:

  1. What do you want to achieve? Keep it simple and clear!
    I want to achieve 3D perlin noise with overhangs as well as the transitioning of different biomes with different perlin noise. Also if it’s possible, I would like to make my structures/flowers also be generated with math…noise so they are the same for everybody’s client side…

  2. What is the issue? Include screenshots / videos if possible!
    I can’t seem to get the 3d perlin noise with the chunk rendering sytem I have. I also cannot find a way to transition from one biome to the other. I also cannot find a way to get a position for the structures/flowers with math.noise.

  3. What solutions have you tried so far? Did you look for solutions on the Developer Hub?
    I have looked on the DevForum for help and I have tried many other people’s solutions however none of them seem to work.

After that, you should include more details if you have any. Try to make your topic as descriptive as possible, so that it’s easier for people to help you!

If possibile I would like to keep using the same rendering system that I am using and hopefully I can find a way to optimize it myself.

local ChunkRenderDistance = 8
local ChunkSize = 16
local ChunkSizeY = 1
local ChunkGrid = {}
local CurrentBiome = 4
local SeedValue = workspace:WaitForChild("Seed")
repeat task.wait() until SeedValue.Value ~= -2
local Seed = SeedValue.Value

local blockSize = 3
local worldHeight = 10
local worldSize = 16
local divisor = 32

local blockSize = 3

local blocks = game.Workspace.Blocks

local uis = game:GetService("UserInputService")

local Biomes = {
	--{'Mountains' ,80, 70},
	{'Plains' ,160, 80},
	{'Forest' ,160, 80},
	{'Desert' ,160, 80},
	{'Rivers' ,160, 80},
}

game.Players.PlayerAdded:Connect(function(player)
	local Character = player.Character or player.CharacterAdded:Wait()

	Character:FindFirstChildWhichIsA("Humanoid").WalkSpeed = 12.951
end)

local function fractalNoise(x, y, octaves, lacunarity, persistence, scale, seed)
	-- The sum of our octaves
	local value = 0 

	-- These coordinates will be scaled the lacunarity
	local x1 = x 
	local y1 = y

	-- Determines the effect of each octave on the previous sum
	local amplitude = 1

	for i = 1, octaves, 1 do
		-- Multiply the noise output by the amplitude and add it to our sum
		value += math.noise(x1 / scale, y1 / scale, seed) * amplitude

		-- Scale up our perlin noise by multiplying the coordinates by lacunarity
		y1 *= lacunarity
		x1 *= lacunarity

		-- Reduce our amplitude by multiplying it by persistence
		amplitude *= persistence
	end

	-- It is possible to have an output value outside of the range [-1,1]
	-- For consistency let's clamp it to that range
	return math.clamp(value, -1, 1)
end

function GenerateNewChunk(Position)
	--if math.random(1,100) == 1 then CurrentBiome = math.random(1, #Biomes) print('Changed') end 
	local Frequency = Biomes[CurrentBiome][2] --
	local Amplitude = Biomes[CurrentBiome][3] --Height of wave

	local ChunkData = {}
	for x = 1, ChunkSize do
		ChunkData[x] = {}
		local LastYPos = 0
		for y = 1, ChunkSizeY do
			for z = 1, ChunkSize do
				local X = ((x * 3) + Position.X + 1.5)
			
				local noiseValue = math.noise((((x * 3) + Position.X + 1.5) / Frequency) + Seed, (((y * 3) + Position.Y + 1.5) / Frequency) + Seed, (((z * 3) + Position.Z + 1.5) / Frequency) + Seed)
				local Y = ((math.floor((noiseValue * Amplitude) / 3) * 3) + 1.5)
				local Z = ((z * 3) + Position.Z + 1.5)

				ChunkData[x][z] = Vector3.new(X, Y, Z)
				LastYPos = Y
			end
		end
	end


	table.insert(ChunkGrid, {Position ,ChunkData})
	return {Position ,ChunkData}
end

function GetBlockTypes(BlockPos, Chunk)
	local YPos = BlockPos.Y

	if CurrentBiome == 1 then
		if YPos < -4 then return game.ReplicatedStorage.Blocks.GrassBlock:Clone() end
		if YPos >= -4 then return game.ReplicatedStorage.Blocks.GrassBlock:Clone() end
	elseif CurrentBiome == 2 then
		if YPos < -4 then return game.ReplicatedStorage.Blocks.GrassBlock:Clone() end
		if YPos >= -4 then return game.ReplicatedStorage.Blocks.GrassBlock:Clone() end
	elseif CurrentBiome == 3 then
		if YPos < -4 then return game.ReplicatedStorage.Blocks.SandBlock:Clone() end
		if YPos >= -4 then return game.ReplicatedStorage.Blocks.SandBlock:Clone() end
	elseif CurrentBiome == 4 then
		if YPos < 1 then
			local WaterBlock = BlockPos
			repeat
				WaterBlock += Vector3.new(0,3,0)
			until WaterBlock.Y + 3 > 2

			local NewWaterBlock = game.ReplicatedStorage.WaterBlock:Clone()
			NewWaterBlock.Parent = Chunk
			NewWaterBlock.Position = WaterBlock + Vector3.new(1,1,1)
		end

		if YPos < 2 then return game.ReplicatedStorage.Blocks.SandBlock:Clone() end
		if YPos >= 2 then return game.ReplicatedStorage.Blocks.GrassBlock:Clone() end
	end
end

local ChunksRendered = 0
local WorldLoaded = false

function RenderChunk(ChunkData, ChunkIndex, player)
	local ChunkCenter = ChunkData[1]
	local BlockData = ChunkData[2]

	local ChunkModel = Instance.new('Model')
	ChunkModel.Name = 'Chunk '..ChunkIndex
	ChunkModel.Parent = workspace.Chunks

	local RunService = game:GetService("RunService")

	for _, Row in ipairs(BlockData) do
		for _, Block in ipairs(Row) do
			local NewBlock = GetBlockTypes(Block ,ChunkModel)
			NewBlock.Position = Block + Vector3.new(1,0,1)
			NewBlock.Parent = ChunkModel
			if NewBlock.Name == "GrassBlock" then
				--[[local OddsMob = math.random(1,1000)
				if OddsMob <= 4 then
					local NewMob = game.ReplicatedStorage.Mobs["Pig"]:Clone()
					NewMob.PrimaryPart.Position = NewBlock.Position + Vector3.new(0,3.345,0)
					NewMob.Parent = game.Workspace.Mobs
					NewMob.Name = NewMob.Name.."#"..#game.Workspace.Mobs:GetChildren()
				end]]

				local OddsPlant = math.random(1, 100)
				if OddsPlant <= 5 then
					local NewPlant = game.ReplicatedStorage.Blocks.FlowerBlock:Clone()
					NewPlant.Position = NewBlock.Position + Vector3.new(0,3,0)
					NewPlant.Parent = ChunkModel
				end
				local OddsStructure = math.random(0, 100)
				if CurrentBiome == 2 then
					if OddsStructure <= 2 then
						local NewStructure = game.ReplicatedStorage.Structures.Tree:Clone()
						NewStructure.PrimaryPart.Position = NewBlock.Position + Vector3.new(0,3,0)
						NewStructure.Parent = ChunkModel

						for _, block in ipairs(NewStructure:GetChildren()) do
							block.Parent = ChunkModel
						end
						NewStructure:Destroy()
					end
				elseif CurrentBiome == 4 then
					if OddsStructure <= 2 then
						local NewStructure = game.ReplicatedStorage.Structures.Tree:Clone()
						NewStructure.PrimaryPart.Position = NewBlock.Position + Vector3.new(0,3,0)
						NewStructure.Parent = ChunkModel

						for _, block in ipairs(NewStructure:GetChildren()) do
							block.Parent = ChunkModel
						end
						NewStructure:Destroy()
					end
				end
			elseif NewBlock.Name == "SandBlock" then
				local OddsStructure = math.random(0, 100)
				if CurrentBiome == 3 then
					if OddsStructure <= 2 then
						local NewStructure = game.ReplicatedStorage.Structures.Cactus:Clone()
						NewStructure.PrimaryPart.Position = NewBlock.Position + Vector3.new(0,3,0)
						NewStructure.Parent = ChunkModel

						for _, block in ipairs(NewStructure:GetChildren()) do
							block.Parent = ChunkModel
						end
						NewStructure:Destroy()
					end
				end
			end
		end
	end

	print(ChunksRendered)
	if ChunksRendered >= 14 then
		if WorldLoaded == false then
			WorldLoaded = true
			game.Workspace.SpawnLocation.CanCollide = false
			game.ReplicatedStorage.Events.WorldLoaded:FireServer()
			ChunksRendered = 1
		else
			ChunksRendered = 1
		end
	else
		ChunksRendered = ChunksRendered + 1
	end
end

function RenderChunksNearPlayer(Player)
	for x = 1, (ChunkRenderDistance) do
		for z = 1, (ChunkRenderDistance) do
			local PlayerPos = workspace:WaitForChild(Player.Name):WaitForChild('HumanoidRootPart').Position
			local FoundChunk = false

			local ChunkX = ((math.floor(PlayerPos.x / (ChunkSize * 3)) * (ChunkSize * 3)) + ((ChunkSize * 3) * x)) - ((ChunkSize * 3) * (ChunkRenderDistance / 1.5))
			local ChunkZ = ((math.floor(PlayerPos.z / (ChunkSize * 3)) * (ChunkSize * 3)) + ((ChunkSize * 3) * z)) - ((ChunkSize * 3) * (ChunkRenderDistance / 1.5))

			for _, Chunk in ipairs(ChunkGrid) do
				if Chunk[1] == Vector3.new(ChunkX, 0, ChunkZ) then
					FoundChunk = true
				end
			end

			if not FoundChunk then
				GenerateNewChunk(Vector3.new(ChunkX, 0, ChunkZ))
			end
		end
	end

	for ChunkIndex, Chunk in ipairs(ChunkGrid) do
		if (workspace:WaitForChild(Player.Name):WaitForChild('HumanoidRootPart').Position - Chunk[1]).Magnitude <= (ChunkSize * ChunkRenderDistance) then
			if not workspace.Chunks:FindFirstChild('Chunk '..ChunkIndex) then
				print('Render')
				RenderChunk(Chunk, ChunkIndex, Player)
			end
		else
			if workspace.Chunks:FindFirstChild('Chunk '..ChunkIndex) then
				workspace.Chunks:FindFirstChild('Chunk '..ChunkIndex):Destroy()
			end
		end
	end
end

local Player = game.Players.LocalPlayer

while task.wait() do
	RenderChunksNearPlayer(Player)
end

Please do not ask people to write entire scripts or design entire systems for you. If you can’t answer the three questions above, you should probably pick a different category.

  1. Deterministic client-side placement of generated items is possible with using Random.new(seed) to create a seeded PRNG that only your code will use.

  2. Biomes regions can be specified by sampling math.noise with one of the 3 arguments set to a constant, as you’re already doing for the height map (you have Y always 0). But you don’t get transitions for free if you just quantize math.noise to an integer per biome. You have to write your own code for how to blur the biome boundaries using some kind of 2D interpolation (like bilinear, bicubic, etc… all the usual suspects) and some custom rules, like using distance of a point from each of the nearest biomes to make “fat” boundaries where the interpolation or transition biomes occur, if you don’t want hard boundaries.

  3. Using the 3rd dimension from Perlin noise won’t give you overhangs for free, or nice cave generation. It will just extend the blobby noise upwards and make unnatural shapes. If you want nice overhanging cliffs and caves, this my friends is an art form to create algorithms for.

1 Like

3D perlin noise with overhang is very tricky. There’s really no clear cut way to approach this. What I recommend is to look online and see what other people done and reverse engineer it. You could also pull up real life photos and study them, then tweak parameters until you get stuff that look similar.


source: https://www.youtube.com/watch?v=TZFv493D7jo


source: 3D Perlin Noise Map Ridges - Graphics and GPU Programming - GameDev.net

As an example. I know what 3D perlin noise looks like (see 1st photo). I look at other map generation systems online. I see photo 2. I would look at this and notice that it looks like a combination of 2d and 3d perlin noise. The bottom half is 2d with a height map and the top looks like 3d. There must be some kind of blending technique going on. Maybe it’s just a simple additive effect? etc. etc.

Same process as above. Look online for inspiration.

th-3940623874
source: https://www.bio.miami.edu/dana/330/330F19_9.html

As an example here, I took inspiration from real life. This is a graph of precipitation and temperature and what biome it produces. It looks like all I’d have to do is generate a 2 independent Perlin noise numbers and map it to a 2d graph, which will correspond to the x and y of a biome chart.

You’re working with a lot of internal math calculations (as opposed to interacting with instances & properties). You could benefit from parallel computation and native code generation.

The Parallel Luau tutorial also demonstrates a chunk loading system.

1 Like

Adding on to the other amazing comments here, IMO structures are the most conceptually tricky part.

The trick for structures is that they need to be generated at a larger scale then each chunk, otherwise they get cut off. Two ways of doing this: decide where structures go on about a 16x16 level then flag chunks (works well if you need large structures) or for small structures (limited to 3x3 chunk size) you just search adjacent chunks to see if the code needs to expand out a structure in the adjacent chunk to add stuff that extends into the current chunk (much simpler).

I also see you’re using the third parameter of the noise for the seed, which is a good way for two dimensional terrain. One easy way you can extend this to 3 dimensions is to use the seed as an offset (e.g. noise(x, y, z+seed) should work well, since your height is probably fairly limited).

One easy way to do 3D noise terrain is to have an increasing threshold for “solidity”: as blocks increase in height, they require a larger and larger value to be solid blocks instead of air blocks. You can then tune the solidity requirement function to get results you like. (And even vary it based on other maps to create oceans, mountains, valleys, etc!)

1 Like