mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-07 13:23:37 +00:00
Wave World, Particle Playground: keep grids within a cell and particle budget
This commit is contained in:
@@ -310,13 +310,32 @@ pub fn wave_world_plan(ctx: &EffectCtx, b: &Buf) -> Option<WavePlan> {
|
||||
wave_world_impl(ctx, b, true).err()
|
||||
}
|
||||
|
||||
/// Most cells of Wave World's grid: the largest Grid Resolution (400) on a layer four times as
|
||||
/// tall as it is wide. Only the horizontal resolution is a parameter; the vertical one follows
|
||||
/// the layer's aspect, so a tall layer would otherwise ask for any number of cells.
|
||||
pub(super) const MAX_WAVE_CELLS: usize = 401 * 1601;
|
||||
|
||||
/// Wave World's grid (`nx` × `ny` nodes, square cells) for `res` cells across a `lw` × `lh`
|
||||
/// layer, coarsened so it has at most [`MAX_WAVE_CELLS`] nodes.
|
||||
pub(super) fn wave_grid(res: usize, lw: f64, lh: f64) -> (usize, usize) {
|
||||
let aspect = lh / lw.max(1.0);
|
||||
let aspect = if aspect.is_finite() { aspect.max(0.0) } else { 0.0 };
|
||||
let nodes = |r: usize| (r as f64 + 1.0) * ((r as f64 * aspect).round().max(2.0) + 1.0);
|
||||
let mut res = res.max(2);
|
||||
if nodes(res) > MAX_WAVE_CELLS as f64 {
|
||||
res = ((res as f64 * (MAX_WAVE_CELLS as f64 / nodes(res)).sqrt()).floor() as usize).max(2);
|
||||
}
|
||||
let nx = res + 1;
|
||||
let ny = ((res as f64 * aspect).round().min((MAX_WAVE_CELLS / nx - 1) as f64) as usize).max(2) + 1;
|
||||
(nx, ny)
|
||||
}
|
||||
|
||||
fn wave_world_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result<Image, WavePlan> {
|
||||
let pr = ctx.params;
|
||||
let [lw, lh] = ctx.layer_size;
|
||||
let res = pr.f("simulation/gridResolution").clamp(1.0, 400.0) as usize;
|
||||
let res = if pr.b("simulation/gridResDownsamples") { ((res as f64) * b.scale).round().max(1.0) as usize } else { res };
|
||||
let nx = res.max(2) + 1;
|
||||
let ny = ((res as f64 * lh / lw.max(1.0)).round() as usize).max(2) + 1;
|
||||
let (nx, ny) = wave_grid(res, lw, lh);
|
||||
let speed = pr.f("simulation/waveSpeed").max(0.0) as f32;
|
||||
let damping = pr.f("simulation/damping").max(0.0) as f32;
|
||||
let reflect = pr.e("simulation/reflectEdges");
|
||||
|
||||
@@ -483,3 +483,18 @@ fn foam_flow_map_pop_velocity_wobble_textures_and_reflection() {
|
||||
let z2 = with(&[off.as_slice(), &[("producer/zoomProducerPoint", Value::Bool(true))]].concat(), 1.0);
|
||||
assert!((cx(&z2) - 8.0).abs() < 2.0 && cx(&z1) < 4.0, "{} vs {}", cx(&z2), cx(&z1));
|
||||
}
|
||||
|
||||
/// Wave World's vertical resolution follows the layer's aspect, but the grid as a whole stays
|
||||
/// within its cell budget however tall (or degenerate) the layer is.
|
||||
#[test]
|
||||
fn wave_world_grid_is_bounded() {
|
||||
use super::surfaces::{MAX_WAVE_CELLS, wave_grid};
|
||||
assert_eq!(wave_grid(60, 160.0, 90.0), (61, 35));
|
||||
assert_eq!(wave_grid(400, 100.0, 400.0), (401, 1601));
|
||||
for (res, lw, lh) in [(400, 1.0, 1e9), (400, 0.0, f64::MAX), (400, 1.0, f64::INFINITY), (400, 1.0, f64::NAN), (100_000, 1e6, 1e6)] {
|
||||
let (nx, ny) = wave_grid(res, lw, lh);
|
||||
assert!(nx >= 3 && ny >= 3 && nx * ny <= MAX_WAVE_CELLS, "{res} {lw}×{lh}: {nx}×{ny}");
|
||||
}
|
||||
let img = run("ec.sim.waveworld", &[("simulation/gridResolution", num(400.0))], Image::new(1, 4000), 0.1);
|
||||
assert_eq!((img.width, img.height), (1, 4000));
|
||||
}
|
||||
|
||||
+41
-27
@@ -501,7 +501,12 @@ impl Pg {
|
||||
if self.pex_radius > 0.0 {
|
||||
let mut out = Vec::with_capacity(st.parts.len());
|
||||
for q in std::mem::take(&mut st.parts) {
|
||||
if q.src == SRC_PARTICLE_EXPLODER || q.ch != 0 || q.r <= self.pex_radius * 1.01 || self.pex_affects.weight(&q) <= 0.5 || out.len() > 60_000 {
|
||||
if q.src == SRC_PARTICLE_EXPLODER
|
||||
|| q.ch != 0
|
||||
|| q.r <= self.pex_radius * 1.01
|
||||
|| self.pex_affects.weight(&q) <= 0.5
|
||||
|| out.len() > MAX_PG_PARTICLES
|
||||
{
|
||||
out.push(q);
|
||||
continue;
|
||||
}
|
||||
@@ -529,7 +534,7 @@ impl Pg {
|
||||
let n = st.carry.floor() as u32;
|
||||
st.carry -= n as f64;
|
||||
for _ in 0..n {
|
||||
if st.parts.len() >= 60_000 {
|
||||
if st.parts.len() >= MAX_PG_PARTICLES {
|
||||
break;
|
||||
}
|
||||
let id = st.next_id;
|
||||
@@ -668,7 +673,7 @@ impl Affects {
|
||||
fn pg_accelerated(ctx: &EffectCtx, key: u64, pg: &Pg) -> Option<std::sync::Arc<PgState>> {
|
||||
let backend = ctx.env.host?.particles()?;
|
||||
let steps = steps_at(ctx.time);
|
||||
let births = crate::psim::births(pg.rate, steps, 60_000)?;
|
||||
let births = crate::psim::births(pg.rate, steps, MAX_PG_PARTICLES)?;
|
||||
let desc = crate::psim::CannonDesc {
|
||||
pos: pg.pos,
|
||||
barrel_angle: pg.barrel_angle,
|
||||
@@ -721,6 +726,10 @@ fn particle_playground(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
playground_plan(ctx, &b).finish(b)
|
||||
}
|
||||
|
||||
/// Most particles Particle Playground keeps, from all its producers (Grid, Layer Exploder,
|
||||
/// Cannon, Particle Exploder).
|
||||
const MAX_PG_PARTICLES: usize = 60_000;
|
||||
|
||||
/// Particle Playground's [`PgPlan`] for buffer geometry `b` (its pixels are not read).
|
||||
pub fn playground_plan(ctx: &EffectCtx, b: &Buf) -> PgPlan {
|
||||
let pr = ctx.params;
|
||||
@@ -768,38 +777,43 @@ pub fn playground_plan(ctx: &EffectCtx, b: &Buf) -> PgPlan {
|
||||
let mut st = PgState::default();
|
||||
if grid_on && across > 0 && down > 0 {
|
||||
let mut k = 0usize;
|
||||
for j in 0..down {
|
||||
for i in 0..across {
|
||||
// Edit Grid Text: one character per grid point in reading order; spaces
|
||||
// leave the point empty.
|
||||
let ch = if grid_text.is_empty() {
|
||||
0
|
||||
} else {
|
||||
let c = grid_text[k % grid_text.len()];
|
||||
k += 1;
|
||||
if c == ' ' {
|
||||
continue;
|
||||
}
|
||||
c as u32
|
||||
};
|
||||
let fx = if across > 1 { i as f64 / (across - 1) as f64 - 0.5 } else { 0.0 };
|
||||
let fy = if down > 1 { j as f64 / (down - 1) as f64 - 0.5 } else { 0.0 };
|
||||
let p = [(gpos[0] + fx * gw) as f32, (gpos[1] + fy * gh) as f32];
|
||||
let id = st.next_id;
|
||||
st.next_id += 1;
|
||||
let mut q = PgParticle::new(p, [0.0; 2], gcol, grad, id, SRC_GRID);
|
||||
q.ch = ch;
|
||||
st.parts.push(q);
|
||||
}
|
||||
// The grid's points in reading order, at most the particle budget of them (the
|
||||
// counts come from the project: 10⁶ × 10⁶ would be a trillion particles).
|
||||
let points = (across as usize).saturating_mul(down as usize).min(MAX_PG_PARTICLES);
|
||||
for n in 0..points {
|
||||
let (i, j) = (n as u32 % across, n as u32 / across);
|
||||
// Edit Grid Text: one character per grid point in reading order; spaces
|
||||
// leave the point empty.
|
||||
let ch = if grid_text.is_empty() {
|
||||
0
|
||||
} else {
|
||||
let c = grid_text[k % grid_text.len()];
|
||||
k += 1;
|
||||
if c == ' ' {
|
||||
continue;
|
||||
}
|
||||
c as u32
|
||||
};
|
||||
let fx = if across > 1 { i as f64 / (across - 1) as f64 - 0.5 } else { 0.0 };
|
||||
let fy = if down > 1 { j as f64 / (down - 1) as f64 - 0.5 } else { 0.0 };
|
||||
let p = [(gpos[0] + fx * gw) as f32, (gpos[1] + fy * gh) as f32];
|
||||
let id = st.next_id;
|
||||
st.next_id += 1;
|
||||
let mut q = PgParticle::new(p, [0.0; 2], gcol, grad, id, SRC_GRID);
|
||||
q.ch = ch;
|
||||
st.parts.push(q);
|
||||
}
|
||||
}
|
||||
if let Some(e) = &exploder {
|
||||
let m = MapImg::from(ctx, e.clone());
|
||||
let step = (ex_r * 2.0).max(1.0);
|
||||
let mut y = step * 0.5;
|
||||
while y < lh {
|
||||
'explode: while y < lh {
|
||||
let mut x = step * 0.5;
|
||||
while x < lw {
|
||||
if st.parts.len() >= MAX_PG_PARTICLES {
|
||||
break 'explode;
|
||||
}
|
||||
let c = m.at(x, y);
|
||||
if c[3] > 0.05 {
|
||||
let id = st.next_id;
|
||||
|
||||
@@ -370,3 +370,21 @@ fn cc_hair_grows_from_opaque_pixels() {
|
||||
let none = run_fx("ec.sim.cchair", &[("length", num(0.0))], img.clone(), 0.0, EffectEnv::default());
|
||||
assert_eq!(none.img.data, img.data);
|
||||
}
|
||||
|
||||
/// Grid counts from a project are held to the particle budget instead of asking for a
|
||||
/// trillion particles (or visiting a trillion empty grid points behind a blank Grid Text).
|
||||
#[test]
|
||||
fn hostile_grid_counts_stay_within_the_particle_budget() {
|
||||
for text in ["", " "] {
|
||||
let img = run(
|
||||
&[
|
||||
("cannonEnabled", Value::Bool(false)),
|
||||
("grid/particlesAcross", num(1e9)),
|
||||
("grid/particlesDown", num(1e9)),
|
||||
("options/gridText", Value::Str(text.into())),
|
||||
],
|
||||
0.0,
|
||||
);
|
||||
assert_eq!((img.width, img.height), (80, 60));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user