Large-scale Landscapes on Roblox
Updated on 4/21/2020 with improvements to the workflow and quality-of-life changes that lead to better results.
Updated (again) on 7/3/2020 with a method to automatically paint materials to an imported mesh based on slope and altitude (rough first version, improvements can be made in this area). Also updated the accompanying intro thumbnails to reflect the improvements that have been made.
Some example landscapes that can be achieved with this workflow.
REQUIRED TOOLS:
- Quadspinner Gaea (Separate program; you just need the free version)
- Blender (Separate program)
- .OBJ Importer (A Studio plugin)
- Automated part to terrain conversion script (There’s one provided in this tutorial)
The problem
Making believable terrain on a large scale is a daunting task. If done by hand, it is a tedious process. Roblox-provided tools such as terrain sculpting and procedural generation are helpful, but oftentimes lacking. While the sculpting tools are perfectly adequate for experiences with smaller worlds, it’s an insurmountable task to model a massive open-world landscape alone using Roblox’s default terrain toolset.
Luckily, Roblox has provided us with tools to make this easier. Importing heightmaps has made it much faster and easier to prototype a good base for big landscapes. This feature presents its own set of issues, however.
A heightmap is good for conveying information of basic landscapes. Change in elevation, whether steep or gradual, is relatively easy to achieve. However, due to the nature of a 2D representation of a 3D space, it’s not possible to generate features such as overhang cliffs or even cave systems with just a basic heightmap. For the sake of avoiding confusion, I won’t go into detail on the specifics of generating such features procedurally.
My intentions with this tutorial are to ensure accessibility. This means that no prior programming knowledge is required; anybody who can follow a set of instructions can achieve this terrain. One of the biggest advantages to this process is that you don’t need a big team to make impressive terrain.
Part 1: Quadspinner Gaea
In my search for alternative tools for terrain generation, I found a program called Quadspinner Gaea. Its free version allows for a map resolution of 1k without a watermark, which is more than enough resolution for generating terrain on Roblox.
In this tutorial, I will teach you how to use Quadspinner Gaea’s features to generate your terrain. Since it’s a node-based experience, it’s relatively easy to pick up and understand quickly.
Before we begin…
PLEASE NOTE: Gaea can eat up a lot of system resources. Bearing that in mind, here are my recommended specs:
-
At least 8 GB of RAM (though I strongly recommend 16 GB, as Studio will eat up a lot of RAM during the import process)
-
A reasonable GPU (at least 1 GB of VRAM as reported by the official website)
(Here’s a useful metric: For slower tasks, Gaea uses < 10% of my 1080 ti, so you probably don’t need anything super powerful here.) -
I’m using an i5-8600k, but anything equivalent to a Ryzen 5 1400 should do.
I haven’t been able to test on lower-end systems, so I can’t vouch for a smooth experience on lesser hardware. It’s definitely worth trying, though. Feel free to report your usage experience in the replies!
Update on the specs situation:
https://devforum.roblox.com/t/large-scale-natural-terrain-the-ultimate-guide/405672/6?u=vexture
As it turns out, lower-end hardware can actually manage this! Thanks, @staplecruncher.
The official website’s listed minimum requirements are very high, but you don’t need that for our purposes. For super-high terrain resolutions (well over 1k resolution) using professional (paid) features, you’d need beefier specs.
The program’s installation is self-explanatory, so I’ll skip that bit.
When you first open the program, you are greeted with this screen:
Each mode allows for different ways to edit terrain. I personally use Create a terrain (Graph), and that is the mode I will be covering in this tutorial. I feel that it is the most powerful mode, but feel free to experiment with the other options.
Once you start a new Gaea project, you will be greeted with the Graph workflow. You will notice 4 vital components: The viewport (middle top), the node editor (middle bottom), the toolbox (left), and the properties menu (right).
The starting shape of your landscape is called a Primitive. Primitives act as the base for your terrain, so all nodes connected to your primitive will act on it. You always start with a Mountain, a Displace node, and an Erosion node. Their functions are self-explanatory; displace warps the terrain a bit, while Erosion etches rivers into your landscape.
The Toolbox
The amazing thing about this program is its sheer versatility. The best way to understand what each node does is to experiment with them yourself, but behaviors are based on the category they’re in. Here’s a short description of what’s in your toolbox, from top to bottom:
-
Primitives: The starting shape of your landscape. They act as the base for your terrain, so all nodes connected to it will act on it.
-
Adjustments: These nodes are like post-processing effects you would apply on images. They are applied indiscriminately to the structure of your landscape. Should you decide to use them, they should probably be the last element on your node tree.
-
Filters: A bit more useful than Adjustments, Filters are a bit smarter. They exhibit predictable behavior, but rather than degrading the structure of your terrain, they enhance it by changing the shape of your primitive.
-
Erosion: Super important! These add depth and detail to your initial primitive. Rivers, landslides, and much more can be easily achieved with these modifiers.
-
Data Maps and Color: I’m combining these two because they work pretty much the same. Data maps and Color are better suited for simulation of things like vegetation growth, ground texture, and more. While I haven’t actually used these in practice yet, you could export these maps as images and place vegetation or textures based on these maps alone, which is super useful in theory.
-
Selectors: The only node from this category I use is Height, though less frequently now due to a step I will introduce later in this tutorial. Height generates a heightmap, which can be imported into ROBLOX through the built-in terrain editor. I will cover generating heightmaps in a separate tutorial; for the best results, I recommend using the methods I’m about to go over.
tl;dr: It’s actually pretty self-explanatory, so trial and error will teach you everything you need to know about your toolbox.
Properties
These are pretty easy to figure out, so just mess around with the sliders. Click on the node you want to edit and its properties will show up on the right, just like Roblox Studio. Use the Scale slider on your primitive to cram your chunk of terrain with more features for bigger maps, or fewer for more enclosed areas.
Interacting with the node editor
It’s so simple!
Exporting your terrain from Gaea
Here’s where the process starts to fork a bit. As I mentioned above, I will be covering heightmaps at a later date. The advantage that heightmaps have over what I’m about to show you is that they are significantly faster to import into ROBLOX as voxel terrain.
For now, here’s how to export:
On the top-left corner, click on Gaea. Then, click on Entire Terrain.
https://gyazo.com/35f528040f01d36517092e52d63bc445
A recent update to Gaea has changed the export process slightly, so I’ve edited this section for accuracy and completeness:
Due to a recent update to Quadspinner Gaea, you cannot export your terrain as a .obj
from any particular node. In order to export your terrain as a .obj
file, you must add a Mesher node at the end of your graph.
EDIT: There have been improvements to heightmaps on Roblox! It is much faster to export your terrain as a heightmap rather than going the mesh route, as you no longer have to wait for content moderation to upload your image and it now supports higher resolutions. The only drawback is that the auto material script shown below will NOT work with imported heightmaps. For material painting in this case, export a color map from Gaea that adheres to Roblox’s colormap material set.
Click on Start Build, choose where you want to save your file, and let it run! Leave the file extension as .tor in the Save As menu.
When complete, Gaea will open a folder with your saved terrain object
Part 2: The Import Process
Now that you’ve made your shiny new terrain, you want to import it to Studio, right? Well, luckily that process is relatively easy! Since we’re not using heightmaps (we’re using the .obj file itself), the aim is to get that file into Studio and convert it to voxel terrain. We do this with NexusAvenger’s .OBJ Importer.
For those of you with weaker PCs, I apologize in advance for the lag you’re about to endure… It’s worth it, though!
This incredible plugin takes vertex data from your .obj file and converts it into parts! This means that you can surpass Roblox’s polygon limit for .OBJ files and it has the added benefit of being convertible to voxel terrain using some simple code! That’s what makes this whole process viable. If we had to use massive amounts of part terrain, this wouldn’t work for a real gameplay setting!
Before we do that, we still want to reduce the poly count, as otherwise we’ll be importing millions of polygons into our world! We definitely don’t want that.
Using Blender to optimize terrain for import*
*Due to some updates I’ve made to this post, I actually find it better to skip the Blender step entirely. If your PC can handle it, I recommend ignoring this step entirely.
Blender optimization (if your PC cannot handle the following import process)
You’ll need to download Blender for this next step. It’s a free and powerful 3D modeling software (but of course you already knew that)!
Since this isn’t a Blender tutorial, I’ll keep this section brief. When you install and open Blender, you’ll be prompted to create a new file. Just click on General.
To import your OBJ file to Blender, go to File > Import > Wavefront (.obj) and navigate to where you saved your terrain.
https://gyazo.com/f09003f750a735393ad8eb9344c75c8b
Click on the blue wrench icon on the right, click Add Modifier, and add the Decimate modifier. We’re using this to reduce the poly count significantly.
Now, just reduce the ratio until the “Face count” is at around 200,000 (Try 50,000 (minimum ratio of 0.1) if you’re getting a weird banding effect in your terrain). This is low poly enough to reasonably import into Roblox as parts while still retaining enough detail for voxel conversion to be viable.
Click on Apply, and then export your optimized .obj by going to File > Export > Wavefront (.obj)
The reason I am now advising against Blender optimization is due to the new conversion method. If you don’t have enough triangles, once you convert your terrain, there will be more holes present in the final product.
Importing to Studio
If your PC isn’t already crying, this is where things might get tough. (You might want to try reducing your poly count further if this next step is impossible for you to complete.)
Install the OBJ importer plugin. In a new blank place, go ahead and open that plugin. You can delete the Baseplate.
This plugin works by reading vertex data pasted into a script and generates all of the faces as wedges.
Right-click on your optimized .obj file and navigate to Open With > Notepad.
Copy all the contents of this notepad file into a script. I named mine Vertex Data, but it doesn’t matter what you call it.
Each line represents a vertex in your terrain model.
Set your scale to whatever size you want your map to be. I personally went with 4,000. The size will not impact part count, as that’s predetermined by your polygon count.
You will see two options under Triangle Type: Part and Terrain. While at first glance Terrain might be the better option, make sure you are using Part.
If you don’t use Part, your final import will look awful, like this:
Now with the script selected, click Continue until it begins to parse the faces.
https://gyazo.com/fe70323814eb910e1cf44fb6891c4f38
One this process is complete, hit continue until the faces start to render. This will take quite some time, so grab a cup of coffee or something.
https://i.gyazo.com/27eb415c3c1d8a26b05e193270aa71c7.mp4
(Yes, this takes a while.)
The hardest part is behind us! There’s just one more step.
Part 3: Converting your parts into terrain
With the recent addition of :FillWedge()
, I have updated this post with new code that gives far better results. I’ve also added new functionality, including automatic material painting. It significantly improves the initial import results even before any by-hand adjustments are made.
Note: Run this code in Studio’s command bar, you shouldn’t run Test Mode or you might not be able to save your terrain.
local rate = .00001
local RunService = game:GetService("RunService")
local whitelist = workspace.Mesh:GetChildren()
local thickness = 25
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
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, Vector3.new(thickness, part.Size.Y + 15, part.Size.Z + 15)
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
workspace.Terrain:FillWedge(cf, size, Material)
part:Destroy()
if tick() % 5 > 4.5 then
TimerWait(rate)
end
end
end
ConvertTerrain()
This code has been updated with a method to automatically paint materials to the terrain based on
slope and altitude.
WARNING: Back up your Studio file before you do this, as crashing is possible. It WILL take time to convert all of those triangles to terrain, but probably not as long as it took to render the triangles themselves.
The end result:
Depending on the primitive you used in Gaea, your landscapes will look vastly different. The end result shown here is using the Mountain primitive with erosion and displacement nodes.
Conclusion
These methods produce consistently impressive results and make large-scale Roblox terrain more convenient and approachable, especially if you don’t want to go down the procedural generation route.
I have significantly updated this post to improve the quality of the results; thanks to Roblox updates, this got a bit easier.
This method is incredibly useful but it requires a beefy PC to generate, import, and convert the terrain without crashing. I do wish there was a faster, more performant way to do terrain (i.e. deformable planes, custom materials. I think that would go far to improve the terrain experience. Mesh deformation is on the way, but as far as I can tell, that’s only for animated bone deformation )
I hope this proved useful. If anything confused you, I encourage you to ask me questions!
If you skipped everything for the tl;dr: Read the tutorial.
By the way, I plan on writing more tutorials and I take suggestions! DM me if there’s something you want to know about, and I might write something up.