Large-Scale Roblox Terrain: The ultimate guide

I didn’t mean replace the entirety of @Vexture 's code with just my FillWedge.
I meant replace only the terrain:FillWedge part of his code with my custom function + use exact wedge size rather than adding on 15. That way you get the accurate fillwedge + the painting (based on steepness) that you desire. This is the code if I add in those edits:


local rate = .00001
local RunService = game:GetService("RunService")
local whitelist = workspace.Mesh:GetChildren()
local thickness = nil --No need to set this anymore. Terrain thickness will just depend on each wedgePart.Size.X.
local sea_level = .125 --From 0 to 1, where 0 is the lowest elevation of the map and 1 is the highest (gets denormalized in terms of the relative elevations of your map automatically)
local regionIncrement = 1024

function FillWedge(wedgeCFrame, wedgeSize, material) --Custom FillWedge with advantages over terrain:FillWedge
	local terrain = workspace.Terrain
	local Zlen, Ylen = wedgeSize.Z, wedgeSize.Y
	local longerSide, shorterSide, isZlonger
	if Zlen > Ylen then
		longerSide, shorterSide, isZlonger = Zlen, Ylen, true
	else
		longerSide, shorterSide, isZlonger = Ylen, Zlen, false
	end
	
	local closestIntDivisor = math.max(1, math.floor(shorterSide/3))
	local closestQuotient = shorterSide/closestIntDivisor
	local scaledLength = closestQuotient*longerSide/shorterSide    
	local cornerPos = Vector3.new(0, -Ylen, Zlen)/2
	
	for i = 1, closestIntDivisor - 1 do
		local longest_baselen = (closestIntDivisor-i)*scaledLength
		local size, cf = Vector3.new(math.max(3, wedgeSize.X), closestQuotient, longest_baselen)
		if isZlonger then
			cf = wedgeCFrame:toWorldSpace(CFrame.new(cornerPos) + Vector3.new(0, (i-0.5)*closestQuotient, -longest_baselen/2))
		else
			cf = wedgeCFrame:toWorldSpace(CFrame.Angles(math.pi/2, 0, 0) + cornerPos + Vector3.new(0, longest_baselen/2, -(i-0.5)*closestQuotient))
		end
		terrain:FillBlock(cf, size, material)
	end
	
	local diagSize = Vector3.new(math.max(3, wedgeSize.X), closestQuotient*scaledLength/math.sqrt(closestQuotient^2 + scaledLength^2), math.sqrt(Zlen^2 + Ylen^2)) --Vector3.new(3, 3, math.sqrt(Zlen^2 + Ylen^2))
	local rv, bv = wedgeCFrame.RightVector, -(Zlen*wedgeCFrame.LookVector - Ylen*wedgeCFrame.UpVector).Unit
	local uv = bv:Cross(rv).Unit
	local diagPos = wedgeCFrame.p - uv*diagSize.Y/2
	local diagCf = CFrame.fromMatrix(diagPos, rv, uv, bv)
	terrain:FillBlock(diagCf, diagSize, material)
end

local function TimerWait(duration) --Framerate-dependent function that waits at the theoretically lowest possible step, Heartbeat:Wait(), until some time dt has passed.
	local start = tick()
	repeat RunService.Heartbeat:Wait() until tick() - start >= duration
	return true
end

