The map editor
The map editor lets a creator change a map after an agent wrote it. The creator paints the ground with materials, raises, lowers, smooths and flattens it, paints where trees, bushes, rocks and grass stand, and sets the height of its sea. Every tool in the editor is also a command, so an agent makes the same edit through the MCP or the CLI, and both write the same file.
This page is the design draft for the editor. It covers the data a map keeps, the materials, the tools and the commands, how an edit is saved, and the order the work is built in. It does not cover:
- Editing on the website, for a creator with no dev server. That is the next project, and it reuses the brushes, the file and the writer this page defines.
- Editing while a game runs for players, in a room or in a published build. A published game loads the terrain and has no editor.
- Caves and overhangs. A hole flag per cell, over a cave placed as a model prop, adds them later, as Unity’s terrain holes and Terrain3D’s hole flag do.
What the other engines do
Section titled “What the other engines do”- Roblox stores terrain as voxels of 4 studs, each with one material and a fill amount, and edits it in the Terrain Editor: Draw, Sculpt, Smooth, Flatten, Paint, Fill and Sea Level, all driven by one brush with a size and a strength. Ctrl lowers, Shift smooths while held, and B resizes. Every tool has a script call, such as
FillBallorReplaceMaterial. AMaterialVariantswaps a material’s look for the whole place. - Unity stores a heightmap, one weight map per terrain layer, a list of tree instances and a density map per grass kind. The Terrain Inspector paints each.
- Unreal stores a 16-bit heightmap per component and a weight map per layer, paints foliage as instances and grows grass from the layer weights.
- Godot, through the Terrain3D add-on, stores a height and a packed control word per sample: a base material, an overlay material and a blend.
- PlayCanvas has no terrain editor. Its tutorial builds a mesh from a heightmap image.
- Phaser with Tiled stores one tile per cell, and a terrain brush picks the transition tiles.
This editor takes Roblox’s tools, brush and keys, and Unity’s and Godot’s data: a 2D grid, one height per point. One height per point is what lets an agent ask “how high is the ground at 40, 12” and get one answer, and it keeps the ground cheap on a phone. Each cell holds a base material, an overlay material and the overlay’s blend, as Terrain3D, Breath of the Wild and The Witcher 3 store per point, rather than Unity’s weight per layer. A query answers “grass (dirt 30%)” rather than a weight for every layer, and the file stays small.
The terrain block
Section titled “The terrain block”A map’s edits live in its own file, src/maps/<map>.json, under a terrain key beside the hills, regions, paths, props and views the agent writes. One file per map is one thing for a creator and an agent to hold in mind, as a Roblox place file holds its terrain beside everything else. A map with no terrain key has no edits. A game’s one registerMaps glob loads the block with its map, so a game’s code does not change. The first edit of an engine map, such as meadow, writes the map into the game as src/maps/meadow.json, as spawnite add map meadow --from meadow would, with the edit under terrain.
The block holds a grid at the map’s cell. Heights live on the grid’s points, size / cell + 1 along each side, because the ground mesh and the physics heightfield are built on points. Materials and scatter live on its cells, size / cell along each side. A 256 m map with 1 m cells has 257 × 257 points and 256 × 256 cells.
{ "seed": 7, "size": 256, "cell": 1, "hills": {}, "regions": {}, "paths": {}, "props": {}, "revision": 14, "terrain": { "size": 256, "cell": 1, "base": { "seed": 7, "hills": {}, "regions": {}, "paths": {} }, "palette": ["grass", "sand", "rock"], "height": [". . . 0.25 0.5 0.25 . . .", "..."], "paint": [". . 1 1 1 . . .", "..."], "scatter": { "tree": [". . 0.5 0.5 0.5 . . .", "..."] }, "erased": { "bush": [ [-22.413725, 20.130154], [-18.024917, 24.502381] ] } }}- Each grid is a list of rows, north to south, and each row is one string of values separated by spaces, so a row is one line and a git diff shows the rows a stroke touched.
- A
.is a point or a cell no brush has touched. A number is an edit, including0, so a height a brush set back to the base still counts as edited. heightis metres added to the base at that point.paintis an index intopalette, the materials this map painted with: each painted cell’s base material.overlayis an index intopalettetoo, the second material a cell mixes in, andblendis how much of it shows, from 0 to 1. A map that paints no overlay writes neither grid.scatterholds one grid per family,tree,bush,rockandtuft: a multiplier of the map’s density, from 0, none, to 2, double. Thetuftgrid scales the grass blades’ density too, as Cover says.erasedholds each family’s instances the Erase brush, Remove this or Make editable took out, each by its spot, its exact [x, z] in metres, which names it among its family’s, as Unreal’s and Unity’s foliage erase take instances out. A More stroke over a spot grows it back. A spot names nothing once the map’sseedorscatterchanges so that no instance of its family stands there:spawnite map checkcounts those spots per family, and the terrain’s next write drops them and keeps each spot an instance still stands at, judged by the placement the scatter itself runs.- A grid no brush touched is left out.
sizeandcellare the map’s when the block was written, so a resample finds the old grid.baseis a copy of the map’s fields that shape the ground,seed,hills,regions,pathsandrim, recorded at each write.spawnite map checkcompares it with the map to say when the agent changed the base under an edit, andkeep-heightsbakes it to recover the old base heights.
The map’s revision, beside the block, counts every write to the file, its terrain, materials, props and sea alike, so a write made against an older file is refused rather than lost, as Saving says.
A block whose size or cell differs from its map’s is refused at load, and spawnite map check names it. spawnite map terrain resample rewrites it at the map’s new grid.
The editor and the commands write the block; nobody writes it by hand. The reads summarise it and never print its rows: spawnite map describe and the MCP’s map_describe give the map’s revision, the points it edits and how far they stand over the base, and list each edited area.
How the ground is built
Section titled “How the ground is built”The ground is the base, then the edits.
-
Height. The base bakes as it does today: hills, then regions, then paths, then the rim. Each edited point then adds its offset.
-
Material. Each cell takes the first of the following that applies, as Minecraft’s surface rules read:
- The cell’s paint, which wins over every rule and brings its own overlay. The Paint tool’s Clear hands a cell back to the rules.
- A path’s
surface, where the path’s weight is over one half: within its half width and half its fade of the path’s line, measured to the cell’s centre. - The first
materialentry ofground.ruleswhose conditions all hold at the cell. ground.steep, the material of ground steeper thanground.steepAbovedegrees,rockabove 35° by default.- A region’s
material, where the region’s weight is over one half: the last such region in table order. - The map’s
ground.basematerial,grassby default.
A cell with no paint then takes the first
overlayentry ofground.rulesthat holds, with itsblend, such as a dusting of snow above 8 m. A rule’s conditions areaboveandbelowin metres,steeperThanandflatterThanin degrees, aregionname, andnoisewith ascalein metres and a thresholdabovefrom −1 to 1. The water conditions match against the map’s sea:deeperThanandshallowerThanby the depth of water over the cell’s centre, andshoreWithinon dry ground by the metres to the nearest water. Where no ground lies below the sea, as on a map left with its default sea, they never match. -
Scatter. The seeded placement runs as today, then each family’s multiplier at the candidate’s cell scales it, and a candidate within 4 m of the sea thins toward its shore, none standing under it. A multiplier under 1 raises the bar a candidate’s draw must pass, so a thinned patch keeps a subset of what stood. A multiplier over 1 adds candidates from a second seeded grid, drawn from other keys, because the first grid holds one candidate per slot: 2 stands the first grid whole and the second whole, double the map’s density. A map with no multiplier over 1 draws no second grid, so an unpainted map stands as it did. An erased instance stands nowhere.
-
Cover. Each cell grows what its materials declare, as thick as their density and the
tuftmultiplier make it. -
Sea. The map’s
sea, one height in metres, puts the ground below it under water. It is a field of the map beside itsterrainrather than in the block, since it is one number an agent reads and writes and it shapes no grid. Every map has a sea: a map that sets none has its sea 1 m below the lowest point of its base, the heights before any offset, so it is dry until a creator digs below that. The default is computed and never written, until a creator or an agent sets a height. The World draws the water at that height across the map, and the navmesh ends where the water is half a metre deep, as the World page describes. Painted water, a pond or a river above the sea, is a later step and not part of the sea.
An offset is always relative to the base, so an agent that reshapes the hills keeps the creator’s edits on top. Flatten writes the difference between the level the creator chose and the base under each point. If the agent then raises the hills under a flattened square, the square rises with them and is no longer level. spawnite map check names each edited area whose base changed, and spawnite map terrain keep-heights rewrites the offsets so every edited point keeps the height it had at the last write: it bakes the copy in base for the old ground, adds the offsets for the height the creator saw, and subtracts the new base. spawnite map terrain move-with-ground keeps the offsets instead and records the new base, so the edits ride on it.
Materials
Section titled “Materials”The engine ships fourteen ground materials: grass, dry-grass, dirt, mud, gravel, sand, snow, ice, rock, cliff, stone, shallow-bed, deep-bed and grid. Dirt, stone and sand are the three path surfaces a map can already name. Each but grid comes in two sets of textures, and a third set draws none:
- Stylised, the default: hand-painted textures generated with Meshy and made to tile.
- Photographed: CC0 textures from Poly Haven and ambientCG, Holdfast’s among them. Snow and ice draw the stylised set’s textures in both.
- Flat: each material in its plain colour, with no textures to load, for a game that lays its own surface over the ground. Holdfast sets it: its own grass, roads and cobbles cover the engine’s ground. Unity’s Terrain turns its surface off the same way, with
drawHeightmap.
Each material is a folder under packages/assets/textures/terrain/<material>/ holding, per set, a colour texture and a detail texture, <material>-<set>.webp and <material>-<set>-detail.webp. The detail texture holds the normal’s x and y in red and green and the height in blue. packages/assets/scripts/terrain.mjs builds both from a source image, and makes a generated image tile. Every file is listed in the assets catalog. grid has no textures in any set: it draws its plain grey with a darker line every metre, the floor of the engine’s grid map, and loads nothing. Its tint changes the grey, and a material made from it keeps the lines. The engine’s table of the built-ins, with each one’s tile size, seam sharpness and roughness, is groundMaterialLibrary. Each built-in names its family, the row the editor shows its tile in, such as grass for dry-grass, and a material made from one joins its family. A game loads only the materials its maps resolve to.
A game keeps the look its maps share in src/maps/materials.json, which registerMaps picks up from the glob the game already passes:
{ "set": "stylised", "materials": { "grass": { "tint": "#8fbf5a", "tile": 3 }, "lava": { "from": "rock", "animate": { "scroll": [0.05, 0] } } }}A map’s own materials field overrides the game’s, entry by entry and field by field, for that map alone. Unity shares a TerrainLayer across scenes the same way; Roblox keeps its MaterialService per place, so an experience with many places repeats its variants in each.
- An entry named after a built-in changes that material wherever it stands.
- An entry with a new name and
fromis a new material, painted like any other. A name that is neither built in nor madefromone stops the ground with an error that names the fix. tintmultiplies the colour, andtintAmount, from 0 to 1 and 1 when left out, says how much of it applies: the ground drawsmix(colour, colour * tint, tintAmount), as Blender’s mix factor does, where Roblox and Unity only multiply.tileis the metres one repeat covers, andbumpsscales the detail’s normal, from 0 to 2.sharpness, from 4 to 64, sets how crisp the material’s seams are.hexTilingbreaks up a repeat that shows, at three samples a read.rotate, on unless the texture has a direction, as sand’s ripples do, turns the texture a random way in each cell.wetplays rain ripples on the material, and darkens and shines it. It is each material’s own, and every material starts dry: a creator turns Wet on for the one that should shine, and one who wants wet and dry dirt duplicates dirt and turns Wet on for the copy.animatemoves the material:scroll, repeats a second along x and z, as Unreal’s Panner does, orframesstacked down the texture withframeTime, as a Minecraft.mcmetadoes.textureswaps the colour texture for one from the game’s public folder.coveris what the material grows on its own, as Cover says: grass on the built-in grass and dry grass.
Refining a material in the editor changes its entry: its tint and the tint’s amount, its tile size, its bumps’ strength and Wet. A new texture for a material, from the asset store or elsewhere, is not built. Each change shows on the ground as its slider moves, and where the drag ends it becomes one undo step that Save writes. Duplicate makes a new entry from a material, such as rock-2 with "from": "rock", and holds it for Paint, so the areas already painted keep their look. spawnite map material <name> and the MCP’s edit_material write the same entry.
The engine’s ground draws every material in one pass of three’s standard material, so the scene’s lights, shadows and fog fall on it. A control texture holds one texel per cell: its base, its overlay and the overlay’s blend, as Terrain3D’s control map does. The colour and detail textures of the map’s materials sit in two texture arrays, up to 16 layers. Where cells meet, each material’s height decides which shows, as in The Witcher 3 and Terrain3D:
- Each pixel weighs the four cells around it bilinearly, from one cell’s middle to the next’s. Each cell’s base counts by 1 − blend and its overlay by blend, so a material’s share at a pixel is the cells’ shares interpolated: a 40% overlay reads as a soft patch rather than a checker of cells, and a painted patch’s edge follows a smooth line rather than the cells’ steps, at every distance.
- The weight is the material’s share plus its height, raised to its sharpness, then normalised, so where two materials meet the taller texture shows. A material that keeps its angle is sampled once for all the cells it covers.
- At Medium quality and up, noise bends the cell grid, so no seam runs straight.
Four alike cells with no overlay take one sample of each texture where the material keeps its angle, and at Low and Minimum quality, every material; a material that turns takes one sample per cell’s angle over the quarter of a cell each side of an edge, the taller angle showing. At Medium and High, macro variation tints the ground by broad noise, so a field of one material never reads flat, and each cell turns its texture. Low and Minimum drop the turns, the variation and the bent seams; the seams stay height-blended along the cell grid. Animation and ripples fade out between 20 m and 35 m from the camera, so a zoomed-out view stays calm and skips their cost. They move on the effects’ clock, as the grass’s wind and the sea’s waves do: the devtools’ pause and a pausing modal hold them, and the devtools’ speed leaves them at the wall clock’s pace.
A material declares the cover it grows, as Unreal’s Landscape Grass Type hangs off a landscape layer and Roblox’s Grass material grows its blades when the terrain’s Decoration is on, so a creator paints one thing and an agent writes one entry. Unity paints its grass as a detail layer apart from the material, two things to paint for one look. The built-in grass grows green blades, and dry grass straw-coloured ones, its swatch’s colour:
"grass": { "cover": { "kind": "grass", "density": 1 } }kindis what grows:grasstoday.density, from 0 to 2, is how thick, 1 when left out; 0 grows none.heightis the tallest blade in metres, 0.32 when left out.colouris the blades’ colour, the material’s swatch as its tint changes it when left out.swayis whether the blades sway in the wind, true when left out.
A cell grows its base’s cover by 1 − blend and its overlay’s by blend, so grass under a 30% dirt overlay grows 70% as thick, and the scatter brush’s tuft multiplier scales the result, up to 2, the most a patch holds, so Scatter’s grass chip thins and thickens the blades with the tufts. spawnite map query answers a cell’s cover, such as grass 0.8, or bare, and spawnite map describe the share of the map the grass grows on and how thick.
The blades come from Holdfast’s, moved into the engine:
- Every blade is one strip of triangles, and every patch of 6 m the same blades in a random order. The vertex shader stands each on the ground’s height and grows it from the cover of the four cells around it, both read from textures, so a stroke rewrites a few texels rather than a buffer of blades.
- Only the patches within the quality level’s reach of the eye draw, each as many of its blades as its nearest point’s distance and its thickest cell keep, and each blade past the share kept where it stands shrinks away rather than popping.
- Blades shrink where a cell’s cover falls, at a material’s edge, weighed from one cell’s middle to the next’s as the ground weighs its materials.
- High draws 220 blades a square metre out to 28 m, Medium 75% of them, Low 30% of them out to 12 m, where the ground’s texture carries the grass beyond, and Minimum none.
- They lie flat round the feet of each player’s character.
- The wind sways them at Medium and High. They stand still at Minimum and Low, under Reduced motion, and on a material whose cover says
sway: false; the player’s Grass sway setting, on the Settings panel’s Video tab, wins over the level and Reduced motion. Still blades skip the wind’s work in the shader. The wind blows on the effects’ clock, which the devtools’ pause and a pausing modal hold and the devtools’ speed leaves at the wall clock’s pace.
The small scatter, bushes and tufts, shrinks away past a distance from the eye the quality level sets, 60 m at High, 45 m at Medium and 30 m at Low, over the last quarter of it, on the blades’ ease, and a pass draws none past it. Trees and rocks draw at every distance, since they are the map’s shape and a creator paints them from above, so the map camera’s far zoom shows the ground, the trees and the rocks. Unity draws details to a detail distance and trees to a tree distance, Unreal culls each foliage type at a distance of its own, Roblox draws its grass to a fixed distance per quality level, and Godot gives each MultiMesh a visibility range.
A game that paints its own surface over the engine’s ground shapes the blades in GLSL through the World’s grass. Holdfast gives every material its map resolves to a cover, and its shape trims the blades by its paint’s own layout, so they give way to its roads, its cobbled ring and its worn earth as before. Painting a material that grows grass pops the cells’ blades up with the scatter’s elastic ease as the stroke is drawn, and painting one that grows none cuts them.
Later layers draw more on the same patches: details only the ground’s shader draws, at no cost in geometry, such as puddles, moss on rock tops, sand ripples and deeper cobbles, and then more 3D kinds, such as flowers, pebbles, reeds by water and leaves under trees.
The editor
Section titled “The editor”The editor lives in the devtools’ edit mode, on the dev server, in a section of its own: Map, on the rail under Entities, opened with ⌘2 to start; the key follows the icon’s place on the rail, as the debugger page says. Its Terrain section holds the tools, the brush and a tile of each material’s texture, in a section per family, Scatter adds its controls to the brush, and the sea’s height has a field of its own. Assets keeps only what a creator places. Unreal keeps Landscape as a mode apart from its content browser, and Roblox’s Terrain Editor is a window of its own. On a phone, Map is a tab of the bar, and its section a sheet. Leaving Map for another section or tab puts the tool in hand down, as Unreal’s Landscape mode does when another mode opens; folding the sidebar keeps it. A game has many maps, so the section’s header names the map it edits, with a chevron, as Figma’s page picker and VS Code’s branch picker do: a press lists every map the game registered, marking the one on screen, and choosing another shows it alone, as ?map=<name> does. The brush, the undo list and every save follow that map by name. Undo is a map’s own, so a switch starts it again. Another map picked over unsaved edits asks “Save changes to meadow?” with Save, Discard and Cancel. The list ends in a muted “Reset meadow to base…”, which Saving the edits describes.
The section reads from the top: the header, with the map’s name at its left and, at its right, Saved or Unsaved, Undo, Redo, Reset and Save, and once an agent moved the base under the edits, its two ways out; the six tools and Clear as a row of icons, the tool in hand filled with the accent. The header and the tool row stay pinned while the rest scrolls under them. Under them, the rest comes in sections that a press on a header folds and opens, each growing out of its header and back into it:
- Brush: the size, in steps of 0.1 m up to 30 m, Sculpt’s Raise or Lower while Sculpt is in hand, and the strength; Flatten’s Level field, Scatter’s families and action, and Fill’s region, each while its tool is in hand.
- The chosen material, headed by its name with Duplicate as an icon at the header’s right: tint and its amount, tile, bumps and Wet. With no material chosen, or with Clear on, the section is not drawn.
- A section per family, such as Grass and Dirt, with a tile of each material’s colour texture, and every family of one under Other.
- Map, last: the Sea field.
Every section starts open, and the overlay keeps which ones are folded while another section of the sidebar shows, as the Wiki section keeps its own. Unity’s terrain inspector and Unreal’s Landscape mode fold the brush settings, the target layer and the layer list the same way, and Roblox’s Terrain Editor groups Brush Settings and Material Settings. A tile chosen holds Paint, unless Fill is in hand, which keeps Fill; the choice is what Paint and Fill lay next. The round Map camera button opens the section as the camera comes on.
The tools are the following:
| Tool | What a stroke does | Roblox |
|---|---|---|
| Sculpt | Raises the ground under the brush; set to Lower, or with Ctrl held, lowers it. | Sculpt, Draw |
| Smooth | Moves each point toward the average of its neighbours. | Smooth |
| Flatten | Moves each point toward its level, or the height where the stroke started in auto. | Flatten |
| Paint | Sets the material of each cell under the brush; Clear hands it back to the rules. | Paint |
| Fill | Paints a material, or flattens to a level, over a dragged rectangle or a named region. | Fill |
| Scatter | Paints how thick the picked families grow, More toward 2 and Thin toward ½, or Erase takes out each instance under the brush. | object scattering |
The brush is one, shared by every tool, as in Roblox, and it stays set between sessions:
- Size, the radius in metres, from 0.1 to 30; a dab too small to reach a point or a cell’s centre edits the one nearest the cursor. B and a drag,
[and], or the ground’s menu change it. - Strength, from 0.1 to 1. Shift and B, and a drag, change it.
- Shape, a circle or a square, with a smooth falloff to the rim. The editor draws a circle; a command or the MCP asks for a square, and the editor has no button for it yet.
Sculpt raises the ground until its Raise or Lower toggle, beside the size, is set to Lower, which the browser keeps with the brush. Ctrl held flips it for a stroke, and the toggle shows the flip while Ctrl is held: Roblox flips with Ctrl alone, and Unity’s Raise or Lower Terrain tool lowers with Shift, so a creator who never learns a key still finds the toggle. Lowering, and a Flatten level below the ground, cut below the map’s base, which is how a pond gets its bed.
Shift held with any height tool smooths while it is held. Alt and a click samples what is under the cursor, as an eyedropper does: the ground’s height into Flatten’s level while Flatten is in hand, the material for Paint and Fill with another tool in hand, and the ground’s height into the Sea field with no tool in hand.
Flatten levels to the height in the section’s Level field, in metres. The field takes a typed height, or a drag of its label, 0.1 m for each 4 px; Alt and a click on the ground, or Pick height in the ground’s menu, sets it to the height there. Empty, the field reads “auto”, and each stroke levels to the height where it starts; “auto ×” beside a set level empties it again. While Flatten is in hand the level shows as a translucent grid plane over the whole map, a line at each cell’s edge, in the accent: at the field’s level, or in auto at the height under the pointer, following it, and during a stroke at the height the stroke levels to. The plane is tested against the ground’s depth, so the hills above the level poke through it and the hollows below it show under it, as Roblox’s Flatten plane and the preview grid of Unreal’s Flatten tool show. It loads with the brush cursor, on the dev server alone. Held still, the brush keeps working at its spot, as in Roblox, Unity and Unreal: the stroke dabs there again every tenth of a second.
The Sea field sets the map’s one sea, in metres. Sea is a field rather than a tool, since it has no brush, and it stands in the Map section, last in the panel, whatever is in hand, always showing the sea’s height: the map’s own, or 1 m below its base’s lowest point where the map sets none. It takes a typed height, or a drag of its label, 0.1 m for each 4 px; Alt and a click on the ground with no tool in hand, or Set sea to here in the ground’s menu, sets it to the height there. The water follows a drag at once, and the ground, the scatter and the navmesh follow where it ends, as one undo step. Roblox’s Sea Level is a tool that fills water to a height inside a box it drags out, and Unreal’s ocean is a water body at one height; this sea covers the whole map, and painted water at other heights is a later step.
The brush cursor shows the brush on the ground under the pointer, as Unreal’s Landscape and Unity’s terrain tools project theirs: a faint fill of its footprint, an outline 3 px wide in the devtools’ accent at its rim, and a thinner ring where the falloff starts, inside which a stroke works at full strength. Every tool draws the same shape, and the fill says the tool: the accent for Sculpt, a warm red-orange while it lowers, a neutral grey for Smooth and while Shift smooths, the accent with the level plane for Flatten, and the chosen material’s swatch colour for Paint, or the accent while Clear is on. Fill draws its rectangle in place of the cursor. It follows the ground’s shape and draws over grass and props, so nothing hides it. Picking up a terrain tool turns the map camera on, unless the map or the free camera is on already: a left drag paints, a middle drag grabs the ground, a right drag turns the view and the wheel zooms, as in Roblox Studio. On a phone, one finger paints and two move the view. Escape puts the tool down, and the brush cursor goes with it.
Cursor modes
Section titled “Cursor modes”What the pointer does over the game in edit mode is the cursor’s mode:
- Select picks entities, and a right-click opens an entity’s menu: Inspect, Select parent, Focus camera, Hide, Duplicate, which opens Send to agent with a request for a copy, Delete, Add behaviour, Copy id and Send to agent. No menu entry moves, turns or scales an entity. A terrain tool is never in hand. With an Assets card in hand, a click places the card instead, as Placing a card says.
- Terrain paints with the terrain tool in hand. Picking up a tool, in the section or the ground’s menu, turns it on, and putting the tool down turns it back to Select.
- Look only moves the camera: it pans, turns and zooms, edits nothing and shows no brush cursor. The store holds it, and no control picks it yet.
The ground’s menu switches between Select and Terrain. Blender, Dreams and Forge put their modes in a pie menu at the cursor; the mode row is the plain version of that. Scatter is a tool of Terrain rather than a mode: it paints with the same brush, cursor and undo list as Paint.
A stroke is resampled along its path every quarter of the brush’s radius, so a fast drag and a slow one paint the same ground. The canvas shows the stroke as it is drawn, applying only the dabs each move adds, and the stroke becomes an unsaved edit when the pointer lifts. A material the map does not draw yet shows from the first stroke, because the material in hand has a layer of the ground’s textures as soon as Paint or Fill holds it.
Each cell a Paint or Fill stroke changes for the first time pops up and settles back: a rise of 0.8·e^(−5t)·sin(3πt) metres over 280 ms, starting 28 ms later for each metre from the pointer, and each corner lifting by the mean of the cells around it. Reduced motion, the player’s setting or the device’s, turns the pop off.
Fill’s drag draws a rectangle from where it starts to the pointer, and the ground shows the material over it until the pointer lifts. With Fill in hand, the section also picks one of the map’s regions and fills it. Clear, beside the tools, turns Paint and Fill into handing each cell back to the map’s rules.
The ground’s menu
Section titled “The ground’s menu”A right click on bare ground in edit mode, one that moves less than 4 px, opens a menu at the pointer; a right drag still turns the view, as in Unreal. In Select mode, a right click on an entity opens the entity’s menu instead. While the Map section or the map camera is on, the ground’s menu opens wherever the ground is under the pointer, grass and all, so a creator starts and undoes from the ground rather than from the panel. The menu’s header says what is there, as spawnite map query answers it, such as “grass (dirt 30%) · 3.2 m · slope 12°”. Under it:
- The mode row: Select and Terrain as icons, the one on filled. Terrain keeps the menu open, so its tools show; Select closes it.
- The tools, in Terrain mode: Sculpt, Smooth, Flatten, Paint and Fill, the one in hand filled. Picking one holds it.
- The brush, while a tool is in hand: sizes of 1, 5, 10 and 15 m, a thirtieth, a sixth, a third and a half of the largest brush, the current one filled where it matches.
Then, under a divider, what to do at the spot, in this order:
- Send to agent opens Send to agent with the spot: the map, x, z, the height and the header, so “make this a pond” needs no coordinates.
- Play from here stands the player on the ground at the spot, facing the way the camera looks, with a camera that follows them behind them, then leaves edit mode and plays, so a creator tests the spot they just sculpted without walking to it, as Unreal’s Play From Here and Roblox Studio’s Play Here do. In a map preview, which has no player, it spawns the game’s player there, the one the game’s scenes mount, with what the game’s own component draws round it, such as a held item or a light, and the camera that follows them, as Unreal’s Play From Here spawns the game mode’s default pawn. On a page joined to a room, which places every player, and in a game scene with no player, such as a lobby, the row stays in its place, off, and its tooltip says why. An agent runs the same with
spawnite play from-here --at x,z, and--map <name>opens a map’s preview first. - Pick material makes the material under the pointer the one Paint and Fill lay, as Alt and a click does.
- Remove this, when the click is on one scattered tree, bush, rock or tuft, erases that one, as one undo step, as Scatter by hand says. On bare ground or a prop the row is not there.
- Undo and Redo, with their keys, so nothing needs the panel. With nothing to undo or redo, the row stays in its place, off, and its tooltip says so.
- Other › opens a submenu with the rarer actions, in this order:
- Lower, while Sculpt is in hand, turns Sculpt’s Lower on or off, and says which it is.
- Wet turns the material’s rain ripples on or off, and says which it is.
- Duplicate the material under the pointer, as its Duplicate button does.
- Pick height sets Flatten’s level to the height here, as Unity’s Set Height samples it. The section’s Level field shows it, and its “auto ×” lets each stroke level to where it starts again.
- Set sea to here sets the map’s sea to the height here, as one undo step.
- Reset to base here hands every point and cell under the brush back to the map’s base and rules, as one undo step.
- Copy position puts
x, z, heighton the clipboard.
Other opens on a click, or once a mouse rests on it for 120 ms, beside the menu on its right, else its left, else over it, level with its row. The submenu grows from the edge nearest Other and shrinks back into it when it closes, never a fade. Escape, a press outside the submenu, or the mouse off both Other and the submenu for 120 ms closes it alone, and it closes with the menu. A click on Other that a hover opened keeps it open.
The arrows move through the menu, and through the submenu while it is open, Enter picks, and every pick but Terrain and Other closes it. Escape closes it and puts the tool in hand down. A phone does not open the ground’s menu: a finger held on the ground paints or turns the view, and the Map sheet holds every action the menu offers but Play from here.
Placing a card
Section titled “Placing a card”A press on a card in the devtools’ Assets section, a model, a scatter kind or a shape, picks it up, and a still click on the ground stands it there. Roblox drops a Toolbox model where the cursor points, Unity and Unreal drop a content-browser asset on the surface under the pointer, and Godot drops a scene from its FileSystem dock; each keeps the placed object selected, and so does this editor. It places with a click rather than a drag, so a phone’s tap places too and the card stays in hand for the next one, as Unreal’s placement mode keeps an actor type in hand.
- A model card’s model starts loading when a mouse rests on the card, focus reaches it or it is picked up, through the loader the placed prop draws with, so the click stands it at once rather than when the file lands. Nothing else in the catalog loads ahead.
- The card in hand turns the map camera on, as a terrain tool does, and puts any tool down: the cursor’s mode is Select with a card in hand. A drag still moves the view, and the click selects nothing else.
- The brush cursor shows the card’s footprint on the ground under the pointer: a metre’s square for a shape, a model’s measured radius, and half a metre where the manifest measured none.
- A click writes a prop into the map’s
propsat the hit point,[x, z], which stands it on the ground’s height, rounded to the centimetre:{ "model": "villager" }for a model,{ "kind": "treeRound" }for a scatter kind and{ "kind": "box" }for a shape, with the schema’s defaults written out, so the fields an agent or a creator can change are in the file. Its name is the card’s, thenvillager-2and on where that name stands. - The placed prop is selected once the World draws it: a model or a shape as its entity. A scatter kind stands among the scatter, which selects none.
- A placed model or shape grows in from the ground at the click with the scatter’s pop, scaled about its base, as Scatter by hand describes. The props a map opens with, and a prop that moves, stand at once.
- Each placing is one undo step, and Save writes the props placed and taken out since the last save in one write of the
propskey, through the writerspawnite map placeand the MCP’splace_propsuse, so a creator’s placing and an agent’s are the same edit. The first placing on an engine map, such as meadow, writes the map into the game. - Escape, a second press on the card, a tool picked up or leaving Assets puts the card down. An avatar’s card places nothing, since a character is placed as an NPC with
add_npc.
Rotating and scaling on the drop, snapping to a grid and scattering by brush are not part of placing: a placed prop turns and scales through its yaw and scale in the map’s props, and Scatter paints families.
Scatter by hand
Section titled “Scatter by hand”With Scatter in hand, chips pick the families a stroke paints, trees, bushes, rocks and grass, the grass chip painting the tufts and the blades alike, any of them at once, and More, Thin or Erase says what it does. With More or Thin, each cell under the brush moves from the multiplier it holds, 1 where the map’s density decides, toward 2 or ½ by the brush’s weight there, as Flatten moves a height toward its level, and takes the stroke once at its strongest, as Paint does. Erase takes out each instance of the picked families that stands inside the brush, judged by its own spot rather than its cell’s, whatever the strength, and writes it under erased; the grid stays as it was. More over the spot grows the erased ones back, and so does Clear. A right click on one tree, bush, rock or tuft offers Remove this in the ground’s menu, which erases that one. Unreal’s and Unity’s foliage erase take out the instances under the brush the same way. The scatter stands again when the pointer lifts, and each tree, bush, rock or tuft new to the ground grows in over 400 ms, snapping a fifth past its size, dipping once and resting, held back by up to 100 ms so a patch ripples in; reduced motion turns it off.
In edit mode a tree, bush or rock already answers a click. Its Inspector gets a Make editable button, which writes a prop of that scatter kind at the instance’s position, turn and height, named for its family, such as tree1, and erases the seeded copy, as Remove this does: one undo step, which Save writes. The Inspector then follows the prop, and shows where it stands as x and z, which a typed value moves, and Delete, each one undo step too. The prop stands unleaned. Grass tufts stay painted only.
Each stroke is one undo step, whatever its length. The step holds the exact values of every point and cell the stroke touched, before and after. ⌘Z undoes it and ⇧⌘Z redoes it on the page, and each is an unsaved edit like a stroke, so an undo past the last save marks the map unsaved.
A sea set, by the Sea field, Alt and a click or Set sea to here, is one step on the same list, holding the sea before and after. A material’s refine is a step on it too: one per slider drag or tint pick, holding the material’s entry before the drag and after it, so an undo shows the entry whole and a material the map did not refine before goes back to the library’s look. Duplicate is one step too, and its undo takes the new material out and holds the one it was made from. Unity, Unreal and Roblox all undo a change to a material’s settings, and a creator with no code has no other way back.
Saving the edits
Section titled “Saving the edits”Edits stay in memory until the creator saves, as in Roblox Studio, Unity, Unreal and Godot, which all save a scene with Ctrl+S. A stroke, a fill, a refine, a Duplicate, a placed card, an undo and a redo each show at once and mark the map unsaved; nothing reaches the file until Save:
- Save,
⌘Sor the header’s Save button, writes every point, cell, material and prop the edits since the last save touched, and the sea, at the values the page shows. The terrain goes in one write of theterrainkey, each material in one write of its entry, the props in one write of thepropskey, and the sea in one write of theseakey. A map whose sea was never set leaves the key out. The header then says Saved, and the undo list stays. - Reset, beside Undo and Redo, drops the unsaved edits: the page shows the map file again, as one undo step, so an undo brings the edits back.
- Reset meadow to base…, the map picker’s last row, hands every point and cell of the map back to its base and rules, the edits an agent’s
edit_terrainmade included, and writes it at once. It asks first: “This removes every edit to meadow. The file’s history in git is the only way back.” It is one undo step, which brings the edits back as unsaved ones. - Leaving the page asks while anything is unsaved, and so does picking another map.
Agents and the CLI still write at once, since an agent holds no session. A write from outside while nothing is unsaved reloads the terrain and clears the undo list, and the editor says so. Over unsaved edits, the header says Changed on disk instead and keeps the edits, and a Save refused because the file moved on says the same. Reload then takes the file, and Save anyway lays the unsaved edits over the file as it now stands and writes them: the creator’s points and cells take the values the page shows, and the agent’s edits elsewhere stay. Play from here and the game read the page’s terrain, so unsaved edits are playable.
A Save over edits whose base the agent moved is refused, and the editor offers two buttons: Keep my heights gives each edit back the height it had, and Move my edits with the ground records the new base under them, so they rise and fall with it. The Save then runs again. An agent makes the same choice with spawnite map terrain keep-heights or move-with-ground.
The editor reads and writes the map through the dev server’s /__map route, which the game’s Vite config mounts with devServer(), as the dev server page shows. On a dev server without it, Save, Reset and picking another map over unsaved edits keep the edits, and the Map panel says the server has no map endpoint and names the import and the call to add.
The commands
Section titled “The commands”edit_terrain in the MCP and spawnite map terrain <tool> in the CLI run one tool, with the same brush the editor uses:
spawnite map terrain sculpt --map meadow --at 12,40 --radius 6 --strength 0.5spawnite map terrain flatten --map meadow --along 0,0:20,0 --radius 4 --level 2spawnite map terrain paint --map meadow --along 0,0:20,0 --radius 3 --material stonespawnite map terrain fill --map meadow --region beach --material sandspawnite map terrain scatter --map meadow --at -30,8 --radius 10 --family tree --multiplier 0.5spawnite map terrain scatter --map meadow --at -30,8 --radius 10 --family tree --erasespawnite map terrain sea --map meadow --level 1- A stroke takes
--atfor one dab, or--alongfor a line of points, resampled as the editor resamples a drag. - An area takes
--rect x,z:x,zor--region <name>. --lowerstands for Sculpt’s Lower,--clearfor Paint’s Clear, and--erasefor Scatter’s Erase, which answers how many instances it took out.seatakes--levelin metres, and no brush.
The reads learn the edits:
spawnite map query --atanswers the material, the cover and whether the point is edited, beside the height it answers today.spawnite map describesummarises the edits by area, such as “34 cells painted cobbles near 20,-4” or “a 12 m square flattened to 2 m at 0,0”, counts the scatter by family once the multipliers scaled it, lists each area a brush painted a family’s multiplier in, such as “tree painted ×0.5 to ×0.9 over 812 cells near (0, 0)”, and each family’s erased instances, such as “tree erased 12 between (-28, 2) and (-20, 14)”.spawnite map describeandspawnite map checkgive the sea’s height, saying when it is the default below the base, the share of the map under it and the metres of its shoreline.spawnite map checknames a terrain block that does not fit its map’s grid, edits whose base changed, each family’s erased spots that no longer name an instance, and a view whose target, a path whose line or edge, or a prop standing on the ground under water.
Saving
Section titled “Saving”One function in the engine applies a tool to a terrain grid. The canvas runs it on every stroke, for the preview and for the edit the page keeps. The CLI and the MCP run it on the map file. A dev-server route, /__map, writes the editor’s Save as a restore of the exact values of every point and cell the edits touched, so the file ends as the canvas shows it.
Every write to a map file goes through one writer: the route’s, the CLI’s and the MCP’s strokes, the route’s placed cards and sea, and place_props. Each caller owns one key, terrain, props, sea or an entry of materials, and the writer changes only that key and the map’s revision, so a stroke and a prop placed at the same moment both land:
- It takes a map’s name, never a path, and the route accepts only requests from the dev server’s own page. The route writes the
terrainkey, one entry ofmaterials, one entry ofprops, as Make editable and a move write it, or theseakey. - It rereads the file, replaces only the caller’s key and leaves every other character as it was, so the agent’s hand-written fields do not move. It formats the key as the repo’s prettier does, one grid row to a line, so a stroke’s diff shows only the rows it touched.
- It validates the whole map with the schema before it writes.
- It counts every write in the map’s one
revision, and refuses a write made against another, whichever key it writes, so a stale editor’s materials and props are refused as its terrain is, and the editor says the map changed on disk. A command an agent runs writes on top of whatever is there. - It queues writes to one map, so two writers never interleave. A write whose file another process changed while it ran is refused and made again on the new file.
- It holds a lock across processes,
proper-lockfile’s, which a dead writer’s successor takes over. - It writes to a temporary file and renames it over the old one, so a crash never leaves half a file. The lock and the temporary file sit in the game’s git-ignored
.spawnite/maps, so nothing a crash leaves lands insrc/maps.
The dev server keeps the page as it is for the editor’s own write. When a write from anywhere else changed only the terrain key, it sends the page the new block rather than reloading it; any other change to a map file reloads as Vite always has.
After a stroke ends, the ground rebuilds what the height feeds: the mesh’s positions and normals under the stroke at once, and at the stroke’s end the physics heightfield, the navmesh, the scatter’s heights and every prop that stands on the ground.
Build order
Section titled “Build order”The work runs in the steps below, one pull request each. Each ends with a tool that works in the editor and as a command:
- Height. The terrain block and its schema, height offsets in the ground’s bake, the
/__maproute and the writer every map file goes through, Sculpt, Smooth and Flatten, the brush, undo, and the reads and checks for height. - Materials. The fourteen materials and their textures, the engine’s ground material, the map’s rules and
materialsfield, Paint and Fill, refining a material, and the reads for materials. - Scatter. The scatter brush, the second seeded grid for multipliers over 1, and Make editable.
- Cover. Grass in the engine, grown from the materials that declare it, Holdfast’s grass moved onto it, and the reads for cover.
- Sea. The map’s sea height and its field, its water plane and the coast rule.
| Name | Chosen | Also weighed | Precedent |
|---|---|---|---|
| The terrain edits | a terrain key in <map>.json |
a file of its own beside the map | Roblox’s place file, which holds its terrain beside everything else |
| The command | edit_terrain, spawnite map terrain |
terrain_brush, sculpt_map |
Roblox’s Terrain calls, Terrain3D’s Terrain3DEditor operations |
| The rules field | ground |
terrain, surface |
Terrain3D’s auto-shader, Roblox’s Generate biomes |
| The look field | materials |
groundMaterials, layers |
Roblox’s MaterialService, Unity’s TerrainLayer, Unreal’s layer info |