mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
Merge pull request #1390 from tylerjenningsw/fix/particle-birth-history-ps2-mercury
Particles: CC Particle Systems II and CC Mr. Mercury keep emitted particles when Birth Rate changes (#1373)
This commit is contained in:
@@ -1312,11 +1312,11 @@ pub fn is_time_dependent(id: &str) -> bool {
|
||||
id.starts_with("ec.sim.") || TIME_DEPENDENT.contains(&id)
|
||||
}
|
||||
|
||||
/// Does effect `id` read its own parameters at earlier times (CC Particle World samples its
|
||||
/// Birth Rate at every step, #1373)? Caches keyed on the current values can't see what an
|
||||
/// Does effect `id` read its own parameters at earlier times (the stepped particle systems sample
|
||||
/// their Birth Rate at every step, #1373)? Caches keyed on the current values can't see what an
|
||||
/// expression returned then.
|
||||
pub fn reads_param_history(id: &str) -> bool {
|
||||
id == "ec.sim.ccparticleworld"
|
||||
matches!(id, "ec.sim.ccparticleworld" | "ec.sim.ccparticlesystems2" | "ec.sim.ccmrmercury")
|
||||
}
|
||||
|
||||
/// Run an effect on a buffer.
|
||||
|
||||
@@ -15,7 +15,7 @@ use effectcraft_raster::{Image, Px};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::generate::value_noise;
|
||||
use crate::util::{SimCache, hash1, params_key, params_key_except, unpremul};
|
||||
use crate::util::{SimCache, hash1, params_key_except, unpremul};
|
||||
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||||
|
||||
mod common;
|
||||
@@ -28,13 +28,13 @@ mod weather;
|
||||
pub(crate) use common::{h, hs, pct, pt, spec};
|
||||
pub use engine::particle_state;
|
||||
#[cfg(test)]
|
||||
pub(crate) use engine::{PState, PW_ANIMS, Phys, simulate};
|
||||
use engine::{MERCURY, PS2, PW_EFFECT_CAMERA, Stepped, rate_history, sim_key, simulate_rates};
|
||||
#[cfg(test)]
|
||||
use engine::{PW_EFFECT_CAMERA, rate_history, sim_key, simulate_rates};
|
||||
pub(crate) use engine::{PState, PW_ANIMS, Phys, simulate};
|
||||
pub(crate) use engine::{SPS, steps_at};
|
||||
pub use particle_fx::{Blob, mercury_blobs};
|
||||
#[cfg(test)]
|
||||
use particle_fx::{PW_RATE, pw_phys};
|
||||
use particle_fx::{PW_RATE, mercury_phys, ps2_phys, pw_phys};
|
||||
pub use registry::{specs, sprite_plan};
|
||||
pub use sprites::{Acc, Post, Shape, Sprite, SpritePlan, Tint};
|
||||
pub(crate) use sprites::{combine, raster, splat};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::common::{h, hs};
|
||||
use super::particle_fx::{PS2_CACHE, PW_CACHE, PW_RATE, ps2_phys, pw_phys};
|
||||
use super::particle_fx::{MERCURY_CACHE, MERCURY_RATE, PS2_CACHE, PS2_RATE, PW_CACHE, PW_RATE, ps2_phys, pw_phys};
|
||||
use super::sprites::{Shape, Sprite};
|
||||
use super::{Buf, EffectCtx, Image, IntoParallelRefMutIterator, ParallelIterator, SimCache, params_key_except, value_noise};
|
||||
|
||||
@@ -114,6 +114,8 @@ impl Phys {
|
||||
Particle { p, v, age: 0.0, life, rnd: h(id, 11, seed), id }
|
||||
}
|
||||
|
||||
/// One step at the constant [`Phys::rate`] (the tests' reference).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn step(&self, st: &mut PState, step: u64) {
|
||||
self.step_at_rate(st, step, self.rate);
|
||||
}
|
||||
@@ -274,12 +276,39 @@ pub(super) fn simulate_rates(cache: &SimCache<PState>, key: u64, phys: &Phys, ra
|
||||
cache.run_where(key, rates.len() as u64, PState::default, |st, i| phys.step_at_rate(st, i, rate(i)), |s, st| prefix.get(s as usize) == Some(&st.hist))
|
||||
}
|
||||
|
||||
/// A stepped particle system's particles at `ctx.time`, born at the Birth Rate of their own
|
||||
/// time (`birthRate` × `k` particles per second, see [`rate_history`]). The cache key is every
|
||||
/// parameter but Birth Rate (its history is checked through `PState::hist`) and those under
|
||||
/// `skip`, with `salt`. `gpu`: the host's particle backend may simulate it.
|
||||
pub(super) struct Stepped<'a> {
|
||||
pub(super) cache: &'a SimCache<PState>,
|
||||
pub(super) salt: u64,
|
||||
pub(super) skip: &'a [&'a str],
|
||||
pub(super) k: f64,
|
||||
pub(super) gpu: bool,
|
||||
}
|
||||
|
||||
impl Stepped<'_> {
|
||||
pub(super) fn state(&self, ctx: &EffectCtx, phys: &Phys) -> std::sync::Arc<PState> {
|
||||
let rates = rate_history(ctx, "birthRate", self.k);
|
||||
let mut skip = self.skip.to_vec();
|
||||
skip.push("birthRate");
|
||||
let key = params_key_except(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, self.salt, &skip);
|
||||
let gpu = if self.gpu { accelerated(ctx, key, phys, Some(&rates)) } else { None };
|
||||
gpu.unwrap_or_else(|| simulate_rates(self.cache, key, phys, &rates))
|
||||
}
|
||||
}
|
||||
|
||||
/// CC Particle World.
|
||||
const PW: Stepped = Stepped { cache: &PW_CACHE, salt: 1, skip: &[PW_EFFECT_CAMERA], k: PW_RATE, gpu: true };
|
||||
/// CC Particle Systems II.
|
||||
pub(super) const PS2: Stepped = Stepped { cache: &PS2_CACHE, salt: 2, skip: &[], k: PS2_RATE, gpu: true };
|
||||
/// CC Mr. Mercury (its blobs are simulated on the CPU).
|
||||
pub(super) const MERCURY: Stepped = Stepped { cache: &MERCURY_CACHE, salt: 3, skip: &[], k: MERCURY_RATE, gpu: false };
|
||||
|
||||
/// CC Particle World's particles at `ctx.time`, born at the Birth Rate of their own time.
|
||||
pub(super) fn pw_state(ctx: &EffectCtx, phys: &Phys) -> std::sync::Arc<PState> {
|
||||
let rates = rate_history(ctx, "birthRate", PW_RATE);
|
||||
// Every parameter but Birth Rate (its history is checked through `PState::hist`).
|
||||
let key = params_key_except(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 1, &[PW_EFFECT_CAMERA, "birthRate"]);
|
||||
accelerated(ctx, key, phys, Some(&rates)).unwrap_or_else(|| simulate_rates(&PW_CACHE, key, phys, &rates))
|
||||
PW.state(ctx, phys)
|
||||
}
|
||||
|
||||
/// The live particles of a stepped particle effect (`ec.sim.ccparticleworld`,
|
||||
@@ -288,23 +317,22 @@ pub(super) fn pw_state(ctx: &EffectCtx, phys: &Phys) -> std::sync::Arc<PState> {
|
||||
pub fn particle_state(id: &str, ctx: &EffectCtx) -> Option<Vec<crate::psim::SimParticle>> {
|
||||
let st = match id {
|
||||
"ec.sim.ccparticleworld" => pw_state(ctx, &pw_phys(ctx)),
|
||||
"ec.sim.ccparticlesystems2" => {
|
||||
let (phys, key) = (ps2_phys(ctx), sim_key(ctx, 2));
|
||||
accelerated(ctx, key, &phys, None).unwrap_or_else(|| simulate(&PS2_CACHE, key, ctx.time, &phys))
|
||||
}
|
||||
"ec.sim.ccparticlesystems2" => PS2.state(ctx, &ps2_phys(ctx)),
|
||||
_ => return None,
|
||||
};
|
||||
Some(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())
|
||||
}
|
||||
|
||||
/// The simulation cache key of a stepped particle effect: every parameter but CC Particle World's
|
||||
/// Effect Camera, which only changes how the particles are seen (animating it doesn't restart
|
||||
/// the simulation).
|
||||
/// The simulation cache key of a stepped particle effect at a constant Birth Rate (the tests'
|
||||
/// reference): every parameter but CC Particle World's Effect Camera.
|
||||
#[cfg(test)]
|
||||
pub(super) fn sim_key(ctx: &EffectCtx, salt: u64) -> u64 {
|
||||
params_key_except(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, salt, &[PW_EFFECT_CAMERA])
|
||||
}
|
||||
|
||||
/// Run a particle system to layer time `t` through `cache`.
|
||||
/// Run a particle system at its constant [`Phys::rate`] to layer time `t` through `cache` (the
|
||||
/// tests' reference).
|
||||
#[cfg(test)]
|
||||
pub(crate) fn simulate(cache: &SimCache<PState>, key: u64, t: f64, phys: &Phys) -> std::sync::Arc<PState> {
|
||||
cache.run(key, steps_at(t), PState::default, |st, i| phys.step(st, i))
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::engine::{PState, PW_EFFECT_CAMERA, Phys, accelerated, norm3, particle_look, pw_state, sim_key, simulate, type_sprite};
|
||||
use super::engine::{MERCURY, PS2, PState, PW_EFFECT_CAMERA, Phys, norm3, particle_look, pw_state, type_sprite};
|
||||
use super::sprites::{Acc, Post, Sprite, SpritePlan, raster, splat};
|
||||
use super::{Buf, EffectCtx, Image, IntoParallelRefIterator, ParallelIterator, SimCache, Value, params_key, unpremul};
|
||||
use super::{Buf, EffectCtx, Image, IntoParallelRefIterator, ParallelIterator, SimCache, Value, unpremul};
|
||||
|
||||
// ------------------------------------------------------------------ CC Particle World
|
||||
|
||||
@@ -192,6 +192,9 @@ fn particle_sprites(sprites: Vec<Sprite>, mode: u32) -> SpritePlan {
|
||||
|
||||
pub(super) static PS2_CACHE: SimCache<PState> = SimCache::new(6);
|
||||
|
||||
/// Particles per second per unit of CC Particle Systems II's Birth Rate.
|
||||
pub(super) const PS2_RATE: f64 = 60.0;
|
||||
|
||||
/// CC Particle Systems II's physics.
|
||||
pub(super) fn ps2_phys(ctx: &EffectCtx) -> Phys {
|
||||
let pr = ctx.params;
|
||||
@@ -200,7 +203,7 @@ pub(super) fn ps2_phys(ctx: &EffectCtx) -> Phys {
|
||||
let pos = pr.v2("producer/position");
|
||||
let dir = (pr.f("physics/direction") as f32).to_radians();
|
||||
Phys {
|
||||
rate: pr.f("birthRate").max(0.0) * 60.0,
|
||||
rate: pr.f("birthRate").max(0.0) * PS2_RATE,
|
||||
life: pr.f("longevity").max(0.0) as f32,
|
||||
producer: [pos[0] as f32 / unit, pos[1] as f32 / unit, 0.0],
|
||||
radius: [pr.f("producer/radiusX").max(0.0) as f32 / unit, pr.f("producer/radiusY").max(0.0) as f32 / unit, 0.0],
|
||||
@@ -227,9 +230,7 @@ pub(super) fn particle_systems2_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||||
let pr = ctx.params;
|
||||
let lh = ctx.layer_size[1] as f32;
|
||||
let unit = lh.max(1.0);
|
||||
let phys = ps2_phys(ctx);
|
||||
let key = sim_key(ctx, 2);
|
||||
let st = accelerated(ctx, key, &phys, None).unwrap_or_else(|| simulate(&PS2_CACHE, key, ctx.time, &phys));
|
||||
let st = PS2.state(ctx, &ps2_phys(ctx));
|
||||
let kind = pr.e("particle/particleType");
|
||||
let birth = pr.color("particle/birthColor");
|
||||
let death = pr.color("particle/deathColor");
|
||||
@@ -252,7 +253,10 @@ pub(super) fn particle_systems2_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||||
|
||||
// ------------------------------------------------------------------ CC Mr. Mercury
|
||||
|
||||
static MERCURY_CACHE: SimCache<PState> = SimCache::new(6);
|
||||
pub(super) static MERCURY_CACHE: SimCache<PState> = SimCache::new(6);
|
||||
|
||||
/// Blobs per second per unit of CC Mr. Mercury's Birth Rate.
|
||||
pub(super) const MERCURY_RATE: f64 = 10.0;
|
||||
|
||||
/// A CC Mr. Mercury blob (buffer px).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -262,15 +266,14 @@ pub struct Blob {
|
||||
pub r: f32,
|
||||
}
|
||||
|
||||
/// CC Mr. Mercury's blobs this frame, from its particle simulation (for buffer geometry `b`).
|
||||
pub fn mercury_blobs(ctx: &EffectCtx, b: &Buf) -> Vec<Blob> {
|
||||
/// CC Mr. Mercury's physics.
|
||||
pub(super) fn mercury_phys(ctx: &EffectCtx) -> Phys {
|
||||
let pr = ctx.params;
|
||||
let lh = ctx.layer_size[1] as f32;
|
||||
let unit = lh.max(1.0);
|
||||
let unit = (ctx.layer_size[1] as f32).max(1.0);
|
||||
let pos = pr.v2("producer");
|
||||
let dir = (pr.f("direction") as f32).to_radians();
|
||||
let phys = Phys {
|
||||
rate: pr.f("birthRate").max(0.0) * 10.0,
|
||||
Phys {
|
||||
rate: pr.f("birthRate").max(0.0) * MERCURY_RATE,
|
||||
life: pr.f("longevity").max(0.0) as f32,
|
||||
producer: [pos[0] as f32 / unit, pos[1] as f32 / unit, 0.0],
|
||||
radius: [pr.f("radiusX").max(0.0) as f32 / unit, pr.f("radiusY").max(0.0) as f32 / unit, 0.0],
|
||||
@@ -284,8 +287,15 @@ pub fn mercury_blobs(ctx: &EffectCtx, b: &Buf) -> Vec<Blob> {
|
||||
seed: ctx.seed.wrapping_mul(0xc2b2ae35),
|
||||
max: 4_000,
|
||||
flat: true,
|
||||
};
|
||||
let st = simulate(&MERCURY_CACHE, params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 3), ctx.time, &phys);
|
||||
}
|
||||
}
|
||||
|
||||
/// CC Mr. Mercury's blobs this frame, from its particle simulation (for buffer geometry `b`).
|
||||
pub fn mercury_blobs(ctx: &EffectCtx, b: &Buf) -> Vec<Blob> {
|
||||
let pr = ctx.params;
|
||||
let unit = (ctx.layer_size[1] as f32).max(1.0);
|
||||
let phys = mercury_phys(ctx);
|
||||
let st = MERCURY.state(ctx, &phys);
|
||||
let (bs, ds) = (pr.f("blobBirthSize") as f32, pr.f("blobDeathSize") as f32);
|
||||
let s = b.scale as f32;
|
||||
st.parts
|
||||
|
||||
@@ -561,3 +561,110 @@ fn the_birth_schedule_follows_the_rate_history() {
|
||||
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");
|
||||
}
|
||||
|
||||
// ------------------------- CC Particle Systems II and CC Mr. Mercury's Birth Rate over time
|
||||
|
||||
/// A stepped particle effect: its id, physics and simulation.
|
||||
struct System {
|
||||
id: &'static str,
|
||||
phys: fn(&EffectCtx) -> Phys,
|
||||
stepped: Stepped<'static>,
|
||||
}
|
||||
|
||||
const SYSTEMS: [System; 2] =
|
||||
[System { id: "ec.sim.ccparticlesystems2", phys: ps2_phys, stepped: PS2 }, System { id: "ec.sim.ccmrmercury", phys: mercury_phys, stepped: MERCURY }];
|
||||
|
||||
/// `id`'s parameters with Birth Rate `now` and Longevity 1 s.
|
||||
fn sys_params(id: &str, now: f64) -> crate::Params {
|
||||
let spec = crate::find(id).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)), ("longevity", num(1.0))] {
|
||||
assert!(p.values.contains_key(k), "{id}: no {k}");
|
||||
p.values.insert(k.to_string(), v);
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
/// The context at layer time `t` (`seed` keeps each test's simulations apart).
|
||||
fn sys_ctx<'a>(p: &'a crate::Params, t: f64, host: Option<&'a Rate>, seed: u32) -> EffectCtx<'a> {
|
||||
let env = EffectEnv { host: host.map(|h| h as &dyn crate::EffectHost), effect_index: host.map_or(0, |h| h.index), ..Default::default() };
|
||||
EffectCtx { params: p, time: t, layer_size: [100.0, 100.0], seed, adjustment: false, env }
|
||||
}
|
||||
|
||||
/// The live particles (id, position, age) of `sys` at `t` through its simulation.
|
||||
fn sys_at(sys: &System, seed: u32, now: f64, t: f64, host: Option<&Rate>) -> Vec<(u32, [f32; 3], f32)> {
|
||||
let p = sys_params(sys.id, now);
|
||||
let ctx = sys_ctx(&p, t, host, seed);
|
||||
sys.stepped.state(&ctx, &(sys.phys)(&ctx)).parts.iter().map(|q| (q.id, q.p, q.age)).collect()
|
||||
}
|
||||
|
||||
/// The same, simulated from layer time 0 with a cache of its own (the reference).
|
||||
fn sys_fresh(sys: &System, seed: u32, now: f64, t: f64, host: &Rate) -> Vec<(u32, [f32; 3], f32)> {
|
||||
let p = sys_params(sys.id, now);
|
||||
let ctx = sys_ctx(&p, t, Some(host), seed);
|
||||
let mut skip = sys.stepped.skip.to_vec();
|
||||
skip.push("birthRate");
|
||||
let key = params_key_except(&ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, sys.stepped.salt, &skip);
|
||||
let st = simulate_rates(&SimCache::new(1), key, &(sys.phys)(&ctx), &rate_history(&ctx, "birthRate", sys.stepped.k));
|
||||
st.parts.iter().map(|q| (q.id, q.p, q.age)).collect()
|
||||
}
|
||||
|
||||
/// What `sys` draws at `t`: CC Particle Systems II's particles, CC Mr. Mercury's blobs.
|
||||
fn sys_drawn(sys: &System, seed: u32, now: f64, t: f64, host: Option<&Rate>) -> usize {
|
||||
let p = sys_params(sys.id, now);
|
||||
let ctx = sys_ctx(&p, t, host, seed);
|
||||
match sys.id {
|
||||
"ec.sim.ccmrmercury" => mercury_blobs(&ctx, &Buf { img: Image::new(100, 100), offset: [0.0; 2], scale: 1.0 }).len(),
|
||||
id => particle_state(id, &ctx).unwrap().len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// #1373 for CC Particle Systems II and CC Mr. Mercury: a burst lives on after Birth Rate
|
||||
/// drops to 0 (what they draw too), moving and ageing, and is gone after its Longevity.
|
||||
#[test]
|
||||
fn particle_systems_ii_and_mr_mercury_keep_a_burst_after_birth_rate_drops() {
|
||||
let host = Rate { index: 1, rate: burst };
|
||||
for sys in &SYSTEMS {
|
||||
let born = crate::psim::births_at(steps_at(0.5), 40_000, |i| burst(i as f64 / SPS) * sys.stepped.k).unwrap().len();
|
||||
assert_eq!(sys_at(sys, 201, 3.0, 0.4, Some(&host)), sys_at(sys, 201, 3.0, 0.4, None), "{}: during the burst", sys.id);
|
||||
let late = sys_at(sys, 201, 0.0, 0.9, Some(&host));
|
||||
assert!(late.len() > born / 2 && late.iter().all(|q| (q.0 as usize) < born), "{}: {} live of {born}", sys.id, late.len());
|
||||
assert!(sys_drawn(sys, 201, 0.0, 0.9, Some(&host)) > 0, "{}: drawn", sys.id);
|
||||
assert_eq!(sys_drawn(sys, 201, 0.0, 0.9, None), 0, "{}: a constant 0 bore none", sys.id);
|
||||
let early = sys_at(sys, 201, 0.0, 0.7, Some(&host));
|
||||
let q0 = early.iter().find(|q| q.0 == late[0].0).unwrap();
|
||||
assert!(q0.1 != late[0].1 && late[0].2 > q0.2 + 0.19, "{}: {q0:?} -> {:?}", sys.id, late[0]);
|
||||
assert!(sys_at(sys, 201, 0.0, 1.8, Some(&host)).is_empty(), "{}: gone after Longevity", sys.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Seeking matches a simulation from 0, and moving an earlier key (same current rate, same
|
||||
/// cache key) isn't served from the old history's checkpoints.
|
||||
#[test]
|
||||
fn particle_systems_ii_and_mr_mercury_seek_and_drop_stale_checkpoints() {
|
||||
let (long, short) = (Rate { index: 0, rate: burst }, Rate { index: 0, rate: short_burst });
|
||||
for sys in &SYSTEMS {
|
||||
for t in [1.4, 0.6, 0.9, 0.3, 0.9, 1.1] {
|
||||
let now = burst(t);
|
||||
assert_eq!(sys_at(sys, 202, now, t, Some(&long)), sys_fresh(sys, 202, now, t, &long), "{} at {t}", sys.id);
|
||||
}
|
||||
let b = sys_at(sys, 202, 0.0, 0.9, Some(&short));
|
||||
assert_eq!(b, sys_fresh(sys, 202, 0.0, 0.9, &short), "{}: the moved key", sys.id);
|
||||
assert!(!b.is_empty() && b.len() < sys_at(sys, 202, 0.0, 0.9, Some(&long)).len(), "{}", sys.id);
|
||||
}
|
||||
}
|
||||
|
||||
/// A constant Birth Rate gives exactly the particles the constant-rate simulation did.
|
||||
#[test]
|
||||
fn particle_systems_ii_and_mr_mercury_are_unchanged_at_a_constant_rate() {
|
||||
let host = Rate { index: 0, rate: |_| 2.0 };
|
||||
for sys in &SYSTEMS {
|
||||
let p = sys_params(sys.id, 2.0);
|
||||
let ctx = sys_ctx(&p, 1.3, None, 203);
|
||||
let before = simulate(&SimCache::new(1), 1, 1.3, &(sys.phys)(&ctx));
|
||||
let before: Vec<_> = before.parts.iter().map(|q| (q.id, q.p, q.age)).collect();
|
||||
assert!(!before.is_empty());
|
||||
assert_eq!(sys_at(sys, 203, 2.0, 1.3, Some(&host)), before, "{}: keyed to a constant", sys.id);
|
||||
assert_eq!(sys_at(sys, 203, 2.0, 1.3, None), before, "{}: no host", sys.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,11 +113,36 @@ impl EffectHost for Keyed {
|
||||
}
|
||||
}
|
||||
|
||||
/// CC Particle World's live particles at layer time `t` through `host`.
|
||||
fn keyed_state(p: &Params, t: f64, host: &Keyed) -> Vec<SimParticle> {
|
||||
/// Effect `id`'s live particles at layer time `t` through `host`.
|
||||
fn keyed_state(id: &str, p: &Params, t: f64, host: &Keyed) -> Vec<SimParticle> {
|
||||
let env = EffectEnv { host: Some(host), ..Default::default() };
|
||||
let ctx = EffectCtx { params: p, time: t, layer_size: [320.0, 240.0], seed: 5, adjustment: false, env };
|
||||
effectcraft_effects::particle_state("ec.sim.ccparticleworld", &ctx).unwrap()
|
||||
effectcraft_effects::particle_state(id, &ctx).unwrap()
|
||||
}
|
||||
|
||||
/// 3 a second from 0 to 0.5 s.
|
||||
fn early(t: f64) -> f64 {
|
||||
if t < 0.5 { 3.0 } else { 0.0 }
|
||||
}
|
||||
|
||||
/// 3 a second from 0.25 to 0.75 s: as many births by 0.9 s as [`early`], at other steps.
|
||||
fn late(t: f64) -> f64 {
|
||||
if (0.25..0.75).contains(&t) { 3.0 } else { 0.0 }
|
||||
}
|
||||
|
||||
/// Effect `id` (parameters `p`) under the [`early`] then the [`late`] Birth Rate history: the
|
||||
/// GPU's particles match the CPU's, and each really came from the GPU.
|
||||
fn keyed_agree(id: &str, p: &Params, g: &'static Gpu) {
|
||||
for (rate, times) in [(early as fn(f64) -> f64, [0.4, 0.9, 1.2]), (late, [0.9, 1.2, 0.6])] {
|
||||
let (cpu_host, gpu_host) = (Keyed { gpu: None, rate }, Keyed { gpu: Some(g), rate });
|
||||
for t in times {
|
||||
let cpu = keyed_state(id, p, t, &cpu_host);
|
||||
let before = crate::particles::DISPATCHES.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let gpu = keyed_state(id, p, t, &gpu_host);
|
||||
assert!(crate::particles::DISPATCHES.load(std::sync::atomic::Ordering::Relaxed) > before, "{id} t {t}: simulated on the GPU");
|
||||
agree(&format!("{id} keyed birth rate t {t}"), &cpu, &gpu, 1e-4, cpu.len() / 100 + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// #1373: with a keyed Birth Rate the GPU is given the births of each moment, as the CPU
|
||||
@@ -127,23 +152,16 @@ fn keyed_state(p: &Params, t: f64, host: &Keyed) -> Vec<SimParticle> {
|
||||
#[test]
|
||||
fn particle_world_with_a_keyed_birth_rate_matches_the_cpu() {
|
||||
let Some(g) = gpu() else { return };
|
||||
fn early(t: f64) -> f64 {
|
||||
if t < 0.5 { 3.0 } else { 0.0 }
|
||||
}
|
||||
fn late(t: f64) -> f64 {
|
||||
if (0.25..0.75).contains(&t) { 3.0 } else { 0.0 }
|
||||
}
|
||||
let p = params("ec.sim.ccparticleworld", &[("birthRate", n(0.0)), ("longevity", n(1.2)), ("extras/randomSeed", n(21.0))]);
|
||||
for (rate, times) in [(early as fn(f64) -> f64, [0.4, 0.9, 1.2]), (late, [0.9, 1.2, 0.6])] {
|
||||
let (cpu_host, gpu_host) = (Keyed { gpu: None, rate }, Keyed { gpu: Some(g), rate });
|
||||
for t in times {
|
||||
let cpu = keyed_state(&p, t, &cpu_host);
|
||||
let before = crate::particles::DISPATCHES.load(std::sync::atomic::Ordering::Relaxed);
|
||||
let gpu = keyed_state(&p, t, &gpu_host);
|
||||
assert!(crate::particles::DISPATCHES.load(std::sync::atomic::Ordering::Relaxed) > before, "t {t}: simulated on the GPU");
|
||||
agree(&format!("keyed birth rate t {t}"), &cpu, &gpu, 1e-4, cpu.len() / 100 + 1);
|
||||
}
|
||||
}
|
||||
keyed_agree("ec.sim.ccparticleworld", &p, g);
|
||||
}
|
||||
|
||||
/// The same for CC Particle Systems II (90 births by 0.9 s in both histories).
|
||||
#[test]
|
||||
fn particle_systems_ii_with_a_keyed_birth_rate_matches_the_cpu() {
|
||||
let Some(g) = gpu() else { return };
|
||||
let p = params("ec.sim.ccparticlesystems2", &[("birthRate", n(0.0)), ("longevity", n(1.2)), ("randomSeed", n(22.0))]);
|
||||
keyed_agree("ec.sim.ccparticlesystems2", &p, g);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -491,6 +491,64 @@ fn particle_world_with_a_birth_rate_expression_is_not_served_from_the_layer_cach
|
||||
assert!(light(&b) > 1.0 && light(&b) < light(&a), "fewer births: {} vs {}", light(&b), light(&a));
|
||||
}
|
||||
|
||||
/// A grey solid (CC Mr. Mercury shades its blobs from it) with effect `id` (CC Particle Systems
|
||||
/// II, CC Mr. Mercury): Birth Rate `rate`,
|
||||
/// or keyed 3 until 0.5 s and 0 a frame later (`None`); Longevity 1 s.
|
||||
fn system_scene(id: &str, rate: Option<f64>) -> (Project, ItemId) {
|
||||
let (mut p, cid, comp) = setup();
|
||||
let mut l = solid(&mut p, &comp, [0.5, 0.5, 0.5], 200, 100);
|
||||
let vals = [("birthRate", Value::Scalar(rate.unwrap_or(3.0))), ("longevity", Value::Scalar(1.0))];
|
||||
add_effect(&mut p, &mut l, id, [200.0, 100.0], &vals);
|
||||
if rate.is_none() {
|
||||
let fx = l.props.sub_mut("effects").unwrap().children.iter_mut().find_map(|c| match c {
|
||||
effectcraft_project::Node::Group(g) => Some(g),
|
||||
_ => None,
|
||||
});
|
||||
let key = |t: f64, v: f64| Keyframe::new(Tick::from_seconds_f64(t), Value::Scalar(v));
|
||||
fx.unwrap().prop_mut("birthRate").unwrap().keys = vec![key(0.0, 3.0), key(0.5, 3.0), key(0.5 + 1.0 / 30.0, 0.0)];
|
||||
}
|
||||
p.comp_mut(cid).unwrap().layers = vec![l];
|
||||
(p, cid)
|
||||
}
|
||||
|
||||
/// #1373 for CC Particle Systems II and CC Mr. Mercury through the renderer: a keyed burst
|
||||
/// draws as the constant rate during it, still draws after the rate drops to 0 (unlike a rate
|
||||
/// of 0 throughout), and draws as nothing born once Longevity has run out.
|
||||
#[test]
|
||||
fn particle_systems_ii_and_mr_mercury_keep_a_burst_after_birth_rate_drops_to_zero() {
|
||||
let at = Tick::from_seconds_f64;
|
||||
for id in ["ec.sim.ccparticlesystems2", "ec.sim.ccmrmercury"] {
|
||||
let (keyed, cid) = system_scene(id, None);
|
||||
let (constant, _) = system_scene(id, Some(3.0));
|
||||
let (none, _) = system_scene(id, Some(0.0));
|
||||
assert_same(&render_cached(&keyed, cid, at(0.4), None), &render_cached(&constant, cid, at(0.4), None), &format!("{id}: during the burst"));
|
||||
let (k, z) = (render_cached(&keyed, cid, at(0.9), None), render_cached(&none, cid, at(0.9), None));
|
||||
assert!((light(&k) - light(&z)).abs() > 1.0, "{id}: the burst is still drawn: {} vs {}", light(&k), light(&z));
|
||||
assert_same(&render_cached(&keyed, cid, at(1.9), None), &render_cached(&none, cid, at(1.9), None), &format!("{id}: gone after Longevity"));
|
||||
}
|
||||
}
|
||||
|
||||
/// A Birth Rate expression on any of the stepped particle systems keeps the layer out of the
|
||||
/// layer cache (its key can't see what the expression returned at earlier times); without one
|
||||
/// the layer is cached.
|
||||
#[test]
|
||||
fn stepped_particle_layers_with_a_birth_rate_expression_are_not_layer_cached() {
|
||||
for id in ["ec.sim.ccparticleworld", "ec.sim.ccparticlesystems2", "ec.sim.ccmrmercury"] {
|
||||
let (mut p, cid) = system_scene(id, Some(2.0));
|
||||
let key = |p: &Project| {
|
||||
let c = p.comp(cid).unwrap();
|
||||
crate::cache::layer_key(&crate::eval::EvalCtx::new(p, cid, c, Tick::from_seconds_f64(0.9)), &c.layers[0], 1.0, false, false)
|
||||
};
|
||||
assert!(key(&p).is_some(), "{id}: cached without an expression");
|
||||
let fx = p.comp_mut(cid).unwrap().layers[0].props.sub_mut("effects").unwrap().children.iter_mut().find_map(|c| match c {
|
||||
effectcraft_project::Node::Group(g) => Some(g),
|
||||
_ => None,
|
||||
});
|
||||
fx.unwrap().prop_mut("birthRate").unwrap().expr = Some(effectcraft_project::Expression { text: "time < 0.5 ? 3 : 0".into(), enabled: true });
|
||||
assert!(key(&p).is_none(), "{id}: not cached with a Birth Rate expression");
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------ layer cache
|
||||
|
||||
fn render_cached(p: &Project, cid: ItemId, t: Tick, cache: Option<&crate::LayerCache>) -> crate::Image {
|
||||
|
||||
@@ -371,12 +371,12 @@ Playground cannons without interacting forces evolve every particle independentl
|
||||
backend (reached through `EffectHost::particles` when the GPU compositor is active) simulates one
|
||||
particle per invocation from the CPU's birth schedule and keeps per-key state checkpoints on the
|
||||
GPU, like `SimCache`; the CPU simulation stays the oracle (tests compare ids and positions).
|
||||
CC Particle World samples its Birth Rate at every step (`EffectHost::param_at`, #1373), so a
|
||||
keyed rate only decides the births of its own time; the rate is left out of the simulation key,
|
||||
and a checkpoint is resumed only with the same rate history (the CPU state's `hist`, the GPU
|
||||
CC Particle World, CC Particle Systems II and CC Mr. Mercury sample their Birth Rate at every
|
||||
step (`EffectHost::param_at`, #1373), so a keyed rate only decides the births of its own time;
|
||||
the rate is left out of the simulation key, and a checkpoint is resumed only with the same rate history (the CPU state's `hist`, the GPU
|
||||
checkpoint's births). The rate's keyframes are read at the step's exact layer time
|
||||
(`EvalCtx::value_at_layer_time`; mapping it to comp time and back rounds under a fractional
|
||||
stretch), a rate without keyframes or an expression isn't sampled, and a Particle World layer with
|
||||
stretch), a rate without keyframes or an expression isn't sampled, and a layer of one of them with
|
||||
expressions isn't layer cached (its key can't see what an expression returned at earlier times).
|
||||
`effectcraft-cli bench --gpu` reports CPU vs GPU ms/frame, speed-up, Auto's ms/frame, pixel
|
||||
agreement and the steady-state upload / readback MB per frame for every comp plus an
|
||||
|
||||
Reference in New Issue
Block a user