//! Native-shaped CPU evaluation for Types 0, 1, and 2 carried by opcode 3. //! //! The FX body and the command opcode are separate concepts: this evaluator //! accepts opcode 3 only when its body discriminator matches the selected //! evaluator. Types 0/1/2 share the position, scale, material-phase, and //! opacity tracks; Types 1/2 additionally use an animated axis. Their render //! basis is selected by the native body discriminator. use crate::environment::EffectResource; use crate::fx_visual::{ compose_native_effect_matrix, native_camera_facing_basis, native_d110_basis, native_effect_local_point, transform_native_effect_quad, EffectQuadFrame, EFFECT_QUAD_UV, }; use crate::{FxCommand, FxOpcode}; const TYPE2_BODY_DISCRIMINATOR: u32 = 2; const TYPE2_ACTIVE_START_OFFSET: usize = 0x1c; const TYPE2_ACTIVE_END_OFFSET: usize = 0x20; const TYPE2_PHASE_START_OFFSET: usize = 0x04; const TYPE2_PHASE_END_OFFSET: usize = 0x08; const TYPE2_PHASE_EXPONENT_OFFSET: usize = 0x0c; const TYPE2_OPACITY_START_OFFSET: usize = 0x10; const TYPE2_OPACITY_END_OFFSET: usize = 0x14; const TYPE2_OPACITY_EXPONENT_OFFSET: usize = 0x18; const TYPE2_POSITION_START_OFFSET: usize = 0x24; const TYPE2_POSITION_END_OFFSET: usize = 0x30; #[cfg(test)] const TYPE2_POSITION_EXPONENT_OFFSET: usize = 0x3c; const TYPE2_AXIS_START_OFFSET: usize = 0x48; const TYPE2_AXIS_END_OFFSET: usize = 0x54; const TYPE2_SCALE_START_OFFSET: usize = 0x60; const TYPE2_SCALE_END_OFFSET: usize = 0x6c; const TYPE2_RESOURCE_OFFSET: usize = 0x84; const TYPE2_RESOURCE_BYTES: usize = 64; const TYPE0_BODY_DISCRIMINATOR: u32 = 0; const TYPE1_BODY_DISCRIMINATOR: u32 = 1; /// Per-frame values sampled from the verified Op3 Type0 body tracks. #[derive(Clone, Copy, Debug, PartialEq)] struct Op3Type0Sample { /// Animated local translation sampled from body+0x24/+0x30 with exponents /// at body+0x3c/+0x40/+0x44. position: [f32; 3], /// Animated scale sampled from body+0x60/+0x6c with reverse-order /// exponents at body+0x78/+0x7c/+0x80. scale: [f32; 3], /// Native command opacity after its authored curve. opacity: f32, /// Wrapped texture phase computed by the native Op3 time track. material_phase: f32, } /// Per-frame values sampled from the verified native Op3 Type2 body tracks. #[derive(Clone, Copy, Debug, PartialEq)] struct Op3Type2Sample { /// Animated local translation used by native helper 0xC5D0. position: [f32; 3], /// Animated local axis used by native helper 0xD110. axis: [f32; 3], /// Animated scale applied to the parent matrix basis. scale: [f32; 3], /// Opacity multiplier sent through native helper 0x9930 and clamped to /// `[0, 1]` for the material descriptor. opacity: f32, /// Wrapped texture phase computed by the native Op3 time track. material_phase: f32, } /// Per-frame values sampled from the same track layout in a Type 1 body. type Op3Type1Sample = Op3Type2Sample; /// Evaluates one FX opcode 3 / Type 0 fixed camera-facing command. /// /// Type 0 uses the shared animated position and scale tracks but forms its /// local basis from the camera vector. The current caller supplies the /// already-composed Control owner transform and header scale. /// The supported caller path assumes native render-context index 0, which /// satisfies Effect.dll's shared `context + 0x60 < 8` gate. #[must_use] pub fn evaluate_op3_type0_command( command: &FxCommand, command_index: usize, world_from_effect: &[[f32; 4]; 4], elapsed_seconds: f32, effect_phase: f32, camera_world: [f32; 3], ) -> Option { let body = &command.raw_body; if command.opcode != FxOpcode::Op3 || read_u32(body, 0)? != TYPE0_BODY_DISCRIMINATOR || body.len() < TYPE2_RESOURCE_OFFSET + TYPE2_RESOURCE_BYTES || !world_from_effect .iter() .flatten() .all(|value| value.is_finite()) || !elapsed_seconds.is_finite() || !effect_phase.is_finite() || !camera_world.iter().all(|value| value.is_finite()) { return None; } let start = read_f32(body, TYPE2_ACTIVE_START_OFFSET)?; let end = read_f32(body, TYPE2_ACTIVE_END_OFFSET)?; if !start.is_finite() || !end.is_finite() || start > end { return None; } let resource = read_resource(command)?; let normalized_time = interval_phase(effect_phase, start, end); let sample = sample_op3_type0_body(body, normalized_time, elapsed_seconds)?; let camera_local = native_effect_local_point(world_from_effect, camera_world)?; let view = sub3(camera_local, sample.position); let local_basis = native_camera_facing_basis(view, sample.position)?; let matrix = compose_native_effect_matrix( world_from_effect, &local_basis, sample.position, sample.scale, )?; let vertices = transform_native_effect_quad(&matrix)?; Some(EffectQuadFrame { command_index, opcode: FxOpcode::Op3, material: resource, vertices, uv: EFFECT_QUAD_UV, color: [1.0, 1.0, 1.0, sample.opacity], elapsed_seconds, normalized_time, material_phase: sample.material_phase, active: effect_phase >= start && effect_phase <= end, native_flag_base: None, }) } /// Evaluates one FX opcode 3 / Type 2 command using native helper matrix rules. /// /// `world_from_effect` is the row-major parent descriptor already composed by /// the caller through the selected CPT row, mission-root transform, resolved /// owner pose (including a mounted component pose), and FX header scale. The /// evaluator does not resolve those inputs itself. The matrix is the parent /// supplied to native helper 0x93D0, not the final quad matrix. /// /// `effect_phase` is the header-transformed, command-clamped overall effect /// phase in `[0, 1]`. The command's inclusive phase window is stored at /// raw-body offsets `0x1c` and `0x20`. /// `render_context` mirrors the native 0x106C0 gate (`context + 0x60 < 8`). /// The function returns `None` for another command/body type, malformed /// resources, non-finite inputs, or a degenerate non-finite transform. #[must_use] pub fn evaluate_op3_type2_command( command: &FxCommand, command_index: usize, world_from_effect: &[[f32; 4]; 4], elapsed_seconds: f32, effect_phase: f32, render_context: u32, ) -> Option { if command.opcode != FxOpcode::Op3 || read_u32(&command.raw_body, 0)? != TYPE2_BODY_DISCRIMINATOR || command.raw_body.len() < TYPE2_RESOURCE_OFFSET + TYPE2_RESOURCE_BYTES || !elapsed_seconds.is_finite() || !effect_phase.is_finite() { return None; } let start = read_f32(&command.raw_body, TYPE2_ACTIVE_START_OFFSET)?; let end = read_f32(&command.raw_body, TYPE2_ACTIVE_END_OFFSET)?; if !start.is_finite() || !end.is_finite() || start > end { return None; } let resource = read_resource(command)?; let normalized_time = interval_phase(effect_phase, start, end); let sample = sample_op3_type2_body(&command.raw_body, normalized_time, elapsed_seconds)?; let matrix = native_op3_type2_matrix(world_from_effect, sample)?; let vertices = transform_native_effect_quad(&matrix)?; Some(EffectQuadFrame { command_index, opcode: FxOpcode::Op3, material: resource, vertices, uv: EFFECT_QUAD_UV, color: [1.0, 1.0, 1.0, sample.opacity], elapsed_seconds, normalized_time, material_phase: sample.material_phase, active: render_context < 8 && effect_phase >= start && effect_phase <= end, native_flag_base: None, }) } /// Evaluates one FX opcode 3 / Type 1 camera-facing command. /// /// Type 1 uses the native camera-to-position vector and sampled axis to form /// its quad basis. The command's body tracks use the same offsets as Type 2. /// The supported caller path assumes native render-context index 0, which /// satisfies the Effect.dll `context + 0x60 < 8` gate. #[must_use] pub fn evaluate_op3_type1_command( command: &FxCommand, command_index: usize, world_from_effect: &[[f32; 4]; 4], elapsed_seconds: f32, effect_phase: f32, camera_world: [f32; 3], ) -> Option { let body = &command.raw_body; if command.opcode != FxOpcode::Op3 || read_u32(body, 0)? != TYPE1_BODY_DISCRIMINATOR || body.len() < TYPE2_RESOURCE_OFFSET + TYPE2_RESOURCE_BYTES || !elapsed_seconds.is_finite() || !effect_phase.is_finite() || !camera_world.iter().all(|value| value.is_finite()) { return None; } let start = read_f32(body, TYPE2_ACTIVE_START_OFFSET)?; let end = read_f32(body, TYPE2_ACTIVE_END_OFFSET)?; if !start.is_finite() || !end.is_finite() || start > end { return None; } let resource = read_resource(command)?; let normalized_time = interval_phase(effect_phase, start, end); let sample = sample_op3_type1_body(body, normalized_time, elapsed_seconds)?; let matrix = native_op3_type1_matrix(world_from_effect, sample, camera_world)?; let vertices = transform_native_effect_quad(&matrix)?; Some(EffectQuadFrame { command_index, opcode: FxOpcode::Op3, material: resource, vertices, uv: EFFECT_QUAD_UV, color: [1.0, 1.0, 1.0, sample.opacity], elapsed_seconds, normalized_time, material_phase: sample.material_phase, active: effect_phase >= start && effect_phase <= end, native_flag_base: None, }) } /// Samples the verified Type2 body tracks used by native Op3 update/render. /// /// `normalized_active_time` is the value produced by the inclusive command /// interval gate. Position and scale use component-wise powers; axis is a /// linear start/end track. The separate phase track uses elapsed seconds when /// its raw start is negative, matching native 0x105F0. #[must_use] fn sample_op3_type2_body( body: &[u8], normalized_active_time: f32, elapsed_seconds: f32, ) -> Option { if read_u32(body, 0)? != TYPE2_BODY_DISCRIMINATOR || body.len() < TYPE2_RESOURCE_OFFSET + TYPE2_RESOURCE_BYTES { return None; } sample_op3_body_tracks(body, normalized_active_time, elapsed_seconds) } /// Samples the verified Op3 Type0 position/scale and common command tracks. #[must_use] fn sample_op3_type0_body( body: &[u8], normalized_active_time: f32, elapsed_seconds: f32, ) -> Option { if read_u32(body, 0)? != TYPE0_BODY_DISCRIMINATOR || body.len() < TYPE2_RESOURCE_OFFSET + TYPE2_RESOURCE_BYTES || !normalized_active_time.is_finite() || !elapsed_seconds.is_finite() { return None; } // Native 0x106C0 samples the record's position into helper+0x78 from the // body position endpoints and exponents. The body tail at +0x78 is the // reverse-ordered scale exponent vector, not a fixed position. let position = sample_power_vector( body, TYPE2_POSITION_START_OFFSET, TYPE2_POSITION_END_OFFSET, [0x3c, 0x40, 0x44], normalized_active_time, )?; let scale = sample_power_vector( body, TYPE2_SCALE_START_OFFSET, TYPE2_SCALE_END_OFFSET, [0x80, 0x7c, 0x78], normalized_active_time, )?; let (material_phase, opacity) = sample_op3_material_phase_and_opacity(body, normalized_active_time, elapsed_seconds)?; Some(Op3Type0Sample { position, scale, opacity, material_phase, }) } /// Samples the verified Op3 Type 1 body tracks. #[must_use] fn sample_op3_type1_body( body: &[u8], normalized_active_time: f32, elapsed_seconds: f32, ) -> Option { if read_u32(body, 0)? != TYPE1_BODY_DISCRIMINATOR || body.len() < TYPE2_RESOURCE_OFFSET + TYPE2_RESOURCE_BYTES { return None; } sample_op3_body_tracks(body, normalized_active_time, elapsed_seconds) } fn sample_op3_body_tracks( body: &[u8], normalized_active_time: f32, elapsed_seconds: f32, ) -> Option { if !normalized_active_time.is_finite() || !elapsed_seconds.is_finite() { return None; } let position = sample_power_vector( body, TYPE2_POSITION_START_OFFSET, TYPE2_POSITION_END_OFFSET, [0x3c, 0x40, 0x44], normalized_active_time, )?; let axis = sample_linear_vector( body, TYPE2_AXIS_START_OFFSET, TYPE2_AXIS_END_OFFSET, normalized_active_time, )?; let scale = sample_power_vector( body, TYPE2_SCALE_START_OFFSET, TYPE2_SCALE_END_OFFSET, // 0x106C0 loads Z then Y then X from the packed reverse-order // exponent vector at body+0x78. [0x80, 0x7c, 0x78], normalized_active_time, )?; let (material_phase, opacity) = sample_op3_material_phase_and_opacity(body, normalized_active_time, elapsed_seconds)?; if !material_phase.is_finite() || !opacity.is_finite() { return None; } Some(Op3Type2Sample { position, axis, scale, opacity, material_phase, }) } fn sample_op3_material_phase_and_opacity( body: &[u8], normalized_active_time: f32, elapsed_seconds: f32, ) -> Option<(f32, f32)> { let raw_phase_start = read_f32(body, TYPE2_PHASE_START_OFFSET)?; let phase_end = read_f32(body, TYPE2_PHASE_END_OFFSET)?; let phase_exponent = read_f32(body, TYPE2_PHASE_EXPONENT_OFFSET)?; let opacity_start = read_f32(body, TYPE2_OPACITY_START_OFFSET)?; let opacity_end = read_f32(body, TYPE2_OPACITY_END_OFFSET)?; let opacity_exponent = read_f32(body, TYPE2_OPACITY_EXPONENT_OFFSET)?; if ![ raw_phase_start, phase_end, phase_exponent, opacity_start, opacity_end, opacity_exponent, normalized_active_time, elapsed_seconds, ] .iter() .all(|value| value.is_finite()) { return None; } let phase_start = raw_phase_start.max(0.0); let phase_delta = phase_end - phase_start; let phase_timebase = if raw_phase_start < 0.0 { elapsed_seconds } else { normalized_active_time }; let material_phase = fract(phase_start + phase_delta * phase_timebase.powf(phase_exponent)); let opacity = (opacity_start + (opacity_end - opacity_start) * normalized_active_time.powf(opacity_exponent)) .clamp(0.0, 1.0); (material_phase.is_finite() && opacity.is_finite()).then_some((material_phase, opacity)) } /// Builds the native Type2 Op3 quad matrix from its parent descriptor and /// already-sampled animated vectors. /// /// This matches 0x93D0's order: scale the parent basis by animated scale, /// multiply by D110's local basis, and then replace the translation with the /// unscaled parent transform of the animated position. #[must_use] fn native_op3_type2_matrix( world_from_effect: &[[f32; 4]; 4], sample: Op3Type2Sample, ) -> Option<[[f32; 4]; 4]> { if !world_from_effect .iter() .flatten() .all(|value| value.is_finite()) || !sample_is_finite(sample) { return None; } let local = native_d110_basis(sample.axis, sample.position)?; compose_native_effect_matrix(world_from_effect, &local, sample.position, sample.scale) } /// Builds the native Type 1 Op3 quad matrix from the parent transform, sampled /// tracks, and camera query point. #[must_use] fn native_op3_type1_matrix( world_from_effect: &[[f32; 4]; 4], sample: Op3Type1Sample, camera_world: [f32; 3], ) -> Option<[[f32; 4]; 4]> { if !world_from_effect .iter() .flatten() .all(|value| value.is_finite()) || !camera_world.iter().all(|value| value.is_finite()) || !sample_is_finite(sample) { return None; } let camera_local = native_effect_local_point(world_from_effect, camera_world)?; let view = sub3(camera_local, sample.position); let local_basis = native_op3_type1_basis(view, sample.axis, sample.position)?; compose_native_effect_matrix( world_from_effect, &local_basis, sample.position, sample.scale, ) } /// Native Effect.dll Type 1 basis. The parallel gate compares the raw /// `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. #[must_use] fn native_op3_type1_basis( view: [f32; 3], axis: [f32; 3], position: [f32; 3], ) -> Option<[[f32; 4]; 4]> { if !view .iter() .chain(axis.iter()) .chain(position.iter()) .all(|value| value.is_finite()) { return None; } let dot = view[0] * axis[0] + view[1] * axis[1] + view[2] * axis[2]; if !dot.is_finite() || dot.abs() == 1.0 { return None; } let mut u = cross3(view, axis); let length = length3(u)?; if length == 0.0 { return None; } for value in &mut u { *value /= length; } 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]], [0.0, 0.0, 0.0, 1.0], ]; result .iter() .flatten() .all(|value| value.is_finite()) .then_some(result) } fn interval_phase(phase: f32, start: f32, end: f32) -> f32 { if phase < start { return 0.0; } if phase > end { return 1.0; } if end == start { 1.0 } else if phase == start { 0.0 } else if phase == end { 1.0 } else { ((phase - start) / (end - start)).clamp(0.0, 1.0) } } fn sample_is_finite(sample: Op3Type2Sample) -> bool { sample .position .iter() .chain(sample.axis.iter()) .chain(sample.scale.iter()) .chain([&sample.opacity, &sample.material_phase]) .all(|value| value.is_finite()) } fn sample_linear_vector(body: &[u8], start: usize, end: usize, time: f32) -> Option<[f32; 3]> { let start = read_vec3(body, start)?; let end = read_vec3(body, end)?; if !start .iter() .chain(end.iter()) .all(|value| value.is_finite()) || !time.is_finite() { return None; } let values = std::array::from_fn(|axis| start[axis] + (end[axis] - start[axis]) * time); values .iter() .all(|value| value.is_finite()) .then_some(values) } fn sample_power_vector( body: &[u8], start: usize, end: usize, exponent_offsets: [usize; 3], time: f32, ) -> Option<[f32; 3]> { let start = read_vec3(body, start)?; let end = read_vec3(body, end)?; let exponents = [ read_f32(body, exponent_offsets[0])?, read_f32(body, exponent_offsets[1])?, read_f32(body, exponent_offsets[2])?, ]; if !start .iter() .chain(end.iter()) .chain(exponents.iter()) .all(|value| value.is_finite()) || !time.is_finite() { return None; } let values = std::array::from_fn(|axis| { let amount = time.powf(exponents[axis]); start[axis] + (end[axis] - start[axis]) * amount }); values .iter() .all(|value| value.is_finite()) .then_some(values) } fn read_vec3(body: &[u8], offset: usize) -> Option<[f32; 3]> { Some([ read_f32(body, offset)?, read_f32(body, offset + 4)?, read_f32(body, offset + 8)?, ]) } fn cross3(left: [f32; 3], right: [f32; 3]) -> [f32; 3] { [ left[1] * right[2] - left[2] * right[1], left[2] * right[0] - left[0] * right[2], left[0] * right[1] - left[1] * right[0], ] } fn sub3(left: [f32; 3], right: [f32; 3]) -> [f32; 3] { [left[0] - right[0], left[1] - right[1], left[2] - right[2]] } fn length3(vector: [f32; 3]) -> Option { let length = (vector[0] * vector[0] + vector[1] * vector[1] + vector[2] * vector[2]).sqrt(); length.is_finite().then_some(length) } fn fract(value: f32) -> f32 { value - value.floor() } fn read_resource(command: &FxCommand) -> Option { let bytes = command .raw_body .get(TYPE2_RESOURCE_OFFSET..TYPE2_RESOURCE_OFFSET + TYPE2_RESOURCE_BYTES)?; let archive = bounded_name(&bytes[..32])?; let name = bounded_name(&bytes[32..])?; (!archive.is_empty() && !name.is_empty()).then_some(EffectResource { archive, name }) } fn bounded_name(bytes: &[u8]) -> Option { let end = bytes .iter() .position(|byte| *byte == 0) .unwrap_or(bytes.len()); let value = std::str::from_utf8(&bytes[..end]).ok()?.trim(); (!value.is_empty()).then(|| value.to_owned()) } fn read_u32(bytes: &[u8], offset: usize) -> Option { Some(u32::from_le_bytes( bytes.get(offset..offset + 4)?.try_into().ok()?, )) } fn read_f32(bytes: &[u8], offset: usize) -> Option { Some(f32::from_le_bytes( bytes.get(offset..offset + 4)?.try_into().ok()?, )) } #[cfg(test)] mod tests { use super::*; use std::sync::Arc; #[test] fn type0_command_samples_position_and_scale_tracks_for_camera_facing_quad() { let mut body = vec![0_u8; 196]; write_u32(&mut body, 0, TYPE0_BODY_DISCRIMINATOR); write_f32(&mut body, TYPE2_PHASE_START_OFFSET, -1.0); write_f32(&mut body, TYPE2_PHASE_END_OFFSET, 1.0); write_f32(&mut body, TYPE2_PHASE_EXPONENT_OFFSET, 1.0); write_f32(&mut body, TYPE2_OPACITY_START_OFFSET, 1.0); write_f32(&mut body, TYPE2_OPACITY_END_OFFSET, 0.0); write_f32(&mut body, TYPE2_OPACITY_EXPONENT_OFFSET, 1.0); write_f32(&mut body, TYPE2_ACTIVE_START_OFFSET, 0.0); write_f32(&mut body, TYPE2_ACTIVE_END_OFFSET, 1.0); write_vec3(&mut body, TYPE2_POSITION_START_OFFSET, [0.0, 0.0, 0.0]); write_vec3(&mut body, TYPE2_POSITION_END_OFFSET, [2.0, 4.0, 6.0]); write_vec3(&mut body, TYPE2_POSITION_EXPONENT_OFFSET, [1.0; 3]); write_vec3(&mut body, TYPE2_SCALE_START_OFFSET, [1.0; 3]); write_vec3(&mut body, TYPE2_SCALE_END_OFFSET, [3.0, 4.0, 5.0]); write_vec3(&mut body, 0x78, [3.0, 2.0, 1.0]); body[TYPE2_RESOURCE_OFFSET..TYPE2_RESOURCE_OFFSET + 11].copy_from_slice(b"effects.rlb"); body[TYPE2_RESOURCE_OFFSET + 32..TYPE2_RESOURCE_OFFSET + 32 + 3].copy_from_slice(b"sun"); let command = FxCommand { word: 3, opcode: FxOpcode::Op3, native_flag: false, raw_body: Arc::from(body.into_boxed_slice()), resource_refs: Vec::new(), }; let identity = [ [1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], ]; let sample = sample_op3_type0_body(&command.raw_body, 0.5, 0.25).unwrap(); assert_eq!(sample.position, [1.0, 2.0, 3.0]); assert_eq!(sample.scale, [2.0, 1.75, 1.5]); assert_eq!(sample.opacity, 0.5); assert_eq!(sample.material_phase, 0.25); let frame = evaluate_op3_type0_command(&command, 7, &identity, 0.25, 0.5, [1.0, 2.0, 13.0]) .unwrap(); assert_eq!(frame.command_index, 7); assert_eq!(frame.material.name, "sun"); assert_eq!(frame.normalized_time, 0.5); assert_eq!(frame.material_phase, 0.25); assert_eq!(frame.color[3], 0.5); assert!(frame.active); let center = std::array::from_fn::<_, 3, _>(|axis| { frame .vertices .iter() .map(|vertex| vertex[axis]) .sum::() / 4.0 }); assert_eq!(center, [1.0, 2.0, 3.0]); assert_ne!(frame.vertices[0], frame.vertices[1]); assert!( evaluate_op3_type0_command(&command, 7, &identity, 0.25, 1.1, [1.0, 2.0, 13.0],) .is_some_and(|frame| !frame.active) ); assert!( evaluate_op3_type0_command(&command, 7, &identity, 0.25, 0.5, [1.0, 2.0, 3.0],) .is_none() ); } #[test] fn type2_matrix_scales_parent_basis_then_overrides_translation() { let parent = [ [2.0, 0.0, 0.0, 10.0], [0.0, 3.0, 0.0, 20.0], [0.0, 0.0, 4.0, 30.0], [0.0, 0.0, 0.0, 1.0], ]; let sample = Op3Type2Sample { position: [1.0, 2.0, 3.0], axis: [1.0, 2.0, 2.0], scale: [2.0, 3.0, 4.0], opacity: 1.0, material_phase: 0.0, }; let result = native_op3_type2_matrix(&parent, sample).unwrap(); assert_eq!(result[0][3], 12.0); assert_eq!(result[1][3], 26.0); assert_eq!(result[2][3], 42.0); assert_eq!(result[0][0], -8.0 / 5.0_f32.sqrt()); assert_eq!(result[1][0], 9.0 / 5.0_f32.sqrt()); assert_eq!(result[3], [0.0, 0.0, 0.0, 1.0]); } #[test] fn type2_command_keeps_inclusive_single_phase_endpoint() { let mut body = vec![0_u8; 196]; body[0..4].copy_from_slice(&TYPE2_BODY_DISCRIMINATOR.to_le_bytes()); body[TYPE2_ACTIVE_START_OFFSET..TYPE2_ACTIVE_START_OFFSET + 4] .copy_from_slice(&1.0_f32.to_le_bytes()); body[TYPE2_ACTIVE_END_OFFSET..TYPE2_ACTIVE_END_OFFSET + 4] .copy_from_slice(&1.0_f32.to_le_bytes()); body[TYPE2_RESOURCE_OFFSET..TYPE2_RESOURCE_OFFSET + 11].copy_from_slice(b"effects.rlb"); body[TYPE2_RESOURCE_OFFSET + 32..TYPE2_RESOURCE_OFFSET + 36].copy_from_slice(b"lamp"); let command = FxCommand { word: 3, opcode: FxOpcode::Op3, native_flag: false, raw_body: Arc::from(body.into_boxed_slice()), resource_refs: Vec::new(), }; let identity = [ [1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], ]; let frame = evaluate_op3_type2_command(&command, 0, &identity, 1.0, 1.0, 0).unwrap(); assert_eq!( frame.material, EffectResource { archive: "effects.rlb".to_owned(), name: "lamp".to_owned(), } ); assert!(frame.active); assert_eq!(frame.normalized_time, 1.0); let before = evaluate_op3_type2_command(&command, 0, &identity, 0.99, 0.99, 0).unwrap(); assert!(!before.active); } #[test] fn samples_verified_type2_position_axis_scale_phase_and_opacity_tracks() { let mut body = vec![0_u8; 196]; write_u32(&mut body, 0, TYPE2_BODY_DISCRIMINATOR); write_f32(&mut body, TYPE2_PHASE_START_OFFSET, -1.0); write_f32(&mut body, TYPE2_PHASE_END_OFFSET, 9.0); write_f32(&mut body, TYPE2_PHASE_EXPONENT_OFFSET, 1.0); write_f32(&mut body, TYPE2_OPACITY_START_OFFSET, 2.0); write_f32(&mut body, TYPE2_OPACITY_END_OFFSET, -1.0); write_f32(&mut body, TYPE2_OPACITY_EXPONENT_OFFSET, 1.0); write_vec3(&mut body, TYPE2_POSITION_START_OFFSET, [0.0, 0.0, 0.0]); write_vec3(&mut body, TYPE2_POSITION_END_OFFSET, [8.0, 16.0, 24.0]); write_vec3(&mut body, TYPE2_POSITION_EXPONENT_OFFSET, [1.0, 2.0, 3.0]); write_vec3(&mut body, TYPE2_AXIS_START_OFFSET, [1.0, 2.0, 3.0]); write_vec3(&mut body, TYPE2_AXIS_END_OFFSET, [5.0, 6.0, 7.0]); write_vec3(&mut body, TYPE2_SCALE_START_OFFSET, [1.0; 3]); write_vec3(&mut body, TYPE2_SCALE_END_OFFSET, [3.0; 3]); write_f32(&mut body, 0x78, 3.0); write_f32(&mut body, 0x7c, 2.0); write_f32(&mut body, 0x80, 1.0); let sample = sample_op3_type2_body(&body, 0.5, 0.25).unwrap(); assert_eq!(sample.position, [4.0, 4.0, 3.0]); assert_eq!(sample.axis, [3.0, 4.0, 5.0]); assert_eq!(sample.scale, [2.0, 1.5, 1.25]); assert_eq!(sample.opacity, 0.5); assert_eq!(sample.material_phase, 0.25); } #[test] fn type1_basis_uses_raw_exact_dot_gate_and_native_cross_order() { // The native comparison is against the raw dot product, so this // oblique pair is rejected even though the vectors are not parallel. assert_eq!( native_op3_type1_basis([1.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0; 3]), None ); assert_eq!( native_op3_type1_basis([1.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0; 3]), None ); assert_eq!( native_op3_type1_basis([1.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [0.0; 3]), None ); // A non-unit view gives a raw dot of 2 and proceeds to the native // 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]); // 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!( native_op3_type1_basis([f32::NAN, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0; 3]), None ); } #[test] fn type1_command_samples_authored_tracks_and_camera_basis() { let mut body = vec![0_u8; 196]; write_u32(&mut body, 0, TYPE1_BODY_DISCRIMINATOR); write_f32(&mut body, TYPE2_PHASE_START_OFFSET, -1.0); write_f32(&mut body, TYPE2_PHASE_END_OFFSET, 2.0); write_f32(&mut body, TYPE2_PHASE_EXPONENT_OFFSET, 1.0); write_f32(&mut body, TYPE2_OPACITY_START_OFFSET, 1.0); write_f32(&mut body, TYPE2_OPACITY_END_OFFSET, 0.0); write_f32(&mut body, TYPE2_OPACITY_EXPONENT_OFFSET, 1.0); write_f32(&mut body, TYPE2_ACTIVE_START_OFFSET, 0.0); write_f32(&mut body, TYPE2_ACTIVE_END_OFFSET, 1.0); write_vec3(&mut body, TYPE2_POSITION_START_OFFSET, [0.0, 0.0, 0.0]); write_vec3(&mut body, TYPE2_POSITION_END_OFFSET, [2.0, 0.0, 0.0]); write_vec3(&mut body, TYPE2_POSITION_EXPONENT_OFFSET, [1.0; 3]); write_vec3(&mut body, TYPE2_AXIS_START_OFFSET, [0.0, 0.0, 1.0]); write_vec3(&mut body, TYPE2_AXIS_END_OFFSET, [0.0, 0.0, 1.0]); write_vec3(&mut body, TYPE2_SCALE_START_OFFSET, [1.0; 3]); write_vec3(&mut body, TYPE2_SCALE_END_OFFSET, [1.0; 3]); write_vec3(&mut body, 0x78, [1.0; 3]); body[TYPE2_RESOURCE_OFFSET..TYPE2_RESOURCE_OFFSET + 12].copy_from_slice(b"material.lib"); body[TYPE2_RESOURCE_OFFSET + 32..TYPE2_RESOURCE_OFFSET + 37].copy_from_slice(b"b_p08"); let command = FxCommand { word: 3, opcode: FxOpcode::Op3, native_flag: false, raw_body: Arc::from(body.into_boxed_slice()), resource_refs: Vec::new(), }; let identity = [ [1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], ]; let frame = evaluate_op3_type1_command(&command, 4, &identity, 0.25, 0.5, [2.0, 1.0, 3.0]).unwrap(); assert_eq!(frame.command_index, 4); assert_eq!( frame.material, EffectResource { archive: "material.lib".to_owned(), name: "b_p08".to_owned(), } ); assert!(frame.active); 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); assert!(evaluate_op3_type1_command( &command, 4, &identity, 0.25, 0.5, [f32::NAN, 1.0, 3.0], ) .is_none()); } #[test] fn rejects_truncated_bodies_and_non_finite_tracks_or_inputs() { let identity = [ [1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], ]; let truncated = FxCommand { word: 3, opcode: FxOpcode::Op3, native_flag: false, raw_body: Arc::from( vec![0_u8; TYPE2_RESOURCE_OFFSET + TYPE2_RESOURCE_BYTES - 1].into_boxed_slice(), ), resource_refs: Vec::new(), }; assert!( evaluate_op3_type0_command(&truncated, 0, &identity, 0.0, 0.0, [0.0; 3],).is_none() ); let mut type0_body = vec![0_u8; TYPE2_RESOURCE_OFFSET + TYPE2_RESOURCE_BYTES]; write_u32(&mut type0_body, 0, TYPE0_BODY_DISCRIMINATOR); write_f32( &mut type0_body, TYPE2_POSITION_EXPONENT_OFFSET, f32::INFINITY, ); assert!(sample_op3_type0_body(&type0_body, 0.5, 0.25).is_none()); let mut type2_body = vec![0_u8; TYPE2_RESOURCE_OFFSET + TYPE2_RESOURCE_BYTES]; write_u32(&mut type2_body, 0, TYPE2_BODY_DISCRIMINATOR); let non_finite_elapsed = FxCommand { word: 3, opcode: FxOpcode::Op3, native_flag: false, raw_body: Arc::from(type2_body.into_boxed_slice()), resource_refs: Vec::new(), }; assert!(evaluate_op3_type2_command( &non_finite_elapsed, 0, &identity, f32::INFINITY, 0.5, 0, ) .is_none()); } fn write_u32(bytes: &mut [u8], offset: usize, value: u32) { bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); } fn write_f32(bytes: &mut [u8], offset: usize, value: f32) { bytes[offset..offset + 4].copy_from_slice(&value.to_le_bytes()); } fn write_vec3(bytes: &mut [u8], offset: usize, value: [f32; 3]) { for (axis, component) in value.into_iter().enumerate() { write_f32(bytes, offset + axis * 4, component); } } }