Collision¶
Verified against: SLUS_212.15 (NTSC-U, SHA1 e9cb2cc49aa046b9e494313dce2f5038ed17b2f4). One runtime claim (PCSX2
2.9.94, 2026-10-05, level99's mesh read over PINE, counts only) says so. The disc-side checks (2026-10-04) read all 64
.lev files of the NTSC-U disc's WAD with
throwaway scripts outside the repository and are reported as counts, ranges and invariants only.
Purpose¶
The static collision of a level: the triangles that characters, cameras and the scripts' "drop to the ground" test
against. It lives in the level file (Level loading) as six chunks that the chunk
system joins into one collision mesh, held by the level object at +0x04. This page gives the format and the
queries (ray casts, the sphere push-out walls use, ground height, switching triangles on and off) closely enough to
reimplement them. Moving objects' bodies and shapes (Physics/) are on Physics.
Everything here is in game axes, z up: the mesh is not converted to RenderWare's axes (confirmed by the disc: see Disc counts).
Original structure¶
The file is c:/Warriors/Source/RayCast/CollisionMesh.cpp (path string 0x005788f8, anchor 0x00350688 on the
Source map). The mesh functions below sit around the anchor, from 0x00350538 to the end of
0x003519f8 at 0x00351da0, where Scene/ begins (inferred: one file). The ground-height helpers and the
material names (0x0034f740-0x0034fc08) lie just before, after Physics/'s last static-initialiser stub, so in
RayCast/ too (Source map, inferred). Names are ours.
| Address | Name | Role | Evidence |
|---|---|---|---|
0x00350580 |
CollisionMesh_OnLoaded |
chunk 0x03 handler: builds the mesh |
confirmed (code) |
0x00350688 |
CollisionMesh_FreeChunks |
frees the chunks of a mesh | confirmed (code) |
0x00350538 |
CollisionTri_PassesMaterialFilter |
material exclusion list | confirmed (code) |
0x00350778 |
CollisionMesh_RayTestCell |
ray against one cell's triangles | confirmed (code) |
0x00350cd8 |
CollisionMesh_RayCast |
ray against the mesh: the grid walk | confirmed (code) |
0x00350aa0 / 0x00351160 |
CollisionMesh_SetEnabledInBox(Cell) |
switch triangles inside a box on or off | confirmed (code) |
0x00351468 / 0x003519f8 |
CollisionMesh_SphereTestCell / CollisionMesh_SpherePush |
push a sphere out of walls | confirmed (code) |
0x00351158 |
CollisionMesh_UpdateEmpty |
called by eleven game modes' Update functions every frame (Unit 16); an empty function |
confirmed (code) |
0x00337920 / 0x00337a60 |
RayTriangle_OneSided / RayTriangle_TwoSided |
ray-triangle intersection (Utils maths, outside the file) |
confirmed (code) |
0x0040db88 |
WorldManager_RayCast |
the world manager's ray cast into the level's mesh; 46 functions, 61 call sites | confirmed (code) |
0x0034f950 |
Collision_DropToGround |
ground height under a point | confirmed (code) |
0x0034fa28 |
Collision_DropToMarkedGround |
the same, skipping triangles whose area byte is 0 | confirmed (code) |
0x0034f740 |
Collision_MarchRay |
a long ray cast in steps | confirmed (code) |
0x0034fba0 |
CollisionMesh_SetEnabledInVolume |
switches the triangles inside a game object's box on or off | confirmed (code) |
0x0034fc08 |
CollisionMaterial_GetName |
material id to name (MATERIAL_*) |
confirmed (code) |
Data¶
The chunks¶
The level file holds, in this order, 0x52, 0x07, 0x04, 0x05, 0x06 and 0x03, all raw except the last
(Chunk system). The 0x03 handler 0x00350580 writes the mesh's vtable
(0x005448b0) into the 160-byte 0x03 chunk itself, then pops the other five and stores their data pointers, then
pushes the mesh as an object (taken by the level's 0x17 handler). Confirmed (code):
| Chunk | Name in the type table | Content | Stored at |
|---|---|---|---|
0x03 |
Collision Mesh | the header, below | the mesh itself |
0x06 |
Collision Strings | triangle index lists, not names: u16 values |
+0x80 |
0x05 |
Collision Grid | one u32 per cell |
+0x78 |
0x04 |
Collision Triangles | 10-byte triangles | +0x88 |
0x07 |
Collision Vertex Buffer | float[4] vertices, w = 1.0 |
+0x8c |
0x52 |
Collision Checked | one bit per triangle, scratch for queries | +0x94 |
After linking, the handler sets bit 0 (enabled) in every triangle's flags and clears the checked bit set. The bit
set's size is (((n + 7) >> 3) + 15) & 0x7ff0 bytes for n triangles (confirmed (code); the disc's 0x52 chunks have
exactly that size).
The header (chunk 0x03, 160 bytes)¶
Confirmed (code) for the offsets the queries read; the disc column is the survey of all 64 files.
| Offset | Type | Meaning | On the disc |
|---|---|---|---|
+0x00 |
vtable, written at load | junk pointers | |
+0x10 |
float[4][4] |
world-to-grid matrix, rows applied as g = x·r0 + y·r1 + z·r2 + r3 |
always a diagonal scale and a translation |
+0x50 |
float[4] |
grid clamp minimum | always (0.5, 0.5, 0.5, 1) |
+0x60 |
float[4] |
grid clamp maximum | always (nx - 0.5, ny - 0.5, nz - 0.5, 1) |
+0x70 / +0x72 / +0x74 |
u16 |
nx, ny, nz: cells per axis |
nx 2-251, ny 1-124, nz 1-2 |
+0x76 |
u16 |
padding | 0, except level118 |
+0x78 |
pointer | the grid | |
+0x7c |
u32 |
number of u16 values in the index lists |
matches the 0x06 chunk |
+0x80 |
pointer | the index lists | |
+0x84 |
u16 |
triangle count | 13-9,051 |
+0x88 |
pointer | the triangles | |
+0x8c |
pointer | the vertices | |
+0x90 |
u32 |
vertex count | 10-5,401 |
+0x94 |
pointer | the checked bit set | |
+0x98 |
float | the lowest vertex z (inferred: no reader traced) |
equal on all 64 files |
+0x9c |
u32 |
0 | always 0 |
The grid¶
A uniform grid over the mesh's bounding box. The cell of cell coordinates (x, y, z) is entry (z·ny + y)·nx + x of
the grid. An entry is an offset, in u16 units, into the index lists; 0 means the cell is empty (the lists' first
u16 is 0 on every file, so offset 0 also reads as an empty list). At an offset there is a u16 count followed by
that many u16 triangle indices. Confirmed (code) at 0x00350cd8 and 0x003519f8.
Point to cell, confirmed (code): g = M·p with the header matrix, clamped per axis to [min, max] from
+0x50/+0x60, then truncated. On the disc the matrix maps the vertices' bounding box exactly onto 0 to n - 1 on
each axis (so cells are centred on integer grid coordinates, and a point on the box's low face lands in cell 0 through
the clamp at 0.5).
Disc check (corroboration): cells are 5.03 to 13.57 units wide. 45 levels have nz = 1 with a z scale of 0
(every point maps to z = 0, clamped to 0.5). 2.3 % to 100 % of cells are non-empty; the longest list holds 11 to
215 triangles; a triangle is listed in 1.07 to 7.24 cells on average. Every triangle is in at least one cell. The
lists are not exactly the cells of each triangle's bounding box: 350 entries fall outside it and 18,650 triangles
miss some cells of it (inferred: the tool tested triangle against cell, with a margin).
Triangles (10 bytes)¶
| Offset | Type | Meaning | Evidence |
|---|---|---|---|
+0x00 |
u16[3] |
vertex indices v0, v1, v2 |
confirmed (code) |
+0x06 |
u16 |
flags, below | confirmed (code) |
+0x08 |
u8 |
material id (MATERIAL_*, below) |
confirmed (code) |
+0x09 |
u8 |
the area byte: 0 to 124; copied into hit results; Collision_DropToMarkedGround passes through triangles where it is 0 |
confirmed (code) for the uses; meaning speculative (an area or zone number) |
The face normal is (v1 - v0) × (v2 - v0); one-sided tests only hit the side it points to.
Flags, confirmed (code) at 0x00350778 and 0x00351468 unless stated:
| Bits | Meaning |
|---|---|
0 (0x0001) |
enabled; set on every triangle at load, switched by CollisionMesh_SetEnabledInBox |
1 (0x0002) |
two-sided |
2-10 (0x07fc) |
type bits: a query skips a triangle when flags & 0xfff & mask & ~0x801 is non-zero. Known meanings: bit 2 (0x0004) climbable by a player and bit 7 (0x0080) climbable (by anyone), read from a ray's result by Climb_TryStart (0x002826f0, Characters); bit 4 (0x0010) shadow ground, where a human may hide (Stealth); bit 5 (0x0020) sets human byte +0x5b7 under the feet (0x002195e0). The rest are not traced |
11 (0x0800) |
testable while disabled, if the query's mask has 0x800 |
| 12-15 | a value 1 to 15; never read by the queries on this page (meaning unknown). The physics sweep of a walking body reads bits 14-15 as an index (0x00347c08, not traced) |
Bit 1 is almost never set on the disc: doors and glass panes set it on their own triangles when they spawn
(Collision_SetTriangleTwoSided 0x003a4768, Objects), so until then a doorway is a
one-sided wall.
Disc check (corroboration), triangles with each bit: bit 0 all 150,567; bit 1 4; bit 2 2,717; bit 3 4,231; bit 4 1,052; bit 5 17,421; bit 6 0; bit 7 16,316; bit 8 20,231; bit 9 4,786; bit 10 3,989; bit 11 4.
Materials¶
0x0034fc08 maps a material id to its name through a jump table at 0x005785d0 with 191 entries (confirmed (code)):
0 undefined, 1 MATERIAL_NONE, 2 GLASS, 3 STEEL, 4 ALUMINIUM, 5 CONCRETE, 6 ASHPHALT (sic), 7 LAMPOST,
..., 35 GRASS, 36 DIRT, ..., 186 SAND, 187 STOREDOOR_GLASS, 188 PUNCHING_BAG, 189 FOAMHAND, 190 SALAMI
(names without their MATERIAL_ prefix after the first). The material decides sounds and effects elsewhere (inferred
from the names). Disc check: 31 ids occur in the level meshes: 0-7, 12, 13, 15, 16, 18, 28, 30, 31, 35, 36, 41,
47, 95, 105-107, 114-116, 118, 122, 152, 153; the most common is 116 (34,199 triangles).
Materials that change movement, confirmed (code): a triangle of material 30 (LOW_FENCE) is climbable whatever
its flags (Climb_TryStart), and while a human climbs over (record +0x08 0x40), its body's contacts with
materials 30, 31 (OPAQUE_FENCE) and 122 (RAILING) are ignored (0x00219d50), so it passes through
the fence (Characters). In level99's mesh (1,718 triangles, read at runtime) the
material-30 triangles stand away from the start room. Material 2 (GLASS) has no movement rule: a pane's triangles
are walls until it breaks, and only the line-of-sight ray of a strike excludes them
(Moving into a pane).
Behaviour¶
Ray cast¶
A ray is origin (+0x00, float[4]), a unit direction (+0x10) and a length (+0x20). A result
(confirmed (code) for the fields the casts write and the callers read):
| Offset | Meaning |
|---|---|
+0x00 |
unit face normal; for a two-sided triangle, flipped to face the ray |
+0x10 |
u32 hit: 1 when something was hit |
+0x14 |
t, the distance along the ray |
+0x18 |
0 |
+0x28 |
material id (the world wrapper sets 1, MATERIAL_NONE, before the cast) |
+0x2c |
u16 the triangle's flags & 0xfff |
+0x2e |
u8 the triangle's area byte |
+0x30 |
pointer to the triangle |
CollisionMesh_RayCast(mesh, ray, result, excludeMaterials, mask) (0x00350cd8), confirmed (code):
end = origin + direction × length. Both ends go to grid space (M·p); per axis, the smaller and the larger, clamped to the clamp box and truncated, give the cell box[xmin, xmax] × [ymin, ymax] × [zmin, zmax].- Clear the checked bit set.
- If
xmin == xmaxorymin == ymax: test every cell of the box. -
Otherwise walk the
xcolumns: for eachixfromxmintoxmax, evaluate the ray's 2D line in grid space (y = slope·x + c, from the unclamped grid-space ends) atixand atix + 1; each value ismax(·, 0)and truncated; order the two, clamp them to[ymin, ymax], and test every cell from the lower to the uppery, for everyzfromzmintozmax.Note that the walk uses column edges at integer grid coordinates while the point-to-cell rule centres cells on integers, so the walk covers its columns half a cell off; a reimplementation that wants the same hits keeps the same arithmetic.
CollisionMesh_RayTestCell(mesh, list, ray, result, excludeMaterials, mask) (0x00350778), for each triangle of the
cell's list, confirmed (code):
- Skip it if its checked bit is set; set it (so a triangle listed in several cells is tested once per cast).
- Skip it if
flags & 0xfff & mask & ~0x801is non-zero. - Skip it unless it is enabled: flag bit 0, or mask bit 0, or (mask bit 11 and flag
0x800). - Intersect (step 5); the material test comes after it. An accepted
ton a triangle whose material is inexcludeMaterials(0x00350538: au32list ended by the value 1; a null list excludes nothing) does not count as a hit, but while nothing has been hit yet it still fills the result (hit 0,t, the material at+0x28, the flags and the triangle), with no nearer-than test; a later real hit overwrites it, and once a real hit is kept excluded triangles are skipped. So a clear ray reports the material of a see-through triangle it crossed (the last one tested): AI: GetItem reads 2 (GLASS) as "an intact pane in the way". Confirmed (code) at0x00350778. - Intersect:
RayTriangle_TwoSided(0x00337a60) when flag bit 1 is set, otherwiseRayTriangle_OneSided(0x00337920). Both are Möller-Trumbore tests that returntor -1. The one-sided test rejects a determinant below1e-6(rays hitting the back are ignored); the two-sided one rejects|det| < 1e-6. - Accept
0 <= t <= length. While the result has no hit yet, the triangle fills it with no nearer-than test and whatever the material filter says (an excluded triangle leaves the hit at 0 but writes itst, material, flags, area and triangle); once there is a hit, an excluded triangle is skipped and a kept one replaces it only when nearer (Physics, Unit 16).
So a cast returns the nearest kept hit along the whole ray, not the first cell's; with none, the last excluded triangle tested.
WorldManager_RayCast(worldManager, ray, result, exclude, mask) (0x0040db88) sets result +0x28 = 1 and the hit
to 0, then casts into the level object's mesh (*(worldManager + 0x40) + 0x04). Everything outside RayCast/ uses
this one (46 functions, 61 call sites; mask 0 and no exclusion list at 55 of them, Unit
16); the cameras (Camera/, 0x00121720-0x0013c710) also call CollisionMesh_RayCast
directly
(19 callers).
Ground height¶
Both take a position and replace it; the ray points down, (0, 0, -1) (0x00511770), from the position, with the
length given. Confirmed (code):
Collision_DropToGround(length, &pos)(0x0034f950): on a hit,pos.z -= t - 0.1: the point ends 0.1 above the ground. Returns whether it hit.Collision_DropToMarkedGround(length, &pos)(0x0034fa28): casts down; while the hit triangle's area byte is 0, moves the origin down byt + 0.01, shortens the length by the same and casts again; on a triangle with a non-zero area byte, setsposto 0.25 above that hit. Returns 0 when a cast misses.Collision_MarchRay(step, max, &out, &origin, &dir)(0x0034f740): casts the segmentdir × stepfromorigin, then from its end, and so on untilmaxis covered;out= the first hit point.
Sphere push-out¶
CollisionMesh_SpherePush(radius, mesh, ¢re, exclude, &firstNormal) (0x003519f8) moves a sphere out of the
walls it overlaps and returns whether it touched any. Called by the cameras and 0x00143590. Confirmed (code):
- Cell box of
centre ± radius(per axis, as for points); visit its non-empty cells (0x00351468for each) with the checked bit set cleared first, so each triangle counts once. - For each triangle: enabled (bit 0), not excluded by material, and a wall: unit normal
nwith|n.z| <= cos 15°(cosfat0x004b8a70). Floors and ceilings never push. dist = n·centre - n·v0. A one-sided triangle withdist < -1e-5is skipped (the sphere is behind it); a two-sided one flipsnanddistinstead.- If
radius - dist > 1e-5and the centre's projection onto the plane lies inside all three edges (for each edge,(edge × n)against the point): addn × (radius - dist)to a push sum, count it, and setfirstNormaltonif it is still zero. - Afterwards, if anything was counted:
centre += push / count(the average, not the sum).
Only the triangle's face counts: a sphere touching an edge or corner and not the face is not pushed.
Switching triangles on and off¶
CollisionMesh_SetEnabledInBox(mesh, min, max, on) (0x00351160) visits the cells of the box and, for every
triangle whose three vertices lie inside it, sets (on) or clears flag bit 0 (0x00350aa0). Its only caller,
0x0034fba0(objectId, on), uses a game object's box (object +0x10/+0x20); inferred to be how doors and
breakable barriers open a gap in the static collision (five level scripts call ChangeCollision; the binding was not
traced to it).
Lifetime¶
The mesh lives in the World Level Pool with the rest of the level file and goes with it. The level object's
destructor (0x0040cf80) frees the mesh's chunks (0x004f4b78). Nothing updates it per frame:
CollisionMesh_UpdateEmpty (0x00351158), which eleven game modes' updates call every frame, does nothing.
The destructor's order is in Physics, Unit 17.
Disc counts¶
All 64 .lev files (NTSC-U, 2026-10-04): 150,567 triangles and 94,294 vertices in all. Facing, from each triangle's
normal in game axes: 39,531 face up (n.z > cos 15°), 5,060 down and 105,976 are walls. Ground mostly faces up, so
the mesh is in game axes with z up. Every vertex has w = 1.0; no index is out of range; the 0x52 and 0x07 sizes
match the header's counts; +0x98 equals the lowest vertex z everywhere. level118's header is odd: a y scale of
0 and a non-zero u16 at +0x76 (its grid still has nx·ny·nz entries).
The hangout's railings¶
level95's mesh (level95.lev on the disc, 5,943 triangles) holds the clubhouse interior far below the
street, between z −200 and −180. The balcony floor is at z −194.38 (material 16) and the room below at −197.52
to −197.58 (material 5). Read from the disc, 2026-10-10:
- The railings along the balcony's edge are material 7 (
LAMPOST) with flags0xa1(enabled, bit 5 and bit 7, climbable) and top bits 1, 3, 12, 13 or 15: triangles 2614-2619 (face 0.90 m tall, from −194.38 to −193.48, at x ≈ −165 facing −x and along y ≈ 99 facing −y) and 2620-2623 (at x ≈ −162.2 facing −x; 2622-2623 follow the stairs down to −197.56). All are one-sided, facing the balcony, and the floor behind each is 3.1 to 3.2 m lower. The only other climbable faces inside are a 1.09 m box (1290-1299, materials 3, 5, 6 and 15). - The fence materials inside are few: 2 triangles of material 122 (2721-2722, nearly flat, by the stairs' head) and 7 of material 31 (the balcony's edge faces below the railings, facing the room below). None is climbable.
So a climb over a hangout railing meets material 7, which Human_OnContact does not skip; the original passes it
because no sweep runs while climbing over (Characters). Outside the clubhouse, the level's
climbable short faces (0.69 to 1.7 m tall, flag 0x80 or 0x4) are of materials 0, 1, 3, 5, 6, 12, 13, 15, 41,
47, 116 and 122, so most are not fence materials either (material 30's 96 wall triangles there are all 1.7 m
tall or more).
Coney's implementation¶
src/raycast/collision_mesh.h (2026-10-04), platform-neutral, in game axes:
onCollisionMeshLoadedis the0x03handler: it pops the header and the five other chunks in reverse file order and pushes aCollisionMeshobject.CollisionMesh::buildchecks what the queries follow before anything is used: the header's counts against the chunk sizes, the clamp box against the grid, every grid offset and list against the index lists, every listed triangle and every triangle's vertices against their counts. It sets every triangle's enabled bit, as the original's handler does.rayCastisCollisionMesh_RayCastwithRayTestCell: the same cell box, the same column walk (half a cell off, as above), the per-cast checked set, the mask and enabled rules, a material exclusion (a span of ids in place of the list ended by 1) and the nearest hit. It skips an excluded triangle before intersecting it and returns only a kept hit, so unlike the original it cannot report the excluded triangle a clear ray crossed (Unit 16).spherePush,setEnabledInBox,dropToGround,dropToMarkedGroundandmarchRayfollow the sections above; the ground helpers cast with mask 0 and no exclusions, which is what the original'sCollision_DropToGround,_DropToMarkedGroundand_MarchRaypass (Unit 16).- Coney's choices:
marchRay's last step is shortened to end at its maximum; the sphere push tests the centre's projection against the edges with the face's own normal, whichever side the sphere is on (the page gives the test as "edge × n against the point" without its sign). - Unit tests on synthetic meshes cover a floor, a one- and a two-sided triangle, a wall from either side, the type bits, the mask, disabled triangles, material exclusion, a triangle listed in many cells, a long diagonal ray across the grid, marked ground and refused data.
- The human (
src/human/human.*, Characters) stands, lands and spawns withrayCast(mask 0; while climbing over, the fence materials 30, 31 and 122 excluded), and is pushed out of walls by its own nearest-triangle test rather thanspherePush(src/human/body.*): the walking sphere is kept a radius from the closest point of each wall triangle (|n.z|≤ 0.65) that the move goes into and that is not under 0.25 m tall, so it cannot slip past a convex edge, which the face-only test allowed. Its climbs read the triangles' flags0x4and0x80and material 30 (Climbing). The follow camera casts with mask0x200. src/raycast/collision_builder.h(Coney's own, for the sandbox) writes the six chunks above from a list of triangles and loads them throughCollisionMesh::build, so a built mesh passes the same checks as a level's. Shared corners become one vertex. The grid's cells are the size asked for; the matrix maps the bounds onto them with z left out (one layer). Each triangle is listed in every cell its bounding box covers, plus one cell on each side. The checked set is sized by the header rule. Unit tests cast rays through a built mesh against a brute-force cast. Beside it,CollisionMesh::addExtraTriangles(also Coney's own; the original never changes a loaded mesh's geometry) adds triangles after a loaded mesh's own for the debug menus' Spawner: each listed the same way through the mesh's own matrix, in a new list per cell that copies the cell's old entries first, so the disc's triangles, indices and list order are unchanged, andclearExtraTrianglesrestores the mesh as loaded.
Disc check (NTSC-U, 2026-10-04, counts only): coney_tests "[disc][collision]" loads all 64 .lev files
through the chunk system with the 0x03 handler: none fails; 150,567 triangles and 94,294 vertices, 39,531 facing up,
5,060 down and 105,976 walls, as above. A point 1 above the centre of every up-facing triangle drops onto ground in all
39,531 cases (39,202 onto that triangle; the rest onto something in between). Two slanted rays at each of those
centres, a steep one 22 long and a shallow one about 102 long, give the same nearest hit through the grid walk as a
brute-force cast over every triangle in all 79,062 casts: on the disc's data the walk's half-cell offset loses no hit
on these rays.
ChangeCollision (2026-10-06) switches the level's triangles wholly inside a volume box with
CollisionMesh::setEnabledInBox (the box's corners sorted per axis). Coney choice: only volume boxes name a box;
the original also takes any object with a box at +0x10 / +0x20.
Open questions¶
- Flag bits 3, 6, 8-10 and 12-15 (bits 2 and 7 mark climbable triangles, bit 4 shadow ground, bit 5 human
+0x5b7), and the masks the callers pass: which other bits mean what (stairs, no-camera, water?). The follow camera's rays use mask0x200(Camera). - The area byte (
+0x09): what the 125 values number. level118's header: is that level's grid usable as it is (itsyscale is 0)?- The cameras' and characters' use (partly answered): characters stand on the ground by a ray 1.0 m above the
feet, 1.5 m down, each update, and land on triangles with
n.z> 0.65; walls for walking bodies are the physics code's own swept sphere against triangles with|n.z|≤ 0.65, skipping those under 0.25 m tall (Characters); climbs read flags0x4and0x80and material 30 (Characters). The masks and exclusion lists of the otherWorldManager_RayCastcallers are not listed. ChangeCollision: whether the Lua binding reaches0x0034fba0.