Procedural Terrain Generation (Parts)

Terrain is pretty cool, and procedural terrain is even cooler, but it can cause performance issues especially when using parts so I’m trying to make it perform well so I can use it without worrying too much about how it will effect gameplay

Here is a gif using the generator I created

this is rather small for what I need and it already has 3600 parts

I could make the parts bigger and the amount of parts lower like so

but then it starts to look less appealing

I have an idea that could drastically reduce part count by making all rows of the same part a single longer part shown below (red lines would be connected parts)

but I’m still not sure if it will be viable then

is there another method of making terrain like this perform better? I don’t know but it might be necessary to swap to actual roblox terrain, but then loss of quality would be pretty high and isn’t really the style I’m going for

Streaming Enabled is an option roblox is also adding level of detail features and improved Streaming

Code

Module


local RunService = game:GetService("RunService")

local Part = Instance.new("Part")
Part.Anchored = true
Part.Color = Color3.fromRGB(255, 255, 255)
Part.Name = "TerrainCell"

local TerrainHeights = {
	["Water"] = {Height = -1020, Material = Enum.Material.Plastic, Color = Color3.fromRGB(4, 175, 236), Transparency = 0,  IncreasedHeight = 0},
	--["Sand"] = {Height = -50, Material = Enum.Material.Plastic, Color = Color3.fromRGB(248, 217, 109), Transparency = 0, IncreasedHeight = 1},
	["Grass"] = {Height = -60, Material = Enum.Material.Plastic, Color = Color3.fromRGB(91, 154, 76), Transparency = 0, IncreasedHeight = 1},
	["DarkGrass"] = {Height = 30, Material = Enum.Material.Plastic, Color = Color3.fromRGB(39, 70, 45), Transparency = 0, IncreasedHeight = 2},
	["Stone"] = {Height = 45, Material = Enum.Material.Plastic, Color = Color3.fromRGB(163, 162, 165), Transparency = 0, IncreasedHeight = 200},
	["DarkStone"] = {Height = 55, Material = Enum.Material.Plastic, Color = Color3.fromRGB(99, 95, 98), Transparency = 0, IncreasedHeight = 300},
	
}

local TerrainHeightsList = {
	TerrainHeights["Water"], 
	--TerrainHeights["Sand"], 
	TerrainHeights["Grass"], 
	TerrainHeights["DarkGrass"], 
	TerrainHeights["Stone"],
	TerrainHeights["DarkStone"]

}

local TerrainModule = {}

function TerrainModule.TerrainPolisher(TerrainTable)
	local MaxY = -100
	
	for i, v in pairs(TerrainTable) do
		if v[1] > MaxY then MaxY = v[1] end
	end
	
	for i, v in pairs(TerrainTable) do
		local Y = math.floor((v[1] / MaxY) * 100) 
		local Type
		
		for i, v in pairs(TerrainHeightsList) do
			if Y > v.Height then
				Type = v
			end
		end
		
		local function SetProperties()
			v[2].Material = Type.Material
			v[2].Color = Type.Color
			v[2].Transparency = Type.Transparency
			
			v[2].Size = v[2].Size + Vector3.new(0, Type.IncreasedHeight, 0)
			v[2].Position = v[2].Position + Vector3.new(0, Type.IncreasedHeight / 2, 0)
		end
		
		
		SetProperties()
	end
	
end


function TerrainModule.GenTerrain(Size, CellSize, Power, Frequency)
	local Seed = math.random(1, 10e6)
	local TerrainList = {}
	--[[
		Size is the amount of Cells
		CellSize is the Size of a Cell
		Power is the magnitude of how high or low the noise map can go
		Frequency is how compact the noise map is
	--]]
	for X = 1, Size do
		for Z = 1, Size do
			
			local Y = math.noise((X * Frequency) / Size, (Z * Frequency) / Size, Seed) * Power
			
			local TerrinCell = Part:Clone()
			TerrinCell.Size = Vector3.new(CellSize, CellSize, CellSize)
			TerrinCell.Position = Vector3.new((X * CellSize), 5, (Z * CellSize))
			TerrinCell.Parent = workspace
			table.insert(TerrainList, {Y, TerrinCell})
			
			
		end
		RunService.Heartbeat:Wait()
	end
	TerrainModule.TerrainPolisher(TerrainList)
end

return TerrainModule

Script

local TerrainModule = require(game.ServerScriptService.TerrainModule)


local Size, CellSize, Power, Frequency = 40, 50, 45, 4

