How could I apply Falloff Map to this Chunk Generation System?

Hey there! So, I want to make a terrain generation with the falloff filter. and yes, i have seen this post before and it sadly couldn’t help me,

also a border. Ever seen Muck?
It generates a random terrain and sure, it doesn’t have a border but im pretty sure it has a inf amount of water which acts like a “border” because you drown in the water.

Muck also uses falloff map:


(Screenshot from dani’s video about the making of Muck)

How would i achieve both this?

Thanks!!

Almost forgot to send code :sweat_smile:

Module script named “Chunk”

local TERRAIN_HEIGHT_COLORS = {
	[-50] = Enum.Material.Sand;
	[-10] = Enum.Material.Grass;
	[0] = Enum.Material.Grass; -- grassy green
	[75] = Enum.Material.Rock; -- stone grey mountain
}
local X, Z = 4, 4
local WIDTH_SCALE = 15
local HEIGHT_SCALE = 100
local TERRAIN_SMOOTHNESS = 20
local MIN_TREE_SPAWN_HEIGHT = -15
local MAX_TREE_SPAWN_HEIGHT = 30
local TREE_DENSITY = 0.85
local SEED = workspace.SEED.Value

local wedge = Instance.new("WedgePart");
wedge.Anchored = true;
wedge.TopSurface = Enum.SurfaceType.Smooth;
wedge.BottomSurface = Enum.SurfaceType.Smooth;

local function draw3dTriangle(a, b, c)
	local ab, ac, bc = b - a, c - a, c - b;
	local abd, acd, bcd = ab:Dot(ab), ac:Dot(ac), bc:Dot(bc);

	if (abd > acd and abd > bcd) then
		c, a = a, c;
	elseif (acd > bcd and acd > abd) then
		a, b = b, a;
	end

	ab, ac, bc = b - a, c - a, c - b;

	local right = ac:Cross(ab).unit;
	local up = bc:Cross(right).unit;
	local back = bc.unit;

	local height = math.abs(ab:Dot(up));

	local w1 = wedge:Clone();
	w1.Size = Vector3.new(0, height, math.abs(ab:Dot(back)));
	w1.CFrame = CFrame.fromMatrix((a + b)/2, right, up, back);
	w1.Parent = workspace;

	local w2 = wedge:Clone();
	w2.Size = Vector3.new(0, height, math.abs(ac:Dot(back)));
	w2.CFrame = CFrame.fromMatrix((a + c)/2, -right, up, -back);
	w2.Parent = workspace;

	return w1, w2;
end

local function getHeight(chunkPosX, chunkPosZ, x, z)
	local height = math.noise(
		(X/TERRAIN_SMOOTHNESS * chunkPosX) + x/TERRAIN_SMOOTHNESS,
		(Z/TERRAIN_SMOOTHNESS * chunkPosZ) + z/TERRAIN_SMOOTHNESS,
		SEED
	) * HEIGHT_SCALE

	if height > 20 then
		local difference = height - 20
		height += (difference * 1.2)
	end

	if height < -20 then
		local difference = height - -20
		height += (difference * 1.2)
	end

	return height
end

local function getPosition(chunkPosX, chunkPosZ, x, z)
	return Vector3.new(
		chunkPosX*X*WIDTH_SCALE + x*WIDTH_SCALE,
		getHeight(chunkPosX, chunkPosZ, x, z),
		chunkPosZ*Z*WIDTH_SCALE + z*WIDTH_SCALE
	)
end

local function getMaterial(wedge)
	local wedgeHeight = wedge.Position.Y

	local color
	local lowerColorHeight
	local higherColorHeight

	for height, heightColor in pairs(TERRAIN_HEIGHT_COLORS) do
		if wedgeHeight == height then
			color = heightColor
			break
		end

		if (wedgeHeight < height) and (not higherColorHeight or height < higherColorHeight) then
			higherColorHeight = height
		end

		if (wedgeHeight > height) and (not lowerColorHeight or height > lowerColorHeight) then
			lowerColorHeight = height
		end
	end

	if not color then
		if higherColorHeight == nil then
			color = TERRAIN_HEIGHT_COLORS[lowerColorHeight]
		elseif lowerColorHeight == nil then
			color = TERRAIN_HEIGHT_COLORS[higherColorHeight]
		else
			color = Enum.Material.Grass
		end
	end

	return color
