Skip to main content

primase_core/particles/
motion.rs

1//! How an emitter's own motion ([`NodeMotion`]) moves it over its active time: [`NodeMotion::advance`] steps a
2//! [`MotionState`], so every renderer that plays a description moves its emitters alike.
3
4use super::{NodeMotion, Value, unit};
5
6/// Where an emitter's motion has taken it, in its parent's frame: what [`NodeMotion::advance`] steps. The emitter's
7/// frame is its parent's moved by `travelled`, turned by `facing`, then placed (its jitter and placement) and turned by
8/// `turned`.
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub struct MotionState {
11    /// The position its motion has reached (metres).
12    pub travelled: [f32; 3],
13    /// The speed along its forward gained by acceleration (metres per second), within its speed limits.
14    pub gained: f32,
15    /// The speed along its lift axis gained by lift (metres per second).
16    pub lifted: f32,
17    /// Its turn about its own axes by its spin and random turns: a unit quaternion `[x, y, z, w]`.
18    pub turned: [f32; 4],
19    /// The turn of its whole frame, in its parent's, that faces its forward along its velocity (identity unless it
20    /// aligns with its motion): a unit quaternion `[x, y, z, w]`.
21    pub facing: [f32; 4],
22    /// The random turns taken, and the active time the next is due at (seconds).
23    pub turns: u32,
24    pub next_turn: f32,
25}
26
27impl Default for MotionState {
28    /// Not moved: at the origin, at rest, unturned, the first random turn due at once.
29    fn default() -> MotionState {
30        MotionState {
31            travelled: [0.0; 3],
32            gained: 0.0,
33            lifted: 0.0,
34            turned: IDENTITY,
35            facing: IDENTITY,
36            turns: 0,
37            next_turn: 0.0,
38        }
39    }
40}
41
42/// The random stream an emitter's random turns draw on (turn `n` on this plus `n % 64`), with the scope
43/// [`TURN_SCOPE`]: above any a source's property modifiers name.
44const TURN_STREAM: u32 = 0x4000_0000 + 20;
45/// The scope of the random turns' draws: one an emitter's activation count does not reach.
46const TURN_SCOPE: u64 = u64::MAX - 1;
47/// How far before a random turn's time the active time may be and still take it (seconds): a time stepped in
48/// fractions of a second lands within a rounding of the time a turn is due at.
49const TURN_TOLERANCE: f32 = 1e-4;
50
51const IDENTITY: [f32; 4] = [0.0, 0.0, 0.0, 1.0];
52
53/// Whose motion [`NodeMotion::advance`] steps: the system's seed and the emitter's number, which key its random draws,
54/// and the system's inputs its values may read.
55#[derive(Clone, Copy)]
56pub struct Mover<'a> {
57    pub seed: u64,
58    pub emitter: u32,
59    pub input: &'a dyn Fn(u32) -> f32,
60}
61
62impl std::fmt::Debug for Mover<'_> {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        f.debug_struct("Mover")
65            .field("seed", &self.seed)
66            .field("emitter", &self.emitter)
67            .finish_non_exhaustive()
68    }
69}
70
71impl NodeMotion {
72    /// Moves `mover`'s `state` on by `dt` seconds, ending at the active time `active` (seconds), the values read at
73    /// `active` (random variation drawn in scope 0). `placement` is
74    /// the rotation of the emitter's placement in its parent's frame (a unit quaternion `[x, y, z, w]`): the forward and
75    /// a lift not in the world turn with it.
76    ///
77    /// The speed along the forward is `speed` times its multiplier plus what acceleration times its multiplier has
78    /// added, held within `min_speed` and `max_speed`; lift adds speed along +Y (the world's, or the placement's).
79    /// The position moves by both, and a displacement, where there is one, replaces it. Spin turns the emitter about
80    /// its own axes (Y, then X, then Z), and each random turn, every `interval` seconds from the start, turns its
81    /// forward by up to `max_angle` about a random axis across it. An emitter that aligns with its motion faces its
82    /// forward along its velocity (`facing`); one at rest keeps the facing it had. Drag is how the particles follow the
83    /// emitter's motion, not the motion itself, and is not stepped here.
84    pub fn advance(
85        &self,
86        state: &mut MotionState,
87        mover: Mover<'_>,
88        active: f32,
89        dt: f32,
90        placement: [f32; 4],
91    ) {
92        let Mover { seed, emitter, input } = mover;
93        let ev = |v: &Value| v.eval(active, seed, emitter, 0, input);
94        let speed = ev(&self.speed)[0] * ev(&self.speed_multiplier)[0];
95        let acceleration = ev(&self.acceleration)[0] * ev(&self.acceleration_multiplier)[0];
96        let limited = self.limit(speed + state.gained + acceleration * dt);
97        state.gained = limited - speed;
98        let along = rotate(mul(placement, state.turned), self.forward);
99        state.lifted += ev(&self.lift)[0] * dt;
100        let up = if self.lift_in_world {
101            [0.0, 1.0, 0.0]
102        } else {
103            rotate(placement, [0.0, 1.0, 0.0])
104        };
105        let velocity: [f32; 3] = std::array::from_fn(|k| along[k] * limited + up[k] * state.lifted);
106        for (t, v) in state.travelled.iter_mut().zip(velocity) {
107            *t += v * dt;
108        }
109        if let Some(d) = &self.displacement {
110            let v = ev(d);
111            state.travelled = [v[0], v[1], v[2]];
112        }
113        let spin: [f32; 3] = std::array::from_fn(|k| ev(&self.spin[k])[0] * ev(&self.spin_multiplier[k])[0]);
114        if spin != [0.0; 3] {
115            let step = euler_yxz(
116                (spin[1] * dt).to_radians(),
117                (spin[0] * dt).to_radians(),
118                (spin[2] * dt).to_radians(),
119            );
120            state.turned = normalize(mul(state.turned, step));
121        }
122        if let Some(t) = &self.turns
123            && t.interval > 0.0
124            && active + TURN_TOLERANCE >= state.next_turn
125        {
126            let back = [0.0, 0.0, -1.0];
127            let turned = random_turn(back, ev(&t.max_angle)[0], seed, emitter, state.turns);
128            state.turned = normalize(mul(state.turned, arc(back, turned)));
129            state.turns += 1;
130            state.next_turn = active + t.interval;
131        }
132        if self.align {
133            let length = velocity.iter().map(|v| v * v).sum::<f32>().sqrt();
134            if length > 1e-6 {
135                let heading = velocity.map(|v| v / length);
136                state.facing = arc(along, heading);
137            }
138        }
139    }
140
141    /// `speed` held within the motion's limits.
142    fn limit(&self, speed: f32) -> f32 {
143        let speed = self.min_speed.map_or(speed, |min| speed.max(min));
144        self.max_speed.map_or(speed, |max| speed.min(max))
145    }
146}
147
148/// `v` turned by up to `max_degrees` about a random axis across it, from the draws of turn number `n`.
149fn random_turn(v: [f32; 3], max_degrees: f32, seed: u64, emitter: u32, n: u32) -> [f32; 3] {
150    let draw = |component| unit(seed, emitter, TURN_SCOPE, TURN_STREAM + n % 64, component);
151    let (t1, t2) = basis(v);
152    let alpha = max_degrees.to_radians() * draw(1);
153    let phi = std::f32::consts::TAU * draw(2);
154    let turned: [f32; 3] =
155        std::array::from_fn(|k| v[k] * alpha.cos() + (t1[k] * phi.cos() + t2[k] * phi.sin()) * alpha.sin());
156    let length = turned.iter().map(|x| x * x).sum::<f32>().sqrt();
157    if length.is_finite() && length > 0.0 {
158        turned.map(|x| x / length)
159    } else {
160        v
161    }
162}
163
164/// Two unit vectors across `dir` and across each other.
165fn basis(dir: [f32; 3]) -> ([f32; 3], [f32; 3]) {
166    let dir = unit_or_zero(dir);
167    let helper = if dir[1].abs() < 0.9 {
168        [0.0, 1.0, 0.0]
169    } else {
170        [1.0, 0.0, 0.0]
171    };
172    let t1 = unit_or_zero(cross(dir, helper));
173    let t2 = unit_or_zero(cross(dir, t1));
174    (t1, t2)
175}
176
177fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
178    [
179        a[1] * b[2] - a[2] * b[1],
180        a[2] * b[0] - a[0] * b[2],
181        a[0] * b[1] - a[1] * b[0],
182    ]
183}
184
185fn dot(a: [f32; 3], b: [f32; 3]) -> f32 {
186    a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
187}
188
189fn unit_or_zero(v: [f32; 3]) -> [f32; 3] {
190    let length = dot(v, v).sqrt();
191    if length.is_finite() && length > 0.0 {
192        v.map(|x| x / length)
193    } else {
194        [0.0; 3]
195    }
196}
197
198/// The product `a b` of two quaternions `[x, y, z, w]`: `b`'s turn, then `a`'s.
199fn mul(a: [f32; 4], b: [f32; 4]) -> [f32; 4] {
200    let [ax, ay, az, aw] = a;
201    let [bx, by, bz, bw] = b;
202    [
203        aw * bx + ax * bw + ay * bz - az * by,
204        aw * by - ax * bz + ay * bw + az * bx,
205        aw * bz + ax * by - ay * bx + az * bw,
206        aw * bw - ax * bx - ay * by - az * bz,
207    ]
208}
209
210fn normalize(q: [f32; 4]) -> [f32; 4] {
211    let length = q.iter().map(|x| x * x).sum::<f32>().sqrt();
212    if length.is_finite() && length > 0.0 {
213        q.map(|x| x / length)
214    } else {
215        IDENTITY
216    }
217}
218
219/// `v` turned by the unit quaternion `q`.
220fn rotate(q: [f32; 4], v: [f32; 3]) -> [f32; 3] {
221    let u = [q[0], q[1], q[2]];
222    let w = q[3];
223    let uv = cross(u, v);
224    let uuv = cross(u, uv);
225    std::array::from_fn(|k| v[k] + 2.0 * (w * uv[k] + uuv[k]))
226}
227
228/// The turn about Y by `y`, then X by `x`, then Z by `z` (radians), applied to a vector Z first: `qy qx qz`.
229fn euler_yxz(y: f32, x: f32, z: f32) -> [f32; 4] {
230    let about = |axis: usize, angle: f32| {
231        let mut q = [0.0, 0.0, 0.0, (angle / 2.0).cos()];
232        q[axis] = (angle / 2.0).sin();
233        q
234    };
235    mul(mul(about(1, y), about(0, x)), about(2, z))
236}
237
238/// The shortest turn taking the unit vector `from` to the unit vector `to`; opposite ones turn half about an axis
239/// across `from`.
240fn arc(from: [f32; 3], to: [f32; 3]) -> [f32; 4] {
241    let d = dot(from, to);
242    if d < -1.0 + 1e-6 {
243        let (axis, _) = basis(from);
244        return [axis[0], axis[1], axis[2], 0.0];
245    }
246    let c = cross(from, to);
247    let s = (2.0 * (1.0 + d)).sqrt();
248    [c[0] / s, c[1] / s, c[2] / s, s / 2.0]
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::particles::{Turns, Value};
255
256    fn v(x: f32) -> Value {
257        Value::constant(&[x])
258    }
259
260    fn motion() -> NodeMotion {
261        NodeMotion {
262            forward: [0.0, 0.0, -1.0],
263            speed: v(0.0),
264            speed_multiplier: v(1.0),
265            acceleration: v(0.0),
266            acceleration_multiplier: v(1.0),
267            lift: v(0.0),
268            lift_in_world: false,
269            displacement: None,
270            spin: [v(0.0), v(0.0), v(0.0)],
271            spin_multiplier: [v(1.0), v(1.0), v(1.0)],
272            align: false,
273            turns: None,
274            drag: None,
275            min_speed: None,
276            max_speed: None,
277        }
278    }
279
280    /// Plays `m` for `seconds` in steps of 1/120 s from active time 0, from rest.
281    fn play(m: &NodeMotion, seconds: f32) -> MotionState {
282        let mut s = MotionState::default();
283        let mover = Mover {
284            seed: 7,
285            emitter: 0,
286            input: &|_| 0.0,
287        };
288        let dt = 1.0 / 120.0;
289        let steps = (seconds / dt).round() as u32;
290        for i in 0..steps {
291            m.advance(&mut s, mover, i as f32 * dt, dt, IDENTITY);
292        }
293        s
294    }
295
296    fn close(a: [f32; 3], b: [f32; 3], tolerance: f32) -> bool {
297        a.iter().zip(b).all(|(x, y)| (x - y).abs() <= tolerance)
298    }
299
300    #[test]
301    fn acceleration_is_held_within_the_speed_limits() {
302        // from 6 m/s at 40 m/s² up to 20 m/s: 0.35 s to reach it, then on at 20
303        let mut m = motion();
304        m.acceleration = v(40.0);
305        m.min_speed = Some(6.0);
306        m.max_speed = Some(20.0);
307        let s = play(&m, 1.0);
308        assert!((s.gained - 20.0).abs() < 1e-3, "{}", s.gained);
309        // 6*0.35 + 40*0.35²/2 + 20*0.65 = 2.1 + 2.45 + 13
310        assert!(close(s.travelled, [0.0, 0.0, -17.55], 0.2), "{:?}", s.travelled);
311        // a deceleration from the limit answers at once: it does not first spend what was clamped away
312        let mut m = motion();
313        m.speed = v(30.0);
314        m.max_speed = Some(10.0);
315        let s = play(&m, 1.0);
316        assert!(close(s.travelled, [0.0, 0.0, -10.0], 1e-3), "{:?}", s.travelled);
317    }
318
319    #[test]
320    fn an_aligned_emitter_faces_its_curving_path_and_one_not_aligned_does_not() {
321        let mut m = motion();
322        m.speed = v(10.0);
323        m.lift = v(-10.0);
324        m.lift_in_world = true;
325        let s = play(&m, 1.0);
326        assert_eq!(s.facing, IDENTITY);
327        m.align = true;
328        let s = play(&m, 1.0);
329        // after 1 s the velocity is (0, -10, -10): the forward is pitched 45 degrees down
330        let forward = rotate(s.facing, [0.0, 0.0, -1.0]);
331        let half = std::f32::consts::FRAC_1_SQRT_2;
332        assert!(close(forward, [0.0, -half, -half], 1e-2), "{forward:?}");
333    }
334
335    #[test]
336    fn random_turns_come_every_interval_and_are_the_same_each_play() {
337        let mut m = motion();
338        m.speed = v(1.0);
339        m.turns = Some(Turns {
340            interval: 0.1,
341            max_angle: v(30.0),
342        });
343        let a = play(&m, 1.0);
344        // turns at 0.0, 0.1, ..., 0.9: each when the active time, in steps of 1/120 s, reaches the time it is due at
345        assert_eq!(a.turns, 10);
346        assert_eq!(a, play(&m, 1.0));
347        assert_ne!(a.turned, IDENTITY);
348    }
349
350    #[test]
351    fn a_displacement_places_the_emitter_where_it_says() {
352        let mut m = motion();
353        m.speed = v(5.0);
354        m.displacement = Some(Value::constant(&[1.0, 2.0, 3.0]));
355        assert_eq!(play(&m, 0.5).travelled, [1.0, 2.0, 3.0]);
356    }
357
358    #[test]
359    fn quaternion_helpers_agree_with_a_quarter_turn() {
360        let q = euler_yxz(std::f32::consts::FRAC_PI_2, 0.0, 0.0);
361        assert!(close(rotate(q, [0.0, 0.0, -1.0]), [-1.0, 0.0, 0.0], 1e-6));
362        let a = arc([0.0, 0.0, -1.0], [1.0, 0.0, 0.0]);
363        assert!(close(rotate(a, [0.0, 0.0, -1.0]), [1.0, 0.0, 0.0], 1e-6));
364        let back = arc([0.0, 0.0, -1.0], [0.0, 0.0, 1.0]);
365        assert!(close(rotate(back, [0.0, 0.0, -1.0]), [0.0, 0.0, 1.0], 1e-6));
366    }
367}