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
+2
View File
@@ -316,6 +316,7 @@ impl GpuContext {
include_str!("shaders/fx_extra.wgsl"),
include_str!("shaders/fx_depth.wgsl"),
include_str!("shaders/fx_lut.wgsl"),
include_str!("shaders/fx_sim.wgsl"),
include_str!("shaders/fx_vr.wgsl"),
]
.concat();
@@ -378,6 +379,7 @@ impl GpuContext {
.chain(crate::fx_extra::KERNELS)
.chain(crate::fx_depth::KERNELS)
.chain(crate::fx_lut::KERNELS)
.chain(crate::fx_sim::KERNELS)
.chain(crate::fx_vr::KERNELS)
.map(|e| (e, &layout))
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
+1
View File
@@ -135,6 +135,7 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
_ if crate::fx_extra::IDS.contains(&id) => crate::fx_extra::apply(e, id, ctx, b),
_ if crate::fx_depth::IDS.contains(&id) => crate::fx_depth::apply(e, id, ctx, b),
_ if crate::fx_lut::IDS.contains(&id) => crate::fx_lut::apply(e, id, ctx, b),
_ if crate::fx_sim::IDS.contains(&id) => crate::fx_sim::apply(e, id, ctx, b),
_ if crate::fx_vr::IDS.contains(&id) => crate::fx_vr::apply(e, id, ctx, b),
_ => pointwise(e, id, ctx, b),
}
+424
View File
@@ -0,0 +1,424 @@
//! 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. Settings drawn otherwise (Shatter's wireframe views,
//! Foam's User Defined texture, Environment Map and flow-map preview) render on the CPU.
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.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).
struct Items {
stride: usize,
recs: Vec<f32>,
w: i32,
h: i32,
tiles: Vec<Vec<u32>>,
tx: i32,
}
impl Items {
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 }
}
/// Add an item (`rec` = its fields, without the bounds) covering float bounds `bb`
/// (`None` or empty = skipped, as sim::raster).
fn push(&mut self, bb: Option<[f32; 4]>, rec: &[f32]) {
let Some(b) = bb else { return };
if !(b[0].is_finite() && b[1].is_finite() && b[2].is_finite() && b[3].is_finite()) {
return;
}
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;
}
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);
}
}
}
/// Kernel data: tile offsets (tiles + 1), item indices, items. `None` when too large.
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],
})
}
fn sprites(e: &mut Enc, b: GBuf, plan: &SpritePlan) -> Option<GBuf> {
let (w, h) = (b.img.width, b.img.height);
let mut items = Items::new(24, 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]);
}
}
items.push(sprite_bbox(s.x, s.y, s.r, s.c[3], s.shape), &rec);
}
let fx = raster(e, items, 0, plan.acc == Acc::Add, &b.img, None, Params::default(), w, h)?;
let mut p = Params::default();
match plan.post {
Post::Combine(m) => p.u[0] = [0, m, 0, 0],
Post::Replace => p.u[0] = [0, 5, 0, 0],
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 })
}
// ---------------------------------------------------------------- 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 };
let Some(plan) = effectcraft_effects::piece_plan(id, ctx, &cpu) else {
// Card Wipe before anything moves: the layer as is; Shatter's wireframes: the CPU.
return (id == "ec.transition.cardwipe").then_some(b);
};
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);
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]);
items.push(Some(q.bbox), &rec);
}
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 })
}
+3
View File
@@ -55,6 +55,7 @@ mod fx_generate;
mod fx_key;
mod fx_lut;
mod fx_noise;
mod fx_sim;
mod fx_stylize;
mod fx_tone;
mod fx_vr;
@@ -218,6 +219,8 @@ mod tests_fx_lut;
#[cfg(test)]
mod tests_fx_noise;
#[cfg(test)]
mod tests_fx_sim;
#[cfg(test)]
mod tests_fx_stylize;
#[cfg(test)]
mod tests_fx_tone;
+653
View File
@@ -0,0 +1,653 @@
// ---------------------------------------------------------------- simulation render passes (fx_sim.rs)
//
// Items (sprites, CC Bubbles bubbles, Mr. Mercury blobs, textured pieces) are binned into 16 × 16
// tiles: data = tile offsets, item indices, then the item records (u[0].w floats each, the last
// four the item's integer pixel bounds). Every shading step mirrors effects::sim / sim2.
fn fxm_unpremul(p: vec4<f32>) -> vec4<f32> {
if (p.w > 1e-6) {
return vec4<f32>(p.xyz / p.w, p.w);
}
return vec4<f32>(0.0, 0.0, 0.0, max(p.w, 0.0));
}
fn fxm_aa(edge: f32) -> f32 {
return clamp(edge + 0.5, 0.0, 1.0);
}
struct FxmShade {
ok: bool,
c: vec4<f32>,
}
fn fxm_none() -> FxmShade {
return FxmShade(false, vec4<f32>(0.0));
}
// sim::sprite_shade, after resolving the sprite's tint (sim::SpritePlan::resolve) against `src`.
fn fxm_sprite(at: u32, x: f32, y: f32) -> FxmShade {
let kind = u32(data[at]);
let sx = data[at + 1u];
let sy = data[at + 2u];
let r0 = data[at + 3u];
var c = vec4<f32>(data[at + 4u], data[at + 5u], data[at + 6u], data[at + 7u]);
let tk = u32(data[at + 11u]);
if (tk != 0u) {
let smp = sample_bilinear(src, data[at + 12u], data[at + 13u]);
let s = fxm_unpremul(smp);
if (tk == 1u) {
if (s.w > 0.0) {
c = vec4<f32>((s.xyz * 0.6 + 0.5) * c.xyz, c.w);
}
} else if (tk == 2u) {
if (s.w <= 0.0) {
c = vec4<f32>(0.0);
} else {
c = vec4<f32>(s.xyz, s.w * c.w);
}
} else {
if (smp.w < 0.5) {
c = vec4<f32>(0.0);
} else {
let hair = vec3<f32>(data[at + 14u], data[at + 15u], data[at + 16u]);
let inherit = data[at + 17u];
let base = s.xyz + (hair - s.xyz) * (1.0 - inherit);
c = vec4<f32>(base * data[at + 18u] + data[at + 19u], c.w);
}
}
}
if (c.w <= 1e-5) {
return fxm_none();
}
var r = r0;
var fade = 1.0;
if (r0 < 0.5) {
r = 0.5;
fade = r0 / 0.5;
}
let dx = x - sx;
let dy = y - sy;
var rgb = c.xyz;
var cov = 0.0;
switch (kind) {
case 0u: {
let d2 = (dx * dx + dy * dy) / (r * r);
if (d2 >= 1.0) {
return fxm_none();
}
let q = 1.0 - d2;
cov = q * q;
}
case 1u: {
cov = fxm_aa(r - sqrt(dx * dx + dy * dy));
}
case 2u: {
let d = sqrt(dx * dx + dy * dy);
let cv = fxm_aa(r - d);
if (cv <= 0.0) {
return fxm_none();
}
let nx = dx / r;
let ny = dy / r;
let nz = sqrt(max(1.0 - nx * nx - ny * ny, 0.0));
let diff = max(nx * -0.48 + ny * -0.58 + nz * 0.66, 0.0);
let rz = 2.0 * diff * nz - 0.66;
let spec = powz(max(rz, 0.0), 20.0) * 0.6;
let k = 0.2 + 0.8 * diff;
rgb = rgb * k + spec;
cov = cv;
}
case 3u: {
let d = sqrt(dx * dx + dy * dy) / r;
cov = powz(max(1.0 - d, 0.0), 1.5);
}
case 4u: {
let d = sqrt(dx * dx + dy * dy) / r;
if (d > 1.0 + 1.0 / r) {
return fxm_none();
}
let rr = (d - 0.9) / 0.12;
let ring = max(1.0 - rr * rr, 0.0);
let hx = dx / r + 0.4;
let hy = dy / r + 0.4;
let hl = max(1.0 - (hx * hx + hy * hy) / 0.04, 0.0);
cov = min(0.12 * select(0.0, 1.0, d <= 1.0) + ring * 0.8 + hl, 1.0);
}
case 5u: {
let sn = sin(data[at + 8u]);
let cs = cos(data[at + 8u]);
let ux = (dx * cs + dy * sn) / r;
let uy = (-dx * sn + dy * cs) / r;
let d = sqrt(ux * ux + uy * uy);
let core = max(1.0 - d, 0.0) * max(1.0 - d, 0.0);
let stx = max(1.0 - abs(ux) / 2.0, 0.0) * max(1.0 - abs(uy) * 6.0, 0.0);
let sty = max(1.0 - abs(uy) / 2.0, 0.0) * max(1.0 - abs(ux) * 6.0, 0.0);
cov = min(core + stx + sty, 1.0);
}
case 6u: {
let sn = sin(data[at + 8u]);
let cs = cos(data[at + 8u]);
let ux = dx * cs + dy * sn;
let uy = -dx * sn + dy * cs;
let e = r * 0.75;
cov = fxm_aa(e - abs(ux)) * fxm_aa(e - abs(uy));
}
case 7u: {
let sn = sin(data[at + 8u]);
let cs = cos(data[at + 8u]);
let ux = dx * cs + dy * sn;
let uy = -dx * sn + dy * cs;
let e = r * 0.5;
var m = 3.4e38;
for (var k = 0; k < 3; k++) {
let a = 1.5707964 + f32(k) * 6.2831855 / 3.0;
m = min(m, e - (ux * cos(a) + uy * sin(a)));
}
cov = fxm_aa(m);
}
default: {
let lx = data[at + 9u];
let ly = data[at + 10u];
let l2 = lx * lx + ly * ly;
var t = 0.0;
if (l2 > 0.0) {
t = clamp((dx * lx + dy * ly) / l2, 0.0, 1.0);
}
let px = dx - lx * t;
let py = dy - ly * t;
cov = fxm_aa(r - sqrt(px * px + py * py));
}
}
let a = cov * c.w * fade;
if (a <= 1e-6) {
return fxm_none();
}
return FxmShade(true, vec4<f32>(rgb * a, a));
}
// CC Bubbles' bubble (sim::bubbles). u[1].y = metal, u[1].z = shading type.
fn fxm_bubble(at: u32, x: f32, y: f32) -> FxmShade {
let qx = data[at + 1u];
let qy = data[at + 2u];
let qr = data[at + 3u];
let dx = (x - qx) / max(qr, 0.5);
let dy = (y - qy) / max(qr, 0.5);
let d2 = dx * dx + dy * dy;
let d = sqrt(d2);
let cov = fxm_aa((1.0 - d) * qr);
if (cov <= 0.0) {
return fxm_none();
}
let k = select(-0.7, 1.6, P.u[1].y != 0u);
let nz = sqrt(max(1.0 - d2, 0.0));
let s = fxm_unpremul(sample_bilinear(src, data[at + 4u] + dx * qr * k, data[at + 5u] + dy * qr * k));
var c = s.xyz;
if (s.w <= 0.0) {
c = vec3<f32>(0.6, 0.7, 0.8);
}
var a = cov;
switch (P.u[1].z) {
case 1u: {
c = c * 0.7 + 0.3 * (1.0 - nz);
}
case 2u: {
c = c * (0.5 + 0.5 * nz);
}
case 3u: {
a *= powz(nz, 0.5);
}
case 4u: {
a *= 1.0 - nz * 0.8;
}
default: {}
}
let hx = dx + 0.35;
let hy = dy + 0.35;
let hl = max(1.0 - (hx * hx + hy * hy) / 0.05, 0.0) * 0.8;
c = c + hl;
return FxmShade(true, vec4<f32>(c * a, a));
}
// Mr. Mercury's blob field (f, ∂f/∂x, ∂f/∂y). f[0].x = 1 + influence.
fn fxm_blob(at: u32, x: f32, y: f32) -> FxmShade {
let dx = x - data[at + 1u];
let dy = y - data[at + 2u];
let rr = data[at + 3u] * 2.0 * P.f[0].x;
let d2 = (dx * dx + dy * dy) / (rr * rr);
if (d2 >= 1.0) {
return fxm_none();
}
let k = 1.0 - d2;
let g = -6.0 * k * k / (rr * rr);
return FxmShade(true, vec4<f32>(k * k * k, g * dx, g * dy, 0.0));
}
// A textured piece (sim2::draw_pieces): src = front texture, aux = back (u[1].y = 1 when set).
fn fxm_piece(at: u32, x: f32, y: f32) -> FxmShade {
let m0 = vec3<f32>(data[at + 1u], data[at + 2u], data[at + 3u]);
let m1 = vec3<f32>(data[at + 4u], data[at + 5u], data[at + 6u]);
let m2 = vec3<f32>(data[at + 7u], data[at + 8u], data[at + 9u]);
let w = m2.x * x + m2.y * y + m2.z;
if (abs(w) < 1e-12) {
return fxm_none();
}
let c0 = vec2<f32>(data[at + 10u], data[at + 11u]);
var u = (m0.x * x + m0.y * y + m0.z) / w + c0.x;
var v = (m1.x * x + m1.y * y + m1.z) / w + c0.y;
let n = u32(data[at + 24u]);
var md = 3.4e38;
for (var i = 0u; i < n; i++) {
let j = (i + 1u) % n;
let ax = data[at + 12u + i * 2u];
let ay = data[at + 13u + i * 2u];
let ex = data[at + 12u + j * 2u] - ax;
let ey = data[at + 13u + j * 2u] - ay;
let l = max(sqrt(ex * ex + ey * ey), 1e-6);
md = min(md, (ex * (v - ay) - ey * (u - ax)) / l);
}
let cov = clamp(md + 1.0, 0.0, 1.0);
if (cov <= 0.0) {
return fxm_none();
}
var c: vec3<f32>;
var a = 1.0;
if (data[at + 33u] != 0.0) {
c = vec3<f32>(data[at + 34u], data[at + 35u], data[at + 36u]);
} else {
let back = data[at + 37u] != 0.0;
let mirror = u32(data[at + 38u]);
if (back && mirror == 1u) {
v = 2.0 * c0.y - v;
} else if (back && mirror == 2u) {
u = 2.0 * c0.x - u;
}
var t: vec4<f32>;
if (back && P.u[1].y != 0u) {
t = sample_bilinear(aux, u, v);
} else {
t = sample_bilinear(src, u, v);
}
let s = fxm_unpremul(t);
c = s.xyz;
a = s.w;
}
a = a * cov * data[at + 32u];
if (a <= 1e-6) {
return FxmShade(false, vec4<f32>(0.0));
}
let tint = vec3<f32>(data[at + 26u], data[at + 27u], data[at + 28u]);
let spec = vec3<f32>(data[at + 29u], data[at + 30u], data[at + 31u]);
let o = c * data[at + 25u] * tint + spec;
return FxmShade(true, vec4<f32>(o * a, a));
}
// sim::raster over a transparent frame. u[0] = (kind: 0 sprites, 1 bubbles, 2 blob field,
// 3 pieces; additive; items offset; item stride); u[1].x = tiles across.
@compute @workgroup_size(16, 16)
fn fxm_raster(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let tile = u32(p.y / 16) * P.u[1].x + u32(p.x / 16);
let start = u32(data[tile]);
let end = u32(data[tile + 1u]);
let stride = P.u[0].w;
let add = P.u[0].y != 0u;
let x = f32(p.x) + 0.5;
let y = f32(p.y) + 0.5;
var d = vec4<f32>(0.0);
for (var k = start; k < end; k++) {
let at = P.u[0].z + u32(data[k]) * stride;
let bb = at + stride - 4u;
if (f32(p.x) < data[bb] || f32(p.y) < data[bb + 1u] || f32(p.x) > data[bb + 2u] || f32(p.y) > data[bb + 3u]) {
continue;
}
var s: FxmShade;
switch (P.u[0].x) {
case 0u: {
s = fxm_sprite(at, x, y);
}
case 1u: {
s = fxm_bubble(at, x, y);
}
case 2u: {
s = fxm_blob(at, x, y);
}
default: {
s = fxm_piece(at, x, y);
}
}
if (!s.ok) {
continue;
}
let c = s.c;
if (add) {
d = vec4<f32>(d.xyz + c.xyz, min(d.w + c.w, 1.0));
} else {
d = c + d * (1.0 - c.w);
}
}
textureStore(out, p, d);
}
// The sprite layer (aux) meets the original (src). u[0].x = 0: sim::combine with mode u[0].y;
// 1: mixed toward the original by f[0].x; 2: over opaque black.
@compute @workgroup_size(16, 16)
fn fxm_post(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let s = textureLoad(src, p, 0);
let f = textureLoad(aux, p, 0);
var o = f;
if (P.u[0].x == 1u) {
o = f + (s - f) * P.f[0].x;
} else if (P.u[0].x == 2u) {
o = vec4<f32>(f.xyz, f.w + (1.0 - f.w));
} else {
switch (P.u[0].y) {
case 0u: {
o = f + s * (1.0 - f.w);
}
case 1u: {
o = s + f - s * f;
}
case 2u: {
o = vec4<f32>(s.xyz + f.xyz, min(s.w + f.w, 1.0));
}
case 3u: {
o = vec4<f32>(max(s.xyz, f.xyz), s.w + f.w - s.w * f.w);
}
case 4u: {
let fu = fxm_unpremul(f);
let m = s.xyz - (s.xyz - min(s.xyz, fu.xyz * s.w)) * fu.w;
o = vec4<f32>(m, s.w);
}
default: {}
}
}
textureStore(out, p, o);
}
fn fxm_norm3(v: vec3<f32>) -> vec3<f32> {
let l = sqrt(v.x * v.x + v.y * v.y + v.z * v.z);
if (l > 1e-9) {
return v / l;
}
return vec3<f32>(0.0, -1.0, 0.0);
}
// Mr. Mercury's shading (src = layer, aux = blob field). f[0] = (ambient, diffuse, specular,
// roughness); f[1] = (metal, light intensity, render scale); f[2] = light direction; f[3] = colour.
@compute @workgroup_size(16, 16)
fn fxm_mercury(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let fv = textureLoad(aux, p, 0);
let f = fv.x;
let thr = 0.35;
if (f <= thr * 0.8) {
textureStore(out, p, vec4<f32>(0.0));
return;
}
let cov = clamp((f - thr * 0.8) / (thr * 0.4), 0.0, 1.0);
let n = fxm_norm3(vec3<f32>(fv.y * 3.0, fv.z * 3.0, 1.0 / (1.0 + f)));
let s = P.f[1].z;
let smp = fxm_unpremul(sample_bilinear_clamped(src, f32(p.x) + 0.5 - n.x * 12.0 * s, f32(p.y) + 0.5 - n.y * 12.0 * s));
var base = smp.xyz;
if (smp.w <= 0.0) {
base = vec3<f32>(0.7, 0.7, 0.75);
}
let l = P.f[2].xyz;
let nl = max(dot(n, l), 0.0);
let hv = fxm_norm3(vec3<f32>(l.x, l.y, l.z + 1.0));
let li = P.f[1].y;
let spec = powz(max(dot(n, hv), 0.0), 1.0 / P.f[0].w) * P.f[0].z * li;
let shade = P.f[0].x + P.f[0].y * nl * li;
let metal = P.f[1].x;
let cc = base * shade + spec * (P.f[3].xyz * (1.0 - metal) + base * metal);
textureStore(out, p, vec4<f32>(cc * cov, cov));
}
// CC Drizzle's ripple height at (x, y): data = drops (x, y, ring radius, amplitude).
fn fxm_ripples(x: f32, y: f32) -> f32 {
var v = 0.0;
for (var i = 0u; i < P.u[0].x; i++) {
let dx = data[i * 4u];
let dy = data[i * 4u + 1u];
let ex = x - dx;
let ey = y - dy;
let d = sqrt(ex * ex + ey * ey);
let r = data[i * 4u + 2u];
let u = (d - r) / P.f[0].y;
if (abs(u) < 1.0) {
let env = 0.5 + 0.5 * cos(u * 3.1415927);
v += data[i * 4u + 3u] * env * cos((d - r) / P.f[0].x * 6.2831855);
}
}
return v;
}
// CC Drizzle. u[0].x = drops; f[0] = (wavelength, ring width, displacement, roughness);
// f[1] = (light direction, intensity); f[2] = (half vector, specular); f[3] = light colour;
// f[4] = (ambient, diffuse).
@compute @workgroup_size(16, 16)
fn fxm_drizzle(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let fx = f32(p.x) + 0.5;
let fy = f32(p.y) + 0.5;
let gx = fxm_ripples(fx + 1.0, fy) - fxm_ripples(fx - 1.0, fy);
let gy = fxm_ripples(fx, fy + 1.0) - fxm_ripples(fx, fy - 1.0);
if (gx == 0.0 && gy == 0.0) {
textureStore(out, p, textureLoad(src, p, 0));
return;
}
let smp = sample_bilinear_clamped(src, fx - gx * P.f[0].z, fy - gy * P.f[0].z);
let v = vec3<f32>(-gx * 6.0, -gy * 6.0, 1.0);
let n = v / sqrt(v.x * v.x + v.y * v.y + 1.0);
let l = P.f[1].xyz;
let li = P.f[1].w;
let nl = max(dot(n, l), 0.0);
let sp = powz(max(dot(n, P.f[2].xyz), 0.0), 1.0 / P.f[0].w) * P.f[2].w * li;
let shade = 1.0 + (P.f[4].x + P.f[4].y * nl - P.f[4].y * l.z) * li;
let a = smp.w;
textureStore(out, p, vec4<f32>(smp.xyz * shade + sp * P.f[3].xyz * a, a));
}
// Wave World's Height Map view. data = grid heights (nx × ny), then water depths when
// u[0].z (dry areas transparent); f[0] = (brightness, contrast, gamma, alpha);
// f[1] = (buffer offset, render scale, layer width).
@compute @workgroup_size(16, 16)
fn fxm_wave(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let nx = P.u[0].x;
let ny = P.u[0].y;
let lw = P.f[1].w;
let lx = (f32(p.x) + 0.5 - P.f[1].x) / P.f[1].z;
let ly = (f32(p.y) + 0.5 - P.f[1].y) / P.f[1].z;
let gx = clamp(lx / lw * f32(nx - 1u), 0.0, f32(nx - 1u));
let gy = clamp(ly / lw * f32(nx - 1u), 0.0, f32(ny - 1u));
let x0 = u32(floor(gx));
let y0 = u32(floor(gy));
let x1 = min(x0 + 1u, nx - 1u);
let y1 = min(y0 + 1u, ny - 1u);
let tx = gx - f32(x0);
let ty = gy - f32(y0);
let g00 = data[y0 * nx + x0];
let g10 = data[y0 * nx + x1];
let g01 = data[y1 * nx + x0];
let g11 = data[y1 * nx + x1];
let a = g00 + (g10 - g00) * tx;
let c = g01 + (g11 - g01) * tx;
let h = a + (c - a) * ty;
let v = powz(clamp(P.f[0].x + P.f[0].y * h, 0.0, 1.0), 1.0 / P.f[0].z);
var al = P.f[0].w;
if (P.u[0].z != 0u) {
let rx = u32(clamp(round_away(lx / lw * f32(nx - 1u)), 0.0, f32(nx - 1u)));
let ry = u32(clamp(round_away(ly / lw * f32(nx - 1u)), 0.0, f32(ny - 1u)));
if (data[nx * ny + ry * nx + rx] <= 0.0) {
al = 0.0;
}
}
textureStore(out, p, vec4<f32>(v * al, v * al, v * al, al));
}
// ---- Caustics
// The water surface's height gradient at (x, y) (aux = height plane, clamped; none = flat).
fn fxm_grad(x: i32, y: i32) -> vec2<f32> {
if (P.u[0].x == 0u) {
return vec2<f32>(0.0);
}
let gx = (tex_get_clamped(aux, x + 1, y).x - tex_get_clamped(aux, x - 1, y).x) * 0.5;
let gy = (tex_get_clamped(aux, x, y + 1).x - tex_get_clamped(aux, x, y - 1).x) * 0.5;
return vec2<f32>(gx, gy);
}
fn fxm_rem(v: f32, n: f32) -> f32 {
return v - n * floor(v / n);
}
fn fxm_mirror(v: f32, n: f32) -> f32 {
let r = fxm_rem(v, 2.0 * n);
return select(r, 2.0 * n - r, r > n);
}
// sim2::sample_repeat of `src`.
fn fxm_sample_repeat(x: f32, y: f32, mode: u32) -> vec4<f32> {
let d = vec2<f32>(textureDimensions(src));
if (mode == 1u) {
return sample_bilinear_clamped(src, fxm_rem(x, d.x), fxm_rem(y, d.y));
}
if (mode == 2u) {
return sample_bilinear_clamped(src, fxm_mirror(x, d.x), fxm_mirror(y, d.y));
}
return sample_bilinear(src, x, y);
}
// A sky texel from `data` (RGBA f32 rows of u[1].y pixels; u[1].zw = size).
fn fxm_sky_texel(x: i32, y: i32, clamped: bool) -> vec4<f32> {
let w = i32(P.u[1].z);
let h = i32(P.u[1].w);
var xi = x;
var yi = y;
if (clamped) {
xi = clamp(x, 0, w - 1);
yi = clamp(y, 0, h - 1);
} else if (x < 0 || y < 0 || x >= w || y >= h) {
return vec4<f32>(0.0);
}
let i = (u32(yi) * P.u[1].y + u32(xi)) * 4u;
return vec4<f32>(data[i], data[i + 1u], data[i + 2u], data[i + 3u]);
}
fn fxm_sky_bilinear(x: f32, y: f32, clamped: bool) -> vec4<f32> {
let fx = x - 0.5;
let fy = y - 0.5;
let x0 = floor(fx);
let y0 = floor(fy);
let tx = fx - x0;
let ty = fy - y0;
let xi = i32(x0);
let yi = i32(y0);
let a = fxm_sky_texel(xi, yi, clamped);
let b = fxm_sky_texel(xi + 1, yi, clamped);
let c = fxm_sky_texel(xi, yi + 1, clamped);
let d = fxm_sky_texel(xi + 1, yi + 1, clamped);
let top = a + (b - a) * tx;
let bot = c + (d - c) * tx;
return top + (bot - top) * ty;
}
fn fxm_sky_repeat(x: f32, y: f32, mode: u32) -> vec4<f32> {
let w = f32(P.u[1].z);
let h = f32(P.u[1].w);
if (mode == 1u) {
return fxm_sky_bilinear(fxm_rem(x, w), fxm_rem(y, h), true);
}
if (mode == 2u) {
return fxm_sky_bilinear(fxm_mirror(x, w), fxm_mirror(y, h), true);
}
return fxm_sky_bilinear(x, y, false);
}
// Caustics (sim2::caustics; src = the blurred bottom, aux = height plane, data = sky rows).
// u[0] = (height, repeat, point light, sky repeat); u[1] = (sky, row, width, height);
// f[0] = (scaling, wave height, displacement k, layer width); f[1] = (centre, light position);
// f[2] = (surface colour, opacity); f[3] = (light colour, intensity); f[4] = (light height,
// caustics strength, ambient, diffuse); f[5] = (specular, sharpness, sky scaling, sky
// intensity); f[6] = (distant light direction, convergence).
@compute @workgroup_size(16, 16)
fn fxm_caustics(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let k = P.f[0].z;
let x = p.x;
let y = p.y;
let dd = -fxm_grad(x, y) * k;
let dxr = -fxm_grad(min(x + 1, dims.x - 1), y).x * k;
let dyd = -fxm_grad(x, min(y + 1, dims.y - 1)).y * k;
let dxl = -fxm_grad(max(x - 1, 0), y).x * k;
let dyu = -fxm_grad(x, max(y - 1, 0)).y * k;
let jac = (1.0 + (dxr - dxl) * 0.5) * (1.0 + (dyd - dyu) * 0.5);
let focus = clamp(1.0 / max(abs(jac), 0.2) - 1.0, -1.0, 4.0);
let cx = P.f[1].x;
let cy = P.f[1].y;
let fx = f32(x) + 0.5;
let fy = f32(y) + 0.5;
let sx = cx + (fx + dd.x - cx) / P.f[0].x;
let sy = cy + (fy + dd.y - cy) / P.f[0].x;
let bot = fxm_unpremul(fxm_sample_repeat(sx, sy, P.u[0].y));
let g = fxm_grad(x, y);
let wh = P.f[0].y;
let v = vec3<f32>(-g.x * wh * 20.0, -g.y * wh * 20.0, 1.0);
let n = v / sqrt(v.x * v.x + v.y * v.y + 1.0);
var lv = P.f[6].xyz;
if (P.u[0].z != 0u) {
let lw = max(P.f[0].w, 1.0);
let q = vec3<f32>((P.f[1].z - fx) / lw, (P.f[1].w - fy) / lw, P.f[4].x);
lv = q / max(sqrt(q.x * q.x + q.y * q.y + q.z * q.z), 1e-12);
}
let li = P.f[3].w;
let nl = max(dot(n, lv), 0.0);
let hq = vec3<f32>(lv.x, lv.y, lv.z + 1.0);
let hv = hq / sqrt(hq.x * hq.x + hq.y * hq.y + hq.z * hq.z);
let spec = powz(max(dot(n, hv), 0.0), P.f[5].y) * P.f[5].x * li;
let ambient = P.f[4].z;
let diffuse = P.f[4].w;
let light = ambient + diffuse * li * nl + P.f[4].y * focus * li;
let surf = P.f[2].xyz;
let surf_op = P.f[2].w;
var surface: vec3<f32>;
if (P.u[1].x != 0u) {
let reach = (1.0 - P.f[6].w) * P.f[0].w * 0.5;
let rx = cx + (fx + n.x * reach - cx) / P.f[5].z;
let ry = cy + (fy + n.y * reach - cy) / P.f[5].z;
let sc = fxm_unpremul(fxm_sky_repeat(rx, ry, P.u[0].w)).xyz;
surface = sc * P.f[5].w * 3.0 * surf;
} else {
surface = surf * (ambient + diffuse * li * nl);
}
let lc = P.f[3].xyz;
let cc = bot.xyz * light * lc * (1.0 - surf_op) + surface * surf_op + spec * lc;
let a = min(bot.w + surf_op * (1.0 - bot.w), 1.0);
textureStore(out, p, vec4<f32>(cc * a, a));
}
+154
View File
@@ -0,0 +1,154 @@
//! GPU simulation render passes vs the CPU effects (the oracle): the frame's plan (sprites,
//! pieces, blobs, drops, height grid) is shared, the raster / shading pass runs on each side.
//! Direct on a buffer (full and half resolution) and composited at 8 and 32 bpc, at a time
//! where the simulations have something to show.
use effectcraft_keyframe::Value;
use effectcraft_project::BitDepth;
use effectcraft_render::RenderOpts;
use effectcraft_time::Tick;
use crate::tests::{Scene, c, check, compare_at, diff, gpu, n, opts, pattern, set};
fn e(v: u32) -> Value {
Value::Enum(v)
}
fn on() -> Value {
Value::Bool(true)
}
fn off() -> Value {
Value::Bool(false)
}
/// The effect alone on a buffer at layer time `t` (`allow` = fraction of pixels over 1e-3).
fn direct(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
let Some(g) = gpu() else { return };
let spec = effectcraft_effects::find(id).unwrap();
let size = [70.0, 44.0];
let mut params =
effectcraft_effects::Params { values: spec.params.iter().map(|ps| (ps.id.to_string(), effectcraft_effects::default_value(ps, size))).collect() };
for (k, v) in vals {
params.values.insert(k.to_string(), v.clone());
}
for scale in [1.0, 0.5] {
let img = pattern(7, (size[0] * scale) as u32, (size[1] * scale) as u32);
let buf = effectcraft_effects::Buf { img, offset: [0.0, 0.0], scale };
let ctx = || effectcraft_effects::EffectCtx { params: &params, time: t, layer_size: size, seed: 11, adjustment: false, env: Default::default() };
let cpu = (spec.render)(&ctx(), buf.clone());
let out = effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).expect("the GPU runs the effect");
assert_eq!((out.offset, out.scale), (cpu.offset, cpu.scale), "{id}: geometry");
let d = diff(&cpu.img, &out.img, 1e-3);
assert!(
d.over as f64 <= allow * d.total as f64,
"{id} {vals:?} scale {scale}: {} of {} pixels over 1e-3 (max {}); worst {:?}",
d.over,
d.total,
d.max,
d.worst
);
}
}
/// A footage layer with the effect over a background and a second footage layer (the target of
/// `Value::Layer(Some(0))` parameters), composited at 8 and 32 bpc at comp time `t`.
fn composited(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
let mut s = Scene::new(depth);
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
s.push(bg);
let other = s.footage(60, 50);
let other_id = s.push(other);
let mut l = s.footage(70, 44);
let vals: Vec<(&str, Value)> =
vals.iter().map(|(k, v)| (*k, if *v == Value::Layer(Some(0)) { Value::Layer(Some(other_id.0)) } else { v.clone() })).collect();
s.effect(&mut l, id, &vals);
set(&mut l, "transform/rotation", Value::Scalar(8.0));
s.push(l);
let at = Tick::from_seconds_f64(t);
check(&format!("{id} {vals:?} {depth:?}"), compare_at(&s, opts(), at), allow);
check(&format!("{id} {vals:?} {depth:?} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, at), allow);
}
}
fn case(id: &str, vals: &[(&str, Value)], t: f64, allow: f64) {
direct(id, vals, t, allow);
composited(id, vals, t, allow);
}
#[test]
fn weather_and_star_burst() {
case("ec.sim.ccrainfall", &[("drops", n(800.0)), ("size", n(3.0))], 0.5, 0.0);
case("ec.sim.ccrainfall", &[("drops", n(500.0)), ("extras/appearance", e(1)), ("transferMode", e(0)), ("wind", n(800.0))], 0.7, 0.0);
case("ec.sim.ccrainfall", &[("drops", n(300.0)), ("compositeWithOriginal", off()), ("size", n(6.0))], 0.3, 0.0);
case("ec.sim.ccsnowfall", &[("flakes", n(600.0)), ("size", n(6.0))], 0.5, 0.0);
case("ec.sim.ccsnowfall", &[("flakes", n(400.0)), ("transferMode", e(1)), ("wiggle/wiggleAmount", n(8.0))], 1.2, 0.0);
case("ec.sim.ccstarburst", &[("gridSpacing", n(6.0))], 0.4, 0.0);
case("ec.sim.ccstarburst", &[("gridSpacing", n(4.0)), ("blendWithOriginal", n(40.0)), ("size", n(160.0))], 0.9, 0.0);
}
#[test]
fn bubbles_drizzle_mercury() {
case("ec.sim.ccbubbles", &[("bubbleAmount", n(60.0)), ("bubbleSize", n(3.0))], 0.5, 0.0);
for shading in 1..5 {
case(
"ec.sim.ccbubbles",
&[("bubbleAmount", n(40.0)), ("bubbleSize", n(4.0)), ("shadingType", e(shading)), ("reflectionType", e(shading % 2))],
0.8,
0.0,
);
}
case("ec.sim.ccdrizzle", &[("dripRate", n(12.0)), ("spreading", n(300.0))], 0.6, 0.0);
case("ec.sim.ccdrizzle", &[("dripRate", n(20.0)), ("shading/diffuse", n(40.0)), ("shading/ambient", n(-20.0)), ("light/lightHeight", n(60.0))], 1.1, 0.0);
case("ec.sim.ccmrmercury", &[("birthRate", n(4.0)), ("blobBirthSize", n(1.5)), ("blobDeathSize", n(1.0))], 1.0, 0.0);
case("ec.sim.ccmrmercury", &[("birthRate", n(3.0)), ("blobInfluence", n(60.0)), ("shading/metal", n(80.0)), ("gravity", n(2.0))], 1.5, 0.0);
}
#[test]
fn hair_wave_world_foam() {
case("ec.sim.cchair", &[("density", n(300.0)), ("length", n(10.0))], 0.0, 0.0);
case("ec.sim.cchair", &[("density", n(200.0)), ("length", n(14.0)), ("hairColor/colorInheritance", n(30.0)), ("thickness", n(1.5))], 0.0, 0.0);
case("ec.sim.waveworld", &[], 0.5, 0.0);
case("ec.sim.waveworld", &[("view", e(1))], 0.8, 0.0);
composited("ec.sim.waveworld", &[("view", e(1)), ("ground/ground", Value::Layer(Some(0))), ("heightMapControls/renderDryAreasAs", e(1))], 0.6, 0.0);
case("ec.sim.foam", &[], 1.0, 0.0);
case("ec.sim.foam", &[("view", e(2))], 1.5, 0.0);
case("ec.sim.foam", &[("view", e(2)), ("rendering/bubbleTexture", e(4)), ("rendering/blendMode", e(2))], 1.2, 0.0);
}
#[test]
fn pieces() {
case("ec.sim.carddance", &[("rows", n(5.0)), ("columns", n(7.0)), ("zRotation/zRotMultiplier", n(20.0))], 0.0, 0.0);
composited("ec.sim.carddance", &[("rows", n(4.0)), ("backLayer", Value::Layer(Some(0))), ("yRotation/yRotMultiplier", n(150.0))], 0.0, 0.0);
case("ec.sim.shatter", &[("view", e(0))], 1.0, 0.0);
case("ec.sim.shatter", &[("view", e(0)), ("shape/pattern", e(2)), ("physics/gravity", n(4.0))], 1.6, 0.0);
case("ec.transition.cardwipe", &[("completion", n(40.0))], 0.0, 0.0);
composited("ec.transition.cardwipe", &[("completion", n(60.0)), ("backLayer", Value::Layer(Some(0))), ("flipAxis", e(1))], 0.0, 0.0);
}
#[test]
fn caustics() {
case("ec.sim.caustics", &[], 0.0, 0.0);
composited(
"ec.sim.caustics",
&[("water/waterSurface", Value::Layer(Some(0))), ("water/waveHeight", n(0.3)), ("water/smoothing", n(4.0)), ("lighting/lightType", e(1))],
0.0,
0.0,
);
composited(
"ec.sim.caustics",
&[
("water/waterSurface", Value::Layer(Some(0))),
("sky/sky", Value::Layer(Some(0))),
("water/surfaceOpacity", n(0.6)),
("bottom/repeatMode", e(2)),
("bottom/scaling", n(1.4)),
("bottom/blur", n(3.0)),
("sky/repeatMode", e(1)),
],
0.0,
0.0,
);
let _ = (c(0.0, 0.0, 0.0), on());
}