Builtin Library
readme.md
pixelArt system
Self-contained pixel-art engine for Zero — the 2D counterpart to the
voxelEngine system. Lives entirely under @builtin::systems.pixelArt and is
the only path through which pixel-art content is created or edited.
Where voxels are a 3D grid of cubes, a pixel canvas is a 2D grid of colored cells rendered as a flat mesh: every non-empty pixel becomes a quad carrying that pixel's color as a per-vertex color, so the art renders with no texture and no per-pixel material. Sheets can be single-frame (a sprite, a sign, a floor tile) or multi-frame animations played back on a timer.
Layout
systems/pixelArt.package/
pixelArt.module/ -- the API
init.luau -- public surface (Pixel.create, Pixel.fromRows, ...)
canvas.module/ -- internal: 2D grid + palette + raster draw ops
mesher.module/ -- internal: pure flat colored-quad mesh builder
config.module/ -- internal: path defaults + knobs
pixbin.module/ -- internal: JSON (de)serializer for templates
PixelCanvas.component/ -- internal per-entity component (data + frames + mesh)
pixelTemplate.assetType/ -- typed asset for saved (reusable) pixel art
tools/pixelArt.toolbox/ -- user-facing tools (Pixel.spawn / draw / anim / ...)
demo/ -- showcase scenes (office, gallery)
documentation/ -- mental model + API + drawing + animation guides
The component is internal. Don't add it by hand or edit its fields by
hand. The module API spawns the entity, registers the component, and routes
every edit through setPixel / fillRect / fromRows / animation ops.
Two kinds of content
| Kind | What | Use for |
|---|---|---|
| Static sheet | one frame (fps = 0) | sprites, signage, framed art, floor tiles, map art |
| Animation | many frames + fps > 0 | walk cycles, blinks, flickers, spinners — any motion |
Both are the same PixelCanvas; an animation is just a sheet with more than
one frame and a non-zero fps. Frames share one palette and one width/height.
Layout plane
plane | Quads in | Front normal | Use for |
|---|---|---|---|
"xy" (default) | XY plane | +Z | upright sprites, billboards, signage |
"xz" | XZ plane (y = 0) | +Y | flat top-down floor art / map tiles |
Quick start (custom code)
local Pixel = require("@builtin::systems.pixelArt.pixelArt")
-- A static sprite painted from text rows (rows[1] is the TOP row on screen).
local heart = Pixel.fromRows({
rows = { " r r ", "rrrrr", "rrrrr", " rrr ", " r " },
legend = { r = "red" },
palette = { red = { 0.9, 0.1, 0.2 } },
position = { 0, 1, 0 },
})
-- A 4-frame spinner animation.
local spin = Pixel.create({ width = 8, height = 8, palette = { c = {0.2,0.8,1} }, fps = 8 })
spin:fillRect(0, 3, 7, 4, "c") -- frame 1: horizontal bar
spin:duplicateFrame(); spin:setActiveFrame(2)
spin:clear(); spin:drawLine(1, 1, 6, 6, "c") -- frame 2: diagonal
spin:duplicateFrame(); spin:setActiveFrame(3)
spin:clear(); spin:fillRect(3, 0, 4, 7, "c") -- frame 3: vertical bar
spin:duplicateFrame(); spin:setActiveFrame(4)
spin:clear(); spin:drawLine(1, 6, 6, 1, "c") -- frame 4: other diagonal
spin:play()
Persistence
A canvas's pixel data + every frame live INLINE in the component (public.frames
/ public.palette) and serialize with the entity — pixel art is small, so no
external data file is needed in the normal case. saveAsTemplate(name) is the
explicit "export this sheet (all frames) as a reusable pixelTemplate asset"
path; Pixel.fromTemplate(name) stamps an independent copy into a scene.
Internals
canvas.module— dense flat array (cells[1 + x + y*width]), palette indexed by name, raster ops (setPixel/fillRect/drawRect/drawLine/fillCircle/drawCircle/fillEllipse/fromRows/clear/setPalette),bounds/countByName, andbuildMeshData. Single-frame; pure data — no entity / mesh / animation knowledge.mesher.module—build(opts)emits a flat colored-quad mesh in the XY or XZ plane, with optional same-color run merging (greedy) and double-sided faces. CCW-front winding ready forrenderer.mesh.create.PixelCanvas.component— holds the frame list, drives playback, caches one mesh per frame (built once, swapped on frame change), and renders via a temporary child entity carrying a Model + the shared white PBR material.
Render backends (renderMode)
A canvas renders one of two ways (per-canvas renderMode):
| Mode | How it renders | Geometry | Best for |
|---|---|---|---|
"mesh" | one greedy-merged colored quad per pixel run (vertex colors) | scales with color spans | small sprites; true per-pixel transparency |
"texture" (default) | a single quad textured with a runtime RGBA texture via renderer.texture.create, nearest-sampled | O(1) (1 quad) regardless of resolution | large / mostly-opaque sheets (floors, maps, big backdrops) |
texture mode is built on the engine's generic runtime-texture primitive
renderer.texture.create({ width, height, rgba, srgb? }) (returns a TextureHandle
whose .guid binds as any material's texture) — not pixel-art-specific. Runtime
textures sample with nearest-neighbour filtering by default (crisp). The office demo's
floor uses renderMode = "texture" (one quad for an 11k-pixel sheet); its
sprites stay on "mesh" for per-pixel transparency.