//! GPU effects, simulation render passes (kernels in `shaders/fx_sim.wgsl`, every entry point //! prefixed `fxm_`): CC Rainfall, CC Snowfall, CC Star Burst, CC Bubbles, CC Drizzle, CC Hair, //! CC Mr. Mercury, Caustics, Wave World, Foam, Shatter, Card Dance and Card Wipe. //! //! The simulation (or closed-form layout) of every frame stays on the CPU, shared with the CPU //! effect: sprite plans (`effects::sprite_plan`: rain drops, snow flakes, star-burst discs, hair //! strands, Foam's bubbles, Wave World's wireframe), piece plans (`effects::piece_plan`: Card //! Dance's cards, Shatter's shards, Card Wipe's cards, sorted far to near), CC Bubbles' and CC //! Mr. Mercury's lists, CC Drizzle's drops, Wave World's height grid and Caustics' prepared //! layers. The raster / render pass runs here: items are binned into 16 × 16 tiles on the CPU //! and `fxm_raster` composites each pixel's items in the CPU's order with the CPU's shading //! (colours that depend on the layer's own pixels — refracting rain, star-burst and hair-root //! colours, bubble refraction — are sampled per pixel from the layer texture). Piece plans read //! the layer's pixels on the CPU (gradient maps and textures default to the layer), so those //! effects read the frame back once. Shatter's wireframe views draw their lines as sprites //! (`effects::pixel_plan`); Foam's User Defined texture, Environment Map and flow-map preview //! are drawn by `fx_particles` over the bubble sprites (`effects::foam_full_plan`). Sprite //! layers and back-less piece plans too large for one item table are drawn in several passes //! (`fx_particles::Chunked`). use effectcraft_effects::{Acc, EffectCtx, PieceTex, Post, Shape, SpritePlan, Tint}; use effectcraft_raster::Image; use crate::context::{Enc, GpuImage, Params}; use crate::effects::{GBuf, gaussian_blur}; /// Compute entry points in `fx_sim.wgsl`. pub(crate) const KERNELS: &[&str] = &["fxm_raster", "fxm_post", "fxm_mercury", "fxm_drizzle", "fxm_wave", "fxm_caustics"]; /// Effect ids implemented here. pub(crate) const IDS: &[&str] = &[ "ec.sim.ccrainfall", "ec.sim.ccsnowfall", "ec.sim.ccstarburst", "ec.sim.ccbubbles", "ec.sim.ccdrizzle", "ec.sim.cchair", "ec.sim.ccmrmercury", "ec.sim.caustics", "ec.sim.waveworld", "ec.sim.foam", "ec.sim.shatter", "ec.sim.carddance", "ec.transition.cardwipe", ]; /// Largest item table (floats) the raster kernel takes; bigger plans render on the CPU. const MAX_FLOATS: usize = 1 << 24; const TILE: i32 = 16; /// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU. pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { let geo = effectcraft_effects::Buf { img: Image::new(0, 0), offset: b.offset, scale: b.scale }; match id { "ec.sim.ccbubbles" => bubbles(e, ctx, b, &geo), "ec.sim.ccdrizzle" => drizzle(e, ctx, b, &geo), "ec.sim.ccmrmercury" => mercury(e, ctx, b, &geo), "ec.sim.caustics" => caustics(e, ctx, b, &geo), "ec.sim.waveworld" => match effectcraft_effects::wave_world_plan(ctx, &geo)? { effectcraft_effects::WavePlan::Wire(plan) => sprites(e, b, &plan), effectcraft_effects::WavePlan::Height { st, depth } => wave_height(e, ctx, b, &st, depth.as_deref()), }, "ec.sim.shatter" | "ec.sim.carddance" | "ec.transition.cardwipe" => pieces(e, id, ctx, b), "ec.sim.foam" => crate::fx_particles::foam(e, ctx, b), "ec.sim.cchair" => match effectcraft_effects::sprite_plan(id, ctx, &geo) { Some(plan) => sprites(e, b, &plan), None => Some(b), }, _ => { let plan = effectcraft_effects::sprite_plan(id, ctx, &geo)?; sprites(e, b, &plan) } } } // ---------------------------------------------------------------- tiled item raster /// Items with their integer pixel bounds, binned into tiles (sim::raster's bands, finer). pub(crate) struct Items { stride: usize, recs: Vec, w: i32, h: i32, tiles: Vec>, pub(crate) tx: i32, /// Item indices in all tiles. n_idx: usize, } impl Items { pub(crate) fn new(stride: usize, w: u32, h: u32) -> Items { let (w, h) = (w as i32, h as i32); let tx = (w + TILE - 1) / TILE; let ty = (h + TILE - 1) / TILE; Items { stride, recs: vec![], w, h, tiles: vec![vec![]; (tx * ty).max(0) as usize], tx, n_idx: 0 } } /// The integer pixel bounds of float bounds `bb` (`None` = skipped, as sim::raster). fn bounds(&self, bb: Option<[f32; 4]>) -> Option<[i32; 4]> { let b = bb?; if !(b[0].is_finite() && b[1].is_finite() && b[2].is_finite() && b[3].is_finite()) { return None; } let x0 = b[0].floor().max(0.0) as i32; let y0 = b[1].floor().max(0.0) as i32; let x1 = (b[2].ceil() as i32).min(self.w - 1); let y1 = (b[3].ceil() as i32).min(self.h - 1); if x1 < x0 || y1 < y0 || b[2] < 0.0 || b[3] < 0.0 { return None; } Some([x0, y0, x1, y1]) } pub(crate) fn is_empty(&self) -> bool { self.recs.is_empty() } /// Whether adding an item covering `bb` keeps the kernel data within [`MAX_FLOATS`]. pub(crate) fn fits(&self, bb: Option<[f32; 4]>) -> bool { let Some([x0, y0, x1, y1]) = self.bounds(bb) else { return true }; let tiles = ((x1 / TILE - x0 / TILE + 1) * (y1 / TILE - y0 / TILE + 1)) as usize; self.tiles.len() + 1 + self.n_idx + tiles + self.recs.len() + self.stride <= MAX_FLOATS } /// Add an item (`rec` = its fields, without the bounds) covering float bounds `bb` /// (`None` or empty = skipped, as sim::raster). pub(crate) fn push(&mut self, bb: Option<[f32; 4]>, rec: &[f32]) { let Some([x0, y0, x1, y1]) = self.bounds(bb) else { return }; let i = (self.recs.len() / self.stride) as u32; let start = self.recs.len(); self.recs.extend_from_slice(rec); self.recs.resize(start + self.stride - 4, 0.0); self.recs.extend([x0 as f32, y0 as f32, x1 as f32, y1 as f32]); for ty in y0 / TILE..=y1 / TILE { for tx in x0 / TILE..=x1 / TILE { self.tiles[(ty * self.tx + tx) as usize].push(i); self.n_idx += 1; } } } /// Kernel data: tile offsets (tiles + 1), item indices, items. `None` when too large. pub(crate) fn data(self) -> Option<(Vec, u32, u32)> { let n_tiles = self.tiles.len(); let n_idx: usize = self.tiles.iter().map(Vec::len).sum(); let total = n_tiles + 1 + n_idx + self.recs.len(); if total > MAX_FLOATS { return None; } let mut d = Vec::with_capacity(total); let base = (n_tiles + 1) as f32; let mut at = 0usize; for t in &self.tiles { d.push(base + at as f32); at += t.len(); } d.push(base + at as f32); for t in &self.tiles { d.extend(t.iter().map(|&i| i as f32)); } let items_at = d.len() as u32; d.extend(self.recs); Some((d, items_at, self.stride as u32)) } } /// Rasterise items over a transparent frame. `kind`: 0 sprites, 1 CC Bubbles, 2 Mr. Mercury /// field, 3 pieces; `src` / `aux` = the textures the items sample. #[allow(clippy::too_many_arguments)] fn raster(e: &mut Enc, items: Items, kind: u32, add: bool, src: &GpuImage, aux: Option<&GpuImage>, mut p: Params, w: u32, h: u32) -> Option { let tx = items.tx as u32; let (data, items_at, stride) = items.data()?; let buf = e.data(&data); p.u[0] = [kind, add as u32, items_at, stride]; p.u[1][0] = tx; let out = e.scratch(w, h); e.pixels("fxm_raster", &p, src, aux, &out, Some(&buf)); Some(out) } /// sim::sprite_bbox. fn sprite_bbox(x: f32, y: f32, r: f32, c3: f32, shape: Shape) -> Option<[f32; 4]> { if c3 <= 1e-5 || r <= 0.0 { return None; } let r = r.max(0.5) + 1.0; Some(match shape { Shape::Line { dx, dy } => [x.min(x + dx) - r, y.min(y + dy) - r, x.max(x + dx) + r, y.max(y + dy) + r], Shape::Star => [x - r * 2.0, y - r * 2.0, x + r * 2.0, y + r * 2.0], _ => [x - r, y - r, x + r, y + r], }) } /// Draw a sprite plan and meet the layer `b` (sim::SpritePlan::finish). pub(crate) fn sprites(e: &mut Enc, b: GBuf, plan: &SpritePlan) -> Option { let (w, h) = (b.img.width, b.img.height); let fx = sprite_layer(e, &b.img, plan)?; let mut p = Params::default(); match plan.post { Post::Combine(m) => p.u[0] = [0, m, 0, 0], Post::Replace => return Some(GBuf { img: fx, ..b }), Post::Lerp(t) => { p.u[0][0] = 1; p.f[0][0] = t; } Post::OverBlack => p.u[0][0] = 2, } let out = e.scratch(w, h); e.pixels("fxm_post", &p, &b.img, Some(&fx), &out, None); Some(GBuf { img: out, ..b }) } /// sim::splat of a sprite plan over a transparent frame of `src`'s size (tints sample `src`), /// in as many passes as the item tables need. pub(crate) fn sprite_layer(e: &mut Enc, src: &GpuImage, plan: &SpritePlan) -> Option { let (w, h) = (src.width, src.height); // Without tints the record stops after the (zero) tint kind. let stride = if plan.tints.is_empty() { 16 } else { 24 }; let mut ch = crate::fx_particles::Chunked::new(0, (plan.acc == Acc::Add) as u32, stride, w, h); for (i, s) in plan.sprites.iter().enumerate() { let (kind, dx, dy) = match s.shape { Shape::Soft => (0.0, 0.0, 0.0), Shape::Disc => (1.0, 0.0, 0.0), Shape::Sphere => (2.0, 0.0, 0.0), Shape::Faded => (3.0, 0.0, 0.0), Shape::Bubble => (4.0, 0.0, 0.0), Shape::Star => (5.0, 0.0, 0.0), Shape::Square => (6.0, 0.0, 0.0), Shape::Tri => (7.0, 0.0, 0.0), Shape::Line { dx, dy } => (8.0, dx, dy), }; let mut rec = [kind, s.x, s.y, s.r, s.c[0], s.c[1], s.c[2], s.c[3], s.rot, dx, dy, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]; match plan.tints.get(i).copied().unwrap_or(Tint::Fixed) { Tint::Fixed => {} Tint::Refract { x, y } => rec[11..14].copy_from_slice(&[1.0, x as f32, y as f32]), Tint::Source { x, y } => rec[11..14].copy_from_slice(&[2.0, x as f32, y as f32]), Tint::Root { x, y, hair, inherit, shade, spec } => { rec[11..20].copy_from_slice(&[3.0, x as f32, y as f32, hair[0], hair[1], hair[2], inherit, shade, spec]); } } ch.push(e, src, sprite_bbox(s.x, s.y, s.r, s.c[3], s.shape), &rec[..stride - 4]); } ch.finish(e, src) } // ---------------------------------------------------------------- CC Bubbles / Mr. Mercury fn bubbles(e: &mut Enc, ctx: &EffectCtx, b: GBuf, geo: &effectcraft_effects::Buf) -> Option { let (w, h) = (b.img.width, b.img.height); let mut items = Items::new(12, w, h); for q in effectcraft_effects::bubble_list(ctx, geo) { items.push(Some([q.x - q.r - 1.0, q.y - q.r - 1.0, q.x + q.r + 1.0, q.y + q.r + 1.0]), &[9.0, q.x, q.y, q.r, q.sx, q.sy]); } let mut p = Params::default(); p.u[1] = [0, (ctx.params.e("reflectionType") == 1) as u32, ctx.params.e("shadingType"), 0]; let img = raster(e, items, 1, false, &b.img, None, p, w, h)?; Some(GBuf { img, ..b }) } fn mercury(e: &mut Enc, ctx: &EffectCtx, b: GBuf, geo: &effectcraft_effects::Buf) -> Option { let pr = ctx.params; let (w, h) = (b.img.width, b.img.height); let infl = 1.0 + pr.f("blobInfluence") as f32 / 100.0; let mut items = Items::new(8, w, h); for q in effectcraft_effects::mercury_blobs(ctx, geo) { let ex = q.r * 2.0 * infl; items.push(Some([q.x - ex, q.y - ex, q.x + ex, q.y + ex]), &[10.0, q.x, q.y, q.r]); } let mut p = Params::default(); p.f[0][0] = infl; let field = raster(e, items, 2, true, &b.img, None, p, w, h)?; let ldir = (pr.f("light/lightDirection") as f32).to_radians(); let lheight = pr.f("light/lightHeight") as f32 / 100.0; let l = norm3([ldir.sin(), -ldir.cos(), lheight.max(0.05)]); let lc = pr.color("light/lightColor"); let mut p = Params::default(); p.f[0] = [ pr.f("shading/ambient") as f32 / 100.0, pr.f("shading/diffuse") as f32 / 100.0, pr.f("shading/specular") as f32 / 100.0, pr.f("shading/roughness").max(0.001) as f32, ]; p.f[1] = [pr.f("shading/metal") as f32 / 100.0, pr.f("light/lightIntensity") as f32 / 100.0, b.scale as f32, 0.0]; p.f[2] = [l[0], l[1], l[2], 0.0]; p.f[3] = [lc[0], lc[1], lc[2], 0.0]; let out = e.scratch(w, h); e.pixels("fxm_mercury", &p, &b.img, Some(&field), &out, None); Some(GBuf { img: out, ..b }) } /// sim::norm3. fn norm3(v: [f32; 3]) -> [f32; 3] { let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt(); if l > 1e-9 { [v[0] / l, v[1] / l, v[2] / l] } else { [0.0, -1.0, 0.0] } } // ---------------------------------------------------------------- CC Drizzle fn drizzle(e: &mut Enc, ctx: &EffectCtx, b: GBuf, geo: &effectcraft_effects::Buf) -> Option { let pr = ctx.params; let Some(drops) = effectcraft_effects::drizzle_drops(ctx, geo) else { return Some(b) }; let rippling = (pr.f("rippling") / 360.0).max(0.05) as f32; let lh = ctx.layer_size[1] as f32; let s = b.scale as f32; let wavelen = (lh * 0.04 * s).max(2.0) / rippling.clamp(0.25, 4.0); let width = wavelen * (1.0 + rippling * 2.0); let ldir = (pr.f("light/lightDirection") as f32).to_radians(); let lheight = pr.f("light/lightHeight") as f32 / 100.0; let (ls, lcs) = ldir.sin_cos(); let l = { let v = [lcs, ls, lheight.max(0.05)]; let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt(); [v[0] / n, v[1] / n, v[2] / n] }; let hz = (l[2] + 1.0) * 0.5; let hv = { let v = [l[0] * 0.5, l[1] * 0.5, hz]; let m = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt(); [v[0] / m, v[1] / m, v[2] / m] }; let lc = pr.color("light/lightColor"); let d: Vec = drops.iter().flatten().copied().collect(); let buf = e.data(&d); let mut p = Params::default(); p.u[0][0] = drops.len() as u32; p.f[0] = [wavelen, width, pr.f("displacement") as f32 * s * 4.0, pr.f("shading/roughness").max(0.001) as f32]; p.f[1] = [l[0], l[1], l[2], pr.f("light/lightIntensity") as f32 / 100.0]; p.f[2] = [hv[0], hv[1], hv[2], pr.f("shading/specular") as f32 / 100.0]; p.f[3] = [lc[0], lc[1], lc[2], 0.0]; p.f[4] = [pr.f("shading/ambient") as f32 / 100.0, pr.f("shading/diffuse") as f32 / 100.0, 0.0, 0.0]; let out = e.scratch(b.img.width, b.img.height); e.pixels("fxm_drizzle", &p, &b.img, None, &out, Some(&buf)); Some(GBuf { img: out, ..b }) } // ---------------------------------------------------------------- Wave World (Height Map) fn wave_height(e: &mut Enc, ctx: &EffectCtx, b: GBuf, st: &effectcraft_effects::Waves, depth: Option<&[f32]>) -> Option { let pr = ctx.params; let mut d = st.u.clone(); if let Some(dp) = depth { d.extend_from_slice(dp); } let buf = e.data(&d); let mut p = Params::default(); let dry_transparent = pr.e("heightMapControls/renderDryAreasAs") == 1 && depth.is_some(); p.u[0] = [st.nx as u32, st.ny as u32, dry_transparent as u32, 0]; p.f[0] = [ pr.f("heightMapControls/brightness") as f32, pr.f("heightMapControls/contrast") as f32, pr.f("heightMapControls/gamma").max(0.01) as f32, 1.0 - pr.f("heightMapControls/transparency") as f32, ]; p.f[1] = [b.offset[0] as f32, b.offset[1] as f32, b.scale as f32, ctx.layer_size[0].max(1.0) as f32]; let out = e.scratch(b.img.width, b.img.height); e.pixels("fxm_wave", &p, &b.img, None, &out, Some(&buf)); Some(GBuf { img: out, ..b }) } // ---------------------------------------------------------------- pieces fn pieces(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { // Planning reads the layer (gradient maps and textures default to it). let img = e.download(&b.img)?; let cpu = effectcraft_effects::Buf { img, offset: b.offset, scale: b.scale }; if id == "ec.sim.shatter" { // The rendered pieces or the wireframe views' lines. return match effectcraft_effects::pixel_plan(id, ctx, &cpu)? { effectcraft_effects::PixelPlan::Pieces(plan) => piece_pass(e, b, &plan), effectcraft_effects::PixelPlan::Sprites(plan) => sprites(e, b, &plan), }; } let Some(plan) = effectcraft_effects::piece_plan(id, ctx, &cpu) else { // Card Wipe before anything moves: the layer as is. return (id == "ec.transition.cardwipe").then_some(b); }; piece_pass(e, b, &plan) } /// Draw a piece plan over a transparent frame of `b`'s size (sim2::PiecePlan::finish). pub(crate) fn piece_pass(e: &mut Enc, b: GBuf, plan: &effectcraft_effects::PiecePlan) -> Option { let (w, h) = (b.img.width, b.img.height); let tex = |t: &PieceTex| -> Option { match t { PieceTex::Layer => Some(b.img.clone()), PieceTex::Image(i) => e.g.upload_image(i), } }; let front = tex(&plan.front)?; let back = match &plan.back { Some(t) => Some(tex(t)?), None => None, }; let mut items = Items::new(48, w, h); // Without a back texture the pieces can be drawn in passes over a base (CC Pixel Polly's // fine grids). let mut ch = back.is_none().then(|| crate::fx_particles::Chunked::new(4, 0, 48, w, h)); for q in &plan.pieces { let m = q.inv; let mut rec = vec![11.0]; for r in m { rec.extend(r.map(|v| v as f32)); } rec.extend(q.c0); for v in q.poly { rec.extend(v); } rec.extend([q.n as f32, q.shade]); rec.extend(q.tint); rec.extend(q.spec); rec.push(q.alpha); match q.flat { Some(c) => rec.extend([1.0, c[0], c[1], c[2]]), None => rec.extend([0.0; 4]), } rec.extend([q.back as u32 as f32, q.back_mirror as f32]); match &mut ch { Some(ch) => ch.push(e, &front, Some(q.bbox), &rec), None => items.push(Some(q.bbox), &rec), } } if let Some(ch) = ch { return Some(GBuf { img: ch.finish(e, &front)?, ..b }); } let mut p = Params::default(); p.u[1][1] = back.is_some() as u32; let img = raster(e, items, 3, false, &front, back.as_ref(), p, w, h)?; Some(GBuf { img, ..b }) } // ---------------------------------------------------------------- Caustics fn caustics(e: &mut Enc, ctx: &EffectCtx, b: GBuf, geo: &effectcraft_effects::Buf) -> Option { let st = effectcraft_effects::caustics_setup(ctx, geo); let (w, h) = (b.img.width, b.img.height); let mut bottom = match &st.bottom { Some(i) => e.g.upload_image(i)?, None => b.img.clone(), }; if st.bottom_sigma > 0.0 { bottom = gaussian_blur(e, &bottom, st.bottom_sigma, st.bottom_sigma, false); } let height = match &st.height { Some(pl) => { let mut img = Image::new(pl.w as u32, pl.h as u32); for (px, v) in img.data.iter_mut().zip(&pl.data) { *px = [*v, 0.0, 0.0, 0.0]; } Some(e.g.upload_image(&img)?) } None => None, }; let sky = match &st.sky { Some(i) => { let g = e.g.upload_image(i)?; Some((e.image_rows(&g), g.width, g.height)) } None => None, }; let distant = { let lw = st.lw.max(1.0); let v = [(st.lx - st.cx) / lw, (st.ly - st.cy) / lw, st.lheight]; 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 mut p = Params::default(); p.u[0] = [height.is_some() as u32, st.repeat, st.point as u32, st.sky_repeat]; p.u[1] = [sky.is_some() as u32, sky.as_ref().map_or(0, |s| s.0.1), sky.as_ref().map_or(0, |s| s.1), sky.as_ref().map_or(0, |s| s.2)]; p.f[0] = [st.scaling as f32, st.wave_h as f32, st.k as f32, st.lw as f32]; p.f[1] = [st.cx as f32, st.cy as f32, st.lx as f32, st.ly as f32]; p.f[2] = [st.surf[0], st.surf[1], st.surf[2], st.surf_op]; p.f[3] = [st.lc[0], st.lc[1], st.lc[2], st.li]; p.f[4] = [st.lheight as f32, st.cstr, st.ambient, st.diffuse]; p.f[5] = [st.specular, st.sharp, st.sky_scaling as f32, st.sky_int]; p.f[6] = [distant[0], distant[1], distant[2], st.convergence as f32]; let out = e.scratch(w, h); e.pixels("fxm_caustics", &p, &bottom, height.as_ref(), &out, sky.as_ref().map(|s| &s.0.0)); Some(GBuf { img: out, ..b }) }