Mesh binaries
A mesh asset-generation run sculpts through a meshing binary on its
PATH — the only channel for shaping the surface, a surface-extraction sibling of
the voxel binaries. Where the voxel
tools paint discrete opaque cubes, a meshing binary builds a continuous signed-
distance field by compositing primitives — a CSG-style paradigm — and extracts a
triangle mesh from it. That distinct paradigm is why the meshing binaries carry
their own vocabulary and their own crates.
There are three algorithms, each with a static and an animated binary, mirroring
the voxel/voxel-anim split:
| Algorithm | static binary | animated binary | asset_kind (static / animated) | character |
|---|---|---|---|---|
| Marching Cubes | mc | mc-anim | mc-model / mc-animation | low poly — coarse sample grid, chunky faceted surfaces |
| Surface Nets | sn | sn-anim | sn-model / sn-animation | smooth mid-fidelity — watertight, uniform triangle density, rounded features |
| Dual Contouring | dc | dc-anim | dc-model / dc-animation | high fidelity — fine grid, preserves sharp edges and corners |
Each static binary (mc, sn, dc) is for a static model: one field,
extracted as a single mesh. Each animated binary (mc-anim, sn-anim,
dc-anim) is for a rigged, animated model: the same field operations plus a
required --part <name> on every op, so each part is a separate field with its
own log and preview, and a set of rig subcommands that build the parts-and-
joints hierarchy and author the animations.
The mesher lives in crates/voxel-mesh (the SDF field type, sampling, and the
MC/SN/DC extraction), and the shared record/preview plumbing, rig model, color,
config, and the generic renderer live in crates/model-core — the same library
the voxel binaries use. Each binary has its own crate (crates/mc,
crates/mc-anim, and so on) and is baked into its own
run-container image — one image per
asset_kind — so a run carries only the tool it uses.
The field is a continuous signed-distance field
Section titled “The field is a continuous signed-distance field”A meshing binary does not place cells. It maintains a signed-distance field
(SDF) over the volume: at every point in space the field records the (signed)
distance to the nearest surface, negative inside the solid and positive outside.
The surface a binary meshes is the field’s zero level set. You shape that
field by compositing primitives — adding material with a sphere or box, carving
it away with another — and each primitive carries an opaque #rrggbb color
(there is no alpha; nothing composites translucently). The field starts
empty (everywhere outside, no surface), and the background a case declares in
its [voxel] table is only the preview PNG’s clear color — it never adds material.
Coordinates match the voxel tools: x across, y up, z in depth. The
[voxel] volume table frames
the field’s bounds — a meshing case reuses that same table (a mesh case declares
no [canvas]). Because the field is continuous, primitive centers and extents
are real-valued within those bounds, not snapped to an integer grid; the
sample resolution the algorithm evaluates the field at is a fixed characteristic of
the binary (see the per-algorithm sections below), not a per-case knob.
The field operations are ordinary CLI subcommands
Section titled “The field operations are ordinary CLI subcommands”A case seeds no operations schema. The vocabulary is the binary’s own --help,
exactly as with the drawing and
voxel tools, and the brief tells the
model to read it:
mc --help # every operationmc add-sphere --help # one operation's exact flagsEach operation is a subcommand with flags — there is no JSON. For example:
mc add-box --x 8 --y 0 --z 10 --width 16 --height 6 --depth 12 --color "#3a4a5a"mc add-sphere --x 16 --y 10 --z 16 --r 6 --color "#c0c0c8" --blend 2mc subtract-cylinder --x 16 --y 6 --z 16 --r 3 --height 12 --axis yThe vocabulary is shared by all three algorithms (mc, sn, dc and their
-anim variants); only Dual Contouring adds the sharp-feature
tag:
- Additive primitives —
add-sphere,add-box,add-ellipsoid(unequal per-axis radii — domes, eggs, boulders), andadd-cylinder(a disc extruded along a chosen axis — barrels, legs, poles). Each takes a center, an extent, and an opaque#rrggbbcolor, and unions its shape into the field. - Subtractive primitives —
subtract-sphere,subtract-box,subtract-ellipsoid,subtract-cylinder— carve the same shapes out of the field, cutting hollows, bores, and notches. --blend <radius>— a flag on any primitive that selects a smooth union/subtraction (a soft-min blend) with the given radius, so material flows into a rounded fillet rather than meeting at a hard seam. It defaults to0= hard, and a hard union produces a genuine crease in the field (a first-order discontinuity at the seam) rather than a rounded join.replace-color— recolor a region of the field (a palette swap or a shading pass), leaving the surface geometry unchanged.- Whole-field edits —
mirror(reflect the field across a symmetry plane — handy for a symmetric hull),translate(shift the whole field by a vector), andcopy(duplicate a source region to a destination offset — a second wheel, a repeated rivet). clear— reset the field to empty.
Primitive centers and extents are real-valued and signed (a primitive may sit partly outside the volume; the out-of-bounds portion is simply not meshed, never a panic). Because the field is a single composited scalar function, the recorded log rebuilds to the same field — an order-dependent composite — from which the binary extracts the mesh.
The three algorithms
Section titled “The three algorithms”All three read the same shared field; they differ only in how they turn its
zero level set into triangles. The output character is a fixed characteristic of
each binary, not a configurable mode — you pick the binary for the surface you
want, and a case’s asset_kind names it.
Marching Cubes (mc) — low poly
Section titled “Marching Cubes (mc) — low poly”Marching Cubes samples the field on a coarse uniform grid and, for each grid cell the surface crosses, emits triangles from a fixed lookup of the cell’s sign pattern, placing each vertex on a cell edge by interpolating the field’s sign change along it. Vertices land only on grid edges, so the result is a chunky, faceted surface whose triangle density tracks the coarse grid — the characteristic low-poly look. It is watertight but visibly tessellated, ideal for a case whose brief wants a blocky, stylised read.
Surface Nets (sn) — smooth mid-fidelity
Section titled “Surface Nets (sn) — smooth mid-fidelity”Surface Nets samples the field on a medium uniform grid and places one vertex per surface-crossing cell, positioned at the field-weighted centroid of the cell’s edge crossings, then stitches neighbouring cell vertices into quads (split to triangles). Because a vertex is free to sit anywhere inside its cell rather than on an edge, the surface relaxes into rounded, smooth features with uniform triangle density and no sharp edges — a watertight, mid-fidelity mesh that reads as organic and clean. It is the middle ground: smoother than Marching Cubes, without Dual Contouring’s cost or crease preservation.
Dual Contouring (dc) — high fidelity
Section titled “Dual Contouring (dc) — high fidelity”Dual Contouring also places one vertex per surface-crossing cell on a fine uniform grid, but it positions that vertex by solving a quadratic error function (QEF) over the field’s surface samples and their normals within the cell. Because the QEF is driven by surface normals, a vertex is pulled onto the exact intersection of the surfaces meeting in the cell, so sharp edges and corners are preserved crisply instead of being rounded off — the high-fidelity result. Dual Contouring reproduces the hard creases a hard union already produces in the field for free, and its fine grid captures fine detail, at a higher triangle and compute cost than the other two.
All three use a uniform grid; there is no octree or adaptive subdivision. Resolution is tuned per algorithm (MC coarse, SN medium, DC fine) and is not a per-case parameter.
Simplification (all three)
Section titled “Simplification (all three)”A uniform grid spends triangles evenly, so a large flat region carries as many
triangles as an equally sized curved one — thousands of coplanar triangles that
describe a single plane. After extraction, every binary runs a quadric-error-metric
(QEM) simplification pass that collapses that redundancy before the mesh is encoded
to its .glb, typically cutting the triangle count of a blocky model by a large
fraction (flat areas collapse the most; tightly curved and sharp regions are left
dense). It is applied to the exported mesh, the preview, and the recorded vertex count
alike, so all three agree.
The pass only performs an edge collapse when it provably preserves the surface’s watertight, 2-manifold topology (the link condition), so it never opens a crack — and it never collapses across a color boundary (so color patches keep crisp edges) or a sharp feature (whose high collapse error keeps Dual Contouring’s creases). It is a fixed characteristic of the binaries, not a per-case knob.
Dual Contouring only: sharp features
Section titled “Dual Contouring only: sharp features”dc and dc-anim add a sharp-feature tag on primitives — a --sharp /
--smooth flag — that gives explicit control over whether an edge or corner is
preserved crisply or rounded, independent of a primitive’s --blend
radius. --blend shapes the field (how two solids join); the sharp tag tells
Dual Contouring how to extract an edge the field contains. Only Dual Contouring
can honor it — Marching Cubes and Surface Nets cannot represent a preserved sharp
feature (their vertex placement rounds by construction), so mc/mc-anim and
sn/sn-anim do not expose the tag at all. (Dual Contouring still preserves
the creases a hard union produces without any tag; the tag is for control beyond
that.)
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. Extracting a surface from the field and rasterizing it through
the wgpu+Mesa renderer is far more expensive than stamping 2D pixels, and a model
takes many operations, so — unlike the 2D drawing binaries — these tools do not
re-render after every call. Rendering is a separate, on-request step, the
render command. The orchestrator seeds a config next to the workspace —
mc.config.json (static) or mc-anim.config.json (animated), and likewise for
sn/dc — giving the volume dimensions, background, and the log/preview and mesh
(.glb) paths, and, for the animated tools, the rig.json path, so neither an
operation nor render needs volume flags.
The render command rebuilds the derived artifacts from the recorded log when
the model asks: it composites the field, extracts and simplifies the surface into the
per-part .glb, and draws the preview PNG. A model runs it to see its progress and,
before it finishes, to emit the .glb the run’s result is built from — an
unrendered model leaves no geometry, which the validator records as an empty part.
mc init # write an empty log (a run starts pre-seeded); renders nothingmc render # extract the surface to the .glb and draw the preview PNGmc render --view front # ...from a chosen camera: iso (default) | front | side | topRecording-then-rendering exists for authoring ergonomics only — the preview lets
the model (and a watching human) see the surface it has built. It is not a
cheat-detection mechanism. The validator
does not regenerate or re-render anything: it decodes the emitted per-part
.glb and parses rig.json, confirms they are
well-formed and readable, and checks the rig contract (that each required
animation is present and actually animates). What is judged is the emitted data plus a
reviewer’s read of the rendered previews — not how the data was produced.
Preview rendering (wgpu + Mesa lavapipe)
Section titled “Preview rendering (wgpu + Mesa lavapipe)”The preview render draws is a real 3D orbit view of the extracted mesh,
produced by a generic mesh renderer — geometry, an orbit camera, and lighting into
a PNG — that lives in the shared crates/model-core library. It renders with
wgpu targeting Mesa lavapipe (a software Vulkan implementation), so it
runs CPU-only and headless, with no GPU in the container. The same renderer
serves every voxel-family binary — the cube tools
render their cube mesh through this path too — so previews are apples-to-apples
across all algorithms: the same orbit camera and shading over whatever surface the
binary extracted. Because nothing is regenerated for scoring, the renderer carries
no determinism requirement. (The still preview is what a model reads and a reviewer
sees; the interactive, rotatable, posable 3D view is the frontend’s rendering — see
the .glb contract and
voxel-runtime.)
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 is streamed to the viewer in real time, mechanically identical to
the voxel and
drawing tools: the orchestrator
adds a live block (a host.docker.internal endpoint and an opaque per-run token)
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 mesh as
per-part .glb bytes (meshLength bytes). The mesh body lets the viewer rebuild the surface in
3D as it is sculpted — orbiting it and assembling the scene exactly as the
finished-run view does — rather than showing only the flat 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 and the
emitted .glb remain the run’s authoritative output.
The animated binaries: one field per part, plus the rig
Section titled “The animated binaries: one field per part, plus the rig”An animated model is a
rig: named parts in
a hierarchy with named joints and model-authored animations. Each animated binary
(mc-anim, sn-anim, dc-anim) is its static counterpart plus a global required
--part <name> that selects which part an operation sculpts into; each part is an
independently-authored field, meshed on its own into its own .glb, with its
own operation log and its own preview (both {part} templates the case declares).
The rig — the parts (each with a pivot), joints, and F-curve animations the model
invents — composes and poses the per-part meshes: parts are the pieces that
animate, not subsections of one mesh. The case’s [model] table fixes only the
required animations; the model creates each part with define-part before it
sculpts into it.
mc-anim --help # same field operations, plus --partmc-anim add-box --part turret --x 12 --y 8 --z 12 --width 8 --height 4 --depth 8 --color "#4a5a3a"mc-anim define-part --name turret --parent hull # create a part before sculpting into itmc-anim init # seed rig.json (the required animation declarations)mc-anim render # extract every part's .glb + draw the assembled scenemc-anim render --component turret # ...or just one part's preview + .glbmc-anim render --time 600 --animation walk # ...or the model posed at 600ms of the walkThe render command is identical in shape to
voxel-anim render:
plain render re-emits every part’s .glb and preview and writes the assembled
rest scene (the call to run before finishing); --component <part> renders one part;
and --time <ms> (with --animation) renders the model posed at that instant of
an animation, to scene/pose.png. Nothing renders automatically — a sculpting
operation only records.
The rig model is identical to voxel-anim
— the same define-part / set-pivot / define-joint / define-animation /
add-keyframe subcommands, the same F-curve interpolation (constant / linear /
bezier plus the ease-in / ease-out / ease-in-out presets), the same
rotation-sign convention, and the same caller-vs-auto joint drives — so it is
not re-documented here. Author the rig and its animations exactly as for a voxel
animation:
- the rig subcommands, joints, and F-curves are documented under The voxel binaries and in Manifests, and
- the design guidance for legged rigs and walk cycles is in Rigging and animating walkers.
The seeded config carries the {part} templates and the rig.json path, so init
seeds a rig.json pre-populated with the case’s required animation declarations
alone — its parts and joints start empty, because a case declares none. No
part exists until the model creates one with define-part (which initializes that
part’s operation log; its preview and .glb are written later, by render); a field
op on an undefined part is rejected. The
model builds the whole rig — inventing the parts and joints the subject needs and
authoring each required animation — and may add further animations of its own; the
required animations are the game-facing contract a reviewer scores against.
The .glb output contract
Section titled “The .glb output contract”Each binary emits, per part (a static model is a single implicit part), a
per-part .glb (a standard glTF 2.0 binary container holding one mesh with
one primitive) — the single source of the extracted geometry. It is written to
meshes/{part}.glb for an animated model and mesh.glb for a static one. Its
primitive carries the same four attributes the runtime’s PartMesh holds (the
output of @test-cabinet/voxel-runtime’s buildPartMesh), as glTF accessors:
positions— thePOSITIONaccessor: F32VEC3(x, y, zper vertex),normals— theNORMALaccessor: F32VEC3(one per vertex),colors— theCOLOR_0accessor: F32VEC3, using the same normalization the runtimePartMeshuses, andindices— the index accessor: a U32SCALARtriangle-vertex list.
A part with no geometry (an empty part — an attach socket) is emitted as a valid glb with an empty scene and no meshes; decoding it yields empty arrays. Because the format is standard glTF 2.0, the Rust encoder and the TypeScript / Node decoders interoperate through the glTF spec, not through each other’s code.
The .glb is the single source of geometry for every consumer, and the Rust
mesher runs once: the wgpu preview renderer
renders the in-memory mesh, the TypeScript @test-cabinet/voxel-runtime
decodes it directly into a PartMesh (no re-meshing in TS), and
scripts/voxel-to-gltf.mjs decodes each part’s .glb and packs it with rig.json
into a single whole-rig
glTF — one mesh per part,
baked animations, and the joint-interface sidecar — for game and WebGL consumption.
The cube binaries emit the same per-part .glb, so one glTF exporter and one
runtime serve every voxel-family type.