Help Needed: Fixing Floating Trees in Chunk Generation System

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 fix a bug in my terrain generation system where some tree models float above the ground or have parts below the ground. This issue has been persistent since I started developing the system, which is heavily inspired by the one made by Okeanskiy.

  1. What is the issue? Include screenshots / videos if possible!

The problem is that some of the trees are floating above the chunks or the ground, while others have parts buried underground. This affects all tree models. Below is an image of the issue:

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

I have searched through the Developer Hub and the Developer Forum but have not found any posts that address a similar issue. I have tried several adjustments in my script, but the problem persists.

local X, Z = 4, 4
local TERRAIN_HEIGHT_COLORS = {
	[-50] = Color3.fromRGB(31, 85, 18); 
	[-10] = Color3.fromRGB(72, 113, 58); 
	[0] = Color3.fromRGB(72, 113, 58); 
}

local BLACK_ENERGY_COLORS = {
	[1] = Color3.fromRGB(12, 17, 101); 
	[2] = Color3.fromRGB(45, 77, 128); 
	[3] = Color3.fromRGB(24, 82, 130); 
}
local WIDTH_SCALE = math.random(15, 30)
local HEIGHT_SCALE = 100
local TERRAIN_SMOOTHNESS = 20
local MIN_TREE_SPAWN_HEIGHT = -60
local MAX_TREE_SPAWN_HEIGHT = 30
local TREE_DENSITY = math.random(2.5,4)
local SEED = math.random(10000000)
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)
	return math.noise(
		(X/TERRAIN_SMOOTHNESS * chunkPosX) + x/TERRAIN_SMOOTHNESS,
		(Z/TERRAIN_SMOOTHNESS * chunkPosZ) + z/TERRAIN_SMOOTHNESS,
        SEED
	) * HEIGHT_SCALE
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 paintWedge(wedge, event)
	local wedgeHeight = wedge.Position.Y

	if event == "NormalChunk" then
		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
				local alpha = (wedgeHeight - lowerColorHeight) / (higherColorHeight - lowerColorHeight)
				local lowerColor = TERRAIN_HEIGHT_COLORS[lowerColorHeight]
				local higherColor = TERRAIN_HEIGHT_COLORS[higherColorHeight]

				color = lowerColor:lerp(higherColor, alpha)
			end
		end

		wedge.Material = Enum.Material.Grass
		wedge.Color = color
	elseif event == "DarkChunk" then
		local colorIndex = math.random(1, #BLACK_ENERGY_COLORS)
		local selectedColor = BLACK_ENERGY_COLORS[colorIndex]
		wedge.Material = Enum.Material.Neon
		wedge.Color = selectedColor
	end
end

local function getTerrainNormal(posGrid, x, z)
	-- Asegúrate de que los índices no salgan fuera de los límites del grid
	if x <= 0 or x >= X - 1 or z <= 0 or z >= Z - 1 then
		return Vector3.new(0, 1, 0)  -- Normal estándar si está en el borde
	end

	local pos = posGrid[x][z]
	local right = posGrid[x + 1][z] - pos
	local forward = posGrid[x][z + 1] - pos
	local normal = right:Cross(forward).unit

	return normal
end
function isTooClose(treePositions, x, z, minDist)
	for _, pos in pairs(treePositions) do
		if (pos.X - x)^2 + (pos.Z - z)^2 < minDist^2 then
			return true
		end
	end
	return false
end

local function getHeightAtPosition(chunkPosX, chunkPosZ, x, z)
	-- Calcular la posición exacta considerando los offsets dentro del chunk
	local exactX = chunkPosX * X * WIDTH_SCALE + x
	local exactZ = chunkPosZ * Z * WIDTH_SCALE + z
	return math.noise(
		(exactX / TERRAIN_SMOOTHNESS),
		(exactZ / TERRAIN_SMOOTHNESS),
		SEED
	) * HEIGHT_SCALE
end


local function addTrees(chunk, chunkModel, event)
	local posGrid = chunk.positionGrid
	local instances = chunk.instances
	local chunkPosX = chunk.x
	local chunkPosZ = chunk.z
	local treePositions = {} 
	local tree_Folder = nil
	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
					if event == "NormalChunk" then
						tree_Folder = game.ReplicatedStorage.Normal_PineTrees:GetChildren()
					elseif event == "DarkChunk" then
						tree_Folder = game.ReplicatedStorage.Dark_Chunk:GetChildren()
					end
					

					if #tree_Folder > 0 then
						local tree = tree_Folder[math.random(1, #tree_Folder)]:Clone()
						print("Randomly selected tree: ", tree.Name)
						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)
								)
							elseif child.Name == "Black_Leaf" then
								child.Color = Color3.fromRGB(
									0 + math.random(-10, 10),  
									0 + math.random(-10, 10),  
									200 + math.random(-50, 50) 
								)
							end
						end

							local xOffset, zOffset, attempts = 0, 0, 0
							repeat
								xOffset = math.random(-WIDTH_SCALE * X / 2, WIDTH_SCALE * X / 2)
								zOffset = math.random(-WIDTH_SCALE * Z / 2, WIDTH_SCALE * Z / 2)
								attempts = attempts + 1
							until (attempts > 50 or not isTooClose(treePositions, pos.X + xOffset, pos.Z + zOffset, 10))  -- Verificar la distancia mínima

							if attempts <= 50 then
							local OffSet = tree:GetAttribute("OffSet")

								local baseOffset = tree.PrimaryPart.Size.Y/2
								local normal = getTerrainNormal(posGrid, x, z)
                              
								local treePosition = Vector3.new(
									pos.X + xOffset,
									pos.Y + baseOffset,
									pos.Z + zOffset
								)
								
						 
								local upVector = Vector3.new(0, -1, 0)
								local angleToUp = math.acos(normal:Dot(upVector))
								local rotationAxis = upVector:Cross(normal).unit

								local treeCFrame = CFrame.new(treePosition) * CFrame.fromAxisAngle(rotationAxis, angleToUp)

								tree:SetPrimaryPartCFrame(treeCFrame)
								tree.Parent = chunkModel

								table.insert(instances, tree)
								table.insert(treePositions, treePosition)  -- Guardar la posición del árbol
							else
								warn("No se pudo colocar un árbol sin superposición después de varios intentos.")
							end
						else
							warn("No trees found in the folder.")
						end
					end
					end
		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, event)
	local chunkModel = Instance.new("Model")
	chunkModel.Name = "Chunk_" .. chunkPosX .. "_" .. chunkPosZ
	chunkModel.Parent = workspace  -- Asegúrate de asignar el parent adecuado para que sea visible y replicado
	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)
			wedgeA.Parent = chunkModel
			wedgeB.Parent = chunkModel
			wedgeC.Parent = chunkModel
			wedgeD.Parent = chunkModel
            paintWedge(wedgeA, event)
			paintWedge(wedgeB, event)
			paintWedge(wedgeC, event)
			paintWedge(wedgeD, event)
			
			table.insert(chunk.instances, wedgeA)
			table.insert(chunk.instances, wedgeB)
			table.insert(chunk.instances, wedgeC)
			table.insert(chunk.instances, wedgeD)
		end
	end
