aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-fx/src/ambient_op3.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/fparkan-fx/src/ambient_op3.rs')
-rw-r--r--crates/fparkan-fx/src/ambient_op3.rs218
1 files changed, 207 insertions, 11 deletions
diff --git a/crates/fparkan-fx/src/ambient_op3.rs b/crates/fparkan-fx/src/ambient_op3.rs
index 5bd2881..1ec2973 100644
--- a/crates/fparkan-fx/src/ambient_op3.rs
+++ b/crates/fparkan-fx/src/ambient_op3.rs
@@ -471,6 +471,7 @@ fn native_op3_type1_matrix(
/// `view · axis` value to exactly ±1 before building the cross-product frame;
/// neither input vector is normalized. The native path normalizes only
/// `view × axis`, then computes `axis × U` without normalizing the result.
+/// The authored axis is the quad's X column (Effect.dll RVA 0x97F6..0x983C).
#[must_use]
fn native_op3_type1_basis(
view: [f32; 3],
@@ -501,9 +502,9 @@ fn native_op3_type1_basis(
}
let w = cross3(axis, u);
let result = [
- [u[0], w[0], axis[0], position[0]],
- [u[1], w[1], axis[1], position[1]],
- [u[2], w[2], axis[2], position[2]],
+ [axis[0], u[0], w[0], position[0]],
+ [axis[1], u[1], w[1], position[1]],
+ [axis[2], u[2], w[2], position[2]],
[0.0, 0.0, 0.0, 1.0],
];
result
@@ -837,19 +838,19 @@ mod tests {
// cross-product frame instead of being normalized before the gate.
let frame =
native_op3_type1_basis([2.0, 1.0, 0.0], [1.0, 0.0, 0.0], [3.0, 4.0, 5.0]).unwrap();
- assert_eq!(frame[0], [0.0, 0.0, 1.0, 3.0]);
- assert_eq!(frame[1], [0.0, 1.0, 0.0, 4.0]);
- assert_eq!(frame[2], [-1.0, 0.0, 0.0, 5.0]);
+ assert_eq!(frame[0], [1.0, 0.0, 0.0, 3.0]);
+ assert_eq!(frame[1], [0.0, 0.0, 1.0, 4.0]);
+ assert_eq!(frame[2], [0.0, -1.0, 0.0, 5.0]);
// The axis is not normalized: W and the axis column keep its scale.
let axis_scaled =
native_op3_type1_basis([2.0, 1.0, 0.0], [0.0, 0.0, 2.0], [0.0; 3]).unwrap();
let inv_sqrt_5 = 1.0 / 5.0_f32.sqrt();
- assert_eq!(axis_scaled[0][0], inv_sqrt_5);
- assert_eq!(axis_scaled[1][0], -2.0 * inv_sqrt_5);
- assert_eq!(axis_scaled[0][1], 4.0 * inv_sqrt_5);
- assert_eq!(axis_scaled[1][1], 2.0 * inv_sqrt_5);
- assert_eq!(axis_scaled[2][2], 2.0);
+ assert_eq!(axis_scaled[0][1], inv_sqrt_5);
+ assert_eq!(axis_scaled[1][1], -2.0 * inv_sqrt_5);
+ assert_eq!(axis_scaled[0][2], 4.0 * inv_sqrt_5);
+ assert_eq!(axis_scaled[1][2], 2.0 * inv_sqrt_5);
+ assert_eq!(axis_scaled[2][0], 2.0);
assert_eq!(
native_op3_type1_basis([f32::NAN, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0; 3]),
@@ -907,6 +908,10 @@ mod tests {
assert_eq!(frame.normalized_time, 0.5);
assert_eq!(frame.color, [1.0, 1.0, 1.0, 0.5]);
assert_eq!(frame.material_phase, 0.5);
+ let authored_axis_edge = std::array::from_fn::<_, 3, _>(|axis| {
+ frame.vertices[2][axis] - frame.vertices[1][axis]
+ });
+ assert_eq!(authored_axis_edge, [0.0, 0.0, 1.0]);
assert!(evaluate_op3_type1_command(
&command,
4,
@@ -919,6 +924,197 @@ mod tests {
}
#[test]
+ #[ignore = "requires the original GOG installation"]
+ fn original_deflector_quad_keeps_its_authored_axis_over_time_and_camera_motion() {
+ let root = std::env::var_os("FPARKAN_CORPUS_PART1_ROOT")
+ .expect("set FPARKAN_CORPUS_PART1_ROOT to the original GOG installation");
+ let bytes = std::fs::read(std::path::Path::new(&root).join("effects.rlb"))
+ .expect("read effects.rlb");
+ let archive = fparkan_nres::decode(bytes.into(), fparkan_nres::ReadProfile::Compatible)
+ .expect("decode effects.rlb");
+ let entry = archive.find("deflector").expect("deflector FXID");
+ let document = crate::decode_fxid(
+ archive
+ .payload(entry)
+ .expect("FXID payload")
+ .to_vec()
+ .into(),
+ )
+ .expect("decode deflector");
+ let command = &document.commands()[0];
+ let parent = [
+ [12.0, 0.0, 0.0, 100.0],
+ [0.0, 3.0, 0.0, 200.0],
+ [0.0, 0.0, 3.0, 20.0],
+ [0.0, 0.0, 0.0, 1.0],
+ ];
+ for elapsed in [0.0_f32, 0.125, 0.5, 1.0, 2.5] {
+ for camera in [
+ [160.0, 230.0, 20.0],
+ [160.0, 200.0, 50.0],
+ [160.0, 170.0, 5.0],
+ ] {
+ let frame = evaluate_op3_type1_command(
+ command,
+ 0,
+ &parent,
+ elapsed,
+ elapsed.min(1.0),
+ camera,
+ )
+ .expect("active deflector quad");
+ assert!(frame.active);
+ assert_eq!(frame.material.name.to_ascii_lowercase(), "defl_b");
+ let axis_edge = std::array::from_fn::<_, 3, _>(|axis| {
+ frame.vertices[2][axis] - frame.vertices[1][axis]
+ });
+ assert_eq!(axis_edge, [12.0, 0.0, 0.0]);
+ let center = std::array::from_fn::<_, 3, _>(|axis| {
+ (frame.vertices[0][axis] + frame.vertices[2][axis]) * 0.5
+ });
+ assert_eq!(center, [106.0, 200.0, 20.0]);
+ assert_eq!(frame.material_phase, (elapsed * 7.0).fract());
+ }
+ }
+ }
+
+ #[test]
+ #[ignore = "requires the original GOG installation"]
+ fn original_tree_ray_58_matches_native_type1_geometry_samples() {
+ let root = std::env::var_os("FPARKAN_CORPUS_PART1_ROOT")
+ .expect("set FPARKAN_CORPUS_PART1_ROOT to the original GOG installation");
+ let bytes = std::fs::read(std::path::Path::new(&root).join("effects.rlb"))
+ .expect("read effects.rlb");
+ let archive = fparkan_nres::decode(bytes.into(), fparkan_nres::ReadProfile::Compatible)
+ .expect("decode effects.rlb");
+ let entry = archive.find("tree_ray_58").expect("tree_ray_58 FXID");
+ let document = crate::decode_fxid(
+ archive
+ .payload(entry)
+ .expect("FXID payload")
+ .to_vec()
+ .into(),
+ )
+ .expect("decode tree_ray_58");
+ assert_eq!(document.header().time_mode, 2);
+ assert_eq!(document.header().duration_seconds, 3.0);
+ assert_eq!(document.commands().len(), 3);
+
+ let parent = [
+ [0.999_924_123, 0.012_319_660, 0.0, 386.673_400_879],
+ [-0.012_319_660, 0.999_924_123, 0.0, 603.692_321_777],
+ [0.0, 0.0, 1.0, 180.414_703_369],
+ [0.0, 0.0, 0.0, 1.0],
+ ];
+ let center = [390.273_132, 603.647_949, 180.414_703];
+ // Original GOG Effect.dll RVA 0x93D0, x87 CW 0x007F; Rust-sampled keys are supplied separately.
+ let native_samples = [
+ (
+ 0,
+ "defl_y_add",
+ [0.0, 35.0, 22.0],
+ [
+ [386.280, 604.229_431, 179.568_024],
+ [386.266_876, 603.165_039, 181.261_383],
+ [394.266_266, 603.066_467, 181.261_383],
+ [394.279_4, 604.130_859, 179.568_024],
+ ],
+ ),
+ (
+ 0,
+ "defl_y_add",
+ [20.0, -30.0, 15.0],
+ [
+ [386.279, 604.147_4, 181.307_663],
+ [386.267_883, 603.247_1, 179.521_744],
+ [394.267_273, 603.148_5, 179.521_744],
+ [394.278_381, 604.048_8, 181.307_663],
+ ],
+ ),
+ (
+ 1,
+ "defl_y1_add",
+ [0.0, 35.0, 22.0],
+ [
+ [386.276_7, 603.963_3, 179.991_364],
+ [386.270_172, 603.431_152, 180.838_043],
+ [394.269_562, 603.332_6, 180.838_043],
+ [394.276_1, 603.864_746, 179.991_364],
+ ],
+ ),
+ ];
+ for (command_index, material_name, camera_offset, expected_vertices) in native_samples {
+ let camera = std::array::from_fn(|axis| center[axis] + camera_offset[axis]);
+ let frame = evaluate_op3_type1_command(
+ &document.commands()[command_index],
+ command_index,
+ &parent,
+ 0.0,
+ 0.0,
+ camera,
+ )
+ .expect("active tree ray quad");
+ assert_eq!(frame.material.name, material_name);
+ for (actual, expected) in frame
+ .vertices
+ .iter()
+ .flatten()
+ .zip(expected_vertices.iter().flatten())
+ {
+ assert!(
+ (actual - expected).abs() < 0.001,
+ "native sample vertex mismatch: {actual} vs {expected}"
+ );
+ }
+ }
+
+ for (command_index, material_name) in [(0, "defl_y_add"), (1, "defl_y1_add")] {
+ let command = &document.commands()[command_index];
+ assert_eq!(command.opcode, FxOpcode::Op3);
+ assert_eq!(
+ read_u32(&command.raw_body, 0),
+ Some(TYPE1_BODY_DISCRIMINATOR)
+ );
+ let camera_offsets = [[0.0, 35.0, 22.0], [20.0, -30.0, 15.0], [-25.0, 18.0, 10.0]];
+ for elapsed in [0.0_f32, 0.375, 0.75, 1.5, 2.25, 2.999, 3.125] {
+ let phase = elapsed.rem_euclid(3.0) / 3.0;
+ for camera_offset in camera_offsets {
+ let camera = std::array::from_fn(|axis| center[axis] + camera_offset[axis]);
+ let frame = evaluate_op3_type1_command(
+ command,
+ command_index,
+ &parent,
+ elapsed,
+ phase,
+ camera,
+ )
+ .expect("active tree ray quad");
+ assert_eq!(frame.material.name, material_name);
+ assert!(frame.active);
+ let actual_center: [f32; 3] = std::array::from_fn(|axis| {
+ frame
+ .vertices
+ .iter()
+ .map(|vertex| vertex[axis])
+ .sum::<f32>()
+ * 0.25
+ });
+ for axis in 0..3 {
+ assert!((actual_center[axis] - center[axis]).abs() < 0.001);
+ }
+ let authored_axis_edge: [f32; 3] = std::array::from_fn(|axis| {
+ frame.vertices[2][axis] - frame.vertices[1][axis]
+ });
+ let native_axis_edge = [7.999_393, -0.098_557_28, 0.0];
+ for axis in 0..3 {
+ assert!((authored_axis_edge[axis] - native_axis_edge[axis]).abs() < 0.001);
+ }
+ }
+ }
+ }
+ }
+
+ #[test]
fn rejects_truncated_bodies_and_non_finite_tracks_or_inputs() {
let identity = [
[1.0, 0.0, 0.0, 0.0],