Smooth terrain

ROBLOX uses a Voxel based terrain system called Smooth Terrain to allow for very large and performance efficient landscapes.

A rendering of 8×8×8 stud voxel spheres of each of the the smooth terrain Materials (excluding Air). Note how some bulge out of their cube and are smooth, while others have sharp edges

Tools[edit]

Convert To Smooth[edit]

The Convert To Smooth tool will convert a ROBLOX place that contains legacy terrain into smooth terrain. This tool must be used before building with the smooth terrain feature. This process is irreversible; if you convert your game there is no action in Studio to revert the change. If you think you may need the legacy terrain from a particular place it is recommended to make a backup of the place before conversion.

Some legacy terrain materials are not implemented yet in smooth terrain. When converting legacy terrain to smooth if a material is not supported a close material will be chosen for the terrain.

Generate[edit]

The tool used to generate worlds and landscapes of smooth terrain.

  • Map Size: Signifies how long and wide the map is when generated. Smaller maps generate quicker.
  • Seed: A code that defines the random numbers that are used to generate the landscape.
  • Caves: A checkbox that decides if caves are generated inside the terrain.
  • Biome Size: Defines roughly how long and wide each biome (or section of terrain type) is.
  • Biomes: Contains a list of biomes that may be used when generating the terrain.
  • Water: A body of water that could be considered a lake, sea, or ocean.
  • Marsh: Low-lying land that is generally half flooded.
  • Hills: Large rolling hills with the occasional dried rivulet.
  • Plains: Mostly flatland with the occasional river or rock outcrop.
  • Dunes: Sharp dunes of sand as you would expect in a desert.
  • Canyons: Sandstone canyons and banded mesa/buttes
  • Mountains: Rough mountainous terrain with snowcaps and occasional lakes.
  • Lavascape: Something crazy with lava
  • Arctic: Barren snowfield with icey outcrops and deep water-filled crevasses.

Add[edit]

The Add tool fills the displayed region with the selected terrain material. When dragging the mouse with this tool a grid will appear in the workspace showing the plane where dragged terrain will be centered. Tilting and rotating the camera will move how this plane is oriented.

  • Size: The size of the brush.
  • Shape: The shape of the brush. Can be either spherical or cubic.
  • Snap to Grid: When on forces the brush to only step between 4x4x4 voxel squares.
  • Material: The terrain material to fill the selected area with. If Auto is selected the the material will automatically be chosen based on the surrounding terrain.
  • Ignore Water: When on, brush treats water as air. Allows for use of brush under or through water.

Subtract[edit]

The Subtract tool removes all terrain from the selected area. When dragging the mouse with this tool a grid will appear in the workspace showing the plane where removed terrain will be centered. Tilting and rotating the camera will move how this plane is oriented.

  • Size: The size of the brush.
  • Shape: The shape of the brush. Can be either spherical or cubic.
  • Snap to Grid: When on forces the brush to only step between 4x4x4 voxel squares.
  • Ignore Water: When on, brush treats water as air. Allows for use of brush under or through water.

Paint[edit]

The Paint tool changes the material in the selected area.

  • Size: The size of the brush.
  • Shape: The shape of the brush. Can be either spherical or cubic.
  • Plane Lock: When on restricts the brush to a plane. Dragging the tool in this mode will only move the tool along the plane (cannot go above or below the plane during the same drag).
  • Snap to Grid: When on forces the brush to only step between 4x4x4 voxel squares.
  • Material: The terrain material to fill the selected area with.
  • Ignore Water: When on, brush treats water as air. Allows for use of brush under or through water.

Grow[edit]

The Grow tool slowly fills in the selected area with terrain. Holding down the mouse button over the same area will continue to grow terrain until the selected area is full.

  • Size: The size of the brush.
  • Strength: The speed the tool fills terrain.
  • Plane Lock: When on restricts the brush to a plane. Dragging the tool in this mode will only move the tool along the plane (cannot go above or below the plane during the same drag).
  • Snap to Grid: When on forces the brush to only step between 4x4x4 voxel squares.
  • Material: The terrain material to fill the selected area with. If Auto is selected the the material will automatically be chosen based on the surrounding terrain.
  • Ignore Water: When on, brush treats water as air. Allows for use of brush under or through water.

Erode[edit]

