Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commite09f1e4457

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^24 cells) 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 for name. Raises on unknown name. Returns 0 for nil.
  • 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 (or nil) / 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 optional predicate / 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 pass chunked = true.
  • Chunked mode never densifies on its own — empty chunks are freed on write so iteration and serialization stay tight.
  • buildMesh in 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 with color only get the default material.
  • mirror and translate are dense-only — chunked support is tracked for a later phase.