end

local function addWater(chunk)
	local cframe = CFrame.new(
		(chunk.x + .5) * chunk.WIDTH_SIZE_X,
		-70,
		(chunk.z + .5) * chunk.WIDTH_SIZE_Z
	)

	local size = Vector3.new(
		chunk.WIDTH_SIZE_X,
		90,
		chunk.WIDTH_SIZE_Z
	)

	workspace.Terrain:FillBlock(cframe, size, Enum.Material.Water)

	chunk.waterCFrame = cframe
	chunk.waterSize = size
end

local function addTrees(chunk)
	local posGrid = chunk.positionGrid
	local instances = chunk.instances
	local chunkPosX = chunk.x
	local chunkPosZ = chunk.z

	for x = 0, X-1 do
		for z = 0, Z-1 do
			local pos = posGrid[x][z]			

			if pos.Y >= MIN_TREE_SPAWN_HEIGHT and pos.Y <= MAX_TREE_SPAWN_HEIGHT then

				math.randomseed(x * (chunkPosX+SEED) + z * (chunkPosZ+SEED))
				if math.random() < TREE_DENSITY then
					local tree = game.ReplicatedStorage.Tree:Clone()

					for index, child in pairs(tree:GetChildren()) do
						if child.Name == "Leaf" then
							child.Color = Color3.fromRGB(
								75 + math.random(-25, 25),
								151 + math.random(-25, 25),
								75 + math.random(-25, 25)
							)
						end
					end

					local cframe = CFrame.new(pos)
						* CFrame.new(
							math.random() * math.random(-10, 10),
							10,
							math.random() * math.random(-10, 10)
						)
						* CFrame.Angles(0, 2 * math.pi * math.random(), 0)

					tree:SetPrimaryPartCFrame(cframe)
					tree.Parent = workspace

					table.insert(instances, tree)
				end

			end
		end
	end
end

local function convert(chunk)
	local instances = chunk.instances

	for i,v in pairs(instances) do
		if v.Name == "Wedge" then
			local Material = getMaterial(v)

			workspace.Terrain:FillWedge(v.CFrame,v.Size+Vector3.new(20,20,20),Material)
			v:Destroy()
		end
	end
end

local Chunk = {}
Chunk.__index = Chunk

Chunk.WIDTH_SIZE_X = X * WIDTH_SCALE
Chunk.WIDTH_SIZE_Z = Z * WIDTH_SCALE

function Chunk.new(chunkPosX, chunkPosZ)
	local chunk = {
		instances = {};
		positionGrid = {};
		x = chunkPosX;
		z = chunkPosZ;
	}

	setmetatable(chunk, Chunk)

	local positionGrid = chunk.positionGrid

	for x = 0, X do
		positionGrid[x] = {}

		for z = 0, Z do
			positionGrid[x][z] = getPosition(chunkPosX, chunkPosZ, x, z)
		end
	end

	for x = 0, X-1 do
		for z = 0, Z-1 do			
			local a = positionGrid[x][z]
			local b = positionGrid[x+1][z]
			local c = positionGrid[x][z+1]
			local d = positionGrid[x+1][z+1]

			local wedgeA, wedgeB = draw3dTriangle(a, b, c)
			local wedgeC, wedgeD = draw3dTriangle(b, c, d)

			table.insert(chunk.instances, wedgeA)
			table.insert(chunk.instances, wedgeB)
			table.insert(chunk.instances, wedgeC)
			table.insert(chunk.instances, wedgeD)
		end
	end

	addWater(chunk)
	addTrees(chunk)
	convert(chunk)

	return chunk
end

