Large-Scale Roblox Terrain: The ultimate guide

Tried this, unfortunately my laptop could not handle exporting the map from Gaea. It gets stuck on the erosion node. Too bad, this is quite amazing!

1 Like

I found a better and less laggy way to do this, gaea unselect everything and click 2d, righth click the image and choose save as image now import it to roblox as a heightmap

4 Likes

It’s not a good practice, it’s working now, just super slow.

1 Like

Thanks for the guidance, you helped me a lot

1 Like

Just looking at these screenshots I thought you rendered it in Blender! I’ll definitely be trying this out for my project!

2 Likes

This is a great guide. Really nice work.

I have a suggestion that I feel would be of significant benefit:

Terrain:FillWedge suffers from artefacts and I see that you’ve worked around this somewhat by doing,
Vector3.new(thickness, part.Size.Y + 15, part.Size.Z + 15) to increase overlap. (Though this still leaves a few artefacts and makes the terrain less smooth in some areas.)

Instead I recommend using my custom function which produces a much closer fit,

This should also work better in smaller spaces such as caves.

For reference, here’s an example of the better aesthetics achieved after switching to my method and removing your increase in wedge size to instead use the exact wedge size.

10 Likes

awesome stuff man, appreciate it.

1 Like

I’ll take a look at this and consider switching to it after a few test runs! Thanks for your contribution.

3 Likes

I’ll give it a go as well, (I use this workflow everyday, has quite literally started my career,) and let you know of the results. Seems promising though.


Edit: @Quasiduck, it’s not exactly a viable option over the other one as it does not automatically paint materials based on height. That would be my recommended change. Other than that, it works like a charm. :slightly_smiling_face:

1 Like

I’d assume it’s still viable (I haven’t tried it myself yet), the only difference is that their code does not include my auto painting code. I’ll happily incorporate it once I get a chance… Life’s been busy lately!

2 Likes

Yeah, totally agree. It works well in the sense of creating the terrain, however due to the fact that it doesn’t auto paint, it’s just not an option for this type of workflow. I’ve actually got a few other suggestions myself, I’d be happy to share them via DMs.

1 Like

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