bevy_director 0.5.0-dev

Unreal-Sequencer-inspired cinematic camera and sequence system for Bevy
Documentation
//! Handheld: a seeded, deterministic wobble layered onto the evaluated
//! pose. A sum of golden-ratio-detuned sines per axis, so it never
//! visibly repeats and never pulls in a rand crate. Same seed, same
//! take, every run.

use bevy::prelude::*;

use crate::sequence::Shake;

const PHI: f32 = 1.618_034;

/// Layered sines in -1..1, detuned so the pattern does not loop.
fn wobble(t: f32, hz: f32, phase: f32) -> f32 {
    let x = t * hz * std::f32::consts::TAU + phase;
    (x.sin() + (x * PHI).sin() * 0.5 + (x * PHI * PHI).sin() * 0.25) / 1.75
}

/// The additive offset at a shot-local time: a local-space position
/// nudge and a rotation to compose onto the aim.
pub(crate) fn shake_offset(shake: &Shake, t: f32) -> (Vec3, Quat) {
    let seed = shake.seed as f32 * 12.9898;
    let amp = shake.amplitude_deg.to_radians();
    let hz = shake.frequency_hz.max(0.01);
    let yaw = wobble(t, hz, seed) * amp;
    let pitch = wobble(t, hz * 1.13, seed + 7.31) * amp;
    // Roll reads twice as strong as it is; keep it half.
    let roll = wobble(t, hz * 0.87, seed + 3.77) * amp * 0.5;
    let pos = Vec3::new(
        wobble(t, hz * 0.71, seed + 1.13),
        wobble(t, hz * 0.79, seed + 9.02),
        0.0,
    ) * shake.pos_amplitude;
    (pos, Quat::from_euler(EulerRot::YXZ, yaw, pitch, roll))
}

#[cfg(test)]
mod tests {
    use super::*;

    fn shake(seed: u32) -> Shake {
        Shake {
            amplitude_deg: 1.5,
            frequency_hz: 9.0,
            seed,
            pos_amplitude: 0.02,
        }
    }

    #[test]
    fn same_seed_same_take() {
        for t in [0.0, 0.37, 2.11, 8.4] {
            assert_eq!(shake_offset(&shake(7), t), shake_offset(&shake(7), t));
        }
    }

    #[test]
    fn different_seeds_diverge_and_stay_bounded() {
        let mut differs = false;
        for i in 0..100 {
            let t = i as f32 * 0.05;
            let (pa, qa) = shake_offset(&shake(1), t);
            let (pb, qb) = shake_offset(&shake(2), t);
            differs |= pa.distance(pb) > 1e-4 || qa.angle_between(qb) > 1e-4;
            assert!(pa.length() <= 0.03, "position blew past its amplitude");
            assert!(qa.to_axis_angle().1 <= 3.0_f32.to_radians());
        }
        assert!(differs);
    }
}