From 67ef9ad93c6363c7c8415de487f480e03db4d5fe Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 21:43:57 +0000 Subject: [PATCH] refactor(effects): split sim2.rs into sim2/*.rs --- crates/effects/src/sim2.rs | 2210 +-------------------------- crates/effects/src/sim2/foam.rs | 408 +++++ crates/effects/src/sim2/pieces.rs | 277 ++++ crates/effects/src/sim2/pixels.rs | 228 +++ crates/effects/src/sim2/registry.rs | 288 ++++ crates/effects/src/sim2/shatter.rs | 481 ++++++ crates/effects/src/sim2/surfaces.rs | 524 +++++++ 7 files changed, 2228 insertions(+), 2188 deletions(-) create mode 100644 crates/effects/src/sim2/foam.rs create mode 100644 crates/effects/src/sim2/pieces.rs create mode 100644 crates/effects/src/sim2/pixels.rs create mode 100644 crates/effects/src/sim2/registry.rs create mode 100644 crates/effects/src/sim2/shatter.rs create mode 100644 crates/effects/src/sim2/surfaces.rs diff --git a/crates/effects/src/sim2.rs b/crates/effects/src/sim2.rs index c4f8c8c17..aec8567d3 100644 --- a/crates/effects/src/sim2.rs +++ b/crates/effects/src/sim2.rs @@ -16,2194 +16,28 @@ use crate::sim::{Acc, Post, Shape, Sprite, SpritePlan, h, hs, pct, pt, raster, s use crate::util::{Plane, SimCache, gauss_plane, layer_or_self, params_key, unpremul}; use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider}; -// ------------------------------------------------------------------ 3D pieces - -pub(crate) type M3 = [[f64; 3]; 3]; - -fn mat_mul(a: &M3, b: &M3) -> M3 { - std::array::from_fn(|i| std::array::from_fn(|j| (0..3).map(|k| a[i][k] * b[k][j]).sum())) -} - -/// Rotation from Euler angles (radians), applied X then Y then Z. -fn rot_xyz(x: f64, y: f64, z: f64) -> M3 { - let (sx, cx) = x.sin_cos(); - let (sy, cy) = y.sin_cos(); - let (sz, cz) = z.sin_cos(); - let rx = [[1.0, 0.0, 0.0], [0.0, cx, -sx], [0.0, sx, cx]]; - let ry = [[cy, 0.0, sy], [0.0, 1.0, 0.0], [-sy, 0.0, cy]]; - let rz = [[cz, -sz, 0.0], [sz, cz, 0.0], [0.0, 0.0, 1.0]]; - mat_mul(&rz, &mat_mul(&ry, &rx)) -} - -/// Rotation by `angle` about unit `axis` (Rodrigues). -pub(crate) fn rot_axis(axis: [f64; 3], angle: f64) -> M3 { - let l = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt(); - if l < 1e-12 || angle == 0.0 { - return [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]; - } - let (x, y, z) = (axis[0] / l, axis[1] / l, axis[2] / l); - let (s, c) = angle.sin_cos(); - let t = 1.0 - c; - [ - [t * x * x + c, t * x * y - s * z, t * x * z + s * y], - [t * x * y + s * z, t * y * y + c, t * y * z - s * x], - [t * x * z - s * y, t * y * z + s * x, t * z * z + c], - ] -} - -fn inv3(m: &M3) -> Option { - let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) - + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]); - if det.abs() < 1e-12 { - return None; - } - let d = 1.0 / det; - Some([ - [(m[1][1] * m[2][2] - m[1][2] * m[2][1]) * d, (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * d, (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * d], - [(m[1][2] * m[2][0] - m[1][0] * m[2][2]) * d, (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * d, (m[0][2] * m[1][0] - m[0][0] * m[1][2]) * d], - [(m[1][0] * m[2][1] - m[1][1] * m[2][0]) * d, (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * d, (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * d], - ]) -} - -/// Perspective camera looking down +z at the layer plane z = 0, `d` pixels away, centred on -/// `c` (buffer pixels): screen = c + (P - c) · d / (d + z). -#[derive(Clone, Copy)] -struct Cam { - c: [f64; 2], - d: f64, -} - -impl Cam { - fn proj(self) -> Proj { - Proj::simple(self.c, self.d) - } -} - -/// A planar textured piece ready to draw. -#[derive(Clone, Copy, Debug)] -pub struct Piece { - /// Screen (buffer px, homogeneous) → piece-local (a, b, w). - pub inv: [[f64; 3]; 3], - /// Rest centre of the piece in texture pixels. - pub c0: [f32; 2], - /// Convex polygon (texture pixels), counter-clockwise in y-down space. - pub poly: [[f32; 2]; 6], - pub n: u8, - pub bbox: [f32; 4], - pub shade: f32, - /// Lighting: colour multiplier and additive specular highlight. - pub tint: [f32; 3], - pub spec: [f32; 3], - pub alpha: f32, - /// Flat straight colour instead of the texture. - pub flat: Option<[f32; 3]>, - /// Facing away from the camera (draw the back texture). - pub back: bool, - /// Back faces sample their texture mirrored about the piece centre: 0 no, 1 vertically - /// (flipped about the X axis), 2 horizontally (about the Y axis). - pub back_mirror: u8, - depth: f64, -} - -/// A piece texture: the layer itself or another image (buffer-aligned). -#[derive(Clone, Debug)] -pub enum PieceTex { - Layer, - Image(Image), -} - -/// What a piece-drawing effect (Card Dance, Shatter, Card Wipe) draws this frame: pieces sorted -/// far to near over a transparent frame, textured by `front` (and `back` for pieces facing -/// away; none = they take `front`). -#[derive(Clone, Debug)] -pub struct PiecePlan { - pub pieces: Vec, - pub front: PieceTex, - pub back: Option, -} - -impl PiecePlan { - /// Rasterise over a transparent frame of the layer `b`'s size. - pub fn finish(&self, mut b: Buf) -> Buf { - let mut out = Image::new(b.img.width, b.img.height); - let front = match &self.front { - PieceTex::Layer => &b.img, - PieceTex::Image(i) => i, - }; - let back = self.back.as_ref().map(|t| match t { - PieceTex::Layer => &b.img, - PieceTex::Image(i) => i, - }); - draw_pieces(&mut out, &self.pieces, front, back); - b.img = out; - b - } -} - -/// The piece plan of Card Dance, Shatter (Rendered view) or Card Wipe for layer `b` (its pixels -/// may be read: gradient maps and textures default to the layer), `None` for other effects or -/// views drawn otherwise. -pub fn piece_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option { - match id { - "ec.sim.carddance" => Some(card_dance_plan(ctx, b)), - "ec.sim.shatter" if ctx.params.e("view") == 0 => match shatter_impl(ctx, b) { - ShatterPlan::Pieces(plan) => Some(plan), - ShatterPlan::Wire(_) => None, - }, - "ec.transition.cardwipe" => crate::transition::card_wipe_plan(ctx, b), - _ => None, - } -} - -impl Piece { - /// Place polygon `poly` (texture px, around rest centre `c0`) with rotation `r` and its - /// centre at 3D `pos` (buffer px; z away from the camera), seen through `cam`. - pub(crate) fn new(poly: &[[f32; 2]], c0: [f32; 2], r: &M3, pos: [f64; 3], cam: impl Into) -> Option { - let cam: Proj = cam.into(); - let n = poly.len().min(6); - if n < 3 { - return None; - } - let m = &cam.m; - let row = |i: usize| -> [f64; 3] { - [ - m[i][0] * r[0][0] + m[i][1] * r[1][0] + m[i][2] * r[2][0], - m[i][0] * r[0][1] + m[i][1] * r[1][1] + m[i][2] * r[2][1], - m[i][0] * pos[0] + m[i][1] * pos[1] + m[i][2] * pos[2] + m[i][3], - ] - }; - let hm: M3 = [row(0), row(1), row(2)]; - let mut bb = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY]; - for v in &poly[..n] { - let (a, b) = ((v[0] - c0[0]) as f64, (v[1] - c0[1]) as f64); - let w = hm[2][0] * a + hm[2][1] * b + hm[2][2]; - if w < cam.near { - return None; - } - let sx = ((hm[0][0] * a + hm[0][1] * b + hm[0][2]) / w) as f32; - let sy = ((hm[1][0] * a + hm[1][1] * b + hm[1][2]) / w) as f32; - bb = [bb[0].min(sx), bb[1].min(sy), bb[2].max(sx), bb[3].max(sy)]; - } - let inv = inv3(&hm)?; - let mut pp = [[0.0f32; 2]; 6]; - pp[..n].copy_from_slice(&poly[..n]); - // Normalise winding so "inside" is cross >= 0 for every edge. - let area: f32 = (0..n).map(|i| pp[i][0] * pp[(i + 1) % n][1] - pp[(i + 1) % n][0] * pp[i][1]).sum(); - if area < 0.0 { - pp[..n].reverse(); - } - // The front faces -z at rest; it faces the camera when its normal points at the eye. - let eye = cam.eye(); - let nrm = [-r[0][2], -r[1][2], -r[2][2]]; - let to_eye = [eye[0] - pos[0], eye[1] - pos[1], eye[2] - pos[2]]; - let facing = nrm[0] * to_eye[0] + nrm[1] * to_eye[1] + nrm[2] * to_eye[2]; - let depth = hm[2][2]; - Some(Piece { - inv, - c0, - poly: pp, - n: n as u8, - bbox: [bb[0] - 1.0, bb[1] - 1.0, bb[2] + 1.0, bb[3] + 1.0], - shade: 1.0, - tint: [1.0; 3], - spec: [0.0; 3], - alpha: 1.0, - flat: None, - back: facing < 0.0, - back_mirror: 0, - depth, - }) - } - - /// Lambert factor of the piece normal against a light shining along +z. - fn facing(r: &M3) -> f32 { - r[2][2].abs() as f32 - } - - /// Light the piece (rotation `r`, centre `pos`) with Lighting / Material. - pub(crate) fn light(&mut self, l: &Lighting, r: &M3, pos: [f64; 3], eye: [f64; 3]) { - let (t, s) = l.shade([-r[0][2], -r[1][2], -r[2][2]], pos, eye); - self.tint = t; - self.spec = s; - } -} - -impl From for Proj { - fn from(c: Cam) -> Proj { - c.proj() - } -} - -/// Draw pieces (already sorted far → near) over `out`. -pub(crate) fn draw_pieces(out: &mut Image, pieces: &[Piece], front: &Image, back: Option<&Image>) { - raster( - out, - pieces, - |q| Some(q.bbox), - |q, x, y| { - let (xf, yf) = (x as f64, y as f64); - let m = &q.inv; - let w = m[2][0] * xf + m[2][1] * yf + m[2][2]; - if w.abs() < 1e-12 { - return None; - } - let u = ((m[0][0] * xf + m[0][1] * yf + m[0][2]) / w) as f32 + q.c0[0]; - let v = ((m[1][0] * xf + m[1][1] * yf + m[1][2]) / w) as f32 + q.c0[1]; - let n = q.n as usize; - let mut md = f32::INFINITY; - for i in 0..n { - let a = q.poly[i]; - let b = q.poly[(i + 1) % n]; - let (ex, ey) = (b[0] - a[0], b[1] - a[1]); - let l = (ex * ex + ey * ey).sqrt().max(1e-6); - md = md.min((ex * (v - a[1]) - ey * (u - a[0])) / l); - } - // Coverage extends half a pixel past the edge so abutting pieces leave no seams. - let cov = (md + 1.0).clamp(0.0, 1.0); - if cov <= 0.0 { - return None; - } - let (c, a) = match q.flat { - Some(c) => (c, 1.0), - None => { - let tex = if q.back { back.unwrap_or(front) } else { front }; - let (u, v) = match (q.back, q.back_mirror) { - (true, 1) => (u, 2.0 * q.c0[1] - v), - (true, 2) => (2.0 * q.c0[0] - u, v), - _ => (u, v), - }; - unpremul(tex.sample_bilinear(u as f64, v as f64)) - } - }; - let a = a * cov * q.alpha; - if a <= 1e-6 { - return None; - } - let k = q.shade; - let o: [f32; 3] = std::array::from_fn(|i| c[i] * k * q.tint[i] + q.spec[i]); - Some([o[0] * a, o[1] * a, o[2] * a, a]) - }, - Acc::Over, - ); -} - -pub(crate) fn sort_far_first(pieces: &mut [Piece]) { - pieces.sort_by(|a, b| b.depth.total_cmp(&a.depth)); -} - -// ------------------------------------------------------------------ CC Ball Action - -fn ball_action(ctx: &EffectCtx, b: Buf) -> Buf { - ball_action_plan(ctx, &b).finish(b) -} - -/// CC Ball Action's balls (far to near), coloured by the layer's pixels. -fn ball_action_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan { - let pr = ctx.params; - let scatter = pr.f("scatter") as f32; - let axis = pr.e("rotationAxis"); - let rot = pr.f("rotation").to_radians(); - let twist_prop = pr.e("twistProperty"); - let twist = pr.f("twistAngle").to_radians(); - let spacing = pr.f("gridSpacing").max(1.0) as f32; - let size = pr.f("ballSize") as f32 / 100.0; - let instab = pr.f("instabilityState").to_radians() as f32; - let seed = ctx.seed.wrapping_mul(0x2545f491); - let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32); - let s = b.scale as f32; - let nx = (lw / spacing).ceil().max(1.0) as u32; - let ny = (lh / spacing).ceil().max(1.0) as u32; - let d = 2.0 * lw.max(lh) as f64; - let (cx, cy) = (lw as f64 * 0.5, lh as f64 * 0.5); - let src = &b.img; - let ax: [f64; 3] = match axis { - 0 => [1.0, 0.0, 0.0], - 1 => [0.0, 1.0, 0.0], - 2 => [0.0, 0.0, 1.0], - 3 => [1.0, 1.0, 0.0], - 4 => [1.0, 0.0, 1.0], - 5 => [0.0, 1.0, 1.0], - _ => [1.0, 1.0, 1.0], - }; - let maxr = ((lw * lw + lh * lh).sqrt() * 0.5).max(1.0); - let mut balls: Vec<(f64, Sprite)> = (0..nx * ny) - .into_par_iter() - .filter_map(|i| { - let gx = (i % nx) as f32 * spacing + spacing * 0.5; - let gy = (i / nx) as f32 * spacing + spacing * 0.5; - let (sx, sy) = b.to_px([gx as f64, gy as f64]); - let (c, a) = unpremul(src.sample_bilinear(sx, sy)); - if a <= 0.0 || size <= 0.0 { - return None; - } - let tp = match twist_prop { - 0 => gx / lw.max(1.0), - 1 => gy / lh.max(1.0), - 2 => ((gx - cx as f32).hypot(gy - cy as f32)) / maxr, - _ => crate::util::rgb3([c[0], c[1], c[2], 0.0]).iter().sum::() / 3.0, - } as f64; - let ang = rot + twist * tp; - // Scatter: random 3D offsets whose direction drifts with the instability state. - let ph = h(i, 1, seed) * std::f32::consts::TAU + instab; - let sc = scatter * h(i, 2, seed); - let off = [(sc * ph.cos()) as f64, (sc * ph.sin()) as f64, (scatter * hs(i, 3, seed)) as f64]; - let p0 = [gx as f64 - cx + off[0], gy as f64 - cy + off[1], off[2]]; - let m = rot_axis(ax, ang); - let q: [f64; 3] = std::array::from_fn(|k| m[k][0] * p0[0] + m[k][1] * p0[1] + m[k][2] * p0[2]); - if d + q[2] <= d * 0.05 { - return None; - } - let persp = d / (d + q[2]); - let (bx, by) = b.to_px([cx + q[0] * persp, cy + q[1] * persp]); - let r = spacing * 0.5 * size * persp as f32 * s; - Some((q[2], Sprite::new(bx as f32, by as f32, r, [c[0], c[1], c[2], a], Shape::Sphere))) - }) - .collect(); - balls.sort_by(|a, b| b.0.total_cmp(&a.0)); - let sprites: Vec = balls.into_iter().map(|(_, s)| s).collect(); - SpritePlan { sprites, tints: vec![], acc: Acc::Over, post: Post::Replace } -} - -// ------------------------------------------------------------------ CC Pixel Polly - -fn pixel_polly(ctx: &EffectCtx, b: Buf) -> Buf { - match pixel_polly_plan(ctx, &b) { - Some(plan) => plan.finish(b), - None => b, - } -} - -/// CC Pixel Polly's pieces, `None` before Start Time (the layer as is). -fn pixel_polly_plan(ctx: &EffectCtx, b: &Buf) -> Option { - let pr = ctx.params; - let t = ctx.time - pr.f("startTime"); - if t <= 0.0 { - return None; - } - let force = pr.f("force"); - let gravity = pr.f("gravity"); - let spin = pr.f("spinning").to_radians(); - let fc = pr.v2("forceCenter"); - let dir_rand = pr.f("directionRandomness") / 100.0; - let spd_rand = pr.f("speedRandomness") / 100.0; - let g = pr.f("gridSpacing").max(1.0); - let object = pr.e("object"); - let depth_sort = pr.b("enableDepthSort"); - let seed = ctx.seed.wrapping_mul(0x632be5ab); - let [lw, lh] = ctx.layer_size; - let s = b.scale; - let cam = Cam { c: [b.offset[0] + lw * 0.5 * s, b.offset[1] + lh * 0.5 * s], d: 2.0 * lw.max(lh) * s }; - let nx = (lw / g).ceil().max(1.0) as u32; - let ny = (lh / g).ceil().max(1.0) as u32; - let tri = object <= 1; - let textured = object == 1 || object == 3; - let maxd = lw.max(lh); - let src = &b.img; - let per = if tri { 2 } else { 1 }; - let mut pieces: Vec = (0..nx * ny * per) - .into_par_iter() - .filter_map(|i| { - let cell = i / per; - let (x0, y0) = ((cell % nx) as f64 * g, (cell / nx) as f64 * g); - let poly: Vec<[f64; 2]> = if !tri { - vec![[x0, y0], [x0 + g, y0], [x0 + g, y0 + g], [x0, y0 + g]] - } else if i % 2 == 0 { - vec![[x0, y0], [x0 + g, y0], [x0, y0 + g]] - } else { - vec![[x0 + g, y0], [x0 + g, y0 + g], [x0, y0 + g]] - }; - let c = [poly.iter().map(|v| v[0]).sum::() / poly.len() as f64, poly.iter().map(|v| v[1]).sum::() / poly.len() as f64]; - let (dx, dy) = (c[0] - fc[0], c[1] - fc[1]); - let dist = (dx * dx + dy * dy).sqrt().max(1e-6); - let ang = dy.atan2(dx) + dir_rand * std::f64::consts::PI * hs(i, 1, seed) as f64; - let speed = force / 100.0 * 0.6 * maxd / (1.0 + dist / (0.5 * maxd)) * (1.0 + spd_rand * hs(i, 2, seed) as f64); - let vz = -speed * 0.5 * h(i, 3, seed) as f64; - let pos = [c[0] + ang.cos() * speed * t, c[1] + ang.sin() * speed * t + 0.5 * gravity * 0.5 * lh * t * t, vz * t]; - let axis = [hs(i, 4, seed) as f64, hs(i, 5, seed) as f64, hs(i, 6, seed) as f64]; - let r = rot_axis(axis, t * (std::f64::consts::PI * h(i, 7, seed) as f64 + spin)); - let tex: Vec<[f32; 2]> = poly.iter().map(|v| [(v[0] * s + b.offset[0]) as f32, (v[1] * s + b.offset[1]) as f32]).collect(); - let c0 = [(c[0] * s + b.offset[0]) as f32, (c[1] * s + b.offset[1]) as f32]; - let posb = [pos[0] * s + b.offset[0], pos[1] * s + b.offset[1], pos[2] * s]; - let mut pc = Piece::new(&tex, c0, &r, posb, cam)?; - pc.shade = 0.35 + 0.65 * Piece::facing(&r); - if !textured { - let (cc, a) = unpremul(src.sample_bilinear(c0[0] as f64, c0[1] as f64)); - if a <= 0.0 { - return None; - } - pc.flat = Some(cc); - pc.alpha = a; - } - Some(pc) - }) - .collect(); - if depth_sort { - sort_far_first(&mut pieces); - } - Some(PiecePlan { pieces, front: PieceTex::Layer, back: None }) -} - -// ------------------------------------------------------------------ CC Scatterize - -fn scatterize(ctx: &EffectCtx, b: Buf) -> Buf { - match scatterize_plan(ctx, &b) { - Some(plan) => plan.finish(b), - None => b, - } -} - -/// CC Scatterize's pixel dots, `None` when nothing moves (the layer as is). -fn scatterize_plan(ctx: &EffectCtx, b: &Buf) -> Option { - let pr = ctx.params; - let scatter = pr.f("scatter") as f32; - let right = pr.f("rightTwist").to_radians(); - let left = pr.f("leftTwist").to_radians(); - if scatter == 0.0 && right == 0.0 && left == 0.0 { - return None; - } - let add = pr.e("transferMode") == 1; - let seed = ctx.seed.wrapping_mul(0x7feb352d); - let s = b.scale; - let (w, hh) = (b.img.width, b.img.height); - let (cx, cy) = (b.offset[0] + ctx.layer_size[0] * 0.5 * s, b.offset[1] + ctx.layer_size[1] * 0.5 * s); - let half = (ctx.layer_size[0] * 0.5 * s).max(1.0); - let d = 2.0 * ctx.layer_size[0].max(ctx.layer_size[1]) * s; - let src = &b.img; - let sprites: Vec = (0..w * hh) - .into_par_iter() - .filter_map(|i| { - let px = src.data[i as usize]; - if px[3] <= 0.0 { - return None; - } - let (x, y) = ((i % w) as f64 + 0.5, (i / w) as f64 + 0.5); - // Twist about the horizontal axis through the centre, growing towards each side. - let k = ((x - cx) / half).abs().min(1.0); - let ang = if x < cx { left } else { right } * k; - let (sa, ca) = ang.sin_cos(); - let (yy, zz) = ((y - cy) * ca, (y - cy) * sa); - let persp = d / (d + zz).max(d * 0.05); - let jx = hs(i, 1, seed) * scatter * s as f32; - let jy = hs(i, 2, seed) * scatter * s as f32; - let sx = cx + (x - cx) * persp + jx as f64; - let sy = cy + yy * persp + jy as f64; - let (c, a) = unpremul(px); - Some(Sprite::new(sx as f32, sy as f32, 0.6 * persp as f32, [c[0], c[1], c[2], a], Shape::Disc)) - }) - .collect(); - Some(SpritePlan { sprites, tints: vec![], acc: if add { Acc::Add } else { Acc::Over }, post: Post::Replace }) -} - -/// What a particle effect that reads the layer's pixels draws this frame (CC Ball Action, CC -/// Pixel Polly, CC Scatterize, Shatter's wireframe views), for layer `b`; `None` for other -/// effects or a frame showing the layer as is. -pub enum PixelPlan { - Sprites(SpritePlan), - Pieces(PiecePlan), -} - -/// The [`PixelPlan`] of effect `id` for layer `b` (its pixels are read). -pub fn pixel_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option { - match id { - "ec.sim.ccballaction" => Some(PixelPlan::Sprites(ball_action_plan(ctx, b))), - "ec.sim.ccpixelpolly" => pixel_polly_plan(ctx, b).map(PixelPlan::Pieces), - "ec.sim.ccscatterize" => scatterize_plan(ctx, b).map(PixelPlan::Sprites), - "ec.sim.shatter" => Some(match shatter_impl(ctx, b) { - ShatterPlan::Pieces(plan) => PixelPlan::Pieces(plan), - ShatterPlan::Wire(plan) => PixelPlan::Sprites(plan), - }), - _ => None, - } -} - -// ------------------------------------------------------------------ Card Dance - -const CD_SOURCES: &[&str] = &["None", "Intensity 1", "Red 1", "Green 1", "Blue 1", "Alpha 1", "Intensity 2", "Red 2", "Green 2", "Blue 2", "Alpha 2"]; -/// (param id prefix, twirl-down group match id) per transformed card property. -const CD_PROPS: [(&str, &str); 8] = [ - ("xPos", "xPosition"), - ("yPos", "yPosition"), - ("zPos", "zPosition"), - ("xRot", "xRotation"), - ("yRot", "yRotation"), - ("zRot", "zRotation"), - ("xScale", "xScale"), - ("yScale", "yScale"), -]; - -fn card_source(src: u32, g1: [f32; 4], g2: [f32; 4]) -> f32 { - let (c1, a1) = unpremul(g1); - let (c2, a2) = unpremul(g2); - let lum = |c: [f32; 3]| effectcraft_color::luminance(c[0], c[1], c[2]); - match src { - 1 => lum(c1), - 2 => c1[0], - 3 => c1[1], - 4 => c1[2], - 5 => a1, - 6 => lum(c2), - 7 => c2[0], - 8 => c2[1], - 9 => c2[2], - 10 => a2, - _ => 0.0, - } -} - -fn card_dance(ctx: &EffectCtx, b: Buf) -> Buf { - card_dance_plan(ctx, &b).finish(b) -} - -fn card_dance_plan(ctx: &EffectCtx, b: &Buf) -> PiecePlan { - let pr = ctx.params; - let rows = pr.f("rows").clamp(1.0, 1000.0) as u32; - let cols = if pr.e("rowsColumns") == 1 { rows } else { pr.f("columns").clamp(1.0, 1000.0) as u32 }; - let g1 = layer_or_self(ctx, b, "gradientLayer1", true, true); - let g2 = layer_or_self(ctx, b, "gradientLayer2", true, true); - let back = ctx.layer_param("backLayer", true).map(|o| crate::util::fit_layer(ctx, b, &o, true)); - let [lw, lh] = ctx.layer_size; - let s = b.scale; - let (cw, ch) = (lw / cols as f64, lh / rows as f64); - let cam = crate::card3d::projection(ctx, b); - let eye = cam.eye(); - let light = Lighting::from(ctx, b); - let props: Vec<(u32, f64, f64)> = - CD_PROPS.iter().map(|(id, g)| (pr.e(&format!("{g}/{id}Source")), pr.f(&format!("{g}/{id}Multiplier")), pr.f(&format!("{g}/{id}Offset")))).collect(); - let mut pieces: Vec = (0..rows * cols) - .into_par_iter() - .filter_map(|i| { - let (cx0, cy0) = ((i % cols) as f64 * cw, (i / cols) as f64 * ch); - let c = [cx0 + cw * 0.5, cy0 + ch * 0.5]; - let (bx, by) = (c[0] * s + b.offset[0], c[1] * s + b.offset[1]); - let (gp1, gp2) = (g1.sample_bilinear(bx, by), g2.sample_bilinear(bx, by)); - let val = |k: usize| { - let (sr, m, o) = props[k]; - o + m * card_source(sr, gp1, gp2) as f64 - }; - let (sx, sy) = (val(6), val(7)); - let r = rot_xyz(val(3).to_radians(), val(4).to_radians(), val(5).to_radians()); - let rs: M3 = std::array::from_fn(|k| [r[k][0] * sx, r[k][1] * sy, r[k][2]]); - let pos = [bx + val(0) * cw * s, by + val(1) * ch * s, val(2) * cw.max(ch) * s]; - let (tx0, ty0) = ((cx0 * s + b.offset[0]) as f32, (cy0 * s + b.offset[1]) as f32); - let (tw, th) = ((cw * s) as f32, (ch * s) as f32); - let poly = [[tx0, ty0], [tx0 + tw, ty0], [tx0 + tw, ty0 + th], [tx0, ty0 + th]]; - let mut pc = Piece::new(&poly, [bx as f32, by as f32], &rs, pos, cam)?; - pc.light(&light, &r, pos, eye); - Some(pc) - }) - .collect(); - sort_far_first(&mut pieces); - PiecePlan { pieces, front: PieceTex::Layer, back: back.map(PieceTex::Image) } -} - -// ------------------------------------------------------------------ Shatter - -/// Shatter's Pattern options; the last, Custom, takes its pieces from Custom Shatter Map. -const SHATTER_PATTERNS: &[&str] = &["Bricks", "Glass", "Hexagons", "Squares", "Triangles", "Custom"]; - -/// Pattern cells (polygons in pattern space, before rotation) covering `[x0, x1]×[y0, y1]`. -fn pattern_cells(kind: u32, cell: f64, x0: f64, y0: f64, x1: f64, y1: f64, seed: u32) -> Vec> { - let mut out = Vec::new(); - let i0 = (x0 / cell).floor() as i64 - 1; - let i1 = (x1 / cell).ceil() as i64 + 1; - let j0 = (y0 / cell).floor() as i64 - 1; - let j1 = (y1 / cell).ceil() as i64 + 1; - match kind { - 0 => { - let bh = cell * 0.5; - let r0 = (y0 / bh).floor() as i64 - 1; - let r1 = (y1 / bh).ceil() as i64 + 1; - for r in r0..=r1 { - let sh = if r.rem_euclid(2) == 1 { cell * 0.5 } else { 0.0 }; - for i in i0..=i1 { - let (x, y) = (i as f64 * cell + sh, r as f64 * bh); - out.push(vec![[x, y], [x + cell, y], [x + cell, y + bh], [x, y + bh]]); - } - } - } - 1 => { - // Jittered lattice split into triangles along a random diagonal. - let jit = |i: i64, j: i64| -> [f64; 2] { - let (a, b) = (i as u32, j as u32); - [i as f64 * cell + hs(a, b.wrapping_add(17), seed) as f64 * cell * 0.35, j as f64 * cell + hs(a, b.wrapping_add(91), seed) as f64 * cell * 0.35] - }; - for j in j0..=j1 { - for i in i0..=i1 { - let (p00, p10, p11, p01) = (jit(i, j), jit(i + 1, j), jit(i + 1, j + 1), jit(i, j + 1)); - if h(i as u32, j as u32, seed ^ 0x55) < 0.5 { - out.push(vec![p00, p10, p11]); - out.push(vec![p00, p11, p01]); - } else { - out.push(vec![p00, p10, p01]); - out.push(vec![p10, p11, p01]); - } - } - } - } - 2 => { - let r = cell / 3f64.sqrt(); - let dx = 1.5 * r; - let dy = 3f64.sqrt() * r; - let c0 = (x0 / dx).floor() as i64 - 1; - let c1 = (x1 / dx).ceil() as i64 + 1; - let rr0 = (y0 / dy).floor() as i64 - 1; - let rr1 = (y1 / dy).ceil() as i64 + 1; - for ci in c0..=c1 { - for rj in rr0..=rr1 { - let cx = ci as f64 * dx; - let cy = (rj as f64 + if ci.rem_euclid(2) == 1 { 0.5 } else { 0.0 }) * dy; - out.push( - (0..6) - .map(|k| { - let a = k as f64 * std::f64::consts::PI / 3.0; - [cx + r * a.cos(), cy + r * a.sin()] - }) - .collect(), - ); - } - } - } - 3 => { - for j in j0..=j1 { - for i in i0..=i1 { - let (x, y) = (i as f64 * cell, j as f64 * cell); - out.push(vec![[x, y], [x + cell, y], [x + cell, y + cell], [x, y + cell]]); - } - } - } - _ => { - for j in j0..=j1 { - for i in i0..=i1 { - let (x, y) = (i as f64 * cell, j as f64 * cell); - if (i + j).rem_euclid(2) == 0 { - out.push(vec![[x, y], [x + cell, y], [x, y + cell]]); - out.push(vec![[x + cell, y], [x + cell, y + cell], [x, y + cell]]); - } else { - out.push(vec![[x, y], [x + cell, y], [x + cell, y + cell]]); - out.push(vec![[x, y], [x + cell, y + cell], [x, y + cell]]); - } - } - } - } - } - out -} - -/// `∫0^t e^{-ks} ds` and `∫0^t ∫0^u e^{-ks} ds du` (drag-damped ballistic factors). -fn drag_factors(k: f64, t: f64) -> (f64, f64) { - if k < 1e-6 { - (t, 0.5 * t * t) - } else { - let e = (-k * t).exp(); - ((1.0 - e) / k, t / k - (1.0 - e) / (k * k)) - } -} -/// Shatter's Front / Side / Back Mode options. -const SHATTER_TEXTURE_MODES: [&str; 3] = ["Color", "Layer", "Tinted Layer"]; - -/// One face's texture: a flat colour, a layer, or a layer tinted by the Textures colour. -struct Face { - mode: u32, - img: Option, -} - -impl Face { - fn from(ctx: &EffectCtx, b: &Buf, mode: &str, layer: &str, default_mode: u32) -> Face { - let mode = ctx.params.get(mode).map(|v| v.as_enum()).unwrap_or(default_mode); - // A face layer of None is the effect's own layer. - let img = (mode != 0).then(|| layer_or_self(ctx, b, layer, true, true)); - Face { mode, img } - } -} - -/// Group key of a Custom Shatter Map colour (pieces of one colour break as one). -fn map_key(c: [f32; 4]) -> u32 { - let (c, a) = unpremul(c); - if a < 0.5 { - return u32::MAX; - } - let q = |v: f32| ((v.clamp(0.0, 1.0) * 15.0).round() as u32) & 15; - q(c[0]) << 8 | q(c[1]) << 4 | q(c[2]) -} - -fn shatter(ctx: &EffectCtx, b: Buf) -> Buf { - match shatter_impl(ctx, &b) { - ShatterPlan::Pieces(plan) => plan.finish(b), - ShatterPlan::Wire(plan) => plan.finish(b), - } -} - -/// What Shatter draws: the pieces (Rendered view) or the wireframe lines. -enum ShatterPlan { - Pieces(PiecePlan), - Wire(SpritePlan), -} - -fn shatter_impl(ctx: &EffectCtx, b: &Buf) -> ShatterPlan { - let pr = ctx.params; - let view = pr.e("view"); - let render = pr.e("render"); - let kind = pr.e("shape/pattern"); - let custom = kind as usize == SHATTER_PATTERNS.len() - 1; - let reps = pr.f("shape/repetitions").clamp(1.0, 500.0); - let dir = pr.f("shape/direction").to_radians(); - let origin = pr.v2("shape/origin"); - let forces = [ - (pr.v2("force1/force1Position"), pr.f("force1/force1Depth"), pr.f("force1/force1Radius"), pr.f("force1/force1Strength")), - (pr.v2("force2/force2Position"), pr.f("force2/force2Depth"), pr.f("force2/force2Radius"), pr.f("force2/force2Strength")), - ]; - let rot_speed = pr.f("physics/rotationSpeed"); - let tumble = pr.e("physics/tumbleAxis"); - let randomness = pr.f("physics/randomness"); - let k = pr.f("physics/viscosity").max(0.0) * 5.0; - let mass_var = pr.f("physics/massVariance") / 100.0; - let gravity = pr.f("physics/gravity"); - let gdir = pr.f("physics/gravityDirection").to_radians(); - let ginc = pr.f("physics/gravityInclination").to_radians(); - let seed = (pr.f("randomSeed") as u32).wrapping_mul(0x9e3779b1) ^ ctx.seed; - let [lw, lh] = ctx.layer_size; - let s = b.scale; - let t = ctx.time.max(0.0); - let unit = lw * 0.1; - let cell = lw / reps; - let extrude = pr.f("shape/extrusionDepth").max(0.0) * cell * s; - let cam = crate::card3d::projection(ctx, b); - let eye = cam.eye(); - let light = Lighting::from(ctx, b); - // Gradient: with a gradient layer only pieces at least as bright as 1 − Shatter Threshold - // (Invert Gradient: as dark) break. - let gradient = ctx.layer_param("gradient/gradientLayer", true).map(|o| crate::util::fit_layer(ctx, b, &o, true)); - let threshold = pr.get("gradient/shatterThreshold").map(Value::as_f64).unwrap_or(0.0) / 100.0; - let invert_gradient = pr.b("gradient/invertGradient"); - // Textures. - let tex_color = pr.get("textures/color").map(|v| v.as_color()).unwrap_or([1.0; 4]); - let tex_opacity = pr.get("textures/opacity").map(Value::as_f64).unwrap_or(1.0).clamp(0.0, 1.0) as f32; - let front = Face::from(ctx, b, "textures/frontMode", "textures/frontLayer", 1); - let side = Face::from(ctx, b, "textures/sideMode", "textures/sideLayer", 0); - let backf = Face::from(ctx, b, "textures/backMode", "textures/backLayer", 1); - let custom_map = custom.then(|| layer_or_self(ctx, b, "shape/customShatterMap", true, true)); - let white_fixed = pr.b("shape/whiteTilesFixed"); - let (sd, cd) = dir.sin_cos(); - let to_layer = |q: [f64; 2]| [origin[0] + q[0] * cd - q[1] * sd, origin[1] + q[0] * sd + q[1] * cd]; - // Pattern-space bounds of the layer rectangle. - let corners = [[0.0, 0.0], [lw, 0.0], [lw, lh], [0.0, lh]].map(|c: [f64; 2]| { - let (dx, dy) = (c[0] - origin[0], c[1] - origin[1]); - [dx * cd + dy * sd, -dx * sd + dy * cd] - }); - let (px0, px1) = (corners.iter().map(|c| c[0]).fold(f64::INFINITY, f64::min), corners.iter().map(|c| c[0]).fold(f64::NEG_INFINITY, f64::max)); - let (py0, py1) = (corners.iter().map(|c| c[1]).fold(f64::INFINITY, f64::min), corners.iter().map(|c| c[1]).fold(f64::NEG_INFINITY, f64::max)); - // Custom Shatter Map: square cells grouped by the map's colour. - let cells = pattern_cells(if custom { 3 } else { kind }, cell, px0, py0, px1, py1, seed); - let polys: Vec> = cells.iter().map(|poly| poly.iter().map(|&q| to_layer(q)).collect()).collect(); - let centre = |lp: &[[f64; 2]]| [lp.iter().map(|v| v[0]).sum::() / lp.len() as f64, lp.iter().map(|v| v[1]).sum::() / lp.len() as f64]; - let keys: Vec = match &custom_map { - Some(m) => polys - .iter() - .map(|lp| { - let c = centre(lp); - map_key(m.sample_bilinear(c[0] * s + b.offset[0], c[1] * s + b.offset[1])) - }) - .collect(), - None => (0..polys.len() as u32).collect(), - }; - // Group centres (custom map) — each group moves as one rigid piece. - let mut groups: std::collections::HashMap = std::collections::HashMap::new(); - for (lp, &key) in polys.iter().zip(&keys) { - let c = centre(lp); - let e = groups.entry(key).or_insert(([0.0; 2], 0.0)); - e.0[0] += c[0]; - e.0[1] += c[1]; - e.1 += 1.0; - } - let g3 = [gdir.sin() * ginc.cos() * gravity * unit, -gdir.cos() * ginc.cos() * gravity * unit, ginc.sin() * gravity * unit]; - let mut pieces: Vec = polys - .par_iter() - .zip(keys.par_iter()) - .flat_map_iter(|(lp, &key)| { - let mut out: Vec = vec![]; - let (mnx, mxx) = (lp.iter().map(|v| v[0]).fold(f64::INFINITY, f64::min), lp.iter().map(|v| v[0]).fold(f64::NEG_INFINITY, f64::max)); - let (mny, mxy) = (lp.iter().map(|v| v[1]).fold(f64::INFINITY, f64::min), lp.iter().map(|v| v[1]).fold(f64::NEG_INFINITY, f64::max)); - if mxx < 0.0 || mxy < 0.0 || mnx > lw || mny > lh || key == u32::MAX { - return out.into_iter(); - } - let pc_c = centre(lp); - let (gsum, gn) = groups[&key]; - let gc = [gsum[0] / gn, gsum[1] / gn]; - let i = key; - // Broken when inside a force sphere (centred `depth` in front of the layer). - let mut v = [0.0f64; 3]; - let mut broken = false; - for (fp, fd, fr, fs) in forces { - let rad = fr * lw; - if rad <= 0.0 { - continue; - } - let dv = [gc[0] - fp[0], gc[1] - fp[1], fd * lw]; - let dist = (dv[0] * dv[0] + dv[1] * dv[1] + dv[2] * dv[2]).sqrt(); - if dist < rad { - broken = true; - let f = fs * (1.0 - dist / rad) * unit / dist.max(1e-6); - for q in 0..3 { - v[q] += dv[q] * f; - } - } - } - if broken && let Some(g) = &gradient { - let (c, _) = unpremul(g.sample_bilinear(gc[0] * s + b.offset[0], gc[1] * s + b.offset[1])); - let mut l = effectcraft_color::luminance(c[0], c[1], c[2]).clamp(0.0, 1.0) as f64; - if invert_gradient { - l = 1.0 - l; - } - broken = threshold > 0.0 && l >= 1.0 - threshold; - } - if broken && custom && white_fixed && key == 0xfff { - broken = false; - } - if (broken && render == 1) || (!broken && render == 2) { - return out.into_iter(); - } - let mut gpos = [gc[0], gc[1], 0.0]; - let mut r = rot_axis([1.0, 0.0, 0.0], 0.0); - let moving = broken && t > 0.0; - if moving { - let mass = 1.0 + mass_var * hs(i, 1, seed) as f64; - for q in 0..3 { - v[q] = v[q] / mass + randomness * unit * hs(i, 2 + q as u32, seed) as f64 * 2.0; - } - let (f1, f2) = drag_factors(k, t); - for q in 0..3 { - gpos[q] += v[q] * f1 + g3[q] * f2; - } - let axis = match tumble { - 1 => [0.0, 0.0, 0.0], - 2 => [1.0, 0.0, 0.0], - 3 => [0.0, 1.0, 0.0], - 4 => [0.0, 0.0, 1.0], - 5 => [hs(i, 6, seed) as f64, hs(i, 7, seed) as f64, 0.0], - 6 => [hs(i, 6, seed) as f64, 0.0, hs(i, 8, seed) as f64], - 7 => [0.0, hs(i, 7, seed) as f64, hs(i, 8, seed) as f64], - _ => [hs(i, 6, seed) as f64, hs(i, 7, seed) as f64, hs(i, 8, seed) as f64], - }; - let speed = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt() / unit; - let omega = rot_speed * std::f64::consts::TAU * (0.5 + h(i, 9, seed) as f64) * speed.min(10.0); - r = rot_axis(axis, omega * f1); - } - // The piece's centre: its offset from the group centre, rotated with the group. - let off = [pc_c[0] - gc[0], pc_c[1] - gc[1], 0.0]; - let pos: [f64; 3] = std::array::from_fn(|q| gpos[q] + (0..3).map(|k| r[q][k] * off[k]).sum::()); - let tex: Vec<[f32; 2]> = lp.iter().map(|v| [(v[0] * s + b.offset[0]) as f32, (v[1] * s + b.offset[1]) as f32]).collect(); - let c0 = [(pc_c[0] * s + b.offset[0]) as f32, (pc_c[1] * s + b.offset[1]) as f32]; - let posb = [pos[0] * s + b.offset[0], pos[1] * s + b.offset[1], pos[2] * s]; - let Some(mut pc) = Piece::new(&tex, c0, &r, posb, cam) else { return out.into_iter() }; - pc.light(&light, &r, posb, eye); - pc.alpha = tex_opacity; - let face = if pc.back { &backf } else { &front }; - if face.mode == 0 { - pc.flat = Some([tex_color[0], tex_color[1], tex_color[2]]); - } else if face.mode == 2 { - pc.tint = std::array::from_fn(|q| pc.tint[q] * tex_color[q]); - } - // Extrusion: the sides of a moving piece, from its front outline back by the depth. - if moving && extrude > 0.0 { - let nb = [r[0][2], r[1][2], r[2][2]]; - let n = tex.len(); - for e in 0..n { - let (a, bv) = (tex[e], tex[(e + 1) % n]); - let ed = [(bv[0] - a[0]) as f64, (bv[1] - a[1]) as f64]; - let len = (ed[0] * ed[0] + ed[1] * ed[1]).sqrt(); - if len < 1e-6 { - continue; - } - let ew: [f64; 3] = std::array::from_fn(|q| (r[q][0] * ed[0] + r[q][1] * ed[1]) / len); - let third = [ew[1] * nb[2] - ew[2] * nb[1], ew[2] * nb[0] - ew[0] * nb[2], ew[0] * nb[1] - ew[1] * nb[0]]; - let rs: M3 = std::array::from_fn(|q| [ew[q], nb[q], third[q]]); - // Side centre in world: edge midpoint (world) plus half the depth backwards. - let mid = [((a[0] + bv[0]) * 0.5 - c0[0]) as f64, ((a[1] + bv[1]) * 0.5 - c0[1]) as f64]; - let mw: [f64; 3] = std::array::from_fn(|q| posb[q] + r[q][0] * mid[0] + r[q][1] * mid[1] + nb[q] * extrude * 0.5); - let (hl, hd) = ((len * 0.5) as f32, (extrude * 0.5) as f32); - let cs = [(a[0] + bv[0]) * 0.5, (a[1] + bv[1]) * 0.5]; - let quad = [[cs[0] - hl, cs[1] - hd], [cs[0] + hl, cs[1] - hd], [cs[0] + hl, cs[1] + hd], [cs[0] - hl, cs[1] + hd]]; - if let Some(mut sp) = Piece::new(&quad, cs, &rs, mw, cam) { - sp.light(&light, &rs, mw, eye); - sp.alpha = tex_opacity; - let colour = match (&side.img, side.mode) { - (Some(img), m) if m != 0 => { - let (c, _) = unpremul(img.sample_bilinear(cs[0] as f64, cs[1] as f64)); - if m == 2 { std::array::from_fn(|q| c[q] * tex_color[q]) } else { c } - } - _ => [tex_color[0], tex_color[1], tex_color[2]], - }; - sp.flat = Some(colour); - out.push(sp); - } - } - } - out.push(pc); - out.into_iter() - }) - .collect(); - sort_far_first(&mut pieces); - if view == 0 { - let tex = |i: &Option| i.clone().map(PieceTex::Image); - let front_t = tex(&front.img).unwrap_or(PieceTex::Layer); - let back_t = tex(&backf.img).unwrap_or_else(|| front_t.clone()); - return ShatterPlan::Pieces(PiecePlan { pieces, front: front_t, back: Some(back_t) }); - } - { - // Wireframes: piece outlines (front view: at rest; otherwise animated), plus forces. - let animated = view == 2 || view == 4; - let mut lines = Vec::new(); - for pc in &pieces { - let n = pc.n as usize; - let pts: Vec<[f32; 2]> = if animated { - let fw = inv3(&pc.inv); - match fw { - Some(m) => pc.poly[..n] - .iter() - .map(|v| { - let (a, bb) = ((v[0] - pc.c0[0]) as f64, (v[1] - pc.c0[1]) as f64); - let w = m[2][0] * a + m[2][1] * bb + m[2][2]; - [((m[0][0] * a + m[0][1] * bb + m[0][2]) / w) as f32, ((m[1][0] * a + m[1][1] * bb + m[1][2]) / w) as f32] - }) - .collect(), - None => continue, - } - } else { - pc.poly[..n].to_vec() - }; - for e in 0..n { - let (a, c) = (pts[e], pts[(e + 1) % n]); - lines.push(Sprite::new(a[0], a[1], 0.5, [0.75, 0.85, 1.0, 1.0], Shape::Line { dx: c[0] - a[0], dy: c[1] - a[1] })); - } - } - if view >= 3 { - for (fp, _, fr, _) in forces { - let (fx, fy) = b.to_px(fp); - let rad = fr * lw * s; - for k in 0..64 { - let (a0, a1) = (k as f64 / 64.0 * std::f64::consts::TAU, (k + 1) as f64 / 64.0 * std::f64::consts::TAU); - let (x0, y0) = (fx + rad * a0.cos(), fy + rad * a0.sin()); - let (x1, y1) = (fx + rad * a1.cos(), fy + rad * a1.sin()); - lines.push(Sprite::new(x0 as f32, y0 as f32, 0.5, [1.0, 0.3, 0.3, 1.0], Shape::Line { dx: (x1 - x0) as f32, dy: (y1 - y0) as f32 })); - } - } - } - ShatterPlan::Wire(SpritePlan { sprites: lines, tints: vec![], acc: Acc::Over, post: Post::Replace }) - } -} - -// ------------------------------------------------------------------ Caustics - -/// Sample with repeat mode: 0 Once (transparent outside), 1 Tiled, 2 Reflected. -fn sample_repeat(img: &Image, x: f64, y: f64, mode: u32) -> Px { - let (w, hh) = (img.width as f64, img.height as f64); - match mode { - 1 => img.sample_bilinear_clamped(x.rem_euclid(w), y.rem_euclid(hh)), - 2 => { - let m = |v: f64, n: f64| { - let r = v.rem_euclid(2.0 * n); - if r > n { 2.0 * n - r } else { r } - }; - img.sample_bilinear_clamped(m(x, w), m(y, hh)) - } - _ => img.sample_bilinear(x, y), - } -} - -/// Everything Caustics' per-pixel pass needs, prepared on the CPU (the other layers it reads -/// and the parameter conversions); the layer's own pixels are not read. -pub struct CausticsSetup { - /// The Bottom layer fitted to the buffer, `None` = the layer itself. - pub bottom: Option, - /// Gaussian σ (buffer px) blurring the bottom (transparent edges), 0 = none. - pub bottom_sigma: f64, - /// The water surface's height field (smoothed luminance), `None` = flat. - pub height: Option, - /// The Sky layer fitted to the buffer. - pub sky: Option, - pub scaling: f64, - pub repeat: u32, - pub wave_h: f64, - /// Displacement per unit of height gradient. - pub k: f64, - /// Layer centre (buffer px). - pub cx: f64, - pub cy: f64, - /// Layer width (buffer px). - pub lw: f64, - pub surf: [f32; 4], - pub surf_op: f32, - pub cstr: f32, - pub lc: [f32; 4], - pub li: f32, - /// Light position (buffer px) and height (layer widths); `point` = from the position to - /// each pixel, else one direction from above the layer centre. - pub lx: f64, - pub ly: f64, - pub lheight: f64, - pub point: bool, - pub ambient: f32, - pub diffuse: f32, - pub specular: f32, - pub sharp: f32, - pub sky_scaling: f64, - pub sky_repeat: u32, - pub sky_int: f32, - pub convergence: f64, -} - -/// Caustics' setup for layer geometry `b` (see [`CausticsSetup`]). -pub fn caustics_setup(ctx: &EffectCtx, b: &Buf) -> CausticsSetup { - let pr = ctx.params; - let s = b.scale; - let bottom = ctx.layer_param("bottom/bottom", true).map(|o| crate::util::fit_layer(ctx, b, &o, pr.e("bottom/bottomSizeDiffers") == 1)); - let blur = pr.f("bottom/blur") * s; - let wave_h = pr.f("water/waveHeight"); - let smoothing = pr.f("water/smoothing") * s; - let depth = pr.f("water/waterDepth"); - let ior = pr.f("water/refractiveIndex").max(1.0); - let lpos = pr.v2("lighting/lightPosition"); - // Height field of the water surface (luminance of the chosen layer; none = flat). - let height = ctx.layer_param("water/waterSurface", true).map(|o| { - let img = crate::util::fit_layer(ctx, b, &o, true); - let pl = Plane::luma(&img); - if smoothing > 0.05 { gauss_plane(&pl, smoothing * 0.5, smoothing * 0.5) } else { pl } - }); - let lw = ctx.layer_size[0] * s; - let k = wave_h * depth * (1.0 - 1.0 / ior) * lw * 2.0; - let (cx, cy) = (b.offset[0] + ctx.layer_size[0] * 0.5 * s, b.offset[1] + ctx.layer_size[1] * 0.5 * s); - let (lx, ly) = b.to_px(lpos); - let (lc, li, lheight) = (pr.color("lighting/lightColor"), pr.f("lighting/lightIntensity") as f32, pr.f("lighting/lightHeight").max(0.01)); - // Light Type: Distant Source (one direction, from the light position above the layer - // centre), Point Source (from the light position to each pixel) or First Comp Light. - let light_type = pr.e("lighting/lightType"); - let comp_light = if light_type == 2 { ctx.env.host.and_then(|h| h.comp_scene()).and_then(|sc| sc.light) } else { None }; - let (lc, li, lx, ly, lheight, point) = match (light_type, comp_light) { - (2, Some(l)) => { - let c = [l.color[0], l.color[1], l.color[2], 1.0]; - // Comp space: z away from the viewer; the light above the water is at −z. - let (px, py) = (l.pos[0] * s + b.offset[0], l.pos[1] * s + b.offset[1]); - (c, li, px, py, (-l.pos[2] / ctx.layer_size[0].max(1.0)).max(0.01), l.kind == 1) - } - (2, None) => (lc, 0.0, lx, ly, lheight, false), - (1, _) => (lc, li, lx, ly, lheight, true), - _ => (lc, li, lx, ly, lheight, false), - }; - // Sky: the layer the water surface reflects (Surface Opacity 1 = a mirror of it). - let sky = ctx.layer_param("sky/sky", true).map(|o| crate::util::fit_layer(ctx, b, &o, pr.e("sky/skySizeDiffers") == 1)); - CausticsSetup { - bottom, - bottom_sigma: if blur > 0.05 { blur * 0.5 } else { 0.0 }, - height, - sky, - scaling: pr.f("bottom/scaling").max(0.01), - repeat: pr.e("bottom/repeatMode"), - wave_h, - k, - cx, - cy, - lw, - surf: pr.color("water/surfaceColor"), - surf_op: pr.f("water/surfaceOpacity") as f32, - cstr: pr.f("water/causticsStrength") as f32, - lc, - li, - lx, - ly, - lheight, - point, - ambient: pr.f("lighting/ambientLight") as f32, - diffuse: pr.f("material/diffuse") as f32, - specular: pr.f("material/specular") as f32, - sharp: pr.f("material/highlightSharpness").max(1.0) as f32, - sky_scaling: pr.get("sky/scaling").map(Value::as_f64).unwrap_or(1.0).max(0.01), - sky_repeat: pr.e("sky/repeatMode"), - sky_int: pr.get("sky/intensity").map(Value::as_f64).unwrap_or(0.3) as f32, - convergence: pr.get("sky/convergence").map(Value::as_f64).unwrap_or(0.5).clamp(0.0, 1.0), - } -} - -fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf { - let st = caustics_setup(ctx, &b); - let mut bottom = st.bottom.clone().unwrap_or_else(|| b.img.clone()); - if st.bottom_sigma > 0.0 { - bottom = effectcraft_raster::gaussian_blur(&bottom, st.bottom_sigma, st.bottom_sigma, false); - } - let CausticsSetup { - scaling, - repeat, - wave_h, - k, - cx, - cy, - lw, - surf, - surf_op, - cstr, - lc, - li, - lx, - ly, - lheight, - point, - ambient, - diffuse, - specular, - sharp, - sky_scaling, - sky_repeat, - sky_int, - convergence, - .. - } = st; - let (height, sky) = (&st.height, &st.sky); - let (w, hh) = (b.img.width as usize, b.img.height as usize); - let norm3 = |v: [f64; 3]| { - let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-12); - [(v[0] / n) as f32, (v[1] / n) as f32, (v[2] / n) as f32] - }; - let lv_at = |x: f64, y: f64| { - if point { - norm3([(lx - x) / lw.max(1.0), (ly - y) / lw.max(1.0), lheight]) - } else { - norm3([(lx - cx) / lw.max(1.0), (ly - cy) / lw.max(1.0), lheight]) - } - }; - let grad = |x: usize, y: usize| -> (f64, f64) { - match height { - Some(pl) => { - let gx = (pl.get_clamped(x as i64 + 1, y as i64) - pl.get_clamped(x as i64 - 1, y as i64)) as f64 * 0.5; - let gy = (pl.get_clamped(x as i64, y as i64 + 1) - pl.get_clamped(x as i64, y as i64 - 1)) as f64 * 0.5; - (gx, gy) - } - None => (0.0, 0.0), - } - }; - // Displacement and its Jacobian (caustic focusing) per pixel. - let disp: Vec<(f64, f64)> = (0..w * hh) - .into_par_iter() - .map(|i| { - let (gx, gy) = grad(i % w, i / w); - (-gx * k, -gy * k) - }) - .collect(); - b.img.rows_mut().for_each(|(y, row)| { - for (x, px) in row.iter_mut().enumerate() { - let i = y * w + x; - let (dx, dy) = disp[i]; - let d = |xx: usize, yy: usize| disp[yy.min(hh - 1) * w + xx.min(w - 1)]; - let (dxr, _) = d(x + 1, y); - let (_, dyd) = d(x, y + 1); - let (dxl, _) = d(x.saturating_sub(1), y); - let (_, dyu) = d(x, y.saturating_sub(1)); - let jac = (1.0 + (dxr - dxl) * 0.5) * (1.0 + (dyd - dyu) * 0.5); - let focus = ((1.0 / jac.abs().max(0.2)) as f32 - 1.0).clamp(-1.0, 4.0); - let sx = cx + (x as f64 + 0.5 + dx - cx) / scaling; - let sy = cy + (y as f64 + 0.5 + dy - cy) / scaling; - let p = sample_repeat(&bottom, sx, sy, repeat); - let (c, a) = unpremul(p); - let (gx, gy) = grad(x, y); - let n = { - let v = [-gx * wave_h * 20.0, -gy * wave_h * 20.0, 1.0]; - let m = (v[0] * v[0] + v[1] * v[1] + 1.0).sqrt(); - [(v[0] / m) as f32, (v[1] / m) as f32, (v[2] / m) as f32] - }; - let lv = lv_at(x as f64 + 0.5, y as f64 + 0.5); - let nl = (n[0] * lv[0] + n[1] * lv[1] + n[2] * lv[2]).max(0.0); - let hv = { - let v = [lv[0], lv[1], lv[2] + 1.0]; - 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 spec = (n[0] * hv[0] + n[1] * hv[1] + n[2] * hv[2]).max(0.0).powf(sharp) * specular * li; - let light = ambient + diffuse * li * nl + cstr * focus * li; - // The surface: its colour lit like the water, or the sky it reflects (sampled along - // the surface normal; Convergence pulls the reflection towards the pixel itself). - let surface: [f32; 3] = match &sky { - Some(sk) => { - let reach = (1.0 - convergence) * lw * 0.5; - let (rx, ry) = (x as f64 + 0.5 + n[0] as f64 * reach, y as f64 + 0.5 + n[1] as f64 * reach); - let (rx, ry) = (cx + (rx - cx) / sky_scaling, cy + (ry - cy) / sky_scaling); - let (sc, _) = unpremul(sample_repeat(sk, rx, ry, sky_repeat)); - std::array::from_fn(|q| sc[q] * sky_int * 3.0 * surf[q]) - } - None => std::array::from_fn(|q| surf[q] * (ambient + diffuse * li * nl)), - }; - let cc: [f32; 3] = std::array::from_fn(|q| { - let base = c[q] * light * lc[q]; - base * (1.0 - surf_op) + surface[q] * surf_op + spec * lc[q] - }); - let a = (a + surf_op * (1.0 - a)).min(1.0); - *px = [cc[0] * a, cc[1] * a, cc[2] * a, a]; - } - }); - b -} - -// ------------------------------------------------------------------ Wave World - -/// Wave World's simulated surface: an `nx` × `ny` grid of heights `u` and velocities `v`. -#[derive(Clone, Debug)] -pub struct Waves { - pub nx: usize, - pub ny: usize, - pub u: Vec, - pub v: Vec, -} - -struct Producer { - ring: bool, - pos: [f32; 2], - len: f32, - width: f32, - angle: f32, - amp: f32, - freq: f32, - phase: f32, -} - -impl Producer { - /// Weight of a grid point (layer-fraction coordinates, x in widths, y in widths). - fn weight(&self, x: f32, y: f32) -> f32 { - let (dx, dy) = (x - self.pos[0], y - self.pos[1]); - let (sa, ca) = self.angle.sin_cos(); - let (u, v) = (dx * ca + dy * sa, -dx * sa + dy * ca); - let d = if self.ring { - let (a, bb) = ((self.len * 0.5).max(1e-3), (self.width * 0.5).max(1e-3)); - ((u / a).powi(2) + (v / bb).powi(2)).sqrt() - } else { - let l = (self.len * 0.5).max(1e-3); - let wd = (self.width * 0.5).max(1e-3); - (u.abs() / l).max(v.abs() / wd) - }; - (1.0 - d).clamp(0.0, 1.0).min(1.0) - } -} - -static WAVE_CACHE: SimCache = SimCache::new(4); - -/// What Wave World draws: the Height Map view of the simulated surface (grid heights and, -/// with a ground layer, water depths per grid point), or the wireframe preview's lines. -pub enum WavePlan { - Height { st: std::sync::Arc, depth: Option> }, - Wire(crate::sim::SpritePlan), -} - -fn wave_world(ctx: &EffectCtx, b: Buf) -> Buf { - match wave_world_impl(ctx, &b, false) { - Ok(img) => Buf { img, ..b }, - Err(WavePlan::Wire(plan)) => plan.finish(b), - Err(WavePlan::Height { .. }) => b, - } -} - -/// Wave World's simulation state for the frame, as a [`WavePlan`] (its pixels are not read). -pub fn wave_world_plan(ctx: &EffectCtx, b: &Buf) -> Option { - wave_world_impl(ctx, b, true).err() -} - -fn wave_world_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result { - 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 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"); - let preroll = pr.f("simulation/preRoll").max(0.0); - let prods: Vec = [1, 2] - .iter() - .map(|k| { - let pos = pr.v2(&format!("producer{k}/producer{k}Position")); - Producer { - ring: pr.e(&format!("producer{k}/producer{k}Type")) == 0, - pos: [(pos[0] / lw.max(1.0)) as f32, (pos[1] / lw.max(1.0)) as f32], - len: pr.f(&format!("producer{k}/producer{k}Length")) as f32, - width: pr.f(&format!("producer{k}/producer{k}Width")) as f32, - angle: (pr.f(&format!("producer{k}/producer{k}Angle")) as f32).to_radians(), - amp: pr.f(&format!("producer{k}/producer{k}Amplitude")) as f32, - freq: pr.f(&format!("producer{k}/producer{k}Frequency")) as f32, - phase: (pr.f(&format!("producer{k}/producer{k}Phase")) as f32).to_radians(), - } - }) - .collect(); - let sps = crate::sim::SPS as f32; - // Wave speed in layer widths per second → cells per second; sub-steps keep it stable. - let cps = speed * (nx - 1) as f32; - let sub = ((cps * 2.0 / sps).ceil() as usize).clamp(1, 64); - let dt = 1.0 / (sps * sub as f32); - let cell = 1.0 / (nx - 1) as f32; - let weights: Vec> = prods.iter().map(|p| (0..nx * ny).map(|i| p.weight((i % nx) as f32 * cell, (i / nx) as f32 * cell)).collect()).collect(); - // Ground: the ground layer's first frame, its luminance an elevation (× Steepness). Where - // it rises above Height the grid is dry (a wall the waves reflect from); shallow water - // slows the waves by Wave Strength. - let ground = pr.get("ground/ground").and_then(Value::as_layer).and_then(|id| { - let host = ctx.env.host?; - host.layer_at(id, ctx.env.comp_time - ctx.time, true) - }); - let steep = pr.get("ground/steepness").map(Value::as_f64).unwrap_or(0.25) as f32; - let level = pr.get("ground/height").map(Value::as_f64).unwrap_or(0.25) as f32; - let wave_strength = pr.f("ground/waveStrength").clamp(0.0, 1.0) as f32; - // Water depth per grid point (None without ground: deep everywhere). - let depth: Option> = ground.as_ref().map(|lp| { - let (gw, gh) = (lp.size[0].max(1.0), lp.size[1].max(1.0)); - (0..nx * ny) - .map(|i| { - let (fx, fy) = ((i % nx) as f64 / (nx - 1) as f64, (i / nx) as f64 / (ny - 1) as f64); - let (c, a) = unpremul(lp.buf.img.sample_bilinear_clamped(fx * gw * lp.buf.scale + lp.buf.offset[0], fy * gh * lp.buf.scale + lp.buf.offset[1])); - level - steep * effectcraft_color::luminance(c[0], c[1], c[2]) * a - }) - .collect() - }); - let speed_k: Vec = match &depth { - Some(d) => d.iter().map(|&v| if v <= 0.0 { 0.0 } else { 1.0 - wave_strength * (1.0 - (v / level.max(1e-3)).clamp(0.0, 1.0).sqrt()) }).collect(), - None => vec![1.0; nx * ny], - }; - let mut key = - params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: if pr.b("simulation/gridResDownsamples") { b.scale } else { 1.0 } }, 4); - if let Some(d) = &depth { - key ^= d.iter().fold(0u64, |a, v| a.rotate_left(3) ^ v.to_bits() as u64); - } - let steps = crate::sim::steps_at(ctx.time + preroll); - let st = WAVE_CACHE.run( - key, - steps, - || Waves { nx, ny, u: vec![0.0; nx * ny], v: vec![0.0; nx * ny] }, - |w, step| { - for k in 0..sub { - let t = (step as f32 + k as f32 / sub as f32) / sps - preroll as f32; - let c2 = cps * cps; - let mut acc = vec![0.0f32; nx * ny]; - for y in 0..ny { - for x in 0..nx { - let i = y * nx + x; - let g = |xx: usize, yy: usize| w.u[yy * nx + xx]; - let l = g(x.saturating_sub(1), y) + g((x + 1).min(nx - 1), y) + g(x, y.saturating_sub(1)) + g(x, (y + 1).min(ny - 1)) - 4.0 * w.u[i]; - acc[i] = c2 * speed_k[i] * speed_k[i] * l - damping * 4.0 * w.v[i]; - } - } - for i in 0..nx * ny { - w.v[i] += acc[i] * dt; - w.u[i] += w.v[i] * dt; - } - // Producers drive the surface. - for (p, wt) in prods.iter().zip(&weights) { - if p.amp == 0.0 { - continue; - } - let target = p.amp * (std::f32::consts::TAU * p.freq * t + p.phase).sin(); - for i in 0..nx * ny { - if wt[i] > 0.0 { - w.u[i] += (target - w.u[i]) * wt[i]; - w.v[i] *= 1.0 - wt[i]; - } - } - } - // Dry ground holds the surface flat. - for i in 0..nx * ny { - if speed_k[i] <= 0.0 { - w.u[i] = 0.0; - w.v[i] = 0.0; - } - } - // Edges: reflective ones mirror; the others absorb in a thin sponge layer. - let refl = |side: u32| reflect == 5 || reflect == side; - let sponge = 4usize.min(nx / 4).max(1); - for y in 0..ny { - for x in 0..nx { - let i = y * nx + x; - let mut f = 1.0f32; - if !refl(1) && x < sponge { - f = f.min(x as f32 / sponge as f32); - } - if !refl(3) && nx - 1 - x < sponge { - f = f.min((nx - 1 - x) as f32 / sponge as f32); - } - if !refl(2) && y < sponge { - f = f.min(y as f32 / sponge as f32); - } - if !refl(4) && ny - 1 - y < sponge { - f = f.min((ny - 1 - y) as f32 / sponge as f32); - } - if f < 1.0 { - let k = 0.85 + 0.15 * f; - w.u[i] *= k; - w.v[i] *= k; - } - } - } - } - }, - ); - let s = b.scale; - let (bw, bh) = (b.img.width, b.img.height); - let sample = |x: f64, y: f64| -> f32 { - let gx = (x / lw.max(1.0) * (st.nx - 1) as f64).clamp(0.0, (st.nx - 1) as f64); - let gy = (y / lw.max(1.0) * (st.nx - 1) as f64).clamp(0.0, (st.ny - 1) as f64); - let (x0, y0) = (gx.floor() as usize, gy.floor() as usize); - let (x1, y1) = ((x0 + 1).min(st.nx - 1), (y0 + 1).min(st.ny - 1)); - let (tx, ty) = ((gx - x0 as f64) as f32, (gy - y0 as f64) as f32); - let g = |xx: usize, yy: usize| st.u[yy * st.nx + xx]; - let a = g(x0, y0) + (g(x1, y0) - g(x0, y0)) * tx; - let c = g(x0, y1) + (g(x1, y1) - g(x0, y1)) * tx; - a + (c - a) * ty - }; - if plan_only && pr.e("view") == 1 { - return Err(WavePlan::Height { st, depth }); - } - if pr.e("view") == 1 { - let mut img = Image::new(bw, bh); - let bright = pr.f("heightMapControls/brightness") as f32; - let contrast = pr.f("heightMapControls/contrast") as f32; - let gamma = pr.f("heightMapControls/gamma").max(0.01) as f32; - let alpha = 1.0 - pr.f("heightMapControls/transparency") as f32; - let dry_transparent = pr.e("heightMapControls/renderDryAreasAs") == 1; - let dry_at = |x: f64, y: f64| -> bool { - let Some(d) = &depth else { return false }; - let gx = (x / lw.max(1.0) * (st.nx - 1) as f64).round().clamp(0.0, (st.nx - 1) as f64) as usize; - let gy = (y / lw.max(1.0) * (st.nx - 1) as f64).round().clamp(0.0, (st.ny - 1) as f64) as usize; - d[gy * st.nx + gx] <= 0.0 - }; - img.rows_mut().for_each(|(y, row)| { - for (x, px) in row.iter_mut().enumerate() { - let lx = (x as f64 + 0.5 - b.offset[0]) / s; - let ly = (y as f64 + 0.5 - b.offset[1]) / s; - let v = (bright + contrast * sample(lx, ly)).clamp(0.0, 1.0).powf(1.0 / gamma); - let al = if dry_transparent && dry_at(lx, ly) { 0.0 } else { alpha }; - *px = [v * al, v * al, v * al, al]; - } - }); - Ok(img) - } else { - // Wireframe preview: the grid turned by Horizontal Rotation about the vertical axis, - // tilted by Vertical Rotation, heights scaled by Vertical Scale. - let hr = pr.f("wireframeControls/horizontalRotation").to_radians(); - let vr = pr.get("wireframeControls/verticalRotation").map(Value::as_f64).unwrap_or(53.0).to_radians(); - let vs = pr.get("wireframeControls/verticalScale").map(Value::as_f64).unwrap_or(0.5); - let (sh, ch) = hr.sin_cos(); - let (sv, cv) = vr.sin_cos(); - let to = |x: usize, y: usize| -> [f32; 2] { - // Grid point relative to the layer centre (layer px), height in layer px. - let gx = x as f64 / (st.nx - 1) as f64 * lw - lw * 0.5; - let gy = y as f64 / (st.nx - 1) as f64 * lw - (st.ny - 1) as f64 / (st.nx - 1) as f64 * lw * 0.5; - let hgt = st.u[y * st.nx + x] as f64 * vs * lh * 0.5; - let (rx, ry) = (gx * ch - gy * sh, gx * sh + gy * ch); - let (px, py) = b.to_px([lw * 0.5 + rx, lh * 0.5 + ry * cv - hgt * sv]); - [px as f32, py as f32] - }; - let mut lines = Vec::new(); - for y in 0..st.ny { - for x in 0..st.nx { - let a = to(x, y); - if x + 1 < st.nx { - let c = to(x + 1, y); - lines.push(Sprite::new(a[0], a[1], 0.4, [0.4, 0.9, 0.4, 1.0], Shape::Line { dx: c[0] - a[0], dy: c[1] - a[1] })); - } - if y + 1 < st.ny { - let c = to(x, y + 1); - lines.push(Sprite::new(a[0], a[1], 0.4, [0.4, 0.9, 0.4, 1.0], Shape::Line { dx: c[0] - a[0], dy: c[1] - a[1] })); - } - } - } - let plan = crate::sim::SpritePlan { sprites: lines, tints: vec![], acc: Acc::Over, post: crate::sim::Post::OverBlack }; - if plan_only { - return Err(WavePlan::Wire(plan)); - } - Ok(plan.finish(Buf { img: Image::new(bw, bh), offset: b.offset, scale: b.scale }).img) - } -} - -// ------------------------------------------------------------------ Foam - -#[derive(Clone, Debug)] -struct FoamBubble { - p: [f32; 2], - v: [f32; 2], - age: f32, - size: f32, - /// Spin (radians) for Physical Orientation. - spin: f32, - id: u32, -} - -#[derive(Clone, Debug, Default)] -struct FoamState { - b: Vec, - carry: f32, - next: u32, -} - -static FOAM_CACHE: SimCache = SimCache::new(4); - -/// Foam steps once per frame at 30 frames per second. -const FOAM_FPS: f64 = 30.0; - -/// Foam's Bubble Texture options (the last uses Bubble Texture Layer). -const FOAM_TEXTURES: [&str; 6] = ["Default Bubble", "Spit", "Bubblegum", "Dishwater", "Milky", "User Defined"]; - -/// Age (in simulation steps) at which bubble `id` pops: its lifespan, shortened at random for -/// frail bubbles (`frailty` 0 = every bubble reaches `lifespan`, 1 = uniformly early). -fn foam_pop_age(id: u32, lifespan: f32, frailty: f32, seed: u32) -> f32 { - lifespan * (1.0 - frailty * (hs(id, 11, seed) * 0.5 + 0.5)) -} - -/// Foam: bubbles born in the producer rectangle grow to their size, drift with the wind, -/// turbulence and the Flow Map (bubbles run downhill on its luminance), repel and stick to each -/// other, wobble, and pop at the end of their (Strength-shortened) lifespan, pushing their -/// neighbours away at Pop Velocity. Rendered bubbles use a built-in texture or the Bubble -/// Texture Layer, oriented by Bubble Orientation, and reflect the Environment Map. -fn foam(ctx: &EffectCtx, b: Buf) -> Buf { - foam_full_plan(ctx, &b).finish(b) -} - -/// Foam's bubbles as sprites, `None` when it draws more than sprites (a User Defined bubble -/// texture, an Environment Map reflection or the Draft + Flow Map view's flow map). -pub(crate) fn foam_plan(ctx: &EffectCtx, b: &Buf) -> Option { - let plan = foam_full_plan(ctx, b); - (plan.user.is_none() && plan.env.is_none() && plan.flow.is_none()).then_some(plan.sprites) -} - -/// A bubble disc filled by Foam's User Defined texture (buffer px; `rot` radians). -#[derive(Clone, Copy, Debug)] -pub struct FoamDisc { - pub x: f64, - pub y: f64, - pub r: f32, - pub rot: f32, - pub fade: f32, -} - -/// Foam's Environment Map reflection: the map fitted to the buffer, Reflection Convergence and -/// Strength, and the bubble discs reflecting it (`rot` and `fade` unused). -pub struct FoamEnv { - pub img: Image, - pub conv: f64, - pub strength: f32, - pub discs: Vec, -} - -/// What Foam draws this frame over a transparent frame of the layer's size: the bubble sprites, -/// then the User Defined texture's discs, the Environment Map's reflections and (Draft + Flow -/// Map) the flow map under it all, each `foam_disc`-composited in bubble order. -pub struct FoamPlan { - pub sprites: crate::sim::SpritePlan, - pub user: Option<(crate::LayerPixels, Vec)>, - pub env: Option, - /// The flow map fitted to the buffer. - pub flow: Option, -} - -impl FoamPlan { - pub fn finish(&self, b: Buf) -> Buf { - let mut out = splat(b.img.width, b.img.height, &self.sprites.sprites, self.sprites.acc); - // User Defined: the texture layer fills each bubble's disc. - if let Some((tex, discs)) = &self.user { - for d in discs { - let (rot, fade) = (d.rot, d.fade); - foam_disc(&mut out, d.x, d.y, d.r as f64, |u, v| { - // u, v in −1..1 across the bubble, rotated into the texture. - let (sr, cr) = rot.sin_cos(); - let (tu, tv) = (u as f32 * cr + v as f32 * sr, -u as f32 * sr + v as f32 * cr); - let (x, y) = ((tu as f64 * 0.5 + 0.5) * tex.size[0], (tv as f64 * 0.5 + 0.5) * tex.size[1]); - let px = tex.buf.img.sample_bilinear(x * tex.buf.scale + tex.buf.offset[0], y * tex.buf.scale + tex.buf.offset[1]); - px.map(|c| c * fade) - }); - } - } - // Environment Map: each bubble reflects the map along its sphere normal (Reflection - // Convergence pulls the reflection towards the bubble's own spot of the map). - if let Some(env) = &self.env { - let (conv, strength) = (env.conv, env.strength); - let (w, h) = (b.img.width as f64, b.img.height as f64); - for d in &env.discs { - let (bx, by) = (d.x, d.y); - foam_disc(&mut out, bx, by, d.r as f64, |u, v| { - // Sphere normal; the reflection looks across the whole map, or (convergence 1) - // just behind the bubble. - let (ex, ey) = - if conv >= 1.0 { (bx, by) } else { (bx * conv + (u * 0.5 + 0.5) * w * (1.0 - conv), by * conv + (v * 0.5 + 0.5) * h * (1.0 - conv)) }; - let e = env.img.sample_bilinear(ex, ey); - let rim = ((u * u + v * v) as f32).min(1.0); - let k = strength * (0.3 + 0.7 * rim); - [e[0] * k, e[1] * k, e[2] * k, 0.0] - }); - } - } - // Draft + Flow Map shows the flow map under the bubbles. - if let Some(fm) = &self.flow { - out.data.par_iter_mut().zip(fm.data.par_iter()).for_each(|(o, f)| { - let k = 1.0 - o[3]; - for c in 0..4 { - o[c] += f[c] * 0.5 * k; - } - }); - } - Buf { img: out, ..b } - } -} - -/// Foam's [`FoamPlan`] for layer geometry `b` (its size; its pixels are not read). -pub fn foam_full_plan(ctx: &EffectCtx, b: &Buf) -> FoamPlan { - let pr = ctx.params; - let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32); - let unit = lw * 0.01; - let prod = pr.v2("producer/producerPoint"); - let (psx, psy) = (pr.f("producer/producerXSize") as f32 * lw, pr.f("producer/producerYSize") as f32 * lh); - let porient = (pr.f("producer/producerOrientation") as f32).to_radians(); - let rate = pr.f("producer/productionRate").max(0.0) as f32; - let size = pr.f("bubbles/size").max(0.0) as f32; - let size_var = pr.f("bubbles/sizeVariance") as f32; - let lifespan = pr.f("bubbles/lifespan").max(1.0) as f32; - let growth = pr.f("bubbles/growthSpeed").max(0.0001) as f32; - // Strength: weaker bubbles tend to pop before their lifespan (at 20 or more every bubble - // lives the full lifespan); each bubble gets its own pop age. - let frailty = 1.0 - (pr.f("bubbles/strength") as f32 / 20.0).clamp(0.0, 1.0); - let init_speed = pr.f("physics/initialSpeed") as f32; - let init_dir = (pr.f("physics/initialDirection") as f32).to_radians(); - let wind = pr.f("physics/windSpeed") as f32; - let wind_dir = (pr.f("physics/windDirection") as f32).to_radians(); - let turb = pr.f("physics/turbulence") as f32; - let wobble = pr.f("physics/wobbleAmount").max(0.0) as f32; - let repulsion = pr.f("physics/repulsion") as f32; - let pop_velocity = pr.f("physics/popVelocity") as f32; - let viscosity = pr.f("physics/viscosity").clamp(0.0, 1.0) as f32; - let sticky = pr.f("physics/stickiness").clamp(0.0, 1.0) as f32; - let universe = pr.f("universeSize").max(0.01) as f32; - let seed = (pr.f("randomSeed") as u32).wrapping_mul(0x68e31da4) ^ ctx.seed; - let wvec = [wind_dir.sin() * wind * unit, -wind_dir.cos() * wind * unit]; - // Flow Map: the map layer's first frame, its luminance a height field (Fits the layer or - // the whole universe). - let flow = pr.get("flowMap/flowMap").and_then(Value::as_layer).and_then(|id| { - let host = ctx.env.host?; - let me = pr.get(&crate::layer_source_id("flowMap/flowMap")).is_none_or(|v| v.as_enum() != 0); - host.layer_at(id, ctx.env.comp_time - ctx.time, me) - }); - let steep = pr.f("flowMap/flowMapSteepness") as f32; - let fits_universe = pr.e("flowMap/flowMapFits") == 1; - let substeps = match pr.e("flowMap/simulationQuality") { - 1 => 2, - 2 => 4, - _ => 1, - }; - let height = flow.as_ref().map(|lp| { - let img = &lp.buf.img; - let pl = Plane::from_image(img, |px| { - let (c, a) = unpremul(px); - effectcraft_color::luminance(c[0], c[1], c[2]) * a - }); - (pl, lp.buf.scale, lp.buf.offset, lp.size) - }); - let (ux0, uy0) = (-(universe - 1.0) * 0.5 * lw, -(universe - 1.0) * 0.5 * lh); - let (ux1, uy1) = (lw - ux0, lh - uy0); - // Height at a layer point (0..1). - let h_at = |x: f32, y: f32| -> f32 { - let Some((pl, s, off, size)) = &height else { return 0.0 }; - let (fx, fy) = if fits_universe { ((x - ux0) / (ux1 - ux0), (y - uy0) / (uy1 - uy0)) } else { (x / lw, y / lh) }; - pl.sample(fx as f64 * size[0] * s + off[0], fy as f64 * size[1] * s + off[1]) - }; - let mut key = params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 5); - if let Some((pl, ..)) = &height { - key ^= pl.data.iter().step_by(5).fold(0u64, |a, v| a.rotate_left(3) ^ v.to_bits() as u64); - } - let steps = if ctx.time <= 0.0 { 0 } else { (ctx.time * FOAM_FPS + 1e-6).floor() as u64 }; - let rad = |q: &FoamBubble| q.size * unit * 3.0 * (q.age * growth).min(1.0); - let st = FOAM_CACHE.run(key, steps, FoamState::default, |st, step| { - let t = step as f32 / FOAM_FPS as f32; - // Births. - st.carry += rate; - let n = st.carry.floor() as u32; - st.carry -= n as f32; - for _ in 0..n { - if st.b.len() >= 3000 { - break; - } - let id = st.next; - st.next = st.next.wrapping_add(1); - let (ux, uy) = (hs(id, 1, seed) * 0.5 * psx, hs(id, 2, seed) * 0.5 * psy); - let (so, co) = porient.sin_cos(); - let p = [prod[0] as f32 + ux * co - uy * so, prod[1] as f32 + ux * so + uy * co]; - let v = [init_dir.sin() * init_speed * unit, -init_dir.cos() * init_speed * unit]; - let sz = (size * (1.0 + size_var * hs(id, 3, seed))).max(0.01); - st.b.push(FoamBubble { p, v, age: 0.0, size: sz, spin: hs(id, 12, seed) * std::f32::consts::PI, id }); - } - for _ in 0..substeps { - let k = 1.0 / substeps as f32; - // Forces: drift towards the wind, turbulence, viscosity, the flow map's slope. - for q in st.b.iter_mut() { - let n1 = value_noise(q.p[0] / (lw * 0.15), q.p[1] / (lw * 0.15), t * 0.5, seed) - 0.5; - let n2 = value_noise(q.p[0] / (lw * 0.15) + 19.0, q.p[1] / (lw * 0.15), t * 0.5, seed) - 0.5; - for c in 0..2 { - q.v[c] += (wvec[c] - q.v[c]) * 0.1 * k; - q.v[c] *= 1.0 - viscosity * 0.5 * k; - } - q.v[0] += n1 * turb * unit * k; - q.v[1] += n2 * turb * unit * k; - if height.is_some() && steep != 0.0 { - let e = unit.max(1.0); - let gx = (h_at(q.p[0] + e, q.p[1]) - h_at(q.p[0] - e, q.p[1])) / (2.0 * e); - let gy = (h_at(q.p[0], q.p[1] + e) - h_at(q.p[0], q.p[1] - e)) / (2.0 * e); - // Downhill: from light (high) to dark (low). - q.v[0] -= gx * steep * lw * 0.5 * k; - q.v[1] -= gy * steep * lw * 0.5 * k; - } - // Physical Orientation: bubbles roll with their sideways motion. - q.spin += q.v[0] / (rad(q).max(0.5)) * k; - } - // Repulsion between overlapping bubbles (grid hashed, deterministic order). - if repulsion > 0.0 && st.b.len() > 1 { - let maxr = st.b.iter().map(rad).fold(0.0f32, f32::max).max(1.0); - let cs = maxr * 2.0; - let mut grid: std::collections::HashMap<(i32, i32), Vec> = std::collections::HashMap::new(); - for (i, q) in st.b.iter().enumerate() { - grid.entry(((q.p[0] / cs).floor() as i32, (q.p[1] / cs).floor() as i32)).or_default().push(i); - } - let mut push = vec![[0.0f32; 2]; st.b.len()]; - for (i, q) in st.b.iter().enumerate() { - let (gx, gy) = ((q.p[0] / cs).floor() as i32, (q.p[1] / cs).floor() as i32); - for dy in -1..=1 { - for dx in -1..=1 { - let Some(list) = grid.get(&(gx + dx, gy + dy)) else { continue }; - for &j in list { - if j <= i { - continue; - } - let o = &st.b[j]; - let (ddx, ddy) = (o.p[0] - q.p[0], o.p[1] - q.p[1]); - let d = (ddx * ddx + ddy * ddy).sqrt().max(1e-4); - let overlap = rad(q) + rad(o) - d; - if overlap > 0.0 { - let f = overlap * 0.5 * repulsion.min(4.0) * (1.0 - sticky * 0.5) / d * k; - push[i][0] -= ddx * f; - push[i][1] -= ddy * f; - push[j][0] += ddx * f; - push[j][1] += ddy * f; - } - } - } - } - } - for (q, d) in st.b.iter_mut().zip(&push) { - q.p[0] += d[0].clamp(-maxr, maxr); - q.p[1] += d[1].clamp(-maxr, maxr); - } - } - for q in st.b.iter_mut() { - q.p[0] += q.v[0] * k; - q.p[1] += q.v[1] * k; - } - } - for q in st.b.iter_mut() { - q.age += 1.0; - } - // Pops: bubbles past their pop age burst, pushing their neighbours away. - let popped: Vec<([f32; 2], f32)> = st.b.iter().filter(|q| q.age >= foam_pop_age(q.id, lifespan, frailty, seed)).map(|q| (q.p, rad(q))).collect(); - if pop_velocity != 0.0 { - for (pp, pr_) in &popped { - let reach = (pr_ * 3.0).max(unit); - for q in st.b.iter_mut() { - let (dx, dy) = (q.p[0] - pp[0], q.p[1] - pp[1]); - let d = (dx * dx + dy * dy).sqrt(); - if d > 1e-4 && d < reach { - let f = pop_velocity * unit * (1.0 - d / reach) / d; - q.v[0] += dx * f; - q.v[1] += dy * f; - } - } - } - } - st.b.retain(|q| q.age < foam_pop_age(q.id, lifespan, frailty, seed) && q.p[0] > ux0 && q.p[0] < ux1 && q.p[1] > uy0 && q.p[1] < uy1); - }); - let view = pr.e("view"); - let zoom = pr.f("zoom").max(0.01) as f32; - let zoom_producer = pr.b("producer/zoomProducerPoint"); - let texture = pr.e("rendering/bubbleTexture"); - let orient = pr.e("rendering/bubbleOrientation"); - let blend = pr.e("rendering/blendMode"); - let s = b.scale as f32; - // Zoom centres on the layer, or on the producer with Zoom Producer Point. - let (cx, cy) = if zoom_producer { (prod[0] as f32, prod[1] as f32) } else { (lw * 0.5, lh * 0.5) }; - let mut order: Vec<&FoamBubble> = st.b.iter().collect(); - if blend == 2 { - // Solid New on Top: oldest first. - order.sort_by(|a, c| c.age.total_cmp(&a.age).then(a.id.cmp(&c.id))); - } else if blend == 1 { - order.sort_by(|a, c| a.age.total_cmp(&c.age).then(a.id.cmp(&c.id))); - } - // Where a bubble is drawn: buffer centre, radius, rotation (radians). - let place = |q: &FoamBubble| { - let wob = if wobble > 0.0 { 1.0 + wobble * 0.15 * ((q.age * 0.35) + hs(q.id, 13, seed) * 6.0).sin() } else { 1.0 }; - let r = rad(q) * wob * zoom * s; - let (bx, by) = b.to_px([(cx + (q.p[0] - cx) * zoom) as f64, (cy + (q.p[1] - cy) * zoom) as f64]); - let rot = match orient { - 1 => q.spin, - 2 => q.v[1].atan2(q.v[0]), - _ => 0.0, - }; - (bx, by, r, rot) - }; - let user_tex = (view == 2 && texture == 5).then(|| ctx.layer_param("rendering/bubbleTextureLayer", true)).flatten(); - let sprites: Vec = order - .iter() - .filter(|_| user_tex.is_none()) - .map(|q| { - let (bx, by, r, rot) = place(q); - let fade = ((foam_pop_age(q.id, lifespan, frailty, seed) - q.age) / 5.0).clamp(0.0, 1.0); - // Draft and Draft + Flow Map show the wireframe-style bubbles. - let mut sp = if view != 2 { - Sprite::new(bx as f32, by as f32, r, [0.55, 0.75, 1.0, fade], Shape::Bubble) - } else { - let (c, shape) = match texture { - 1 => ([0.95, 0.95, 0.92], Shape::Faded), - 2 => ([1.0, 0.6, 0.8], Shape::Bubble), - 3 => ([0.8, 0.85, 0.75], Shape::Bubble), - 4 => ([0.97, 0.97, 0.95], Shape::Sphere), - _ => ([1.0, 1.0, 1.0], Shape::Bubble), - }; - Sprite::new(bx as f32, by as f32, r, [c[0], c[1], c[2], fade], shape) - }; - sp.rot = rot; - sp - }) - .collect(); - let acc = if blend == 0 && view == 2 { Acc::Add } else { Acc::Over }; - let disc = |q: &&FoamBubble| { - let (x, y, r, rot) = place(q); - let fade = ((foam_pop_age(q.id, lifespan, frailty, seed) - q.age) / 5.0).clamp(0.0, 1.0); - FoamDisc { x, y, r, rot, fade } - }; - let user = user_tex.map(|tex| (tex, order.iter().map(disc).collect())); - let strength = pr.f("rendering/reflectionStrength").clamp(0.0, 1.0) as f32; - let env = if view == 2 - && strength > 0.0 - && let Some(env) = ctx.layer_param("rendering/environmentMap", true) - { - Some(FoamEnv { - img: crate::util::fit_layer(ctx, b, &env, true), - conv: pr.f("rendering/reflectionConvergence").clamp(0.0, 1.0), - strength, - discs: order.iter().map(disc).collect(), - }) - } else { - None - }; - let flow = if view == 1 - && let Some(lp) = &flow - { - Some(crate::util::fit_layer(ctx, b, lp, true)) - } else { - None - }; - FoamPlan { sprites: crate::sim::SpritePlan { sprites, tints: vec![], acc, post: crate::sim::Post::Replace }, user, env, flow } -} - -/// Add `f(u, v)` (premultiplied; u, v ∈ −1..1 across the disc) over the disc of radius `r` at -/// (`x`, `y`) with anti-aliased edges ("over" for opaque texels, additive for alpha 0). -fn foam_disc(out: &mut Image, x: f64, y: f64, r: f64, f: impl Fn(f64, f64) -> Px) { - if r <= 0.0 { - return; - } - let (x0, x1) = ((x - r - 1.0).floor().max(0.0) as i64, (x + r + 1.0).ceil().min(out.width as f64) as i64); - let (y0, y1) = ((y - r - 1.0).floor().max(0.0) as i64, (y + r + 1.0).ceil().min(out.height as f64) as i64); - for py in y0..y1 { - for px in x0..x1 { - let (u, v) = ((px as f64 + 0.5 - x) / r, (py as f64 + 0.5 - y) / r); - let d = (u * u + v * v).sqrt(); - let cov = ((1.0 - d) * r + 0.5).clamp(0.0, 1.0) as f32; - if cov <= 0.0 { - continue; - } - let s = f(u.clamp(-1.0, 1.0), v.clamp(-1.0, 1.0)).map(|c| c * cov); - let o = out.get(px, py); - let k = 1.0 - s[3]; - out.set(px as u32, py as u32, [s[0] + o[0] * k, s[1] + o[1] * k, s[2] + o[2] * k, (s[3] + o[3] * k).min(1.0)]); - } - } -} - -// ------------------------------------------------------------------ specs - -fn producer_params(k: u32, amp: f64, pos: (f64, f64)) -> Vec { - let ids: [&'static str; 8] = match k { - 1 => [ - "producer1/producer1Type", - "producer1/producer1Position", - "producer1/producer1Length", - "producer1/producer1Width", - "producer1/producer1Angle", - "producer1/producer1Amplitude", - "producer1/producer1Frequency", - "producer1/producer1Phase", - ], - _ => [ - "producer2/producer2Type", - "producer2/producer2Position", - "producer2/producer2Length", - "producer2/producer2Width", - "producer2/producer2Angle", - "producer2/producer2Amplitude", - "producer2/producer2Frequency", - "producer2/producer2Phase", - ], - }; - vec![ - p(ids[0], "Type", Value::Enum(0), popup(&["Ring", "Line"])), - p(ids[1], "Position", pt(pos.0, pos.1), ParamUi::Point), - p(ids[2], "Height/Length", num(0.1), slider(0.0, 2.0, 0.0, 1.0, 3)), - p(ids[3], "Width", num(0.1), slider(0.0, 2.0, 0.0, 1.0, 3)), - p(ids[4], "Angle", num(0.0), ParamUi::Angle), - p(ids[5], "Amplitude", num(amp), slider(-5.0, 5.0, -1.0, 1.0, 3)), - p(ids[6], "Frequency", num(1.0), slider(0.0, 30.0, 0.0, 5.0, 3)), - p(ids[7], "Phase", num(0.0), ParamUi::Angle), - ] -} - -fn card_dance_params() -> Vec { - let mut v = vec![ - p("rowsColumns", "Rows & Columns", Value::Enum(0), popup(&["Independent", "Columns Follows Rows"])), - p("rows", "Rows", num(10.0), slider(1.0, 1000.0, 1.0, 100.0, 0)), - p("columns", "Columns", num(10.0), slider(1.0, 1000.0, 1.0, 100.0, 0)), - p("backLayer", "Back Layer", Value::Layer(None), ParamUi::Layer), - p("gradientLayer1", "Gradient Layer 1", Value::Layer(None), ParamUi::Layer), - p("gradientLayer2", "Gradient Layer 2", Value::Layer(None), ParamUi::Layer), - ]; - let names: [[&'static str; 4]; 8] = [ - ["xPosition/xPosSource", "xPosition/xPosMultiplier", "xPosition/xPosOffset", "X Position"], - ["yPosition/yPosSource", "yPosition/yPosMultiplier", "yPosition/yPosOffset", "Y Position"], - ["zPosition/zPosSource", "zPosition/zPosMultiplier", "zPosition/zPosOffset", "Z Position"], - ["xRotation/xRotSource", "xRotation/xRotMultiplier", "xRotation/xRotOffset", "X Rotation"], - ["yRotation/yRotSource", "yRotation/yRotMultiplier", "yRotation/yRotOffset", "Y Rotation"], - ["zRotation/zRotSource", "zRotation/zRotMultiplier", "zRotation/zRotOffset", "Z Rotation"], - ["xScale/xScaleSource", "xScale/xScaleMultiplier", "xScale/xScaleOffset", "X Scale"], - ["yScale/yScaleSource", "yScale/yScaleMultiplier", "yScale/yScaleOffset", "Y Scale"], - ]; - debug_assert_eq!(names.len(), CD_PROPS.len()); - for (i, n) in names.iter().enumerate() { - let rot = (3..6).contains(&i); - let scale = i >= 6; - let (lo, hi) = if rot { (-3600.0, 3600.0) } else { (-100.0, 100.0) }; - v.push(p(n[0], "Source", Value::Enum(0), popup(CD_SOURCES))); - v.push(p(n[1], "Multiplier", num(0.0), slider(lo, hi, if rot { -360.0 } else { -10.0 }, if rot { 360.0 } else { 10.0 }, 2))); - v.push(p(n[2], "Offset", num(if scale { 1.0 } else { 0.0 }), slider(lo, hi, if rot { -360.0 } else { -10.0 }, if rot { 360.0 } else { 10.0 }, 2))); - } - v.extend(crate::card3d::camera_params(2.0)); - v.extend(crate::card3d::lighting_params(1.0, 0.25)); - v.extend(crate::card3d::material_params(0.75)); - v -} - -pub fn specs() -> Vec { - vec![ - spec( - "ec.sim.ccballaction", - "CC Ball Action", - vec![ - p("scatter", "Scatter", num(0.0), slider(0.0, 2000.0, 0.0, 300.0, 1)), - p("rotationAxis", "Rotation Axis", Value::Enum(0), popup(&["X Axis", "Y Axis", "Z Axis", "XY Axis", "XZ Axis", "YZ Axis", "XYZ Axis"])), - p("rotation", "Rotation", num(0.0), ParamUi::Angle), - p("twistProperty", "Twist Property", Value::Enum(0), popup(&["X Axis", "Y Axis", "Radius", "Brightness"])), - p("twistAngle", "Twist Angle", num(0.0), ParamUi::Angle), - p("gridSpacing", "Grid Spacing", num(4.0), slider(1.0, 200.0, 1.0, 50.0, 0)), - p("ballSize", "Ball Size", num(40.0), slider(0.0, 400.0, 0.0, 200.0, 1)), - p("instabilityState", "Instability State", num(0.0), ParamUi::Angle), - ], - ball_action, - ), - spec( - "ec.sim.ccpixelpolly", - "CC Pixel Polly", - vec![ - p("force", "Force", num(100.0), slider(-1000.0, 1000.0, -300.0, 300.0, 1)), - p("gravity", "Gravity", num(1.0), slider(-20.0, 20.0, -5.0, 5.0, 2)), - p("spinning", "Spinning", num(0.0), ParamUi::Angle), - p("forceCenter", "Force Center", pt(0.5, 0.5), ParamUi::Point), - p("directionRandomness", "Direction Randomness", num(10.0), pct()), - p("speedRandomness", "Speed Randomness", num(10.0), pct()), - p("gridSpacing", "Grid Spacing", num(10.0), slider(1.0, 500.0, 2.0, 100.0, 0)), - p("object", "Object", Value::Enum(0), popup(&["Polygon", "Textured Polygon", "Square", "Textured Square"])), - p("enableDepthSort", "Enable Depth Sort", Value::Bool(true), ParamUi::Checkbox), - p("startTime", "Start Time (sec)", num(0.0), slider(-1000.0, 1000.0, 0.0, 10.0, 2)), - ], - pixel_polly, - ), - spec( - "ec.sim.ccscatterize", - "CC Scatterize", - vec![ - p("scatter", "Scatter", num(0.0), slider(0.0, 1000.0, 0.0, 200.0, 1)), - p("rightTwist", "Right Twist", num(0.0), ParamUi::Angle), - p("leftTwist", "Left Twist", num(0.0), ParamUi::Angle), - p("transferMode", "Transfer Mode", Value::Enum(0), popup(&["Composite", "Add"])), - ], - scatterize, - ), - spec("ec.sim.carddance", "Card Dance", card_dance_params(), card_dance), - spec( - "ec.sim.caustics", - "Caustics", - vec![ - p("bottom/bottom", "Bottom", Value::Layer(None), ParamUi::Layer), - p("bottom/scaling", "Scaling", num(1.0), slider(0.01, 10.0, 0.1, 3.0, 3)), - p("bottom/repeatMode", "Repeat Mode", Value::Enum(2), popup(&["Once", "Tiled", "Reflected"])), - p("bottom/bottomSizeDiffers", "If Layer Size Differs", Value::Enum(1), popup(&["Center", "Stretch to Fit"])), - p("bottom/blur", "Blur", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)), - p("water/waterSurface", "Water Surface", Value::Layer(None), ParamUi::Layer), - p("water/waveHeight", "Wave Height", num(0.2), slider(-1.0, 1.0, -1.0, 1.0, 3)), - p("water/smoothing", "Smoothing", num(5.0), slider(0.0, 100.0, 0.0, 50.0, 1)), - p("water/waterDepth", "Water Depth", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)), - p("water/refractiveIndex", "Refractive Index", num(1.2), slider(1.0, 3.0, 1.0, 2.0, 3)), - p("water/surfaceColor", "Surface Color", col(1.0, 1.0, 1.0), ParamUi::Color), - p("water/surfaceOpacity", "Surface Opacity", num(0.3), slider(0.0, 1.0, 0.0, 1.0, 3)), - p("water/causticsStrength", "Caustics Strength", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)), - p("sky/sky", "Sky", Value::Layer(None), ParamUi::Layer), - p("sky/scaling", "Scaling", num(1.0), slider(0.01, 10.0, 0.1, 3.0, 3)), - p("sky/repeatMode", "Repeat Mode", Value::Enum(2), popup(&["Once", "Tiled", "Reflected"])), - p("sky/skySizeDiffers", "If Layer Size Differs", Value::Enum(1), popup(&["Center", "Stretch to Fit"])), - p("sky/intensity", "Intensity", num(0.3), slider(0.0, 1.0, 0.0, 1.0, 3)), - p("sky/convergence", "Convergence", num(0.5), slider(0.0, 1.0, 0.0, 1.0, 3)), - p("lighting/lightType", "Light Type", Value::Enum(0), popup(&crate::card3d::LIGHT_TYPES)), - p("lighting/lightIntensity", "Light Intensity", num(1.0), slider(0.0, 4.0, 0.0, 2.0, 2)), - p("lighting/lightColor", "Light Color", col(1.0, 1.0, 1.0), ParamUi::Color), - p("lighting/lightPosition", "Light Position", pt(0.0, 0.0), ParamUi::Point), - p("lighting/lightHeight", "Light Height", num(1.0), slider(0.0, 10.0, 0.0, 4.0, 2)), - p("lighting/ambientLight", "Ambient Light", num(0.35), slider(0.0, 2.0, 0.0, 1.0, 2)), - p("material/diffuse", "Diffuse Reflection", num(0.75), slider(0.0, 2.0, 0.0, 1.0, 2)), - p("material/specular", "Specular Reflection", num(0.2), slider(0.0, 2.0, 0.0, 1.0, 2)), - p("material/highlightSharpness", "Highlight Sharpness", num(15.0), slider(1.0, 100.0, 1.0, 100.0, 1)), - ], - caustics, - ), - spec( - "ec.sim.waveworld", - "Wave World", - { - let mut v = vec![ - p("view", "View", Value::Enum(0), popup(&["Wireframe Preview", "Height Map"])), - p("wireframeControls/horizontalRotation", "Horizontal Rotation", num(0.0), ParamUi::Angle), - p("wireframeControls/verticalRotation", "Vertical Rotation", num(53.0), slider(0.0, 90.0, 0.0, 90.0, 1)), - p("wireframeControls/verticalScale", "Vertical Scale", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 3)), - p("heightMapControls/brightness", "Brightness", num(0.5), slider(-1.0, 2.0, 0.0, 1.0, 3)), - p("heightMapControls/contrast", "Contrast", num(0.75), slider(0.0, 4.0, 0.0, 2.0, 3)), - p("heightMapControls/gamma", "Gamma Adjustment", num(1.0), slider(0.01, 10.0, 0.2, 5.0, 3)), - p("heightMapControls/renderDryAreasAs", "Render Dry Areas As", Value::Enum(0), popup(&["Solid", "Transparent"])), - p("heightMapControls/transparency", "Transparency", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)), - p("simulation/gridResolution", "Grid Resolution", num(60.0), slider(1.0, 400.0, 1.0, 200.0, 0)), - p("simulation/gridResDownsamples", "Grid Res Downsamples", Value::Bool(false), ParamUi::Checkbox), - p("simulation/waveSpeed", "Wave Speed", num(0.2), slider(0.0, 5.0, 0.0, 1.0, 3)), - p("simulation/damping", "Damping", num(0.05), slider(0.0, 5.0, 0.0, 1.0, 3)), - p("simulation/reflectEdges", "Reflect Edges", Value::Enum(0), popup(&["None", "Left", "Top", "Right", "Bottom", "All"])), - p("simulation/preRoll", "Pre-roll (seconds)", num(0.0), slider(0.0, 30.0, 0.0, 10.0, 2)), - p("ground/ground", "Ground", Value::Layer(None), ParamUi::Layer), - p("ground/steepness", "Steepness", num(0.25), slider(0.0, 2.0, 0.0, 1.0, 3)), - p("ground/height", "Height", num(0.25), slider(0.0, 2.0, 0.0, 1.0, 3)), - p("ground/waveStrength", "Wave Strength", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)), - ]; - v.extend(producer_params(1, 0.5, (0.5, 0.5))); - v.extend(producer_params(2, 0.0, (0.25, 0.25))); - v - }, - wave_world, - ), - spec( - "ec.sim.shatter", - "Shatter", - vec![ - p( - "view", - "View", - Value::Enum(1), - popup(&["Rendered", "Wireframe Front View", "Wireframe", "Wireframe Front View + Forces", "Wireframe + Forces"]), - ), - p("render", "Render", Value::Enum(0), popup(&["All", "Layer", "Pieces"])), - p("shape/pattern", "Pattern", Value::Enum(0), popup(SHATTER_PATTERNS)), - p("shape/repetitions", "Repetitions", num(10.0), slider(1.0, 500.0, 1.0, 100.0, 2)), - p("shape/direction", "Direction", num(0.0), ParamUi::Angle), - p("shape/origin", "Origin", pt(0.5, 0.5), ParamUi::Point), - p("shape/customShatterMap", "Custom Shatter Map", Value::Layer(None), ParamUi::Layer), - p("shape/whiteTilesFixed", "White Tiles Fixed", Value::Bool(false), ParamUi::Checkbox), - p("shape/extrusionDepth", "Extrusion Depth", num(0.05), slider(0.0, 1.0, 0.0, 1.0, 3)), - p("force1/force1Position", "Position", pt(0.5, 0.5), ParamUi::Point), - p("force1/force1Depth", "Depth", num(0.1), slider(-1.0, 1.0, -1.0, 1.0, 3)), - p("force1/force1Radius", "Radius", num(0.4), slider(0.0, 4.0, 0.0, 2.0, 3)), - p("force1/force1Strength", "Strength", num(5.0), slider(-20.0, 20.0, -10.0, 10.0, 2)), - p("force2/force2Position", "Position", pt(0.25, 0.25), ParamUi::Point), - p("force2/force2Depth", "Depth", num(0.1), slider(-1.0, 1.0, -1.0, 1.0, 3)), - p("force2/force2Radius", "Radius", num(0.0), slider(0.0, 4.0, 0.0, 2.0, 3)), - p("force2/force2Strength", "Strength", num(5.0), slider(-20.0, 20.0, -10.0, 10.0, 2)), - p("physics/rotationSpeed", "Rotation Speed", num(0.2), slider(0.0, 5.0, 0.0, 1.0, 3)), - p("physics/tumbleAxis", "Tumble Axis", Value::Enum(0), popup(&["Free", "None", "X", "Y", "Z", "XY", "XZ", "YZ"])), - p("physics/randomness", "Randomness", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)), - p("physics/viscosity", "Viscosity", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)), - p("physics/massVariance", "Mass Variance", num(30.0), pct()), - p("physics/gravity", "Gravity", num(3.0), slider(0.0, 100.0, 0.0, 10.0, 2)), - p("physics/gravityDirection", "Gravity Direction", num(180.0), ParamUi::Angle), - p("physics/gravityInclination", "Gravity Inclination", num(0.0), slider(-90.0, 90.0, -90.0, 90.0, 1)), - p("gradient/shatterThreshold", "Shatter Threshold", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)), - p("gradient/gradientLayer", "Gradient Layer", Value::Layer(None), ParamUi::Layer), - p("gradient/invertGradient", "Invert Gradient", Value::Bool(false), ParamUi::Checkbox), - p("textures/color", "Color", col(1.0, 1.0, 1.0), ParamUi::Color), - p("textures/opacity", "Opacity", num(1.0), slider(0.0, 1.0, 0.0, 1.0, 3)), - p("textures/frontMode", "Front Mode", Value::Enum(1), popup(&SHATTER_TEXTURE_MODES)), - p("textures/sideMode", "Side Mode", Value::Enum(0), popup(&SHATTER_TEXTURE_MODES)), - p("textures/backMode", "Back Mode", Value::Enum(1), popup(&SHATTER_TEXTURE_MODES)), - p("textures/frontLayer", "Front Layer", Value::Layer(None), ParamUi::Layer), - p("textures/sideLayer", "Side Layer", Value::Layer(None), ParamUi::Layer), - p("textures/backLayer", "Back Layer", Value::Layer(None), ParamUi::Layer), - ] - .into_iter() - .chain(crate::card3d::camera_params(4.0)) - .chain(crate::card3d::lighting_params(1.0, 0.25)) - .chain(crate::card3d::material_params(0.75)) - .chain([p("randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0))]) - .collect(), - shatter, - ), - spec( - "ec.sim.foam", - "Foam", - vec![ - p("view", "View", Value::Enum(0), popup(&["Draft", "Draft + Flow Map", "Rendered"])), - p("producer/producerPoint", "Producer Point", pt(0.5, 0.5), ParamUi::Point), - p("producer/producerXSize", "Producer X Size", num(0.05), slider(0.0, 2.0, 0.0, 1.0, 3)), - p("producer/producerYSize", "Producer Y Size", num(0.05), slider(0.0, 2.0, 0.0, 1.0, 3)), - p("producer/producerOrientation", "Producer Orientation", num(0.0), ParamUi::Angle), - p("producer/zoomProducerPoint", "Zoom Producer Point", Value::Bool(false), ParamUi::Checkbox), - p("producer/productionRate", "Production Rate", num(1.0), slider(0.0, 50.0, 0.0, 10.0, 3)), - p("bubbles/size", "Size", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 3)), - p("bubbles/sizeVariance", "Size Variance", num(0.5), slider(0.0, 1.0, 0.0, 1.0, 3)), - p("bubbles/lifespan", "Lifespan", num(300.0), slider(1.0, 30_000.0, 1.0, 1000.0, 1)), - p("bubbles/growthSpeed", "Bubble Growth Speed", num(0.1), slider(0.0001, 10.0, 0.0001, 1.0, 3)), - p("bubbles/strength", "Strength", num(10.0), slider(0.0, 100.0, 0.0, 20.0, 2)), - p("physics/initialSpeed", "Initial Speed", num(0.0), slider(-100.0, 100.0, -10.0, 10.0, 3)), - p("physics/initialDirection", "Initial Direction", num(0.0), ParamUi::Angle), - p("physics/windSpeed", "Wind Speed", num(0.5), slider(-100.0, 100.0, -10.0, 10.0, 3)), - p("physics/windDirection", "Wind Direction", num(90.0), ParamUi::Angle), - p("physics/turbulence", "Turbulence", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 3)), - p("physics/wobbleAmount", "Wobble Amount", num(0.05), slider(0.0, 10.0, 0.0, 2.0, 3)), - p("physics/repulsion", "Repulsion", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 3)), - p("physics/popVelocity", "Pop Velocity", num(1.0), slider(-100.0, 100.0, -10.0, 10.0, 3)), - p("physics/viscosity", "Viscosity", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)), - p("physics/stickiness", "Stickiness", num(0.75), slider(0.0, 1.0, 0.0, 1.0, 3)), - p("zoom", "Zoom", num(1.0), slider(0.01, 50.0, 0.1, 5.0, 3)), - p("universeSize", "Universe Size", num(1.0), slider(0.01, 10.0, 0.5, 3.0, 3)), - p("rendering/blendMode", "Blend Mode", Value::Enum(0), popup(&["Transparent", "Solid Old on Top", "Solid New on Top"])), - p("rendering/bubbleTexture", "Bubble Texture", Value::Enum(0), popup(&FOAM_TEXTURES)), - p("rendering/bubbleTextureLayer", "Bubble Texture Layer", Value::Layer(None), ParamUi::Layer), - p("rendering/bubbleOrientation", "Bubble Orientation", Value::Enum(0), popup(&["Fixed", "Physical Orientation", "Bubble Velocity"])), - p("rendering/environmentMap", "Environment Map", Value::Layer(None), ParamUi::Layer), - p("rendering/reflectionStrength", "Reflection Strength", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)), - p("rendering/reflectionConvergence", "Reflection Convergence", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)), - p("flowMap/flowMap", "Flow Map", Value::Layer(None), ParamUi::Layer), - p("flowMap/flowMapSteepness", "Flow Map Steepness", num(0.05), slider(-100.0, 100.0, -1.0, 1.0, 3)), - p("flowMap/flowMapFits", "Flow Map Fits", Value::Enum(0), popup(&["Background", "Universe"])), - p("flowMap/simulationQuality", "Simulation Quality", Value::Enum(0), popup(&["Normal", "High", "Intense"])), - p("randomSeed", "Random Seed", num(1.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)), - ], - foam, - ), - ] -} +mod foam; +mod pieces; +mod pixels; +mod registry; +mod shatter; +mod surfaces; + +pub(crate) use foam::foam_plan; +#[cfg(test)] +use foam::foam_pop_age; +pub use foam::{FoamDisc, FoamEnv, FoamPlan, foam_full_plan}; +#[cfg(test)] +use pieces::Cam; +#[cfg(test)] +pub(crate) use pieces::draw_pieces; +pub(crate) use pieces::{rot_axis, sort_far_first}; +pub use pieces::{Piece, PiecePlan, PieceTex, piece_plan}; +pub use pixels::{PixelPlan, pixel_plan}; +pub use registry::specs; +#[cfg(test)] +use shatter::SHATTER_PATTERNS; +pub use surfaces::{CausticsSetup, WavePlan, Waves, caustics_setup, wave_world_plan}; #[cfg(test)] mod tests; diff --git a/crates/effects/src/sim2/foam.rs b/crates/effects/src/sim2/foam.rs new file mode 100644 index 000000000..cf9009492 --- /dev/null +++ b/crates/effects/src/sim2/foam.rs @@ -0,0 +1,408 @@ +use super::*; + +// ------------------------------------------------------------------ Foam + +#[derive(Clone, Debug)] +struct FoamBubble { + p: [f32; 2], + v: [f32; 2], + age: f32, + size: f32, + /// Spin (radians) for Physical Orientation. + spin: f32, + id: u32, +} + +#[derive(Clone, Debug, Default)] +struct FoamState { + b: Vec, + carry: f32, + next: u32, +} + +static FOAM_CACHE: SimCache = SimCache::new(4); + +/// Foam steps once per frame at 30 frames per second. +const FOAM_FPS: f64 = 30.0; + +/// Foam's Bubble Texture options (the last uses Bubble Texture Layer). +pub(super) const FOAM_TEXTURES: [&str; 6] = ["Default Bubble", "Spit", "Bubblegum", "Dishwater", "Milky", "User Defined"]; + +/// Age (in simulation steps) at which bubble `id` pops: its lifespan, shortened at random for +/// frail bubbles (`frailty` 0 = every bubble reaches `lifespan`, 1 = uniformly early). +pub(super) fn foam_pop_age(id: u32, lifespan: f32, frailty: f32, seed: u32) -> f32 { + lifespan * (1.0 - frailty * (hs(id, 11, seed) * 0.5 + 0.5)) +} + +/// Foam: bubbles born in the producer rectangle grow to their size, drift with the wind, +/// turbulence and the Flow Map (bubbles run downhill on its luminance), repel and stick to each +/// other, wobble, and pop at the end of their (Strength-shortened) lifespan, pushing their +/// neighbours away at Pop Velocity. Rendered bubbles use a built-in texture or the Bubble +/// Texture Layer, oriented by Bubble Orientation, and reflect the Environment Map. +pub(super) fn foam(ctx: &EffectCtx, b: Buf) -> Buf { + foam_full_plan(ctx, &b).finish(b) +} + +/// Foam's bubbles as sprites, `None` when it draws more than sprites (a User Defined bubble +/// texture, an Environment Map reflection or the Draft + Flow Map view's flow map). +pub(crate) fn foam_plan(ctx: &EffectCtx, b: &Buf) -> Option { + let plan = foam_full_plan(ctx, b); + (plan.user.is_none() && plan.env.is_none() && plan.flow.is_none()).then_some(plan.sprites) +} + +/// A bubble disc filled by Foam's User Defined texture (buffer px; `rot` radians). +#[derive(Clone, Copy, Debug)] +pub struct FoamDisc { + pub x: f64, + pub y: f64, + pub r: f32, + pub rot: f32, + pub fade: f32, +} + +/// Foam's Environment Map reflection: the map fitted to the buffer, Reflection Convergence and +/// Strength, and the bubble discs reflecting it (`rot` and `fade` unused). +pub struct FoamEnv { + pub img: Image, + pub conv: f64, + pub strength: f32, + pub discs: Vec, +} + +/// What Foam draws this frame over a transparent frame of the layer's size: the bubble sprites, +/// then the User Defined texture's discs, the Environment Map's reflections and (Draft + Flow +/// Map) the flow map under it all, each `foam_disc`-composited in bubble order. +pub struct FoamPlan { + pub sprites: crate::sim::SpritePlan, + pub user: Option<(crate::LayerPixels, Vec)>, + pub env: Option, + /// The flow map fitted to the buffer. + pub flow: Option, +} + +impl FoamPlan { + pub fn finish(&self, b: Buf) -> Buf { + let mut out = splat(b.img.width, b.img.height, &self.sprites.sprites, self.sprites.acc); + // User Defined: the texture layer fills each bubble's disc. + if let Some((tex, discs)) = &self.user { + for d in discs { + let (rot, fade) = (d.rot, d.fade); + foam_disc(&mut out, d.x, d.y, d.r as f64, |u, v| { + // u, v in −1..1 across the bubble, rotated into the texture. + let (sr, cr) = rot.sin_cos(); + let (tu, tv) = (u as f32 * cr + v as f32 * sr, -u as f32 * sr + v as f32 * cr); + let (x, y) = ((tu as f64 * 0.5 + 0.5) * tex.size[0], (tv as f64 * 0.5 + 0.5) * tex.size[1]); + let px = tex.buf.img.sample_bilinear(x * tex.buf.scale + tex.buf.offset[0], y * tex.buf.scale + tex.buf.offset[1]); + px.map(|c| c * fade) + }); + } + } + // Environment Map: each bubble reflects the map along its sphere normal (Reflection + // Convergence pulls the reflection towards the bubble's own spot of the map). + if let Some(env) = &self.env { + let (conv, strength) = (env.conv, env.strength); + let (w, h) = (b.img.width as f64, b.img.height as f64); + for d in &env.discs { + let (bx, by) = (d.x, d.y); + foam_disc(&mut out, bx, by, d.r as f64, |u, v| { + // Sphere normal; the reflection looks across the whole map, or (convergence 1) + // just behind the bubble. + let (ex, ey) = + if conv >= 1.0 { (bx, by) } else { (bx * conv + (u * 0.5 + 0.5) * w * (1.0 - conv), by * conv + (v * 0.5 + 0.5) * h * (1.0 - conv)) }; + let e = env.img.sample_bilinear(ex, ey); + let rim = ((u * u + v * v) as f32).min(1.0); + let k = strength * (0.3 + 0.7 * rim); + [e[0] * k, e[1] * k, e[2] * k, 0.0] + }); + } + } + // Draft + Flow Map shows the flow map under the bubbles. + if let Some(fm) = &self.flow { + out.data.par_iter_mut().zip(fm.data.par_iter()).for_each(|(o, f)| { + let k = 1.0 - o[3]; + for c in 0..4 { + o[c] += f[c] * 0.5 * k; + } + }); + } + Buf { img: out, ..b } + } +} + +/// Foam's [`FoamPlan`] for layer geometry `b` (its size; its pixels are not read). +pub fn foam_full_plan(ctx: &EffectCtx, b: &Buf) -> FoamPlan { + let pr = ctx.params; + let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32); + let unit = lw * 0.01; + let prod = pr.v2("producer/producerPoint"); + let (psx, psy) = (pr.f("producer/producerXSize") as f32 * lw, pr.f("producer/producerYSize") as f32 * lh); + let porient = (pr.f("producer/producerOrientation") as f32).to_radians(); + let rate = pr.f("producer/productionRate").max(0.0) as f32; + let size = pr.f("bubbles/size").max(0.0) as f32; + let size_var = pr.f("bubbles/sizeVariance") as f32; + let lifespan = pr.f("bubbles/lifespan").max(1.0) as f32; + let growth = pr.f("bubbles/growthSpeed").max(0.0001) as f32; + // Strength: weaker bubbles tend to pop before their lifespan (at 20 or more every bubble + // lives the full lifespan); each bubble gets its own pop age. + let frailty = 1.0 - (pr.f("bubbles/strength") as f32 / 20.0).clamp(0.0, 1.0); + let init_speed = pr.f("physics/initialSpeed") as f32; + let init_dir = (pr.f("physics/initialDirection") as f32).to_radians(); + let wind = pr.f("physics/windSpeed") as f32; + let wind_dir = (pr.f("physics/windDirection") as f32).to_radians(); + let turb = pr.f("physics/turbulence") as f32; + let wobble = pr.f("physics/wobbleAmount").max(0.0) as f32; + let repulsion = pr.f("physics/repulsion") as f32; + let pop_velocity = pr.f("physics/popVelocity") as f32; + let viscosity = pr.f("physics/viscosity").clamp(0.0, 1.0) as f32; + let sticky = pr.f("physics/stickiness").clamp(0.0, 1.0) as f32; + let universe = pr.f("universeSize").max(0.01) as f32; + let seed = (pr.f("randomSeed") as u32).wrapping_mul(0x68e31da4) ^ ctx.seed; + let wvec = [wind_dir.sin() * wind * unit, -wind_dir.cos() * wind * unit]; + // Flow Map: the map layer's first frame, its luminance a height field (Fits the layer or + // the whole universe). + let flow = pr.get("flowMap/flowMap").and_then(Value::as_layer).and_then(|id| { + let host = ctx.env.host?; + let me = pr.get(&crate::layer_source_id("flowMap/flowMap")).is_none_or(|v| v.as_enum() != 0); + host.layer_at(id, ctx.env.comp_time - ctx.time, me) + }); + let steep = pr.f("flowMap/flowMapSteepness") as f32; + let fits_universe = pr.e("flowMap/flowMapFits") == 1; + let substeps = match pr.e("flowMap/simulationQuality") { + 1 => 2, + 2 => 4, + _ => 1, + }; + let height = flow.as_ref().map(|lp| { + let img = &lp.buf.img; + let pl = Plane::from_image(img, |px| { + let (c, a) = unpremul(px); + effectcraft_color::luminance(c[0], c[1], c[2]) * a + }); + (pl, lp.buf.scale, lp.buf.offset, lp.size) + }); + let (ux0, uy0) = (-(universe - 1.0) * 0.5 * lw, -(universe - 1.0) * 0.5 * lh); + let (ux1, uy1) = (lw - ux0, lh - uy0); + // Height at a layer point (0..1). + let h_at = |x: f32, y: f32| -> f32 { + let Some((pl, s, off, size)) = &height else { return 0.0 }; + let (fx, fy) = if fits_universe { ((x - ux0) / (ux1 - ux0), (y - uy0) / (uy1 - uy0)) } else { (x / lw, y / lh) }; + pl.sample(fx as f64 * size[0] * s + off[0], fy as f64 * size[1] * s + off[1]) + }; + let mut key = params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 5); + if let Some((pl, ..)) = &height { + key ^= pl.data.iter().step_by(5).fold(0u64, |a, v| a.rotate_left(3) ^ v.to_bits() as u64); + } + let steps = if ctx.time <= 0.0 { 0 } else { (ctx.time * FOAM_FPS + 1e-6).floor() as u64 }; + let rad = |q: &FoamBubble| q.size * unit * 3.0 * (q.age * growth).min(1.0); + let st = FOAM_CACHE.run(key, steps, FoamState::default, |st, step| { + let t = step as f32 / FOAM_FPS as f32; + // Births. + st.carry += rate; + let n = st.carry.floor() as u32; + st.carry -= n as f32; + for _ in 0..n { + if st.b.len() >= 3000 { + break; + } + let id = st.next; + st.next = st.next.wrapping_add(1); + let (ux, uy) = (hs(id, 1, seed) * 0.5 * psx, hs(id, 2, seed) * 0.5 * psy); + let (so, co) = porient.sin_cos(); + let p = [prod[0] as f32 + ux * co - uy * so, prod[1] as f32 + ux * so + uy * co]; + let v = [init_dir.sin() * init_speed * unit, -init_dir.cos() * init_speed * unit]; + let sz = (size * (1.0 + size_var * hs(id, 3, seed))).max(0.01); + st.b.push(FoamBubble { p, v, age: 0.0, size: sz, spin: hs(id, 12, seed) * std::f32::consts::PI, id }); + } + for _ in 0..substeps { + let k = 1.0 / substeps as f32; + // Forces: drift towards the wind, turbulence, viscosity, the flow map's slope. + for q in st.b.iter_mut() { + let n1 = value_noise(q.p[0] / (lw * 0.15), q.p[1] / (lw * 0.15), t * 0.5, seed) - 0.5; + let n2 = value_noise(q.p[0] / (lw * 0.15) + 19.0, q.p[1] / (lw * 0.15), t * 0.5, seed) - 0.5; + for c in 0..2 { + q.v[c] += (wvec[c] - q.v[c]) * 0.1 * k; + q.v[c] *= 1.0 - viscosity * 0.5 * k; + } + q.v[0] += n1 * turb * unit * k; + q.v[1] += n2 * turb * unit * k; + if height.is_some() && steep != 0.0 { + let e = unit.max(1.0); + let gx = (h_at(q.p[0] + e, q.p[1]) - h_at(q.p[0] - e, q.p[1])) / (2.0 * e); + let gy = (h_at(q.p[0], q.p[1] + e) - h_at(q.p[0], q.p[1] - e)) / (2.0 * e); + // Downhill: from light (high) to dark (low). + q.v[0] -= gx * steep * lw * 0.5 * k; + q.v[1] -= gy * steep * lw * 0.5 * k; + } + // Physical Orientation: bubbles roll with their sideways motion. + q.spin += q.v[0] / (rad(q).max(0.5)) * k; + } + // Repulsion between overlapping bubbles (grid hashed, deterministic order). + if repulsion > 0.0 && st.b.len() > 1 { + let maxr = st.b.iter().map(rad).fold(0.0f32, f32::max).max(1.0); + let cs = maxr * 2.0; + let mut grid: std::collections::HashMap<(i32, i32), Vec> = std::collections::HashMap::new(); + for (i, q) in st.b.iter().enumerate() { + grid.entry(((q.p[0] / cs).floor() as i32, (q.p[1] / cs).floor() as i32)).or_default().push(i); + } + let mut push = vec![[0.0f32; 2]; st.b.len()]; + for (i, q) in st.b.iter().enumerate() { + let (gx, gy) = ((q.p[0] / cs).floor() as i32, (q.p[1] / cs).floor() as i32); + for dy in -1..=1 { + for dx in -1..=1 { + let Some(list) = grid.get(&(gx + dx, gy + dy)) else { continue }; + for &j in list { + if j <= i { + continue; + } + let o = &st.b[j]; + let (ddx, ddy) = (o.p[0] - q.p[0], o.p[1] - q.p[1]); + let d = (ddx * ddx + ddy * ddy).sqrt().max(1e-4); + let overlap = rad(q) + rad(o) - d; + if overlap > 0.0 { + let f = overlap * 0.5 * repulsion.min(4.0) * (1.0 - sticky * 0.5) / d * k; + push[i][0] -= ddx * f; + push[i][1] -= ddy * f; + push[j][0] += ddx * f; + push[j][1] += ddy * f; + } + } + } + } + } + for (q, d) in st.b.iter_mut().zip(&push) { + q.p[0] += d[0].clamp(-maxr, maxr); + q.p[1] += d[1].clamp(-maxr, maxr); + } + } + for q in st.b.iter_mut() { + q.p[0] += q.v[0] * k; + q.p[1] += q.v[1] * k; + } + } + for q in st.b.iter_mut() { + q.age += 1.0; + } + // Pops: bubbles past their pop age burst, pushing their neighbours away. + let popped: Vec<([f32; 2], f32)> = st.b.iter().filter(|q| q.age >= foam_pop_age(q.id, lifespan, frailty, seed)).map(|q| (q.p, rad(q))).collect(); + if pop_velocity != 0.0 { + for (pp, pr_) in &popped { + let reach = (pr_ * 3.0).max(unit); + for q in st.b.iter_mut() { + let (dx, dy) = (q.p[0] - pp[0], q.p[1] - pp[1]); + let d = (dx * dx + dy * dy).sqrt(); + if d > 1e-4 && d < reach { + let f = pop_velocity * unit * (1.0 - d / reach) / d; + q.v[0] += dx * f; + q.v[1] += dy * f; + } + } + } + } + st.b.retain(|q| q.age < foam_pop_age(q.id, lifespan, frailty, seed) && q.p[0] > ux0 && q.p[0] < ux1 && q.p[1] > uy0 && q.p[1] < uy1); + }); + let view = pr.e("view"); + let zoom = pr.f("zoom").max(0.01) as f32; + let zoom_producer = pr.b("producer/zoomProducerPoint"); + let texture = pr.e("rendering/bubbleTexture"); + let orient = pr.e("rendering/bubbleOrientation"); + let blend = pr.e("rendering/blendMode"); + let s = b.scale as f32; + // Zoom centres on the layer, or on the producer with Zoom Producer Point. + let (cx, cy) = if zoom_producer { (prod[0] as f32, prod[1] as f32) } else { (lw * 0.5, lh * 0.5) }; + let mut order: Vec<&FoamBubble> = st.b.iter().collect(); + if blend == 2 { + // Solid New on Top: oldest first. + order.sort_by(|a, c| c.age.total_cmp(&a.age).then(a.id.cmp(&c.id))); + } else if blend == 1 { + order.sort_by(|a, c| a.age.total_cmp(&c.age).then(a.id.cmp(&c.id))); + } + // Where a bubble is drawn: buffer centre, radius, rotation (radians). + let place = |q: &FoamBubble| { + let wob = if wobble > 0.0 { 1.0 + wobble * 0.15 * ((q.age * 0.35) + hs(q.id, 13, seed) * 6.0).sin() } else { 1.0 }; + let r = rad(q) * wob * zoom * s; + let (bx, by) = b.to_px([(cx + (q.p[0] - cx) * zoom) as f64, (cy + (q.p[1] - cy) * zoom) as f64]); + let rot = match orient { + 1 => q.spin, + 2 => q.v[1].atan2(q.v[0]), + _ => 0.0, + }; + (bx, by, r, rot) + }; + let user_tex = (view == 2 && texture == 5).then(|| ctx.layer_param("rendering/bubbleTextureLayer", true)).flatten(); + let sprites: Vec = order + .iter() + .filter(|_| user_tex.is_none()) + .map(|q| { + let (bx, by, r, rot) = place(q); + let fade = ((foam_pop_age(q.id, lifespan, frailty, seed) - q.age) / 5.0).clamp(0.0, 1.0); + // Draft and Draft + Flow Map show the wireframe-style bubbles. + let mut sp = if view != 2 { + Sprite::new(bx as f32, by as f32, r, [0.55, 0.75, 1.0, fade], Shape::Bubble) + } else { + let (c, shape) = match texture { + 1 => ([0.95, 0.95, 0.92], Shape::Faded), + 2 => ([1.0, 0.6, 0.8], Shape::Bubble), + 3 => ([0.8, 0.85, 0.75], Shape::Bubble), + 4 => ([0.97, 0.97, 0.95], Shape::Sphere), + _ => ([1.0, 1.0, 1.0], Shape::Bubble), + }; + Sprite::new(bx as f32, by as f32, r, [c[0], c[1], c[2], fade], shape) + }; + sp.rot = rot; + sp + }) + .collect(); + let acc = if blend == 0 && view == 2 { Acc::Add } else { Acc::Over }; + let disc = |q: &&FoamBubble| { + let (x, y, r, rot) = place(q); + let fade = ((foam_pop_age(q.id, lifespan, frailty, seed) - q.age) / 5.0).clamp(0.0, 1.0); + FoamDisc { x, y, r, rot, fade } + }; + let user = user_tex.map(|tex| (tex, order.iter().map(disc).collect())); + let strength = pr.f("rendering/reflectionStrength").clamp(0.0, 1.0) as f32; + let env = if view == 2 + && strength > 0.0 + && let Some(env) = ctx.layer_param("rendering/environmentMap", true) + { + Some(FoamEnv { + img: crate::util::fit_layer(ctx, b, &env, true), + conv: pr.f("rendering/reflectionConvergence").clamp(0.0, 1.0), + strength, + discs: order.iter().map(disc).collect(), + }) + } else { + None + }; + let flow = if view == 1 + && let Some(lp) = &flow + { + Some(crate::util::fit_layer(ctx, b, lp, true)) + } else { + None + }; + FoamPlan { sprites: crate::sim::SpritePlan { sprites, tints: vec![], acc, post: crate::sim::Post::Replace }, user, env, flow } +} + +/// Add `f(u, v)` (premultiplied; u, v ∈ −1..1 across the disc) over the disc of radius `r` at +/// (`x`, `y`) with anti-aliased edges ("over" for opaque texels, additive for alpha 0). +fn foam_disc(out: &mut Image, x: f64, y: f64, r: f64, f: impl Fn(f64, f64) -> Px) { + if r <= 0.0 { + return; + } + let (x0, x1) = ((x - r - 1.0).floor().max(0.0) as i64, (x + r + 1.0).ceil().min(out.width as f64) as i64); + let (y0, y1) = ((y - r - 1.0).floor().max(0.0) as i64, (y + r + 1.0).ceil().min(out.height as f64) as i64); + for py in y0..y1 { + for px in x0..x1 { + let (u, v) = ((px as f64 + 0.5 - x) / r, (py as f64 + 0.5 - y) / r); + let d = (u * u + v * v).sqrt(); + let cov = ((1.0 - d) * r + 0.5).clamp(0.0, 1.0) as f32; + if cov <= 0.0 { + continue; + } + let s = f(u.clamp(-1.0, 1.0), v.clamp(-1.0, 1.0)).map(|c| c * cov); + let o = out.get(px, py); + let k = 1.0 - s[3]; + out.set(px as u32, py as u32, [s[0] + o[0] * k, s[1] + o[1] * k, s[2] + o[2] * k, (s[3] + o[3] * k).min(1.0)]); + } + } +} diff --git a/crates/effects/src/sim2/pieces.rs b/crates/effects/src/sim2/pieces.rs new file mode 100644 index 000000000..3aa3a68f6 --- /dev/null +++ b/crates/effects/src/sim2/pieces.rs @@ -0,0 +1,277 @@ +use super::shatter::{ShatterPlan, card_dance_plan, shatter_impl}; +use super::*; + +// ------------------------------------------------------------------ 3D pieces + +pub(crate) type M3 = [[f64; 3]; 3]; + +fn mat_mul(a: &M3, b: &M3) -> M3 { + std::array::from_fn(|i| std::array::from_fn(|j| (0..3).map(|k| a[i][k] * b[k][j]).sum())) +} + +/// Rotation from Euler angles (radians), applied X then Y then Z. +pub(super) fn rot_xyz(x: f64, y: f64, z: f64) -> M3 { + let (sx, cx) = x.sin_cos(); + let (sy, cy) = y.sin_cos(); + let (sz, cz) = z.sin_cos(); + let rx = [[1.0, 0.0, 0.0], [0.0, cx, -sx], [0.0, sx, cx]]; + let ry = [[cy, 0.0, sy], [0.0, 1.0, 0.0], [-sy, 0.0, cy]]; + let rz = [[cz, -sz, 0.0], [sz, cz, 0.0], [0.0, 0.0, 1.0]]; + mat_mul(&rz, &mat_mul(&ry, &rx)) +} + +/// Rotation by `angle` about unit `axis` (Rodrigues). +pub(crate) fn rot_axis(axis: [f64; 3], angle: f64) -> M3 { + let l = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]).sqrt(); + if l < 1e-12 || angle == 0.0 { + return [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]]; + } + let (x, y, z) = (axis[0] / l, axis[1] / l, axis[2] / l); + let (s, c) = angle.sin_cos(); + let t = 1.0 - c; + [ + [t * x * x + c, t * x * y - s * z, t * x * z + s * y], + [t * x * y + s * z, t * y * y + c, t * y * z - s * x], + [t * x * z - s * y, t * y * z + s * x, t * z * z + c], + ] +} + +pub(super) fn inv3(m: &M3) -> Option { + let det = m[0][0] * (m[1][1] * m[2][2] - m[1][2] * m[2][1]) - m[0][1] * (m[1][0] * m[2][2] - m[1][2] * m[2][0]) + + m[0][2] * (m[1][0] * m[2][1] - m[1][1] * m[2][0]); + if det.abs() < 1e-12 { + return None; + } + let d = 1.0 / det; + Some([ + [(m[1][1] * m[2][2] - m[1][2] * m[2][1]) * d, (m[0][2] * m[2][1] - m[0][1] * m[2][2]) * d, (m[0][1] * m[1][2] - m[0][2] * m[1][1]) * d], + [(m[1][2] * m[2][0] - m[1][0] * m[2][2]) * d, (m[0][0] * m[2][2] - m[0][2] * m[2][0]) * d, (m[0][2] * m[1][0] - m[0][0] * m[1][2]) * d], + [(m[1][0] * m[2][1] - m[1][1] * m[2][0]) * d, (m[0][1] * m[2][0] - m[0][0] * m[2][1]) * d, (m[0][0] * m[1][1] - m[0][1] * m[1][0]) * d], + ]) +} + +/// Perspective camera looking down +z at the layer plane z = 0, `d` pixels away, centred on +/// `c` (buffer pixels): screen = c + (P - c) · d / (d + z). +#[derive(Clone, Copy)] +pub(super) struct Cam { + pub(super) c: [f64; 2], + pub(super) d: f64, +} + +impl Cam { + fn proj(self) -> Proj { + Proj::simple(self.c, self.d) + } +} + +/// A planar textured piece ready to draw. +#[derive(Clone, Copy, Debug)] +pub struct Piece { + /// Screen (buffer px, homogeneous) → piece-local (a, b, w). + pub inv: [[f64; 3]; 3], + /// Rest centre of the piece in texture pixels. + pub c0: [f32; 2], + /// Convex polygon (texture pixels), counter-clockwise in y-down space. + pub poly: [[f32; 2]; 6], + pub n: u8, + pub bbox: [f32; 4], + pub shade: f32, + /// Lighting: colour multiplier and additive specular highlight. + pub tint: [f32; 3], + pub spec: [f32; 3], + pub alpha: f32, + /// Flat straight colour instead of the texture. + pub flat: Option<[f32; 3]>, + /// Facing away from the camera (draw the back texture). + pub back: bool, + /// Back faces sample their texture mirrored about the piece centre: 0 no, 1 vertically + /// (flipped about the X axis), 2 horizontally (about the Y axis). + pub back_mirror: u8, + depth: f64, +} + +/// A piece texture: the layer itself or another image (buffer-aligned). +#[derive(Clone, Debug)] +pub enum PieceTex { + Layer, + Image(Image), +} + +/// What a piece-drawing effect (Card Dance, Shatter, Card Wipe) draws this frame: pieces sorted +/// far to near over a transparent frame, textured by `front` (and `back` for pieces facing +/// away; none = they take `front`). +#[derive(Clone, Debug)] +pub struct PiecePlan { + pub pieces: Vec, + pub front: PieceTex, + pub back: Option, +} + +impl PiecePlan { + /// Rasterise over a transparent frame of the layer `b`'s size. + pub fn finish(&self, mut b: Buf) -> Buf { + let mut out = Image::new(b.img.width, b.img.height); + let front = match &self.front { + PieceTex::Layer => &b.img, + PieceTex::Image(i) => i, + }; + let back = self.back.as_ref().map(|t| match t { + PieceTex::Layer => &b.img, + PieceTex::Image(i) => i, + }); + draw_pieces(&mut out, &self.pieces, front, back); + b.img = out; + b + } +} + +/// The piece plan of Card Dance, Shatter (Rendered view) or Card Wipe for layer `b` (its pixels +/// may be read: gradient maps and textures default to the layer), `None` for other effects or +/// views drawn otherwise. +pub fn piece_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option { + match id { + "ec.sim.carddance" => Some(card_dance_plan(ctx, b)), + "ec.sim.shatter" if ctx.params.e("view") == 0 => match shatter_impl(ctx, b) { + ShatterPlan::Pieces(plan) => Some(plan), + ShatterPlan::Wire(_) => None, + }, + "ec.transition.cardwipe" => crate::transition::card_wipe_plan(ctx, b), + _ => None, + } +} + +impl Piece { + /// Place polygon `poly` (texture px, around rest centre `c0`) with rotation `r` and its + /// centre at 3D `pos` (buffer px; z away from the camera), seen through `cam`. + pub(crate) fn new(poly: &[[f32; 2]], c0: [f32; 2], r: &M3, pos: [f64; 3], cam: impl Into) -> Option { + let cam: Proj = cam.into(); + let n = poly.len().min(6); + if n < 3 { + return None; + } + let m = &cam.m; + let row = |i: usize| -> [f64; 3] { + [ + m[i][0] * r[0][0] + m[i][1] * r[1][0] + m[i][2] * r[2][0], + m[i][0] * r[0][1] + m[i][1] * r[1][1] + m[i][2] * r[2][1], + m[i][0] * pos[0] + m[i][1] * pos[1] + m[i][2] * pos[2] + m[i][3], + ] + }; + let hm: M3 = [row(0), row(1), row(2)]; + let mut bb = [f32::INFINITY, f32::INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY]; + for v in &poly[..n] { + let (a, b) = ((v[0] - c0[0]) as f64, (v[1] - c0[1]) as f64); + let w = hm[2][0] * a + hm[2][1] * b + hm[2][2]; + if w < cam.near { + return None; + } + let sx = ((hm[0][0] * a + hm[0][1] * b + hm[0][2]) / w) as f32; + let sy = ((hm[1][0] * a + hm[1][1] * b + hm[1][2]) / w) as f32; + bb = [bb[0].min(sx), bb[1].min(sy), bb[2].max(sx), bb[3].max(sy)]; + } + let inv = inv3(&hm)?; + let mut pp = [[0.0f32; 2]; 6]; + pp[..n].copy_from_slice(&poly[..n]); + // Normalise winding so "inside" is cross >= 0 for every edge. + let area: f32 = (0..n).map(|i| pp[i][0] * pp[(i + 1) % n][1] - pp[(i + 1) % n][0] * pp[i][1]).sum(); + if area < 0.0 { + pp[..n].reverse(); + } + // The front faces -z at rest; it faces the camera when its normal points at the eye. + let eye = cam.eye(); + let nrm = [-r[0][2], -r[1][2], -r[2][2]]; + let to_eye = [eye[0] - pos[0], eye[1] - pos[1], eye[2] - pos[2]]; + let facing = nrm[0] * to_eye[0] + nrm[1] * to_eye[1] + nrm[2] * to_eye[2]; + let depth = hm[2][2]; + Some(Piece { + inv, + c0, + poly: pp, + n: n as u8, + bbox: [bb[0] - 1.0, bb[1] - 1.0, bb[2] + 1.0, bb[3] + 1.0], + shade: 1.0, + tint: [1.0; 3], + spec: [0.0; 3], + alpha: 1.0, + flat: None, + back: facing < 0.0, + back_mirror: 0, + depth, + }) + } + + /// Lambert factor of the piece normal against a light shining along +z. + pub(super) fn facing(r: &M3) -> f32 { + r[2][2].abs() as f32 + } + + /// Light the piece (rotation `r`, centre `pos`) with Lighting / Material. + pub(crate) fn light(&mut self, l: &Lighting, r: &M3, pos: [f64; 3], eye: [f64; 3]) { + let (t, s) = l.shade([-r[0][2], -r[1][2], -r[2][2]], pos, eye); + self.tint = t; + self.spec = s; + } +} + +impl From for Proj { + fn from(c: Cam) -> Proj { + c.proj() + } +} + +/// Draw pieces (already sorted far → near) over `out`. +pub(crate) fn draw_pieces(out: &mut Image, pieces: &[Piece], front: &Image, back: Option<&Image>) { + raster( + out, + pieces, + |q| Some(q.bbox), + |q, x, y| { + let (xf, yf) = (x as f64, y as f64); + let m = &q.inv; + let w = m[2][0] * xf + m[2][1] * yf + m[2][2]; + if w.abs() < 1e-12 { + return None; + } + let u = ((m[0][0] * xf + m[0][1] * yf + m[0][2]) / w) as f32 + q.c0[0]; + let v = ((m[1][0] * xf + m[1][1] * yf + m[1][2]) / w) as f32 + q.c0[1]; + let n = q.n as usize; + let mut md = f32::INFINITY; + for i in 0..n { + let a = q.poly[i]; + let b = q.poly[(i + 1) % n]; + let (ex, ey) = (b[0] - a[0], b[1] - a[1]); + let l = (ex * ex + ey * ey).sqrt().max(1e-6); + md = md.min((ex * (v - a[1]) - ey * (u - a[0])) / l); + } + // Coverage extends half a pixel past the edge so abutting pieces leave no seams. + let cov = (md + 1.0).clamp(0.0, 1.0); + if cov <= 0.0 { + return None; + } + let (c, a) = match q.flat { + Some(c) => (c, 1.0), + None => { + let tex = if q.back { back.unwrap_or(front) } else { front }; + let (u, v) = match (q.back, q.back_mirror) { + (true, 1) => (u, 2.0 * q.c0[1] - v), + (true, 2) => (2.0 * q.c0[0] - u, v), + _ => (u, v), + }; + unpremul(tex.sample_bilinear(u as f64, v as f64)) + } + }; + let a = a * cov * q.alpha; + if a <= 1e-6 { + return None; + } + let k = q.shade; + let o: [f32; 3] = std::array::from_fn(|i| c[i] * k * q.tint[i] + q.spec[i]); + Some([o[0] * a, o[1] * a, o[2] * a, a]) + }, + Acc::Over, + ); +} + +pub(crate) fn sort_far_first(pieces: &mut [Piece]) { + pieces.sort_by(|a, b| b.depth.total_cmp(&a.depth)); +} diff --git a/crates/effects/src/sim2/pixels.rs b/crates/effects/src/sim2/pixels.rs new file mode 100644 index 000000000..2079f03c3 --- /dev/null +++ b/crates/effects/src/sim2/pixels.rs @@ -0,0 +1,228 @@ +use super::pieces::{Cam, Piece, PiecePlan, PieceTex, rot_axis, sort_far_first}; +use super::shatter::{ShatterPlan, shatter_impl}; +use super::*; + +// ------------------------------------------------------------------ CC Ball Action + +pub(super) fn ball_action(ctx: &EffectCtx, b: Buf) -> Buf { + ball_action_plan(ctx, &b).finish(b) +} + +/// CC Ball Action's balls (far to near), coloured by the layer's pixels. +fn ball_action_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan { + let pr = ctx.params; + let scatter = pr.f("scatter") as f32; + let axis = pr.e("rotationAxis"); + let rot = pr.f("rotation").to_radians(); + let twist_prop = pr.e("twistProperty"); + let twist = pr.f("twistAngle").to_radians(); + let spacing = pr.f("gridSpacing").max(1.0) as f32; + let size = pr.f("ballSize") as f32 / 100.0; + let instab = pr.f("instabilityState").to_radians() as f32; + let seed = ctx.seed.wrapping_mul(0x2545f491); + let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32); + let s = b.scale as f32; + let nx = (lw / spacing).ceil().max(1.0) as u32; + let ny = (lh / spacing).ceil().max(1.0) as u32; + let d = 2.0 * lw.max(lh) as f64; + let (cx, cy) = (lw as f64 * 0.5, lh as f64 * 0.5); + let src = &b.img; + let ax: [f64; 3] = match axis { + 0 => [1.0, 0.0, 0.0], + 1 => [0.0, 1.0, 0.0], + 2 => [0.0, 0.0, 1.0], + 3 => [1.0, 1.0, 0.0], + 4 => [1.0, 0.0, 1.0], + 5 => [0.0, 1.0, 1.0], + _ => [1.0, 1.0, 1.0], + }; + let maxr = ((lw * lw + lh * lh).sqrt() * 0.5).max(1.0); + let mut balls: Vec<(f64, Sprite)> = (0..nx * ny) + .into_par_iter() + .filter_map(|i| { + let gx = (i % nx) as f32 * spacing + spacing * 0.5; + let gy = (i / nx) as f32 * spacing + spacing * 0.5; + let (sx, sy) = b.to_px([gx as f64, gy as f64]); + let (c, a) = unpremul(src.sample_bilinear(sx, sy)); + if a <= 0.0 || size <= 0.0 { + return None; + } + let tp = match twist_prop { + 0 => gx / lw.max(1.0), + 1 => gy / lh.max(1.0), + 2 => ((gx - cx as f32).hypot(gy - cy as f32)) / maxr, + _ => crate::util::rgb3([c[0], c[1], c[2], 0.0]).iter().sum::() / 3.0, + } as f64; + let ang = rot + twist * tp; + // Scatter: random 3D offsets whose direction drifts with the instability state. + let ph = h(i, 1, seed) * std::f32::consts::TAU + instab; + let sc = scatter * h(i, 2, seed); + let off = [(sc * ph.cos()) as f64, (sc * ph.sin()) as f64, (scatter * hs(i, 3, seed)) as f64]; + let p0 = [gx as f64 - cx + off[0], gy as f64 - cy + off[1], off[2]]; + let m = rot_axis(ax, ang); + let q: [f64; 3] = std::array::from_fn(|k| m[k][0] * p0[0] + m[k][1] * p0[1] + m[k][2] * p0[2]); + if d + q[2] <= d * 0.05 { + return None; + } + let persp = d / (d + q[2]); + let (bx, by) = b.to_px([cx + q[0] * persp, cy + q[1] * persp]); + let r = spacing * 0.5 * size * persp as f32 * s; + Some((q[2], Sprite::new(bx as f32, by as f32, r, [c[0], c[1], c[2], a], Shape::Sphere))) + }) + .collect(); + balls.sort_by(|a, b| b.0.total_cmp(&a.0)); + let sprites: Vec = balls.into_iter().map(|(_, s)| s).collect(); + SpritePlan { sprites, tints: vec![], acc: Acc::Over, post: Post::Replace } +} + +// ------------------------------------------------------------------ CC Pixel Polly + +pub(super) fn pixel_polly(ctx: &EffectCtx, b: Buf) -> Buf { + match pixel_polly_plan(ctx, &b) { + Some(plan) => plan.finish(b), + None => b, + } +} + +/// CC Pixel Polly's pieces, `None` before Start Time (the layer as is). +fn pixel_polly_plan(ctx: &EffectCtx, b: &Buf) -> Option { + let pr = ctx.params; + let t = ctx.time - pr.f("startTime"); + if t <= 0.0 { + return None; + } + let force = pr.f("force"); + let gravity = pr.f("gravity"); + let spin = pr.f("spinning").to_radians(); + let fc = pr.v2("forceCenter"); + let dir_rand = pr.f("directionRandomness") / 100.0; + let spd_rand = pr.f("speedRandomness") / 100.0; + let g = pr.f("gridSpacing").max(1.0); + let object = pr.e("object"); + let depth_sort = pr.b("enableDepthSort"); + let seed = ctx.seed.wrapping_mul(0x632be5ab); + let [lw, lh] = ctx.layer_size; + let s = b.scale; + let cam = Cam { c: [b.offset[0] + lw * 0.5 * s, b.offset[1] + lh * 0.5 * s], d: 2.0 * lw.max(lh) * s }; + let nx = (lw / g).ceil().max(1.0) as u32; + let ny = (lh / g).ceil().max(1.0) as u32; + let tri = object <= 1; + let textured = object == 1 || object == 3; + let maxd = lw.max(lh); + let src = &b.img; + let per = if tri { 2 } else { 1 }; + let mut pieces: Vec = (0..nx * ny * per) + .into_par_iter() + .filter_map(|i| { + let cell = i / per; + let (x0, y0) = ((cell % nx) as f64 * g, (cell / nx) as f64 * g); + let poly: Vec<[f64; 2]> = if !tri { + vec![[x0, y0], [x0 + g, y0], [x0 + g, y0 + g], [x0, y0 + g]] + } else if i % 2 == 0 { + vec![[x0, y0], [x0 + g, y0], [x0, y0 + g]] + } else { + vec![[x0 + g, y0], [x0 + g, y0 + g], [x0, y0 + g]] + }; + let c = [poly.iter().map(|v| v[0]).sum::() / poly.len() as f64, poly.iter().map(|v| v[1]).sum::() / poly.len() as f64]; + let (dx, dy) = (c[0] - fc[0], c[1] - fc[1]); + let dist = (dx * dx + dy * dy).sqrt().max(1e-6); + let ang = dy.atan2(dx) + dir_rand * std::f64::consts::PI * hs(i, 1, seed) as f64; + let speed = force / 100.0 * 0.6 * maxd / (1.0 + dist / (0.5 * maxd)) * (1.0 + spd_rand * hs(i, 2, seed) as f64); + let vz = -speed * 0.5 * h(i, 3, seed) as f64; + let pos = [c[0] + ang.cos() * speed * t, c[1] + ang.sin() * speed * t + 0.5 * gravity * 0.5 * lh * t * t, vz * t]; + let axis = [hs(i, 4, seed) as f64, hs(i, 5, seed) as f64, hs(i, 6, seed) as f64]; + let r = rot_axis(axis, t * (std::f64::consts::PI * h(i, 7, seed) as f64 + spin)); + let tex: Vec<[f32; 2]> = poly.iter().map(|v| [(v[0] * s + b.offset[0]) as f32, (v[1] * s + b.offset[1]) as f32]).collect(); + let c0 = [(c[0] * s + b.offset[0]) as f32, (c[1] * s + b.offset[1]) as f32]; + let posb = [pos[0] * s + b.offset[0], pos[1] * s + b.offset[1], pos[2] * s]; + let mut pc = Piece::new(&tex, c0, &r, posb, cam)?; + pc.shade = 0.35 + 0.65 * Piece::facing(&r); + if !textured { + let (cc, a) = unpremul(src.sample_bilinear(c0[0] as f64, c0[1] as f64)); + if a <= 0.0 { + return None; + } + pc.flat = Some(cc); + pc.alpha = a; + } + Some(pc) + }) + .collect(); + if depth_sort { + sort_far_first(&mut pieces); + } + Some(PiecePlan { pieces, front: PieceTex::Layer, back: None }) +} + +// ------------------------------------------------------------------ CC Scatterize + +pub(super) fn scatterize(ctx: &EffectCtx, b: Buf) -> Buf { + match scatterize_plan(ctx, &b) { + Some(plan) => plan.finish(b), + None => b, + } +} + +/// CC Scatterize's pixel dots, `None` when nothing moves (the layer as is). +fn scatterize_plan(ctx: &EffectCtx, b: &Buf) -> Option { + let pr = ctx.params; + let scatter = pr.f("scatter") as f32; + let right = pr.f("rightTwist").to_radians(); + let left = pr.f("leftTwist").to_radians(); + if scatter == 0.0 && right == 0.0 && left == 0.0 { + return None; + } + let add = pr.e("transferMode") == 1; + let seed = ctx.seed.wrapping_mul(0x7feb352d); + let s = b.scale; + let (w, hh) = (b.img.width, b.img.height); + let (cx, cy) = (b.offset[0] + ctx.layer_size[0] * 0.5 * s, b.offset[1] + ctx.layer_size[1] * 0.5 * s); + let half = (ctx.layer_size[0] * 0.5 * s).max(1.0); + let d = 2.0 * ctx.layer_size[0].max(ctx.layer_size[1]) * s; + let src = &b.img; + let sprites: Vec = (0..w * hh) + .into_par_iter() + .filter_map(|i| { + let px = src.data[i as usize]; + if px[3] <= 0.0 { + return None; + } + let (x, y) = ((i % w) as f64 + 0.5, (i / w) as f64 + 0.5); + // Twist about the horizontal axis through the centre, growing towards each side. + let k = ((x - cx) / half).abs().min(1.0); + let ang = if x < cx { left } else { right } * k; + let (sa, ca) = ang.sin_cos(); + let (yy, zz) = ((y - cy) * ca, (y - cy) * sa); + let persp = d / (d + zz).max(d * 0.05); + let jx = hs(i, 1, seed) * scatter * s as f32; + let jy = hs(i, 2, seed) * scatter * s as f32; + let sx = cx + (x - cx) * persp + jx as f64; + let sy = cy + yy * persp + jy as f64; + let (c, a) = unpremul(px); + Some(Sprite::new(sx as f32, sy as f32, 0.6 * persp as f32, [c[0], c[1], c[2], a], Shape::Disc)) + }) + .collect(); + Some(SpritePlan { sprites, tints: vec![], acc: if add { Acc::Add } else { Acc::Over }, post: Post::Replace }) +} + +/// What a particle effect that reads the layer's pixels draws this frame (CC Ball Action, CC +/// Pixel Polly, CC Scatterize, Shatter's wireframe views), for layer `b`; `None` for other +/// effects or a frame showing the layer as is. +pub enum PixelPlan { + Sprites(SpritePlan), + Pieces(PiecePlan), +} + +/// The [`PixelPlan`] of effect `id` for layer `b` (its pixels are read). +pub fn pixel_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option { + match id { + "ec.sim.ccballaction" => Some(PixelPlan::Sprites(ball_action_plan(ctx, b))), + "ec.sim.ccpixelpolly" => pixel_polly_plan(ctx, b).map(PixelPlan::Pieces), + "ec.sim.ccscatterize" => scatterize_plan(ctx, b).map(PixelPlan::Sprites), + "ec.sim.shatter" => Some(match shatter_impl(ctx, b) { + ShatterPlan::Pieces(plan) => PixelPlan::Pieces(plan), + ShatterPlan::Wire(plan) => PixelPlan::Sprites(plan), + }), + _ => None, + } +} diff --git a/crates/effects/src/sim2/registry.rs b/crates/effects/src/sim2/registry.rs new file mode 100644 index 000000000..93508e7b5 --- /dev/null +++ b/crates/effects/src/sim2/registry.rs @@ -0,0 +1,288 @@ +use super::foam::{FOAM_TEXTURES, foam}; +use super::pixels::{ball_action, pixel_polly, scatterize}; +use super::shatter::{CD_PROPS, CD_SOURCES, SHATTER_PATTERNS, SHATTER_TEXTURE_MODES, card_dance, shatter}; +use super::surfaces::{caustics, wave_world}; +use super::*; + +// ------------------------------------------------------------------ specs + +fn producer_params(k: u32, amp: f64, pos: (f64, f64)) -> Vec { + let ids: [&'static str; 8] = match k { + 1 => [ + "producer1/producer1Type", + "producer1/producer1Position", + "producer1/producer1Length", + "producer1/producer1Width", + "producer1/producer1Angle", + "producer1/producer1Amplitude", + "producer1/producer1Frequency", + "producer1/producer1Phase", + ], + _ => [ + "producer2/producer2Type", + "producer2/producer2Position", + "producer2/producer2Length", + "producer2/producer2Width", + "producer2/producer2Angle", + "producer2/producer2Amplitude", + "producer2/producer2Frequency", + "producer2/producer2Phase", + ], + }; + vec![ + p(ids[0], "Type", Value::Enum(0), popup(&["Ring", "Line"])), + p(ids[1], "Position", pt(pos.0, pos.1), ParamUi::Point), + p(ids[2], "Height/Length", num(0.1), slider(0.0, 2.0, 0.0, 1.0, 3)), + p(ids[3], "Width", num(0.1), slider(0.0, 2.0, 0.0, 1.0, 3)), + p(ids[4], "Angle", num(0.0), ParamUi::Angle), + p(ids[5], "Amplitude", num(amp), slider(-5.0, 5.0, -1.0, 1.0, 3)), + p(ids[6], "Frequency", num(1.0), slider(0.0, 30.0, 0.0, 5.0, 3)), + p(ids[7], "Phase", num(0.0), ParamUi::Angle), + ] +} + +fn card_dance_params() -> Vec { + let mut v = vec![ + p("rowsColumns", "Rows & Columns", Value::Enum(0), popup(&["Independent", "Columns Follows Rows"])), + p("rows", "Rows", num(10.0), slider(1.0, 1000.0, 1.0, 100.0, 0)), + p("columns", "Columns", num(10.0), slider(1.0, 1000.0, 1.0, 100.0, 0)), + p("backLayer", "Back Layer", Value::Layer(None), ParamUi::Layer), + p("gradientLayer1", "Gradient Layer 1", Value::Layer(None), ParamUi::Layer), + p("gradientLayer2", "Gradient Layer 2", Value::Layer(None), ParamUi::Layer), + ]; + let names: [[&'static str; 4]; 8] = [ + ["xPosition/xPosSource", "xPosition/xPosMultiplier", "xPosition/xPosOffset", "X Position"], + ["yPosition/yPosSource", "yPosition/yPosMultiplier", "yPosition/yPosOffset", "Y Position"], + ["zPosition/zPosSource", "zPosition/zPosMultiplier", "zPosition/zPosOffset", "Z Position"], + ["xRotation/xRotSource", "xRotation/xRotMultiplier", "xRotation/xRotOffset", "X Rotation"], + ["yRotation/yRotSource", "yRotation/yRotMultiplier", "yRotation/yRotOffset", "Y Rotation"], + ["zRotation/zRotSource", "zRotation/zRotMultiplier", "zRotation/zRotOffset", "Z Rotation"], + ["xScale/xScaleSource", "xScale/xScaleMultiplier", "xScale/xScaleOffset", "X Scale"], + ["yScale/yScaleSource", "yScale/yScaleMultiplier", "yScale/yScaleOffset", "Y Scale"], + ]; + debug_assert_eq!(names.len(), CD_PROPS.len()); + for (i, n) in names.iter().enumerate() { + let rot = (3..6).contains(&i); + let scale = i >= 6; + let (lo, hi) = if rot { (-3600.0, 3600.0) } else { (-100.0, 100.0) }; + v.push(p(n[0], "Source", Value::Enum(0), popup(CD_SOURCES))); + v.push(p(n[1], "Multiplier", num(0.0), slider(lo, hi, if rot { -360.0 } else { -10.0 }, if rot { 360.0 } else { 10.0 }, 2))); + v.push(p(n[2], "Offset", num(if scale { 1.0 } else { 0.0 }), slider(lo, hi, if rot { -360.0 } else { -10.0 }, if rot { 360.0 } else { 10.0 }, 2))); + } + v.extend(crate::card3d::camera_params(2.0)); + v.extend(crate::card3d::lighting_params(1.0, 0.25)); + v.extend(crate::card3d::material_params(0.75)); + v +} + +pub fn specs() -> Vec { + vec![ + spec( + "ec.sim.ccballaction", + "CC Ball Action", + vec![ + p("scatter", "Scatter", num(0.0), slider(0.0, 2000.0, 0.0, 300.0, 1)), + p("rotationAxis", "Rotation Axis", Value::Enum(0), popup(&["X Axis", "Y Axis", "Z Axis", "XY Axis", "XZ Axis", "YZ Axis", "XYZ Axis"])), + p("rotation", "Rotation", num(0.0), ParamUi::Angle), + p("twistProperty", "Twist Property", Value::Enum(0), popup(&["X Axis", "Y Axis", "Radius", "Brightness"])), + p("twistAngle", "Twist Angle", num(0.0), ParamUi::Angle), + p("gridSpacing", "Grid Spacing", num(4.0), slider(1.0, 200.0, 1.0, 50.0, 0)), + p("ballSize", "Ball Size", num(40.0), slider(0.0, 400.0, 0.0, 200.0, 1)), + p("instabilityState", "Instability State", num(0.0), ParamUi::Angle), + ], + ball_action, + ), + spec( + "ec.sim.ccpixelpolly", + "CC Pixel Polly", + vec![ + p("force", "Force", num(100.0), slider(-1000.0, 1000.0, -300.0, 300.0, 1)), + p("gravity", "Gravity", num(1.0), slider(-20.0, 20.0, -5.0, 5.0, 2)), + p("spinning", "Spinning", num(0.0), ParamUi::Angle), + p("forceCenter", "Force Center", pt(0.5, 0.5), ParamUi::Point), + p("directionRandomness", "Direction Randomness", num(10.0), pct()), + p("speedRandomness", "Speed Randomness", num(10.0), pct()), + p("gridSpacing", "Grid Spacing", num(10.0), slider(1.0, 500.0, 2.0, 100.0, 0)), + p("object", "Object", Value::Enum(0), popup(&["Polygon", "Textured Polygon", "Square", "Textured Square"])), + p("enableDepthSort", "Enable Depth Sort", Value::Bool(true), ParamUi::Checkbox), + p("startTime", "Start Time (sec)", num(0.0), slider(-1000.0, 1000.0, 0.0, 10.0, 2)), + ], + pixel_polly, + ), + spec( + "ec.sim.ccscatterize", + "CC Scatterize", + vec![ + p("scatter", "Scatter", num(0.0), slider(0.0, 1000.0, 0.0, 200.0, 1)), + p("rightTwist", "Right Twist", num(0.0), ParamUi::Angle), + p("leftTwist", "Left Twist", num(0.0), ParamUi::Angle), + p("transferMode", "Transfer Mode", Value::Enum(0), popup(&["Composite", "Add"])), + ], + scatterize, + ), + spec("ec.sim.carddance", "Card Dance", card_dance_params(), card_dance), + spec( + "ec.sim.caustics", + "Caustics", + vec![ + p("bottom/bottom", "Bottom", Value::Layer(None), ParamUi::Layer), + p("bottom/scaling", "Scaling", num(1.0), slider(0.01, 10.0, 0.1, 3.0, 3)), + p("bottom/repeatMode", "Repeat Mode", Value::Enum(2), popup(&["Once", "Tiled", "Reflected"])), + p("bottom/bottomSizeDiffers", "If Layer Size Differs", Value::Enum(1), popup(&["Center", "Stretch to Fit"])), + p("bottom/blur", "Blur", num(0.0), slider(0.0, 100.0, 0.0, 20.0, 1)), + p("water/waterSurface", "Water Surface", Value::Layer(None), ParamUi::Layer), + p("water/waveHeight", "Wave Height", num(0.2), slider(-1.0, 1.0, -1.0, 1.0, 3)), + p("water/smoothing", "Smoothing", num(5.0), slider(0.0, 100.0, 0.0, 50.0, 1)), + p("water/waterDepth", "Water Depth", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)), + p("water/refractiveIndex", "Refractive Index", num(1.2), slider(1.0, 3.0, 1.0, 2.0, 3)), + p("water/surfaceColor", "Surface Color", col(1.0, 1.0, 1.0), ParamUi::Color), + p("water/surfaceOpacity", "Surface Opacity", num(0.3), slider(0.0, 1.0, 0.0, 1.0, 3)), + p("water/causticsStrength", "Caustics Strength", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)), + p("sky/sky", "Sky", Value::Layer(None), ParamUi::Layer), + p("sky/scaling", "Scaling", num(1.0), slider(0.01, 10.0, 0.1, 3.0, 3)), + p("sky/repeatMode", "Repeat Mode", Value::Enum(2), popup(&["Once", "Tiled", "Reflected"])), + p("sky/skySizeDiffers", "If Layer Size Differs", Value::Enum(1), popup(&["Center", "Stretch to Fit"])), + p("sky/intensity", "Intensity", num(0.3), slider(0.0, 1.0, 0.0, 1.0, 3)), + p("sky/convergence", "Convergence", num(0.5), slider(0.0, 1.0, 0.0, 1.0, 3)), + p("lighting/lightType", "Light Type", Value::Enum(0), popup(&crate::card3d::LIGHT_TYPES)), + p("lighting/lightIntensity", "Light Intensity", num(1.0), slider(0.0, 4.0, 0.0, 2.0, 2)), + p("lighting/lightColor", "Light Color", col(1.0, 1.0, 1.0), ParamUi::Color), + p("lighting/lightPosition", "Light Position", pt(0.0, 0.0), ParamUi::Point), + p("lighting/lightHeight", "Light Height", num(1.0), slider(0.0, 10.0, 0.0, 4.0, 2)), + p("lighting/ambientLight", "Ambient Light", num(0.35), slider(0.0, 2.0, 0.0, 1.0, 2)), + p("material/diffuse", "Diffuse Reflection", num(0.75), slider(0.0, 2.0, 0.0, 1.0, 2)), + p("material/specular", "Specular Reflection", num(0.2), slider(0.0, 2.0, 0.0, 1.0, 2)), + p("material/highlightSharpness", "Highlight Sharpness", num(15.0), slider(1.0, 100.0, 1.0, 100.0, 1)), + ], + caustics, + ), + spec( + "ec.sim.waveworld", + "Wave World", + { + let mut v = vec![ + p("view", "View", Value::Enum(0), popup(&["Wireframe Preview", "Height Map"])), + p("wireframeControls/horizontalRotation", "Horizontal Rotation", num(0.0), ParamUi::Angle), + p("wireframeControls/verticalRotation", "Vertical Rotation", num(53.0), slider(0.0, 90.0, 0.0, 90.0, 1)), + p("wireframeControls/verticalScale", "Vertical Scale", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 3)), + p("heightMapControls/brightness", "Brightness", num(0.5), slider(-1.0, 2.0, 0.0, 1.0, 3)), + p("heightMapControls/contrast", "Contrast", num(0.75), slider(0.0, 4.0, 0.0, 2.0, 3)), + p("heightMapControls/gamma", "Gamma Adjustment", num(1.0), slider(0.01, 10.0, 0.2, 5.0, 3)), + p("heightMapControls/renderDryAreasAs", "Render Dry Areas As", Value::Enum(0), popup(&["Solid", "Transparent"])), + p("heightMapControls/transparency", "Transparency", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)), + p("simulation/gridResolution", "Grid Resolution", num(60.0), slider(1.0, 400.0, 1.0, 200.0, 0)), + p("simulation/gridResDownsamples", "Grid Res Downsamples", Value::Bool(false), ParamUi::Checkbox), + p("simulation/waveSpeed", "Wave Speed", num(0.2), slider(0.0, 5.0, 0.0, 1.0, 3)), + p("simulation/damping", "Damping", num(0.05), slider(0.0, 5.0, 0.0, 1.0, 3)), + p("simulation/reflectEdges", "Reflect Edges", Value::Enum(0), popup(&["None", "Left", "Top", "Right", "Bottom", "All"])), + p("simulation/preRoll", "Pre-roll (seconds)", num(0.0), slider(0.0, 30.0, 0.0, 10.0, 2)), + p("ground/ground", "Ground", Value::Layer(None), ParamUi::Layer), + p("ground/steepness", "Steepness", num(0.25), slider(0.0, 2.0, 0.0, 1.0, 3)), + p("ground/height", "Height", num(0.25), slider(0.0, 2.0, 0.0, 1.0, 3)), + p("ground/waveStrength", "Wave Strength", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)), + ]; + v.extend(producer_params(1, 0.5, (0.5, 0.5))); + v.extend(producer_params(2, 0.0, (0.25, 0.25))); + v + }, + wave_world, + ), + spec( + "ec.sim.shatter", + "Shatter", + vec![ + p( + "view", + "View", + Value::Enum(1), + popup(&["Rendered", "Wireframe Front View", "Wireframe", "Wireframe Front View + Forces", "Wireframe + Forces"]), + ), + p("render", "Render", Value::Enum(0), popup(&["All", "Layer", "Pieces"])), + p("shape/pattern", "Pattern", Value::Enum(0), popup(SHATTER_PATTERNS)), + p("shape/repetitions", "Repetitions", num(10.0), slider(1.0, 500.0, 1.0, 100.0, 2)), + p("shape/direction", "Direction", num(0.0), ParamUi::Angle), + p("shape/origin", "Origin", pt(0.5, 0.5), ParamUi::Point), + p("shape/customShatterMap", "Custom Shatter Map", Value::Layer(None), ParamUi::Layer), + p("shape/whiteTilesFixed", "White Tiles Fixed", Value::Bool(false), ParamUi::Checkbox), + p("shape/extrusionDepth", "Extrusion Depth", num(0.05), slider(0.0, 1.0, 0.0, 1.0, 3)), + p("force1/force1Position", "Position", pt(0.5, 0.5), ParamUi::Point), + p("force1/force1Depth", "Depth", num(0.1), slider(-1.0, 1.0, -1.0, 1.0, 3)), + p("force1/force1Radius", "Radius", num(0.4), slider(0.0, 4.0, 0.0, 2.0, 3)), + p("force1/force1Strength", "Strength", num(5.0), slider(-20.0, 20.0, -10.0, 10.0, 2)), + p("force2/force2Position", "Position", pt(0.25, 0.25), ParamUi::Point), + p("force2/force2Depth", "Depth", num(0.1), slider(-1.0, 1.0, -1.0, 1.0, 3)), + p("force2/force2Radius", "Radius", num(0.0), slider(0.0, 4.0, 0.0, 2.0, 3)), + p("force2/force2Strength", "Strength", num(5.0), slider(-20.0, 20.0, -10.0, 10.0, 2)), + p("physics/rotationSpeed", "Rotation Speed", num(0.2), slider(0.0, 5.0, 0.0, 1.0, 3)), + p("physics/tumbleAxis", "Tumble Axis", Value::Enum(0), popup(&["Free", "None", "X", "Y", "Z", "XY", "XZ", "YZ"])), + p("physics/randomness", "Randomness", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)), + p("physics/viscosity", "Viscosity", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)), + p("physics/massVariance", "Mass Variance", num(30.0), pct()), + p("physics/gravity", "Gravity", num(3.0), slider(0.0, 100.0, 0.0, 10.0, 2)), + p("physics/gravityDirection", "Gravity Direction", num(180.0), ParamUi::Angle), + p("physics/gravityInclination", "Gravity Inclination", num(0.0), slider(-90.0, 90.0, -90.0, 90.0, 1)), + p("gradient/shatterThreshold", "Shatter Threshold", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)), + p("gradient/gradientLayer", "Gradient Layer", Value::Layer(None), ParamUi::Layer), + p("gradient/invertGradient", "Invert Gradient", Value::Bool(false), ParamUi::Checkbox), + p("textures/color", "Color", col(1.0, 1.0, 1.0), ParamUi::Color), + p("textures/opacity", "Opacity", num(1.0), slider(0.0, 1.0, 0.0, 1.0, 3)), + p("textures/frontMode", "Front Mode", Value::Enum(1), popup(&SHATTER_TEXTURE_MODES)), + p("textures/sideMode", "Side Mode", Value::Enum(0), popup(&SHATTER_TEXTURE_MODES)), + p("textures/backMode", "Back Mode", Value::Enum(1), popup(&SHATTER_TEXTURE_MODES)), + p("textures/frontLayer", "Front Layer", Value::Layer(None), ParamUi::Layer), + p("textures/sideLayer", "Side Layer", Value::Layer(None), ParamUi::Layer), + p("textures/backLayer", "Back Layer", Value::Layer(None), ParamUi::Layer), + ] + .into_iter() + .chain(crate::card3d::camera_params(4.0)) + .chain(crate::card3d::lighting_params(1.0, 0.25)) + .chain(crate::card3d::material_params(0.75)) + .chain([p("randomSeed", "Random Seed", num(0.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0))]) + .collect(), + shatter, + ), + spec( + "ec.sim.foam", + "Foam", + vec![ + p("view", "View", Value::Enum(0), popup(&["Draft", "Draft + Flow Map", "Rendered"])), + p("producer/producerPoint", "Producer Point", pt(0.5, 0.5), ParamUi::Point), + p("producer/producerXSize", "Producer X Size", num(0.05), slider(0.0, 2.0, 0.0, 1.0, 3)), + p("producer/producerYSize", "Producer Y Size", num(0.05), slider(0.0, 2.0, 0.0, 1.0, 3)), + p("producer/producerOrientation", "Producer Orientation", num(0.0), ParamUi::Angle), + p("producer/zoomProducerPoint", "Zoom Producer Point", Value::Bool(false), ParamUi::Checkbox), + p("producer/productionRate", "Production Rate", num(1.0), slider(0.0, 50.0, 0.0, 10.0, 3)), + p("bubbles/size", "Size", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 3)), + p("bubbles/sizeVariance", "Size Variance", num(0.5), slider(0.0, 1.0, 0.0, 1.0, 3)), + p("bubbles/lifespan", "Lifespan", num(300.0), slider(1.0, 30_000.0, 1.0, 1000.0, 1)), + p("bubbles/growthSpeed", "Bubble Growth Speed", num(0.1), slider(0.0001, 10.0, 0.0001, 1.0, 3)), + p("bubbles/strength", "Strength", num(10.0), slider(0.0, 100.0, 0.0, 20.0, 2)), + p("physics/initialSpeed", "Initial Speed", num(0.0), slider(-100.0, 100.0, -10.0, 10.0, 3)), + p("physics/initialDirection", "Initial Direction", num(0.0), ParamUi::Angle), + p("physics/windSpeed", "Wind Speed", num(0.5), slider(-100.0, 100.0, -10.0, 10.0, 3)), + p("physics/windDirection", "Wind Direction", num(90.0), ParamUi::Angle), + p("physics/turbulence", "Turbulence", num(0.5), slider(0.0, 10.0, 0.0, 2.0, 3)), + p("physics/wobbleAmount", "Wobble Amount", num(0.05), slider(0.0, 10.0, 0.0, 2.0, 3)), + p("physics/repulsion", "Repulsion", num(1.0), slider(0.0, 10.0, 0.0, 2.0, 3)), + p("physics/popVelocity", "Pop Velocity", num(1.0), slider(-100.0, 100.0, -10.0, 10.0, 3)), + p("physics/viscosity", "Viscosity", num(0.1), slider(0.0, 1.0, 0.0, 1.0, 3)), + p("physics/stickiness", "Stickiness", num(0.75), slider(0.0, 1.0, 0.0, 1.0, 3)), + p("zoom", "Zoom", num(1.0), slider(0.01, 50.0, 0.1, 5.0, 3)), + p("universeSize", "Universe Size", num(1.0), slider(0.01, 10.0, 0.5, 3.0, 3)), + p("rendering/blendMode", "Blend Mode", Value::Enum(0), popup(&["Transparent", "Solid Old on Top", "Solid New on Top"])), + p("rendering/bubbleTexture", "Bubble Texture", Value::Enum(0), popup(&FOAM_TEXTURES)), + p("rendering/bubbleTextureLayer", "Bubble Texture Layer", Value::Layer(None), ParamUi::Layer), + p("rendering/bubbleOrientation", "Bubble Orientation", Value::Enum(0), popup(&["Fixed", "Physical Orientation", "Bubble Velocity"])), + p("rendering/environmentMap", "Environment Map", Value::Layer(None), ParamUi::Layer), + p("rendering/reflectionStrength", "Reflection Strength", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)), + p("rendering/reflectionConvergence", "Reflection Convergence", num(0.0), slider(0.0, 1.0, 0.0, 1.0, 3)), + p("flowMap/flowMap", "Flow Map", Value::Layer(None), ParamUi::Layer), + p("flowMap/flowMapSteepness", "Flow Map Steepness", num(0.05), slider(-100.0, 100.0, -1.0, 1.0, 3)), + p("flowMap/flowMapFits", "Flow Map Fits", Value::Enum(0), popup(&["Background", "Universe"])), + p("flowMap/simulationQuality", "Simulation Quality", Value::Enum(0), popup(&["Normal", "High", "Intense"])), + p("randomSeed", "Random Seed", num(1.0), slider(0.0, 10_000.0, 0.0, 1000.0, 0)), + ], + foam, + ), + ] +} diff --git a/crates/effects/src/sim2/shatter.rs b/crates/effects/src/sim2/shatter.rs new file mode 100644 index 000000000..90b5163f1 --- /dev/null +++ b/crates/effects/src/sim2/shatter.rs @@ -0,0 +1,481 @@ +use super::pieces::{M3, Piece, PiecePlan, PieceTex, inv3, rot_axis, rot_xyz, sort_far_first}; +use super::*; + +// ------------------------------------------------------------------ Card Dance + +pub(super) const CD_SOURCES: &[&str] = + &["None", "Intensity 1", "Red 1", "Green 1", "Blue 1", "Alpha 1", "Intensity 2", "Red 2", "Green 2", "Blue 2", "Alpha 2"]; + +/// (param id prefix, twirl-down group match id) per transformed card property. +pub(super) const CD_PROPS: [(&str, &str); 8] = [ + ("xPos", "xPosition"), + ("yPos", "yPosition"), + ("zPos", "zPosition"), + ("xRot", "xRotation"), + ("yRot", "yRotation"), + ("zRot", "zRotation"), + ("xScale", "xScale"), + ("yScale", "yScale"), +]; + +fn card_source(src: u32, g1: [f32; 4], g2: [f32; 4]) -> f32 { + let (c1, a1) = unpremul(g1); + let (c2, a2) = unpremul(g2); + let lum = |c: [f32; 3]| effectcraft_color::luminance(c[0], c[1], c[2]); + match src { + 1 => lum(c1), + 2 => c1[0], + 3 => c1[1], + 4 => c1[2], + 5 => a1, + 6 => lum(c2), + 7 => c2[0], + 8 => c2[1], + 9 => c2[2], + 10 => a2, + _ => 0.0, + } +} + +pub(super) fn card_dance(ctx: &EffectCtx, b: Buf) -> Buf { + card_dance_plan(ctx, &b).finish(b) +} + +pub(super) fn card_dance_plan(ctx: &EffectCtx, b: &Buf) -> PiecePlan { + let pr = ctx.params; + let rows = pr.f("rows").clamp(1.0, 1000.0) as u32; + let cols = if pr.e("rowsColumns") == 1 { rows } else { pr.f("columns").clamp(1.0, 1000.0) as u32 }; + let g1 = layer_or_self(ctx, b, "gradientLayer1", true, true); + let g2 = layer_or_self(ctx, b, "gradientLayer2", true, true); + let back = ctx.layer_param("backLayer", true).map(|o| crate::util::fit_layer(ctx, b, &o, true)); + let [lw, lh] = ctx.layer_size; + let s = b.scale; + let (cw, ch) = (lw / cols as f64, lh / rows as f64); + let cam = crate::card3d::projection(ctx, b); + let eye = cam.eye(); + let light = Lighting::from(ctx, b); + let props: Vec<(u32, f64, f64)> = + CD_PROPS.iter().map(|(id, g)| (pr.e(&format!("{g}/{id}Source")), pr.f(&format!("{g}/{id}Multiplier")), pr.f(&format!("{g}/{id}Offset")))).collect(); + let mut pieces: Vec = (0..rows * cols) + .into_par_iter() + .filter_map(|i| { + let (cx0, cy0) = ((i % cols) as f64 * cw, (i / cols) as f64 * ch); + let c = [cx0 + cw * 0.5, cy0 + ch * 0.5]; + let (bx, by) = (c[0] * s + b.offset[0], c[1] * s + b.offset[1]); + let (gp1, gp2) = (g1.sample_bilinear(bx, by), g2.sample_bilinear(bx, by)); + let val = |k: usize| { + let (sr, m, o) = props[k]; + o + m * card_source(sr, gp1, gp2) as f64 + }; + let (sx, sy) = (val(6), val(7)); + let r = rot_xyz(val(3).to_radians(), val(4).to_radians(), val(5).to_radians()); + let rs: M3 = std::array::from_fn(|k| [r[k][0] * sx, r[k][1] * sy, r[k][2]]); + let pos = [bx + val(0) * cw * s, by + val(1) * ch * s, val(2) * cw.max(ch) * s]; + let (tx0, ty0) = ((cx0 * s + b.offset[0]) as f32, (cy0 * s + b.offset[1]) as f32); + let (tw, th) = ((cw * s) as f32, (ch * s) as f32); + let poly = [[tx0, ty0], [tx0 + tw, ty0], [tx0 + tw, ty0 + th], [tx0, ty0 + th]]; + let mut pc = Piece::new(&poly, [bx as f32, by as f32], &rs, pos, cam)?; + pc.light(&light, &r, pos, eye); + Some(pc) + }) + .collect(); + sort_far_first(&mut pieces); + PiecePlan { pieces, front: PieceTex::Layer, back: back.map(PieceTex::Image) } +} + +// ------------------------------------------------------------------ Shatter + +/// Shatter's Pattern options; the last, Custom, takes its pieces from Custom Shatter Map. +pub(super) const SHATTER_PATTERNS: &[&str] = &["Bricks", "Glass", "Hexagons", "Squares", "Triangles", "Custom"]; + +/// Pattern cells (polygons in pattern space, before rotation) covering `[x0, x1]×[y0, y1]`. +fn pattern_cells(kind: u32, cell: f64, x0: f64, y0: f64, x1: f64, y1: f64, seed: u32) -> Vec> { + let mut out = Vec::new(); + let i0 = (x0 / cell).floor() as i64 - 1; + let i1 = (x1 / cell).ceil() as i64 + 1; + let j0 = (y0 / cell).floor() as i64 - 1; + let j1 = (y1 / cell).ceil() as i64 + 1; + match kind { + 0 => { + let bh = cell * 0.5; + let r0 = (y0 / bh).floor() as i64 - 1; + let r1 = (y1 / bh).ceil() as i64 + 1; + for r in r0..=r1 { + let sh = if r.rem_euclid(2) == 1 { cell * 0.5 } else { 0.0 }; + for i in i0..=i1 { + let (x, y) = (i as f64 * cell + sh, r as f64 * bh); + out.push(vec![[x, y], [x + cell, y], [x + cell, y + bh], [x, y + bh]]); + } + } + } + 1 => { + // Jittered lattice split into triangles along a random diagonal. + let jit = |i: i64, j: i64| -> [f64; 2] { + let (a, b) = (i as u32, j as u32); + [i as f64 * cell + hs(a, b.wrapping_add(17), seed) as f64 * cell * 0.35, j as f64 * cell + hs(a, b.wrapping_add(91), seed) as f64 * cell * 0.35] + }; + for j in j0..=j1 { + for i in i0..=i1 { + let (p00, p10, p11, p01) = (jit(i, j), jit(i + 1, j), jit(i + 1, j + 1), jit(i, j + 1)); + if h(i as u32, j as u32, seed ^ 0x55) < 0.5 { + out.push(vec![p00, p10, p11]); + out.push(vec![p00, p11, p01]); + } else { + out.push(vec![p00, p10, p01]); + out.push(vec![p10, p11, p01]); + } + } + } + } + 2 => { + let r = cell / 3f64.sqrt(); + let dx = 1.5 * r; + let dy = 3f64.sqrt() * r; + let c0 = (x0 / dx).floor() as i64 - 1; + let c1 = (x1 / dx).ceil() as i64 + 1; + let rr0 = (y0 / dy).floor() as i64 - 1; + let rr1 = (y1 / dy).ceil() as i64 + 1; + for ci in c0..=c1 { + for rj in rr0..=rr1 { + let cx = ci as f64 * dx; + let cy = (rj as f64 + if ci.rem_euclid(2) == 1 { 0.5 } else { 0.0 }) * dy; + out.push( + (0..6) + .map(|k| { + let a = k as f64 * std::f64::consts::PI / 3.0; + [cx + r * a.cos(), cy + r * a.sin()] + }) + .collect(), + ); + } + } + } + 3 => { + for j in j0..=j1 { + for i in i0..=i1 { + let (x, y) = (i as f64 * cell, j as f64 * cell); + out.push(vec![[x, y], [x + cell, y], [x + cell, y + cell], [x, y + cell]]); + } + } + } + _ => { + for j in j0..=j1 { + for i in i0..=i1 { + let (x, y) = (i as f64 * cell, j as f64 * cell); + if (i + j).rem_euclid(2) == 0 { + out.push(vec![[x, y], [x + cell, y], [x, y + cell]]); + out.push(vec![[x + cell, y], [x + cell, y + cell], [x, y + cell]]); + } else { + out.push(vec![[x, y], [x + cell, y], [x + cell, y + cell]]); + out.push(vec![[x, y], [x + cell, y + cell], [x, y + cell]]); + } + } + } + } + } + out +} + +/// `∫0^t e^{-ks} ds` and `∫0^t ∫0^u e^{-ks} ds du` (drag-damped ballistic factors). +fn drag_factors(k: f64, t: f64) -> (f64, f64) { + if k < 1e-6 { + (t, 0.5 * t * t) + } else { + let e = (-k * t).exp(); + ((1.0 - e) / k, t / k - (1.0 - e) / (k * k)) + } +} + +/// Shatter's Front / Side / Back Mode options. +pub(super) const SHATTER_TEXTURE_MODES: [&str; 3] = ["Color", "Layer", "Tinted Layer"]; + +/// One face's texture: a flat colour, a layer, or a layer tinted by the Textures colour. +struct Face { + mode: u32, + img: Option, +} + +impl Face { + fn from(ctx: &EffectCtx, b: &Buf, mode: &str, layer: &str, default_mode: u32) -> Face { + let mode = ctx.params.get(mode).map(|v| v.as_enum()).unwrap_or(default_mode); + // A face layer of None is the effect's own layer. + let img = (mode != 0).then(|| layer_or_self(ctx, b, layer, true, true)); + Face { mode, img } + } +} + +/// Group key of a Custom Shatter Map colour (pieces of one colour break as one). +fn map_key(c: [f32; 4]) -> u32 { + let (c, a) = unpremul(c); + if a < 0.5 { + return u32::MAX; + } + let q = |v: f32| ((v.clamp(0.0, 1.0) * 15.0).round() as u32) & 15; + q(c[0]) << 8 | q(c[1]) << 4 | q(c[2]) +} + +pub(super) fn shatter(ctx: &EffectCtx, b: Buf) -> Buf { + match shatter_impl(ctx, &b) { + ShatterPlan::Pieces(plan) => plan.finish(b), + ShatterPlan::Wire(plan) => plan.finish(b), + } +} + +/// What Shatter draws: the pieces (Rendered view) or the wireframe lines. +pub(super) enum ShatterPlan { + Pieces(PiecePlan), + Wire(SpritePlan), +} + +pub(super) fn shatter_impl(ctx: &EffectCtx, b: &Buf) -> ShatterPlan { + let pr = ctx.params; + let view = pr.e("view"); + let render = pr.e("render"); + let kind = pr.e("shape/pattern"); + let custom = kind as usize == SHATTER_PATTERNS.len() - 1; + let reps = pr.f("shape/repetitions").clamp(1.0, 500.0); + let dir = pr.f("shape/direction").to_radians(); + let origin = pr.v2("shape/origin"); + let forces = [ + (pr.v2("force1/force1Position"), pr.f("force1/force1Depth"), pr.f("force1/force1Radius"), pr.f("force1/force1Strength")), + (pr.v2("force2/force2Position"), pr.f("force2/force2Depth"), pr.f("force2/force2Radius"), pr.f("force2/force2Strength")), + ]; + let rot_speed = pr.f("physics/rotationSpeed"); + let tumble = pr.e("physics/tumbleAxis"); + let randomness = pr.f("physics/randomness"); + let k = pr.f("physics/viscosity").max(0.0) * 5.0; + let mass_var = pr.f("physics/massVariance") / 100.0; + let gravity = pr.f("physics/gravity"); + let gdir = pr.f("physics/gravityDirection").to_radians(); + let ginc = pr.f("physics/gravityInclination").to_radians(); + let seed = (pr.f("randomSeed") as u32).wrapping_mul(0x9e3779b1) ^ ctx.seed; + let [lw, lh] = ctx.layer_size; + let s = b.scale; + let t = ctx.time.max(0.0); + let unit = lw * 0.1; + let cell = lw / reps; + let extrude = pr.f("shape/extrusionDepth").max(0.0) * cell * s; + let cam = crate::card3d::projection(ctx, b); + let eye = cam.eye(); + let light = Lighting::from(ctx, b); + // Gradient: with a gradient layer only pieces at least as bright as 1 − Shatter Threshold + // (Invert Gradient: as dark) break. + let gradient = ctx.layer_param("gradient/gradientLayer", true).map(|o| crate::util::fit_layer(ctx, b, &o, true)); + let threshold = pr.get("gradient/shatterThreshold").map(Value::as_f64).unwrap_or(0.0) / 100.0; + let invert_gradient = pr.b("gradient/invertGradient"); + // Textures. + let tex_color = pr.get("textures/color").map(|v| v.as_color()).unwrap_or([1.0; 4]); + let tex_opacity = pr.get("textures/opacity").map(Value::as_f64).unwrap_or(1.0).clamp(0.0, 1.0) as f32; + let front = Face::from(ctx, b, "textures/frontMode", "textures/frontLayer", 1); + let side = Face::from(ctx, b, "textures/sideMode", "textures/sideLayer", 0); + let backf = Face::from(ctx, b, "textures/backMode", "textures/backLayer", 1); + let custom_map = custom.then(|| layer_or_self(ctx, b, "shape/customShatterMap", true, true)); + let white_fixed = pr.b("shape/whiteTilesFixed"); + let (sd, cd) = dir.sin_cos(); + let to_layer = |q: [f64; 2]| [origin[0] + q[0] * cd - q[1] * sd, origin[1] + q[0] * sd + q[1] * cd]; + // Pattern-space bounds of the layer rectangle. + let corners = [[0.0, 0.0], [lw, 0.0], [lw, lh], [0.0, lh]].map(|c: [f64; 2]| { + let (dx, dy) = (c[0] - origin[0], c[1] - origin[1]); + [dx * cd + dy * sd, -dx * sd + dy * cd] + }); + let (px0, px1) = (corners.iter().map(|c| c[0]).fold(f64::INFINITY, f64::min), corners.iter().map(|c| c[0]).fold(f64::NEG_INFINITY, f64::max)); + let (py0, py1) = (corners.iter().map(|c| c[1]).fold(f64::INFINITY, f64::min), corners.iter().map(|c| c[1]).fold(f64::NEG_INFINITY, f64::max)); + // Custom Shatter Map: square cells grouped by the map's colour. + let cells = pattern_cells(if custom { 3 } else { kind }, cell, px0, py0, px1, py1, seed); + let polys: Vec> = cells.iter().map(|poly| poly.iter().map(|&q| to_layer(q)).collect()).collect(); + let centre = |lp: &[[f64; 2]]| [lp.iter().map(|v| v[0]).sum::() / lp.len() as f64, lp.iter().map(|v| v[1]).sum::() / lp.len() as f64]; + let keys: Vec = match &custom_map { + Some(m) => polys + .iter() + .map(|lp| { + let c = centre(lp); + map_key(m.sample_bilinear(c[0] * s + b.offset[0], c[1] * s + b.offset[1])) + }) + .collect(), + None => (0..polys.len() as u32).collect(), + }; + // Group centres (custom map) — each group moves as one rigid piece. + let mut groups: std::collections::HashMap = std::collections::HashMap::new(); + for (lp, &key) in polys.iter().zip(&keys) { + let c = centre(lp); + let e = groups.entry(key).or_insert(([0.0; 2], 0.0)); + e.0[0] += c[0]; + e.0[1] += c[1]; + e.1 += 1.0; + } + let g3 = [gdir.sin() * ginc.cos() * gravity * unit, -gdir.cos() * ginc.cos() * gravity * unit, ginc.sin() * gravity * unit]; + let mut pieces: Vec = polys + .par_iter() + .zip(keys.par_iter()) + .flat_map_iter(|(lp, &key)| { + let mut out: Vec = vec![]; + let (mnx, mxx) = (lp.iter().map(|v| v[0]).fold(f64::INFINITY, f64::min), lp.iter().map(|v| v[0]).fold(f64::NEG_INFINITY, f64::max)); + let (mny, mxy) = (lp.iter().map(|v| v[1]).fold(f64::INFINITY, f64::min), lp.iter().map(|v| v[1]).fold(f64::NEG_INFINITY, f64::max)); + if mxx < 0.0 || mxy < 0.0 || mnx > lw || mny > lh || key == u32::MAX { + return out.into_iter(); + } + let pc_c = centre(lp); + let (gsum, gn) = groups[&key]; + let gc = [gsum[0] / gn, gsum[1] / gn]; + let i = key; + // Broken when inside a force sphere (centred `depth` in front of the layer). + let mut v = [0.0f64; 3]; + let mut broken = false; + for (fp, fd, fr, fs) in forces { + let rad = fr * lw; + if rad <= 0.0 { + continue; + } + let dv = [gc[0] - fp[0], gc[1] - fp[1], fd * lw]; + let dist = (dv[0] * dv[0] + dv[1] * dv[1] + dv[2] * dv[2]).sqrt(); + if dist < rad { + broken = true; + let f = fs * (1.0 - dist / rad) * unit / dist.max(1e-6); + for q in 0..3 { + v[q] += dv[q] * f; + } + } + } + if broken && let Some(g) = &gradient { + let (c, _) = unpremul(g.sample_bilinear(gc[0] * s + b.offset[0], gc[1] * s + b.offset[1])); + let mut l = effectcraft_color::luminance(c[0], c[1], c[2]).clamp(0.0, 1.0) as f64; + if invert_gradient { + l = 1.0 - l; + } + broken = threshold > 0.0 && l >= 1.0 - threshold; + } + if broken && custom && white_fixed && key == 0xfff { + broken = false; + } + if (broken && render == 1) || (!broken && render == 2) { + return out.into_iter(); + } + let mut gpos = [gc[0], gc[1], 0.0]; + let mut r = rot_axis([1.0, 0.0, 0.0], 0.0); + let moving = broken && t > 0.0; + if moving { + let mass = 1.0 + mass_var * hs(i, 1, seed) as f64; + for q in 0..3 { + v[q] = v[q] / mass + randomness * unit * hs(i, 2 + q as u32, seed) as f64 * 2.0; + } + let (f1, f2) = drag_factors(k, t); + for q in 0..3 { + gpos[q] += v[q] * f1 + g3[q] * f2; + } + let axis = match tumble { + 1 => [0.0, 0.0, 0.0], + 2 => [1.0, 0.0, 0.0], + 3 => [0.0, 1.0, 0.0], + 4 => [0.0, 0.0, 1.0], + 5 => [hs(i, 6, seed) as f64, hs(i, 7, seed) as f64, 0.0], + 6 => [hs(i, 6, seed) as f64, 0.0, hs(i, 8, seed) as f64], + 7 => [0.0, hs(i, 7, seed) as f64, hs(i, 8, seed) as f64], + _ => [hs(i, 6, seed) as f64, hs(i, 7, seed) as f64, hs(i, 8, seed) as f64], + }; + let speed = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt() / unit; + let omega = rot_speed * std::f64::consts::TAU * (0.5 + h(i, 9, seed) as f64) * speed.min(10.0); + r = rot_axis(axis, omega * f1); + } + // The piece's centre: its offset from the group centre, rotated with the group. + let off = [pc_c[0] - gc[0], pc_c[1] - gc[1], 0.0]; + let pos: [f64; 3] = std::array::from_fn(|q| gpos[q] + (0..3).map(|k| r[q][k] * off[k]).sum::()); + let tex: Vec<[f32; 2]> = lp.iter().map(|v| [(v[0] * s + b.offset[0]) as f32, (v[1] * s + b.offset[1]) as f32]).collect(); + let c0 = [(pc_c[0] * s + b.offset[0]) as f32, (pc_c[1] * s + b.offset[1]) as f32]; + let posb = [pos[0] * s + b.offset[0], pos[1] * s + b.offset[1], pos[2] * s]; + let Some(mut pc) = Piece::new(&tex, c0, &r, posb, cam) else { return out.into_iter() }; + pc.light(&light, &r, posb, eye); + pc.alpha = tex_opacity; + let face = if pc.back { &backf } else { &front }; + if face.mode == 0 { + pc.flat = Some([tex_color[0], tex_color[1], tex_color[2]]); + } else if face.mode == 2 { + pc.tint = std::array::from_fn(|q| pc.tint[q] * tex_color[q]); + } + // Extrusion: the sides of a moving piece, from its front outline back by the depth. + if moving && extrude > 0.0 { + let nb = [r[0][2], r[1][2], r[2][2]]; + let n = tex.len(); + for e in 0..n { + let (a, bv) = (tex[e], tex[(e + 1) % n]); + let ed = [(bv[0] - a[0]) as f64, (bv[1] - a[1]) as f64]; + let len = (ed[0] * ed[0] + ed[1] * ed[1]).sqrt(); + if len < 1e-6 { + continue; + } + let ew: [f64; 3] = std::array::from_fn(|q| (r[q][0] * ed[0] + r[q][1] * ed[1]) / len); + let third = [ew[1] * nb[2] - ew[2] * nb[1], ew[2] * nb[0] - ew[0] * nb[2], ew[0] * nb[1] - ew[1] * nb[0]]; + let rs: M3 = std::array::from_fn(|q| [ew[q], nb[q], third[q]]); + // Side centre in world: edge midpoint (world) plus half the depth backwards. + let mid = [((a[0] + bv[0]) * 0.5 - c0[0]) as f64, ((a[1] + bv[1]) * 0.5 - c0[1]) as f64]; + let mw: [f64; 3] = std::array::from_fn(|q| posb[q] + r[q][0] * mid[0] + r[q][1] * mid[1] + nb[q] * extrude * 0.5); + let (hl, hd) = ((len * 0.5) as f32, (extrude * 0.5) as f32); + let cs = [(a[0] + bv[0]) * 0.5, (a[1] + bv[1]) * 0.5]; + let quad = [[cs[0] - hl, cs[1] - hd], [cs[0] + hl, cs[1] - hd], [cs[0] + hl, cs[1] + hd], [cs[0] - hl, cs[1] + hd]]; + if let Some(mut sp) = Piece::new(&quad, cs, &rs, mw, cam) { + sp.light(&light, &rs, mw, eye); + sp.alpha = tex_opacity; + let colour = match (&side.img, side.mode) { + (Some(img), m) if m != 0 => { + let (c, _) = unpremul(img.sample_bilinear(cs[0] as f64, cs[1] as f64)); + if m == 2 { std::array::from_fn(|q| c[q] * tex_color[q]) } else { c } + } + _ => [tex_color[0], tex_color[1], tex_color[2]], + }; + sp.flat = Some(colour); + out.push(sp); + } + } + } + out.push(pc); + out.into_iter() + }) + .collect(); + sort_far_first(&mut pieces); + if view == 0 { + let tex = |i: &Option| i.clone().map(PieceTex::Image); + let front_t = tex(&front.img).unwrap_or(PieceTex::Layer); + let back_t = tex(&backf.img).unwrap_or_else(|| front_t.clone()); + return ShatterPlan::Pieces(PiecePlan { pieces, front: front_t, back: Some(back_t) }); + } + { + // Wireframes: piece outlines (front view: at rest; otherwise animated), plus forces. + let animated = view == 2 || view == 4; + let mut lines = Vec::new(); + for pc in &pieces { + let n = pc.n as usize; + let pts: Vec<[f32; 2]> = if animated { + let fw = inv3(&pc.inv); + match fw { + Some(m) => pc.poly[..n] + .iter() + .map(|v| { + let (a, bb) = ((v[0] - pc.c0[0]) as f64, (v[1] - pc.c0[1]) as f64); + let w = m[2][0] * a + m[2][1] * bb + m[2][2]; + [((m[0][0] * a + m[0][1] * bb + m[0][2]) / w) as f32, ((m[1][0] * a + m[1][1] * bb + m[1][2]) / w) as f32] + }) + .collect(), + None => continue, + } + } else { + pc.poly[..n].to_vec() + }; + for e in 0..n { + let (a, c) = (pts[e], pts[(e + 1) % n]); + lines.push(Sprite::new(a[0], a[1], 0.5, [0.75, 0.85, 1.0, 1.0], Shape::Line { dx: c[0] - a[0], dy: c[1] - a[1] })); + } + } + if view >= 3 { + for (fp, _, fr, _) in forces { + let (fx, fy) = b.to_px(fp); + let rad = fr * lw * s; + for k in 0..64 { + let (a0, a1) = (k as f64 / 64.0 * std::f64::consts::TAU, (k + 1) as f64 / 64.0 * std::f64::consts::TAU); + let (x0, y0) = (fx + rad * a0.cos(), fy + rad * a0.sin()); + let (x1, y1) = (fx + rad * a1.cos(), fy + rad * a1.sin()); + lines.push(Sprite::new(x0 as f32, y0 as f32, 0.5, [1.0, 0.3, 0.3, 1.0], Shape::Line { dx: (x1 - x0) as f32, dy: (y1 - y0) as f32 })); + } + } + } + ShatterPlan::Wire(SpritePlan { sprites: lines, tints: vec![], acc: Acc::Over, post: Post::Replace }) + } +} diff --git a/crates/effects/src/sim2/surfaces.rs b/crates/effects/src/sim2/surfaces.rs new file mode 100644 index 000000000..0bcc639c4 --- /dev/null +++ b/crates/effects/src/sim2/surfaces.rs @@ -0,0 +1,524 @@ +use super::*; + +// ------------------------------------------------------------------ Caustics + +/// Sample with repeat mode: 0 Once (transparent outside), 1 Tiled, 2 Reflected. +fn sample_repeat(img: &Image, x: f64, y: f64, mode: u32) -> Px { + let (w, hh) = (img.width as f64, img.height as f64); + match mode { + 1 => img.sample_bilinear_clamped(x.rem_euclid(w), y.rem_euclid(hh)), + 2 => { + let m = |v: f64, n: f64| { + let r = v.rem_euclid(2.0 * n); + if r > n { 2.0 * n - r } else { r } + }; + img.sample_bilinear_clamped(m(x, w), m(y, hh)) + } + _ => img.sample_bilinear(x, y), + } +} + +/// Everything Caustics' per-pixel pass needs, prepared on the CPU (the other layers it reads +/// and the parameter conversions); the layer's own pixels are not read. +pub struct CausticsSetup { + /// The Bottom layer fitted to the buffer, `None` = the layer itself. + pub bottom: Option, + /// Gaussian σ (buffer px) blurring the bottom (transparent edges), 0 = none. + pub bottom_sigma: f64, + /// The water surface's height field (smoothed luminance), `None` = flat. + pub height: Option, + /// The Sky layer fitted to the buffer. + pub sky: Option, + pub scaling: f64, + pub repeat: u32, + pub wave_h: f64, + /// Displacement per unit of height gradient. + pub k: f64, + /// Layer centre (buffer px). + pub cx: f64, + pub cy: f64, + /// Layer width (buffer px). + pub lw: f64, + pub surf: [f32; 4], + pub surf_op: f32, + pub cstr: f32, + pub lc: [f32; 4], + pub li: f32, + /// Light position (buffer px) and height (layer widths); `point` = from the position to + /// each pixel, else one direction from above the layer centre. + pub lx: f64, + pub ly: f64, + pub lheight: f64, + pub point: bool, + pub ambient: f32, + pub diffuse: f32, + pub specular: f32, + pub sharp: f32, + pub sky_scaling: f64, + pub sky_repeat: u32, + pub sky_int: f32, + pub convergence: f64, +} + +/// Caustics' setup for layer geometry `b` (see [`CausticsSetup`]). +pub fn caustics_setup(ctx: &EffectCtx, b: &Buf) -> CausticsSetup { + let pr = ctx.params; + let s = b.scale; + let bottom = ctx.layer_param("bottom/bottom", true).map(|o| crate::util::fit_layer(ctx, b, &o, pr.e("bottom/bottomSizeDiffers") == 1)); + let blur = pr.f("bottom/blur") * s; + let wave_h = pr.f("water/waveHeight"); + let smoothing = pr.f("water/smoothing") * s; + let depth = pr.f("water/waterDepth"); + let ior = pr.f("water/refractiveIndex").max(1.0); + let lpos = pr.v2("lighting/lightPosition"); + // Height field of the water surface (luminance of the chosen layer; none = flat). + let height = ctx.layer_param("water/waterSurface", true).map(|o| { + let img = crate::util::fit_layer(ctx, b, &o, true); + let pl = Plane::luma(&img); + if smoothing > 0.05 { gauss_plane(&pl, smoothing * 0.5, smoothing * 0.5) } else { pl } + }); + let lw = ctx.layer_size[0] * s; + let k = wave_h * depth * (1.0 - 1.0 / ior) * lw * 2.0; + let (cx, cy) = (b.offset[0] + ctx.layer_size[0] * 0.5 * s, b.offset[1] + ctx.layer_size[1] * 0.5 * s); + let (lx, ly) = b.to_px(lpos); + let (lc, li, lheight) = (pr.color("lighting/lightColor"), pr.f("lighting/lightIntensity") as f32, pr.f("lighting/lightHeight").max(0.01)); + // Light Type: Distant Source (one direction, from the light position above the layer + // centre), Point Source (from the light position to each pixel) or First Comp Light. + let light_type = pr.e("lighting/lightType"); + let comp_light = if light_type == 2 { ctx.env.host.and_then(|h| h.comp_scene()).and_then(|sc| sc.light) } else { None }; + let (lc, li, lx, ly, lheight, point) = match (light_type, comp_light) { + (2, Some(l)) => { + let c = [l.color[0], l.color[1], l.color[2], 1.0]; + // Comp space: z away from the viewer; the light above the water is at −z. + let (px, py) = (l.pos[0] * s + b.offset[0], l.pos[1] * s + b.offset[1]); + (c, li, px, py, (-l.pos[2] / ctx.layer_size[0].max(1.0)).max(0.01), l.kind == 1) + } + (2, None) => (lc, 0.0, lx, ly, lheight, false), + (1, _) => (lc, li, lx, ly, lheight, true), + _ => (lc, li, lx, ly, lheight, false), + }; + // Sky: the layer the water surface reflects (Surface Opacity 1 = a mirror of it). + let sky = ctx.layer_param("sky/sky", true).map(|o| crate::util::fit_layer(ctx, b, &o, pr.e("sky/skySizeDiffers") == 1)); + CausticsSetup { + bottom, + bottom_sigma: if blur > 0.05 { blur * 0.5 } else { 0.0 }, + height, + sky, + scaling: pr.f("bottom/scaling").max(0.01), + repeat: pr.e("bottom/repeatMode"), + wave_h, + k, + cx, + cy, + lw, + surf: pr.color("water/surfaceColor"), + surf_op: pr.f("water/surfaceOpacity") as f32, + cstr: pr.f("water/causticsStrength") as f32, + lc, + li, + lx, + ly, + lheight, + point, + ambient: pr.f("lighting/ambientLight") as f32, + diffuse: pr.f("material/diffuse") as f32, + specular: pr.f("material/specular") as f32, + sharp: pr.f("material/highlightSharpness").max(1.0) as f32, + sky_scaling: pr.get("sky/scaling").map(Value::as_f64).unwrap_or(1.0).max(0.01), + sky_repeat: pr.e("sky/repeatMode"), + sky_int: pr.get("sky/intensity").map(Value::as_f64).unwrap_or(0.3) as f32, + convergence: pr.get("sky/convergence").map(Value::as_f64).unwrap_or(0.5).clamp(0.0, 1.0), + } +} + +pub(super) fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf { + let st = caustics_setup(ctx, &b); + let mut bottom = st.bottom.clone().unwrap_or_else(|| b.img.clone()); + if st.bottom_sigma > 0.0 { + bottom = effectcraft_raster::gaussian_blur(&bottom, st.bottom_sigma, st.bottom_sigma, false); + } + let CausticsSetup { + scaling, + repeat, + wave_h, + k, + cx, + cy, + lw, + surf, + surf_op, + cstr, + lc, + li, + lx, + ly, + lheight, + point, + ambient, + diffuse, + specular, + sharp, + sky_scaling, + sky_repeat, + sky_int, + convergence, + .. + } = st; + let (height, sky) = (&st.height, &st.sky); + let (w, hh) = (b.img.width as usize, b.img.height as usize); + let norm3 = |v: [f64; 3]| { + let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-12); + [(v[0] / n) as f32, (v[1] / n) as f32, (v[2] / n) as f32] + }; + let lv_at = |x: f64, y: f64| { + if point { + norm3([(lx - x) / lw.max(1.0), (ly - y) / lw.max(1.0), lheight]) + } else { + norm3([(lx - cx) / lw.max(1.0), (ly - cy) / lw.max(1.0), lheight]) + } + }; + let grad = |x: usize, y: usize| -> (f64, f64) { + match height { + Some(pl) => { + let gx = (pl.get_clamped(x as i64 + 1, y as i64) - pl.get_clamped(x as i64 - 1, y as i64)) as f64 * 0.5; + let gy = (pl.get_clamped(x as i64, y as i64 + 1) - pl.get_clamped(x as i64, y as i64 - 1)) as f64 * 0.5; + (gx, gy) + } + None => (0.0, 0.0), + } + }; + // Displacement and its Jacobian (caustic focusing) per pixel. + let disp: Vec<(f64, f64)> = (0..w * hh) + .into_par_iter() + .map(|i| { + let (gx, gy) = grad(i % w, i / w); + (-gx * k, -gy * k) + }) + .collect(); + b.img.rows_mut().for_each(|(y, row)| { + for (x, px) in row.iter_mut().enumerate() { + let i = y * w + x; + let (dx, dy) = disp[i]; + let d = |xx: usize, yy: usize| disp[yy.min(hh - 1) * w + xx.min(w - 1)]; + let (dxr, _) = d(x + 1, y); + let (_, dyd) = d(x, y + 1); + let (dxl, _) = d(x.saturating_sub(1), y); + let (_, dyu) = d(x, y.saturating_sub(1)); + let jac = (1.0 + (dxr - dxl) * 0.5) * (1.0 + (dyd - dyu) * 0.5); + let focus = ((1.0 / jac.abs().max(0.2)) as f32 - 1.0).clamp(-1.0, 4.0); + let sx = cx + (x as f64 + 0.5 + dx - cx) / scaling; + let sy = cy + (y as f64 + 0.5 + dy - cy) / scaling; + let p = sample_repeat(&bottom, sx, sy, repeat); + let (c, a) = unpremul(p); + let (gx, gy) = grad(x, y); + let n = { + let v = [-gx * wave_h * 20.0, -gy * wave_h * 20.0, 1.0]; + let m = (v[0] * v[0] + v[1] * v[1] + 1.0).sqrt(); + [(v[0] / m) as f32, (v[1] / m) as f32, (v[2] / m) as f32] + }; + let lv = lv_at(x as f64 + 0.5, y as f64 + 0.5); + let nl = (n[0] * lv[0] + n[1] * lv[1] + n[2] * lv[2]).max(0.0); + let hv = { + let v = [lv[0], lv[1], lv[2] + 1.0]; + 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 spec = (n[0] * hv[0] + n[1] * hv[1] + n[2] * hv[2]).max(0.0).powf(sharp) * specular * li; + let light = ambient + diffuse * li * nl + cstr * focus * li; + // The surface: its colour lit like the water, or the sky it reflects (sampled along + // the surface normal; Convergence pulls the reflection towards the pixel itself). + let surface: [f32; 3] = match &sky { + Some(sk) => { + let reach = (1.0 - convergence) * lw * 0.5; + let (rx, ry) = (x as f64 + 0.5 + n[0] as f64 * reach, y as f64 + 0.5 + n[1] as f64 * reach); + let (rx, ry) = (cx + (rx - cx) / sky_scaling, cy + (ry - cy) / sky_scaling); + let (sc, _) = unpremul(sample_repeat(sk, rx, ry, sky_repeat)); + std::array::from_fn(|q| sc[q] * sky_int * 3.0 * surf[q]) + } + None => std::array::from_fn(|q| surf[q] * (ambient + diffuse * li * nl)), + }; + let cc: [f32; 3] = std::array::from_fn(|q| { + let base = c[q] * light * lc[q]; + base * (1.0 - surf_op) + surface[q] * surf_op + spec * lc[q] + }); + let a = (a + surf_op * (1.0 - a)).min(1.0); + *px = [cc[0] * a, cc[1] * a, cc[2] * a, a]; + } + }); + b +} + +// ------------------------------------------------------------------ Wave World + +/// Wave World's simulated surface: an `nx` × `ny` grid of heights `u` and velocities `v`. +#[derive(Clone, Debug)] +pub struct Waves { + pub nx: usize, + pub ny: usize, + pub u: Vec, + pub v: Vec, +} + +struct Producer { + ring: bool, + pos: [f32; 2], + len: f32, + width: f32, + angle: f32, + amp: f32, + freq: f32, + phase: f32, +} + +impl Producer { + /// Weight of a grid point (layer-fraction coordinates, x in widths, y in widths). + fn weight(&self, x: f32, y: f32) -> f32 { + let (dx, dy) = (x - self.pos[0], y - self.pos[1]); + let (sa, ca) = self.angle.sin_cos(); + let (u, v) = (dx * ca + dy * sa, -dx * sa + dy * ca); + let d = if self.ring { + let (a, bb) = ((self.len * 0.5).max(1e-3), (self.width * 0.5).max(1e-3)); + ((u / a).powi(2) + (v / bb).powi(2)).sqrt() + } else { + let l = (self.len * 0.5).max(1e-3); + let wd = (self.width * 0.5).max(1e-3); + (u.abs() / l).max(v.abs() / wd) + }; + (1.0 - d).clamp(0.0, 1.0).min(1.0) + } +} + +static WAVE_CACHE: SimCache = SimCache::new(4); + +/// What Wave World draws: the Height Map view of the simulated surface (grid heights and, +/// with a ground layer, water depths per grid point), or the wireframe preview's lines. +pub enum WavePlan { + Height { st: std::sync::Arc, depth: Option> }, + Wire(crate::sim::SpritePlan), +} + +pub(super) fn wave_world(ctx: &EffectCtx, b: Buf) -> Buf { + match wave_world_impl(ctx, &b, false) { + Ok(img) => Buf { img, ..b }, + Err(WavePlan::Wire(plan)) => plan.finish(b), + Err(WavePlan::Height { .. }) => b, + } +} + +/// Wave World's simulation state for the frame, as a [`WavePlan`] (its pixels are not read). +pub fn wave_world_plan(ctx: &EffectCtx, b: &Buf) -> Option { + wave_world_impl(ctx, b, true).err() +} + +fn wave_world_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result { + 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 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"); + let preroll = pr.f("simulation/preRoll").max(0.0); + let prods: Vec = [1, 2] + .iter() + .map(|k| { + let pos = pr.v2(&format!("producer{k}/producer{k}Position")); + Producer { + ring: pr.e(&format!("producer{k}/producer{k}Type")) == 0, + pos: [(pos[0] / lw.max(1.0)) as f32, (pos[1] / lw.max(1.0)) as f32], + len: pr.f(&format!("producer{k}/producer{k}Length")) as f32, + width: pr.f(&format!("producer{k}/producer{k}Width")) as f32, + angle: (pr.f(&format!("producer{k}/producer{k}Angle")) as f32).to_radians(), + amp: pr.f(&format!("producer{k}/producer{k}Amplitude")) as f32, + freq: pr.f(&format!("producer{k}/producer{k}Frequency")) as f32, + phase: (pr.f(&format!("producer{k}/producer{k}Phase")) as f32).to_radians(), + } + }) + .collect(); + let sps = crate::sim::SPS as f32; + // Wave speed in layer widths per second → cells per second; sub-steps keep it stable. + let cps = speed * (nx - 1) as f32; + let sub = ((cps * 2.0 / sps).ceil() as usize).clamp(1, 64); + let dt = 1.0 / (sps * sub as f32); + let cell = 1.0 / (nx - 1) as f32; + let weights: Vec> = prods.iter().map(|p| (0..nx * ny).map(|i| p.weight((i % nx) as f32 * cell, (i / nx) as f32 * cell)).collect()).collect(); + // Ground: the ground layer's first frame, its luminance an elevation (× Steepness). Where + // it rises above Height the grid is dry (a wall the waves reflect from); shallow water + // slows the waves by Wave Strength. + let ground = pr.get("ground/ground").and_then(Value::as_layer).and_then(|id| { + let host = ctx.env.host?; + host.layer_at(id, ctx.env.comp_time - ctx.time, true) + }); + let steep = pr.get("ground/steepness").map(Value::as_f64).unwrap_or(0.25) as f32; + let level = pr.get("ground/height").map(Value::as_f64).unwrap_or(0.25) as f32; + let wave_strength = pr.f("ground/waveStrength").clamp(0.0, 1.0) as f32; + // Water depth per grid point (None without ground: deep everywhere). + let depth: Option> = ground.as_ref().map(|lp| { + let (gw, gh) = (lp.size[0].max(1.0), lp.size[1].max(1.0)); + (0..nx * ny) + .map(|i| { + let (fx, fy) = ((i % nx) as f64 / (nx - 1) as f64, (i / nx) as f64 / (ny - 1) as f64); + let (c, a) = unpremul(lp.buf.img.sample_bilinear_clamped(fx * gw * lp.buf.scale + lp.buf.offset[0], fy * gh * lp.buf.scale + lp.buf.offset[1])); + level - steep * effectcraft_color::luminance(c[0], c[1], c[2]) * a + }) + .collect() + }); + let speed_k: Vec = match &depth { + Some(d) => d.iter().map(|&v| if v <= 0.0 { 0.0 } else { 1.0 - wave_strength * (1.0 - (v / level.max(1e-3)).clamp(0.0, 1.0).sqrt()) }).collect(), + None => vec![1.0; nx * ny], + }; + let mut key = + params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: if pr.b("simulation/gridResDownsamples") { b.scale } else { 1.0 } }, 4); + if let Some(d) = &depth { + key ^= d.iter().fold(0u64, |a, v| a.rotate_left(3) ^ v.to_bits() as u64); + } + let steps = crate::sim::steps_at(ctx.time + preroll); + let st = WAVE_CACHE.run( + key, + steps, + || Waves { nx, ny, u: vec![0.0; nx * ny], v: vec![0.0; nx * ny] }, + |w, step| { + for k in 0..sub { + let t = (step as f32 + k as f32 / sub as f32) / sps - preroll as f32; + let c2 = cps * cps; + let mut acc = vec![0.0f32; nx * ny]; + for y in 0..ny { + for x in 0..nx { + let i = y * nx + x; + let g = |xx: usize, yy: usize| w.u[yy * nx + xx]; + let l = g(x.saturating_sub(1), y) + g((x + 1).min(nx - 1), y) + g(x, y.saturating_sub(1)) + g(x, (y + 1).min(ny - 1)) - 4.0 * w.u[i]; + acc[i] = c2 * speed_k[i] * speed_k[i] * l - damping * 4.0 * w.v[i]; + } + } + for i in 0..nx * ny { + w.v[i] += acc[i] * dt; + w.u[i] += w.v[i] * dt; + } + // Producers drive the surface. + for (p, wt) in prods.iter().zip(&weights) { + if p.amp == 0.0 { + continue; + } + let target = p.amp * (std::f32::consts::TAU * p.freq * t + p.phase).sin(); + for i in 0..nx * ny { + if wt[i] > 0.0 { + w.u[i] += (target - w.u[i]) * wt[i]; + w.v[i] *= 1.0 - wt[i]; + } + } + } + // Dry ground holds the surface flat. + for i in 0..nx * ny { + if speed_k[i] <= 0.0 { + w.u[i] = 0.0; + w.v[i] = 0.0; + } + } + // Edges: reflective ones mirror; the others absorb in a thin sponge layer. + let refl = |side: u32| reflect == 5 || reflect == side; + let sponge = 4usize.min(nx / 4).max(1); + for y in 0..ny { + for x in 0..nx { + let i = y * nx + x; + let mut f = 1.0f32; + if !refl(1) && x < sponge { + f = f.min(x as f32 / sponge as f32); + } + if !refl(3) && nx - 1 - x < sponge { + f = f.min((nx - 1 - x) as f32 / sponge as f32); + } + if !refl(2) && y < sponge { + f = f.min(y as f32 / sponge as f32); + } + if !refl(4) && ny - 1 - y < sponge { + f = f.min((ny - 1 - y) as f32 / sponge as f32); + } + if f < 1.0 { + let k = 0.85 + 0.15 * f; + w.u[i] *= k; + w.v[i] *= k; + } + } + } + } + }, + ); + let s = b.scale; + let (bw, bh) = (b.img.width, b.img.height); + let sample = |x: f64, y: f64| -> f32 { + let gx = (x / lw.max(1.0) * (st.nx - 1) as f64).clamp(0.0, (st.nx - 1) as f64); + let gy = (y / lw.max(1.0) * (st.nx - 1) as f64).clamp(0.0, (st.ny - 1) as f64); + let (x0, y0) = (gx.floor() as usize, gy.floor() as usize); + let (x1, y1) = ((x0 + 1).min(st.nx - 1), (y0 + 1).min(st.ny - 1)); + let (tx, ty) = ((gx - x0 as f64) as f32, (gy - y0 as f64) as f32); + let g = |xx: usize, yy: usize| st.u[yy * st.nx + xx]; + let a = g(x0, y0) + (g(x1, y0) - g(x0, y0)) * tx; + let c = g(x0, y1) + (g(x1, y1) - g(x0, y1)) * tx; + a + (c - a) * ty + }; + if plan_only && pr.e("view") == 1 { + return Err(WavePlan::Height { st, depth }); + } + if pr.e("view") == 1 { + let mut img = Image::new(bw, bh); + let bright = pr.f("heightMapControls/brightness") as f32; + let contrast = pr.f("heightMapControls/contrast") as f32; + let gamma = pr.f("heightMapControls/gamma").max(0.01) as f32; + let alpha = 1.0 - pr.f("heightMapControls/transparency") as f32; + let dry_transparent = pr.e("heightMapControls/renderDryAreasAs") == 1; + let dry_at = |x: f64, y: f64| -> bool { + let Some(d) = &depth else { return false }; + let gx = (x / lw.max(1.0) * (st.nx - 1) as f64).round().clamp(0.0, (st.nx - 1) as f64) as usize; + let gy = (y / lw.max(1.0) * (st.nx - 1) as f64).round().clamp(0.0, (st.ny - 1) as f64) as usize; + d[gy * st.nx + gx] <= 0.0 + }; + img.rows_mut().for_each(|(y, row)| { + for (x, px) in row.iter_mut().enumerate() { + let lx = (x as f64 + 0.5 - b.offset[0]) / s; + let ly = (y as f64 + 0.5 - b.offset[1]) / s; + let v = (bright + contrast * sample(lx, ly)).clamp(0.0, 1.0).powf(1.0 / gamma); + let al = if dry_transparent && dry_at(lx, ly) { 0.0 } else { alpha }; + *px = [v * al, v * al, v * al, al]; + } + }); + Ok(img) + } else { + // Wireframe preview: the grid turned by Horizontal Rotation about the vertical axis, + // tilted by Vertical Rotation, heights scaled by Vertical Scale. + let hr = pr.f("wireframeControls/horizontalRotation").to_radians(); + let vr = pr.get("wireframeControls/verticalRotation").map(Value::as_f64).unwrap_or(53.0).to_radians(); + let vs = pr.get("wireframeControls/verticalScale").map(Value::as_f64).unwrap_or(0.5); + let (sh, ch) = hr.sin_cos(); + let (sv, cv) = vr.sin_cos(); + let to = |x: usize, y: usize| -> [f32; 2] { + // Grid point relative to the layer centre (layer px), height in layer px. + let gx = x as f64 / (st.nx - 1) as f64 * lw - lw * 0.5; + let gy = y as f64 / (st.nx - 1) as f64 * lw - (st.ny - 1) as f64 / (st.nx - 1) as f64 * lw * 0.5; + let hgt = st.u[y * st.nx + x] as f64 * vs * lh * 0.5; + let (rx, ry) = (gx * ch - gy * sh, gx * sh + gy * ch); + let (px, py) = b.to_px([lw * 0.5 + rx, lh * 0.5 + ry * cv - hgt * sv]); + [px as f32, py as f32] + }; + let mut lines = Vec::new(); + for y in 0..st.ny { + for x in 0..st.nx { + let a = to(x, y); + if x + 1 < st.nx { + let c = to(x + 1, y); + lines.push(Sprite::new(a[0], a[1], 0.4, [0.4, 0.9, 0.4, 1.0], Shape::Line { dx: c[0] - a[0], dy: c[1] - a[1] })); + } + if y + 1 < st.ny { + let c = to(x, y + 1); + lines.push(Sprite::new(a[0], a[1], 0.4, [0.4, 0.9, 0.4, 1.0], Shape::Line { dx: c[0] - a[0], dy: c[1] - a[1] })); + } + } + } + let plan = crate::sim::SpritePlan { sprites: lines, tints: vec![], acc: Acc::Over, post: crate::sim::Post::OverBlack }; + if plan_only { + return Err(WavePlan::Wire(plan)); + } + Ok(plan.finish(Buf { img: Image::new(bw, bh), offset: b.offset, scale: b.scale }).img) + } +}