Procedural terrain generation and weird chunk borders

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!
    Hello, i am following a tutoriial on procedural terrain generation by Sebastian Lague (of course the tutorial is for unity so i modified it to fit my style and work on roblox)

  2. What is the issue? Include screenshots / videos if possible!
    While many chunks connect ideally some have issues, i cannot find the reason why

  3. What solutions have you tried so far? Did you look for solutions on the Developer Hub?

Yes i did but couldn’t find any solution

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!

Code:

local noise = require(script.ModuleScript);
local TweenService = game:GetService("TweenService");

type TerrainType = {
	height: number,
	color: Color3,
	name: string,
	material: Enum.Material
}

-- Currently not used but might be in the future, good for debugging If anyone needs it
function generateHeightMap(mapHeight, mapWidth, noiseMap)
	local colorMap = {};
	for x = 0, mapHeight do
		colorMap[x] = {};
		for y = 0, mapWidth do
			local black = Color3.new(0, 0, 0);
			colorMap[x][y] = black:Lerp(Color3.new(1,1,1), noiseMap[x][y]);
		end
	end	
	return colorMap;
end

function generateColorMap(mapHeight, mapWidth, noiseMap, regions)
	local colorMap = {};
	for x = 0, mapHeight do
		colorMap[x] = {};
		for y = 0, mapWidth do
			local currentHeight = noiseMap[x][y];
			for i = 1, #regions do
				if currentHeight <= regions[i].height then
					colorMap[x][y] = regions[i].color;
					break;
				end
			end
		end
	end
	return colorMap;
end

function generateMaterialMap(mapHeight, mapWidth, noiseMap, regions)
	local colorMap = {};
	for x = 0, mapHeight do
		colorMap[x] = {};
		for y = 0, mapWidth do
			local currentHeight = noiseMap[x][y];
			for i = 1, #regions do
				if currentHeight <= regions[i].height then
					colorMap[x][y] = regions[i].material;
					break;
				end
			end
		end
	end
	return colorMap;
en

function getSurroundingPositions(range, position: Vector3): { [number]: Vector3 }
	local inRange = {};
	local location = position - Vector3.new(3,0,3);

	for x = 0, 6 do
		for y = 0, 6 do
			local newLocation = location + Vector3.new(x, 0 ,y);
			table.insert(inRange, newLocation);
		end
	end

	
	return inRange;
end

local players = game.Players;
local Chunk = require(script.Chunks);
local chunks = {};
local CHUNK_SIZE = 32;
local seed = 10;

function onPlayerMove(character)
	local humanoidRootPart: BasePart = character.HumanoidRootPart;
	local xPosition = math.floor(humanoidRootPart.Position.X / 160);
	local zPosition = math.floor(humanoidRootPart.Position.Z / 160);
	local chunkPosition = Vector3.new(xPosition, 0, zPosition);
	
	if not chunks[chunkPosition] then
		local chunk = Chunk.new(seed, chunkPosition, Vector3.new(32,32,32), script:GetAttribute("offset"));
		chunk:render();
		
		chunks[chunkPosition] = chunk;
		
		local surroundingPositions = getSurroundingPositions(3, chunkPosition);
		for i, position in pairs(surroundingPositions) do
			local newChunk = Chunk.new(seed, position);
			
			chunks[position] = newChunk
			newChunk:render();
		end
	end
end

players.PlayerAdded:Connect(function(player)
	player.CharacterAdded:Connect(function(character)
		local humanoid: Humanoid = character:WaitForChild("HumanoidRootPart");
		
		while true do
			wait()
			onPlayerMove(character);
		end
	end)
end)

Chunk class

Chunk = {}
Chunk.__index = Chunk
local ChunkGenerator = require(script.Parent.ModuleScript);

local regions = {
	{
		name = "Water",
		material = Enum.Material.Water,
		height = 0.3,
		color = Color3.new(0.2, 0.384314, 0.752941)
	},
	{
		name = "Sand",
		material = Enum.Material.Sand,
		height = 0.35,
		color = Color3.new(0.823529, 0.823529, 0.490196)
	},
	{
		name = "Grass",
		material = Enum.Material.Grass,
		height = 0.55,
		color = Color3.new(0.341176, 0.592157, 0.0823529)
	},
	{
		name = "Grass2",
		height = 0.6,
		material = Enum.Material.LeafyGrass,
		color = Color3.new(0.243137, 0.419608, 0.0745098)
	},
	{
		name = "Rock",
		material = Enum.Material.Asphalt,
		height = 0.7,
		color = Color3.new(0.356863, 0.262745, 0.227451)
	},
	{
		name = "Rock2",
		height = 0.9,
		material = Enum.Material.Rock,
		color = Color3.new(0.278431, 0.223529, 0.215686)
	},
	{
		name = "Snow",
		material = Enum.Material.Snow,
		height = 1,
		color = Color3.new(1, 1, 1)
	}
}

