effectcraft/crates/gpu/src/fx_sim.rs

480 lines
21 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

//! 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<GBuf> {
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<f32>,
w: i32,
h: i32,
tiles: Vec<Vec<u32>>,
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<f32>, 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<GpuImage> {
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<GBuf> {
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<GpuImage> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<f32> = 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<GBuf> {
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<GBuf> {
// 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<GBuf> {
let (w, h) = (b.img.width, b.img.height);
let tex = |t: &PieceTex| -> Option<GpuImage> {
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<GBuf> {
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 })
}