Decode tfrag normals from the overlay's sine table
Normals are per-vertex, and the tfrag's light records store them as an azimuth and elevation *index* into a 256-entry sine table rather than as angles. This decodes that table and builds normals for the renderer. The reference reads the table from an ELF at BOOT_NORMAL_TABLE_VADDR (0x165500) and notes every level overlay carries a copy. The overlay has no ELF header -- it is a raw loadable image starting with code -- so there is no program header to map a virtual address through, and the offset is not fixed either: level 0 has the table at 0xb650c, level 5 at 0xcca30, level 26 at 0xb6284. So it is located by content. Every table starts with exactly (1.0, 0.0), angle zero, and the scan then requires all 256 entries to match cos(i*pi/128), sin(i*pi/128). Validating the whole curve is what makes the search safe: a lone (1.0, 0.0) pair of floats occurs by accident in a 2.7 MB executable, and a test plants one as a decoy. The values are read from the disc rather than recomputed, since those are the numbers the game indexes. A normal is (cos(az)cos(el), sin(az)cos(el), sin(el)) *negated* -- the stored normals point into the surface -- and then given the same Z-up to Y-up swap as positions, or the lighting comes out sideways. The azimuth and elevation fields are stored signed but index 0..=255, so entry 200 must be read as 200, not as -56; there is a test for that. Also frames and parses the level light bank from the gameplay extent, and the pipeline now writes overlay.bin. The per-vertex lighting evaluation is not written yet, so the renderer stays unlit by default. GC_ENGINE_LIT=1 substitutes a host directional light for inspection -- that is a stand-in, not the game's lighting, and it is deliberately not the default. The hash table is regenerated for the 27 new overlay.bin files: now 5,213 files and 5,762,497,559 bytes.
This commit is contained in:
@@ -528,16 +528,36 @@ brightness changes did nothing at all. Faking it would also have hidden the
|
||||
gap. The colour attribute is picked up from the mesh automatically, so it is
|
||||
the *colour* data that is missing lighting, not a flag that was left off.
|
||||
|
||||
The real model is `LightTfrags`, which needs three things this project has not
|
||||
parsed yet:
|
||||
The real model is `LightTfrags`, which needs three things:
|
||||
|
||||
* the **light bank** from the level's `gameplay` extent (directional and point
|
||||
light sets, and the per-vertex light `select` field);
|
||||
* the **normal table**, 256 pairs of cos/sin read from an ELF at
|
||||
`BOOT_NORMAL_TABLE_VADDR` (`0x165500`) -- every level overlay carries one, so
|
||||
it does not need the boot ELF;
|
||||
* the **light records** themselves, which are already parsed: azimuth and
|
||||
elevation at pi/128 radians per unit, and the base colour.
|
||||
* the **light records** -- already parsed: azimuth and elevation as *indices*
|
||||
into the overlay's sine table, plus the base colour;
|
||||
* the **normal table** -- now decoded (`gc_formats::tfrag_light`), and normals
|
||||
are built and fed to the renderer;
|
||||
* the **light bank** from the level's `gameplay` extent -- framed and parsed,
|
||||
but the per-vertex evaluation is not written yet.
|
||||
|
||||
### The normal table, and why it is found by searching
|
||||
|
||||
The reference reads the table from an ELF at `BOOT_NORMAL_TABLE_VADDR`
|
||||
(`0x165500`) and notes every level overlay carries a copy. **The overlay has no
|
||||
ELF header** -- it is a raw loadable image starting with code -- so there is no
|
||||
program header to map a virtual address through, and the offset is not fixed
|
||||
either: level 0 has the table at `0xb650c`, level 5 at `0xcca30`, level 26 at
|
||||
`0xb6284`.
|
||||
|
||||
So it is located by content. Every table starts with exactly `(1.0, 0.0)` --
|
||||
angle zero -- and the scan then requires all 256 entries to match
|
||||
`cos(i·pi/128), sin(i·pi/128)`. Validating the whole curve is what makes the
|
||||
search safe: a lone `(1.0, 0.0)` pair of floats occurs by accident in a 2.7 MB
|
||||
executable, and a test plants one as a decoy. The values read from the disc are
|
||||
used as-is rather than recomputed, since those are the numbers the game indexes.
|
||||
|
||||
A normal is `(cos(az)cos(el), sin(az)cos(el), sin(el))` **negated** -- the
|
||||
stored normals point into the surface -- then given the same Z-up to Y-up swap
|
||||
as positions, or the lighting comes out sideways. The azimuth and elevation
|
||||
fields are stored signed but index 0..=255, so the byte is what matters, not its
|
||||
sign: entry 200 is read as 200, not as -56.
|
||||
|
||||
Given the table, a normal is `(cos(az)cos(el), sin(az)cos(el), sin(el))`,
|
||||
negated, exactly as `decode_normal` builds it. The lighting pass itself is
|
||||
|
||||
+13
-5
@@ -14,7 +14,7 @@ early, while they are still cheap to retire.
|
||||
- [x] Repository hygiene in place (pre-commit guard, `.gitignore`, licence
|
||||
record)
|
||||
- [x] **Phase 1 complete.** The disc pipeline runs end to end and is verified
|
||||
against a committed hash table: 5,186 files, 5,685,541,143 bytes, every
|
||||
against a committed hash table: 5,213 files, 5,762,497,559 bytes, every
|
||||
one of the 92 asset containers parsing.
|
||||
- [x] **Phase 2 in progress.** Level containers are parsed through their typed
|
||||
header, and the `data` extent is parsed as a container of byte ranges. The
|
||||
@@ -35,11 +35,19 @@ early, while they are still cheap to retire.
|
||||
drawn unlit so the frame shows real albedo rather than a faked lighting
|
||||
model. Verified against the stored values (level 0 patch 0 runs
|
||||
`(25,26,26)` to `(108,144,141)`), which are genuinely dark.
|
||||
- [ ] **tfrag lighting.** Needs the level light bank from `gameplay`, the
|
||||
256-entry normal table from the overlay ELF at `0x165500`, and the
|
||||
already-parsed azimuth/elevation light records. The pass itself is
|
||||
- [x] **Tfrag normals.** The 256-entry sine table is decoded from the level
|
||||
overlay and normals are built per vertex and handed to the renderer. The
|
||||
overlay has **no ELF header**, so the table cannot be reached through a
|
||||
program header, and its offset differs per level (level 0 `0xb650c`,
|
||||
level 5 `0xcca30`); it is located by matching the whole 256-entry curve.
|
||||
Normals are negated as stored and get the same axis swap as positions.
|
||||
The pipeline now also writes `overlay.bin`.
|
||||
- [ ] **tfrag lighting pass.** The light bank from `gameplay` is framed and
|
||||
parsed; the per-vertex evaluation is not written yet. It is
|
||||
`base + A*max(dA, dA*wA) + B*max(dB, dB*wB)` and has to run in the PS2
|
||||
float model. Detail in `plan/level-assets.md`.
|
||||
float model. Until then the renderer stays unlit by default;
|
||||
`GC_ENGINE_LIT=1` substitutes a host directional light for inspection,
|
||||
which is not the game's lighting.
|
||||
- [ ] Then the GS register set and the guard-band clip test.
|
||||
|
||||
The disc being the exact build the reference projects pin is the single
|
||||
|
||||
Reference in New Issue
Block a user