3bc6c2d15566054a4d22046f2d3c35f04cb32868
4
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
65b5d366de |
Implement the PS2 float model and the LightTfrags pass
Two pieces, and the first is the reason the second can be trusted. ps2.rs is the EE's FPU on raw u32 bit patterns. It is not f32: there are no denormals (exponent 0 *is* zero), no NaN and no infinity (exponent 255 is an ordinary value, so MAX reads back in Rust as NaN -- a test asserts exactly that, because it is the sharpest available statement of why this module exists), every result truncates toward zero, the adder discards the small operand's bits more than one below the large one's LSB and returns the large operand unchanged across a 25-bit exponent gap, and rsqrt is sqrt-then-divide with truncation between the steps rather than one rounded operation. The lighting pass accumulates all of these across every vertex of every patch, so this is a visible difference rather than a rounding curiosity. tfrag_light.rs is the pass itself: PEXT5 base colour, the select word picking one light set or blending two by bits 8..15 / 256, blended directions re-normalised where unblended ones are not, and base + A.rgb*max(dA, dA*wA) + B.rgb*max(dB, dB*wB). Lighting indexes positions, not vertex-info entries, since a common entry and a LOD entry can share a position. TfragLight gains a `select` field for the u16 at +6. It was briefly reconstructed with an unsafe pointer cast over the struct, which is exactly the sort of thing that breaks silently the moment a field is reordered; it is now a plainly parsed field. The pass does not run yet. The light bank framing the reference uses -- a u32 offset at +0x04, an i32 count there -- gives offset 0xa0d and a count of 698810368 on RAC2 level 0, so RAC2's gameplay extent is not laid out that way. The renderer now counts the vertices it actually lit and prints it, because falling back silently to the stored colours produces a plausible but wrong picture: lighting: 0 of 80274 vertices shaded by LightTfrags; the rest fall back to the tfrag's stored colours (no usable light bank) That line is the current honest state, and the frame has not changed as a result. Where RAC2 keeps its light bank is the open question. |
||
|
|
90f02890f8 |
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. |
||
|
|
17c7e8ccc9 |
Draw tfrag per-vertex colour, unlit, and record why the frame is dark
The tfrag RGBA array now modulates the texture. It is indexed by *position*, not by vertex-info: several vertex-info entries can share a position, and the array is sized vert_count, which is a position count. Colour follows the same rule as the vertex itself. Morphing blends the position only -- the game keeps the high-detail colour -- while collapse replaces the vertex outright, colour included. Getting that the other way round would tint distant terrain. The material is unlit, and that is a decision rather than a shortcut. Level 0's colours really are dark (patch 0 runs (25,26,26) to (108,144,141)) and the game's lighting is what brightens them, so what is on screen is the true albedo. Worth recording: the obvious move -- add a directional light, or crank ambient brightness -- does nothing at all. With no normals the PBR path is degenerate, so the brightness change has no effect, and a host light would have hidden the gap rather than shown it. The colour data was always reaching the mesh; what is missing is lighting, not a material flag. Written up in plan/level-assets.md, including what LightTfrags actually needs: the light bank from the level's gameplay extent, the 256-entry normal table at BOOT_NORMAL_TABLE_VADDR (0x165500) which every level overlay carries, and the already-parsed azimuth/elevation light records. |
||
|
|
192dcde247 |
Render a level: first drawable frame, and the origin bug it exposed
Adds gc-engine's binary and a level.rs that joins the format layer to the renderer: LOD selection and morphing per patch, then triangles grouped by texture so each texture is one draw rather than one per triangle. The PS2 world is Z-up at 1024 raw units per world unit, so the conversion is (x, y, z) -> (x, z, -y). The sign flip on the third axis turns the PS2's right-handed frame into Bevy's left-handed one rather than negating a determinant, which is what stops the level rendering mirrored. Morphing and collapse are kept distinct. Morphing blends a vertex toward the midpoint of its two parents across its tier's band; collapse replaces the vertex outright, position and UV, once it is past the far edge. Morphing is a blend, collapse is a cut, and conflating them makes distant terrain shimmer. Blending has to happen in raw units before the axis swap: mixing a converted parent with an unconverted child scales the blend by 1024. Level 0 renders as 26,758 textured triangles over 92 textures, correct UVs, with LOD selection live (26 patches at LOD 0, 650 at LOD 2 from the start camera). GC_ENGINE_SHOT writes one frame as a PPM and exits. Rendering immediately exposed a bug no test could. Positions are i16 offsets from a per-patch origin, and the obvious rule -- the last non-index STROW row -- is wrong on RAC2, which also uses STROW for per-vertex light records that come *after* the origin. Taking the last one yields [186, 186, 186, 186] and collapses the whole level onto the world origin. The entire level then fit in a 63-unit box and every test still passed: vert_count matched, tri_count matched, every parent and triangle index resolved, because all of those are relative to the same broken origin. Only a screenshot showed it. The rule that works is the STROW immediately preceding the first V3_16 position unpack; with it, level 0 spans x 86..444, y 46..211, z -477..40, matching the patch bounding spheres. That is worth more than the renderer: a parser can be completely self-consistent and still describe the wrong thing, and where a value is absolute rather than relative it has to be checked against something independent. The frame is flat and dark because there are no normals and no lighting yet. That is the next task -- the per-vertex light records -- not something to paper over with an exposure fudge. |