Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Concepts & conventions

Five conventions run through every roxlap API. They are all inherited from Voxlap — that heritage is what lets twenty years of .vxl / .kv6 assets load unchanged — and three of them are backwards from what most 3D engines taught you. Absorb this chapter before writing real code; everything else in the book assumes it.

The snippets here come from a runnable, assertion-checked example:

cargo run -p roxlap-core --example book_conventions

+z is down

The world axes are x = east, y = north, and z pointing down into the map. Consequences you will hit immediately:

  • “Up” is toward smaller z. A tower is built by decreasing z; a mine shaft by increasing it.
  • Within a chunk, z runs 0..256: z = 0 is the top (sky), z = 255 the bottom (bedrock). Ground surfaces typically sit somewhere in the 200s, filling downward — that is why the quickstart’s grass slab is set_rect(.., z=210 ..= z=254) with the dome above it at z = 205.
  • Positive camera pitch aims downward:
    // +z points DOWN. Positive pitch aims the camera downward (the
    // forward axis gains a positive z component); "up" in the world
    // is toward smaller z.
    // A camera pitched down has forward pointing at +z:
    let cam = Camera::from_yaw_pitch([0.0, 0.0, 128.0], 0.0, 0.4);
    assert!(cam.forward[2] > 0.0);

The horizontal mirror

Physically, the triple (east, north, up) is left-handed, so a faithfully projected view is horizontally mirrored relative to a real camera at the same heading — your geometric right lands on screen-left. This is Voxlap’s native projection, reproduced deliberately (it is baked into the bit-exact reference goldens); it is not a bug, and most games never notice it.

If your project needs an un-mirrored world, do not negate the camera’s right vector — that breaks basis chirality (next section) and silently culls every sprite. Fix it on the consumer side instead: mirror one world axis when you place content, or negate your yaw input. The full reasoning lives in the roxlap-core crate docs (“World handedness and the horizontal mirror”).

One voxel = one world unit

There is no separate voxel-size parameter: world coordinates are voxel coordinates, scaled 1:1 (a grid’s placement can translate and rotate them, but not scale — see the scene graph). Grids store voxels in chunks of 128×128×256 (CHUNK_SIZE_XY / CHUNK_SIZE_Z); you address voxels in grid-local integer coordinates and the chunk decomposition is invisible to you.

The colour family: VoxColor, Rgb, OverlayColor

roxlap inherited three distinct colour packings from Voxlap, and each is its own newtype — mixing them up is a compile error rather than a visual bug:

  • VoxColor — voxel colours. RGB plus a brightness byte in the high bits (NOT alpha!): VoxColor::rgb(r, g, b) uses the neutral 0x80, and lighting bakes (chapter 6) rewrite the byte per voxel — ambient and AO live there. Translucency is a material property, never a colour channel.
  • Rgb — plain colour: sprite/particle tints, the sky / fog / clear colours, and colour→material map keys.
  • OverlayColor — the one packing with real alpha, used only by the overlay-line API (Line3).

All three are transparent wrappers over the wire u32 (the .0 field), so file formats and GPU buffers are unaffected:

fn packed_colors() {
    // The QE-B6 colour family: three packings, three types — mixing
    // them up is a compile error, not an over-bright voxel.
    // VoxColor = RGB + a brightness byte (NOT alpha; 0x80 = neutral,
    // lighting bakes rewrite it per voxel — see the lighting chapter).
    let grass = VoxColor::rgb(0x4d, 0x8a, 0x3a);
    assert_eq!(grass.0, 0x80_4d_8a_3a); // .0 is the raw wire word
    assert_eq!(grass.with_brightness(0xff).0, 0xff_4d_8a_3a);
    // Rgb = plain colour: tints, sky/fog, material-map keys.
    assert_eq!(Rgb::new(0x8f, 0xbc, 0xd4).0, 0x00_8f_bc_d4);
    // OverlayColor = REAL alpha — only the overlay-line API uses it.
    assert_eq!(OverlayColor::rgba(0xff, 0xd0, 0x40, 0x80).0, 0x80_ff_d0_40);
}

The camera basis — and the chirality footgun

A Camera is a position plus three orthonormal f64 axes: right, down, forward (Voxlap’s ist/ihe/ifo). The engine requires the right-handed relation right × down = +forward. The renderer will happily draw terrain under a basis with the opposite chirality — but the sprite frustum cull rejects everything, which presents as “my sprites disappeared” with no error anywhere.

The rule that keeps you safe: always build cameras with the constructorsCamera::from_yaw_pitch, Camera::orbit, Camera::look_at. Never hand-roll the axes, and never build a camera by rotating Camera::default() — the default is a .vxl-header placeholder whose basis is left-handed:

    // The canonical constructors (`from_yaw_pitch` / `orbit` /
    // `look_at`) produce the right-handed basis the engine requires:
    // right × down == +forward. The sprite frustum cull depends on
    // this chirality — a hand-rolled basis that gets it backwards
    // renders the terrain fine and silently culls every sprite.
    let cam = Camera::from_yaw_pitch([0.0, 0.0, 128.0], 0.6, 0.2);
    assert!(approx_eq(cross(cam.right, cam.down), cam.forward));

    // `Camera::default()` is the trap: its placeholder basis (from
    // the .vxl header convention) is LEFT-handed. Never build an
    // interactive camera by rotating `default()` — construct one.
    let trap = Camera::default();
    let anti = cross(trap.right, trap.down);
    // right × down == -forward here: the wrong chirality.
    assert!(approx_eq(anti, [0.0, -1.0, 0.0]));

Yaw/pitch conventions: yaw = 0 looks down +x, increasing yaw turns toward +y; pitch = 0 is level, positive pitch aims down.

The precision model

Three numeric domains, each with a job:

  • f64 for the world. Camera position/basis and every GridTransform (grid origin + rotation quaternion) are f64, so a huge world doesn’t jitter far from the origin.
  • i32 for voxels. Edits and queries take grid-local integer voxel coordinates (glam::IVec3).
  • f32 for sprite instances. Per-instance sprite/clip transforms (chapter 7) are f32 — cheap to stream in bulk, precise enough for object-sized placements.

Where these conventions come from

They are Voxlap’s, kept so assets and ported math work unchanged. The design history — what was ported faithfully, what was replaced (the renderer itself is a clean-room per-pixel DDA, not Voxlap’s raycaster) — lives in docs/porting/, starting from PORTING-RUST.md and PORTING-SCENE.md.