floor test inclusion rule from the code (edge planes with a small tolerance, highest plane wins): explains kuzco floating next to the crate in jungle day 2 (needle triangle tip), tools/floortest.py re-implements the engine test
This commit is contained in:
1 parent
c39d58bdac
commit
9e8e7f9a10
5 files changed
+183
-1
No files matched your search
+90
-1
@@ -88,7 +88,93 @@ and are set on load.
|
||||
* normals ×127 (code multiplies by 32 → 4096 scale), d ×4096 (fixed point) → local coordinates only ±8 units!
|
||||
* type bit0 = quad (4 edges), otherwise triangle (3 edges, 5th plane = 0). Bit2 ≈ normal points down (ny<0; ceilings/overhangs) [likely].
|
||||
* material: ground type (0 = normal; 0x11/0x12 are ignored depending on WFPC 0x8000000, 0x1e = water/death plane [likely]; the value ends up in the player's ground state).
|
||||
* Collision queries: `FUN_004036d0` (floor/ceiling), `FUN_00403ad0` (walls), `FUN_004042f0`, `FUN_004046e0`. Only **the tiles of the grid cell the player stands in** are tested (cell list from MAP), point relative to the tile's B position, rotated by −rot.
|
||||
* Collision queries: `FUN_004036d0` (floor/ceiling against the map tiles), `FUN_00403ad0` (the same floor test against strat objects with a collision model, not walls – see the floor test section below), `FUN_004042f0`, `FUN_004046e0`. Only **the tiles of the grid cell the player stands in** are tested (cell list from MAP), point relative to the tile's B position, rotated by −rot.
|
||||
|
||||
### Floor test: inclusion rule and the floating-next-to-the-log case
|
||||
(2026-11-08, from the decompile + objdump of 0x4036d0–0x403ad0, a bit-exact Python re-implementation
|
||||
`tools/floortest.py`, and live reads in game. [certain] unless marked.)
|
||||
|
||||
**Who calls what.** Per strat and frame `FUN_00403020(strat, models=DAT_005846ec, world=DAT_00584648)`:
|
||||
`FUN_004036d0` = floor test against the **map tiles of the strat's cell**; `FUN_00403ad0` = the same test
|
||||
against **strat objects with a collision model** (strat+0xec bit 0x20, within the box radius strat+0x13c;
|
||||
result index = (strat − DAT_006d9dc0)/0x154), skipped when strat+0xe8 bit 0x400. Neither is a wall test.
|
||||
The map result wins unless the object floor is higher. Result → floor record *(strat+0xc0):
|
||||
+0 height, +4 material (record byte 1), +6 flags (bit0 map floor, bit1 object floor), +0xc tile, +0xe polygon.
|
||||
Material 0x1e → treated as "no floor" (height 0xffff0000).
|
||||
|
||||
**FUN_004036d0, per tile of the cell, per collision record** (all ints, 20.12):
|
||||
1. Probe point, tile-local: `lx,lz` = (strat x/z − tile B x/z) rotated by −ry with the static sine table
|
||||
`DAT_00574318` (4096 entries, cos = sin[i+0x400]), `ly = strat y − tile y + DAT_0057dd98`;
|
||||
DAT_0057dd98 = **0x400 (0.25)**, or floor-record+0x18 / 2 for strats with flag 0x200000.
|
||||
2. Skip material 0x11 (0x12 instead when strat+0xe8 bit 0x8000000). Skip ny = 0; skip ny < 0 unless flag 0x200000.
|
||||
3. Height of the probe above the surface plane, measured **vertically**:
|
||||
`i6 = (d0·4096 − nz0·32·lz − nx0·32·lx − ny0·32·ly) / (ny0·32)` (C division), `dist = −i6`.
|
||||
The plane point under/over the probe is `Y = ly + i6` (plane equation solved for y at (lx,lz)).
|
||||
4. `dist < −0x80` (plane more than 1/32 above the probe) → ceiling/"surface above" branch (below).
|
||||
Else, if `dist < best` (strictly; initial 1000000) and ny > 0 → edge test.
|
||||
5. **Edge test** (3 edges, 4 if type bit0), on the point (lx, Y, lz) *on the plane*:
|
||||
`e_k = ((nyk·32·Y + nzk·32·lz + nxk·32·lx) >> 12) − dk`; **fail if e_k < tol** with
|
||||
`tol = −0x40 − ((0x1000 − ny0·32) >> 6)` — i.e. 64 + (4096−4064·ny)/64 units of 1/4064:
|
||||
**≈ 0.016 for flat floors, 0.023 at ny 0.5** (perpendicular distance outside the edge plane).
|
||||
No radius, no vertex test, no other use of the record (bytes 5, 11, … are padding; for triangles the
|
||||
5th plane is never read). The point must be inside (within tol) **every** edge plane.
|
||||
6. All edges pass → `best = dist`, height = tile y + Y, tile, polygon index (counted over groups A+B+C).
|
||||
**Selection rule: the passing plane with the smallest dist, i.e. the highest surface not more than
|
||||
0.25 + 1/32 above the feet.** Consequence: a passing surface up to 0.28 above the feet wins over the
|
||||
ground under the llama, and the llama is put onto it (verified, step 3 below).
|
||||
7. Ceiling branch (dist < −0x80): passing up-facing planes are kept as "lowest surface above"
|
||||
(local_70/local_5c). `FUN_00403020` uses that as the floor when no floor below was found
|
||||
(unless strat+0xe8 & 0x40200) – from the decompile only, not observed.
|
||||
|
||||
**Why polygon edges reach out: the tolerance turns sharp corners into long spikes.**
|
||||
Every edge is moved outwards by `tol`. At a corner with interior angle θ the accepted area therefore
|
||||
sticks out by ≈ tol / sin(θ/2): 0.023 at a right angle (every normal log/crate edge, all quads of the
|
||||
path), but ~0.19 at a 13.5° corner.
|
||||
|
||||
**The case (t1l4m002, world 4 chapter 2).** Cell 57,43 contains only tile 443 = model 129 (the
|
||||
wooden crate/log with its piece of path) at B (57.5, 2.0, 43.5), ry 3072. The transform was checked
|
||||
three ways: render faces (with z mirrored, see §2) and collision of model 129 line up (crate box on the
|
||||
crate box); the bit-exact engine re-implementation gives exactly the live floor record; the debug-wireframe
|
||||
screenshot shows the crate's cut end right next to the llama. Collision polygon 15 (type 0 = triangle, mat 0,
|
||||
plane (108,65,10)/127 → ny 0.51, a steep bevel strip along the crate's side) is a **needle triangle**:
|
||||
corners (world x/y/z) (57.725, 2.553, 43.734), (57.750, 2.651, 43.677), (57.315, 2.497, 43.729); the corner at
|
||||
57.315 has **13.5°** and points away from the crate, exactly along the crate's side face (x ≈ 57.31).
|
||||
Its tolerance zone is a 0.01–0.02 wide sliver that continues ~0.19 beyond the tip, over the path
|
||||
(pink in `img/floor_needle_t1l4m002.png`). Kuzco's position 57.204 / 43.733:
|
||||
* local (0.2329, ·, 0.2959); poly 15 edges e = (−37, 2005, −72) ≥ tol −95 → **passes**; Y = 2.4744 (the
|
||||
steep plane extended 0.11 beyond its tip), dist 0.2498.
|
||||
* poly 2 (path, ny 0.99) also passes, e = (525, 304, 850, 1101), height 2.270, dist 0.454 → loses
|
||||
because 2.474 is higher and still within 0.28 of the feet.
|
||||
* The sliver at x = 57.204 is only z 43.7300–43.7402 wide; x reaches down to ≈57.12.
|
||||
* Why exactly x = 57.204 [verified, mechanism not decoded]: putting the llama into the crate
|
||||
(x 57.25, z 43.60 / 43.70 / 43.733) the game pushes it back to x = 57.202–57.204 (wall push-out from the
|
||||
crate side at ~0.11 distance). So walking along/against the crate's side face puts the llama exactly on
|
||||
the line the needle's tolerance sliver extends to; only the ~1 cm band at the corner's z catches it.
|
||||
|
||||
**In game** (apptest :25, boot-to-level start "57.204, 2.474, 43.733, 0 @ 4/2", winedbg --gdb read/write of
|
||||
strat +0x30/+0x50 and the floor record):
|
||||
|
||||
| written pos (x y z) | game afterwards: pos / floor h / poly | re-implementation |
|
||||
|---|---|---|
|
||||
| boot start 57.204 2.474 43.733 | 57.204/2.474/43.733, h 2.474, mat 0, tile 443 **poly 15** | poly 15, 2.4744 |
|
||||
| 57.204 **2.27** 43.733 (on the path) | snapped **up** to y 2.474, poly 15 | poly 15, 2.4744 |
|
||||
| 57.204 2.27 43.736 | y 2.469, poly 15 | poly 15, 2.4690 |
|
||||
| 57.204 2.27 43.745 | y 2.270, poly 2 | poly 2, 2.2700 |
|
||||
| 57.15 2.27 43.733 | y 2.266, poly 2 | poly 2, 2.2661 |
|
||||
| 57.25 2.3 43.70 | pushed to x 57.204, y 2.270, poly 2 | – |
|
||||
|
||||
Screenshots: `img/floor_float_t1l4m002.jpg` (left: boot at the floating spot, poly 15; right: 0.012 further
|
||||
in z on the path, poly 2; debug wireframe on), `img/floor_float_t1l4m002_full.jpg`.
|
||||
After teleporting onto poly 15 with the debugger the llama sometimes slid off within seconds (steep
|
||||
plane, ny 0.51); started there by boot-to-level it stays [observed, not analysed].
|
||||
|
||||
**How common.** Scan of all 31 t1 level WADs (sample points 0.3/0.6/0.9 of the way from each corner to its
|
||||
tolerance tip, exact engine test over all tiles of that cell, counted when this polygon wins, the point
|
||||
is ≥ 0.08 outside the real polygon and the next lower floor is > 0.05 below): 2–526 polygons per level,
|
||||
t1l4m002: 110 (of 6468 floor-polygon instances), poly 443/15 among them (predicted spot 57.199/2.469/43.735).
|
||||
Most are sharp triangle tips of terrain/rock meshes at ledges; normal right-angled log/crate edges
|
||||
reach only 0.023 and never show it. [Counts are a heuristic scan; reachability of the spots not checked.]
|
||||
Scripts: `tools/floortest.py` (single position); scan code: `$CLAUDE_JOB_DIR/tmp/floorre/exposed.py` + `reach.py` (scratch, not in the repo).
|
||||
|
||||
### Face flags (u16)
|
||||
| Bit | Meaning | Confidence |
|
||||
@@ -217,3 +303,6 @@ Implemented in `tools/geom_import.py` (OBJ → TRAK + MAP tile part, rest of the
|
||||
* `geom_import.py BASE.wad MESH.obj NEW.wad [--coll-obj C.obj] [--keep-tiles] [--zones flat|keep] [--default-tex N] [--export-space]`
|
||||
* `geom_demo.py BASE.wad demo.obj [--plaza]` – example geometry made of texture-tile quads.
|
||||
* `geomlib.py` – parser/builder (TRAK, STPC models, MAP tile part, TEXT table/pages, collision).
|
||||
* `floortest.py WAD X Y Z` – bit-exact re-implementation of the engine's map floor test (FUN_004036d0) at a world
|
||||
position: every candidate polygon of the cell, its edge values/tolerance, and the chosen floor. Note: the engine's
|
||||
plane scale is n/127 with d/4064 (normal×32 vs d×4096), `geomlib.coll_decode` uses d/4096 (0.8 % off).
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 231 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 148 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 8.1 KiB |
Reference in new issue
Block a user