feat(fx): evaluate native Op3 body types
Docs Deploy / Build and Deploy MkDocs (push) Successful in 33s

This commit is contained in:
Valentin Popov committed 2026-10-18 08:39:37 +04:00
1 parent dda322d8be
commit 3edf3dfc12
3 files changed
+995

No files matched your search

+984
View File
@@ -0,0 +1,984 @@
//! 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);
}
}
}
+1
View File
@@ -6,6 +6,7 @@
//! reference stub until opcode timing, gates, RNG, and command-body semantics
//! are backed by runtime-captured evidence.
pub mod ambient_op3;
pub mod atmosphere;
pub mod environment;
pub mod fx_visual;