mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-02 22:42:24 -06:00
CC Particle World: tests for a Birth Rate that changes over time (#1373)
A burst lives on after the rate drops to 0: as a constant rate during it, only its particles afterwards, still moving, gone after their Longevity. Another effect's history isn't used. A ramp bears what each moment asked for. Seeking backward and forward matches a simulation from 0. Moving an earlier key isn't served from stale checkpoints with the same current rate. A constant rate is unchanged. The birth schedule matches the CPU's ids.
This commit is contained in:
@@ -2029,4 +2029,159 @@ mod tests {
|
||||
assert!((sum(&full.img) - sum(&half.img)).abs() < 0.05, "{} {}", sum(&full.img), sum(&half.img));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------- Birth Rate over time (#1373)
|
||||
|
||||
/// A host that evaluates effect `index`'s Birth Rate at other layer times as `rate(t)`.
|
||||
struct Rate {
|
||||
index: usize,
|
||||
rate: fn(f64) -> f64,
|
||||
}
|
||||
impl crate::EffectHost for Rate {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<crate::LayerPixels> {
|
||||
None
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
fn param_at(&self, effect_index: usize, id: &str, t: f64) -> Option<Value> {
|
||||
(effect_index == self.index && id == "birthRate").then(|| num((self.rate)(t)))
|
||||
}
|
||||
}
|
||||
|
||||
/// Birth Rate 3 until 0.5 s, then 0 (the issue's one-shot burst).
|
||||
fn burst(t: f64) -> f64 {
|
||||
if t < 0.5 { 3.0 } else { 0.0 }
|
||||
}
|
||||
|
||||
/// The same burst, cut at 0.25 s (an earlier key moved).
|
||||
fn short_burst(t: f64) -> f64 {
|
||||
if t < 0.25 { 3.0 } else { 0.0 }
|
||||
}
|
||||
|
||||
/// Birth Rate falling linearly from 3 at 0 s to 0 at 0.5 s.
|
||||
fn falling(t: f64) -> f64 {
|
||||
(3.0 * (1.0 - t / 0.5)).max(0.0)
|
||||
}
|
||||
|
||||
/// CC Particle World's parameters: random seed `seed`, Birth Rate `now` (its value at the
|
||||
/// rendered time), Longevity 1 s.
|
||||
fn pw_params(seed: f64, now: f64) -> crate::Params {
|
||||
let spec = crate::find("ec.sim.ccparticleworld").unwrap();
|
||||
let mut p = crate::Params { values: spec.params.iter().map(|ps| (ps.id.to_string(), crate::default_value(ps, [100.0, 100.0]))).collect() };
|
||||
for (k, v) in [("birthRate", num(now)), ("extras/randomSeed", num(seed)), ("longevity", num(1.0))] {
|
||||
p.values.insert(k.to_string(), v);
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
fn pw_ctx<'a>(p: &'a crate::Params, t: f64, host: Option<&'a Rate>, index: usize) -> EffectCtx<'a> {
|
||||
let env = EffectEnv { host: host.map(|h| h as &dyn crate::EffectHost), effect_index: index, ..Default::default() };
|
||||
EffectCtx { params: p, time: t, layer_size: [100.0, 100.0], seed: 1, adjustment: false, env }
|
||||
}
|
||||
|
||||
/// The live particles at layer time `t` with Birth Rate `now` there and `host`'s history.
|
||||
fn pw_at(seed: f64, now: f64, t: f64, host: Option<&Rate>) -> Vec<crate::psim::SimParticle> {
|
||||
let p = pw_params(seed, now);
|
||||
particle_state("ec.sim.ccparticleworld", &pw_ctx(&p, t, host, host.map_or(0, |h| h.index))).unwrap()
|
||||
}
|
||||
|
||||
/// The same, simulated from layer time 0 with a cache of its own (the reference).
|
||||
fn pw_fresh(seed: f64, now: f64, t: f64, host: &Rate) -> Vec<crate::psim::SimParticle> {
|
||||
let p = pw_params(seed, now);
|
||||
let ctx = pw_ctx(&p, t, Some(host), host.index);
|
||||
let key = params_key_except(&ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 1, &[PW_EFFECT_CAMERA, "birthRate"]);
|
||||
let st = simulate_rates(&SimCache::new(1), key, &pw_phys(&ctx), &rate_history(&ctx, "birthRate", PW_RATE));
|
||||
st.parts.iter().map(|q| crate::psim::SimParticle { p: q.p, v: q.v, age: q.age, life: q.life, rnd: q.rnd, id: q.id }).collect()
|
||||
}
|
||||
|
||||
/// Particles born in the first `t` seconds of `rate`.
|
||||
fn born_by(rate: fn(f64) -> f64, t: f64) -> usize {
|
||||
crate::psim::births_at(steps_at(t), 40_000, |i| rate(i as f64 / SPS) * PW_RATE).unwrap().len()
|
||||
}
|
||||
|
||||
/// #1373: Birth Rate only decides the births of its own time. After a burst stops, the
|
||||
/// particles born in it live on (moving and ageing) until their Longevity runs out, rather
|
||||
/// than vanishing because the rate is now 0.
|
||||
#[test]
|
||||
fn a_burst_lives_on_after_birth_rate_drops_to_zero() {
|
||||
let host = Rate { index: 2, rate: burst };
|
||||
// During the burst: as a constant Birth Rate of 3.
|
||||
assert_eq!(pw_at(101.0, 3.0, 0.4, Some(&host)), pw_at(101.0, 3.0, 0.4, None));
|
||||
// After it: the burst's particles, none newer.
|
||||
let late = pw_at(101.0, 0.0, 0.9, Some(&host));
|
||||
let n = born_by(burst, 0.5);
|
||||
assert!(late.len() > n / 2 && late.iter().all(|q| (q.id as usize) < n), "{} live of {n} born", late.len());
|
||||
assert!(pw_at(101.0, 0.0, 0.9, None).is_empty(), "a constant Birth Rate of 0 never bore any");
|
||||
// They keep moving.
|
||||
let early = pw_at(101.0, 0.0, 0.7, Some(&host));
|
||||
let q0 = early.iter().find(|q| q.id == late[0].id).unwrap();
|
||||
assert!(q0.p != late[0].p && late[0].age > q0.age + 0.19, "{q0:?} -> {:?}", late[0]);
|
||||
// Longevity 1 s (± 20 %): all gone by 1.8 s.
|
||||
assert!(pw_at(101.0, 0.0, 1.8, Some(&host)).is_empty());
|
||||
// The effect asks for its own index: another effect's history isn't its own.
|
||||
let p = pw_params(101.0, 0.0);
|
||||
assert!(particle_state("ec.sim.ccparticleworld", &pw_ctx(&p, 0.9, Some(&host), 0)).unwrap().is_empty());
|
||||
}
|
||||
|
||||
/// A Birth Rate ramping down isn't simulated as if its current value had always applied.
|
||||
#[test]
|
||||
fn a_ramped_birth_rate_bears_what_each_moment_asked_for() {
|
||||
let host = Rate { index: 0, rate: falling };
|
||||
let ramped = pw_at(102.0, falling(0.25), 0.25, Some(&host));
|
||||
let constant = pw_at(102.0, falling(0.25), 0.25, None);
|
||||
assert_eq!(ramped.len(), born_by(falling, 0.25), "every particle born so far is alive");
|
||||
assert_eq!(constant.len(), born_by(|_| 1.5, 0.25));
|
||||
assert!(ramped.len() > constant.len() + 10, "{} vs {}", ramped.len(), constant.len());
|
||||
}
|
||||
|
||||
/// Seeking backward, forward and to the same time again gives the state simulated from 0.
|
||||
#[test]
|
||||
fn seeking_through_a_birth_rate_history_matches_simulating_from_zero() {
|
||||
let host = Rate { index: 0, rate: burst };
|
||||
for t in [1.4, 0.6, 0.9, 0.3, 0.9, 1.1] {
|
||||
let now = burst(t);
|
||||
assert_eq!(pw_at(103.0, now, t, Some(&host)), pw_fresh(103.0, now, t, &host), "at {t}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The simulation cache key leaves Birth Rate out (so an animated rate keeps its checkpoints),
|
||||
/// but a checkpoint is only resumed with the history that reached it: after an earlier key
|
||||
/// moves, the same current rate (0) gives the new history's particles.
|
||||
#[test]
|
||||
fn an_earlier_birth_rate_change_is_not_served_from_stale_checkpoints() {
|
||||
let (long, short) = (Rate { index: 0, rate: burst }, Rate { index: 0, rate: short_burst });
|
||||
let a = pw_at(104.0, 0.0, 0.9, Some(&long));
|
||||
let b = pw_at(104.0, 0.0, 0.9, Some(&short));
|
||||
assert_eq!(b, pw_fresh(104.0, 0.0, 0.9, &short));
|
||||
assert!(b.len() < a.len() && !b.is_empty(), "{} vs {}", b.len(), a.len());
|
||||
assert_eq!(pw_at(104.0, 0.0, 1.0, Some(&long)), pw_fresh(104.0, 0.0, 1.0, &long));
|
||||
}
|
||||
|
||||
/// A constant Birth Rate gives exactly the particles it did before the history was sampled.
|
||||
#[test]
|
||||
fn a_constant_birth_rate_is_unchanged() {
|
||||
let host = Rate { index: 0, rate: |_| 2.0 };
|
||||
let p = pw_params(105.0, 2.0);
|
||||
let ctx = pw_ctx(&p, 1.3, None, 0);
|
||||
let phys = pw_phys(&ctx);
|
||||
let before = simulate(&SimCache::new(1), sim_key(&ctx, 1), 1.3, &phys);
|
||||
let now = pw_at(105.0, 2.0, 1.3, Some(&host));
|
||||
assert_eq!(now.len(), before.parts.len());
|
||||
assert!(now.iter().zip(&before.parts).all(|(a, b)| a.id == b.id && a.p == b.p && a.v == b.v && a.age == b.age));
|
||||
assert_eq!(crate::psim::births_at(78, 40_000, |_| phys.rate), crate::psim::births(phys.rate, 78, 40_000));
|
||||
}
|
||||
|
||||
/// The GPU's birth schedule for a rate history is the CPU's: the ids born are the ids the
|
||||
/// CPU simulation numbers.
|
||||
#[test]
|
||||
fn the_birth_schedule_follows_the_rate_history() {
|
||||
let host = Rate { index: 0, rate: falling };
|
||||
let p = pw_params(106.0, 0.0);
|
||||
let ctx = pw_ctx(&p, 0.7, Some(&host), 0);
|
||||
let rates = rate_history(&ctx, "birthRate", PW_RATE);
|
||||
let st = simulate_rates(&SimCache::new(1), 9, &pw_phys(&ctx), &rates);
|
||||
let births = crate::psim::births_at(rates.len() as u64, 40_000, |i| rates[i as usize]).unwrap();
|
||||
assert_eq!(st.next_id as usize, births.len());
|
||||
assert!(births.iter().all(|&b| f64::from(b) / SPS < 0.5), "nothing born once the ramp reaches 0");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user