§ How to · with Picoo

How to generate terrain and maps in Roblox

By Sametcan Tasgiran, Founder & Developer·Published ·Updated

Almost every procedural Roblox terrain that comes out flat and blocky was built with the wrong API.

Terrain is voxels on a 4-stud grid, and generating good land is three decisions: the right write call, enough noise octaves, and a region that lines up.

Voxel-based generation

WriteVoxels with a real heightmap, not stacked FillBlock calls.

Layered noise

Multiple octaves of math.noise so hills have both large shape and small detail.

Grid-aligned regions

Every Region3 expanded to the 4-stud voxel grid before it is written.

Material by height and slope

Rock on cliffs, grass on flats, sand at the waterline — derived, not painted.

Chunked writes

Large maps generated in pieces so a single call does not hang the server.

Seeded and repeatable

The same seed produces the same world, so a map can be regenerated exactly.

Files Picoo ships for this prompt

2 files · 185 lines · ~35s · 1 credit

TerrainGenerator

Heightmap, octave noise, voxel writing, chunking, material rules.

145 lines

TerrainConfig

Seed, size, frequency, octaves, height range, biome thresholds.

40 lines

Sample output: ServerScriptService.TerrainGenerator

-- THE mistake: building terrain out of FillBlock / FillBall calls. Each one
-- fills an axis-aligned box with a single material, so you get stair-stepped
-- blocks no matter how many you stack. WriteVoxels writes the occupancy grid
-- directly, which is what produces smooth land.
local RES = 4        -- Terrain voxels are 4 studs. Every region must fit this grid.

-- One octave of noise is a rolling hill and nothing else. Layering octaves --
-- each at double the frequency and half the amplitude -- is what gives you a
-- large shape WITH small detail. Four to six is the useful range; one or two
-- looks synthetic, ten is expensive and invisible.
local function height(x: number, z: number): number
	local value, freq, amp, norm = 0, BASE_FREQ, 1, 0
	for _ = 1, OCTAVES do
		value += math.noise(x / freq + SEED, z / freq + SEED) * amp
		norm += amp
		freq /= 2          -- higher frequency = finer detail
		amp /= 2
	end
	return (value / norm) * HEIGHT_RANGE     -- math.noise returns about -0.5..0.5
end

local function writeChunk(origin: Vector3, size: Vector3)
	-- ExpandToGrid(4) is NOT optional. A Region3 that does not line up with the
	-- voxel grid is rejected, and the error does not say "your region is 2 studs
	-- off" -- it just fails.
	local region = Region3.new(origin, origin + size):ExpandToGrid(RES)
	local mat, occ = buildVoxelArrays(region)
	workspace.Terrain:WriteVoxels(region, RES, mat, occ)
end

How to generate terrain and maps in Roblox

Procedural terrain in Roblox is one of those tasks where the difference between a good result and an obviously fake one comes down to two API choices made in the first ten minutes.

The first is which call you write with. FillBlock and FillBall are the discoverable ones — they take a position, a size and a material, and they work immediately. They also fill axis-aligned volumes with a single material, which means terrain built out of them is stair-stepped no matter how small you make each step. WriteVoxels is the one you want: it writes the occupancy grid directly, and partial occupancy is precisely what lets a voxel be half-full and produce a smooth slope.

The second is how much noise you layer. A single call to math.noise gives you a smooth rolling surface that reads as artificial the moment you look at it, because real landscape has structure at several scales at once. Layering octaves — each at double the frequency and half the amplitude — gives you the large shape and the small detail from the same function. Four to six octaves is the useful range; beyond that the variation is finer than a voxel and you are paying for detail that cannot be rendered.

One detail will cost you an hour if nobody tells you: terrain voxels are four studs, and every Region3 you read or write has to line up with that grid. Call ExpandToGrid(4) before you use a region. When it is misaligned the operation simply fails, and the error does not mention alignment, so it looks like the API is broken.

Finally, generate in chunks. One WriteVoxels call spanning a large map blocks the server thread until it completes — players disconnect, and it looks like a crash. Breaking the work into chunks with a yield between them turns the same operation into a world visibly building itself, which is both safer and better to watch.

And know where terrain stops. It is the right tool for hills, caves, water and cliffs; it is the wrong tool for buildings and roads. Maps that try to model architecture in voxels end up looking melted, and no amount of noise tuning fixes that.

See more on the Luau generator, the game builder, or browse the full blog.

Frequently asked

Why does my generated terrain look blocky?+

It was built with FillBlock or FillBall. Those fill an axis-aligned volume with one material, so stacking them gives you stair steps regardless of how small you make each block. WriteVoxels writes the occupancy grid itself, and partial occupancy is what produces smooth slopes.

Why does my Region3 error?+

Terrain voxels are 4 studs, and a region has to line up with that grid. Call ExpandToGrid(4) on the region before reading or writing. The failure is not descriptive — it does not tell you the region is misaligned — so it is worth doing unconditionally.

How many noise octaves should I use?+

Four to six. One octave gives you smooth rolling hills with no detail, which reads as artificial immediately. Each additional octave doubles the frequency and halves the amplitude, adding finer variation. Past six the detail is smaller than a voxel and you are paying for something nobody sees.

Is math.noise random?+

No — it is deterministic Perlin noise. The same coordinates and the same offset always return the same value, which is exactly what you want: store the seed and the map regenerates identically. If you want a different world, change the offset you add to the coordinates, not the algorithm.

How do I stop terrain generation freezing the server?+

Generate in chunks and yield between them. A single WriteVoxels call covering a large map blocks the whole server until it finishes, which disconnects players. Splitting into chunks with a task.wait() between them turns a freeze into a visible build.

Terrain or parts for a map?+

Terrain for natural landscape — hills, caves, water, cliffs. Parts for anything built: buildings, roads, platforms. Terrain is cheaper for large organic volumes and worse for precise shapes, and trying to make terrain do architecture is how maps end up looking melted.

Related Picoo prompts