8.8 KiB
Geometry and collision
The world is not one big mesh and there is no BSP. It is a grid of tiles, inherited from
Croc: TRAK holds a set of small models (the level's construction kit), and MAP places them on a
W × H grid, one cell = one unit. Every byte described here is reproduced exactly by
format/Trak.java,
format/MapChunk.java and
tools/geomlib.py for all original levels.
Full detail with all loader addresses: notes/geometry.md.
Coordinate spaces
| Space | Used by | Axes |
|---|---|---|
| Game space | strats, collision, MAP fixed-point tile positions, lights | x east, y up, z = grid row (positive); 20.12 fixed point (4096 = 1.0); right-handed as the game shows it |
| Export space | TRAK/STPC vertices, MAP float tile positions | same, but z negated (grid at z ∈ (−H, 0]); drawn by a Direct3D (left-handed) camera |
| File space | OBJ/glTF of every LlamaMod and kit tool | = game space (y up, right-handed): Blender shows a level the same way round as the game |
Converting export ↔ file space negates z and reverses the triangle corner order. Angles: 4096 = 360° in MAP records; the strat VM uses 0x1000000 = 360°. A tile is turned only about y (0, 1024, 2048, 3072 in practice). Rotation 0 faces +z, 90° faces +x.
TRAK: tile models (certain)
u32 n, then all n headers (0x84 bytes each), then the data of every model in order.
Typical levels have 100–650 models with up to 422 vertices / 798 faces each.
| Header field | Meaning |
|---|---|
| 0x00 | bounding-box centre (3 × f32) |
| 0x0c | 8 box corners (used for frustum culling) |
| 0x6c / 0x6e | vertex count / face count |
| 0x78 / 0x7a / 0x7c | collision polygon counts in three groups (the engine always uses the sum; meaning of the groups unknown) |
| pointers | 0 in the file, set on load |
Data per model:
- Vertex, 24 bytes:
f32 x, y, z, nx, ny, nz(lighting comes from the MAP vertex colours, not from the normals). - Face, 28 bytes, triangles only:
u16 flags, u16 v0, v1, v2, u16 texture, u16 0, f32 plane nx, ny, nz, d. - Collision polygon, 32 bytes:
u8 type, u8 material, then 5 planes{i8 nx, ny, nz, u8 0, i16 d}. Plane 0 is the surface, planes 1–4 the edges (normals point inwards). Type bit 0 = quad, otherwise triangle. Normals are ×127, d is 20.12 fixed point – so a collision polygon must lie within ±8 units of its tile's origin.
Face flags
| Bit | Meaning |
|---|---|
| 0x0008 | start of a new render batch (texture page or blend mode changes) – must be set on the first face of each batch, at the latest every 256 faces |
| 0x0010, 0x0020, 0x0800 | which corners of the texture rectangle the triangle uses (below) |
| 0x0400 | double-sided (no backface culling) |
| 0x4000 | ignore vertex colours |
| 0x0001, 0x0002 | special sorting/transparency modes (likely) |
| others | present in the data, not read by the PC renderer (unknown, probably PlayStation leftovers) |
No free texture coordinates
The engine has no per-vertex UVs. Every triangle shows one half of a texture rectangle from the TEXT table: with flag 0x800 the face is the first or (with 0x10) second half of a quad, without it a triangle covering the lower-left half; 0x20 mirrors horizontally. 96 % of world faces are quad halves. This is why the level builder and editor split surfaces into 1 × 1 pieces and why a narrow wall shows a squashed texture. See Textures.
MAP: the tile grid (certain)
MAP starts with the grid and the tiles; objects follow (see Objects). The blocks that concern geometry:
| Block | Format |
|---|---|
| header | u32 nTiles, u32 W, u32 H |
| tiles (floats) | nTiles × {f32 x, y, z, u32 rotY, u32 model, u32 hasAlt} in export space; x, z = cell centre |
| regions | 32 × {u32 visibility mask, i32 value}, then W·H × u32 region number per cell |
| tiles (fixed point) | nTiles × {rx, ry, rz, x, y, z} 20.12 in game space – used for drawing and collision; must match the float copy exactly |
| model index | nTiles × u32 (= the float block's model) |
| tile swaps (WFPC 0x10000) | one {cellX, y, cellZ, modelA, modelB} per tile with hasAlt ≠ 0: doors and bridges that change their model (strat opcodes 0x11a / 0x11b) |
| vertex colours | per tile: one u32 RGBA per vertex of its model (0x80 = 1.0, the game doubles and clamps); more sets for animated colours; then the same for the swap model |
Rules that crash the game or corrupt the level when broken:
- every tile must lie inside the grid (
0 ≤ x < W,0 ≤ row < H) – outside, the cell list writes into foreign memory; - the vertex-colour block must have exactly one entry per vertex of the tile's model – else everything after it (the objects) is read shifted;
- the float and fixed-point copies and the model index must agree;
- tile swaps: one entry per tile with
hasAlt ≠ 0, in tile order.
The cell of a tile is floor(x) + floor(−z) · W (export-space floats).
Collision (certain)
- Only the tiles listed in the cell the player stands in are tested (
FUN_004036d0floors and ceilings,FUN_00403ad0walls). A wall polygon that sticks into the neighbouring cell has no effect there – builders enter a polygon into every cell it covers, as a collision-only tile if needed. - Collision is in game space, relative to the tile's fixed-point position.
- There is no step-up logic: a 0.12-unit edge with a wall polygon already blocks the llama. Low plinths need walls without collision; stairs need an invisible slope.
Collision materials
The material byte ends up in the player's ground state and is read by strats (floorMaterial).
| Material | Meaning |
|---|---|
| 0 | normal ground |
| 16 | exit floor: the LevelDoor ends the level only when the llama stands on material 16 in the door's cell; also used under the village house doors (zone teleports) |
| 17, 18 | ignored when WFPC flag 0x8000000 is set (likely: special surfaces) |
| 30 (0x1e) | death plane / water (likely): falling onto it empties the energy and the llama respawns at the last checkpoint or the start (Model 0 of many levels is such an invisible plane at y = −2.75) |
| others | special surfaces, meaning unknown – the debug view colours them |
Visibility regions (no BSP)
Drawing (FUN_0042bf40) walks the grid cells within the draw distance around the camera and draws
a cell's tiles only if the cell's region bit is set in the mask of the camera's region. Up to 32
regions; mask 0 or a camera outside the grid means "everything". New geometry must therefore get a
region that the surrounding regions can see, or it stays invisible – the builders put everything
into region 0 with mask 0xffffffff. Objects in hidden cells are skipped too (unless strat flag
0x800). The editor's Visibility regions tool edits this
(manual chapter 13).
STPC: object models and code
STPC starts with the object models (same model format as TRAK, header 0x8c bytes, optionally skinned with a bone table), then the animation sets (per-frame bone matrices and compressed vertex deltas – the compression is only partly understood), then the strat code units (Strats). The engine never parses the code; MAP records and bytecode refer into STPC by absolute byte offsets, so STPC can only be extended at the end or merged with relocation (notes/editor-cross-import.md).
WFPC flags
WFPC is a u32 of feature flags. Some change the data layout and must stay as they are: 0x10, 0x100, 0x10000 (tile swaps), 0x100000, 0x200000, 0x4000000, 0x10000000. Others: 0x800 renderer fog flag, 0x80000 read by a strat opcode, 0x8000000 makes collision materials 17/18 ignored.
Writing new geometry – checklist
- TRAK: correct counts, bounding box and face planes recomputed, texture index below the number of texture entries, batch bit 0x8, ≤ 8000 vertices per model.
- Collision polygons within ±8 units of the tile origin, entered into every cell they cover.
- MAP: tiles inside the grid, float/fixed/model blocks consistent, vertex colours exactly per
vertex, tile-swap block matching
hasAlt, a visible region. - Rewrite the WAD's total length.
The editor and both level builders do all of this; the details of the builder's approach (splitting into tiles per cell and height band, merging identical tiles, baking sun, sky and ambient occlusion into vertex colours) are in notes/levelbau.md.

