Log inGet started
World

Builtin Library

Branchmain
Branchesmainpublish
Commit3bb1da2647

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

KindWhatUse for
Static sheetone frame (fps = 0)sprites, signage, framed art, floor tiles, map art
Animationmany frames + fps > 0walk 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

planeQuads inFront normalUse for
"xy" (default)XY plane+Zupright sprites, billboards, signage
"xz"XZ plane (y = 0)+Yflat 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, and buildMeshData. 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 for renderer.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):

ModeHow it rendersGeometryBest for
"mesh"one greedy-merged colored quad per pixel run (vertex colors)scales with color spanssmall sprites; true per-pixel transparency
"texture" (default)a single quad textured with a runtime RGBA texture via renderer.texture.create, nearest-sampledO(1) (1 quad) regardless of resolutionlarge / 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.