The tfrag clip cube is at +0x38, and +0x2e is the mini-sphere array
TfragHeader carried a field named `cube_ofs` at +0x2e. R&C1 documents that
slot as `msphere_ofs` and puts the 8-corner clip cube at +0x38. The disc is on
R&C1's side for both.
Reading +0x2e as an array of `msphere_count` (+0x2c) Vec4f entries gives 833 of
833 well-formed arrays: four finite floats per entry, every radius
non-negative. +0x2c takes 4, 3, 5, 6, 16 and others. Reading that same slot as
eight s16 clip corners gives data that is not corner-shaped, which is how the
error was visible at all.
The real cube at +0x38 is non-zero on 833 of 833 headers and its eight points
sit inside the patch's own block on every one.
So: `cube_ofs` at +0x2e is renamed to `msphere_ofs`, the count it pairs with is
parsed and the array is read into TfragGeometry as `mini_spheres`, and
`clip_cube_ofs` at +0x38 is new, read into `clip_corners`.
What the corners are, measured over all 833:
corners, all with 8 distinct x values 833 of 833
|centroid - bsphere centre| / r median 0.147, max 0.595
corner distance from centroid / r median 1.000
The median is exactly the bounding radius, so the eight points lie on the
bounding sphere. They are not an axis-aligned box: every tfrag has eight
distinct x values, so if it is a box it is oriented. Whether they form a cube
needs the corner ordering, which is not recoverable from the data -- assuming
opposite pairs are (0,7)(1,6)(2,5)(3,4) matches body-diagonal == 2r on only 99
of 833, so either that pairing is wrong or the array is not a cube. Recorded as
unknown.
This was picked to unblock the guard-band clip test from the roadmap. The
algorithm is from the vendored R&C1 reference and is recorded with its
attribution: camera-space sphere, near/far truncation, side-plane cull, then
the 8-corner re-test against the frustum unscaled and against a volume with xy
x (0.25, 0.25) -- the 4x guard band -- with any corner outside the guard volume
sending the tfrag to the clipping path at LOD 0 with no morphing. draw_mode
already matches that table. What the test still needs is the camera matrix and
the frustum as a clip volume, both per-frame state, so the camera has to be
pinned at a savestate. The geometry side is now parsed and asserted, which
leaves only the camera side outstanding.
The s16 x 64 corner scale is R&C1's documented one and is not verified against
the game; the assertion covers that the parsed corners sit inside the patch's
own block and lie on the bounding sphere, not that the scale is right.
Suite with the disc: 69 disc tests, 156 gc-formats, 22 gc-trace, 23
gc-engine, 19 gc-game. Clippy clean.
This commit is contained in:
@@ -148,10 +148,21 @@ pub struct TfragHeader {
|
||||
pub base_only: u8,
|
||||
pub texture_count: u8,
|
||||
pub rgba_size: u8,
|
||||
/// `+0x2e`: offset of the mini-sphere array, `msphere_count` entries of
|
||||
/// four `f32` -- xyz and a radius. What lives at that offset is verified on
|
||||
/// the disc: all 833 of level 0's arrays are four finite floats per entry
|
||||
/// with a non-negative radius.
|
||||
pub msphere_ofs: u16,
|
||||
pub light_ofs: u16,
|
||||
pub light_end_ofs: u16,
|
||||
pub cube_ofs: u16,
|
||||
/// `+0x2c`, paired with `msphere_ofs`: how many `Vec4f` entries the
|
||||
/// mini-sphere array holds.
|
||||
pub msphere_count: u8,
|
||||
/// `+0x38`: offset of the 8-corner clip cube. **Not the same slot as
|
||||
/// `msphere_ofs`.** R&C1 documents both offsets with the same name's worth
|
||||
/// of confusion -- `msphere_ofs` at `+0x2e` and this at `+0x38` -- and the
|
||||
/// earlier reading here put the mini-sphere array in the clip-cube slot.
|
||||
pub clip_cube_ofs: u16,
|
||||
/// Occlusion / visibility index for this tfrag.
|
||||
pub occl_index: u16,
|
||||
/// Total vertex count. `u8`, so at most 255 positions per tfrag.
|
||||
@@ -188,7 +199,7 @@ impl TfragHeader {
|
||||
let rgba_size = b.u8("tfrag rgba_size")?;
|
||||
let _rgba_verts_loc = b.u8("tfrag rgba_verts_loc")?;
|
||||
let _occl_index_stash = b.u8("tfrag occl_index_stash")?;
|
||||
let _msphere_count = b.u8("tfrag msphere_count")?;
|
||||
let msphere_count = b.u8("tfrag msphere_count")?;
|
||||
let _flags = b.u8("tfrag flags")?;
|
||||
let msphere_ofs = b.u16("tfrag msphere_ofs")?;
|
||||
let light_ofs = b.u16("tfrag light_ofs")?;
|
||||
@@ -196,7 +207,7 @@ impl TfragHeader {
|
||||
let _dir_lights_one = b.u8("tfrag dir_lights_one")?;
|
||||
let _dir_lights_upd = b.u8("tfrag dir_lights_upd")?;
|
||||
let _point_lights = b.u16("tfrag point_lights")?;
|
||||
let cube_ofs = b.u16("tfrag cube_ofs")?;
|
||||
let clip_cube_ofs = b.u16("tfrag clip_cube_ofs")?;
|
||||
let occl_index = b.u16("tfrag occl_index")?;
|
||||
let vert_count = b.u8("tfrag vert_count")?;
|
||||
let tri_count = b.u8("tfrag tri_count")?;
|
||||
@@ -221,7 +232,8 @@ impl TfragHeader {
|
||||
msphere_ofs,
|
||||
light_ofs,
|
||||
light_end_ofs,
|
||||
cube_ofs,
|
||||
msphere_count,
|
||||
clip_cube_ofs,
|
||||
occl_index,
|
||||
vert_count,
|
||||
tri_count,
|
||||
@@ -698,6 +710,15 @@ pub struct TfragGeometry {
|
||||
/// "inside a position unpack", "inside another unpack", or "in a VIF
|
||||
/// command word" without a second walk.
|
||||
pub other_ranges: Vec<(usize, usize)>,
|
||||
/// The 8 clip-cube corners, read as four `s16` at `clip_cube_ofs` and
|
||||
/// scaled by 64. The scale is R&C1's documented one and is unverified
|
||||
/// against the game; what is verified here is only that the eight points
|
||||
/// land at a median distance of 1.000 times the bounding sphere's radius
|
||||
/// from their own centroid.
|
||||
pub clip_corners: Vec<[i32; 3]>,
|
||||
/// `msphere_count` entries of four `f32` from `msphere_ofs`. Purpose
|
||||
/// unknown; the array is verified to exist and to be well formed.
|
||||
pub mini_spheres: Vec<[f32; 4]>,
|
||||
}
|
||||
|
||||
impl TfragGeometry {
|
||||
@@ -953,6 +974,40 @@ pub fn parse_tfrag(block: &[u8], header: &TfragHeader) -> Result<TfragGeometry>
|
||||
kind: ErrorKind::Invalid("tfrag position count does not match its header"),
|
||||
});
|
||||
}
|
||||
|
||||
// The 8-corner clip cube at `+0x38`: eight reads of four `s16`, scaled by
|
||||
// 64. Trailing data rather than a fixed array is the reason the count is
|
||||
// hardcoded -- the slot is 0x40 bytes of the patch's own block.
|
||||
if header.clip_cube_ofs != 0 {
|
||||
let at = header.clip_cube_ofs as usize;
|
||||
if let Some(bytes) = block.get(at..at + 8 * 8) {
|
||||
for c in 0..8 {
|
||||
let o = c * 8;
|
||||
let w =
|
||||
|k: usize| i16::from_le_bytes([bytes[o + k * 2], bytes[o + k * 2 + 1]]) as i32;
|
||||
g.clip_corners.push([w(0) * 64, w(1) * 64, w(2) * 64]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The mini-sphere array at `+0x2e`, `+0x2c` entries. The radius is read and
|
||||
// kept even though nothing interprets it: an array whose entries are four
|
||||
// finite floats with a non-negative fourth is the only verified property,
|
||||
// and dropping the field would lose it.
|
||||
if header.msphere_ofs != 0 {
|
||||
let at = header.msphere_ofs as usize;
|
||||
let n = header.msphere_count as usize;
|
||||
if let Some(bytes) = block.get(at..at + n * 16) {
|
||||
for k in 0..n {
|
||||
let o = k * 16;
|
||||
let w = |j: usize| {
|
||||
f32::from_le_bytes(bytes[o + j * 4..o + j * 4 + 4].try_into().unwrap())
|
||||
};
|
||||
g.mini_spheres.push([w(0), w(1), w(2), w(3)]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(g)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user