function Chunk:Destroy()
	for index, instance in ipairs(self.instances) do
		instance:Destroy()
	end

	workspace.Terrain:FillBlock(self.waterCFrame, self.waterSize, Enum.Material.Air)
end

return Chunk

TL; DR: use a distance function, the one in the post you sent has an example, the two links I added below should have more detail:

Only one source? A good programmer should always refer to multiple sources of information. Here are two more I found which also use the same method but have a lot more detail with pictures and stuff:

These two sources also use the same method a distance function, in the post above it looks like this:

This is the distance away from the center of the map and is a distance function.

I’ll let Red blob games explain it further in detail:

Red blob games islands section:

https://www.redblobgames.com/maps/terrain-from-noise/#islands

How does this work? There are two ingredients :

  1. A distance function assigns a distance to every position on the map, from 0 at the center to 1 at the border.
  2. A shaping function (as used in the Redistribution section) takes an elevation as input and chooses a new output elevation.

At the center of the map (distance 0), we’ll use a shaping function that always outputs land . At the border of the map (distance 1), we’ll use a shaping function that always outputs water . In between, we’ll allow both land and water. For simplicity, I’ll assume the water level is 0.5, so ≥0.5 means land and <0.5 means water.

This blog talks about an “Island Mask” which also uses a distance function.

The simplest mask I like to use is a quadratic distance function from the center of the island.

https://shanee.io/blog/2015/09/25/procedural-island-generation/

1 Like

Im actually unsurprisingly lost, can you explain better since i didn’t really understand anything.

I also have absolutely no idea on how i would implement this

Sorry about that, im not too advanced into terrain generation and im quite interested in this, so if you could help me that would be awesome!

Honestly with implementation with your system it’s way harder than usual because your system has these constants built in.

Height range
[-HEIGHT_SCALE, HEIGHT_SCALE]
[-100, 100]
Water level range
[-100, -70]

The problem is that red blob games uses a range which assumes the following
[0, 1] height range
[0, 0.5] water range

Converting between these two is difficult but possible. Without conversion the multiplication done in the second step will not work at all.

Extensive use of the map function was used

Code:

local TERRAIN_HEIGHT_COLORS = {
	[-50] = Enum.Material.Sand;
	[-10] = Enum.Material.Grass;
	[0] = Enum.Material.Grass; -- grassy green
	[75] = Enum.Material.Rock; -- stone grey mountain
}
local X, Z = 4, 4
local WIDTH_SCALE = 15
local HEIGHT_SCALE = 100
local TERRAIN_SMOOTHNESS = 20
local MIN_TREE_SPAWN_HEIGHT = -15
local MAX_TREE_SPAWN_HEIGHT = 30
local TREE_DENSITY = 0.85
local SEED = math.random(1, 1000)

local wedge = Instance.new("WedgePart");
wedge.Anchored = true;
wedge.TopSurface = Enum.SurfaceType.Smooth;
wedge.BottomSurface = Enum.SurfaceType.Smooth;

local function draw3dTriangle(a, b, c)
	local ab, ac, bc = b - a, c - a, c - b;
	local abd, acd, bcd = ab:Dot(ab), ac:Dot(ac), bc:Dot(bc);

	if (abd > acd and abd > bcd) then
		c, a = a, c;
	elseif (acd > bcd and acd > abd) then
		a, b = b, a;
	end

	ab, ac, bc = b - a, c - a, c - b;

	local right = ac:Cross(ab).unit;
	local up = bc:Cross(right).unit;
	local back = bc.unit;

	local height = math.abs(ab:Dot(up));

	local w1 = wedge:Clone();
	w1.Size = Vector3.new(0, height, math.abs(ab:Dot(back)));
	w1.CFrame = CFrame.fromMatrix((a + b)/2, right, up, back);
	w1.Parent = workspace;

	local w2 = wedge:Clone();
	w2.Size = Vector3.new(0, height, math.abs(ac:Dot(back)));
	w2.CFrame = CFrame.fromMatrix((a + c)/2, -right, up, -back);
	w2.Parent = workspace;

	return w1, w2;
