156 lines
8.8 KiB
Markdown
156 lines
8.8 KiB
Markdown
# 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`](../../src/main/java/com/eliasmueller/llamamod/format/Trak.java),
|
||
[`format/MapChunk.java`](../../src/main/java/com/eliasmueller/llamamod/format/MapChunk.java) and
|
||
[`tools/geomlib.py`](../../tools/geomlib.py) for all original levels.
|
||
|
||
Full detail with all loader addresses: [notes/geometry.md](../../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](textures.md).
|
||
|
||
## MAP: the tile grid (*certain*)
|
||
|
||
MAP starts with the grid and the tiles; objects follow (see [Objects](objects.md)). 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_004036d0` floors and
|
||
ceilings, `FUN_00403ad0` walls). 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](../manual/13-visibility.md)).
|
||
|
||
## 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](../strats/README.md)). 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](../../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
|
||
|
||
1. TRAK: correct counts, bounding box and face planes recomputed, texture index below the
|
||
number of texture entries, batch bit 0x8, ≤ 8000 vertices per model.
|
||
2. Collision polygons within ±8 units of the tile origin, entered into every cell they cover.
|
||
3. MAP: tiles inside the grid, float/fixed/model blocks consistent, vertex colours exactly per
|
||
vertex, tile-swap block matching `hasAlt`, a visible region.
|
||
4. 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](../../notes/levelbau.md).
|