addTrees(chunk, chunkModel, event)
	return chunk
end

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

return Chunk

1 Like

Can someone help me to fix this issue?

Can someone help me to fix this issue?

This is a lot of code to read through, could you point to the part where you’re placing the trees?

1 Like

Alright, here it is:

local function addTrees(chunk, chunkModel, event)
	local posGrid = chunk.positionGrid
	local instances = chunk.instances
	local chunkPosX = chunk.x
	local chunkPosZ = chunk.z
	local treePositions = {} 
	local tree_Folder = nil
	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
					if event == "NormalChunk" then
						tree_Folder = game.ReplicatedStorage.Normal_PineTrees:GetChildren()
					elseif event == "DarkChunk" then
						tree_Folder = game.ReplicatedStorage.Dark_Chunk:GetChildren()
					end
					

					if #tree_Folder > 0 then
						local tree = tree_Folder[math.random(1, #tree_Folder)]:Clone()
						print("Randomly selected tree: ", tree.Name)
						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)
								)
							elseif child.Name == "Black_Leaf" then
								child.Color = Color3.fromRGB(
									0 + math.random(-10, 10),  
									0 + math.random(-10, 10),  
									200 + math.random(-50, 50) 
								)
							end
						end

							local xOffset, zOffset, attempts = 0, 0, 0
							repeat
								xOffset = math.random(-WIDTH_SCALE * X / 2, WIDTH_SCALE * X / 2)
								zOffset = math.random(-WIDTH_SCALE * Z / 2, WIDTH_SCALE * Z / 2)
								attempts = attempts + 1
							until (attempts > 50 or not isTooClose(treePositions, pos.X + xOffset, pos.Z + zOffset, 10))  -- Verificar la distancia mínima

							if attempts <= 50 then
							local OffSet = tree:GetAttribute("OffSet")

								local baseOffset = tree.PrimaryPart.Size.Y/2
								local normal = getTerrainNormal(posGrid, x, z)
                              
								local treePosition = Vector3.new(
									pos.X + xOffset,
									pos.Y + baseOffset,
									pos.Z + zOffset
								)
								
						 
								local upVector = Vector3.new(0, -1, 0)
								local angleToUp = math.acos(normal:Dot(upVector))
								local rotationAxis = upVector:Cross(normal).unit

								local treeCFrame = CFrame.new(treePosition) * CFrame.fromAxisAngle(rotationAxis, angleToUp)

								tree:SetPrimaryPartCFrame(treeCFrame)
								tree.Parent = chunkModel

								table.insert(instances, tree)
								table.insert(treePositions, treePosition)  -- Guardar la posición del árbol
							else
								warn("No se pudo colocar un árbol sin superposición después de varios intentos.")
							end
						else
							warn("No trees found in the folder.")
						end
					end
					end
		end
	end
end

Maybe try raycasting down from the root of the tree and if it hits the ground, adjust it to that position? would be an easy fix

1 Like

I have a question: does the solution you told me cause a lot of lag?

I think that a better solution might be to adjust the script logic with the CFrames and Vectors but I don’t know a good way of doing it.

1 Like

No, raycasting on all the trees is an o(n) where n is the number of trees operation. And you’re only raycasting once so it will have 0 impact on the game