end

local function getXZPosition(chunkPosX, chunkPosZ, x, z)
	return Vector3.new(
		chunkPosX*X*WIDTH_SCALE + x*WIDTH_SCALE,
		0,
		chunkPosZ*Z*WIDTH_SCALE + z*WIDTH_SCALE
	)
end
-- -70 is water range
-- Height scale -100, 100
--there fore at -0.7 it is border
local maxDistance = 0 --for debugging purposes


local BORDER_DISTANCE = 750 -- units is in studs, away from center of the map

-- Maps a number from one range to another:
-- [in_min, in_max] -> [out_min, out_max]
local function map(x, in_min, in_max, out_min, out_max)
	return out_min + (x - in_min)*(out_max - out_min)/(in_max - in_min)
end

local function getHeight(chunkPosX, chunkPosZ, x, z)
	
	--Step 1 obtain distance as a range 0 at center of the map 1 at the edge of the map
	local CENTER_OF_MAP = Vector3.zero
	local distanceFromCenterStuds = (CENTER_OF_MAP - getXZPosition(chunkPosX, chunkPosZ, x, z)).Magnitude
	
	local distanceFromCenterBounded = map(distanceFromCenterStuds, 0, BORDER_DISTANCE, 0, 1)
	
	--maxDistance = math.max(maxDistance,distanceFromCenterBounded)
	--print(maxDistance)

	--Step 2 apply shape function to apply to height
	
	local noiseResult = math.noise(
		(X/TERRAIN_SMOOTHNESS * chunkPosX) + x/TERRAIN_SMOOTHNESS,
		(Z/TERRAIN_SMOOTHNESS * chunkPosZ) + z/TERRAIN_SMOOTHNESS,
		SEED
	)
	noiseResult = math.clamp(noiseResult, -1, 1)
	
	--Transform height from [-1, 1] to [0, 1]
	noiseResult = map(noiseResult, -1, 1, 0, 1)
	
	--Apply shape function [0,1] height
--Multiplication is from procedural island generation blog
-- at distance 0 center of island (1-distance) will equal 1 so noise is unchanged
--at border distance it will become (1-distance) will equal 0 so it forces it to be sea
	noiseResult *= 1 - distanceFromCenterBounded
	
	--Transform the noise result to your system range from [0, 1] to [-1, 1]
	noiseResult = map(noiseResult, 0, 1, -1, 1)
	
	local height =  noiseResult* HEIGHT_SCALE
		
	return height
end

local function getPosition(chunkPosX, chunkPosZ, x, z)
	return Vector3.new(
		chunkPosX*X*WIDTH_SCALE + x*WIDTH_SCALE,
		getHeight(chunkPosX, chunkPosZ, x, z),
		chunkPosZ*Z*WIDTH_SCALE + z*WIDTH_SCALE
	)
end

local function getMaterial(wedge)
	local wedgeHeight = wedge.Position.Y

	local color
	local lowerColorHeight
	local higherColorHeight

	for height, heightColor in pairs(TERRAIN_HEIGHT_COLORS) do
		if wedgeHeight == height then
			color = heightColor
			break
		end

		if (wedgeHeight < height) and (not higherColorHeight or height < higherColorHeight) then
			higherColorHeight = height
		end

		if (wedgeHeight > height) and (not lowerColorHeight or height > lowerColorHeight) then
			lowerColorHeight = height
		end
	end

	if not color then
		if higherColorHeight == nil then
			color = TERRAIN_HEIGHT_COLORS[lowerColorHeight]
		elseif lowerColorHeight == nil then
			color = TERRAIN_HEIGHT_COLORS[higherColorHeight]
		else
			color = Enum.Material.Grass
		end
	end

	return color
end

local function addWater(chunk)
	local cframe = CFrame.new(
		(chunk.x + .5) * chunk.WIDTH_SIZE_X,
		-70,
		(chunk.z + .5) * chunk.WIDTH_SIZE_Z
	)

	local size = Vector3.new(
		chunk.WIDTH_SIZE_X,
		90,
		chunk.WIDTH_SIZE_Z
	)

	workspace.Terrain:FillBlock(cframe, size, Enum.Material.Water)

	chunk.waterCFrame = cframe
	chunk.waterSize = size
