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:
@@ -28,6 +28,7 @@ use gc_formats::tfrag::{
|
|||||||
parse_tfrag, tfrag_lod_link, tfrag_triangles, TfragAdGifs, TfragDrawMode, TfragGeometry,
|
parse_tfrag, tfrag_lod_link, tfrag_triangles, TfragAdGifs, TfragDrawMode, TfragGeometry,
|
||||||
TfragHeader, TfragsBlock,
|
TfragHeader, TfragsBlock,
|
||||||
};
|
};
|
||||||
|
use gc_formats::tfrag_light::NormalTable;
|
||||||
|
|
||||||
/// Raw position units per world unit.
|
/// Raw position units per world unit.
|
||||||
pub const UNITS_PER_UNIT: f32 = 1024.0;
|
pub const UNITS_PER_UNIT: f32 = 1024.0;
|
||||||
@@ -67,6 +68,8 @@ pub struct TriangleBatch {
|
|||||||
pub positions: Vec<[f32; 3]>,
|
pub positions: Vec<[f32; 3]>,
|
||||||
pub uvs: Vec<[f32; 2]>,
|
pub uvs: Vec<[f32; 2]>,
|
||||||
pub colors: Vec<[f32; 4]>,
|
pub colors: Vec<[f32; 4]>,
|
||||||
|
/// Surface normals, from the patch's own light records.
|
||||||
|
pub normals: Vec<[f32; 3]>,
|
||||||
/// Three vertex indices per triangle.
|
/// Three vertex indices per triangle.
|
||||||
pub indices: Vec<u32>,
|
pub indices: Vec<u32>,
|
||||||
/// The ad-gif in force per triangle, parallel to `indices`.
|
/// The ad-gif in force per triangle, parallel to `indices`.
|
||||||
@@ -78,6 +81,9 @@ pub struct LevelSource<'a> {
|
|||||||
pub core_data: &'a [u8],
|
pub core_data: &'a [u8],
|
||||||
pub core_index: &'a [u8],
|
pub core_index: &'a [u8],
|
||||||
pub gs_ram: &'a [u8],
|
pub gs_ram: &'a [u8],
|
||||||
|
/// The level overlay, for the normal table. Optional: without it the level
|
||||||
|
/// still builds, just without normals.
|
||||||
|
pub overlay: Option<&'a [u8]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where a level's terrain sits: the centre of its patch bounding spheres and
|
/// Where a level's terrain sits: the centre of its patch bounding spheres and
|
||||||
@@ -180,6 +186,25 @@ pub struct ResolvedVertex {
|
|||||||
pub color: [f32; 4],
|
pub color: [f32; 4],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The surface normal of a vertex's position, in Bevy space.
|
||||||
|
///
|
||||||
|
/// Taken from the patch's per-vertex light record, which stores an azimuth and
|
||||||
|
/// elevation index into the overlay's sine table rather than an angle. Morphing
|
||||||
|
/// does not rotate the normal -- the game lerps positions and leaves lighting
|
||||||
|
/// to its own pass -- so this is the vertex's own normal regardless of blend.
|
||||||
|
pub fn vertex_normal(g: &TfragGeometry, vinfo: usize, table: Option<&NormalTable>) -> [f32; 3] {
|
||||||
|
let Some(table) = table else {
|
||||||
|
return [0.0, 1.0, 0.0];
|
||||||
|
};
|
||||||
|
let i = g.position_index(vinfo);
|
||||||
|
let Some(light) = g.lights.get(i) else {
|
||||||
|
return [0.0, 1.0, 0.0];
|
||||||
|
};
|
||||||
|
let n = table.normal(light.azimuth_index(), light.elevation_index());
|
||||||
|
// Same Z-up to Y-up swap as positions, or the lighting comes out sideways.
|
||||||
|
[n[0], n[2], -n[1]]
|
||||||
|
}
|
||||||
|
|
||||||
/// The stored colour of a vertex's position, or opaque white if absent.
|
/// The stored colour of a vertex's position, or opaque white if absent.
|
||||||
pub fn vertex_color(g: &TfragGeometry, vinfo: usize) -> [f32; 4] {
|
pub fn vertex_color(g: &TfragGeometry, vinfo: usize) -> [f32; 4] {
|
||||||
let i = g.position_index(vinfo);
|
let i = g.position_index(vinfo);
|
||||||
@@ -276,6 +301,7 @@ pub fn mode_collapses(mode: TfragDrawMode) -> bool {
|
|||||||
|
|
||||||
/// Decode every patch in a level's tfrags block, as seen from `camera`.
|
/// Decode every patch in a level's tfrags block, as seen from `camera`.
|
||||||
pub fn build_level(src: &LevelSource<'_>, camera: [f32; 3]) -> anyhow::Result<LevelGeometry> {
|
pub fn build_level(src: &LevelSource<'_>, camera: [f32; 3]) -> anyhow::Result<LevelGeometry> {
|
||||||
|
let normals = src.overlay.and_then(|o| NormalTable::from_overlay(o).ok());
|
||||||
let ch = LevelCoreHeader::parse(src.core_index)?;
|
let ch = LevelCoreHeader::parse(src.core_index)?;
|
||||||
let table = texture_table(src.core_index, ch.tfrag_textures)?;
|
let table = texture_table(src.core_index, ch.tfrag_textures)?;
|
||||||
|
|
||||||
@@ -350,6 +376,7 @@ pub fn build_level(src: &LevelSource<'_>, camera: [f32; 3]) -> anyhow::Result<Le
|
|||||||
positions: Vec::new(),
|
positions: Vec::new(),
|
||||||
uvs: Vec::new(),
|
uvs: Vec::new(),
|
||||||
colors: Vec::new(),
|
colors: Vec::new(),
|
||||||
|
normals: Vec::new(),
|
||||||
indices: Vec::new(),
|
indices: Vec::new(),
|
||||||
ad_gifs: Vec::new(),
|
ad_gifs: Vec::new(),
|
||||||
});
|
});
|
||||||
@@ -359,6 +386,7 @@ pub fn build_level(src: &LevelSource<'_>, camera: [f32; 3]) -> anyhow::Result<Le
|
|||||||
batch.positions.push(r.position);
|
batch.positions.push(r.position);
|
||||||
batch.uvs.push(r.uv);
|
batch.uvs.push(r.uv);
|
||||||
batch.colors.push(r.color);
|
batch.colors.push(r.color);
|
||||||
|
batch.normals.push(vertex_normal(&g, v, normals.as_ref()));
|
||||||
}
|
}
|
||||||
batch.indices.extend_from_slice(&[base, base + 1, base + 2]);
|
batch.indices.extend_from_slice(&[base, base + 1, base + 2]);
|
||||||
batch.ad_gifs.push(*ad);
|
batch.ad_gifs.push(*ad);
|
||||||
@@ -534,3 +562,77 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod normal_tests {
|
||||||
|
use super::*;
|
||||||
|
use gc_formats::tfrag::TfragLight;
|
||||||
|
use gc_formats::tfrag_light::NormalTable;
|
||||||
|
|
||||||
|
fn table() -> NormalTable {
|
||||||
|
// 256 entries of (cos, sin) at i * pi / 128, as the overlay holds them.
|
||||||
|
let mut cos_sin = Vec::with_capacity(256);
|
||||||
|
for i in 0..256usize {
|
||||||
|
let a = i as f64 * std::f64::consts::PI / 128.0;
|
||||||
|
cos_sin.push([(a.cos() as f32).to_bits(), (a.sin() as f32).to_bits()]);
|
||||||
|
}
|
||||||
|
NormalTable { cos_sin }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn geometry_with_light(azimuth: i16, elevation: i16) -> TfragGeometry {
|
||||||
|
TfragGeometry {
|
||||||
|
positions: vec![[0, 0, 0]],
|
||||||
|
vertex_info: vec![gc_formats::tfrag::TfragVertexInfo {
|
||||||
|
s: 0,
|
||||||
|
t: 0,
|
||||||
|
parent: 0,
|
||||||
|
vertex: 0,
|
||||||
|
}],
|
||||||
|
lights: vec![TfragLight {
|
||||||
|
unknown_0: 0,
|
||||||
|
intensity: 0,
|
||||||
|
azimuth: azimuth as i8,
|
||||||
|
elevation: elevation as i8,
|
||||||
|
color: 0,
|
||||||
|
}],
|
||||||
|
..TfragGeometry::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_light_record_indexes_the_table() {
|
||||||
|
// Azimuth 0, elevation 0 is -X in the table, which becomes -X in Bevy.
|
||||||
|
let g = geometry_with_light(0, 0);
|
||||||
|
let n = vertex_normal(&g, 0, Some(&table()));
|
||||||
|
assert!((n[0] + 1.0).abs() < 1e-5, "{n:?}");
|
||||||
|
assert!(n[1].abs() < 1e-5 && n[2].abs() < 1e-5, "{n:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normals_get_the_same_axis_swap_as_positions() {
|
||||||
|
// Elevation 64 is pi/2, so the table gives a normal along Z, negated.
|
||||||
|
let g = geometry_with_light(0, 64);
|
||||||
|
let n = vertex_normal(&g, 0, Some(&table()));
|
||||||
|
// PS2 (0, 0, -1) must land on Bevy -Y.
|
||||||
|
assert!(n[0].abs() < 1e-5 && n[2].abs() < 1e-5, "{n:?}");
|
||||||
|
assert!((n[1] + 1.0).abs() < 1e-5, "{n:?}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_unsigned_index_is_used_even_when_the_field_reads_negative() {
|
||||||
|
// 200 as an i8 is -56, but it indexes table entry 200, not 56.
|
||||||
|
let g = geometry_with_light(-56, 0);
|
||||||
|
let from_field = vertex_normal(&g, 0, Some(&table()));
|
||||||
|
let mut g2 = g.clone();
|
||||||
|
g2.lights[0].azimuth = -56;
|
||||||
|
assert_eq!(from_field, vertex_normal(&g2, 0, Some(&table())));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_normals_fall_back_rather_than_panicking() {
|
||||||
|
let g = geometry_with_light(0, 0);
|
||||||
|
assert_eq!(vertex_normal(&g, 0, None), [0.0, 1.0, 0.0]);
|
||||||
|
let empty = TfragGeometry::default();
|
||||||
|
assert_eq!(vertex_normal(&empty, 7, Some(&table())), [0.0, 1.0, 0.0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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_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 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 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 {
|
let src = LevelSource {
|
||||||
core_data: &core_data,
|
core_data: &core_data,
|
||||||
core_index: &core_index,
|
core_index: &core_index,
|
||||||
gs_ram: &gs_ram,
|
gs_ram: &gs_ram,
|
||||||
|
overlay: overlay.as_deref(),
|
||||||
};
|
};
|
||||||
let (centre, _radius) = level_bounds(&src)?;
|
let (centre, _radius) = level_bounds(&src)?;
|
||||||
Ok((
|
Ok((
|
||||||
@@ -255,6 +257,7 @@ fn load_source(dir: &std::path::Path, level: usize) -> Result<(LevelFiles, [f32;
|
|||||||
core_data,
|
core_data,
|
||||||
core_index,
|
core_index,
|
||||||
gs_ram,
|
gs_ram,
|
||||||
|
overlay,
|
||||||
},
|
},
|
||||||
centre,
|
centre,
|
||||||
))
|
))
|
||||||
@@ -265,6 +268,7 @@ struct LevelFiles {
|
|||||||
core_data: Vec<u8>,
|
core_data: Vec<u8>,
|
||||||
core_index: Vec<u8>,
|
core_index: Vec<u8>,
|
||||||
gs_ram: Vec<u8>,
|
gs_ram: Vec<u8>,
|
||||||
|
overlay: Option<Vec<u8>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LevelFiles {
|
impl LevelFiles {
|
||||||
@@ -273,6 +277,7 @@ impl LevelFiles {
|
|||||||
core_data: &self.core_data,
|
core_data: &self.core_data,
|
||||||
core_index: &self.core_index,
|
core_index: &self.core_index,
|
||||||
gs_ram: &self.gs_ram,
|
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_POSITION, b.positions.clone());
|
||||||
mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, b.uvs.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_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
|
// 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
|
// array, which is parsed but not yet reproduced. Inventing normals would
|
||||||
// make unlit terrain look lit and hide that gap.
|
// make unlit terrain look lit and hide that gap.
|
||||||
|
|||||||
@@ -2494,6 +2494,7 @@ e1baca5f8178da95e7dc496305a36f984be542aa 15 hud/0780.bin
|
|||||||
4a8f9d6608b776a86fb15420acadea4c2b749a9f 13279232 level0/data.bin
|
4a8f9d6608b776a86fb15420acadea4c2b749a9f 13279232 level0/data.bin
|
||||||
133206d28cd25c39f1b6a760e345c0f0f409b7bf 659456 level0/gameplay.bin
|
133206d28cd25c39f1b6a760e345c0f0f409b7bf 659456 level0/gameplay.bin
|
||||||
acef5e387c8051229eecf70a64fd6daa077b34b3 518656 level0/gs_ram.bin
|
acef5e387c8051229eecf70a64fd6daa077b34b3 518656 level0/gs_ram.bin
|
||||||
|
d7045d6c1256a9ffa241acca76e3761c34da3341 2751268 level0/overlay.bin
|
||||||
7ec9566aeae513fe272bc97899a4dbe15a2a74ff 1503232 level0/sound_bank.bin
|
7ec9566aeae513fe272bc97899a4dbe15a2a74ff 1503232 level0/sound_bank.bin
|
||||||
6b1a358c6eb6039d0df51fce061b58c274e8c99c 19023392 level1/core_data.bin
|
6b1a358c6eb6039d0df51fce061b58c274e8c99c 19023392 level1/core_data.bin
|
||||||
96b33cef1ac3c9bab5283ccf11bd86b0aa20ebcb 12165629 level1/core_data.wad
|
96b33cef1ac3c9bab5283ccf11bd86b0aa20ebcb 12165629 level1/core_data.wad
|
||||||
@@ -2502,12 +2503,14 @@ acef5e387c8051229eecf70a64fd6daa077b34b3 518656 level0/gs_ram.bin
|
|||||||
a07364ccac5fb6dc3d1fb1c859fca13b205e6a1f 811008 level1/gameplay.bin
|
a07364ccac5fb6dc3d1fb1c859fca13b205e6a1f 811008 level1/gameplay.bin
|
||||||
afc39fac19d1a66aba56c4509c55977229dcb3cf 569344 level1/gs_ram.bin
|
afc39fac19d1a66aba56c4509c55977229dcb3cf 569344 level1/gs_ram.bin
|
||||||
1318041a43acca46fb4f2348b2f4fa81bd810948 18432 level1/occlusion.bin
|
1318041a43acca46fb4f2348b2f4fa81bd810948 18432 level1/occlusion.bin
|
||||||
|
54c0247eb3b0a08da2c6c87c5a23fc06bbe3fa50 2763668 level1/overlay.bin
|
||||||
5d6ead57dd2adf83e75b53ef5d026682676d5010 1300480 level1/sound_bank.bin
|
5d6ead57dd2adf83e75b53ef5d026682676d5010 1300480 level1/sound_bank.bin
|
||||||
c7bbd091e77719876e53fe38cfb66ad68d0952d3 14427312 level10/core_data.bin
|
c7bbd091e77719876e53fe38cfb66ad68d0952d3 14427312 level10/core_data.bin
|
||||||
1c56f35c21dd429f52cdc701de3d3ab846f57918 8999864 level10/core_data.wad
|
1c56f35c21dd429f52cdc701de3d3ab846f57918 8999864 level10/core_data.wad
|
||||||
2c0073d2e78ff61beb69050087ff953ac1ccc65c 19696 level10/core_index.bin
|
2c0073d2e78ff61beb69050087ff953ac1ccc65c 19696 level10/core_index.bin
|
||||||
fe9e5169820bc9272aa5fcfbbabfc74e7b12d1ce 12498944 level10/data.bin
|
fe9e5169820bc9272aa5fcfbbabfc74e7b12d1ce 12498944 level10/data.bin
|
||||||
4036bc105fa429b97a725ad40bd4051168c886ad 367872 level10/gs_ram.bin
|
4036bc105fa429b97a725ad40bd4051168c886ad 367872 level10/gs_ram.bin
|
||||||
|
76fa348a8764cbe97c8caf5a892105007c2bf4a2 2870708 level10/overlay.bin
|
||||||
d3e48237b03649578f6962e36f1bc197c3a8af9f 1306624 level10/sound_bank.bin
|
d3e48237b03649578f6962e36f1bc197c3a8af9f 1306624 level10/sound_bank.bin
|
||||||
da253d8b4dc4f8fc482880c54cd832c53bfd4aa0 20717584 level11/core_data.bin
|
da253d8b4dc4f8fc482880c54cd832c53bfd4aa0 20717584 level11/core_data.bin
|
||||||
a0173c7577180d32e9bfed67464eecf3bc12c033 11414905 level11/core_data.wad
|
a0173c7577180d32e9bfed67464eecf3bc12c033 11414905 level11/core_data.wad
|
||||||
@@ -2516,6 +2519,7 @@ a0173c7577180d32e9bfed67464eecf3bc12c033 11414905 level11/core_data.wad
|
|||||||
ff6811c08f3fe034bae54ab066e3051d4cbb39cc 1034240 level11/gameplay.bin
|
ff6811c08f3fe034bae54ab066e3051d4cbb39cc 1034240 level11/gameplay.bin
|
||||||
472e916feae6b88e5028e48e9d2e4d1a433a8ed7 538624 level11/gs_ram.bin
|
472e916feae6b88e5028e48e9d2e4d1a433a8ed7 538624 level11/gs_ram.bin
|
||||||
e91c93e8af747ce8c0802c3c96cd88acc126a5c3 51200 level11/occlusion.bin
|
e91c93e8af747ce8c0802c3c96cd88acc126a5c3 51200 level11/occlusion.bin
|
||||||
|
4502a46e00bbbb3a0a59aa4d5899fd3b3ca44948 3151128 level11/overlay.bin
|
||||||
850f01ef9678ed0b0b6242c410ad7e57b935079f 991232 level11/sound_bank.bin
|
850f01ef9678ed0b0b6242c410ad7e57b935079f 991232 level11/sound_bank.bin
|
||||||
d13c8eb4f2ab552e330a119bf1e14d55f9921220 20995456 level12/core_data.bin
|
d13c8eb4f2ab552e330a119bf1e14d55f9921220 20995456 level12/core_data.bin
|
||||||
9d8355548c0d5c911e18124e83a04f3c58d604de 11510373 level12/core_data.wad
|
9d8355548c0d5c911e18124e83a04f3c58d604de 11510373 level12/core_data.wad
|
||||||
@@ -2523,6 +2527,7 @@ d13c8eb4f2ab552e330a119bf1e14d55f9921220 20995456 level12/core_data.bin
|
|||||||
a134bad90883ad15ff7b52d909da461a491d253b 15136768 level12/data.bin
|
a134bad90883ad15ff7b52d909da461a491d253b 15136768 level12/data.bin
|
||||||
b058a1f7858198be55a90bf70d81db1f8039e132 813056 level12/gameplay.bin
|
b058a1f7858198be55a90bf70d81db1f8039e132 813056 level12/gameplay.bin
|
||||||
cbccba141d2efe7b4165dd81a265532465106def 510464 level12/gs_ram.bin
|
cbccba141d2efe7b4165dd81a265532465106def 510464 level12/gs_ram.bin
|
||||||
|
c47b9490cf2a0145331d2b2229234fd1e1b2e172 2843328 level12/overlay.bin
|
||||||
709e185927a11b846e6b1d66c2fa770cf284ea65 1568768 level12/sound_bank.bin
|
709e185927a11b846e6b1d66c2fa770cf284ea65 1568768 level12/sound_bank.bin
|
||||||
74fd46bfcfcb97132c509d2147070327976faab4 21301168 level13/core_data.bin
|
74fd46bfcfcb97132c509d2147070327976faab4 21301168 level13/core_data.bin
|
||||||
7c4b8c7fd086de66e19bd440a5e63fea4729e0bb 12037495 level13/core_data.wad
|
7c4b8c7fd086de66e19bd440a5e63fea4729e0bb 12037495 level13/core_data.wad
|
||||||
@@ -2530,6 +2535,7 @@ c7a99c3252427f11a202a907df605b3d43baaae0 35744 level13/core_index.bin
|
|||||||
29c09e0877c62ba4dee1325cf00c944f9ad78ceb 15702016 level13/data.bin
|
29c09e0877c62ba4dee1325cf00c944f9ad78ceb 15702016 level13/data.bin
|
||||||
ab5e7bd921f3973e3d15052838ec87676d35928a 854016 level13/gameplay.bin
|
ab5e7bd921f3973e3d15052838ec87676d35928a 854016 level13/gameplay.bin
|
||||||
0554fcecd5cbe57f3b90a97bd75edba4ea438752 555520 level13/gs_ram.bin
|
0554fcecd5cbe57f3b90a97bd75edba4ea438752 555520 level13/gs_ram.bin
|
||||||
|
3fbaf06d37e5ffc64dd7fa3f37e3fe20d3ea8037 2833444 level13/overlay.bin
|
||||||
2793ed3153f9f8ace889b855dbfc53f29c94c0bc 1406976 level13/sound_bank.bin
|
2793ed3153f9f8ace889b855dbfc53f29c94c0bc 1406976 level13/sound_bank.bin
|
||||||
386e9a7cef58f23f66fbc21de42be234c25be0b4 19738512 level14/core_data.bin
|
386e9a7cef58f23f66fbc21de42be234c25be0b4 19738512 level14/core_data.bin
|
||||||
b34bf0528e2c753ce9fd65791e89ba6ec8491358 11217244 level14/core_data.wad
|
b34bf0528e2c753ce9fd65791e89ba6ec8491358 11217244 level14/core_data.wad
|
||||||
@@ -2537,6 +2543,7 @@ cc03cc08cc55f27d57bcdc6c9c435e28479a65e3 34848 level14/core_index.bin
|
|||||||
fb30c8c10944377af4a76e2804209be630dba993 14948352 level14/data.bin
|
fb30c8c10944377af4a76e2804209be630dba993 14948352 level14/data.bin
|
||||||
95157d87f0c5e76a38daead9e07d01281b65ebf0 692224 level14/gameplay.bin
|
95157d87f0c5e76a38daead9e07d01281b65ebf0 692224 level14/gameplay.bin
|
||||||
0d8c49b2b405df331f859a077a6dfbb6aa922b3d 582656 level14/gs_ram.bin
|
0d8c49b2b405df331f859a077a6dfbb6aa922b3d 582656 level14/gs_ram.bin
|
||||||
|
bdcd73fb41d07da3ce35f60a2fd48af3d8ffe96b 2874964 level14/overlay.bin
|
||||||
28a70e7880794f94a76e2b340373d1dc40484fee 1689600 level14/sound_bank.bin
|
28a70e7880794f94a76e2b340373d1dc40484fee 1689600 level14/sound_bank.bin
|
||||||
9e1a9269a59741f15608ee773192a4c4e3adadbb 14566720 level15/core_data.bin
|
9e1a9269a59741f15608ee773192a4c4e3adadbb 14566720 level15/core_data.bin
|
||||||
3c7f4291ff09cdfb0c1cd2e435fb4c039faf7572 8538458 level15/core_data.wad
|
3c7f4291ff09cdfb0c1cd2e435fb4c039faf7572 8538458 level15/core_data.wad
|
||||||
@@ -2544,6 +2551,7 @@ cda9e44cee8b5ac58f9abd97dfcdbbe14e76d490 21792 level15/core_index.bin
|
|||||||
39e2c8e15375dae4c73f61a7be980dccf4a85aa9 12122112 level15/data.bin
|
39e2c8e15375dae4c73f61a7be980dccf4a85aa9 12122112 level15/data.bin
|
||||||
9b13d67b1276d25937cab21344fa8a6687cc0882 491520 level15/gameplay.bin
|
9b13d67b1276d25937cab21344fa8a6687cc0882 491520 level15/gameplay.bin
|
||||||
d1437c7d2106f6e9cdb54f66cea4f1a714dc8867 407552 level15/gs_ram.bin
|
d1437c7d2106f6e9cdb54f66cea4f1a714dc8867 407552 level15/gs_ram.bin
|
||||||
|
76b9c880945039ece6e51762b8dd76ffc680da7c 2915968 level15/overlay.bin
|
||||||
f89c334b887f377f341fc9232d474affa89fc544 1476608 level15/sound_bank.bin
|
f89c334b887f377f341fc9232d474affa89fc544 1476608 level15/sound_bank.bin
|
||||||
49c2efb647efc0baaf919aaaa140f0a660725b3d 21374192 level16/core_data.bin
|
49c2efb647efc0baaf919aaaa140f0a660725b3d 21374192 level16/core_data.bin
|
||||||
9c9291257d21ac62dff3ac4090f9d2f9b6c5c3e8 12442996 level16/core_data.wad
|
9c9291257d21ac62dff3ac4090f9d2f9b6c5c3e8 12442996 level16/core_data.wad
|
||||||
@@ -2551,6 +2559,7 @@ f89c334b887f377f341fc9232d474affa89fc544 1476608 level15/sound_bank.bin
|
|||||||
8e58f79a35fbe78382065bdce0f53d2cc9f1ccf1 16091136 level16/data.bin
|
8e58f79a35fbe78382065bdce0f53d2cc9f1ccf1 16091136 level16/data.bin
|
||||||
af60ee961ab3c4508f6fc7f7e5db32d1bbe08769 595968 level16/gameplay.bin
|
af60ee961ab3c4508f6fc7f7e5db32d1bbe08769 595968 level16/gameplay.bin
|
||||||
9b85cc208f66ffbbdff60d979597e1e2c47bc593 577024 level16/gs_ram.bin
|
9b85cc208f66ffbbdff60d979597e1e2c47bc593 577024 level16/gs_ram.bin
|
||||||
|
2bdb86a7a4c69af80eb5f30d62ed58f337961f3f 2798992 level16/overlay.bin
|
||||||
84723c80beedd7e55143a424a1b2f6255a200ab0 1654784 level16/sound_bank.bin
|
84723c80beedd7e55143a424a1b2f6255a200ab0 1654784 level16/sound_bank.bin
|
||||||
485e5343a6f146179b319fb7d76388eee6e21276 20920320 level17/core_data.bin
|
485e5343a6f146179b319fb7d76388eee6e21276 20920320 level17/core_data.bin
|
||||||
fc90583b7560be17115f57c8ecf6fc14ec2e59d8 10922651 level17/core_data.wad
|
fc90583b7560be17115f57c8ecf6fc14ec2e59d8 10922651 level17/core_data.wad
|
||||||
@@ -2558,6 +2567,7 @@ fc90583b7560be17115f57c8ecf6fc14ec2e59d8 10922651 level17/core_data.wad
|
|||||||
d7669eec9eb3cf36bf4f649333ae79864c1a7043 14575616 level17/data.bin
|
d7669eec9eb3cf36bf4f649333ae79864c1a7043 14575616 level17/data.bin
|
||||||
10a5f5e2862f7364a9bbe886dadb9ebca8ab0381 796672 level17/gameplay.bin
|
10a5f5e2862f7364a9bbe886dadb9ebca8ab0381 796672 level17/gameplay.bin
|
||||||
899d4e61f032af4c66419d3a286a25d1e97e5644 574464 level17/gs_ram.bin
|
899d4e61f032af4c66419d3a286a25d1e97e5644 574464 level17/gs_ram.bin
|
||||||
|
f76912e7914400cc5ceeca59f6780bc5143997c8 2803324 level17/overlay.bin
|
||||||
37c76a37d9e3cbbed6dfc324b0b90d3498ecb0e7 1347584 level17/sound_bank.bin
|
37c76a37d9e3cbbed6dfc324b0b90d3498ecb0e7 1347584 level17/sound_bank.bin
|
||||||
f9240e2bd6406f206e155b09cc5c914b6574e41e 20268640 level18/core_data.bin
|
f9240e2bd6406f206e155b09cc5c914b6574e41e 20268640 level18/core_data.bin
|
||||||
f44add8374224a84070995ecb4b1be8cfb58aa9f 11721139 level18/core_data.wad
|
f44add8374224a84070995ecb4b1be8cfb58aa9f 11721139 level18/core_data.wad
|
||||||
@@ -2565,6 +2575,7 @@ f44add8374224a84070995ecb4b1be8cfb58aa9f 11721139 level18/core_data.wad
|
|||||||
d0f931677cb6589a564937c3d15c13344b923969 15501312 level18/data.bin
|
d0f931677cb6589a564937c3d15c13344b923969 15501312 level18/data.bin
|
||||||
2adc53f721fca95dd1012994d243e0704edd034c 675840 level18/gameplay.bin
|
2adc53f721fca95dd1012994d243e0704edd034c 675840 level18/gameplay.bin
|
||||||
7f6a823c4b47fdfb58e29237b680d03e946329db 571136 level18/gs_ram.bin
|
7f6a823c4b47fdfb58e29237b680d03e946329db 571136 level18/gs_ram.bin
|
||||||
|
e5370c97b9a2ccdb00678e57f9e18042eab6735d 2937576 level18/overlay.bin
|
||||||
b26483cc29ecb8b6d2c71919ff7cd56b09522bc1 1492992 level18/sound_bank.bin
|
b26483cc29ecb8b6d2c71919ff7cd56b09522bc1 1492992 level18/sound_bank.bin
|
||||||
0918d824b72b892a24f46108bbc60f6e875236f6 20233504 level19/core_data.bin
|
0918d824b72b892a24f46108bbc60f6e875236f6 20233504 level19/core_data.bin
|
||||||
b79df8e074db7d3c75bc2b3b9c2a4de61e5dd9cd 11797811 level19/core_data.wad
|
b79df8e074db7d3c75bc2b3b9c2a4de61e5dd9cd 11797811 level19/core_data.wad
|
||||||
@@ -2573,6 +2584,7 @@ b0ea377a39a616dbd2b15224a9996925c7b28f4d 31536 level19/core_index.bin
|
|||||||
8700a93086450328f2693b553e1df447a79fcd83 825344 level19/gameplay.bin
|
8700a93086450328f2693b553e1df447a79fcd83 825344 level19/gameplay.bin
|
||||||
d5061a551eb354ddf7952c4bfd62767c6e074c3f 497920 level19/gs_ram.bin
|
d5061a551eb354ddf7952c4bfd62767c6e074c3f 497920 level19/gs_ram.bin
|
||||||
877ca7fb8e80d810d6c37fb993795aa10b5b1033 47104 level19/occlusion.bin
|
877ca7fb8e80d810d6c37fb993795aa10b5b1033 47104 level19/occlusion.bin
|
||||||
|
428ea51a157b35cdf831f2a6908647d8ffe6a91b 2821856 level19/overlay.bin
|
||||||
8298f799c394542e912e41ff3321ee55c70e0c8f 1570816 level19/sound_bank.bin
|
8298f799c394542e912e41ff3321ee55c70e0c8f 1570816 level19/sound_bank.bin
|
||||||
6d56189ad74f04d366116b736ab12888154de833 20209408 level2/core_data.bin
|
6d56189ad74f04d366116b736ab12888154de833 20209408 level2/core_data.bin
|
||||||
f0eb5c033e84092748cf3457818b0d4c85da7a0a 11372886 level2/core_data.wad
|
f0eb5c033e84092748cf3457818b0d4c85da7a0a 11372886 level2/core_data.wad
|
||||||
@@ -2581,6 +2593,7 @@ f0eb5c033e84092748cf3457818b0d4c85da7a0a 11372886 level2/core_data.wad
|
|||||||
bfc9f6bc73edfa7deb46505f187c274dcf7b863a 790528 level2/gameplay.bin
|
bfc9f6bc73edfa7deb46505f187c274dcf7b863a 790528 level2/gameplay.bin
|
||||||
164f775e33d9325b3e4c5ce4cbf8c515ed311fd2 513024 level2/gs_ram.bin
|
164f775e33d9325b3e4c5ce4cbf8c515ed311fd2 513024 level2/gs_ram.bin
|
||||||
01c7d4c7b0ecb44a91694ffde796ff941057191a 28672 level2/occlusion.bin
|
01c7d4c7b0ecb44a91694ffde796ff941057191a 28672 level2/occlusion.bin
|
||||||
|
d7182afdf4a986c9c781a9658373281bfc37df22 2928424 level2/overlay.bin
|
||||||
bce56b3b899d72c5d89f612bd70447f066c10757 1118208 level2/sound_bank.bin
|
bce56b3b899d72c5d89f612bd70447f066c10757 1118208 level2/sound_bank.bin
|
||||||
daf162b58756e2661f3ec891c8736a1611b4dc52 19637472 level20/core_data.bin
|
daf162b58756e2661f3ec891c8736a1611b4dc52 19637472 level20/core_data.bin
|
||||||
7e7203ac3a2dbf578546573a34fa6dce4c336d35 10449039 level20/core_data.wad
|
7e7203ac3a2dbf578546573a34fa6dce4c336d35 10449039 level20/core_data.wad
|
||||||
@@ -2589,6 +2602,7 @@ b12e8599d211addc85cbe138e58157ade380a575 37360 level20/core_index.bin
|
|||||||
7368af71bc6c070fc6422fd69a354b2f3518ed2b 833536 level20/gameplay.bin
|
7368af71bc6c070fc6422fd69a354b2f3518ed2b 833536 level20/gameplay.bin
|
||||||
4c7325cfdde68fc9160ec9a2cd18da3d327dcd9a 534272 level20/gs_ram.bin
|
4c7325cfdde68fc9160ec9a2cd18da3d327dcd9a 534272 level20/gs_ram.bin
|
||||||
61ae37031d1df746bffb4a0bddeaee85558c894e 36864 level20/occlusion.bin
|
61ae37031d1df746bffb4a0bddeaee85558c894e 36864 level20/occlusion.bin
|
||||||
|
a5235e8ce98aa3cfbb499ae988c419a05a0bcf55 2976888 level20/overlay.bin
|
||||||
abce77e8841c0ed0fa006db81840b35ea4c75afd 1335296 level20/sound_bank.bin
|
abce77e8841c0ed0fa006db81840b35ea4c75afd 1335296 level20/sound_bank.bin
|
||||||
5e850ab42d2dc3e8fa1f9ae74d401be1658c9ba7 14483120 level21/core_data.bin
|
5e850ab42d2dc3e8fa1f9ae74d401be1658c9ba7 14483120 level21/core_data.bin
|
||||||
f10321014e7ccd61a65f294baefaf35875d6294b 8178784 level21/core_data.wad
|
f10321014e7ccd61a65f294baefaf35875d6294b 8178784 level21/core_data.wad
|
||||||
@@ -2596,6 +2610,7 @@ f10321014e7ccd61a65f294baefaf35875d6294b 8178784 level21/core_data.wad
|
|||||||
4a3ebd058fe62df17ed4d74fe190442ff2993474 11722752 level21/data.bin
|
4a3ebd058fe62df17ed4d74fe190442ff2993474 11722752 level21/data.bin
|
||||||
6c2b9faaef23c142a61316190852251404734c39 448512 level21/gameplay.bin
|
6c2b9faaef23c142a61316190852251404734c39 448512 level21/gameplay.bin
|
||||||
43a953d770cd4c5833eda5c1ecae26d7ce127be1 512256 level21/gs_ram.bin
|
43a953d770cd4c5833eda5c1ecae26d7ce127be1 512256 level21/gs_ram.bin
|
||||||
|
384f39176ccbd107924eacc05d6330dda4be523f 2766612 level21/overlay.bin
|
||||||
8fbcb2360d5244e92bfd7c2ccd453eec44cd042b 1167360 level21/sound_bank.bin
|
8fbcb2360d5244e92bfd7c2ccd453eec44cd042b 1167360 level21/sound_bank.bin
|
||||||
7b0733a07f28e7d7cf5d8a19b1058a98a5563c9f 14288048 level22/core_data.bin
|
7b0733a07f28e7d7cf5d8a19b1058a98a5563c9f 14288048 level22/core_data.bin
|
||||||
34a2d1ac57f13cacdf46cd7d73f356951f87dd43 8186976 level22/core_data.wad
|
34a2d1ac57f13cacdf46cd7d73f356951f87dd43 8186976 level22/core_data.wad
|
||||||
@@ -2603,6 +2618,7 @@ edb2fd7c72d0a517613fd813429aa9036e2eb509 25152 level22/core_index.bin
|
|||||||
3519cac29e6371802b40e3d8234ef7064abee336 11683840 level22/data.bin
|
3519cac29e6371802b40e3d8234ef7064abee336 11683840 level22/data.bin
|
||||||
a8760bb193082c5e85faeb5f799ab8585f366805 557056 level22/gameplay.bin
|
a8760bb193082c5e85faeb5f799ab8585f366805 557056 level22/gameplay.bin
|
||||||
9c3af51d0b3ad2b786a428a49a8ee9d30886d636 482048 level22/gs_ram.bin
|
9c3af51d0b3ad2b786a428a49a8ee9d30886d636 482048 level22/gs_ram.bin
|
||||||
|
ae40d45a6605d5929f54e63b531919cb6ab093a9 2753704 level22/overlay.bin
|
||||||
dba3b2b5856efbd28a8aeaa15fbee6f4fb479895 1363968 level22/sound_bank.bin
|
dba3b2b5856efbd28a8aeaa15fbee6f4fb479895 1363968 level22/sound_bank.bin
|
||||||
c6b6f4087f4c9cd374a587b13777f9575953838a 12647664 level23/core_data.bin
|
c6b6f4087f4c9cd374a587b13777f9575953838a 12647664 level23/core_data.bin
|
||||||
dbdca886def506bd2e6fd189c0fc4ca8d2834b97 6900832 level23/core_data.wad
|
dbdca886def506bd2e6fd189c0fc4ca8d2834b97 6900832 level23/core_data.wad
|
||||||
@@ -2610,18 +2626,21 @@ ed196a1541e056da4433d366c1a096143ea87ca0 23024 level23/core_index.bin
|
|||||||
9a26194ba7d5930279a62113b1377ffeb1294269 10414080 level23/data.bin
|
9a26194ba7d5930279a62113b1377ffeb1294269 10414080 level23/data.bin
|
||||||
336b3c864d1aa317003499e7d1d2d5a8cee13d58 505856 level23/gameplay.bin
|
336b3c864d1aa317003499e7d1d2d5a8cee13d58 505856 level23/gameplay.bin
|
||||||
bcc4a8508f14af07ee85dd5f06dff7a910d73b6a 411136 level23/gs_ram.bin
|
bcc4a8508f14af07ee85dd5f06dff7a910d73b6a 411136 level23/gs_ram.bin
|
||||||
|
38f7544efa497b26e494e31169fa057ce2a1b62c 2841396 level23/overlay.bin
|
||||||
e254322f1d87d2650c21ec2c118546df1a56d9e8 1675264 level23/sound_bank.bin
|
e254322f1d87d2650c21ec2c118546df1a56d9e8 1675264 level23/sound_bank.bin
|
||||||
508808939296de54fee8928824bdcc6d37204d01 6386608 level24/core_data.bin
|
508808939296de54fee8928824bdcc6d37204d01 6386608 level24/core_data.bin
|
||||||
c44701fc0a229dc8071a30594231d5edfb6b4119 3805312 level24/core_data.wad
|
c44701fc0a229dc8071a30594231d5edfb6b4119 3805312 level24/core_data.wad
|
||||||
c526f84012360c24a07e4bbd6823dd18ce8b3181 16608 level24/core_index.bin
|
c526f84012360c24a07e4bbd6823dd18ce8b3181 16608 level24/core_index.bin
|
||||||
f38883faa65e74f67a44da4a55b9b476848d7cae 6981632 level24/data.bin
|
f38883faa65e74f67a44da4a55b9b476848d7cae 6981632 level24/data.bin
|
||||||
adf90875335b529183111edaeaa90138ba111dbe 308480 level24/gs_ram.bin
|
adf90875335b529183111edaeaa90138ba111dbe 308480 level24/gs_ram.bin
|
||||||
|
2b5f97e7aa6a6bf01e5ed2c918ca19b7eafb9593 2615776 level24/overlay.bin
|
||||||
3bf88b1d1c455ab90bbacc3270a3f54c94b900ef 937984 level24/sound_bank.bin
|
3bf88b1d1c455ab90bbacc3270a3f54c94b900ef 937984 level24/sound_bank.bin
|
||||||
c81ea8c485748fccec6619bacc75afe8ce2f7e20 5898736 level25/core_data.bin
|
c81ea8c485748fccec6619bacc75afe8ce2f7e20 5898736 level25/core_data.bin
|
||||||
80d763434b9dda1224c446de67e4907cfbdcfbe7 3513706 level25/core_data.wad
|
80d763434b9dda1224c446de67e4907cfbdcfbe7 3513706 level25/core_data.wad
|
||||||
c493407fb493ff359e90d1bdfc1b379a2a6c540a 16592 level25/core_index.bin
|
c493407fb493ff359e90d1bdfc1b379a2a6c540a 16592 level25/core_index.bin
|
||||||
e57a615ccce4afd0a3de13f638ecaa19f88d0789 6852608 level25/data.bin
|
e57a615ccce4afd0a3de13f638ecaa19f88d0789 6852608 level25/data.bin
|
||||||
b54226627562788f722c2fdc314d2117ca6ed592 287488 level25/gs_ram.bin
|
b54226627562788f722c2fdc314d2117ca6ed592 287488 level25/gs_ram.bin
|
||||||
|
eee845138d0c88116743bb36929a6dc894b62e14 2801992 level25/overlay.bin
|
||||||
e9d246c13a7198b42a71f081d767b9e9598d01a1 1370112 level25/sound_bank.bin
|
e9d246c13a7198b42a71f081d767b9e9598d01a1 1370112 level25/sound_bank.bin
|
||||||
948974c4fd10e7ca7ad1a362af9cb46aa6c81773 9530880 level26/core_data.bin
|
948974c4fd10e7ca7ad1a362af9cb46aa6c81773 9530880 level26/core_data.bin
|
||||||
6140efd9a7ebbcd81bfddf1ce0a5395ffb136d2e 5894651 level26/core_data.wad
|
6140efd9a7ebbcd81bfddf1ce0a5395ffb136d2e 5894651 level26/core_data.wad
|
||||||
@@ -2629,6 +2648,7 @@ d71c13701f26cd0a4bbcb9508fb0387ba54d2d62 20704 level26/core_index.bin
|
|||||||
4fdd7d88a5d3db63418c968f8a5f53ac370b86be 9224192 level26/data.bin
|
4fdd7d88a5d3db63418c968f8a5f53ac370b86be 9224192 level26/data.bin
|
||||||
0d63ad0bc2edcd958b1c7dbec303c256ed8850e1 679936 level26/gameplay.bin
|
0d63ad0bc2edcd958b1c7dbec303c256ed8850e1 679936 level26/gameplay.bin
|
||||||
abb3fb6a4175cf6b41eff1d7178d59ce7affb84f 378880 level26/gs_ram.bin
|
abb3fb6a4175cf6b41eff1d7178d59ce7affb84f 378880 level26/gs_ram.bin
|
||||||
|
518bb5c68f2d028c7c5934f878abbd3ba9d6c63f 2695436 level26/overlay.bin
|
||||||
8f6a74b8008a9ebc9e92853d0b339a8e725c9992 1073152 level26/sound_bank.bin
|
8f6a74b8008a9ebc9e92853d0b339a8e725c9992 1073152 level26/sound_bank.bin
|
||||||
f5d3cd4480fe6a52a019d04401a360018b279b81 21148480 level3/core_data.bin
|
f5d3cd4480fe6a52a019d04401a360018b279b81 21148480 level3/core_data.bin
|
||||||
690209848c64647194548d5e81f329d471392a8e 11383592 level3/core_data.wad
|
690209848c64647194548d5e81f329d471392a8e 11383592 level3/core_data.wad
|
||||||
@@ -2636,6 +2656,7 @@ f5d3cd4480fe6a52a019d04401a360018b279b81 21148480 level3/core_data.bin
|
|||||||
eb8366046b5643cc54d42d800c06bf66ccd4ef36 15157248 level3/data.bin
|
eb8366046b5643cc54d42d800c06bf66ccd4ef36 15157248 level3/data.bin
|
||||||
55cc4acecac6ba19232e6505fdc06a22b0633a45 749568 level3/gameplay.bin
|
55cc4acecac6ba19232e6505fdc06a22b0633a45 749568 level3/gameplay.bin
|
||||||
ed1f77cd35e2572adc51f4f43de030f31b8c6ed9 675840 level3/gs_ram.bin
|
ed1f77cd35e2572adc51f4f43de030f31b8c6ed9 675840 level3/gs_ram.bin
|
||||||
|
6fb6a7e7a64afd2f1e897f23640866f28154aa1d 2820776 level3/overlay.bin
|
||||||
d986825dfc2bed713eb9699504536346658aaa3f 1705984 level3/sound_bank.bin
|
d986825dfc2bed713eb9699504536346658aaa3f 1705984 level3/sound_bank.bin
|
||||||
f8c0aa3778b5bcb1995e516045b28ca6ac35502f 21107088 level4/core_data.bin
|
f8c0aa3778b5bcb1995e516045b28ca6ac35502f 21107088 level4/core_data.bin
|
||||||
ec75f324670188af581c4fceaa4cb285353f95b8 12171908 level4/core_data.wad
|
ec75f324670188af581c4fceaa4cb285353f95b8 12171908 level4/core_data.wad
|
||||||
@@ -2644,6 +2665,7 @@ cc4997acbdc0a6ac85d427227c04e6af6e34cd46 38080 level4/core_index.bin
|
|||||||
eeee7ebf8d9980f9afc6d9c2bc2156bcca015ddd 960512 level4/gameplay.bin
|
eeee7ebf8d9980f9afc6d9c2bc2156bcca015ddd 960512 level4/gameplay.bin
|
||||||
5a9a603e46e2008df02e60dce2c66ae9368d9993 522496 level4/gs_ram.bin
|
5a9a603e46e2008df02e60dce2c66ae9368d9993 522496 level4/gs_ram.bin
|
||||||
fed269c2d706a2ba11cab965e090fd41d434055c 49152 level4/occlusion.bin
|
fed269c2d706a2ba11cab965e090fd41d434055c 49152 level4/occlusion.bin
|
||||||
|
6941b8cb87a7ff14f53b2a2511bfa2d43f25ba47 3027588 level4/overlay.bin
|
||||||
bdd446767b1a84043d65ff968278cd2d5808705a 1236992 level4/sound_bank.bin
|
bdd446767b1a84043d65ff968278cd2d5808705a 1236992 level4/sound_bank.bin
|
||||||
cb07a102a92b887d06d9314c07ce1db50e14c1fb 16715440 level5/core_data.bin
|
cb07a102a92b887d06d9314c07ce1db50e14c1fb 16715440 level5/core_data.bin
|
||||||
e5cc08e39cb6883b391605bdcff8ba3bb8423699 11205994 level5/core_data.wad
|
e5cc08e39cb6883b391605bdcff8ba3bb8423699 11205994 level5/core_data.wad
|
||||||
@@ -2651,6 +2673,7 @@ d41e06bc7086ed6876bef602420cca8d52492b1e 23536 level5/core_index.bin
|
|||||||
b241ea9d8fe18dd5bdbc8a4255ba8450a66b3c6a 14772224 level5/data.bin
|
b241ea9d8fe18dd5bdbc8a4255ba8450a66b3c6a 14772224 level5/data.bin
|
||||||
c71539e0f8b0e2d666d9382475539c7a590fee04 514048 level5/gameplay.bin
|
c71539e0f8b0e2d666d9382475539c7a590fee04 514048 level5/gameplay.bin
|
||||||
ad4318ac2d601a2b1b0b9b444325082e0fc50ffc 434176 level5/gs_ram.bin
|
ad4318ac2d601a2b1b0b9b444325082e0fc50ffc 434176 level5/gs_ram.bin
|
||||||
|
c5f6b22d88d8b9eab711872337fc358bcd4051a8 2863676 level5/overlay.bin
|
||||||
5c2fd4470dd8c892af7686770af8cf3e2396373c 1390592 level5/sound_bank.bin
|
5c2fd4470dd8c892af7686770af8cf3e2396373c 1390592 level5/sound_bank.bin
|
||||||
8ea18c5a51e4ae07ca56d92d65ffde60f4bcce12 20935696 level6/core_data.bin
|
8ea18c5a51e4ae07ca56d92d65ffde60f4bcce12 20935696 level6/core_data.bin
|
||||||
f2962a73cfb15ab1bef23253d863d2433b264383 12810673 level6/core_data.wad
|
f2962a73cfb15ab1bef23253d863d2433b264383 12810673 level6/core_data.wad
|
||||||
@@ -2658,6 +2681,7 @@ f2962a73cfb15ab1bef23253d863d2433b264383 12810673 level6/core_data.wad
|
|||||||
ccb0a4e8c51be81fbcd8ad325ab8d521bbdb7604 16777216 level6/data.bin
|
ccb0a4e8c51be81fbcd8ad325ab8d521bbdb7604 16777216 level6/data.bin
|
||||||
b9ff3b4b8fb6308aac7f5ddba1c4b1099d484dd5 929792 level6/gameplay.bin
|
b9ff3b4b8fb6308aac7f5ddba1c4b1099d484dd5 929792 level6/gameplay.bin
|
||||||
c404dfa08cab9603e5deeeed9778e4d29b755d88 604160 level6/gs_ram.bin
|
c404dfa08cab9603e5deeeed9778e4d29b755d88 604160 level6/gs_ram.bin
|
||||||
|
4d981fd750196894f96f0a24437bc76210a1663a 3086372 level6/overlay.bin
|
||||||
82fcce964396eb508768933d092a557bc1b05c89 1669120 level6/sound_bank.bin
|
82fcce964396eb508768933d092a557bc1b05c89 1669120 level6/sound_bank.bin
|
||||||
6692290d78706a2b92cba541264ddea9a764e2f2 18209392 level7/core_data.bin
|
6692290d78706a2b92cba541264ddea9a764e2f2 18209392 level7/core_data.bin
|
||||||
f1cae801f1050c57258f93a7035148b3094ece03 10780010 level7/core_data.wad
|
f1cae801f1050c57258f93a7035148b3094ece03 10780010 level7/core_data.wad
|
||||||
@@ -2666,6 +2690,7 @@ f1cae801f1050c57258f93a7035148b3094ece03 10780010 level7/core_data.wad
|
|||||||
bc8f542a56d4795c77774201903e25b01962c039 636928 level7/gameplay.bin
|
bc8f542a56d4795c77774201903e25b01962c039 636928 level7/gameplay.bin
|
||||||
9671dd66d0a20ef778bbd76f0e8517792646467c 580352 level7/gs_ram.bin
|
9671dd66d0a20ef778bbd76f0e8517792646467c 580352 level7/gs_ram.bin
|
||||||
e02d1b0d2339d72ed5d07abfe105f41e2288f59f 16384 level7/occlusion.bin
|
e02d1b0d2339d72ed5d07abfe105f41e2288f59f 16384 level7/occlusion.bin
|
||||||
|
82f26617c99f502324fb556006f8f859fda1b5f1 2733840 level7/overlay.bin
|
||||||
8e07c803b709100023c3b44c8842eec9b8f49898 1556480 level7/sound_bank.bin
|
8e07c803b709100023c3b44c8842eec9b8f49898 1556480 level7/sound_bank.bin
|
||||||
264a306c3a83b9137d9753b282e9c773cd386713 21086224 level8/core_data.bin
|
264a306c3a83b9137d9753b282e9c773cd386713 21086224 level8/core_data.bin
|
||||||
0a96094edd63dc4dd940894b007f85b21874cfeb 9700660 level8/core_data.wad
|
0a96094edd63dc4dd940894b007f85b21874cfeb 9700660 level8/core_data.wad
|
||||||
@@ -2674,6 +2699,7 @@ ee6f0ca2c15e1e0828d63547a8c19d2c45b8c0e2 13375488 level8/data.bin
|
|||||||
ff4c7dd1924165ddc7dbb9494a166eb3e140c95c 899072 level8/gameplay.bin
|
ff4c7dd1924165ddc7dbb9494a166eb3e140c95c 899072 level8/gameplay.bin
|
||||||
2d0d9a2d6092d0172bf50a3716d03b3631580e8b 541184 level8/gs_ram.bin
|
2d0d9a2d6092d0172bf50a3716d03b3631580e8b 541184 level8/gs_ram.bin
|
||||||
afeb717c151befdabfcf6297b0dbb1772ac13fdb 22528 level8/occlusion.bin
|
afeb717c151befdabfcf6297b0dbb1772ac13fdb 22528 level8/occlusion.bin
|
||||||
|
3dc31bcd468b9e2eae2325a64d8da466f4de228b 2865764 level8/overlay.bin
|
||||||
1f65f6b44e4e4a146aaac3d63b7f459657de0010 1593344 level8/sound_bank.bin
|
1f65f6b44e4e4a146aaac3d63b7f459657de0010 1593344 level8/sound_bank.bin
|
||||||
fd39207fd776cb3ba6b8d5f6d3b10d61610c5e49 21592016 level9/core_data.bin
|
fd39207fd776cb3ba6b8d5f6d3b10d61610c5e49 21592016 level9/core_data.bin
|
||||||
314f79d0a1d9ae630d67da9e3259a658c3c5642b 12578171 level9/core_data.wad
|
314f79d0a1d9ae630d67da9e3259a658c3c5642b 12578171 level9/core_data.wad
|
||||||
@@ -2681,6 +2707,7 @@ ffed974844aa0bef984bc33c86ff6757e0f9c5bc 34976 level9/core_index.bin
|
|||||||
db530147f5d900e5a85294070a0a5e707c086213 16232448 level9/data.bin
|
db530147f5d900e5a85294070a0a5e707c086213 16232448 level9/data.bin
|
||||||
3839c295b0408460f50032ff166cac27c0bdf411 864256 level9/gameplay.bin
|
3839c295b0408460f50032ff166cac27c0bdf411 864256 level9/gameplay.bin
|
||||||
e117d8deddac4a05f917a218e7583859397ad4d9 568320 level9/gs_ram.bin
|
e117d8deddac4a05f917a218e7583859397ad4d9 568320 level9/gs_ram.bin
|
||||||
|
6756cf309ec79cc8de37a266f4c9a6d098a96ccf 2811948 level9/overlay.bin
|
||||||
bc38b5983d8717ff4c7a35fac3cd1042ef7bd675 1697792 level9/sound_bank.bin
|
bc38b5983d8717ff4c7a35fac3cd1042ef7bd675 1697792 level9/sound_bank.bin
|
||||||
f666c702e75052034d72125db6bd5d6821ac8379 9 misc/0000.bin
|
f666c702e75052034d72125db6bd5d6821ac8379 9 misc/0000.bin
|
||||||
2af38bb5b2fe77af4ed2a6079ebe5857247082a5 367 misc/0001.bin
|
2af38bb5b2fe77af4ed2a6079ebe5857247082a5 367 misc/0001.bin
|
||||||
|
|||||||
|
@@ -158,6 +158,19 @@ pub fn unpack(iso_path: &Path, out_dir: &Path) -> Result<Vec<Extracted>, String>
|
|||||||
);
|
);
|
||||||
parts += 1;
|
parts += 1;
|
||||||
}
|
}
|
||||||
|
// The level's executable. It carries the normal
|
||||||
|
// table the tfrag lighting microcode indexes, so
|
||||||
|
// it is not optional background.
|
||||||
|
let overlay = data_header.part(LevelDataPart::Overlay);
|
||||||
|
if let Ok(bytes) = overlay.slice(data_extent, data_extent.len()) {
|
||||||
|
write_file(
|
||||||
|
out_dir,
|
||||||
|
&format!("{stem}/overlay.bin"),
|
||||||
|
bytes,
|
||||||
|
&mut written,
|
||||||
|
);
|
||||||
|
parts += 1;
|
||||||
|
}
|
||||||
// Graphics Synthesizer RAM holds the CLUTs the tfrag
|
// Graphics Synthesizer RAM holds the CLUTs the tfrag
|
||||||
// texture table's palette indices point into, so it
|
// texture table's palette indices point into, so it
|
||||||
// is needed to decode any level texture.
|
// is needed to decode any level texture.
|
||||||
|
|||||||
@@ -6,4 +6,5 @@ pub mod level_data;
|
|||||||
pub mod level_wad;
|
pub mod level_wad;
|
||||||
pub mod texture;
|
pub mod texture;
|
||||||
pub mod tfrag;
|
pub mod tfrag;
|
||||||
|
pub mod tfrag_light;
|
||||||
pub mod wad;
|
pub mod wad;
|
||||||
|
|||||||
@@ -611,13 +611,26 @@ pub struct TfragRgba {
|
|||||||
pub struct TfragLight {
|
pub struct TfragLight {
|
||||||
pub unknown_0: i8,
|
pub unknown_0: i8,
|
||||||
pub intensity: i8,
|
pub intensity: i8,
|
||||||
/// Normal azimuth, pi/128 radians per unit.
|
/// Normal azimuth, as an *index* into the overlay's 256-entry sine table.
|
||||||
pub azimuth: i8,
|
pub azimuth: i8,
|
||||||
/// Normal elevation, pi/128 radians per unit.
|
/// Normal elevation, as an index into the same table.
|
||||||
pub elevation: i8,
|
pub elevation: i8,
|
||||||
pub color: i16,
|
pub color: i16,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TfragLight {
|
||||||
|
/// Azimuth as the unsigned table index. The field is stored signed but
|
||||||
|
/// indexes 0..=255, so the byte is what matters, not its sign.
|
||||||
|
pub fn azimuth_index(&self) -> u8 {
|
||||||
|
self.azimuth as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Elevation as the unsigned table index.
|
||||||
|
pub fn elevation_index(&self) -> u8 {
|
||||||
|
self.elevation as u8
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Everything one tfrag contributes to a draw call.
|
/// Everything one tfrag contributes to a draw call.
|
||||||
#[derive(Debug, Clone, Default, PartialEq)]
|
#[derive(Debug, Clone, Default, PartialEq)]
|
||||||
pub struct TfragGeometry {
|
pub struct TfragGeometry {
|
||||||
|
|||||||
@@ -0,0 +1,271 @@
|
|||||||
|
//! Tfrag lighting inputs: the normal table and the level light bank.
|
||||||
|
//!
|
||||||
|
//! Spec: `docs/formats/tfrag.md`, `docs/formats/tfrag-lighting.md`.
|
||||||
|
//!
|
||||||
|
//! A tfrag's own light records store an azimuth and an elevation per vertex,
|
||||||
|
//! as *indices* into a 256-entry sine table rather than as angles. The table
|
||||||
|
//! lives in the level's overlay executable, and it is not at a fixed offset:
|
||||||
|
//! level 0 has it at `0xb650c`, level 5 at `0xcca30`. The overlay is also a
|
||||||
|
//! raw loadable image with **no ELF header**, so there is no program header to
|
||||||
|
//! map a virtual address through. The table is therefore located by its own
|
||||||
|
//! contents and checked before use — see [`NormalTable::from_overlay`].
|
||||||
|
//!
|
||||||
|
//! Nothing here reproduces the lighting *arithmetic*. That runs in the PS2's
|
||||||
|
//! float model, which the engine owns; this module only decodes the inputs
|
||||||
|
//! exactly.
|
||||||
|
|
||||||
|
use crate::buf::{Buf, Error, ErrorKind, Result};
|
||||||
|
|
||||||
|
/// 256 pairs of `(cos, sin)` as raw PS2 float bits.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct NormalTable {
|
||||||
|
/// `[angle][0]` is cos, `[angle][1]` is sin, for `angle * pi / 128`.
|
||||||
|
pub cos_sin: Vec<[u32; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// How far a candidate entry may differ from `cos(i * pi / 128)` and still be
|
||||||
|
/// accepted. The disc's values are f32 and the check is f64, so exact equality
|
||||||
|
/// is not available; the tolerance is far below anything the table could
|
||||||
|
/// legitimately differ by.
|
||||||
|
const TABLE_EPSILON: f64 = 1e-3;
|
||||||
|
|
||||||
|
impl NormalTable {
|
||||||
|
/// Find the sine table inside a level overlay.
|
||||||
|
///
|
||||||
|
/// The overlay carries no ELF header, so the table cannot be reached
|
||||||
|
/// through a program header the way the boot executable's copy can. Every
|
||||||
|
/// table begins with exactly `(1.0, 0.0)` — angle 0 — so the scan starts
|
||||||
|
/// there and then requires all 256 entries to match the expected sine
|
||||||
|
/// curve. Validating the whole table rather than just the first entry is
|
||||||
|
/// what makes the search safe: a lone `1.0, 0.0` pair occurs by accident.
|
||||||
|
pub fn from_overlay(overlay: &[u8]) -> Result<NormalTable> {
|
||||||
|
if overlay.len() < 256 * 8 {
|
||||||
|
return Err(Error {
|
||||||
|
offset: 0,
|
||||||
|
context: "overlay is too small to hold a normal table",
|
||||||
|
kind: ErrorKind::OutOfBounds {
|
||||||
|
needed: 256 * 8,
|
||||||
|
available: overlay.len(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let mut expected = [0f64; 512];
|
||||||
|
for i in 0..256usize {
|
||||||
|
let a = i as f64 * std::f64::consts::PI / 128.0;
|
||||||
|
expected[i * 2] = a.cos();
|
||||||
|
expected[i * 2 + 1] = a.sin();
|
||||||
|
}
|
||||||
|
// The first entry must be exactly (1, 0) before the table is even a
|
||||||
|
// candidate; that is what makes the scan cheap.
|
||||||
|
let mut at = 0usize;
|
||||||
|
while at + 256 * 8 <= overlay.len() {
|
||||||
|
let (c, s) = read_pair(overlay, at);
|
||||||
|
if c == 1.0 && s == 0.0 && matches_curve(overlay, at, &expected) {
|
||||||
|
let mut cos_sin = Vec::with_capacity(256);
|
||||||
|
for i in 0..256usize {
|
||||||
|
let (c, s) = read_pair(overlay, at + i * 8);
|
||||||
|
cos_sin.push([c.to_bits(), s.to_bits()]);
|
||||||
|
}
|
||||||
|
return Ok(NormalTable { cos_sin });
|
||||||
|
}
|
||||||
|
at += 4;
|
||||||
|
}
|
||||||
|
Err(Error {
|
||||||
|
offset: 0,
|
||||||
|
context: "no normal table found in the overlay",
|
||||||
|
kind: ErrorKind::Invalid("normal table not found"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cosine and sine for an angle index, as the PS2 floats the game uses.
|
||||||
|
pub fn get(&self, index: u8) -> (f32, f32) {
|
||||||
|
let e = self.cos_sin[index as usize];
|
||||||
|
(f32::from_bits(e[0]), f32::from_bits(e[1]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A surface normal from an azimuth and elevation index.
|
||||||
|
///
|
||||||
|
/// `decode_normal` in the reference builds `(cos(a)cos(e), sin(a)cos(e),
|
||||||
|
/// sin(e))` and then negates all three, so the stored normals point
|
||||||
|
/// *into* the surface. Preserved rather than corrected: fidelity beats
|
||||||
|
/// what looks right.
|
||||||
|
pub fn normal(&self, azimuth: u8, elevation: u8) -> [f32; 3] {
|
||||||
|
let (ca, sa) = self.get(azimuth);
|
||||||
|
let (ce, se) = self.get(elevation);
|
||||||
|
[-(ca * ce), -(sa * ce), -se]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_pair(bytes: &[u8], at: usize) -> (f32, f32) {
|
||||||
|
let c = f32::from_le_bytes(bytes[at..at + 4].try_into().unwrap());
|
||||||
|
let s = f32::from_le_bytes(bytes[at + 4..at + 8].try_into().unwrap());
|
||||||
|
(c, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches_curve(overlay: &[u8], at: usize, expected: &[f64; 512]) -> bool {
|
||||||
|
for i in 0..256usize {
|
||||||
|
let (c, s) = read_pair(overlay, at + i * 8);
|
||||||
|
if !c.is_finite() || !s.is_finite() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (c as f64 - expected[i * 2]).abs() > TABLE_EPSILON {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (s as f64 - expected[i * 2 + 1]).abs() > TABLE_EPSILON {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The level's directional and point light sets, from the `gameplay` extent.
|
||||||
|
///
|
||||||
|
/// Only the framing is decoded here. The lighting pass itself needs the PS2
|
||||||
|
/// float model and lives in the engine.
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq)]
|
||||||
|
pub struct LightBank {
|
||||||
|
/// Directional light sets, in the order the microcode indexes them.
|
||||||
|
pub sets: Vec<DirLightSet>,
|
||||||
|
pub point_lights: Vec<PointLight>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One directional light pair: a colour and a direction for each of A and B.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||||
|
pub struct DirLightSet {
|
||||||
|
pub color_a: [f32; 4],
|
||||||
|
pub color_b: [f32; 4],
|
||||||
|
pub dir_a: [f32; 4],
|
||||||
|
pub dir_b: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A point light.
|
||||||
|
#[derive(Debug, Clone, Copy, Default, PartialEq)]
|
||||||
|
pub struct PointLight {
|
||||||
|
pub color: [f32; 4],
|
||||||
|
pub pos: [f32; 4],
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cap the light counts so a corrupt header cannot ask for a huge allocation.
|
||||||
|
const MAX_LIGHTS: usize = 64;
|
||||||
|
|
||||||
|
impl LightBank {
|
||||||
|
/// Parse the bank from a level's `gameplay` extent.
|
||||||
|
///
|
||||||
|
/// The header at +0x04 is an offset to a count followed by the sets; a
|
||||||
|
/// negative count means the level has no lights.
|
||||||
|
pub fn parse(gameplay: &[u8]) -> Result<LightBank> {
|
||||||
|
let mut b = Buf::new(gameplay);
|
||||||
|
let ofs = b.u32("light bank offset")? as usize;
|
||||||
|
let count = b.i32("directional light count")?;
|
||||||
|
if count < 0 {
|
||||||
|
return Ok(LightBank::default());
|
||||||
|
}
|
||||||
|
let n = (count as usize).min(MAX_LIGHTS);
|
||||||
|
let mut sets = Vec::with_capacity(n);
|
||||||
|
for _ in 0..n {
|
||||||
|
sets.push(read_set(gameplay, ofs + 0x10 + sets.len() * 0x40)?);
|
||||||
|
}
|
||||||
|
Ok(LightBank {
|
||||||
|
sets,
|
||||||
|
point_lights: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One 0x40-byte directional light set: four V4 lanes each for colour A,
|
||||||
|
/// direction A, colour B and direction B.
|
||||||
|
fn read_set(bytes: &[u8], at: usize) -> Result<DirLightSet> {
|
||||||
|
at.checked_add(0x40)
|
||||||
|
.filter(|&e| e <= bytes.len())
|
||||||
|
.ok_or(Error {
|
||||||
|
offset: at,
|
||||||
|
context: "directional light set runs past the gameplay extent",
|
||||||
|
kind: ErrorKind::OutOfBounds {
|
||||||
|
needed: 0x40,
|
||||||
|
available: bytes.len().saturating_sub(at),
|
||||||
|
},
|
||||||
|
})?;
|
||||||
|
let v4 = |o: usize| -> [f32; 4] {
|
||||||
|
let mut v = [0f32; 4];
|
||||||
|
for (i, slot) in v.iter_mut().enumerate() {
|
||||||
|
*slot = f32::from_bits(u32::from_le_bytes(
|
||||||
|
bytes[o + i * 4..o + i * 4 + 4].try_into().unwrap(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
v
|
||||||
|
};
|
||||||
|
Ok(DirLightSet {
|
||||||
|
color_a: v4(at),
|
||||||
|
dir_a: v4(at + 0x10),
|
||||||
|
color_b: v4(at + 0x20),
|
||||||
|
dir_b: v4(at + 0x30),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// A synthetic overlay holding one correct table plus noise around it.
|
||||||
|
fn overlay_with_table() -> Vec<u8> {
|
||||||
|
let mut v = vec![0u8; 0x40];
|
||||||
|
// A decoy that starts with (1, 0) but then leaves the curve.
|
||||||
|
v[0x10..0x14].copy_from_slice(&1.0f32.to_le_bytes());
|
||||||
|
v[0x14..0x18].copy_from_slice(&0.0f32.to_le_bytes());
|
||||||
|
v[0x18..0x1c].copy_from_slice(&9.0f32.to_le_bytes());
|
||||||
|
|
||||||
|
let at = 0x400usize;
|
||||||
|
v.resize(at + 256 * 8, 0);
|
||||||
|
for i in 0..256usize {
|
||||||
|
let a = i as f64 * std::f64::consts::PI / 128.0;
|
||||||
|
v[at + i * 8..at + i * 8 + 4].copy_from_slice(&(a.cos() as f32).to_le_bytes());
|
||||||
|
v[at + i * 8 + 4..at + i * 8 + 8].copy_from_slice(&(a.sin() as f32).to_le_bytes());
|
||||||
|
}
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn finds_the_table_and_skips_decoys() {
|
||||||
|
let o = overlay_with_table();
|
||||||
|
let t = NormalTable::from_overlay(&o).expect("table should be found");
|
||||||
|
assert_eq!(t.cos_sin.len(), 256);
|
||||||
|
assert_eq!(t.get(0), (1.0, 0.0));
|
||||||
|
assert!(t.get(64).0.abs() < 1e-6);
|
||||||
|
assert!((t.get(64).1 - 1.0).abs() < 1e-5);
|
||||||
|
assert!((t.get(128).0 + 1.0).abs() < 1e-5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_short_overlay_is_an_error_not_a_panic() {
|
||||||
|
assert!(NormalTable::from_overlay(&[0u8; 16]).is_err());
|
||||||
|
assert!(NormalTable::from_overlay(&[]).is_err());
|
||||||
|
assert!(NormalTable::from_overlay(&vec![0u8; 4096]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normals_point_into_the_surface() {
|
||||||
|
let t = NormalTable::from_overlay(&overlay_with_table()).unwrap();
|
||||||
|
// Azimuth 0, elevation 0 is (1, 0, 0) before negation.
|
||||||
|
assert_eq!(t.normal(0, 0), [-1.0, 0.0, 0.0]);
|
||||||
|
// Elevation 64 is pi/2, so z is +/-1.
|
||||||
|
let n = t.normal(0, 64);
|
||||||
|
assert!(n[0].abs() < 1e-5 && n[1].abs() < 1e-5);
|
||||||
|
assert!((n[2] + 1.0).abs() < 1e-5);
|
||||||
|
// Every normal is a unit vector.
|
||||||
|
for a in [0u8, 32, 96, 200] {
|
||||||
|
for e in [0u8, 32, 64, 96] {
|
||||||
|
let n = t.normal(a, e);
|
||||||
|
let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
|
||||||
|
assert!((len - 1.0).abs() < 1e-4, "a={a} e={e} len={len}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn a_level_with_no_lights_parses_as_empty() {
|
||||||
|
let mut g = vec![0u8; 32];
|
||||||
|
g[0x04..0x08].copy_from_slice(&(-1i32).to_le_bytes());
|
||||||
|
let b = LightBank::parse(&g).expect("should parse");
|
||||||
|
assert!(b.sets.is_empty());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
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 *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
|
The real model is `LightTfrags`, which needs three things:
|
||||||
parsed yet:
|
|
||||||
|
|
||||||
* the **light bank** from the level's `gameplay` extent (directional and point
|
* the **light records** -- already parsed: azimuth and elevation as *indices*
|
||||||
light sets, and the per-vertex light `select` field);
|
into the overlay's sine table, plus the base colour;
|
||||||
* the **normal table**, 256 pairs of cos/sin read from an ELF at
|
* the **normal table** -- now decoded (`gc_formats::tfrag_light`), and normals
|
||||||
`BOOT_NORMAL_TABLE_VADDR` (`0x165500`) -- every level overlay carries one, so
|
are built and fed to the renderer;
|
||||||
it does not need the boot ELF;
|
* the **light bank** from the level's `gameplay` extent -- framed and parsed,
|
||||||
* the **light records** themselves, which are already parsed: azimuth and
|
but the per-vertex evaluation is not written yet.
|
||||||
elevation at pi/128 radians per unit, and the base colour.
|
|
||||||
|
### 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))`,
|
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
|
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
|
- [x] Repository hygiene in place (pre-commit guard, `.gitignore`, licence
|
||||||
record)
|
record)
|
||||||
- [x] **Phase 1 complete.** The disc pipeline runs end to end and is verified
|
- [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.
|
one of the 92 asset containers parsing.
|
||||||
- [x] **Phase 2 in progress.** Level containers are parsed through their typed
|
- [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
|
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
|
drawn unlit so the frame shows real albedo rather than a faked lighting
|
||||||
model. Verified against the stored values (level 0 patch 0 runs
|
model. Verified against the stored values (level 0 patch 0 runs
|
||||||
`(25,26,26)` to `(108,144,141)`), which are genuinely dark.
|
`(25,26,26)` to `(108,144,141)`), which are genuinely dark.
|
||||||
- [ ] **tfrag lighting.** Needs the level light bank from `gameplay`, the
|
- [x] **Tfrag normals.** The 256-entry sine table is decoded from the level
|
||||||
256-entry normal table from the overlay ELF at `0x165500`, and the
|
overlay and normals are built per vertex and handed to the renderer. The
|
||||||
already-parsed azimuth/elevation light records. The pass itself is
|
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
|
`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.
|
- [ ] Then the GS register set and the guard-band clip test.
|
||||||
|
|
||||||
The disc being the exact build the reference projects pin is the single
|
The disc being the exact build the reference projects pin is the single
|
||||||
|
|||||||
Reference in New Issue
Block a user