M13.23: GPU simulation render passes

CC Rainfall, Snowfall, Star Burst, Bubbles, Drizzle, Hair, Mr. Mercury,
Caustics, Wave World, Foam, Shatter, Card Dance and Card Wipe render on the GPU
(fx_sim.rs / fx_sim.wgsl). The per-frame simulation / layout stays on the CPU
and is shared with the CPU effect through plans (effects::sprite_plan,
piece_plan, bubble_list, mercury_blobs, drizzle_drops, wave_world_plan,
caustics_setup); a tiled item rasteriser composites sprites, bubbles, blob
fields and textured pieces in the CPU's order and shading. Layer-dependent
sprite colours (refracting rain, star-burst and hair roots) are sampled per
pixel; piece plans read the frame back once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-03 18:35:22 -04:00
co-authored by Claude Opus 5.5
parent 2e5ea051a8
commit 1ec9336d7c
12 changed files with 1796 additions and 206 deletions
+286 -82
View File
@@ -80,31 +80,78 @@ impl Cam {
}
/// A planar textured piece ready to draw.
#[derive(Clone, Copy)]
pub(crate) struct Piece {
#[derive(Clone, Copy, Debug)]
pub struct Piece {
/// Screen (buffer px, homogeneous) → piece-local (a, b, w).
inv: M3,
pub inv: [[f64; 3]; 3],
/// Rest centre of the piece in texture pixels.
c0: [f32; 2],
pub c0: [f32; 2],
/// Convex polygon (texture pixels), counter-clockwise in y-down space.
poly: [[f32; 2]; 6],
n: u8,
bbox: [f32; 4],
shade: f32,
pub poly: [[f32; 2]; 6],
pub n: u8,
pub bbox: [f32; 4],
pub shade: f32,
/// Lighting: colour multiplier and additive specular highlight.
tint: [f32; 3],
spec: [f32; 3],
pub(crate) alpha: f32,
pub tint: [f32; 3],
pub spec: [f32; 3],
pub alpha: f32,
/// Flat straight colour instead of the texture.
pub(crate) flat: Option<[f32; 3]>,
pub flat: Option<[f32; 3]>,
/// Facing away from the camera (draw the back texture).
pub(crate) back: bool,
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(crate) back_mirror: u8,
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<Piece>,
pub front: PieceTex,
pub back: Option<PieceTex>,
}
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<PiecePlan> {
match id {
"ec.sim.carddance" => Some(card_dance_plan(ctx, b)),
"ec.sim.shatter" if ctx.params.e("view") == 0 => shatter_impl(ctx, b, true).err(),
"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`.
@@ -462,22 +509,25 @@ fn card_source(src: u32, g1: [f32; 4], g2: [f32; 4]) -> f32 {
}
}
fn card_dance(ctx: &EffectCtx, mut b: Buf) -> Buf {
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 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 cam = crate::card3d::projection(ctx, b);
let eye = cam.eye();
let light = Lighting::from(ctx, &b);
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 src = b.img.clone();
let mut pieces: Vec<Piece> = (0..rows * cols)
.into_par_iter()
.filter_map(|i| {
@@ -502,10 +552,7 @@ fn card_dance(ctx: &EffectCtx, mut b: Buf) -> Buf {
})
.collect();
sort_far_first(&mut pieces);
let mut out = Image::new(src.width, src.height);
draw_pieces(&mut out, &pieces, &src, back.as_ref());
b.img = out;
b
PiecePlan { pieces, front: PieceTex::Layer, back: back.map(PieceTex::Image) }
}
// ------------------------------------------------------------------ Shatter
@@ -638,7 +685,15 @@ fn map_key(c: [f32; 4]) -> u32 {
q(c[0]) << 8 | q(c[1]) << 4 | q(c[2])
}
fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
fn shatter(ctx: &EffectCtx, b: Buf) -> Buf {
match shatter_impl(ctx, &b, false) {
Ok(img) => Buf { img, ..b },
Err(plan) => plan.finish(b),
}
}
/// Shatter: the frame (`Ok`), or with `plan_only` in the Rendered view its pieces (`Err`).
fn shatter_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result<Image, PiecePlan> {
let pr = ctx.params;
let view = pr.e("view");
let render = pr.e("render");
@@ -666,21 +721,21 @@ fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
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 cam = crate::card3d::projection(ctx, b);
let eye = cam.eye();
let light = Lighting::from(ctx, &b);
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 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 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];
@@ -843,12 +898,18 @@ fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
})
.collect();
sort_far_first(&mut pieces);
let mut out = Image::new(src.width, src.height);
let front_img = front.img.as_ref().unwrap_or(&src);
let back_img = backf.img.as_ref().unwrap_or(front_img);
if view == 0 {
draw_pieces(&mut out, &pieces, front_img, Some(back_img));
} else {
let tex = |i: &Option<Image>| 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());
let plan = PiecePlan { pieces, front: front_t, back: Some(back_t) };
if plan_only {
return Err(plan);
}
return Ok(plan.finish(Buf { img: src, offset: b.offset, scale: b.scale }).img);
}
let out;
{
// Wireframes: piece outlines (front view: at rest; otherwise animated), plus forces.
let animated = view == 2 || view == 4;
let mut lines = Vec::new();
@@ -889,8 +950,7 @@ fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
}
out = splat(src.width, src.height, &lines, Acc::Over);
}
b.img = out;
b
Ok(out)
}
// ------------------------------------------------------------------ Caustics
@@ -911,35 +971,62 @@ fn sample_repeat(img: &Image, x: f64, y: f64, mode: u32) -> Px {
}
}
fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
/// 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<Image>,
/// 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<Plane>,
/// The Sky layer fitted to the buffer.
pub sky: Option<Image>,
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 mut bottom = layer_or_self(ctx, &b, "bottom/bottom", true, pr.e("bottom/bottomSizeDiffers") == 1);
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;
if blur > 0.05 {
bottom = effectcraft_raster::gaussian_blur(&bottom, blur * 0.5, blur * 0.5, false);
}
let scaling = pr.f("bottom/scaling").max(0.01);
let repeat = pr.e("bottom/repeatMode");
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 surf = pr.color("water/surfaceColor");
let surf_op = pr.f("water/surfaceOpacity") as f32;
let cstr = pr.f("water/causticsStrength") as f32;
let li = pr.f("lighting/lightIntensity") as f32;
let lc = pr.color("lighting/lightColor");
let lpos = pr.v2("lighting/lightPosition");
let lheight = pr.f("lighting/lightHeight").max(0.01);
let ambient = pr.f("lighting/ambientLight") as f32;
let diffuse = pr.f("material/diffuse") as f32;
let specular = pr.f("material/specular") as f32;
let sharp = pr.f("material/highlightSharpness").max(1.0) as f32;
let (w, hh) = (b.img.width as usize, b.img.height as usize);
// 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 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 }
});
@@ -947,6 +1034,7 @@ fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
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");
@@ -962,6 +1050,75 @@ fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
(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]
@@ -973,14 +1130,8 @@ fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
norm3([(lx - cx) / lw.max(1.0), (ly - cy) / lw.max(1.0), lheight])
}
};
// 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));
let sky_scaling = pr.get("sky/scaling").map(Value::as_f64).unwrap_or(1.0).max(0.01);
let sky_repeat = pr.e("sky/repeatMode");
let sky_int = pr.get("sky/intensity").map(Value::as_f64).unwrap_or(0.3) as f32;
let convergence = pr.get("sky/convergence").map(Value::as_f64).unwrap_or(0.5).clamp(0.0, 1.0);
let grad = |x: usize, y: usize| -> (f64, f64) {
match &height {
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;
@@ -1052,12 +1203,13 @@ fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
// ------------------------------------------------------------------ Wave World
/// Wave World's simulated surface: an `nx` × `ny` grid of heights `u` and velocities `v`.
#[derive(Clone, Debug)]
struct Waves {
nx: usize,
ny: usize,
u: Vec<f32>,
v: Vec<f32>,
pub struct Waves {
pub nx: usize,
pub ny: usize,
pub u: Vec<f32>,
pub v: Vec<f32>,
}
struct Producer {
@@ -1091,7 +1243,27 @@ impl Producer {
static WAVE_CACHE: SimCache<Waves> = SimCache::new(4);
fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
/// 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<Waves>, depth: Option<Vec<f32>> },
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<WavePlan> {
wave_world_impl(ctx, b, true).err()
}
fn wave_world_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result<Image, WavePlan> {
let pr = ctx.params;
let [lw, lh] = ctx.layer_size;
let res = pr.f("simulation/gridResolution").clamp(1.0, 400.0) as usize;
@@ -1239,7 +1411,11 @@ fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
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;
@@ -1251,7 +1427,7 @@ fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
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
};
b.img.rows_mut().for_each(|(y, row)| {
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;
@@ -1260,6 +1436,7 @@ fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
*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.
@@ -1291,10 +1468,12 @@ fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
}
}
}
let wire = splat(bw, bh, &lines, Acc::Over);
b.img = crate::sim::combine(&Image::filled(bw, bh, [0.0, 0.0, 0.0, 1.0]), &wire, 0);
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)
}
b
}
// ------------------------------------------------------------------ Foam
@@ -1336,7 +1515,29 @@ fn foam_pop_age(id: u32, lifespan: f32, frailty: f32, seed: u32) -> f32 {
/// 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, mut b: Buf) -> Buf {
fn foam(ctx: &EffectCtx, b: Buf) -> Buf {
match foam_impl(ctx, &b, false) {
Ok(img) => Buf { img, ..b },
Err(plan) => plan.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<crate::sim::SpritePlan> {
let pr = ctx.params;
let set = |id: &str| matches!(pr.get(id), Some(Value::Layer(Some(_))));
let view = pr.e("view");
if (view == 2 && pr.e("rendering/bubbleTexture") == 5)
|| (view == 2 && pr.f("rendering/reflectionStrength") > 0.0 && set("rendering/environmentMap"))
|| (view == 1 && set("flowMap/flowMap"))
{
return None;
}
foam_impl(ctx, b, true).err()
}
fn foam_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result<Image, crate::sim::SpritePlan> {
let pr = ctx.params;
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
let unit = lw * 0.01;
@@ -1558,7 +1759,11 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
sp
})
.collect();
let mut out = splat(b.img.width, b.img.height, &sprites, if blend == 0 && view == 2 { Acc::Add } else { Acc::Over });
let acc = if blend == 0 && view == 2 { Acc::Add } else { Acc::Over };
if plan_only {
return Err(crate::sim::SpritePlan { sprites, tints: vec![], acc, post: crate::sim::Post::Replace });
}
let mut out = splat(b.img.width, b.img.height, &sprites, acc);
// User Defined: the texture layer fills each bubble's disc.
if let Some(tex) = &user_tex {
for q in &order {
@@ -1581,7 +1786,7 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
&& strength > 0.0
&& let Some(env) = ctx.layer_param("rendering/environmentMap", true)
{
let env_img = crate::util::fit_layer(ctx, &b, &env, true);
let env_img = crate::util::fit_layer(ctx, b, &env, true);
let conv = pr.f("rendering/reflectionConvergence").clamp(0.0, 1.0);
let (w, h) = (b.img.width as f64, b.img.height as f64);
for q in &order {
@@ -1602,7 +1807,7 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
if view == 1
&& let Some(lp) = &flow
{
let fm = crate::util::fit_layer(ctx, &b, lp, true);
let fm = crate::util::fit_layer(ctx, b, lp, true);
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 {
@@ -1610,8 +1815,7 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
}
});
}
b.img = out;
b
Ok(out)
}
/// Add `f(u, v)` (premultiplied; u, v ∈ −1..1 across the disc) over the disc of radius `r` at