mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-27 03:28:22 -06:00
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:
co-authored by
Claude Opus 5.5
parent
2e5ea051a8
commit
1ec9336d7c
@@ -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)))
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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: ¶ms, 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());
|
||||
}
|
||||
Reference in New Issue
Block a user