TerrainModule.GenTerrain(Size, CellSize, Power, Frequency)

I wasn’t sure if this should go in Scripting Support or Code Review because it is a bit of both

6 Likes

One thing you could consider is using pre-constructed buildings and walls that are made out of unions instead of generating the buildings from scratch block by block.
The blocky style you’re going for also makes the block count increase, you could consider using triangles for terrain, instead. For further small optimization, you can make the water 1 block (assuming everything is at the same level).

I think if you switch to pre-fab buildings made out of only a couple of unions, you can dramatically decrease the part count.

I’d recommend then meshing the Unions, as they are more performance-friendly iirc.

You are using a lot of unnecessary parts. In its current implementation, a 256x256x4 layer of grass would take 4096 4x4x4 parts to draw, when you only need one. Instead, apply all the height and polishing modifications before creating the parts. Then, use a compression algorithm to fill connected parts of the same color with as few parts as possible.

I’m not on my computer rn to draw a graphic of how that compression algorithm might work, but here’s a 2D version from my Twitter that should get you started. https://twitter.com/AstroCodeRBLX/status/907100484967825408?s=19

EDIT: Apparently I didn’t read your post well enough. You’re on the right track with combining similar parts in the same line! Ideally, you’ll want to do that in all 3 dimensions.

1 Like

I’m not quite sure how to implement a compression algorithm, ill try to figure it out when I wake up

I made a simple compression algorithm

local PartsFolder = game.Workspace:WaitForChild("Parts")
local PartsFolderCount = #PartsFolder:GetChildren()



local i = 0
repeat i = i + 1
	
if PartsFolder[i].Color == PartsFolder[i + 1].Color then
	
	PartsFolder[i + 1].Size = PartsFolder[i].Size + (Vector3.new(PartsFolder[i + 1].Size.Z, 0, 0))
	PartsFolder[i + 1].Position = PartsFolder[i].Position + Vector3.new(5 / 2, 0, 0)
	PartsFolder[i]:Destroy()
	
end

until i == PartsFolderCount - 1

I didn’t apply it to the generator yet doing that now, is this efficient or should I go a different route?

5 Likes

This reminds me of trying to combine voxelized terrain into convex surfaces to be made into a navigation mesh, but in 3D. To summarize it, finding the optimal way to combine parts to minimize the number of parts used could be reduced to a NP-hard optimization problem. The best method is probably to use an approximate solution in linear time, like mentioned above.

I’ll start by assuming that the map generated is a height map with special labels for the type of cell. For every cell at random, perform a check to see if there are any cells adjacent (so perform a randomized stencil operation) that satisfy the following properties:

  1. They are relatively the same height (+/- a tolerance)
  2. They are the same label/type of cell
  3. Are not a part of a previously made group (to be explained later)

If so, group them. Continue in that direction, checking if the next cell can be merged into the group. if it reaches a cell of another group, stop. The next action will guarentee that any encountered group cannot be merged. Next, we will try to expand this group any of the other three possible directions. When expanding orthogonal to a direction already expanded in, a row of cells will need to be checked for the above properties instead of a single cell at a time. If so, the row is added to the group and a plane of grouped parts is formed. Repeat this until every cell belongs to a group.

This will perform a linear amount of checks up to a maximum of 4 * the number of cells. To obtain a slightly better map you could attempt to resize groups. You could also decide to allow smaller cells to form groups under cells with more height. To do this, you would need to keep track of multiple groups per a cell, with the start/stop heights for each group range. This would allow the water as pictured above to be a single part, and the whole map in roughly two hundred or so parts. This is much more complicated, so I’d only try to implement it only if performance remains an issue after the simpler compression this builds on. It still wont form an optimal map, but it will be much, much better.

4 Likes

that’s kind of tricky to do because you can’t simply just group them as parts have to be exact rectangles or squares

like this

image

and cant be like this without using multiple parts

image

I might just not understand fully though

Edit: and also the grass and water always stay at the same level so I can just make bigger parts under the other cells

Right, that is why when expanding a group in the first direction you will be checking one cell at a time before you add it, but when you expand a direction orthogonal to a direction you have expanded before (after going left, going up is orthogonal) then you need to check a whole row of cells before you add them all to the group. In this way, the resulting shape will always be a rectangle.

2 Likes

Using your idea about putting smaller cells under larger cells and using your method to combine parts(would be the mountains and such) I think the map could be viable thanks :slightly_smiling_face:

1 Like