The Erode tool slowly removes terrain in the selected area. Holding down the mouse button over the same area will continue to remove terrain until the selected area is empty.

  • Size: The size of the brush.
  • Strength: The speed the tool fills terrain.
  • Plane Lock: When on restricts the brush to a plane. Dragging the tool in this mode will only move the tool along the plane (cannot go above or below the plane during the same drag).
  • Snap to Grid: When on forces the brush to only step between 4x4x4 voxel squares.
  • Ignore Water: When on, brush treats water as air. Allows for use of brush under or through water.

Smooth[edit]

The Smooth tool will smooth terrain in the selected area.

  • Size: The size of the brush.
  • Strength: The speed the tool fills terrain.
  • Plane Lock: When on restricts the brush to a plane. Dragging the tool in this mode will only move the tool along the plane (cannot go above or below the plane during the same drag).
  • Snap to Grid: When on forces the brush to only step between 4x4x4 voxel squares.

Regions[edit]

Regions allow transformation of existing terrain, along with copy and paste.

  • Select: Creates a 3D region in the workspace that the other Region tools will use. This region can be changed by dragging the handles on the side.
  • Move: Allows movement of the selected region by dragging on the handles on the side.
  • Resize: Allows resizing of the selected region by dragging on the handles on the side.
  • Rotate: Allows rotation of the selected region by dragging on the handles on the rotation axes.
  • Copy: Copies the selected region.
  • Paste: Pastes a copy of the selected region and automatically selects the move tool so you can move the copied region.
  • Delete: Deletes the terrain in the current selection.
  • Fill: Fills air in the selected region with a material of choice. If ‘’’Merge Empty’’’ is off, ‘’all’’ cells in selected region will be set to the material chosen.

How to add the Smooth Terrain Tools to your game[edit]

If you would like to add the smooth terrain tools to your game as player-usable tools, you can download a ZIP file of tools from ROBLOX Studio Tools GitHub. If you place the TerrainTools model into the Starterpack icon.pngStarterPack of your game, the terrain tools will be given to all players.

Scripting[edit]

To understand how Smooth Terrain works, it is important to know a few of the fundamental concepts behind it. Firstly, Smooth Terrain is based on a voxel system. A voxel is basically a point in space, often arranged in a grid. In ROBLOX, each cell in the voxel grid measures 4x4x4 studs.

To create the terrain effect, points in the voxel grid are assigned a material. This material is then filled in around the voxel to create the terrain. With ROBLOX's legacy terrain, the material was shaped into a cube around the voxel (or sometimes a 45 degree slope). With Smooth Terrain however, this is not the case. With Smooth Terrain each voxel now contains an occupancy value along with its material value. This occupancy value defines how full the voxel is with the given material. This value can be anywhere between 0 (almost completely empty) to 1 (very full, sometimes overflowing). When ROBLOX generates terrain based on these values, the shape of the terrain is organically generated to create smooth curves to accommodate varying occupancy values.

FillBlock[edit]

The FillBlock function can be used to create terrain from the volume defined by a Part. This can be used to quickly create terrain by simply placing parts where you want the terrain to go. When the terrain is generated the volume will be filled as close as possible by adjusting the occupancy of the voxels the volume overlaps. This is not guaranteed to get a perfect match to the volume, but will be the approximate shape. This function takes several parameters:

  • CFrame cframe: The CFrame of the region you want to fill. As with all cframes, this can define both a position and an orientation.
  • Vector3 size: The size of the region to be filled. If the cframe is rotated the size will be rotated accordingly.
  • Material material: The material to be used when generating the terrain.

Example:

Given a Part in the Workspace called Part, the following will generate grass terrain to fill the volume of the part.

game.Workspace.Terrain:FillBlock(game.Workspace.Part.CFrame, game.Workspace.Part.Size, Enum.Material.Grass)

FillBlock.png

FillBall[edit]

The FillBall function creates terrain to fill a spherical volume. This function takes several parameters:

  • Vector3 center: The center of the sphere to be filled.
  • float radius: The radius of the sphere.
  • Material material: The material to be used when generating the terrain.

Example: This will create a sphere of sand above the center of a place:

game.Workspace.Terrain:FillBall(Vector3.new(0, 100, 0), 50, Enum.Material.Sand)

FillBall.png

FillRegion[edit]

The FillRegion function creates terrain within a defined Region3.

  • Region3 region: The region to be filled.
  • float resolution: The resolution of voxels to fill. Must be set at 4. See #Resolution.
  • Material material: The material to be used when generating the terrain.
Unlike FillBlock and FillBall, FillRegion must be aligned perfectly to the voxel grid. The other functions will use the occupancy to approximate the shape passed into the function, but FillRegion has to be on the grid. To help with this, use the ExpandToGrid function. This will convert a Region3 to another Region3 that has been expanded if necessary to align to a given voxel resolution.

