Something went wrong. Try again.
Homebrew 4D Game Engine In Rust, inspired by Engine4D
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285//! Interpolation traits and utilities for 4D math types//!//! This module provides the [`Interpolatable`] trait for types that support//! linear interpolation (lerp), which is foundational for animation and tweening.
use crate::{Rotor4, Vec4};
/// Trait for types that can be linearly interpolated////// Linear interpolation smoothly blends between two values based on a/// parameter `t` where `t=0.0` returns `a` and `t=1.0` returns `b`.////// # Naming Convention////// This trait uses a **static method** signature: `Interpolatable::lerp(&a, &b, t)`./// This differs from the **instance method** signature used by [`Vec4::lerp`]:/// `a.lerp(b, t)`.////// The static signature was chosen because:/// - It works uniformly for all types (including primitives like `f32`)/// - It matches the mathematical notation `lerp(a, b, t)`/// - It avoids ambiguity about which value is "self" vs the target////// When using `Vec4` specifically, both APIs are available:/// ```ignore/// // Instance method (Vec4-specific)/// let result = a.lerp(b, t);////// // Static method (generic Interpolatable)/// let result = <Vec4 as Interpolatable>::lerp(&a, &b, t);/// let result = Interpolatable::lerp(&a, &b, t); // with type inference/// ```////// The `Interpolatable` implementation for `Vec4` delegates to `Vec4::lerp` internally,/// so both produce identical results.pub trait Interpolatable: Clone { /// Linear interpolation from `a` to `b` at parameter `t` /// /// # Arguments /// * `a` - The starting value (returned when t=0.0) /// * `b` - The ending value (returned when t=1.0) /// * `t` - Interpolation parameter, typically in range [0.0, 1.0] /// /// # Returns /// The interpolated value. When t is outside [0.0, 1.0], the result /// is extrapolated beyond the input values. fn lerp(a: &Self, b: &Self, t: f32) -> Self;}
impl Interpolatable for f32 { #[inline] fn lerp(a: &Self, b: &Self, t: f32) -> Self { a + (b - a) * t }}
impl Interpolatable for f64 { #[inline] fn lerp(a: &Self, b: &Self, t: f32) -> Self { a + (b - a) * (t as f64) }}
impl Interpolatable for Vec4 { #[inline] fn lerp(a: &Self, b: &Self, t: f32) -> Self { // Use Vec4's built-in lerp for efficiency a.lerp(*b, t) }}
impl Interpolatable for Rotor4 { /// Spherical linear interpolation (slerp) for rotors /// /// This interpolates between two rotations along the shortest path /// on the rotation hypersphere, maintaining constant angular velocity. fn lerp(a: &Self, b: &Self, t: f32) -> Self { // Compute dot product of rotor components (measures how similar they are) let mut dot = a.s * b.s + a.b_xy * b.b_xy + a.b_xz * b.b_xz + a.b_xw * b.b_xw + a.b_yz * b.b_yz + a.b_yw * b.b_yw + a.b_zw * b.b_zw + a.p * b.p;
// If dot is negative, negate one rotor to take the shorter path let (b_s, b_xy, b_xz, b_xw, b_yz, b_yw, b_zw, b_p) = if dot < 0.0 { dot = -dot; ( -b.s, -b.b_xy, -b.b_xz, -b.b_xw, -b.b_yz, -b.b_yw, -b.b_zw, -b.p, ) } else { (b.s, b.b_xy, b.b_xz, b.b_xw, b.b_yz, b.b_yw, b.b_zw, b.p) };
// If rotors are very close, use linear interpolation to avoid division by zero const THRESHOLD: f32 = 0.9995; if dot > THRESHOLD { // Linear interpolation and normalize let result = Rotor4 { s: a.s + t * (b_s - a.s), b_xy: a.b_xy + t * (b_xy - a.b_xy), b_xz: a.b_xz + t * (b_xz - a.b_xz), b_xw: a.b_xw + t * (b_xw - a.b_xw), b_yz: a.b_yz + t * (b_yz - a.b_yz), b_yw: a.b_yw + t * (b_yw - a.b_yw), b_zw: a.b_zw + t * (b_zw - a.b_zw), p: a.p + t * (b_p - a.p), }; return result.normalize(); }
// Standard slerp formula let theta = dot.acos(); let sin_theta = theta.sin(); let sin_t_theta = (t * theta).sin(); let sin_1mt_theta = ((1.0 - t) * theta).sin();
let scale_a = sin_1mt_theta / sin_theta; let scale_b = sin_t_theta / sin_theta;
// Normalize the result to prevent floating-point error accumulation over many slerp calls. // While a single slerp theoretically produces a unit rotor, repeated interpolations // (e.g., in animation chains or blending) can cause gradual denormalization. Rotor4 { s: scale_a * a.s + scale_b * b_s, b_xy: scale_a * a.b_xy + scale_b * b_xy, b_xz: scale_a * a.b_xz + scale_b * b_xz, b_xw: scale_a * a.b_xw + scale_b * b_xw, b_yz: scale_a * a.b_yz + scale_b * b_yz, b_yw: scale_a * a.b_yw + scale_b * b_yw, b_zw: scale_a * a.b_zw + scale_b * b_zw, p: scale_a * a.p + scale_b * b_p, } .normalize() }}
#[cfg(test)]mod tests { use super::*; use crate::RotationPlane; use std::f32::consts::PI;
const EPSILON: f32 = 0.001;
fn approx_eq(a: f32, b: f32) -> bool { (a - b).abs() < EPSILON }
fn vec_approx_eq(a: Vec4, b: Vec4) -> bool { approx_eq(a.x, b.x) && approx_eq(a.y, b.y) && approx_eq(a.z, b.z) && approx_eq(a.w, b.w) }
#[test] fn test_f32_lerp() { assert!(approx_eq(f32::lerp(&0.0, &10.0, 0.0), 0.0)); assert!(approx_eq(f32::lerp(&0.0, &10.0, 0.5), 5.0)); assert!(approx_eq(f32::lerp(&0.0, &10.0, 1.0), 10.0)); }
#[test] fn test_f32_lerp_extrapolation() { // Beyond bounds should extrapolate assert!(approx_eq(f32::lerp(&0.0, &10.0, -0.5), -5.0)); assert!(approx_eq(f32::lerp(&0.0, &10.0, 1.5), 15.0)); }
#[test] fn test_vec4_lerp_boundaries() { let a = Vec4::new(0.0, 0.0, 0.0, 0.0); let b = Vec4::new(10.0, 20.0, 30.0, 40.0);
let at_zero = <Vec4 as Interpolatable>::lerp(&a, &b, 0.0); assert!(vec_approx_eq(at_zero, a));
let at_one = <Vec4 as Interpolatable>::lerp(&a, &b, 1.0); assert!(vec_approx_eq(at_one, b)); }
#[test] fn test_vec4_lerp_midpoint() { let a = Vec4::new(0.0, 0.0, 0.0, 0.0); let b = Vec4::new(10.0, 20.0, 30.0, 40.0);
let mid = <Vec4 as Interpolatable>::lerp(&a, &b, 0.5); assert!(approx_eq(mid.x, 5.0)); assert!(approx_eq(mid.y, 10.0)); assert!(approx_eq(mid.z, 15.0)); assert!(approx_eq(mid.w, 20.0)); }
#[test] fn test_rotor4_lerp_identity() { let a = Rotor4::IDENTITY; let b = Rotor4::from_plane_angle(RotationPlane::XY, PI / 2.0);
// At t=0 should be close to identity let at_zero = Rotor4::lerp(&a, &b, 0.0); assert!(approx_eq(at_zero.s, a.s), "s: {} vs {}", at_zero.s, a.s);
// At t=1 should be close to b let at_one = Rotor4::lerp(&a, &b, 1.0); let v = Vec4::X; let expected = b.rotate(v); let actual = at_one.rotate(v); assert!( vec_approx_eq(actual, expected), "expected {:?}, got {:?}", expected, actual ); }
#[test] fn test_rotor4_lerp_halfway() { let a = Rotor4::IDENTITY; let b = Rotor4::from_plane_angle(RotationPlane::XY, PI / 2.0);
// Halfway should give 45 degree rotation let mid = Rotor4::lerp(&a, &b, 0.5); let v = Vec4::X; let rotated = mid.rotate(v);
// 45 degrees in XY should put X at (cos45, sin45, 0, 0) let expected_x = (PI / 4.0).cos(); let expected_y = (PI / 4.0).sin(); assert!( approx_eq(rotated.x, expected_x), "x: {} vs {}", rotated.x, expected_x ); assert!( approx_eq(rotated.y, expected_y), "y: {} vs {}", rotated.y, expected_y ); }
#[test] fn test_rotor4_lerp_short_path() { // Test that slerp takes the short path // Rotation of -90 degrees should interpolate through 0, not through 270 let a = Rotor4::IDENTITY; let b = Rotor4::from_plane_angle(RotationPlane::XY, -PI / 2.0);
let mid = Rotor4::lerp(&a, &b, 0.5); let v = Vec4::X; let rotated = mid.rotate(v);
// Should be at -45 degrees, not 135 degrees let expected_x = (PI / 4.0).cos(); let expected_y = -(PI / 4.0).sin(); assert!( approx_eq(rotated.x, expected_x), "x: {} vs {}", rotated.x, expected_x ); assert!( approx_eq(rotated.y, expected_y), "y: {} vs {}", rotated.y, expected_y ); }
#[test] fn test_rotor4_lerp_preserves_unit() { let a = Rotor4::from_plane_angle(RotationPlane::XZ, 0.3); let b = Rotor4::from_plane_angle(RotationPlane::YW, 0.7);
for i in 0..=10 { let t = i as f32 / 10.0; let result = Rotor4::lerp(&a, &b, t); let mag = result.magnitude(); assert!(approx_eq(mag, 1.0), "magnitude at t={}: {}", t, mag); } }}