Voxel binaries
A voxel
asset-generation run sculpts through a voxel binary on its PATH — the only
channel for placing a voxel, the 3D counterpart of the
drawing binaries. There are two, both built
on the shared model-core library:
voxel— for a static model (asset_kind = "voxel-model"): one opaque-RGB voxel volume, sculpted as a single model.voxel-anim— for a rigged, animated model (asset_kind = "voxel-animation"): the same operations plus a required--part <name>on every op, so each part is a separate volume with its own log and preview, and a set of rig subcommands that build the parts and joints hierarchy and author the animations.
The binaries are built from crates/voxel and crates/voxel-anim on the shared
crates/model-core library — the rig/animation model, the CLI record/preview
plumbing, the cube mesher, and the wgpu renderer — and each is baked into its own
run-container image: voxel into
the voxel image (test-cabinet-voxel) and voxel-anim into the
voxel-animation image (test-cabinet-voxel-animation), so a run carries only
the tool it uses. Nothing is regenerated or re-rendered after the run: the binary
emits the geometry (a per-part .glb) and the rig (rig.json), and the
validator parses those and confirms they are well-formed (see
Evaluation).
Voxels are opaque and the volume starts empty
Section titled “Voxels are opaque and the volume starts empty”Every voxel cell is an opaque #rrggbb color — there is no alpha, so an
operation that touches a cell either sets it to a solid color or clears it back to
empty; nothing composites. The voxel volume always starts empty; the
background a case declares is only the preview PNG’s clear color and
never places a voxel. Coordinates are the volume’s integer grid: x across,
y up, z in depth, each in 0..extent.
The operations are ordinary CLI subcommands
Section titled “The operations are ordinary CLI subcommands”A case seeds no operations schema. The vocabulary is the binary’s own
--help, and the brief tells the model to read it:
voxel --help # every operationvoxel fill-box --help # one operation's exact flagsEach operation is a subcommand with flags — there is no JSON. For example:
voxel set-voxel --x 16 --y 4 --z 16 --color "#c0c0c8"voxel fill-box --x 6 --y 0 --z 10 --width 20 --height 4 --depth 12 --color "#3a4a5a"voxel mirror --plane x --at 16The operations are:
- Placement:
set-voxel,fill-box,line(a 3D Bresenham run of voxels), and the convenience shapesstroke-box(a hollow box),fill-sphere,fill-ellipsoid(a sphere with unequal per-axis radii — domes, eggs, boulders), andfill-cylinder(a disc extruded along a chosen axis — barrels, legs, poles, wheels). - Clearing:
clear-voxel,clear-box. - Whole-volume edits:
mirror(reflect the volume across a plane, handy for a symmetric hull),replace-color(recolor every voxel of one color to another — a palette swap or shading pass),translate(shift every occupied voxel by a vector, clearing what it vacates), andcopy-box(duplicate a source box’s voxels to a destination offset — a second wheel, a repeated rivet).
Coordinates are signed (a shape may be placed partially outside the volume; the
out-of-bounds portion is clipped, never a panic); sizes and radii are unsigned.
Colors are opaque #rrggbb. A set/fill operation replaces the cells it touches,
so the recorded log produces an exact, order-only volume.
How a call records; rendering is on request
Section titled “How a call records; rendering is on request”Each operation only appends itself to the run’s operation log — that is all a
sculpting call does. Unlike the 2D drawing binaries,
the voxel tools do not re-render after every call: meshing a volume and
rasterizing it through the wgpu+Mesa renderer is far more expensive than stamping
2D pixels, and a voxel model takes many operations, so rendering is a separate,
on-request step. The orchestrator seeds a voxel.config.json (static) or
voxel-anim.config.json (animated) next to the workspace giving the volume
dimensions, background, and the log/preview/geometry paths — and, for the animated
tool, the rig.json path — so neither an operation nor render needs any volume
flags.
The render command regenerates the derived artifacts from the recorded log
when the model asks for them: it meshes the model into its per-part .glb — the
surface as a standard glTF 2.0 binary, decoded into the runtime’s PartMesh shape —
and draws the preview PNG from that geometry. A model runs it to read its
progress between edits and, before it finishes, to emit the geometry the run’s
result is built from — an unrendered model leaves no .glb, which the validator
records as an empty part (see Evaluation).
voxel init # write an empty log (a run starts pre-seeded); renders nothingvoxel render # mesh the model to its .glb and draw the preview PNGvoxel render --view front # ...from a chosen camera: iso (default) | front | side | topvoxel render --out check.png # ...to an explicit path (the .glb still goes to its configured path)The preview
Section titled “The preview”The preview render draws is a real 3D render of the meshed model. The binary
meshes the voxel volume into geometry and renders it with wgpu targeting Mesa
lavapipe — software Vulkan, running on the CPU, headless (there is no GPU in the
run container) — through an orbit camera with directional shading, encoded as a
PNG. This generic mesh renderer lives in model-core and serves every voxel-family
binary, so previews are apples-to-apples across tools. It replaces the retired
integer-only isometric rasterizer (crates/voxel/src/raster.rs); the preview no
longer needs to be byte-reproducible, because nothing regenerates it after the run
(see Evaluation). The preview is a still
image; the interactive, rotatable 3D view is the frontend’s three.js rendering of the
emitted per-part .glb (see voxel-runtime),
not something the binary produces.
Live preview
Section titled “Live preview”When a run is being watched — driven by a driver
or the Tauri app rather than a plain tcab run —
the model’s sculpting can be streamed to the viewer in real time, exactly as for
the drawing binaries: the
orchestrator adds a live block to the seeded config, and when the model runs
render the binary connects back to the run host and streams a one-line JSON header
({ token, frame, operationCount, operation, length, meshLength }) followed by the
freshly rendered preview PNG’s raw bytes and then the part’s current .glb bytes
(meshLength bytes) — the same glTF geometry the 3D client renders. (Because a
sculpting operation renders nothing, frames flow only when the model renders; a scene
render streams one per part.) The mesh body
lets the viewer rebuild the model in 3D as it is sculpted — rotating it and
assembling the scene exactly as the finished-run view does — rather than showing only
the still preview PNG; a PNG-only viewer simply ignores it. For an animated model the
frame field carries
the part index, so the viewer can show the most-recently-sculpted part, the
status of every part, and the assembled scene at once (a static model uses part
index 0). Streaming is best-effort and non-essential — absent for an unwatched
run, never fails an operation, and never recorded; the recorded operation log
documents how the model built each part, and the reviewed artifacts are the geometry
and preview the binary emits.
voxel-anim: one volume per part, plus the rig
Section titled “voxel-anim: one volume per part, plus the rig”An animated model is a rig:
named parts in a hierarchy with named joints. voxel-anim is voxel plus a
required --part <name> that selects which part an operation sculpts into; that
part has its own operation log and its own preview, both {part} templates the
case declares (for example parts/{part}.actions.json and parts/{part}.png).
Every part is sculpted in the same shared volume’s coordinates — the full
[voxel] dims — in place where the part sits on the assembled model (a turret
already up on top of the hull, a barrel already out front), not in a private
per-part box. A part’s pivot is the anchor its joints rotate about, not a
placement offset: the voxel-runtime poses a
part by rotating it about that pivot, and at rest a part stays exactly where it was
sculpted. (Sculpting each part in place is what lets the parts be composed into the
assembled model with no per-part offset — see the assembled scene below.)
voxel-anim --help # same operations, plus --partvoxel-anim fill-box --part turret --x 12 --y 8 --z 12 --width 8 --height 4 --depth 8 --color "#4a5a3a"voxel-anim init # seed rig.json (the required animation declarations)voxel-anim render # mesh every part's .glb + draw the assembled scenevoxel-anim render --component turret # ...or just one part's preview + .glbvoxel-anim render --time 600 --animation walk # ...or the model posed at 600ms of the walkThe seeded voxel-anim.config.json carries the {part} templates and the
rig.json path, so voxel-anim init seeds a rig.json pre-populated with the
case’s required animation declarations (empty tracks; its parts and joints
start empty, because a case declares none). No part exists until the model
creates one with define-part — which then initializes that part’s operation log —
so init renders nothing and seeds no per-part previews. The per-part emitted data
and previews are the scored artifacts; the assembled scene below is a non-scored
extra.
The render command
Section titled “The render command”Like voxel, voxel-anim renders only on request — a sculpting operation just
records. Its render has three modes:
render(no options) — render the whole assembled scene: it re-emits every part’s.glband preview from its log (so one call produces all the geometry the run’s result reads and refreshes every scored per-part image), then composes the parts at rest and writes one PNG per view to the config’sscenetemplate (defaultscene/{view}.png). This is the call to run before finishing.render --component <part>— render just that part: its own preview PNG and.glb, at a chosen--view. Cheap for iterating on one part.render --time <ms> [--animation <name>]— render the model posed at that instant of an animation, so you can check how the motion reads (a leg mid-stride, a barrel at full elevation). Each part’s rest mesh is transformed by its animated world transform, exactly as the client poses it.--animationdefaults to the sole or auto-play animation; the posed image goes toscene/pose.png(override with--out) and does not touch the parts’.glbs.
The assembled scene is what catches assembly mistakes a per-part preview can’t: a turret that reads fine alone but sits off-center on the hull, or a barrel that misses the turret front. Its views are:
iso— a 3D orbit render matching the per-part previews, for a read of the whole model.front,side,top— orthographic-camera elevations of the meshed model down each axis, so it is easy to check a part is centered and aligned head-on.
The plain scene composes parts at rest (every joint rests at 0, so this is the
true rest pose); use --time to see joint motion. Neither the per-part previews nor
the scene is a scored artifact.
Rig subcommands
Section titled “Rig subcommands”Beyond sculpting voxels, voxel-anim edits the rig structure in rig.json:
its parts, its joints, and its animations. The case pre-seeds only the
required animation declarations (each just a name plus its loop/auto-play intent
— no parts, no joints, no keyframes) from its [model] table; the seeded rig.json
starts with empty parts and joints. These subcommands are how the model
builds the whole rig — inventing the parts and joints the subject needs and
authoring each required animation’s motion — plus adding any further animations
of its own (the produced rig.json carries everything the model builds):
voxel-anim define-part --name skirt --parent chassisvoxel-anim set-pivot --part turret --x 16 --y 9 --z 16voxel-anim define-joint --name turret_yaw --part turret --kind rotation --axis y \ --pivot-x 16 --pivot-y 9 --pivot-z 16 --min=-3.14159 --max 3.14159 --rest 0 --drive callervoxel-anim define-joint --name barrel_mount --part barrel --kind rotation --axis x \ --pivot-x 16 --pivot-y 10 --pivot-z 20 --min 0 --max 0 --rest 0 \ --orient-x 0.2 --offset-y 1 # a fixed compound attach: mount tilted + raisedvoxel-anim define-animation --name walk --period-ms 1200 --loop true --auto-play falsevoxel-anim add-keyframe --animation walk --joint hip_l --t-ms 0 --value 0.35 --interp beziervoxel-anim add-keyframe --animation walk --joint hip_l --t-ms 600 --value=-0.35 --interp ease-invoxel-anim add-keyframe --animation walk --joint hip_l --t-ms 1200 --value 0.35 --interp bezierdefine-partadds a part under a declared--parent(the first part defined is the root, with no parent) and initializes that part’s operation log so it immediately becomes a--parttarget for sculpting (its preview and.glbare written later, byrender). A field operation on a part that has not beendefine-part’d yet is rejected. Set its pivot withset-pivot. A part sculpted with no voxels is an attach point (amuzzle, an exhaust) — an empty named node a game reads as a socket for a projectile or effect.set-pivotsets an existing part’s pivot — the point, in the shared volume’s coordinates, its joints rotate about.define-jointadds a named degree of freedom on a part — its--kind(rotation/translation),--axis,--pivot,--min/--max/--restrange, and--drive. Acallerjoint is the procedural interface a consuming game drives per frame (a turret’s yaw, a gun’s pitch), exported so the game can drive it within its limits; anautojoint is driven only by the model’s animations. A joint may also carry a fixed compound mount applied in addition to its driven motion:--offset-x/y/z(a fixed translation in voxels) and--orient-x/y/z(a fixed rotation in radians, applied as Euler X→Y→Z about the pivot). This is how a component is attached at a custom rotation and translation — a joint with an empty driven range (--min 0 --max 0 --rest 0) but a non-zero mount is a purely static attachment; a joint with both a range and a mount does both.define-animationcreates or redefines a named animation: its--period-ms(one loop),--loop(loop vs. play once and hold), and--auto-play(whether it plays continuously by default — a decorative idle such as a sweeping radar — versus a named playable the game triggers, such as a walk or a recoil). Its tracks are added withadd-keyframe.add-keyframeadds or replaces one keyframe on an animation’s track for a--joint(the first keyframe for a joint creates that track): its--t-ms,--value, and--interp— the F-curve interpolation of the segment leaving this key (constant|linear|bezier, or an easing presetease-in|ease-out|ease-in-out) — with optional--out-handle <dt,dv>and--in-handle <dt,dv>Bézier tangent handles (omitted, abezierkey uses auto tangents).
F-curves
Section titled “F-curves”An animation track is an F-curve — the graph-editor curve real 3D tools use — so
motion carries weight and snap instead of sliding linearly between poses. Each
keyframe’s --interp sets how the curve leaves it:
constantholds the value until the next key (a step),lineardraws a straight line to it,bezierdraws a smooth curve shaped by tangent handles — an out-handle on this key and an in-handle on the next, each a control point offset from its key as<dt_ms,dvalue>; the segment is the cubic Bézier through them (abezierkey with no handles uses smooth auto tangents).
The easing presets expand to standard handles so common shaping needs no
hand-computed tangents: ease-in starts slow and accelerates into the next key
(the “thump” of a foot-plant or a recoil kick), ease-out starts fast and
decelerates, ease-in-out eases both ends (a smooth, weighty motion). The
voxel-runtime samples these curves when it
poses the rig, and the glTF exporter
bakes them so the eased motion survives into a game engine. See
Rigging and animating walkers for how
to choose curves for a walk cycle.
Rotation direction
Section titled “Rotation direction”The volume is y-up with forward at +z (a part points toward higher z).
For a rotation joint, the sign of a value follows this convention:
- Pitch (
--axis x) — a positive value elevates: it lifts a forward-pointing (+z) part up toward +y, and a negative value depresses it down. So for a gun that points forward,maxaims high andminaims low — e.g. abarrel_pitchwithmin = -0.2(barrel depressed) andmax = 0.8(barrel lobbing high) raises the barrel as the value grows. - Yaw (
--axis y) and roll (--axis z) are right-handed rotations about their axis through the pivot.
Rotation happens about the joint’s --pivot, so place the pivot at the hinge the
part should swing on (the shoulder, the turret ring, the barrel mount).
The case’s required animations are the game-facing contract a reviewer scores
against: the model must author every one so it actually animates, but the parts and
joints that realize them are entirely its own to invent. The rig subcommands load,
mutate, and rewrite rig.json in place, so it stays the single description of the
produced rig, which the validator reconciles against the required
[model] animations and the
voxel-runtime poses.