Example: This will create slate terrain in the region specified. Note that the region is not initially aligned to the voxel grid but the ExpandToGrid function is used to fix that.

local region = Region3.new(Vector3.new(0,0,-3), Vector3.new(4,4,4))
region = region:ExpandToGrid(4)
game.Workspace.Terrain:FillRegion(region, 4, Enum.Material.Slate)

FillRegion.png

Reading and writing voxels[edit]

The functions outlined above do a lot of work to automatically calculate which voxels to fill with terrain and how much occupancy to use. Sometimes you may want to directly read and write to specific voxels to specify the material and occupancy.

ReadVoxels[edit]

ReadVoxels takes a Region3 and a resolution and returns the raw voxel data for the region specified. This data is returned as two 3D arrays. The first array contains material values, the second contains occupancy. Both arrays also have a Vector3 property called Size that can be used to determine the size of the arrays in each of their dimensions. As with FillRegion the region supplied must be aligned to the voxel grid. ExpandToGrid can be used to ensure the correct region is used.

local region = Region3.new(Vector3.new(0,0,-15), Vector3.new(4,8,4))
region = region:ExpandToGrid(4)
local material, occupancy = game.Workspace.Terrain:ReadVoxels(region, 4)
local size = material.Size
for x = 1, size.X do
	for y = 1, size.Y do
		for z = 1, size.Z do
			print("Material at (", x, y, z, "): ", material[x][y][z])
			print("Occupancy at (", x, y, z, "): ", occupancy[x][y][z])
		end
	end
end

WriteVoxels[edit]

WriteVoxels allows you to specify specific material and occupancy values for each voxel in a region. Like ReadVoxels and FillRegion the region specified must be aligned to the voxel grid using a method like ExpandToGrid. The material and occupancy arrays must also be the correct size for this function to work properly.

local region = Region3.new(Vector3.new(0,0,0), Vector3.new(4,4,16))
region = region:ExpandToGrid(4)
 
local function create3dTable(size)
	local ret = {}
	for x = 1, size.X do
		ret[x] = {}
		for y = 1, size.Y do
			ret[x][y] = {}
		end
	end	
	return ret
end
 
local material = create3dTable(Vector3.new(1,1,4))
material[1][1][1] = Enum.Material.Sand
material[1][1][2] = Enum.Material.Sand
material[1][1][3] = Enum.Material.Grass
material[1][1][4] = Enum.Material.Grass
 
local occupancy = create3dTable(Vector3.new(1,1,4))
occupancy[1][1][1] = .25
occupancy[1][1][2] = .5
occupancy[1][1][3] = .75
occupancy[1][1][4] = 1
 
game.Workspace.Terrain:WriteVoxels(region, 4, material, occupancy)

WriteVoxels.png

Resolution[edit]

The functions FillRegion, ReadVoxels, and WriteVoxels all require a parameter to define the resolution of the voxels the functions are intended to work with. At the moment this must always be set to 4, as ROBLOX voxels are currently 4x4x4 studs in size. This setting however is left as a variable as ROBLOX may implement a smaller voxel resolution at some point. Leaving this as a variable allows code written with the current system to work expecting 4x4x4 voxels to work with any size voxel in the future.

Examples[edit]

Flood Fill[edit]

This LocalScript will flood fill depressions in terrain with water. If the algorithm detects that the fill could potentially go on forever, it will abort the process and not generate any new terrain.

local mouse = game.Players.LocalPlayer:GetMouse()
local terrain = game.Workspace.Terrain
 
local function checkDirection(origin, direction)
	local ray = Ray.new(origin, direction)
	local part, point, normal = game.Workspace:FindPartOnRay(ray, nil, true)
	return part
end
 
local function processDirection(queue, processed, current, direction)
	local nextNode = current + direction
	if not processed[tostring(nextNode)] then
		table.insert(queue, nextNode)
	end
end
 
