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:
@@ -244,10 +244,12 @@ fn load_source(dir: &std::path::Path, level: usize) -> Result<(LevelFiles, [f32;
|
||||
let core_data = std::fs::read(level_dir.join("core_data.bin")).context("core_data.bin")?;
|
||||
let core_index = std::fs::read(level_dir.join("core_index.bin")).context("core_index.bin")?;
|
||||
let gs_ram = std::fs::read(level_dir.join("gs_ram.bin")).context("gs_ram.bin")?;
|
||||
let overlay = std::fs::read(level_dir.join("overlay.bin")).ok();
|
||||
let src = LevelSource {
|
||||
core_data: &core_data,
|
||||
core_index: &core_index,
|
||||
gs_ram: &gs_ram,
|
||||
overlay: overlay.as_deref(),
|
||||
};
|
||||
let (centre, _radius) = level_bounds(&src)?;
|
||||
Ok((
|
||||
@@ -255,6 +257,7 @@ fn load_source(dir: &std::path::Path, level: usize) -> Result<(LevelFiles, [f32;
|
||||
core_data,
|
||||
core_index,
|
||||
gs_ram,
|
||||
overlay,
|
||||
},
|
||||
centre,
|
||||
))
|
||||
@@ -265,6 +268,7 @@ struct LevelFiles {
|
||||
core_data: Vec<u8>,
|
||||
core_index: Vec<u8>,
|
||||
gs_ram: Vec<u8>,
|
||||
overlay: Option<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl LevelFiles {
|
||||
@@ -273,6 +277,7 @@ impl LevelFiles {
|
||||
core_data: &self.core_data,
|
||||
core_index: &self.core_index,
|
||||
gs_ram: &self.gs_ram,
|
||||
overlay: self.overlay.as_deref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -389,6 +394,7 @@ fn mesh_of(b: &TriangleBatch) -> Option<Mesh> {
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, b.positions.clone());
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, b.uvs.clone());
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_COLOR, b.colors.clone());
|
||||
mesh.insert_attribute(Mesh::ATTRIBUTE_NORMAL, b.normals.clone());
|
||||
// No normals and no colours: lighting is the game's own per-vertex light
|
||||
// array, which is parsed but not yet reproduced. Inventing normals would
|
||||
// make unlit terrain look lit and hide that gap.
|
||||
|
||||
Reference in New Issue
Block a user