end

local function addTrees(chunk)
	--local posGrid = chunk.positionGrid
	--local instances = chunk.instances
	--local chunkPosX = chunk.x
	--local chunkPosZ = chunk.z

	--for x = 0, X-1 do
	--	for z = 0, Z-1 do
	--		local pos = posGrid[x][z]			

	--		if pos.Y >= MIN_TREE_SPAWN_HEIGHT and pos.Y <= MAX_TREE_SPAWN_HEIGHT then

	--			math.randomseed(x * (chunkPosX+SEED) + z * (chunkPosZ+SEED))
	--			if math.random() < TREE_DENSITY then
	--				local tree = game.ReplicatedStorage.Tree:Clone()

	--				for index, child in pairs(tree:GetChildren()) do
	--					if child.Name == "Leaf" then
	--						child.Color = Color3.fromRGB(
	--							75 + math.random(-25, 25),
	--							151 + math.random(-25, 25),
	--							75 + math.random(-25, 25)
	--						)
	--					end
	--				end

	--				local cframe = CFrame.new(pos)
	--					* CFrame.new(
	--						math.random() * math.random(-10, 10),
	--						10,
	--						math.random() * math.random(-10, 10)
	--					)
	--					* CFrame.Angles(0, 2 * math.pi * math.random(), 0)

	--				tree:SetPrimaryPartCFrame(cframe)
	--				tree.Parent = workspace

	--				table.insert(instances, tree)
	--			end

	--		end
	--	end
	--end
end

local function convert(chunk)
	local instances = chunk.instances

	for i,v in pairs(instances) do
		if v.Name == "Wedge" then
			local Material = getMaterial(v)

			workspace.Terrain:FillWedge(v.CFrame,v.Size+Vector3.new(20,20,20),Material)
			v:Destroy()
		end
	end
end

local Chunk = {}
Chunk.__index = Chunk

Chunk.WIDTH_SIZE_X = X * WIDTH_SCALE
Chunk.WIDTH_SIZE_Z = Z * WIDTH_SCALE

function Chunk.new(chunkPosX, chunkPosZ)
	local chunk = {
		instances = {};
		positionGrid = {};
		x = chunkPosX;
		z = chunkPosZ;
	}

	setmetatable(chunk, Chunk)

	local positionGrid = chunk.positionGrid

	for x = 0, X do
		positionGrid[x] = {}

		for z = 0, Z do
			positionGrid[x][z] = getPosition(chunkPosX, chunkPosZ, x, z)
		end
	end

	for x = 0, X-1 do
		for z = 0, Z-1 do			
			local a = positionGrid[x][z]
			local b = positionGrid[x+1][z]
			local c = positionGrid[x][z+1]
			local d = positionGrid[x+1][z+1]

			local wedgeA, wedgeB = draw3dTriangle(a, b, c)
			local wedgeC, wedgeD = draw3dTriangle(b, c, d)

			table.insert(chunk.instances, wedgeA)
			table.insert(chunk.instances, wedgeB)
			table.insert(chunk.instances, wedgeC)
			table.insert(chunk.instances, wedgeD)
		end
	end

	addWater(chunk)
	addTrees(chunk)
	convert(chunk)

	return chunk
end

function Chunk:Destroy()
	for index, instance in ipairs(self.instances) do
		instance:Destroy()
	end

	workspace.Terrain:FillBlock(self.waterCFrame, self.waterSize, Enum.Material.Air)
end


for x=-10, 10 do
	for y=-10, 10 do
		Chunk.new(x,y)	
	end
end
1 Like

Amazing is all i can say!

Wow, im speechless, anyway, thank you!

It was very hard for me to configure since as you said, the converting is difficult, gonna go through the code and hopefully learn something from this!

Again, Thank You!!

1 Like