local function floodFill(voxelPos)
	local region = Region3.new(voxelPos - Vector3.new(2,2,2), voxelPos + Vector3.new(2,2,2))
	local material, occupancy = terrain:ReadVoxels(region, 4)
	if material[1][1][1] ~= Enum.Material.Air then
		print("Could not fill from that voxel: Voxel not empty")
		return
	end
 
	local queue = {}
	local processed = {}
	local success = true
	table.insert(queue, voxelPos)
 
	while #queue > 0 do
		local current = table.remove(queue)
		region = Region3.new(current - Vector3.new(2,2,2), current + Vector3.new(2,2,2))
		material, occupancy = terrain:ReadVoxels(region, 4)
		if material[1][1][1] == Enum.Material.Air then
			if  checkDirection(current, Vector3.new(1000, 0, 0)) and
				checkDirection(current, Vector3.new(-1000, 0, 0)) and
				checkDirection(current, Vector3.new(0, 0, 1000)) and
				checkDirection(current, Vector3.new(0, 0, -1000)) and
				checkDirection(current, Vector3.new(0, -1000, 0)) then			
 
				processed[tostring(current)] = current
 
				processDirection(queue, processed, current, Vector3.new(4,0,0))
				processDirection(queue, processed, current, Vector3.new(-4,0,0))
				processDirection(queue, processed, current, Vector3.new(0,0,4))
				processDirection(queue, processed, current, Vector3.new(0,0,-4))
				processDirection(queue, processed, current, Vector3.new(0,-4,0))
 
			else
				processed = {}
				success = false
				break
			end
		end
	end
 
	if success then
		for _, position in pairs(processed) do
			region = Region3.new(position - Vector3.new(2,2,2), position + Vector3.new(2,2,2))
			material[1][1][1] = Enum.Material.Water
			occupancy[1][1][1] = 1
			terrain:WriteVoxels(region, 4, material, occupancy)
		end
	else
		print("Could not fill from that voxel: Would create infinite fill")
	end
end
 
local function round(num)
	return math.floor(num + .5)
end
 
mouse.Button1Up:connect(function()
	local hit = mouse.Hit
 
	local x = round(mouse.Hit.p.X)
	x = x - x%4 + 2
	local y = round(mouse.Hit.p.Y)
	y = y - y%4 + 2
	local z = round(mouse.Hit.p.Z)
	z = z - z%4 + 2
 
	floodFill(Vector3.new(x, y, z))
end)

Procedural terrain generation[edit]

This example generates terrain as the player walks through the world. This code generates a heightmap using math.noise and fills in the terrain using FillBlock.

local baseHeight 		= 10				-- The main height factor for the terrain.
local chunkScale 		= 3 				-- The grid scale for terrain generation. Should be kept relatively low if used in real-time.
local renderDist 		= 120/4 			-- The length/width of chunks in voxels that should be around the player at all times
local xScale 			= 90/4				-- How much we should strech the X scale of the generation noise
local zScale 			= 90/4				-- How much we should strech the Z scale of the generation noise
local generationSeed	= math.random() 	-- Seed for determining the main height map of the terrain.
 
------------------------------------------------------------------------------------------------------------------------------------------------
 
local chunks = {}
 
function chunkExists(chunkX,chunkZ)
	if not chunks[chunkX] then
		chunks[chunkX] = {}
	end
	return chunks[chunkX][chunkZ]
end
 
function mountLayer(x,heightY,z,material)
	local begY = -baseHeight
	local endY = heightY
	workspace.Terrain:FillBlock(CFrame.new(x*4+2, (begY+endY)*4/2, z*4+2), Vector3.new(4, (endY-begY)*4, 4), material)	
end
 
function makeChunk(chunkX,chunkZ)
	local rootPos = Vector3.new(chunkX*chunkScale,0,chunkZ*chunkScale)
	chunks[chunkX][chunkZ] = true -- Acknowledge the chunk's existance.
	for x = 0,chunkScale-1 do
		for z = 0,chunkScale-1 do
			local cx = (chunkX*chunkScale) + x
			local cz = (chunkZ*chunkScale) + z
			local noise = math.noise(generationSeed,cx/xScale,cz/zScale)
			local cy = noise*baseHeight
			mountLayer(cx,cy,cz,Enum.Material.Grass)
		end
	end
end
 
function checkSurroundings(location)
	local chunkX,chunkZ = math.floor(location.X/4/chunkScale),math.floor(location.Z/4/chunkScale)
	local range = math.max(1,renderDist/chunkScale)
	for x = -range,range do
		for z = -range,range do
			local cx,cz = chunkX + x,chunkZ + z
			if not chunkExists(cx,cz) then
				makeChunk(cx,cz)
			end
		end
	end
end
 
while true do
	for _,player in pairs(game.Players:GetPlayers()) do
		if player.Character then
			local torso = player.Character:FindFirstChild("Torso")
			if torso then
				checkSurroundings(torso.Position)
			end
		end
	end
	wait(1)
end