aboutsummaryrefslogtreecommitdiff
path: root/crates/fparkan-fx/src/environment.rs
diff options
context:
space:
mode:
authorValentin Popov <valentin@popov.link>2026-10-31 17:43:05 +0300
committerValentin Popov <valentin@popov.link>2026-10-31 17:43:05 +0300
commit108a04b70e0cd5032d417cc7fd00cec2a385246b (patch)
treeba0380f1d581b6554acaaf12722db944296d4e6b /crates/fparkan-fx/src/environment.rs
parentc31e5b734252c9191afe7809f7ef32f85f68aa0f (diff)
downloadfparkan-108a04b70e0cd5032d417cc7fd00cec2a385246b.tar.xz
fparkan-108a04b70e0cd5032d417cc7fd00cec2a385246b.zip
fix(render): scale Control light radius with emitter transform
Diffstat (limited to 'crates/fparkan-fx/src/environment.rs')
-rw-r--r--crates/fparkan-fx/src/environment.rs74
1 files changed, 52 insertions, 22 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]