Builtin Library
readme.md
voxelEngine system
Self-contained voxel engine for Zero. Lives entirely under
@builtin::systems.voxelEngine and is the only path through which voxel
content is created or edited.
Layout
systems/voxelEngine/
voxelEngine.module/ -- the API
init.luau -- public surface (Voxel.createWorld, Voxel.createShape, ...)
grid.module/ -- internal: dense voxel grid + palette + edit ops
mesher.module/ -- internal: pure-function mesh builder (simple + greedy)
VoxelWorld.component/ -- internal singleton component (the world entity)
VoxelShape.component/ -- internal per-entity component (props, characters, ...)
readme.md -- this file
tools/voxelEngine.toolbox/ -- user-facing tools (Voxel.createWorld, Voxel.setBlock, ...)
-- Lives here today because toolbox discovery is currently
-- baked at build time and only scans `lib/tools/`. See
-- issue #1220 — the toolbox should colocate under
-- `systems/voxelEngine/voxelEngine.toolbox/` once
-- runtime VFS-driven toolbox discovery lands (parity
-- with how `.module` and `.component` are loaded).
Flow
┌──────────────┐
user / agent ──┤ voxelEngine ├──┐
│ .toolbox │ │
└──────────────┘ │
▼
┌──────────────────┐
custom code ──┤ voxelEngine │── add / edit components
(require) │ .module (API) │
└──────────────────┘
│
▼
┌──────────────────────────┐
│ VoxelWorld / VoxelShape │
│ components │
└──────────────────────────┘
│
▼
Grid → Mesher → renderer.mesh.create
The components are internal. Don't add them by hand, don't edit their
fields by hand. The module API spawns the entity, registers the component,
and routes every edit through setBlock / fillBox / setPalette etc.
Two kinds of voxel content
| Kind | Component | Lifetime | Use for |
|---|---|---|---|
| World | VoxelWorld (singleton — only one per scene) | Entity at the scene origin | Terrain, additive build-out where every voxel sits on the same grid |
| Shape | VoxelShape (one per entity) | Per-entity (move it, parent it, give it physics) | Props, characters, items — anything not part of the world grid |
A shape is independent of the world. It can be parented under any entity, given
its own physics (physics = "dynamic"), and removed without affecting the
world. The world is the additive surface everything else sits on or in.
Tool calls
-- Create / get the singleton world
local w = Voxel.createWorld({
size = { x = 64, y = 32, z = 64 },
blockSize = 1.0,
palette = {
grass = { 0.2, 0.7, 0.2 },
dirt = { 0.5, 0.3, 0.15 },
stone = { 0.5, 0.5, 0.5 },
},
})
Voxel.fillBox("world", { 0, 0, 0 }, { 63, 0, 63 }, "grass") -- ground plane
Voxel.fillSphere("world", { 32, 6, 32 }, 4, "stone") -- a pile of stone
Voxel.setBlock("world", 32, 7, 32, "grass")
-- Create a per-entity prop
local hero = Voxel.createShape({
name = "hero",
size = { 4, 8, 4 },
blockSize = 0.25,
position = { 32, 1, 32 },
palette = { skin = { 0.95, 0.75, 0.6 }, shirt = { 0.2, 0.4, 0.8 } },
})
Voxel.fillBox(hero, { 0, 0, 0 }, { 3, 4, 3 }, "shirt")
Voxel.fillBox(hero, { 1, 5, 1 }, { 2, 7, 2 }, "skin")
-- Persist
world.debugSaveToDisk()
Custom code
local Voxel = require("@builtin::systems.voxelEngine.voxelEngine")
local w = Voxel.createWorld({ size = { 32, 16, 32 } })
w:fillBox({ 0, 0, 0 }, { 31, 0, 31 }, "grass")
The handle returned by createWorld / createShape exposes the same surface
as the tools (setBlock, fillBox, fillSphere, setPalette, clear,
flush, bounds, destroy, exists).
Persistence
Both components serialize their state (size, blockSize, palette, grid,
physics, greedy) with the entity. world.debugSaveToDisk() writes them to disk;
layers.load() rebuilds the Grid + mesh on load. Live edits made via the
API land in the component's public.grid so what you save is what you
ran.
Internals
grid.module— dense flat array (grid[1 + x + y*sx + z*sx*sy]), palette indexed by name, dirty flag,setBlock/fillBox/fillSphere/clear/setPalette/buildMeshops. Pure data — no entity / mesh knowledge.mesher.module—buildSimple(opts)emits one quad per visible face (favored for small props, easy to debug).buildGreedy(opts)merges adjacent same-color faces in the same slice. Both produce CCW-front winding ready forrenderer.mesh.create().- The components hold a Grid instance plus the component's
publicstate. All edits flowVoxel.X(...)→ handle method →comp.X(...)→ Grid mutation →requestRebuild()→ coalescedrenderer.mesh.createnext defer.