diff options
Diffstat (limited to 'crates/fparkan-fx')
| -rw-r--r-- | crates/fparkan-fx/src/environment.rs | 74 | ||||
| -rw-r--r-- | crates/fparkan-fx/src/sky.rs | 29 |
2 files changed, 73 insertions, 30 deletions
diff --git a/crates/fparkan-fx/src/environment.rs b/crates/fparkan-fx/src/environment.rs index 68a2c8f..713a3f9 100644 --- a/crates/fparkan-fx/src/environment.rs +++ b/crates/fparkan-fx/src/environment.rs @@ -590,7 +590,7 @@ impl LightningBounds { /// Effect.dll's opcode 1 handler (`1000F6E0`) updates a LightManager record. /// The A and B groups are the submitted position and direction, C is the /// four-component HDR light color, the scalar pair at body offsets 108 and -/// 112 is the light range, and body offsets 120, 124, and 128 are the native +/// 112 is the local light radius, and body offsets 120, 124, and 128 are the native /// attenuation coefficients. These values do not describe lightning mesh /// geometry or texture coordinates. #[derive(Clone, Copy, Debug, PartialEq)] @@ -615,8 +615,7 @@ pub struct PointLightFrame { pub range: f32, /// Native attenuation coefficients `(a0, a1, a2)`. pub attenuation: [f32; 3], - /// The scalar at body offset 116. Its native role is an effect factor, - /// not alpha or a UV parameter, so it is preserved without reinterpretation. + /// Radius jitter amplitude at body offset 116; retained but not sampled yet. pub effect_factor: f32, /// Whether the opcode 1 command is active at `command_time`. pub active: bool, @@ -626,8 +625,8 @@ pub struct PointLightFrame { /// applies a parent basis to its local position and direction. `basis` stores /// the world-space vectors corresponding to local +X, +Y, and +Z. /// `command_time` is resolved by the caller; FX header duration and lifecycle -/// remain with that caller. The interpolated range is retained as authored; -/// native Op1 does not clamp it to zero. +/// remain with that caller. Native Op1 scales the radius by the transformed +/// direction length and replaces a non-positive result with 0.01. #[must_use] pub fn evaluate_op1_point_light_with_basis( command: &FxCommand, @@ -677,7 +676,22 @@ pub fn evaluate_op1_point_light_with_basis( { return None; } - let direction = normalize_op1_direction(transformed_direction); + let direction_length = transformed_direction + .into_iter() + .map(|component| component * component) + .sum::<f32>() + .sqrt(); + // shortcut: Op1 color/radius jitter is omitted; add native RNG sampling + // when the reported randomized Control lights are handled in phase R4. + let scaled_range = sample.range * direction_length; + if !direction_length.is_finite() || !scaled_range.is_finite() { + return None; + } + let direction = if direction_length > 0.0 { + transformed_direction.map(|component| component / direction_length) + } else { + [0.0; 3] + }; Some(PointLightFrame { effect_type: sample.effect_type, @@ -688,7 +702,11 @@ pub fn evaluate_op1_point_light_with_basis( position, direction, color: sample.color, - range: sample.range, + range: if scaled_range <= 0.0 { + 0.01 + } else { + scaled_range + }, attenuation: sample.attenuation, effect_factor: sample.effect_factor, active: true, @@ -722,19 +740,6 @@ fn sample_op1_point_light(body: &[u8], normalized_time: f32) -> Option<Op1PointL }) } -fn normalize_op1_direction(direction: [f32; 3]) -> [f32; 3] { - // Native FastProc normalization preserves a degenerate vector as zero; - // this path must not invent a fallback axis. - let length = - (direction[0] * direction[0] + direction[1] * direction[1] + direction[2] * direction[2]) - .sqrt(); - if length.is_finite() && length > 0.0 { - direction.map(|component| component / length) - } else { - [0.0; 3] - } -} - fn lightning_point_light_direction(local_direction: [f32; 3], basis: [[f32; 3]; 3]) -> [f32; 3] { // Preserve the pre-existing Lightning consumer fallback, which is // separate from the generic native Op1 behavior. @@ -2964,7 +2969,7 @@ mod tests { } #[test] - fn generic_opcode1_preserves_negative_native_range() { + fn generic_opcode1_clamps_non_positive_scaled_range() { let mut command = op1_test_command(5, 0.0, 1.0); let mut body = command.raw_body.to_vec(); body[108..112].copy_from_slice(&(-2.0_f32).to_le_bytes()); @@ -2973,7 +2978,7 @@ mod tests { let sample = evaluate_op1_point_light_identity(&command, 0.5, [0.0; 3]) .expect("native Op1 sample with a negative range"); - assert_eq!(sample.range, -3.0); + assert_eq!(sample.range, 0.01); } #[test] @@ -2986,6 +2991,31 @@ mod tests { let sample = evaluate_op1_point_light_identity(&command, 0.5, [0.0; 3]) .expect("native Op1 sample with a zero direction"); assert_eq!(sample.direction, [0.0; 3]); + assert_eq!(sample.range, 0.01); + } + + #[test] + fn generic_opcode1_scales_radius_like_live_factory_light() { + // Live Effect.dll Op1: authored radius 7, FastProc direction length + // 0x409b1f91, submitted LightManager radius 0x4207bb9f. + let mut command = op1_test_command(7, 0.75, 1.0); + let mut body = command.raw_body.to_vec(); + body[48..60].copy_from_slice(&[1.0_f32, 0.0, 0.0].map(f32::to_le_bytes).concat()); + body[112..116].copy_from_slice(&7.0_f32.to_le_bytes()); + command.raw_body = Arc::from(body.into_boxed_slice()); + let sample = evaluate_op1_point_light_with_basis( + &command, + 1.0, + [0.0; 3], + [ + [f32::from_bits(0x409b_1f91), 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, 1.0], + ], + ) + .expect("live factory radius"); + assert_eq!(sample.range.to_bits(), 0x4207_bb9f); + assert_eq!(sample.direction, [1.0, 0.0, 0.0]); } #[test] diff --git a/crates/fparkan-fx/src/sky.rs b/crates/fparkan-fx/src/sky.rs index 26e68ab..d9ba3ed 100644 --- a/crates/fparkan-fx/src/sky.rs +++ b/crates/fparkan-fx/src/sky.rs @@ -1143,12 +1143,11 @@ impl SunFrame { /// Converts an interpolated atmosphere sun sample to a renderer-facing frame. /// /// The original update computes `orbit_direction = [cos(theta), 0, -/// -sin(theta)]`, then applies the row-vector product of the two birth-angle +/// -sin(theta)]`, then applies the column-vector product of the two birth-angle /// matrices. With `A` and `B` in radians, the resulting direction is: /// /// ```text -/// [x*cos(A), -x*sin(A)*cos(B) + z*sin(B), -/// x*sin(A)*sin(B) + z*cos(B)] +/// [x*cos(A) + z*sin(A)*sin(B), x*sin(A) - z*cos(A)*sin(B), z*cos(B)] /// ``` /// /// where `x` and `z` are the two non-zero orbit components. @@ -1178,9 +1177,9 @@ pub fn sun_frame(sample: &SunSample, now_seconds: f32, config: SunConfig) -> Sun let x = orbit_direction[0]; let z = orbit_direction[2]; let direction = normalize3([ - x * cos_a, - -x * sin_a * cos_b + z * sin_b, - x * sin_a * sin_b + z * cos_b, + x * cos_a + z * sin_a * sin_b, + x * sin_a - z * cos_a * sin_b, + z * cos_b, ]); SunFrame { active: true, @@ -1892,16 +1891,30 @@ mod tests { let sample = sun(true); let at_start = sun_frame(&sample, 0.0, SunConfig::new(0.0, 1.0, [90.0, 0.0])); assert!((at_start.direction[0]).abs() < 1.0e-5); - assert!((at_start.direction[1] + at_start.orbit_direction[0]).abs() < 1.0e-5); + assert!((at_start.direction[1] - at_start.orbit_direction[0]).abs() < 1.0e-5); assert!((at_start.direction[2] - at_start.orbit_direction[2]).abs() < 1.0e-5); let side = sun_frame(&sample, 0.0, SunConfig::new(0.0, 1.0, [0.0, 90.0])); assert!((side.direction[0] - side.orbit_direction[0]).abs() < 1.0e-5); - assert!((side.direction[1] - side.orbit_direction[2]).abs() < 1.0e-5); + assert!((side.direction[1] + side.orbit_direction[2]).abs() < 1.0e-5); assert!(side.direction[2].abs() < 1.0e-5); } #[test] + fn sun_direction_matches_live_native_birth_matrix() { + // Terrain CSun::Update, elapsed 282184 ms, lifetime 525000 ms. + let frame = sun_frame( + &sun(true), + 282.184, + SunConfig::new(0.0, 525.0, [90.0, 30.0]), + ); + let expected = [0xbefd_7269, 0xbe10_4168, 0xbf5b_7dda].map(f32::from_bits); + for (actual, expected) in frame.direction.into_iter().zip(expected) { + assert!((actual - expected).abs() < 3.0e-6, "{actual} != {expected}"); + } + } + + #[test] fn native_birth_keeps_the_real_start_time() { assert!(!celestial_frame(&sun(false), 100.0).active); let birth = SunBirth { |
