Builtin Library
README.md
grid
Voxel grid + named palette. The data backbone shared by worlds and shapes.
Holds blocks, supports edits, and builds meshes via the sibling mesher
module. Pure data — no entity / component knowledge.
Two storage modes selected at create time, unified behind the same method surface on the returned instance:
- dense — one flat array of size
sx * sy * sz. Best for small, fixed-extent shapes (props, vehicles). - chunked — sparse map of fixed-size sub-grids keyed by chunk coordinates. Best for large worlds where most space is empty and writes are local. Never densifies.
Dense allocations are capped by MAX_DENSE_VOXELS = 2^24 to prevent Luau
VM table overflow crashes (issue #1276). Callers asking for more space
than the cap must opt into chunked = true.
Exports
M.create(opts: CreateOpts) -> Grid— construct a new grid instance.M.MAX_DENSE_VOXELS: number— hard cap (2^24cells) on dense allocations.
Types:
Vec3 = { x: number, y: number, z: number }ColorTriple = { number }—{ r, g, b }or{ r, g, b, a }ColorLike = ColorTriple | { r, g, b, a? }PaletteEntry = { name: string, color: ColorLike?, material: string? }CreateOpts = { size: Vec3, blockSize?: number, origin?: Vec3, palette?: { PaletteEntry }, grid?: { number }, chunked?: boolean, chunkSize?: number }Grid— closure-built instance. Method surface below.
Instance methods (returned Grid)
Palette and bounds (mode-agnostic):
g:setPalette(palette)— replace the palette wholesale. Invalidates every chunk's mesh in chunked mode.g:addPaletteEntry(name, color, material?) -> index— add or update a palette entry by name.g:paletteIndex(name) -> integer— look up the palette index forname. Raises on unknown name. Returns 0 fornil.g:inBounds(x, y, z) -> boolean— true iff(x,y,z)is in the grid's declared extent.
Block reads / writes (both modes):
g:setBlock(x, y, z, name)/g:setBlockIndex(x, y, z, idx)— write by palette name or raw index.g:getBlock(x, y, z) -> string?/g:getBlockIndex(x, y, z) -> integer— read the palette name (ornil) / raw index.g:fillBox(min, max, name)/g:fillSphere(center, radius, name)— bulk paint.g:clear()— empty the grid (dense fills with 0; chunked drops all chunks).
Dirty tracking + meshing:
g:isDirty() -> boolean— whether anything's pending rebuild.g:dirtyChunks() -> { [chunkKey]: true }— set of chunks with pending rebuild flags (empty in dense mode).g:clearDirty(key?)— clear all (no arg) or one chunk's dirty flag.g:buildMesh(greedy: boolean?) -> MeshDescriptor— mesh the entire grid via the sibling mesher.g:buildChunkMesh(key, greedy: boolean?) -> MeshDescriptor?— chunked-only; mesh a single chunk with cross-chunk neighbor lookups for correct face culling at seams.
Chunked-only:
g:chunks_sorted() -> { { key, chunk } }— deterministic chunk enumeration sorted by(cx, cy, cz).g:densify()— dense-mode helper; ensures every cell has a defined value.
Querying + bulk ops:
g:bounds() -> { min: Vec3, max: Vec3, blockSize: number }— world-voxel extents.g:getNeighbors(x, y, z) -> { posX, negX, posY, negY, posZ, negZ }— 6-direction palette-name lookup.g:queryRegion(min, max, opts?) -> { { pos, name, index } }— enumerate cells in an AABB with optionalpredicate/includeEmpty/limit.g:countByName(min, max) -> { [name]: count }— count non-empty cells per palette name.g:raycast(originPos, dirVec, maxDist?) -> hit?— Amanatides-Woo voxel DDA in world units.g:sphereQuery(center, radius) -> {...}/g:lineQuery(p1, p2) -> {...}— region queries.g:copyRegion(min, max) -> regionData/g:pasteRegion(offset, regionData, opts?)— copy / paste.g:mirror(axis)/g:translate(dx, dy, dz)— dense-only structural ops (chunked support TBD).g:fillCylinder(p1, p2, radius, name)/g:fillLine(p1, p2, name)— geometric fills.
Usage
local Grid = require("@builtin::systems.voxelEngine.voxelEngine.grid")
local g = Grid.create({
size = { x = 32, y = 32, z = 32 },
palette = {
{ name = "stone", color = { 0.5, 0.5, 0.5 } },
{ name = "wood", color = { 0.6, 0.4, 0.2 }, material = "wood" },
},
})
g:fillBox({ x = 0, y = 0, z = 0 }, { x = 31, y = 0, z = 31 }, "stone")
g:setBlock(16, 1, 16, "wood")
local mesh = g:buildMesh(true) -- greedy = true
Notes
- Dense mode raises on creation when
sx * sy * sz > MAX_DENSE_VOXELS. Callers needing more space must passchunked = true. - Chunked mode never densifies on its own — empty chunks are freed on write so iteration and serialization stay tight.
buildMeshin chunked mode walks chunks in lexicographic order and re-groups per-material submeshes across all chunks; the combined output still has one submesh entry per distinct material.- The mesher resolves materials from
palette[i].material; nil maps to the mesher's default bucket. Palette entries withcoloronly get the default material. mirrorandtranslateare dense-only — chunked support is tracked for a later phase.