1742 lines
76 KiB
Rust
1742 lines
76 KiB
Rust
//! Simulation effects (particles, physics, liquids): CC Rainfall, CC Snowfall, CC Bubbles,
|
||
//! CC Drizzle, CC Star Burst, CC Particle World, CC Particle Systems II and CC Mr. Mercury.
|
||
//!
|
||
//! Written from the public descriptions of the effects' behaviour. Weather/bubble/star effects
|
||
//! are closed-form functions of time (every frame independent); particle systems are stepped
|
||
//! from layer time 0 at a fixed rate through [`SimCache`], so any frame can be rendered
|
||
//! directly and equals the frame reached by scrubbing.
|
||
//!
|
||
//! Shared here (also used by `sim2`): a band-bucketed rasteriser for many small items
|
||
//! ([`raster`]), particle sprites ([`Sprite`]) and transfer-mode compositing ([`combine`]).
|
||
|
||
use effectcraft_keyframe::Value;
|
||
use effectcraft_project::ParamUi;
|
||
use effectcraft_raster::{Image, Px};
|
||
use rayon::prelude::*;
|
||
|
||
use crate::generate::value_noise;
|
||
use crate::util::{SimCache, hash1, params_key, unpremul};
|
||
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||
|
||
pub(crate) fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {
|
||
EffectSpec { id, name, category: "Simulation", params, render, gpu: false, float: true }
|
||
}
|
||
|
||
pub(crate) fn pt(x: f64, y: f64) -> Value {
|
||
Value::Vec2([x, y])
|
||
}
|
||
|
||
pub(crate) fn pct() -> ParamUi {
|
||
slider(0.0, 100.0, 0.0, 100.0, 1)
|
||
}
|
||
|
||
/// Deterministic hash → [0, 1).
|
||
#[inline]
|
||
pub(crate) fn h(i: u32, k: u32, seed: u32) -> f32 {
|
||
hash1(i, k, seed)
|
||
}
|
||
|
||
/// Signed hash → [-1, 1).
|
||
#[inline]
|
||
pub(crate) fn hs(i: u32, k: u32, seed: u32) -> f32 {
|
||
hash1(i, k, seed) * 2.0 - 1.0
|
||
}
|
||
|
||
// ------------------------------------------------------------------ band rasteriser
|
||
|
||
const BAND: usize = 16;
|
||
|
||
/// How rasterised items combine with what is already in the target.
|
||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||
pub enum Acc {
|
||
/// Premultiplied "over" (later items on top).
|
||
Over,
|
||
/// Additive (glows, light).
|
||
Add,
|
||
}
|
||
|
||
/// Rasterise many small items into `img` (buffer pixels). `bbox` gives an item's pixel bounds
|
||
/// `[x0, y0, x1, y1]` (or `None` to skip), `shade` its premultiplied colour at a pixel centre.
|
||
/// Items are bucketed into horizontal bands rendered in parallel; within a band they are drawn
|
||
/// in slice order.
|
||
pub(crate) fn raster<T: Sync>(
|
||
img: &mut Image,
|
||
items: &[T],
|
||
bbox: impl Fn(&T) -> Option<[f32; 4]> + Sync,
|
||
shade: impl Fn(&T, f32, f32) -> Option<Px> + Sync,
|
||
acc: Acc,
|
||
) {
|
||
let (w, hgt) = (img.width as usize, img.height as usize);
|
||
if w == 0 || hgt == 0 || items.is_empty() {
|
||
return;
|
||
}
|
||
let nb = hgt.div_ceil(BAND);
|
||
let boxes: Vec<Option<[i32; 4]>> = items
|
||
.par_iter()
|
||
.map(|it| {
|
||
let b = bbox(it)?;
|
||
if !(b[0].is_finite() && b[1].is_finite() && b[2].is_finite() && b[3].is_finite()) {
|
||
return None;
|
||
}
|
||
let x0 = b[0].floor().max(0.0) as i32;
|
||
let y0 = b[1].floor().max(0.0) as i32;
|
||
let x1 = (b[2].ceil() as i32).min(w as i32 - 1);
|
||
let y1 = (b[3].ceil() as i32).min(hgt as i32 - 1);
|
||
if x1 < x0 || y1 < y0 || b[2] < 0.0 || b[3] < 0.0 {
|
||
return None;
|
||
}
|
||
Some([x0, y0, x1, y1])
|
||
})
|
||
.collect();
|
||
let mut buckets: Vec<Vec<u32>> = vec![Vec::new(); nb];
|
||
for (i, b) in boxes.iter().enumerate() {
|
||
if let Some(b) = b {
|
||
for band in (b[1] as usize / BAND)..=(b[3] as usize / BAND).min(nb - 1) {
|
||
buckets[band].push(i as u32);
|
||
}
|
||
}
|
||
}
|
||
img.data.par_chunks_mut(w * BAND).enumerate().for_each(|(band, chunk)| {
|
||
let by0 = band * BAND;
|
||
let rows = chunk.len() / w;
|
||
for &i in &buckets[band] {
|
||
let Some(b) = boxes[i as usize] else { continue };
|
||
let it = &items[i as usize];
|
||
let ya = (b[1] as usize).max(by0);
|
||
let yb = (b[3] as usize).min(by0 + rows - 1);
|
||
for y in ya..=yb {
|
||
let row = &mut chunk[(y - by0) * w..(y - by0 + 1) * w];
|
||
for x in b[0] as usize..=b[2] as usize {
|
||
if let Some(c) = shade(it, x as f32 + 0.5, y as f32 + 0.5) {
|
||
let d = &mut row[x];
|
||
match acc {
|
||
Acc::Over => {
|
||
let k = 1.0 - c[3];
|
||
*d = [c[0] + d[0] * k, c[1] + d[1] * k, c[2] + d[2] * k, c[3] + d[3] * k];
|
||
}
|
||
Acc::Add => {
|
||
*d = [d[0] + c[0], d[1] + c[1], d[2] + c[2], (d[3] + c[3]).min(1.0)];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// ------------------------------------------------------------------ sprites
|
||
|
||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||
pub enum Shape {
|
||
/// Soft round dot (Gaussian-like falloff).
|
||
Soft,
|
||
/// Hard anti-aliased disc.
|
||
Disc,
|
||
/// Lit sphere (upper-left light, specular).
|
||
Sphere,
|
||
/// Disc fading linearly to its rim.
|
||
Faded,
|
||
/// Thin ring with a highlight.
|
||
Bubble,
|
||
/// Four-point star.
|
||
Star,
|
||
/// Rotated square (angle in `rot`).
|
||
Square,
|
||
/// Triangle (angle in `rot`).
|
||
Tri,
|
||
/// Segment from (x, y) by (dx, dy), half-width `r`.
|
||
Line { dx: f32, dy: f32 },
|
||
}
|
||
|
||
/// A particle sprite in buffer pixels; `c` is straight colour + opacity.
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub struct Sprite {
|
||
pub x: f32,
|
||
pub y: f32,
|
||
pub r: f32,
|
||
pub c: [f32; 4],
|
||
pub shape: Shape,
|
||
pub rot: f32,
|
||
}
|
||
|
||
impl Sprite {
|
||
pub(crate) fn new(x: f32, y: f32, r: f32, c: [f32; 4], shape: Shape) -> Sprite {
|
||
Sprite { x, y, r, c, shape, rot: 0.0 }
|
||
}
|
||
}
|
||
|
||
/// Where a sprite's colour comes from when it depends on the layer's pixels (resolved by
|
||
/// [`SpritePlan::resolve`] on the CPU, per pixel by the GPU compositor). Positions are buffer
|
||
/// pixels.
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub enum Tint {
|
||
/// The sprite's own colour.
|
||
Fixed,
|
||
/// CC Rainfall's refracting drops: where the layer shows (alpha > 0), colour × (layer
|
||
/// colour × 0.6 + 0.5) sampled at (x, y).
|
||
Refract { x: f64, y: f64 },
|
||
/// CC Star Burst: the layer's colour at (x, y), opacity × its alpha (none where clear).
|
||
Source { x: f64, y: f64 },
|
||
/// CC Hair: the root's colour at (x, y) mixed with the hair colour (`inherit` = 1 keeps the
|
||
/// root's), × `shade`, + `spec`; no strand where the root's alpha is below ½.
|
||
Root { x: f64, y: f64, hair: [f32; 3], inherit: f32, shade: f32, spec: f32 },
|
||
}
|
||
|
||
/// How a sprite layer meets the original layer.
|
||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||
pub enum Post {
|
||
/// [`combine`] with this mode (0 Composite … 5 sprites only).
|
||
Combine(u32),
|
||
/// The sprites alone.
|
||
Replace,
|
||
/// The sprites, mixed toward the original by this fraction.
|
||
Lerp(f32),
|
||
/// The sprites over opaque black.
|
||
OverBlack,
|
||
}
|
||
|
||
/// What a sprite-drawing effect draws this frame: built on the CPU (closed-form or from the
|
||
/// simulation state), rasterised by [`SpritePlan::finish`] or the GPU compositor.
|
||
#[derive(Clone, Debug)]
|
||
pub struct SpritePlan {
|
||
pub sprites: Vec<Sprite>,
|
||
/// Per sprite (empty = all [`Tint::Fixed`]).
|
||
pub tints: Vec<Tint>,
|
||
pub acc: Acc,
|
||
pub post: Post,
|
||
}
|
||
|
||
impl SpritePlan {
|
||
/// Resolve layer-dependent colours against the layer `src`.
|
||
pub fn resolve(&mut self, src: &Image) {
|
||
let tints = std::mem::take(&mut self.tints);
|
||
self.sprites.par_iter_mut().zip(tints.par_iter()).for_each(|(sp, t)| match *t {
|
||
Tint::Fixed => {}
|
||
Tint::Refract { x, y } => {
|
||
let (sc, sa) = unpremul(src.sample_bilinear(x, y));
|
||
if sa > 0.0 {
|
||
let c = sp.c;
|
||
sp.c = [(sc[0] * 0.6 + 0.5) * c[0], (sc[1] * 0.6 + 0.5) * c[1], (sc[2] * 0.6 + 0.5) * c[2], c[3]];
|
||
}
|
||
}
|
||
Tint::Source { x, y } => {
|
||
let (c, a) = unpremul(src.sample_bilinear(x, y));
|
||
sp.c = if a <= 0.0 { [0.0; 4] } else { [c[0], c[1], c[2], a * sp.c[3]] };
|
||
}
|
||
Tint::Root { x, y, hair, inherit, shade, spec } => {
|
||
let root = src.sample_bilinear(x, y);
|
||
if root[3] < 0.5 {
|
||
sp.c = [0.0; 4];
|
||
return;
|
||
}
|
||
let (rc, _) = unpremul(root);
|
||
let base: [f32; 3] = std::array::from_fn(|k| rc[k] + (hair[k] - rc[k]) * (1.0 - inherit));
|
||
sp.c = [base[0] * shade + spec, base[1] * shade + spec, base[2] * shade + spec, sp.c[3]];
|
||
}
|
||
});
|
||
}
|
||
|
||
/// Rasterise (after [`SpritePlan::resolve`]) and meet the original layer `b`.
|
||
pub fn finish(&self, mut b: Buf) -> Buf {
|
||
let fx = splat(b.img.width, b.img.height, &self.sprites, self.acc);
|
||
b.img = match self.post {
|
||
Post::Combine(m) => combine(&b.img, &fx, m),
|
||
Post::Replace => fx,
|
||
Post::Lerp(t) => {
|
||
let mut o = fx;
|
||
o.data.par_iter_mut().zip(b.img.data.par_iter()).for_each(|(o, s0)| *o = crate::util::lerp4(*o, *s0, t));
|
||
o
|
||
}
|
||
Post::OverBlack => combine(&Image::filled(b.img.width, b.img.height, [0.0, 0.0, 0.0, 1.0]), &fx, 0),
|
||
};
|
||
b
|
||
}
|
||
}
|
||
|
||
/// The sprite plan of a sprite-drawing simulation effect for buffer geometry `b` (its pixels
|
||
/// are not read), `None` for other effects or settings drawn otherwise (the GPU compositor's
|
||
/// render pass).
|
||
pub fn sprite_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option<SpritePlan> {
|
||
match id {
|
||
"ec.sim.ccrainfall" => Some(rainfall_plan(ctx, b)),
|
||
"ec.sim.ccsnowfall" => Some(snowfall_plan(ctx, b)),
|
||
"ec.sim.ccstarburst" => Some(star_burst_plan(ctx, b)),
|
||
"ec.sim.cchair" => crate::sim3::hair_plan(ctx, b),
|
||
"ec.sim.waveworld" => crate::sim2::wave_world_plan(ctx, b).and_then(|p| match p {
|
||
crate::sim2::WavePlan::Wire(sp) => Some(sp),
|
||
crate::sim2::WavePlan::Height { .. } => None,
|
||
}),
|
||
"ec.sim.foam" => crate::sim2::foam_plan(ctx, b),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
fn sprite_bbox(s: &Sprite) -> Option<[f32; 4]> {
|
||
if s.c[3] <= 1e-5 || s.r <= 0.0 {
|
||
return None;
|
||
}
|
||
let r = s.r.max(0.5) + 1.0;
|
||
Some(match s.shape {
|
||
Shape::Line { dx, dy } => [s.x.min(s.x + dx) - r, s.y.min(s.y + dy) - r, s.x.max(s.x + dx) + r, s.y.max(s.y + dy) + r],
|
||
Shape::Star => [s.x - r * 2.0, s.y - r * 2.0, s.x + r * 2.0, s.y + r * 2.0],
|
||
_ => [s.x - r, s.y - r, s.x + r, s.y + r],
|
||
})
|
||
}
|
||
|
||
#[inline]
|
||
fn aa(edge: f32) -> f32 {
|
||
(edge + 0.5).clamp(0.0, 1.0)
|
||
}
|
||
|
||
fn sprite_shade(s: &Sprite, x: f32, y: f32) -> Option<Px> {
|
||
// Sub-pixel sprites keep their energy: draw at radius 0.5 with reduced opacity.
|
||
let (r, fade) = if s.r < 0.5 { (0.5, s.r / 0.5) } else { (s.r, 1.0) };
|
||
let (dx, dy) = (x - s.x, y - s.y);
|
||
let mut rgb = [s.c[0], s.c[1], s.c[2]];
|
||
let cov = match s.shape {
|
||
Shape::Soft => {
|
||
let d2 = (dx * dx + dy * dy) / (r * r);
|
||
if d2 >= 1.0 {
|
||
return None;
|
||
}
|
||
let q = 1.0 - d2;
|
||
q * q
|
||
}
|
||
Shape::Disc => aa(r - (dx * dx + dy * dy).sqrt()),
|
||
Shape::Faded => {
|
||
let d = (dx * dx + dy * dy).sqrt() / r;
|
||
(1.0 - d).max(0.0).powf(1.5)
|
||
}
|
||
Shape::Sphere => {
|
||
let d = (dx * dx + dy * dy).sqrt();
|
||
let cv = aa(r - d);
|
||
if cv <= 0.0 {
|
||
return None;
|
||
}
|
||
let (nx, ny) = (dx / r, dy / r);
|
||
let nz = (1.0 - nx * nx - ny * ny).max(0.0).sqrt();
|
||
let (lx, ly, lz) = (-0.48, -0.58, 0.66);
|
||
let diff = (nx * lx + ny * ly + nz * lz).max(0.0);
|
||
let rz = 2.0 * diff * nz - lz;
|
||
let spec = rz.max(0.0).powi(20) * 0.6;
|
||
let k = 0.2 + 0.8 * diff;
|
||
rgb = [rgb[0] * k + spec, rgb[1] * k + spec, rgb[2] * k + spec];
|
||
cv
|
||
}
|
||
Shape::Bubble => {
|
||
let d = (dx * dx + dy * dy).sqrt() / r;
|
||
if d > 1.0 + 1.0 / r {
|
||
return None;
|
||
}
|
||
let ring = (1.0 - ((d - 0.9) / 0.12).powi(2)).max(0.0);
|
||
let hl = (1.0 - (((dx / r) + 0.4).powi(2) + ((dy / r) + 0.4).powi(2)) / 0.04).max(0.0);
|
||
(0.12 * (d <= 1.0) as u8 as f32 + ring * 0.8 + hl).min(1.0)
|
||
}
|
||
Shape::Star => {
|
||
let (sn, cs) = s.rot.sin_cos();
|
||
let (ux, uy) = ((dx * cs + dy * sn) / r, (-dx * sn + dy * cs) / r);
|
||
let d = (ux * ux + uy * uy).sqrt();
|
||
let core = (1.0 - d).max(0.0).powi(2);
|
||
let sx = (1.0 - ux.abs() / 2.0).max(0.0) * (1.0 - (uy.abs() * 6.0)).max(0.0);
|
||
let sy = (1.0 - uy.abs() / 2.0).max(0.0) * (1.0 - (ux.abs() * 6.0)).max(0.0);
|
||
(core + sx + sy).min(1.0)
|
||
}
|
||
Shape::Square => {
|
||
let (sn, cs) = s.rot.sin_cos();
|
||
let (ux, uy) = (dx * cs + dy * sn, -dx * sn + dy * cs);
|
||
let e = r * 0.75;
|
||
aa(e - ux.abs()) * aa(e - uy.abs())
|
||
}
|
||
Shape::Tri => {
|
||
let (sn, cs) = s.rot.sin_cos();
|
||
let (ux, uy) = (dx * cs + dy * sn, -dx * sn + dy * cs);
|
||
// Equilateral triangle with circumradius r: three half-planes.
|
||
let e = r * 0.5;
|
||
let mut m = f32::INFINITY;
|
||
for k in 0..3 {
|
||
let a = std::f32::consts::FRAC_PI_2 + k as f32 * std::f32::consts::TAU / 3.0;
|
||
let (ns, nc) = a.sin_cos();
|
||
m = m.min(e - (ux * nc + uy * ns));
|
||
}
|
||
aa(m)
|
||
}
|
||
Shape::Line { dx: lx, dy: ly } => {
|
||
let l2 = lx * lx + ly * ly;
|
||
let t = if l2 > 0.0 { ((dx * lx + dy * ly) / l2).clamp(0.0, 1.0) } else { 0.0 };
|
||
let (px, py) = (dx - lx * t, dy - ly * t);
|
||
aa(r - (px * px + py * py).sqrt())
|
||
}
|
||
};
|
||
let a = cov * s.c[3] * fade;
|
||
if a <= 1e-6 {
|
||
return None;
|
||
}
|
||
Some([rgb[0] * a, rgb[1] * a, rgb[2] * a, a])
|
||
}
|
||
|
||
/// Draw sprites into a new transparent image of `w`×`h`.
|
||
pub(crate) fn splat(w: u32, hgt: u32, sprites: &[Sprite], acc: Acc) -> Image {
|
||
let mut img = Image::new(w, hgt);
|
||
raster(&mut img, sprites, sprite_bbox, sprite_shade, acc);
|
||
img
|
||
}
|
||
|
||
/// Combine an effect layer `fx` with the original `src` (both premultiplied, same size):
|
||
/// 0 Composite (fx over src), 1 Screen, 2 Add, 3 Lighten, 4 Darken, 5 fx only.
|
||
pub(crate) fn combine(src: &Image, fx: &Image, mode: u32) -> Image {
|
||
let mut out = fx.clone();
|
||
out.data.par_iter_mut().zip(src.data.par_iter()).for_each(|(o, s)| {
|
||
let f = *o;
|
||
*o = match mode {
|
||
0 => {
|
||
let k = 1.0 - f[3];
|
||
[f[0] + s[0] * k, f[1] + s[1] * k, f[2] + s[2] * k, f[3] + s[3] * k]
|
||
}
|
||
1 => [s[0] + f[0] - s[0] * f[0], s[1] + f[1] - s[1] * f[1], s[2] + f[2] - s[2] * f[2], s[3] + f[3] - s[3] * f[3]],
|
||
2 => [s[0] + f[0], s[1] + f[1], s[2] + f[2], (s[3] + f[3]).min(1.0)],
|
||
3 => [s[0].max(f[0]), s[1].max(f[1]), s[2].max(f[2]), s[3] + f[3] - s[3] * f[3]],
|
||
4 => {
|
||
let (fc, fa) = unpremul(f);
|
||
let m = |c: f32, k: usize| c - (c - c.min(fc[k] * s[3])) * fa;
|
||
[m(s[0], 0), m(s[1], 1), m(s[2], 2), s[3]]
|
||
}
|
||
_ => f,
|
||
};
|
||
});
|
||
out
|
||
}
|
||
|
||
// ------------------------------------------------------------------ CC Rainfall / CC Snowfall
|
||
|
||
/// Shared weather layout: per-item depth factor `k` in (0, 1] (1 = nearest) from Scene Depth.
|
||
#[inline]
|
||
fn depth_k(z: f32, scene_depth: f32) -> f32 {
|
||
1.0 / (1.0 + z * scene_depth / 2500.0)
|
||
}
|
||
|
||
/// Wrap `v` into `[lo, lo + span)`.
|
||
#[inline]
|
||
fn wrap(v: f32, lo: f32, span: f32) -> f32 {
|
||
if span <= 0.0 { v } else { lo + (v - lo).rem_euclid(span) }
|
||
}
|
||
|
||
fn rainfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
||
let mut plan = rainfall_plan(ctx, &b);
|
||
plan.resolve(&b.img);
|
||
plan.finish(b)
|
||
}
|
||
|
||
fn rainfall_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||
let pr = ctx.params;
|
||
let n = pr.f("drops").clamp(0.0, 100_000.0) as u32;
|
||
let size = pr.f("size").max(0.0) as f32;
|
||
let depth = pr.f("sceneDepth").max(0.0) as f32;
|
||
let speed = pr.f("speed") as f32;
|
||
let wind = pr.f("wind") as f32;
|
||
let wvar = pr.f("windVariation") as f32 / 100.0;
|
||
let spread = pr.f("spread") as f32;
|
||
let color = pr.color("color");
|
||
let opacity = pr.f("opacity") as f32 / 100.0;
|
||
let mode = pr.e("transferMode");
|
||
let with_orig = pr.b("compositeWithOriginal");
|
||
let refract = pr.e("extras/appearance") == 0;
|
||
let off = pr.v2("extras/offset");
|
||
let seed = pr.f("extras/randomSeed") as u32 ^ ctx.seed.wrapping_mul(0x9e37);
|
||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||
let s = b.scale as f32;
|
||
let t = ctx.time as f32;
|
||
let (ox, oy) = (off[0] as f32 - lw * 0.5, off[1] as f32 - lh * 0.5);
|
||
let (sprites, tints): (Vec<Sprite>, Vec<Tint>) = (0..n)
|
||
.into_par_iter()
|
||
.map(|i| {
|
||
let z = h(i, 1, seed);
|
||
let k = depth_k(z, depth);
|
||
let vy = speed / 4000.0 * lh * 2.2 * k;
|
||
let w = wind / 1000.0 * lh * (1.0 + wvar * hs(i, 2, seed)) * k + spread * hs(i, 7, seed) * k;
|
||
let len = (vy * 0.04 * size.max(0.2)).max(1.0);
|
||
let lenx = w * 0.04 * size.max(0.2);
|
||
let margin = len + lenx.abs() + 4.0;
|
||
let y = wrap(h(i, 3, seed) * (lh + 2.0 * margin) + vy * t + oy, -margin, lh + 2.0 * margin);
|
||
let x = wrap(h(i, 4, seed) * (lw + 2.0 * margin) + w * t + ox, -margin, lw + 2.0 * margin);
|
||
let (bx, by) = b.to_px([x as f64, y as f64]);
|
||
let c = [color[0], color[1], color[2], opacity * (0.35 + 0.65 * k)];
|
||
// Refracting drops show the scene behind them, brightened and offset.
|
||
let tint = if refract { Tint::Refract { x: bx + 2.0 * s as f64, y: by } } else { Tint::Fixed };
|
||
let r = (size * 0.6 * k * s).max(0.25);
|
||
(Sprite::new(bx as f32, by as f32, r, c, Shape::Line { dx: -lenx * s, dy: -len * s }), tint)
|
||
})
|
||
.unzip();
|
||
SpritePlan { sprites, tints, acc: Acc::Over, post: weather_post(mode, with_orig) }
|
||
}
|
||
|
||
fn weather_post(mode: u32, with_orig: bool) -> Post {
|
||
if with_orig { Post::Combine(if mode == 1 { 3 } else { 0 }) } else { Post::Replace }
|
||
}
|
||
|
||
fn snowfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
||
snowfall_plan(ctx, &b).finish(b)
|
||
}
|
||
|
||
fn snowfall_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||
let pr = ctx.params;
|
||
let n = pr.f("flakes").clamp(0.0, 200_000.0) as u32;
|
||
let size = pr.f("size").max(0.0) as f32;
|
||
let svar = pr.f("sizeVariation") as f32 / 100.0;
|
||
let depth = pr.f("sceneDepth").max(0.0) as f32;
|
||
let speed = pr.f("speed") as f32;
|
||
let spvar = pr.f("speedVariation") as f32 / 100.0;
|
||
let wind = pr.f("wind") as f32;
|
||
let wvar = pr.f("windVariation") as f32 / 100.0;
|
||
let spread = pr.f("spread") as f32;
|
||
let wig = pr.f("wiggle/wiggleAmount") as f32;
|
||
let wigvar = pr.f("wiggle/wiggleAmountVariation") as f32 / 100.0;
|
||
let wfreq = pr.f("wiggle/wiggleFrequency") as f32;
|
||
let wfvar = pr.f("wiggle/wiggleFrequencyVariation") as f32 / 100.0;
|
||
let color = pr.color("color");
|
||
let opacity = pr.f("opacity") as f32 / 100.0;
|
||
let mode = pr.e("transferMode");
|
||
let with_orig = pr.b("compositeWithOriginal");
|
||
let off = pr.v2("extras/offset");
|
||
let seed = pr.f("extras/randomSeed") as u32 ^ ctx.seed.wrapping_mul(0x85eb);
|
||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||
let s = b.scale as f32;
|
||
let t = ctx.time as f32;
|
||
let (ox, oy) = (off[0] as f32 - lw * 0.5, off[1] as f32 - lh * 0.5);
|
||
let sprites: Vec<Sprite> = (0..n)
|
||
.into_par_iter()
|
||
.map(|i| {
|
||
let z = h(i, 1, seed);
|
||
let k = depth_k(z, depth);
|
||
let r = (size * 0.5 * k * (1.0 + svar * hs(i, 2, seed))).max(0.05);
|
||
let vy = speed * (lh / 600.0) * k * (1.0 + spvar * hs(i, 3, seed));
|
||
let vx = wind * (lh / 1200.0) * k * (1.0 + wvar * hs(i, 4, seed)) + spread * hs(i, 8, seed) * k;
|
||
let amp = wig * 2.0 * k * (1.0 + wigvar * hs(i, 5, seed));
|
||
let fr = wfreq * (1.0 + wfvar * hs(i, 6, seed));
|
||
let ph = h(i, 9, seed) * std::f32::consts::TAU;
|
||
let margin = r + amp.abs() + 2.0;
|
||
let y = wrap(h(i, 10, seed) * (lh + 2.0 * margin) + vy * t + oy, -margin, lh + 2.0 * margin);
|
||
let x = wrap(h(i, 11, seed) * (lw + 2.0 * margin) + vx * t + ox, -margin, lw + 2.0 * margin) + amp * (fr * t * std::f32::consts::TAU + ph).sin();
|
||
let (bx, by) = b.to_px([x as f64, y as f64]);
|
||
Sprite::new(bx as f32, by as f32, r * s, [color[0], color[1], color[2], opacity * (0.4 + 0.6 * k)], Shape::Soft)
|
||
})
|
||
.collect();
|
||
SpritePlan { sprites, tints: vec![], acc: Acc::Over, post: weather_post(mode, with_orig) }
|
||
}
|
||
|
||
// ------------------------------------------------------------------ CC Bubbles
|
||
|
||
/// A CC Bubbles bubble in buffer pixels.
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub struct Bubble {
|
||
pub x: f32,
|
||
pub y: f32,
|
||
pub r: f32,
|
||
/// Source sample centre (buffer px).
|
||
pub sx: f32,
|
||
pub sy: f32,
|
||
}
|
||
|
||
/// CC Bubbles' bubbles this frame (closed-form in time) for buffer geometry `b`.
|
||
pub fn bubble_list(ctx: &EffectCtx, b: &Buf) -> Vec<Bubble> {
|
||
let pr = ctx.params;
|
||
let n = pr.f("bubbleAmount").clamp(0.0, 10_000.0) as u32;
|
||
let speed = pr.f("bubbleSpeed") as f32;
|
||
let wamp = pr.f("wobbleAmplitude") as f32;
|
||
let wfreq = pr.f("wobbleFrequency") as f32;
|
||
let bsize = pr.f("bubbleSize").max(0.0) as f32;
|
||
let seed = ctx.seed.wrapping_mul(0x27d4);
|
||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||
let s = b.scale as f32;
|
||
let t = ctx.time as f32;
|
||
let base = lw.min(lh) * 0.035 * bsize;
|
||
(0..n)
|
||
.map(|i| {
|
||
let r = base * (0.5 + h(i, 1, seed));
|
||
let travel = lh + 2.0 * r;
|
||
let v = speed * lh * 0.3 * (0.6 + 0.8 * h(i, 2, seed));
|
||
let y = lh + r - (h(i, 3, seed) * travel + v * t).rem_euclid(travel);
|
||
let x0 = h(i, 4, seed) * lw;
|
||
let x = x0 + wamp * (r / base.max(1e-3)) * (t * wfreq * std::f32::consts::TAU + h(i, 5, seed) * std::f32::consts::TAU).sin();
|
||
let (bx, by) = b.to_px([x as f64, y as f64]);
|
||
let (sx, sy) = b.to_px([x0 as f64, y as f64]);
|
||
Bubble { x: bx as f32, y: by as f32, r: r * s, sx: sx as f32, sy: sy as f32 }
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn bubbles(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let pr = ctx.params;
|
||
let metal = pr.e("reflectionType") == 1;
|
||
let shading = pr.e("shadingType");
|
||
let list = bubble_list(ctx, &b);
|
||
let src = b.img.clone();
|
||
let mut out = Image::new(src.width, src.height);
|
||
raster(
|
||
&mut out,
|
||
&list,
|
||
|q| Some([q.x - q.r - 1.0, q.y - q.r - 1.0, q.x + q.r + 1.0, q.y + q.r + 1.0]),
|
||
|q, x, y| {
|
||
let (dx, dy) = ((x - q.x) / q.r.max(0.5), (y - q.y) / q.r.max(0.5));
|
||
let d2 = dx * dx + dy * dy;
|
||
let d = d2.sqrt();
|
||
let cov = aa((1.0 - d) * q.r);
|
||
if cov <= 0.0 {
|
||
return None;
|
||
}
|
||
// Liquid refracts (inverted, magnified), metal reflects (wide-angle mirror).
|
||
let k = if metal { 1.6 } else { -0.7 };
|
||
let nz = (1.0 - d2).max(0.0).sqrt();
|
||
let p = src.sample_bilinear((q.sx + dx * q.r * k) as f64, (q.sy + dy * q.r * k) as f64);
|
||
let (c, sa) = unpremul(p);
|
||
let mut c = if sa <= 0.0 { [0.6, 0.7, 0.8] } else { c };
|
||
let mut a = cov;
|
||
match shading {
|
||
1 => c = c.map(|v| v * 0.7 + 0.3 * (1.0 - nz)),
|
||
2 => c = c.map(|v| v * (0.5 + 0.5 * nz)),
|
||
3 => a *= nz.powf(0.5),
|
||
4 => a *= 1.0 - nz * 0.8,
|
||
_ => {}
|
||
}
|
||
let hl = (1.0 - ((dx + 0.35).powi(2) + (dy + 0.35).powi(2)) / 0.05).max(0.0) * 0.8;
|
||
let c = c.map(|v| v + hl);
|
||
Some([c[0] * a, c[1] * a, c[2] * a, a])
|
||
},
|
||
Acc::Over,
|
||
);
|
||
b.img = out;
|
||
b
|
||
}
|
||
|
||
// ------------------------------------------------------------------ CC Drizzle
|
||
|
||
/// CC Drizzle's live drops this frame (buffer px: centre x, y, ring radius, amplitude);
|
||
/// `None` = no drops yet (the layer passes through).
|
||
pub fn drizzle_drops(ctx: &EffectCtx, b: &Buf) -> Option<Vec<[f32; 4]>> {
|
||
let pr = ctx.params;
|
||
let rate = pr.f("dripRate").max(0.0);
|
||
let life = pr.f("longevity").max(0.01);
|
||
let spreading = pr.f("spreading") as f32;
|
||
let height = pr.f("rippleHeight") as f32 / 100.0;
|
||
let seed = ctx.seed.wrapping_mul(0x165667b1);
|
||
let t = ctx.time;
|
||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||
let s = b.scale as f32;
|
||
if rate <= 0.0 || t < 0.0 {
|
||
return None;
|
||
}
|
||
// Drops born at k / rate with hashed positions; alive while younger than the longevity.
|
||
let k0 = ((t - life) * rate).floor().max(0.0) as u32;
|
||
let k1 = (t * rate).floor() as u32;
|
||
let mut drops = Vec::new();
|
||
for k in k0..=k1 {
|
||
let born = k as f64 / rate;
|
||
let age = (t - born) as f32;
|
||
if age < 0.0 || age as f64 >= life {
|
||
continue;
|
||
}
|
||
let (x, y) = b.to_px([(h(k, 1, seed) * lw) as f64, (h(k, 2, seed) * lh) as f64]);
|
||
let radius = spreading * age * lh / 400.0 * s;
|
||
let amp = height * (1.0 - age / life as f32) * (0.6 + 0.4 * h(k, 3, seed));
|
||
drops.push([x as f32, y as f32, radius, amp]);
|
||
}
|
||
Some(drops)
|
||
}
|
||
|
||
fn drizzle(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let pr = ctx.params;
|
||
let Some(drops) = drizzle_drops(ctx, &b) else { return b };
|
||
let rippling = (pr.f("rippling") / 360.0).max(0.05) as f32;
|
||
let disp = pr.f("displacement") as f32;
|
||
let li = pr.f("light/lightIntensity") as f32 / 100.0;
|
||
let lc = pr.color("light/lightColor");
|
||
let lheight = pr.f("light/lightHeight") as f32 / 100.0;
|
||
let ldir = (pr.f("light/lightDirection") as f32).to_radians();
|
||
let ambient = pr.f("shading/ambient") as f32 / 100.0;
|
||
let diffuse = pr.f("shading/diffuse") as f32 / 100.0;
|
||
let specular = pr.f("shading/specular") as f32 / 100.0;
|
||
let rough = pr.f("shading/roughness").max(0.001) as f32;
|
||
let lh = ctx.layer_size[1] as f32;
|
||
let s = b.scale as f32;
|
||
let wavelen = (lh * 0.04 * s).max(2.0) / rippling.clamp(0.25, 4.0);
|
||
let width = wavelen * (1.0 + rippling * 2.0);
|
||
let field = |x: f32, y: f32| -> f32 {
|
||
let mut v = 0.0;
|
||
for &[dx, dy, r, a] in &drops {
|
||
let d = ((x - dx).powi(2) + (y - dy).powi(2)).sqrt();
|
||
let u = (d - r) / width;
|
||
if u.abs() < 1.0 {
|
||
let env = 0.5 + 0.5 * (u * std::f32::consts::PI).cos();
|
||
v += a * env * ((d - r) / wavelen * std::f32::consts::TAU).cos();
|
||
}
|
||
}
|
||
v
|
||
};
|
||
let src = b.img.clone();
|
||
let (ls, lcs) = ldir.sin_cos();
|
||
let l = {
|
||
let v = [lcs, ls, lheight.max(0.05)];
|
||
let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
||
[v[0] / n, v[1] / n, v[2] / n]
|
||
};
|
||
b.img.rows_mut().for_each(|(y, row)| {
|
||
for (x, px) in row.iter_mut().enumerate() {
|
||
let (fx, fy) = (x as f32 + 0.5, y as f32 + 0.5);
|
||
let gx = field(fx + 1.0, fy) - field(fx - 1.0, fy);
|
||
let gy = field(fx, fy + 1.0) - field(fx, fy - 1.0);
|
||
if gx == 0.0 && gy == 0.0 {
|
||
continue;
|
||
}
|
||
let p = src.sample_bilinear_clamped((fx - gx * disp * s * 4.0) as f64, (fy - gy * disp * s * 4.0) as f64);
|
||
let n = {
|
||
let v = [-gx * 6.0, -gy * 6.0, 1.0];
|
||
let m = (v[0] * v[0] + v[1] * v[1] + 1.0).sqrt();
|
||
[v[0] / m, v[1] / m, v[2] / m]
|
||
};
|
||
let nl = (n[0] * l[0] + n[1] * l[1] + n[2] * l[2]).max(0.0);
|
||
let hz = (l[2] + 1.0) * 0.5;
|
||
let hv = {
|
||
let v = [l[0] * 0.5, l[1] * 0.5, hz];
|
||
let m = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
||
[v[0] / m, v[1] / m, v[2] / m]
|
||
};
|
||
let sp = (n[0] * hv[0] + n[1] * hv[1] + n[2] * hv[2]).max(0.0).powf(1.0 / rough) * specular * li;
|
||
let shade = 1.0 + (ambient + diffuse * nl - diffuse * l[2]) * li;
|
||
let a = p[3];
|
||
*px = [p[0] * shade + sp * lc[0] * a, p[1] * shade + sp * lc[1] * a, p[2] * shade + sp * lc[2] * a, a];
|
||
}
|
||
});
|
||
b
|
||
}
|
||
|
||
// ------------------------------------------------------------------ CC Star Burst
|
||
|
||
fn star_burst(ctx: &EffectCtx, b: Buf) -> Buf {
|
||
let mut plan = star_burst_plan(ctx, &b);
|
||
plan.resolve(&b.img);
|
||
plan.finish(b)
|
||
}
|
||
|
||
fn star_burst_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||
let pr = ctx.params;
|
||
let scatter = pr.f("scatter") as f32;
|
||
let speed = pr.f("speed") as f32;
|
||
let phase = pr.f("phase") as f32 / 360.0;
|
||
let spacing = pr.f("gridSpacing").max(1.0) as f32;
|
||
let size = pr.f("size") as f32 / 100.0;
|
||
let blend = pr.f("blendWithOriginal") as f32 / 100.0;
|
||
let seed = ctx.seed.wrapping_mul(0x3c6e);
|
||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||
let s = b.scale as f32;
|
||
let t = ctx.time as f32;
|
||
let nx = (lw / spacing).ceil().max(1.0) as u32;
|
||
let ny = (lh / spacing).ceil().max(1.0) as u32;
|
||
let (cx, cy) = (lw * 0.5, lh * 0.5);
|
||
let mut list: Vec<(f32, Sprite, Tint)> = (0..nx * ny)
|
||
.into_par_iter()
|
||
.map(|i| {
|
||
let (gx, gy) = ((i % nx) as f32 * spacing + spacing * 0.5, (i / nx) as f32 * spacing + spacing * 0.5);
|
||
let (sx, sy) = b.to_px([gx as f64, gy as f64]);
|
||
// Depth cycles towards the viewer: z in [0, 1), perspective 1 / (1 - 0.9 z).
|
||
let z = (h(i, 1, seed) + t * speed * 0.25 + phase).rem_euclid(1.0);
|
||
let persp = 1.0 / (1.0 - 0.9 * z);
|
||
let jx = hs(i, 2, seed) * scatter * 0.01 * lw * 0.5;
|
||
let jy = hs(i, 3, seed) * scatter * 0.01 * lh * 0.5;
|
||
let x = cx + (gx - cx + jx) * persp;
|
||
let y = cy + (gy - cy + jy) * persp;
|
||
let (bx, by) = b.to_px([x as f64, y as f64]);
|
||
let r = spacing * 0.5 * size * persp * s;
|
||
let fade = (z * 8.0).min(1.0) * ((1.0 - z) * 6.0).min(1.0);
|
||
// The layer's colour at the grid point (none where it is clear).
|
||
(z, Sprite::new(bx as f32, by as f32, r, [0.0, 0.0, 0.0, fade], Shape::Disc), Tint::Source { x: sx, y: sy })
|
||
})
|
||
.collect();
|
||
list.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||
let (sprites, tints) = list.into_iter().map(|(_, s, t)| (s, t)).unzip();
|
||
SpritePlan { sprites, tints, acc: Acc::Over, post: if blend > 0.0 { Post::Lerp(blend) } else { Post::Replace } }
|
||
}
|
||
|
||
// ------------------------------------------------------------------ particle engine
|
||
|
||
/// Steps per second of the stepped particle simulations.
|
||
pub(crate) const SPS: f64 = 60.0;
|
||
|
||
/// Number of whole steps reached at layer time `t`.
|
||
pub(crate) fn steps_at(t: f64) -> u64 {
|
||
if t <= 0.0 { 0 } else { (t * SPS + 1e-6).floor() as u64 }
|
||
}
|
||
|
||
#[derive(Clone, Debug)]
|
||
pub(crate) struct Particle {
|
||
pub p: [f32; 3],
|
||
pub v: [f32; 3],
|
||
pub age: f32,
|
||
pub life: f32,
|
||
/// Per-particle random in [0, 1) (size variation, colour jitter…).
|
||
pub rnd: f32,
|
||
pub id: u32,
|
||
}
|
||
|
||
#[derive(Clone, Debug, Default)]
|
||
pub(crate) struct PState {
|
||
pub parts: Vec<Particle>,
|
||
pub carry: f64,
|
||
pub next_id: u32,
|
||
}
|
||
|
||
/// Physics configuration (world units; y down).
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub(crate) struct Phys {
|
||
/// Particles per second.
|
||
pub rate: f64,
|
||
pub life: f32,
|
||
pub producer: [f32; 3],
|
||
pub radius: [f32; 3],
|
||
pub anim: u32,
|
||
pub speed: f32,
|
||
pub gravity: [f32; 3],
|
||
pub resistance: f32,
|
||
pub axis: [f32; 3],
|
||
pub extra: f32,
|
||
pub extra_angle: f32,
|
||
pub seed: u32,
|
||
pub max: usize,
|
||
/// 2D systems keep z = 0.
|
||
pub flat: bool,
|
||
}
|
||
|
||
pub(crate) const PW_ANIMS: &[&str] =
|
||
&["Explosive", "Direction Axis", "Cone Axis", "Viscouse", "Twirly", "Vortex", "Fire", "Jet Sideways", "Fractal Omni", "Fractal Uni"];
|
||
|
||
fn norm3(v: [f32; 3]) -> [f32; 3] {
|
||
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
|
||
if l > 1e-9 { [v[0] / l, v[1] / l, v[2] / l] } else { [0.0, -1.0, 0.0] }
|
||
}
|
||
|
||
fn rand_dir(i: u32, k: u32, seed: u32, flat: bool) -> [f32; 3] {
|
||
if flat {
|
||
let a = h(i, k, seed) * std::f32::consts::TAU;
|
||
return [a.cos(), a.sin(), 0.0];
|
||
}
|
||
let z = hs(i, k, seed);
|
||
let a = h(i, k + 1, seed) * std::f32::consts::TAU;
|
||
let r = (1.0 - z * z).max(0.0).sqrt();
|
||
[r * a.cos(), r * a.sin(), z]
|
||
}
|
||
|
||
impl Phys {
|
||
fn spawn(&self, id: u32) -> Particle {
|
||
let seed = self.seed;
|
||
let mut p = [0.0f32; 3];
|
||
// Uniform in the producer ellipsoid.
|
||
let u = rand_dir(id, 1, seed, self.flat);
|
||
let rr = h(id, 3, seed).powf(if self.flat { 0.5 } else { 1.0 / 3.0 });
|
||
for k in 0..3 {
|
||
p[k] = self.producer[k] + u[k] * self.radius[k] * rr;
|
||
}
|
||
if self.flat {
|
||
p[2] = 0.0;
|
||
}
|
||
let ax = norm3(self.axis);
|
||
let jitter = rand_dir(id, 5, seed, self.flat);
|
||
let dir = match self.anim {
|
||
1 | 9 => norm3([ax[0] + jitter[0] * 0.08, ax[1] + jitter[1] * 0.08, ax[2] + jitter[2] * 0.08]),
|
||
2 => {
|
||
let k = (self.extra_angle.to_radians() * 0.5).tan().abs().min(20.0);
|
||
norm3([ax[0] + jitter[0] * k * h(id, 8, seed), ax[1] + jitter[1] * k * h(id, 8, seed), ax[2] + jitter[2] * k * h(id, 8, seed)])
|
||
}
|
||
5 => [jitter[0], 0.0, if self.flat { 0.0 } else { jitter[2] }],
|
||
6 => norm3([jitter[0] * 0.25, -1.0, jitter[2] * 0.25]),
|
||
7 => norm3([1.0, jitter[1] * 0.05, jitter[2] * 0.05]),
|
||
_ => jitter,
|
||
};
|
||
let sp = self.speed * (0.5 + 0.5 * h(id, 9, seed));
|
||
let mut v = [dir[0] * sp, dir[1] * sp, dir[2] * sp];
|
||
if self.flat {
|
||
v[2] = 0.0;
|
||
}
|
||
let life = self.life * (0.8 + 0.4 * h(id, 10, seed));
|
||
Particle { p, v, age: 0.0, life, rnd: h(id, 11, seed), id }
|
||
}
|
||
|
||
pub(crate) fn step(&self, st: &mut PState, step: u64) {
|
||
let dt = (1.0 / SPS) as f32;
|
||
let t = step as f32 * dt;
|
||
let mut drag = self.resistance;
|
||
if self.anim == 3 {
|
||
drag += 3.0 * self.extra.max(0.1);
|
||
}
|
||
let damp = (-drag * dt).exp();
|
||
let seed = self.seed;
|
||
st.parts.par_iter_mut().for_each(|q| {
|
||
let mut a = self.gravity;
|
||
match self.anim {
|
||
4 => {
|
||
// Twirly: swirl around the vertical axis through the producer.
|
||
let (dx, dz) = (q.p[0] - self.producer[0], q.p[2] - self.producer[2]);
|
||
let k = 4.0 * self.extra;
|
||
a[0] += -dz * k;
|
||
a[2] += dx * k;
|
||
if self.flat {
|
||
let dy = q.p[1] - self.producer[1];
|
||
a[0] += -dy * k;
|
||
a[1] += dx * k;
|
||
}
|
||
}
|
||
5 => {
|
||
// Vortex: strong tangential + inward pull.
|
||
let (dx, dy) = (q.p[0] - self.producer[0], q.p[1] - self.producer[1]);
|
||
let k = 6.0 * self.extra;
|
||
a[0] += -dy * k - dx * 2.0;
|
||
a[1] += dx * k - dy * 2.0;
|
||
}
|
||
6 => {
|
||
// Fire: buoyant, flickering turbulence.
|
||
let n = value_noise(q.p[0] * 8.0, q.p[1] * 8.0, t * 3.0, seed) - 0.5;
|
||
a[0] += n * 4.0 * self.extra;
|
||
a[1] -= self.gravity[1] * 1.5 + 0.3 * self.extra;
|
||
}
|
||
8 | 9 => {
|
||
let f = 6.0 * self.extra;
|
||
a[0] += (value_noise(q.p[0] * 5.0, q.p[1] * 5.0, q.p[2] * 5.0 + t, seed) - 0.5) * f;
|
||
a[1] += (value_noise(q.p[0] * 5.0 + 17.0, q.p[1] * 5.0, q.p[2] * 5.0 + t, seed) - 0.5) * f;
|
||
if !self.flat {
|
||
a[2] += (value_noise(q.p[0] * 5.0, q.p[1] * 5.0 + 31.0, q.p[2] * 5.0 + t, seed) - 0.5) * f;
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
for k in 0..3 {
|
||
q.v[k] = (q.v[k] + a[k] * dt) * damp;
|
||
q.p[k] += q.v[k] * dt;
|
||
}
|
||
if self.flat {
|
||
q.p[2] = 0.0;
|
||
q.v[2] = 0.0;
|
||
}
|
||
q.age += dt;
|
||
});
|
||
st.parts.retain(|q| q.age < q.life);
|
||
st.carry += self.rate / SPS;
|
||
let n = st.carry.floor() as u32;
|
||
st.carry -= n as f64;
|
||
for _ in 0..n {
|
||
if st.parts.len() >= self.max {
|
||
break;
|
||
}
|
||
let id = st.next_id;
|
||
st.next_id = st.next_id.wrapping_add(1);
|
||
// Sub-step birth offset keeps continuous streams smooth.
|
||
let mut q = self.spawn(id);
|
||
let frac = h(id, 12, seed) * dt;
|
||
for k in 0..3 {
|
||
q.p[k] += q.v[k] * frac;
|
||
}
|
||
q.age = frac;
|
||
st.parts.push(q);
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Phys {
|
||
/// The backend description (per-step and per-spawn constants evaluated here, in f32).
|
||
pub(crate) fn desc(&self) -> crate::psim::EngineDesc {
|
||
let mut drag = self.resistance;
|
||
if self.anim == 3 {
|
||
drag += 3.0 * self.extra.max(0.1);
|
||
}
|
||
let dt = (1.0 / SPS) as f32;
|
||
crate::psim::EngineDesc {
|
||
life: self.life,
|
||
producer: self.producer,
|
||
radius: self.radius,
|
||
anim: self.anim,
|
||
speed: self.speed,
|
||
gravity: self.gravity,
|
||
axis: norm3(self.axis),
|
||
extra: self.extra,
|
||
cone: (self.extra_angle.to_radians() * 0.5).tan().abs().min(20.0),
|
||
damp: (-drag * dt).exp(),
|
||
seed: self.seed,
|
||
flat: self.flat,
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The state at layer time `ctx.time` from the host's particle backend (GPU particles), or
|
||
/// `None` (simulate on the CPU).
|
||
fn accelerated(ctx: &EffectCtx, key: u64, phys: &Phys) -> Option<std::sync::Arc<PState>> {
|
||
let backend = ctx.env.host?.particles()?;
|
||
let steps = steps_at(ctx.time);
|
||
let births = crate::psim::births(phys.rate, steps, phys.max)?;
|
||
let req = crate::psim::SimRequest { key, steps, births: &births, system: crate::psim::ParticleSystem::Engine(phys.desc()) };
|
||
let parts = backend.simulate(&req)?;
|
||
Some(std::sync::Arc::new(PState {
|
||
parts: parts.into_iter().map(|q| Particle { p: q.p, v: q.v, age: q.age, life: q.life, rnd: q.rnd, id: q.id }).collect(),
|
||
carry: 0.0,
|
||
next_id: births.len() as u32,
|
||
}))
|
||
}
|
||
|
||
/// The live particles of a stepped particle effect (`ec.sim.ccparticleworld`,
|
||
/// `ec.sim.ccparticlesystems2`) at `ctx.time`: from the host's particle backend when it has
|
||
/// one (GPU particles), else from the CPU simulation. For comparing the two.
|
||
pub fn particle_state(id: &str, ctx: &EffectCtx) -> Option<Vec<crate::psim::SimParticle>> {
|
||
let (phys, cache, salt) = match id {
|
||
"ec.sim.ccparticleworld" => (pw_phys(ctx), &PW_CACHE, 1),
|
||
"ec.sim.ccparticlesystems2" => (ps2_phys(ctx), &PS2_CACHE, 2),
|
||
_ => return None,
|
||
};
|
||
let key = params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, salt);
|
||
let st = accelerated(ctx, key, &phys).unwrap_or_else(|| simulate(cache, key, ctx.time, &phys));
|
||
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())
|
||
}
|
||
|
||
/// Run a particle system to layer time `t` through `cache`.
|
||
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))
|
||
}
|
||
|
||
pub(crate) const PARTICLE_TYPES: &[&str] =
|
||
&["Line", "Shaded Sphere", "Faded Sphere", "Bubble", "Star", "Motion Polygon", "Tri-Polygon", "Cube", "Textured Square"];
|
||
|
||
/// Colour/size/opacity of a particle at its age.
|
||
fn particle_look(q: &Particle, birth: [f32; 4], death: [f32; 4], bsize: f32, dsize: f32, svar: f32, max_op: f32) -> ([f32; 4], f32) {
|
||
let u = (q.age / q.life.max(1e-4)).clamp(0.0, 1.0);
|
||
let size = (bsize + (dsize - bsize) * u) * (1.0 + svar * (q.rnd * 2.0 - 1.0));
|
||
let fade = (u * 20.0).min(1.0) * ((1.0 - u) * 5.0).min(1.0);
|
||
let c = [birth[0] + (death[0] - birth[0]) * u, birth[1] + (death[1] - birth[1]) * u, birth[2] + (death[2] - birth[2]) * u, max_op * fade];
|
||
(c, size.max(0.0))
|
||
}
|
||
|
||
fn type_sprite(kind: u32, x: f32, y: f32, r: f32, c: [f32; 4], vx: f32, vy: f32, rot: f32) -> Sprite {
|
||
let vl = (vx * vx + vy * vy).sqrt().max(1e-6);
|
||
let shape = match kind {
|
||
0 => Shape::Line { dx: -vx / vl * r * 2.0, dy: -vy / vl * r * 2.0 },
|
||
1 => Shape::Sphere,
|
||
2 => Shape::Faded,
|
||
3 => Shape::Bubble,
|
||
4 => Shape::Star,
|
||
5 => Shape::Line { dx: -vx * 0.05, dy: -vy * 0.05 },
|
||
6 => Shape::Tri,
|
||
_ => Shape::Square,
|
||
};
|
||
let rr = match kind {
|
||
0 => (r * 0.08).max(0.5),
|
||
5 => r * 0.5,
|
||
_ => r,
|
||
};
|
||
Sprite { x, y, r: rr, c, shape, rot }
|
||
}
|
||
|
||
// ------------------------------------------------------------------ CC Particle World
|
||
|
||
static PW_CACHE: SimCache<PState> = SimCache::new(6);
|
||
|
||
/// CC Particle World's physics.
|
||
fn pw_phys(ctx: &EffectCtx) -> Phys {
|
||
let pr = ctx.params;
|
||
Phys {
|
||
rate: pr.f("birthRate").max(0.0) * 120.0,
|
||
life: pr.f("longevity").max(0.0) as f32,
|
||
producer: [pr.f("producer/producerX") as f32, pr.f("producer/producerY") as f32, pr.f("producer/producerZ") as f32],
|
||
radius: [pr.f("producer/radiusX").max(0.0) as f32, pr.f("producer/radiusY").max(0.0) as f32, pr.f("producer/radiusZ").max(0.0) as f32],
|
||
anim: pr.e("physics/animation"),
|
||
speed: pr.f("physics/velocity") as f32 * 0.5,
|
||
gravity: {
|
||
let g = pr.f("physics/gravity") as f32;
|
||
let gv =
|
||
[pr.f("physics/gravityVector/gravityX") as f32, pr.f("physics/gravityVector/gravityY") as f32, pr.f("physics/gravityVector/gravityZ") as f32];
|
||
[gv[0] * g, gv[1] * g, gv[2] * g]
|
||
},
|
||
resistance: pr.f("physics/resistance").max(0.0) as f32,
|
||
axis: [pr.f("physics/directionAxis/axisX") as f32, pr.f("physics/directionAxis/axisY") as f32, pr.f("physics/directionAxis/axisZ") as f32],
|
||
extra: pr.f("physics/extra") as f32,
|
||
extra_angle: pr.f("physics/extraAngle") as f32,
|
||
seed: (pr.f("extras/randomSeed") as u32).wrapping_mul(0x9e3779b9) ^ ctx.seed,
|
||
max: 40_000,
|
||
flat: false,
|
||
}
|
||
}
|
||
|
||
fn particle_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let pr = ctx.params;
|
||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||
let phys = pw_phys(ctx);
|
||
if phys.rate <= 0.0 && ctx.time <= 0.0 {
|
||
return b;
|
||
}
|
||
let key = params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 1);
|
||
let st = accelerated(ctx, key, &phys).unwrap_or_else(|| simulate(&PW_CACHE, key, ctx.time, &phys));
|
||
let kind = pr.e("particle/particleType");
|
||
let birth = pr.color("particle/birthColor");
|
||
let death = pr.color("particle/deathColor");
|
||
let (bs, ds) = (pr.f("particle/birthSize") as f32, pr.f("particle/deathSize") as f32);
|
||
let svar = pr.f("particle/sizeVariation") as f32 / 100.0;
|
||
let max_op = pr.f("particle/maxOpacity") as f32 / 100.0;
|
||
let s = b.scale as f32;
|
||
// Camera at distance 2 (world units = layer width) looking at the origin (layer centre).
|
||
let cam = 2.0f32;
|
||
let mut list: Vec<(f32, Sprite)> = st
|
||
.parts
|
||
.par_iter()
|
||
.filter_map(|q| {
|
||
let zc = cam + q.p[2];
|
||
if zc <= 0.05 {
|
||
return None;
|
||
}
|
||
let k = cam / zc;
|
||
let x = lw * 0.5 + q.p[0] * lw * k;
|
||
let y = lh * 0.5 + q.p[1] * lw * k;
|
||
let (bx, by) = b.to_px([x as f64, y as f64]);
|
||
let (c, size) = particle_look(q, birth, death, bs, ds, svar, max_op);
|
||
let r = size * 0.5 * lw * k * s * 0.25;
|
||
let sp = type_sprite(kind, bx as f32, by as f32, r, c, q.v[0] * lw * k * s, q.v[1] * lw * k * s, (q.id % 628) as f32 * 0.01 + q.age * 2.0);
|
||
Some((zc, sp))
|
||
})
|
||
.collect();
|
||
list.sort_by(|a, b| b.0.total_cmp(&a.0));
|
||
let sprites: Vec<Sprite> = list.into_iter().map(|(_, s)| s).collect();
|
||
let mode = pr.e("particle/transferMode");
|
||
let fx = splat(b.img.width, b.img.height, &sprites, if mode == 2 { Acc::Add } else { Acc::Over });
|
||
b.img = combine(
|
||
&b.img,
|
||
&fx,
|
||
match mode {
|
||
0 => 0,
|
||
1 => 1,
|
||
2 => 2,
|
||
3 => 5,
|
||
_ => 0,
|
||
},
|
||
);
|
||
b
|
||
}
|
||
|
||
// ------------------------------------------------------------------ CC Particle Systems II
|
||
|
||
static PS2_CACHE: SimCache<PState> = SimCache::new(6);
|
||
|
||
/// CC Particle Systems II's physics.
|
||
fn ps2_phys(ctx: &EffectCtx) -> Phys {
|
||
let pr = ctx.params;
|
||
let lh = ctx.layer_size[1] as f32;
|
||
let unit = lh.max(1.0);
|
||
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,
|
||
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],
|
||
anim: [0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9][pr.e("physics/animation").min(9) as usize],
|
||
speed: pr.f("physics/velocity") as f32 * 0.25,
|
||
gravity: [0.0, pr.f("physics/gravity") as f32 * 0.5, 0.0],
|
||
resistance: pr.f("physics/resistance").max(0.0) as f32,
|
||
// AE angles: 0° = up, clockwise.
|
||
axis: [dir.sin(), -dir.cos(), 0.0],
|
||
extra: pr.f("physics/extra") as f32,
|
||
extra_angle: 45.0 * pr.f("physics/extra") as f32,
|
||
seed: (pr.f("randomSeed") as u32).wrapping_mul(0x85ebca6b) ^ ctx.seed,
|
||
max: 40_000,
|
||
flat: true,
|
||
}
|
||
}
|
||
|
||
fn particle_systems2(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
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 = params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 2);
|
||
let st = accelerated(ctx, key, &phys).unwrap_or_else(|| simulate(&PS2_CACHE, key, ctx.time, &phys));
|
||
let kind = pr.e("particle/particleType");
|
||
let birth = pr.color("particle/birthColor");
|
||
let death = pr.color("particle/deathColor");
|
||
let (bs, ds) = (pr.f("particle/birthSize") as f32, pr.f("particle/deathSize") as f32);
|
||
let svar = pr.f("particle/sizeVariation") as f32 / 100.0;
|
||
let max_op = pr.f("particle/maxOpacity") as f32 / 100.0;
|
||
let s = b.scale as f32;
|
||
let sprites: Vec<Sprite> = st
|
||
.parts
|
||
.iter()
|
||
.map(|q| {
|
||
let (bx, by) = b.to_px([(q.p[0] * unit) as f64, (q.p[1] * unit) as f64]);
|
||
let (c, size) = particle_look(q, birth, death, bs, ds, svar, max_op);
|
||
let r = size * unit * 0.02 * s;
|
||
type_sprite(kind, bx as f32, by as f32, r, c, q.v[0] * unit * s, q.v[1] * unit * s, (q.id % 628) as f32 * 0.01 + q.age * 2.0)
|
||
})
|
||
.collect();
|
||
let mode = pr.e("particle/transferMode");
|
||
let fx = splat(b.img.width, b.img.height, &sprites, if mode == 2 { Acc::Add } else { Acc::Over });
|
||
b.img = combine(
|
||
&b.img,
|
||
&fx,
|
||
match mode {
|
||
0 => 0,
|
||
1 => 1,
|
||
2 => 2,
|
||
3 => 5,
|
||
_ => 0,
|
||
},
|
||
);
|
||
b
|
||
}
|
||
|
||
// ------------------------------------------------------------------ CC Mr. Mercury
|
||
|
||
static MERCURY_CACHE: SimCache<PState> = SimCache::new(6);
|
||
|
||
/// A CC Mr. Mercury blob (buffer px).
|
||
#[derive(Clone, Copy, Debug)]
|
||
pub struct Blob {
|
||
pub x: f32,
|
||
pub y: f32,
|
||
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> {
|
||
let pr = ctx.params;
|
||
let lh = ctx.layer_size[1] as f32;
|
||
let unit = lh.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,
|
||
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],
|
||
anim: [0u32, 1, 2, 3, 4, 5, 6, 7, 8, 9][pr.e("animation").min(9) as usize],
|
||
speed: pr.f("velocity") as f32 * 0.3,
|
||
gravity: [0.0, pr.f("gravity") as f32 * 0.5, 0.0],
|
||
resistance: pr.f("resistance").max(0.0) as f32,
|
||
axis: [dir.sin(), -dir.cos(), 0.0],
|
||
extra: pr.f("extra") as f32,
|
||
extra_angle: 45.0 * pr.f("extra") as f32,
|
||
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);
|
||
let (bs, ds) = (pr.f("blobBirthSize") as f32, pr.f("blobDeathSize") as f32);
|
||
let s = b.scale as f32;
|
||
st.parts
|
||
.iter()
|
||
.map(|q| {
|
||
let u = (q.age / q.life.max(1e-4)).clamp(0.0, 1.0);
|
||
let size = bs + (ds - bs) * u;
|
||
let (bx, by) = b.to_px([(q.p[0] * unit) as f64, (q.p[1] * unit) as f64]);
|
||
Blob { x: bx as f32, y: by as f32, r: (size * unit * 0.08 * s).max(0.0) }
|
||
})
|
||
.filter(|q| q.r > 0.1)
|
||
.collect()
|
||
}
|
||
|
||
fn mr_mercury(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||
let pr = ctx.params;
|
||
let blobs = mercury_blobs(ctx, &b);
|
||
let influence = pr.f("blobInfluence") as f32 / 100.0;
|
||
let s = b.scale as f32;
|
||
let (w, hh) = (b.img.width, b.img.height);
|
||
// Field and gradient accumulate additively: [f, gx, gy, _].
|
||
let mut field = Image::new(w, hh);
|
||
let infl = 1.0 + influence;
|
||
raster(
|
||
&mut field,
|
||
&blobs,
|
||
|q| {
|
||
let e = q.r * 2.0 * infl;
|
||
Some([q.x - e, q.y - e, q.x + e, q.y + e])
|
||
},
|
||
|q, x, y| {
|
||
let (dx, dy) = (x - q.x, y - q.y);
|
||
let rr = q.r * 2.0 * infl;
|
||
let d2 = (dx * dx + dy * dy) / (rr * rr);
|
||
if d2 >= 1.0 {
|
||
return None;
|
||
}
|
||
let k = 1.0 - d2;
|
||
let f = k * k * k;
|
||
let g = -6.0 * k * k / (rr * rr);
|
||
Some([f, g * dx, g * dy, 0.0])
|
||
},
|
||
Acc::Add,
|
||
);
|
||
let thr = 0.35f32;
|
||
let ambient = pr.f("shading/ambient") as f32 / 100.0;
|
||
let diffuse = pr.f("shading/diffuse") as f32 / 100.0;
|
||
let specular = pr.f("shading/specular") as f32 / 100.0;
|
||
let rough = pr.f("shading/roughness").max(0.001) as f32;
|
||
let metal = pr.f("shading/metal") as f32 / 100.0;
|
||
let li = pr.f("light/lightIntensity") as f32 / 100.0;
|
||
let lc = pr.color("light/lightColor");
|
||
let ldir = (pr.f("light/lightDirection") as f32).to_radians();
|
||
let lheight = pr.f("light/lightHeight") as f32 / 100.0;
|
||
let l = norm3([ldir.sin(), -ldir.cos(), lheight.max(0.05)]);
|
||
let src = b.img.clone();
|
||
let mut out = Image::new(w, hh);
|
||
out.rows_mut().for_each(|(y, row)| {
|
||
for (x, px) in row.iter_mut().enumerate() {
|
||
let fv = field.data[y * w as usize + x];
|
||
let f = fv[0];
|
||
if f <= thr * 0.8 {
|
||
continue;
|
||
}
|
||
let cov = ((f - thr * 0.8) / (thr * 0.4)).clamp(0.0, 1.0);
|
||
let n = norm3([fv[1] * 3.0, fv[2] * 3.0, 1.0 / (1.0 + f)]);
|
||
// Refract the layer through the blob.
|
||
let sp = src.sample_bilinear_clamped(x as f64 + 0.5 - n[0] as f64 * 12.0 * s as f64, y as f64 + 0.5 - n[1] as f64 * 12.0 * s as f64);
|
||
let (c, sa) = unpremul(sp);
|
||
let base = if sa > 0.0 { c } else { [0.7, 0.7, 0.75] };
|
||
let nl = (n[0] * l[0] + n[1] * l[1] + n[2] * l[2]).max(0.0);
|
||
let hv = norm3([l[0], l[1], l[2] + 1.0]);
|
||
let spec = (n[0] * hv[0] + n[1] * hv[1] + n[2] * hv[2]).max(0.0).powf(1.0 / rough) * specular * li;
|
||
let shade = ambient + diffuse * nl * li;
|
||
let cc: [f32; 3] = std::array::from_fn(|k| base[k] * shade + spec * (lc[k] * (1.0 - metal) + base[k] * metal));
|
||
*px = [cc[0] * cov, cc[1] * cov, cc[2] * cov, cov];
|
||
}
|
||
});
|
||
b.img = out;
|
||
b
|
||
}
|
||
|
||
// ------------------------------------------------------------------ specs
|
||
|
||
pub fn specs() -> Vec<EffectSpec> {
|
||
let transfer = || popup(&["Composite", "Lighten"]);
|
||
vec![
|
||
spec(
|
||
"ec.sim.ccrainfall",
|
||
"CC Rainfall",
|
||
vec![
|
||
p("drops", "Drops", num(5000.0), slider(0.0, 100_000.0, 0.0, 20_000.0, 0)),
|
||
p("size", "Size", num(1.0), slider(0.0, 50.0, 0.0, 10.0, 2)),
|
||
p("sceneDepth", "Scene Depth", num(5000.0), slider(0.0, 20_000.0, 0.0, 10_000.0, 0)),
|
||
p("speed", "Speed", num(4000.0), slider(0.0, 20_000.0, 0.0, 10_000.0, 0)),
|
||
p("wind", "Wind", num(0.0), slider(-10_000.0, 10_000.0, -2000.0, 2000.0, 1)),
|
||
p("windVariation", "Variation % (Wind)", num(0.0), pct()),
|
||
p("spread", "Spread", num(0.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||
p("color", "Color", col(0.8, 0.8, 0.8), ParamUi::Color),
|
||
p("opacity", "Opacity", num(25.0), pct()),
|
||
p("transferMode", "Transfer Mode", Value::Enum(1), transfer()),
|
||
p("compositeWithOriginal", "Composite With Original", Value::Bool(true), ParamUi::Checkbox),
|
||
p("extras/appearance", "Appearance", Value::Enum(0), popup(&["Refracting", "Soft Solid"])),
|
||
p("extras/offset", "Offset", pt(0.5, 0.5), ParamUi::Point),
|
||
p("extras/randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||
],
|
||
rainfall,
|
||
),
|
||
spec(
|
||
"ec.sim.ccsnowfall",
|
||
"CC Snowfall",
|
||
vec![
|
||
p("flakes", "Flakes", num(10_000.0), slider(0.0, 200_000.0, 0.0, 50_000.0, 0)),
|
||
p("size", "Size", num(3.0), slider(0.0, 50.0, 0.0, 20.0, 2)),
|
||
p("sizeVariation", "Variation % (Size)", num(50.0), pct()),
|
||
p("sceneDepth", "Scene Depth", num(5000.0), slider(0.0, 20_000.0, 0.0, 10_000.0, 0)),
|
||
p("speed", "Speed", num(100.0), slider(0.0, 5000.0, 0.0, 1000.0, 1)),
|
||
p("speedVariation", "Variation % (Speed)", num(50.0), pct()),
|
||
p("wind", "Wind", num(25.0), slider(-5000.0, 5000.0, -500.0, 500.0, 1)),
|
||
p("windVariation", "Variation % (Wind)", num(50.0), pct()),
|
||
p("spread", "Spread", num(0.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||
p("wiggle/wiggleAmount", "Amount", num(2.0), slider(0.0, 100.0, 0.0, 20.0, 2)),
|
||
p("wiggle/wiggleAmountVariation", "Variation % (Amount)", num(50.0), pct()),
|
||
p("wiggle/wiggleFrequency", "Frequency", num(1.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||
p("wiggle/wiggleFrequencyVariation", "Variation % (Frequency)", num(50.0), pct()),
|
||
p("color", "Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||
p("opacity", "Opacity", num(100.0), pct()),
|
||
p("transferMode", "Transfer Mode", Value::Enum(0), transfer()),
|
||
p("compositeWithOriginal", "Composite With Original", Value::Bool(true), ParamUi::Checkbox),
|
||
p("extras/offset", "Offset", pt(0.5, 0.5), ParamUi::Point),
|
||
p("extras/randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||
],
|
||
snowfall,
|
||
),
|
||
spec(
|
||
"ec.sim.ccbubbles",
|
||
"CC Bubbles",
|
||
vec![
|
||
p("bubbleAmount", "Bubble Amount", num(100.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||
p("bubbleSpeed", "Bubble Speed", num(0.5), slider(-10.0, 10.0, -2.0, 2.0, 2)),
|
||
p("wobbleAmplitude", "Wobble Amplitude", num(5.0), slider(0.0, 500.0, 0.0, 100.0, 1)),
|
||
p("wobbleFrequency", "Wobble Frequency", num(1.0), slider(0.0, 50.0, 0.0, 10.0, 2)),
|
||
p("bubbleSize", "Bubble Size", num(1.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||
p("reflectionType", "Reflection Type", Value::Enum(0), popup(&["Liquid", "Metal"])),
|
||
p("shadingType", "Shading Type", Value::Enum(0), popup(&["None", "Lighten", "Darken", "Fade Inwards", "Fade Outwards"])),
|
||
],
|
||
bubbles,
|
||
),
|
||
spec(
|
||
"ec.sim.ccdrizzle",
|
||
"CC Drizzle",
|
||
vec![
|
||
p("dripRate", "Drip Rate", num(10.0), slider(0.0, 200.0, 0.0, 50.0, 2)),
|
||
p("longevity", "Longevity (sec)", num(1.0), slider(0.0, 30.0, 0.0, 5.0, 2)),
|
||
p("rippling", "Rippling", num(360.0), ParamUi::Angle),
|
||
p("displacement", "Displacement", num(5.0), slider(0.0, 100.0, 0.0, 30.0, 2)),
|
||
p("rippleHeight", "Ripple Height", num(30.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||
p("spreading", "Spreading", num(100.0), slider(0.0, 1000.0, 0.0, 300.0, 1)),
|
||
p("light/lightIntensity", "Light Intensity", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||
p("light/lightColor", "Light Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||
p("light/lightHeight", "Light Height", num(25.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||
p("light/lightDirection", "Light Direction", num(-45.0), ParamUi::Angle),
|
||
p("shading/ambient", "Ambient", num(0.0), slider(-200.0, 200.0, -100.0, 100.0, 1)),
|
||
p("shading/diffuse", "Diffuse", num(0.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||
p("shading/specular", "Specular", num(20.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||
p("shading/roughness", "Roughness", num(0.05), slider(0.001, 1.0, 0.001, 0.5, 3)),
|
||
],
|
||
drizzle,
|
||
),
|
||
spec(
|
||
"ec.sim.ccstarburst",
|
||
"CC Star Burst",
|
||
vec![
|
||
p("scatter", "Scatter", num(100.0), slider(0.0, 1000.0, 0.0, 300.0, 1)),
|
||
p("speed", "Speed", num(1.0), slider(-10.0, 10.0, -4.0, 4.0, 2)),
|
||
p("phase", "Phase", num(0.0), ParamUi::Angle),
|
||
p("gridSpacing", "Grid Spacing", num(2.0), slider(1.0, 100.0, 1.0, 20.0, 0)),
|
||
p("size", "Size", num(100.0), slider(0.0, 1000.0, 0.0, 300.0, 1)),
|
||
p("blendWithOriginal", "Blend w. Original", num(0.0), pct()),
|
||
],
|
||
star_burst,
|
||
),
|
||
spec(
|
||
"ec.sim.ccparticleworld",
|
||
"CC Particle World",
|
||
vec![
|
||
p("birthRate", "Birth Rate", num(0.5), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||
p("longevity", "Longevity (sec)", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||
p("producer/producerX", "Position X", num(0.0), slider(-10.0, 10.0, -1.0, 1.0, 2)),
|
||
p("producer/producerY", "Position Y", num(0.0), slider(-10.0, 10.0, -1.0, 1.0, 2)),
|
||
p("producer/producerZ", "Position Z", num(0.0), slider(-10.0, 10.0, -1.0, 1.0, 2)),
|
||
p("producer/radiusX", "Radius X", num(0.025), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||
p("producer/radiusY", "Radius Y", num(0.025), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||
p("producer/radiusZ", "Radius Z", num(0.025), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||
p("physics/animation", "Animation", Value::Enum(0), popup(PW_ANIMS)),
|
||
p("physics/velocity", "Velocity", num(1.0), slider(-10.0, 10.0, 0.0, 5.0, 2)),
|
||
p("physics/inheritVelocity", "Inherit Velocity %", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||
p("physics/gravity", "Gravity", num(0.5), slider(-10.0, 10.0, -2.0, 2.0, 3)),
|
||
p("physics/resistance", "Resistance", num(0.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||
p("physics/extra", "Extra", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||
p("physics/extraAngle", "Extra Angle", num(360.0), ParamUi::Angle),
|
||
p("physics/directionAxis/axisX", "Axis X", num(0.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||
p("physics/directionAxis/axisY", "Axis Y", num(-1.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||
p("physics/directionAxis/axisZ", "Axis Z", num(0.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||
p("physics/gravityVector/gravityX", "Gravity X", num(0.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||
p("physics/gravityVector/gravityY", "Gravity Y", num(1.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||
p("physics/gravityVector/gravityZ", "Gravity Z", num(0.0), slider(-1.0, 1.0, -1.0, 1.0, 2)),
|
||
p("particle/particleType", "Particle Type", Value::Enum(0), popup(PARTICLE_TYPES)),
|
||
p("particle/birthSize", "Birth Size", num(0.2), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||
p("particle/deathSize", "Death Size", num(0.2), slider(0.0, 10.0, 0.0, 1.0, 3)),
|
||
p("particle/sizeVariation", "Size Variation", num(0.0), pct()),
|
||
p("particle/maxOpacity", "Max Opacity", num(75.0), pct()),
|
||
p("particle/birthColor", "Birth Color", col(1.0, 1.0, 0.0), ParamUi::Color),
|
||
p("particle/deathColor", "Death Color", col(0.6, 0.0, 0.0), ParamUi::Color),
|
||
p("particle/transferMode", "Transfer Mode", Value::Enum(0), popup(&["Composite", "Screen", "Add", "Black Matte"])),
|
||
p("extras/randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||
],
|
||
particle_world,
|
||
),
|
||
spec(
|
||
"ec.sim.ccparticlesystems2",
|
||
"CC Particle Systems II",
|
||
vec![
|
||
p("birthRate", "Birth Rate", num(2.0), slider(0.0, 100.0, 0.0, 20.0, 2)),
|
||
p("longevity", "Longevity (sec)", num(1.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||
p("producer/position", "Position", pt(0.5, 0.5), ParamUi::Point),
|
||
p("producer/radiusX", "Radius X", num(0.0), slider(0.0, 2000.0, 0.0, 200.0, 1)),
|
||
p("producer/radiusY", "Radius Y", num(0.0), slider(0.0, 2000.0, 0.0, 200.0, 1)),
|
||
p("physics/animation", "Animation", Value::Enum(0), popup(PW_ANIMS)),
|
||
p("physics/velocity", "Velocity", num(2.0), slider(-20.0, 20.0, 0.0, 10.0, 2)),
|
||
p("physics/inheritVelocity", "Inherit Velocity %", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||
p("physics/gravity", "Gravity", num(1.0), slider(-20.0, 20.0, -5.0, 5.0, 2)),
|
||
p("physics/resistance", "Resistance", num(0.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||
p("physics/direction", "Direction", num(0.0), ParamUi::Angle),
|
||
p("physics/extra", "Extra", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||
p("particle/particleType", "Particle Type", Value::Enum(0), popup(PARTICLE_TYPES)),
|
||
p("particle/birthSize", "Birth Size", num(0.5), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||
p("particle/deathSize", "Death Size", num(1.5), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||
p("particle/sizeVariation", "Size Variation", num(0.0), pct()),
|
||
p("particle/maxOpacity", "Max Opacity", num(75.0), pct()),
|
||
p("particle/birthColor", "Birth Color", col(1.0, 1.0, 0.0), ParamUi::Color),
|
||
p("particle/deathColor", "Death Color", col(0.6, 0.0, 0.0), ParamUi::Color),
|
||
p("particle/transferMode", "Transfer Mode", Value::Enum(0), popup(&["Composite", "Screen", "Add", "Black Matte"])),
|
||
p("randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)),
|
||
],
|
||
particle_systems2,
|
||
),
|
||
spec(
|
||
"ec.sim.ccmrmercury",
|
||
"CC Mr. Mercury",
|
||
vec![
|
||
p("radiusX", "Radius X", num(50.0), slider(0.0, 2000.0, 0.0, 300.0, 1)),
|
||
p("radiusY", "Radius Y", num(50.0), slider(0.0, 2000.0, 0.0, 300.0, 1)),
|
||
p("producer", "Producer", pt(0.5, 0.5), ParamUi::Point),
|
||
p("direction", "Direction", num(0.0), ParamUi::Angle),
|
||
p("velocity", "Velocity", num(1.0), slider(-20.0, 20.0, 0.0, 5.0, 2)),
|
||
p("birthRate", "Birth Rate", num(1.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||
p("longevity", "Longevity (sec)", num(2.0), slider(0.0, 100.0, 0.0, 10.0, 2)),
|
||
p("gravity", "Gravity", num(0.5), slider(-20.0, 20.0, -5.0, 5.0, 2)),
|
||
p("resistance", "Resistance", num(0.0), slider(0.0, 20.0, 0.0, 5.0, 2)),
|
||
p("extra", "Extra", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||
p("animation", "Animation", Value::Enum(0), popup(PW_ANIMS)),
|
||
p("blobInfluence", "Blob Influence", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||
p("blobBirthSize", "Blob Birth Size", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||
p("blobDeathSize", "Blob Death Size", num(0.25), slider(0.0, 10.0, 0.0, 2.0, 2)),
|
||
p("light/lightIntensity", "Light Intensity", num(100.0), slider(0.0, 400.0, 0.0, 200.0, 1)),
|
||
p("light/lightColor", "Light Color", col(1.0, 1.0, 1.0), ParamUi::Color),
|
||
p("light/lightHeight", "Light Height", num(50.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||
p("light/lightDirection", "Light Direction", num(-45.0), ParamUi::Angle),
|
||
p("shading/ambient", "Ambient", num(0.0), slider(-200.0, 200.0, -100.0, 100.0, 1)),
|
||
p("shading/diffuse", "Diffuse", num(75.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||
p("shading/specular", "Specular", num(50.0), slider(0.0, 200.0, 0.0, 100.0, 1)),
|
||
p("shading/roughness", "Roughness", num(0.05), slider(0.001, 1.0, 0.001, 0.5, 3)),
|
||
p("shading/metal", "Metal", num(100.0), pct()),
|
||
],
|
||
mr_mercury,
|
||
),
|
||
]
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::{EffectEnv, run_fx};
|
||
|
||
fn grey(w: u32, hh: u32) -> Image {
|
||
Image::filled(w, hh, [0.2, 0.2, 0.2, 1.0])
|
||
}
|
||
|
||
fn ramp(w: u32, hh: u32) -> Image {
|
||
let mut img = Image::new(w, hh);
|
||
for y in 0..hh {
|
||
for x in 0..w {
|
||
img.set(x, y, [x as f32 / w as f32, y as f32 / hh as f32, 0.4, 1.0]);
|
||
}
|
||
}
|
||
img
|
||
}
|
||
|
||
fn run(id: &str, vals: &[(&str, Value)], img: Image, t: f64) -> Image {
|
||
run_fx(id, vals, img, t, EffectEnv::default()).img
|
||
}
|
||
|
||
fn diff(a: &Image, b: &Image) -> f32 {
|
||
a.data.iter().zip(&b.data).map(|(p, q)| (0..4).map(|k| (p[k] - q[k]).abs()).sum::<f32>()).sum()
|
||
}
|
||
|
||
/// Centre of mass (luminance-weighted difference from `base`) in y.
|
||
fn mass_y(img: &Image, base: &Image) -> f32 {
|
||
let (mut m, mut sy) = (0.0, 0.0);
|
||
for y in 0..img.height {
|
||
for x in 0..img.width {
|
||
let i = img.idx(x, y);
|
||
let d: f32 = (0..3).map(|k| (img.data[i][k] - base.data[i][k]).abs()).sum();
|
||
m += d;
|
||
sy += d * y as f32;
|
||
}
|
||
}
|
||
if m > 0.0 { sy / m } else { -1.0 }
|
||
}
|
||
|
||
#[test]
|
||
fn rasteriser_over_and_add() {
|
||
let s = [Sprite::new(5.0, 5.0, 2.0, [1.0, 0.0, 0.0, 1.0], Shape::Disc)];
|
||
let img = splat(10, 40, &s, Acc::Over);
|
||
assert!(img.get(5, 5)[0] > 0.99 && img.get(9, 30)[3] == 0.0);
|
||
let two = [s[0], s[0]];
|
||
let add = splat(10, 40, &two, Acc::Add);
|
||
assert!(add.get(5, 5)[0] > 1.9);
|
||
// Items spanning band boundaries are drawn in every band.
|
||
let tall = [Sprite::new(5.0, 16.0, 6.0, [0.0, 1.0, 0.0, 1.0], Shape::Square)];
|
||
let t = splat(10, 40, &tall, Acc::Over);
|
||
assert!(t.get(5, 13)[1] > 0.9 && t.get(5, 18)[1] > 0.9);
|
||
}
|
||
|
||
#[test]
|
||
fn rainfall_deterministic_and_falls() {
|
||
let vals = [("drops", num(200.0)), ("opacity", num(100.0)), ("extras/appearance", Value::Enum(1)), ("color", col(1.0, 1.0, 1.0))];
|
||
let a = run("ec.sim.ccrainfall", &vals, grey(40, 40), 0.5);
|
||
let b = run("ec.sim.ccrainfall", &vals, grey(40, 40), 0.5);
|
||
assert_eq!(a, b);
|
||
assert!(diff(&a, &grey(40, 40)) > 1.0, "drops visible");
|
||
let none = run("ec.sim.ccrainfall", &[("drops", num(0.0))], grey(40, 40), 0.5);
|
||
assert_eq!(none, grey(40, 40));
|
||
let later = run("ec.sim.ccrainfall", &vals, grey(40, 40), 0.52);
|
||
assert!(diff(&a, &later) > 0.1, "rain moves");
|
||
}
|
||
|
||
#[test]
|
||
fn snowfall_deterministic_and_moves() {
|
||
let vals = [("flakes", num(300.0))];
|
||
let a = run("ec.sim.ccsnowfall", &vals, grey(40, 40), 1.0);
|
||
assert_eq!(a, run("ec.sim.ccsnowfall", &vals, grey(40, 40), 1.0));
|
||
assert!(diff(&a, &grey(40, 40)) > 1.0);
|
||
assert!(diff(&a, &run("ec.sim.ccsnowfall", &vals, grey(40, 40), 1.2)) > 0.1);
|
||
let off = run("ec.sim.ccsnowfall", &[("flakes", num(300.0)), ("compositeWithOriginal", Value::Bool(false))], grey(40, 40), 1.0);
|
||
assert!(off.data.iter().any(|p| p[3] == 0.0), "without original the background is transparent");
|
||
}
|
||
|
||
#[test]
|
||
fn bubbles_rise() {
|
||
let vals = [("bubbleAmount", num(30.0)), ("bubbleSize", num(3.0))];
|
||
let a = run("ec.sim.ccbubbles", &vals, ramp(40, 40), 0.3);
|
||
assert_eq!(a, run("ec.sim.ccbubbles", &vals, ramp(40, 40), 0.3));
|
||
assert!(a.data.iter().any(|p| p[3] > 0.5));
|
||
let none = run("ec.sim.ccbubbles", &[("bubbleAmount", num(0.0))], ramp(40, 40), 0.3);
|
||
assert!(none.data.iter().all(|p| p[3] == 0.0), "bubbles replace the layer");
|
||
// A single slow bubble moves up over a short time.
|
||
let one = [("bubbleAmount", num(1.0)), ("bubbleSize", num(4.0)), ("wobbleAmplitude", num(0.0)), ("bubbleSpeed", num(0.2))];
|
||
let blank = Image::new(40, 40);
|
||
let y0 = mass_y(&run("ec.sim.ccbubbles", &one, ramp(40, 40), 0.0), &blank);
|
||
let y1 = mass_y(&run("ec.sim.ccbubbles", &one, ramp(40, 40), 0.1), &blank);
|
||
assert!(y0 >= 0.0 && y1 >= 0.0 && (y1 < y0 || y0 < 8.0), "{y0} -> {y1}");
|
||
}
|
||
|
||
#[test]
|
||
fn drizzle_ripples() {
|
||
let a = run("ec.sim.ccdrizzle", &[], ramp(48, 48), 0.6);
|
||
assert_eq!(a, run("ec.sim.ccdrizzle", &[], ramp(48, 48), 0.6));
|
||
assert!(diff(&a, &ramp(48, 48)) > 0.1);
|
||
let none = run("ec.sim.ccdrizzle", &[("dripRate", num(0.0))], ramp(48, 48), 0.6);
|
||
assert_eq!(none, ramp(48, 48));
|
||
}
|
||
|
||
#[test]
|
||
fn star_burst_moves_stars() {
|
||
let a = run("ec.sim.ccstarburst", &[("gridSpacing", num(4.0))], ramp(40, 40), 0.5);
|
||
assert_eq!(a, run("ec.sim.ccstarburst", &[("gridSpacing", num(4.0))], ramp(40, 40), 0.5));
|
||
assert!(a.data.iter().any(|p| p[3] > 0.0));
|
||
let b = run("ec.sim.ccstarburst", &[("gridSpacing", num(4.0))], ramp(40, 40), 0.9);
|
||
assert!(diff(&a, &b) > 0.5);
|
||
let full = run("ec.sim.ccstarburst", &[("blendWithOriginal", num(100.0))], ramp(40, 40), 0.5);
|
||
assert!(diff(&full, &ramp(40, 40)) < 1e-3);
|
||
}
|
||
|
||
fn pw_vals(seed: f64) -> Vec<(&'static str, Value)> {
|
||
vec![
|
||
("birthRate", num(1.0)),
|
||
("extras/randomSeed", num(seed)),
|
||
("particle/particleType", Value::Enum(2)),
|
||
("particle/birthSize", num(0.3)),
|
||
("particle/deathSize", num(0.3)),
|
||
]
|
||
}
|
||
|
||
#[test]
|
||
fn particle_world_seek_consistent() {
|
||
let v = pw_vals(11.0);
|
||
let direct = run("ec.sim.ccparticleworld", &v, Image::new(40, 40), 2.0);
|
||
let v2 = pw_vals(12.0);
|
||
let _ = run("ec.sim.ccparticleworld", &v2, Image::new(40, 40), 1.0);
|
||
let stepped = run("ec.sim.ccparticleworld", &v2, Image::new(40, 40), 2.0);
|
||
let direct2 = run("ec.sim.ccparticleworld", &v2, Image::new(40, 40), 2.0);
|
||
assert_eq!(stepped, direct2);
|
||
assert!(direct.data.iter().any(|p| p[3] > 0.0), "particles alive");
|
||
// Before birth nothing is drawn.
|
||
let t0 = run("ec.sim.ccparticleworld", &v, Image::new(40, 40), 0.0);
|
||
assert!(t0.data.iter().all(|p| p[3] == 0.0));
|
||
}
|
||
|
||
#[test]
|
||
fn engine_resume_equals_from_scratch() {
|
||
for anim in 0..PW_ANIMS.len() as u32 {
|
||
let phys = Phys {
|
||
rate: 90.0,
|
||
life: 1.5,
|
||
producer: [0.0; 3],
|
||
radius: [0.05; 3],
|
||
anim,
|
||
speed: 0.5,
|
||
gravity: [0.0, 0.5, 0.0],
|
||
resistance: 0.2,
|
||
axis: [0.0, -1.0, 0.0],
|
||
extra: 1.0,
|
||
extra_angle: 60.0,
|
||
seed: 9,
|
||
max: 10_000,
|
||
flat: anim % 2 == 0,
|
||
};
|
||
let a: SimCache<PState> = SimCache::new(2);
|
||
let b: SimCache<PState> = SimCache::new(2);
|
||
let direct = simulate(&a, 1, 2.0, &phys);
|
||
let _ = simulate(&b, 1, 1.0, &phys);
|
||
let _ = simulate(&b, 1, 0.37, &phys);
|
||
let resumed = simulate(&b, 1, 2.0, &phys);
|
||
assert_eq!(direct.parts.len(), resumed.parts.len(), "anim {anim}");
|
||
assert!(direct.parts.iter().zip(&resumed.parts).all(|(p, q)| p.p == q.p && p.v == q.v && p.id == q.id), "anim {anim}");
|
||
assert!(!direct.parts.is_empty());
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn particle_world_gravity_pulls_down() {
|
||
let base = |g: f64| {
|
||
vec![
|
||
("birthRate", num(2.0)),
|
||
("physics/velocity", num(0.2)),
|
||
("physics/gravity", num(g)),
|
||
("particle/particleType", Value::Enum(2)),
|
||
("particle/birthSize", num(0.2)),
|
||
("particle/deathSize", num(0.2)),
|
||
("longevity", num(3.0)),
|
||
]
|
||
};
|
||
let blank = Image::new(48, 48);
|
||
let up = mass_y(&run("ec.sim.ccparticleworld", &base(0.0), blank.clone(), 1.0), &blank);
|
||
let down = mass_y(&run("ec.sim.ccparticleworld", &base(2.0), blank.clone(), 1.0), &blank);
|
||
assert!(down > up + 2.0, "{up} {down}");
|
||
}
|
||
|
||
#[test]
|
||
fn particle_systems2_seek_and_direction() {
|
||
let v = vec![
|
||
("producer/position", pt(24.0, 24.0)),
|
||
("birthRate", num(3.0)),
|
||
("physics/gravity", num(0.0)),
|
||
("physics/velocity", num(1.0)),
|
||
("physics/animation", Value::Enum(1)),
|
||
];
|
||
let _ = run("ec.sim.ccparticlesystems2", &v, Image::new(48, 48), 0.5);
|
||
let a = run("ec.sim.ccparticlesystems2", &v, Image::new(48, 48), 1.0);
|
||
let mut v2 = v.clone();
|
||
v2.push(("randomSeed", num(0.0)));
|
||
assert_eq!(a, run("ec.sim.ccparticlesystems2", &v2, Image::new(48, 48), 1.0));
|
||
// Direction 0° (up): particles above the producer.
|
||
let blank = Image::new(48, 48);
|
||
let y = mass_y(&a, &blank);
|
||
assert!((0.0..24.0).contains(&y), "{y}");
|
||
let none = run("ec.sim.ccparticlesystems2", &[("birthRate", num(0.0))], grey(20, 20), 1.0);
|
||
assert_eq!(none, grey(20, 20));
|
||
}
|
||
|
||
#[test]
|
||
fn mercury_blobs_and_seek() {
|
||
let v = vec![("producer", pt(24.0, 16.0)), ("radiusX", num(5.0)), ("radiusY", num(5.0)), ("birthRate", num(3.0))];
|
||
let _ = run("ec.sim.ccmrmercury", &v, ramp(48, 48), 0.7);
|
||
let a = run("ec.sim.ccmrmercury", &v, ramp(48, 48), 1.4);
|
||
let mut v2 = v.clone();
|
||
v2.push(("shading/metal", num(100.0)));
|
||
let b = run("ec.sim.ccmrmercury", &v2, ramp(48, 48), 1.4);
|
||
assert_eq!(a, b);
|
||
assert!(a.data.iter().any(|p| p[3] > 0.5), "blobs visible");
|
||
let none = run("ec.sim.ccmrmercury", &[("birthRate", num(0.0))], ramp(48, 48), 1.4);
|
||
assert!(none.data.iter().all(|p| p[3] == 0.0));
|
||
}
|
||
|
||
#[test]
|
||
fn half_resolution_scales_sprites() {
|
||
// Downsampled buffers render the same layout at half size.
|
||
let s = crate::find("ec.sim.ccsnowfall").unwrap();
|
||
let mut params = crate::Params { values: s.params.iter().map(|p| (p.id.to_string(), p.default.clone())).collect() };
|
||
params.values.insert("flakes".into(), num(50.0));
|
||
params.values.insert("offset".into(), pt(20.0, 20.0));
|
||
let ctx = EffectCtx { params: ¶ms, time: 0.5, layer_size: [40.0, 40.0], seed: 1, adjustment: false, env: EffectEnv::default() };
|
||
let full = crate::apply(s, &ctx, Buf { img: grey(40, 40), offset: [0.0; 2], scale: 1.0 });
|
||
let half = crate::apply(s, &ctx, Buf { img: grey(20, 20), offset: [0.0; 2], scale: 0.5 });
|
||
let sum = |i: &Image| i.data.iter().map(|p| p[0]).sum::<f32>() / i.data.len() as f32;
|
||
assert!((sum(&full.img) - sum(&half.img)).abs() < 0.05, "{} {}", sum(&full.img), sum(&half.img));
|
||
}
|
||
}
|