function getHeightMultiplier(heightMultiplier: NumberSequence, _time: number)
	local keypoints = heightMultiplier.Keypoints;
	if _time == 0 then
		return keypoints[1].Value;
	end
	if _time == 1 then
		return keypoints[#keypoints].Value;
	end

	for i, keypoint in pairs(keypoints) do
		local _next = keypoints[i + 1];

		if _time >= keypoint.Time and _time < _next.Time then
			local alpha = (_time - keypoint.Time) / (_next.Time - keypoint.Time);

			return (_next.Value - keypoint.Value) * alpha + keypoint.Value
		end
	end

end

function generateMaterialMap(mapHeight, mapWidth, noiseMap, regions)
	local colorMap = {};
	for x = 1, mapHeight do
		colorMap[x] = {};
		for y = 1, mapWidth do
			local currentHeight = noiseMap[x][y];
			for i = 1, #regions do
				if currentHeight <= regions[i].height then
					colorMap[x][y] = regions[i].material;
					break;
				end
			end
		end
	end
	return colorMap;
end

function Chunk.new(seed: number, location: Vector3, size: Vector3?, offset)
	local self = {}
	local size = size or Vector3.new(32,32,32);
	setmetatable(self, Chunk)
	
	self.seed = seed;
	self.chunkLocation = location;
	self.chunkSize = size;
	self.offset = offset;
	
	return self
end


function Chunk:render()
	local heightMultiplier = script.Parent:GetAttribute("heightMultiplier");
	local noiseMap = ChunkGenerator.GenerateNoiseMap(32, 32, self.seed, 27.6, 4, 0.5, 1.87, Vector2.new(0,0), Vector2.new(self.chunkLocation.X * 32, self.chunkLocation.Z * 32));
	
	local materialMap = generateMaterialMap(32, 32, noiseMap, regions);
	 
	
	for x = 1, 32 do
		for y = 1, 32 do
			local height = noiseMap[x][y];
			

			local size = Vector3.new(5,height * getHeightMultiplier(heightMultiplier, height) * 100 + 1,5);
			local position = CFrame.new((x * 5) + self.chunkLocation.X * 160, 0, (y * 5) + self.chunkLocation.Z * 160);

			workspace.Terrain:FillBlock(position, size, materialMap[x][y])
		end
	end
end

return Chunk

ChunkGenerator

local module = {}

function module.perlinNoise(x, y)
	local clampedNoise = math.clamp(math.noise(x, y), -1, 1);
	--return (clampedNoise + 1) / 2
	return clampedNoise;
end

function InverseLerp(from, to, value)
	if from < to then      
		if value < from then 
			return 0
		end

		if value > to then      
			return 1
		end

		value = value - from
		value = value/(to - from)
		return value
	end

	if from <= to then
		return 0
	end

	if value < to then
		return 1
	end

	if value > from then
		return 0
	end

	return 1.0 - ((value - to) / (from - to))
end

function module.GenerateNoiseMap(mapWidth: number, mapHeight: number, seed: number, scale: number, octaves: number, persistance: number, lacunarity: number, offset: Vector2, realOffset: Vector2)
	local prng = Random.new(seed);
	local noiseMap = {};
	local octaveOffsets = {};
	
	for i = 1, octaves do
		local offsetX = prng:NextInteger(-100000, 100000) + offset.X;
		local offsetY = prng:NextInteger(-100000, 100000) + offset.Y;
		
		octaveOffsets[i] = Vector2.new(offsetX, offsetY);
	end
	
	if scale <= 0 then
		scale = 0.0001;
	end

	local maxNoiseHeight = -3.402823E+38;
	local minNoiseHeight = 3.402823E+38;
	
	local halfWidth = mapWidth / 2;
	local halfHeight = mapHeight / 2;
	
	for x = 1, mapHeight do
		noiseMap[x] = {};
		for y = 1, mapWidth do

			local amplitude = 1;
			local frequency = 1;
			local noiseHeight = 0;

			for i = 1, octaves do
				local realX = x + realOffset.X;
				local realY = y + realOffset.Y;
				local sampleX = (realX - halfWidth) / scale * frequency + octaveOffsets[i].X;
				local sampleY = (realY - halfHeight) / scale * frequency + octaveOffsets[i].Y;

				local perlinValue = module.perlinNoise(sampleX, sampleY);

				noiseHeight += perlinValue * amplitude;

				amplitude *= persistance;
				frequency *= lacunarity;
			end

			if noiseHeight > maxNoiseHeight then
				maxNoiseHeight = noiseHeight;
			else if noiseHeight < minNoiseHeight then
					minNoiseHeight = noiseHeight;
				end
			end

			noiseMap[x][y] = noiseHeight;
		end
	end

	for x = 1, mapHeight do
		for y = 1, mapWidth do
			noiseMap[x][y] = InverseLerp(minNoiseHeight, maxNoiseHeight, noiseMap[x][y]);
		end
	end

	return noiseMap;
end

return module