voxolith

Scales and refinement

Why parameters stay at 10 voxels per metre, how a finer world asks for the same design, and how gen-kit refine turns a coarse model into a sparse one.

Every generator's parameters are written in 10 voxels per metre: a storey is about 24 voxels, an oak about 190. A world with finer voxels does not get new parameters; it asks for the same design at more voxels per metre, and the generator decides how to add the detail.

import { seededRandom } from "@voxolith/renderer/core";
import { generateTree, PRESETS } from "@voxolith/gen-tree";

const coarse = generateTree(PRESETS.oak, seededRandom(42)).entity;
const fine = generateTree(PRESETS.oak, seededRandom(42), "oak", { voxelsPerMetre: 100 }).entity;

fine.model.size;   // exactly 10 times coarse.model.size on every axis
fine.model.sparse; // 8³ bricks; fine.model.data is empty

The context

generate(params, rng, ctx?) takes an optional GenerateContext whose only field is voxelsPerMetre. refinement(ctx) turns it into a whole-number factor k (1 at the native DEFAULT_VOXELS_PER_METRE of 10). A generator lists the finer scales it supports in scales; every entity generator lists [20, 50, 100] (5, 2 and 1 cm voxels) except voxolith/creature, which is native only (see below).

Two rules keep this invisible to everything that already works:

  • A context at 10, or no context, gives the byte-identical model it always did. The contract checks it, so share codes and cached models never change meaning.
  • Parameters are not rescaled. A share code describes the design; the world it is built for is the host's business.
  • A finer model stays within one native voxel of the native one: every fine voxel, divided by k, lies in or next to an occupied voxel of the model built at 10. The contract checks that too, and it is what lets a page draw the 10 voxels per metre model enlarged as a placeholder and swap in the fine one at the same placement with the outline moving by one coarse voxel at most (see coarse first, one swap).

Refine, never generate fine

A generator builds its model at the native scale first, then refines it. Generating a 1 cm-voxel tree directly would run every whole-volume pass (hull, carving, sky occlusion, the connectivity flood) over a grid a thousand times larger. The coarse model already decides the shape, the shading and which voxel is bark and which is leaf; what the finer grid adds is surface: a rounded silhouette instead of 10 cm steps, and detail that only exists at the small scale (single leaves, bark furrows, mortar joints, roof tiles).

refine in gen-kit visits only the coarse voxels on the surface and writes their fine blocks into a sparse model, keeping a shell a few fine voxels deep, so cost and memory follow the surface, not the volume. The result is k times the coarse size with the anchor scaled with it.

Rules per role

Each role picks a RoleRule:

modeforwhat it writes
smoothorganic masses: bark, stone, moss, snowthe 0.5 level of the trilinearly interpolated coarse occupancy, which rounds every step and corner and fills concave notches, displaced by noise (roughness in coarse voxels, roughScale in fine voxels)
crispbuilt things: walls, frames, glassexact cubes, so edges stay sharp; detail comes from the rule's own pattern
leavesfoliage, petals, thornssmall discs (or shape: "needle", short lines) at random places and angles inside each coarse leaf voxel, in that voxel's role, so the coarse light and dark shading survives; tones and toneChance swap in a neighbouring shade
bladesgrassthin leaning columns, continuous up a stack of coarse voxels and cut at a random height in the top one
skipanything the generator redraws itselfnothing; the voxels still count as solid for their neighbours

A rule's detail(cell) then decides the final role of each fine voxel, or 0 to carve it. The cell says where the fine voxel is, which coarse voxel it came from, its role, the outward axis of the nearest open face and how deep it is below it. This is where a generator puts its small-scale patterns: hairline cracks, bark furrows, brick bond, roof courses.

faces: "outside" refines only faces open to air reachable from outside the model. A building's rooms are sealed by walls and glass and are skipped; from outside they are seen at most through a window.

Redrawing from the skeleton

A limb at 10 voxels per metre is a staircase of cubes, and smoothing a staircase gives a lumpy rod. The vegetation generators still have the skeleton that drew it, so they mark their wood skip and draw every segment again at k times the size with drawSkeletonFine: a thin shell of an exact tapered capsule, thinner above the trunk than the coarse minimum of one voxel allowed. coarseRoleAt gives each fine voxel the role of the coarse voxel it falls in, looked up a little off the point by noise so tone patches come out organic rather than as k-voxel cubes.

What each generator does at a finer scale

generatorrefinement
treewood redrawn from the skeleton with a root flare and fine furrows; moss and crevices keep their places; each leaf voxel becomes a few single leaves (needles on a conifer); snow and cones round off
bushstems redrawn much slimmer than the coarse bush could draw them, with faint streaks; leaves become single leaves; thorns become pairs of short needles
grassblades redrawn from the skeleton as rods a voxel or two across, shaded base to tip from the coarse blades; flowers become petal discs, seed heads stay compact
rockrounds off its steps with a grainy surface; the mid tone breaks into speckle; hairline cracks run across the faces; moss and lichen become ragged patches
buildingstays crisp, outside faces only; masonry and joinery at their real size (about twice it at 20): brick in running bond with recessed joints, ashlar courses, tile, slate, shingle or thatch courses, boards on doors and shutters, planks on floors. The coarse building is generated without its relief, since the joints are drawn at their own scale here
creaturenone. It is authored near that scale already; atScale sizes a preset for a fixed-unit world

The terrain has its own refiner, refineTerrain, which answers per brick rather than building a model.

At 20 voxels per metre

20 voxels per metre is a factor of 2: each coarse voxel becomes 2 × 2 × 2 fine ones. It is the scale the examples give phones, with about an eighth of the bricks of 50. Detail sized for 1 cm voxels does not survive at 5 cm, so three rules apply below a factor of 3 only:

  • Thin limbs are lines. At a larger factor a limb thinner than one fine voxel in radius is widened to one, three voxels across; at a factor of 2 that is fatter than the coarse voxel it came from. There such a limb is drawn as a one-voxel line that crosses a face at a time, so it stays 6-connected. Skeleton shells are at least 2 voxels thick, since a one-voxel shell of a round limb leaves diagonal gaps a ray can pass through.
  • Building patterns have minimum sizes. At real size a brick course or a slate row would be under two voxels. Brick courses are at least 3 × 7 voxels, roof rows 3, tiles 4 and boards 3, and the plaster, grain and thatch noise keeps the size it has at 50. So at 20 masonry and roofing are drawn at about twice their real size, which still reads as bricks and tiles.
  • Terrain blades vary. refineTerrain's default bladeHeight is max(4, round(1.4k)), so blades at k = 2 are 2 or 3 voxels tall rather than all 2.

Factor 5 and 10 already exceed every one of these minimums, so the models at 10, 50 and 100 voxels per metre are byte-identical to what they were before 20 was added. The full contract runs at 20 as at the other scales.

Sparse models

A refined model is an EntityModel with sparse set (8³ bricks, the renderer's SparseVoxels) and data empty. Read either form with modelAt. Results are a few to a few tens of MB of GPU bricks and take 0.1 to 3 seconds each; the contract holds every default to 20 seconds and 96 MB.

Sparse models are drawn by reference with renderer.addModel and setInstances rather than stamped into the world; see instances and engine instances.

Loose roles

At 10 voxels per metre everything is one grounded piece. At finer scales a single leaf, petal or seed head may legitimately float free of its twig. A generator names those roles in looseRoles; the contract ignores them when it floods from the base, and still requires 99% of the rest (the structure) to be grounded.

On this page