local function MinMaxAvg(data)
	local min, max, average = data[1], data[#data], 0
	for _, element in pairs(data) do
		if element < min then
			min = element
		end
		if element > max then
			max = element
		end
		average = average + element
	end
	average = average / #data
	
	return min, max, average
end

local function Normalize(Min, Max, Val)
	local Normal = (Val - Min) / (Max - Min)
	return Normal
end

local function Denormalize(Min, Max, Val)
	local Denormal = (Val * (Max - Min)) + Min
	return Denormal
end

local function ConvertTerrain()
	local data = {}
	
	for _, part in pairs(whitelist) do
		table.insert(data, part.Position.Y)
		if tick() % 5 > 4.5 then
			TimerWait(rate)
		end
	end
	
	local min, max, average = MinMaxAvg(data)
	local orient, extents = workspace.Mesh:GetBoundingBox()
	local MeshCenter, MeshSize = workspace.Mesh:GetModelCFrame().p, workspace.Mesh:GetExtentsSize()
	local LowerBound = Vector3.new(MeshCenter.X - MeshSize.X/2, MeshCenter.Y - MeshSize.Y/2, MeshCenter.Z - MeshSize.Z/2)
	local UpperBound = Vector3.new(MeshCenter.X + MeshSize.X/2, Denormalize(min, max, sea_level), MeshCenter.Z + MeshSize.Z/2)
	
	
	--Enable for automatic sea level filling based on sea_level variable (VERY slow, recommend using Roblox's sea level method instead. My version of this functionality isn't complete.
	--(It's also not guaranteed that it will be aligned with your map. That's why it's off by default.)
	--[[for x = LowerBound.X, UpperBound.X, regionIncrement do
		for z = LowerBound.Z, UpperBound.Z, regionIncrement do
			if tick() % 5 > 4.5 then
				TimerWait(rate)
			end
			for y = LowerBound.Y, UpperBound.Y, regionIncrement do
				local Lower = Vector3.new(x, y, z)
				local Upper = Vector3.new(x + regionIncrement, y + regionIncrement, z + regionIncrement)
				local WaterRegion = Region3.new(Lower, Upper)
				workspace.Terrain:FillRegion(WaterRegion:ExpandToGrid(4), 4, Enum.Material.Water)
			end
		end
	end]]--
	
	for _, part in pairs(whitelist) do
		local Material
		local cf, pos, size = part.CFrame, part.CFrame.p, part.Size --Vector3.new(thickness, part.Size.Y, part.Size.Z)
		if math.abs(90 - math.abs(part.Orientation.Z)) > 35 or math.abs(0 - math.abs(part.Orientation.X)) > 35 then
			Material = Enum.Material.Rock
		else
			Material = Enum.Material.Grass
		end
		
		if pos.Y < Denormalize(min, max, sea_level) + 6 then
			if Material == Enum.Material.Grass then
				Material = Enum.Material.Sand
			elseif Material == Enum.Material.Rock then
				Material = Enum.Material.Slate
			end
		end
		
		
		FillWedge(cf, size, Material)
		
		part:Destroy()
		if tick() % 5 > 4.5 then
			TimerWait(rate)
		end
	end
end

ConvertTerrain()

Here’s also a repo comparing the above edits to before:

(Disable new script and enable old script to compare the difference)
(You’ll notice the old version has a lot of holes & artefacts and appears a lot more jagged-looking. The new version is a lot smoother and preferable visually.)

CustomFillWedgeDemo.rbxl (340.8 KB)

13 Likes

I’ll give the new one a shot today, thank you!

Edit: Oops, autocorrect. :grimacing:

3 Likes

All I can say is WOW. Your method is amazing! I highly recommend using it over terrain:FillWedge! Thank you so much!

5 Likes

Dang my PC only has an i3. Guess I’ll have to use someone else’s.
Very helpful guide, this is just what I was looking for.

1 Like

I think it would work, my computer is a potato, and while it’s laggy and takes and hour or 2, it does work.

2 Likes

Alright, I’ll try it. Hope it works and my PC doesn’t blow up.

3 Likes

I just discovered the select all feature in Notepad while coyping the terrain.

3 Likes

Can Gaea run on lower end PCs? Gaea says I need this:

  • Intel® i7 or similar
  • 24GB RAM (32GB recommended)
  • 10GB HDD space
  • GPU with 1GB VRAM, Shader Model 3, DirectX 11 support

I have an i5 processer and 16 GB, can I still run it?

2 Likes

You should be fine. Gaea expects heavier workloads in general than what you’ll need for roblox.

3 Likes

wow man my computer sucks i tried making the mountain one into terrain and the computer just forced itsself to restart.
My computer specs :
CPU : Intel Core i5 4460
Ram : 8GB of DDR3 ram
Graphics Card : Intel Graphics 4600
i know my computer sucks

1 Like

Dude my PC has an i3 and it works like a charm, with no lag or glitch.

1 Like

i3/i5/i7 doesn’t matter as much as the specific encompassing generation. For Gaea specifically, as a GPU accelerated program, your GPU and memory (RAM and VRAM) are the most important factors.

2 Likes

This is very very promising! I am using it right now, but, I need to add a 3rd material in the code to insert in the command line. I tried tinkering with it to get it to work with no luck. Any ideas on seamlessly adding more materials based on wedge orientations?

2 Likes

This is awesome. Great resources.

1 Like

Depending on the material (and if you want to it to appear naturally integrated), using just orientations isn’t necessarily the best approach.

One easy step is to use an elevation approach, but this would lead to banding - that is, there would be a notable boundary between materials. If you want to add water and beaches, though, one could take this approach. You would get the minimum and maximum elevation, sample the elevation of each wedge, normalize that value between 0 and 1 using the min and max, and apply a material based on that normalized resultant number. (For water levels, you would fill the region occupied by the wedges up to a height determined by denormalizing some value between 0 and 1 - say, 0.2).

To somewhat answer your question specifically:
In order to add more materials using just orientation, you could change the angles of orientation required to apply a given material. This would lead to tighter restraints for each material, though.

You could use a combination of both orientation and elevation to add more materials. This would be a good way to add snowy peaks with ice formations, for example.

There are more complex methods to be explored here as well - maybe I’ll look into this at a future date. I personally am very interested in procedural terrain generation and would love to take this topic further on Roblox.

This thread focuses on using software to generate the terrain and then manipulating the result post-generation. This is a powerful, entry-level approach to terrain generation that is accessible to many people, but generating your own terrain in-engine provides you with far more control and access to information that you just don’t get by replacing a series of wedges with terrain.

Eventually, I am certain that I will revisit this area with a writeup on how to create your own procedural terrain, complete with the basics of heightmaps, domain warping, and using noise, elevation, and latitude to organize biomes and their internal details.

3 Likes

Oh boy my roblox studio crashed when it rendered it

GTX 1660 TI
Ryzen 5 2600

2 Likes

Ah yes. Definitely keep pursuing this. I would really really really like to see this as a intuitive plugin similar to UE4 versus constant re-adding wedge terrain and then converting it by entering the code into command line, it makes more sense to get it directly from obj information and the tweak it from there in real time.
What I am wanting to do is nothing of the sort for snow or beaches / water. I just want to add more materials based on slope. Its more realistic to have a smoother transition between just rock and grass. It makes sense to have, say, the Ground material to transition between the two.
I cannot figure the code enough to simply add if X is between say 90 & 45, if Y is between 45 & 30, and Z is between 30 & 0, and so on so forth
I tried multiple else, elseif statements

1 Like

The code used for the command line to convert does not seem to work anymore. Stack ends. Any ideas?

1 Like

What does the rate value do/mean?

1 Like

I’m having an issue with this method. I’ve imported a heightmap to blender and then from there, I imported the obj. through the plugin, but it only appeared half of the squares (one triangle for each square), so when I run the command to convert it to terrain it only converts those triangles leaving huge gaps on the terrain. I’ve tried subdividing the mesh, to see if could get better results, but it only crashes my pc while importing via plugin or converting it into terrain. I don’t know what I should do now. Any help would be highly appreciated.

1 Like