Skip to content

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:

Algorithmstatic binaryanimated binaryasset_kind (static / animated)character
Marching Cubesmcmc-animmc-model / mc-animationlow poly — coarse sample grid, chunky faceted surfaces
Surface Netssnsn-animsn-model / sn-animationsmooth mid-fidelity — watertight, uniform triangle density, rounded features
Dual Contouringdcdc-animdc-model / dc-animationhigh 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 operation
mc add-sphere --help # one operation's exact flags

Each 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 2
mc subtract-cylinder --x 16 --y 6 --z 16 --r 3 --height 12 --axis y

The vocabulary is shared by all three algorithms (mc, sn, dc and their -anim variants); only Dual Contouring adds the sharp-feature tag:

  • Additive primitivesadd-sphere, add-box, add-ellipsoid (unequal per-axis radii — domes, eggs, boulders), and add-cylinder (a disc extruded along a chosen axis — barrels, legs, poles). Each takes a center, an extent, and an opaque #rrggbb color, and unions its shape into the field.
  • Subtractive primitivessubtract-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 to 0 = 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 editsmirror (reflect the field across a symmetry plane — handy for a symmetric hull), translate (shift the whole field by a vector), and copy (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.

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 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 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 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.

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.

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 nothing
mc render # extract the surface to the .glb and draw the preview PNG
mc render --view front # ...from a chosen camera: iso (default) | front | side | top

Recording-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.

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.)

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 inventscomposes 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 --part
mc-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 it
mc-anim init # seed rig.json (the required animation declarations)
mc-anim render # extract every part's .glb + draw the assembled scene
mc-anim render --component turret # ...or just one part's preview + .glb
mc-anim render --time 600 --animation walk # ...or the model posed at 600ms of the walk

The 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 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.

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 — the POSITION accessor: F32 VEC3 (x, y, z per vertex),
  • normals — the NORMAL accessor: F32 VEC3 (one per vertex),
  • colors — the COLOR_0 accessor: F32 VEC3, using the same normalization the runtime PartMesh uses, and
  • indices — the index accessor: a U32 SCALAR triangle-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.