Files
fparkan/crates/fparkan-fx/src/ambient_op3.rs
T
Valentin Popov 3edf3dfc12
Docs Deploy / Build and Deploy MkDocs (push) Successful in 33s
feat(fx): evaluate native Op3 body types
2026-10-18 08:39:37 +04:00

985 lines
34 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<EffectQuadFrame> {
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<EffectQuadFrame> {
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<EffectQuadFrame> {
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<Op3Type2Sample> {
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<Op3Type0Sample> {
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<Op3Type1Sample> {
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<Op3Type2Sample> {
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<f32> {
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<EffectResource> {
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<String> {
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<u32> {
Some(u32::from_le_bytes(
bytes.get(offset..offset + 4)?.try_into().ok()?,
))
}
fn read_f32(bytes: &[u8], offset: usize) -> Option<f32> {
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::<f32>()
/ 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);
}
}
}