mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -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
@@ -100,6 +100,8 @@ pub use ocio::color_stabilizer_maps;
|
||||
// GPU effects, part B (3D Channel, Immersive Video, colour management, simulation render passes).
|
||||
pub use channel3d::{CRYPTO_LAYERS, id_color, selection_hashes};
|
||||
pub use ocio::{ColorOp, Straight, Tf, color_program};
|
||||
pub use sim::{Acc, Blob, Bubble, Post, Shape, Sprite, SpritePlan, Tint, bubble_list, drizzle_drops, mercury_blobs, sprite_plan};
|
||||
pub use sim2::{CausticsSetup, Piece, PiecePlan, PieceTex, WavePlan, Waves, caustics_setup, piece_plan, wave_world_plan};
|
||||
pub use utility::{Lut, load_lut};
|
||||
pub use vr::{equi_dir as vr_equi_dir, rotation as vr_rotation};
|
||||
|
||||
@@ -966,6 +968,20 @@ pub const GPU_EFFECTS: &[&str] = &[
|
||||
"ec.vr.rotatesphere",
|
||||
"ec.vr.sharpen",
|
||||
"ec.vr.spheretoplane",
|
||||
// Simulation render passes.
|
||||
"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",
|
||||
];
|
||||
|
||||
/// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated
|
||||
|
||||
+204
-76
@@ -48,7 +48,7 @@ const BAND: usize = 16;
|
||||
|
||||
/// How rasterised items combine with what is already in the target.
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||
pub(crate) enum Acc {
|
||||
pub enum Acc {
|
||||
/// Premultiplied "over" (later items on top).
|
||||
Over,
|
||||
/// Additive (glows, light).
|
||||
@@ -128,7 +128,7 @@ pub(crate) fn raster<T: Sync>(
|
||||
// ------------------------------------------------------------------ sprites
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Debug)]
|
||||
pub(crate) enum Shape {
|
||||
pub enum Shape {
|
||||
/// Soft round dot (Gaussian-like falloff).
|
||||
Soft,
|
||||
/// Hard anti-aliased disc.
|
||||
@@ -151,7 +151,7 @@ pub(crate) enum Shape {
|
||||
|
||||
/// A particle sprite in buffer pixels; `c` is straight colour + opacity.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct Sprite {
|
||||
pub struct Sprite {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub r: f32,
|
||||
@@ -166,6 +166,112 @@ impl Sprite {
|
||||
}
|
||||
}
|
||||
|
||||
/// Where a sprite's colour comes from when it depends on the layer's pixels (resolved by
|
||||
/// [`SpritePlan::resolve`] on the CPU, per pixel by the GPU compositor). Positions are buffer
|
||||
/// pixels.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Tint {
|
||||
/// The sprite's own colour.
|
||||
Fixed,
|
||||
/// CC Rainfall's refracting drops: where the layer shows (alpha > 0), colour × (layer
|
||||
/// colour × 0.6 + 0.5) sampled at (x, y).
|
||||
Refract { x: f64, y: f64 },
|
||||
/// CC Star Burst: the layer's colour at (x, y), opacity × its alpha (none where clear).
|
||||
Source { x: f64, y: f64 },
|
||||
/// CC Hair: the root's colour at (x, y) mixed with the hair colour (`inherit` = 1 keeps the
|
||||
/// root's), × `shade`, + `spec`; no strand where the root's alpha is below ½.
|
||||
Root { x: f64, y: f64, hair: [f32; 3], inherit: f32, shade: f32, spec: f32 },
|
||||
}
|
||||
|
||||
/// How a sprite layer meets the original layer.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Post {
|
||||
/// [`combine`] with this mode (0 Composite … 5 sprites only).
|
||||
Combine(u32),
|
||||
/// The sprites alone.
|
||||
Replace,
|
||||
/// The sprites, mixed toward the original by this fraction.
|
||||
Lerp(f32),
|
||||
/// The sprites over opaque black.
|
||||
OverBlack,
|
||||
}
|
||||
|
||||
/// What a sprite-drawing effect draws this frame: built on the CPU (closed-form or from the
|
||||
/// simulation state), rasterised by [`SpritePlan::finish`] or the GPU compositor.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SpritePlan {
|
||||
pub sprites: Vec<Sprite>,
|
||||
/// Per sprite (empty = all [`Tint::Fixed`]).
|
||||
pub tints: Vec<Tint>,
|
||||
pub acc: Acc,
|
||||
pub post: Post,
|
||||
}
|
||||
|
||||
impl SpritePlan {
|
||||
/// Resolve layer-dependent colours against the layer `src`.
|
||||
pub fn resolve(&mut self, src: &Image) {
|
||||
let tints = std::mem::take(&mut self.tints);
|
||||
self.sprites.par_iter_mut().zip(tints.par_iter()).for_each(|(sp, t)| match *t {
|
||||
Tint::Fixed => {}
|
||||
Tint::Refract { x, y } => {
|
||||
let (sc, sa) = unpremul(src.sample_bilinear(x, y));
|
||||
if sa > 0.0 {
|
||||
let c = sp.c;
|
||||
sp.c = [(sc[0] * 0.6 + 0.5) * c[0], (sc[1] * 0.6 + 0.5) * c[1], (sc[2] * 0.6 + 0.5) * c[2], c[3]];
|
||||
}
|
||||
}
|
||||
Tint::Source { x, y } => {
|
||||
let (c, a) = unpremul(src.sample_bilinear(x, y));
|
||||
sp.c = if a <= 0.0 { [0.0; 4] } else { [c[0], c[1], c[2], a * sp.c[3]] };
|
||||
}
|
||||
Tint::Root { x, y, hair, inherit, shade, spec } => {
|
||||
let root = src.sample_bilinear(x, y);
|
||||
if root[3] < 0.5 {
|
||||
sp.c = [0.0; 4];
|
||||
return;
|
||||
}
|
||||
let (rc, _) = unpremul(root);
|
||||
let base: [f32; 3] = std::array::from_fn(|k| rc[k] + (hair[k] - rc[k]) * (1.0 - inherit));
|
||||
sp.c = [base[0] * shade + spec, base[1] * shade + spec, base[2] * shade + spec, sp.c[3]];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Rasterise (after [`SpritePlan::resolve`]) and meet the original layer `b`.
|
||||
pub fn finish(&self, mut b: Buf) -> Buf {
|
||||
let fx = splat(b.img.width, b.img.height, &self.sprites, self.acc);
|
||||
b.img = match self.post {
|
||||
Post::Combine(m) => combine(&b.img, &fx, m),
|
||||
Post::Replace => fx,
|
||||
Post::Lerp(t) => {
|
||||
let mut o = fx;
|
||||
o.data.par_iter_mut().zip(b.img.data.par_iter()).for_each(|(o, s0)| *o = crate::util::lerp4(*o, *s0, t));
|
||||
o
|
||||
}
|
||||
Post::OverBlack => combine(&Image::filled(b.img.width, b.img.height, [0.0, 0.0, 0.0, 1.0]), &fx, 0),
|
||||
};
|
||||
b
|
||||
}
|
||||
}
|
||||
|
||||
/// The sprite plan of a sprite-drawing simulation effect for buffer geometry `b` (its pixels
|
||||
/// are not read), `None` for other effects or settings drawn otherwise (the GPU compositor's
|
||||
/// render pass).
|
||||
pub fn sprite_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option<SpritePlan> {
|
||||
match id {
|
||||
"ec.sim.ccrainfall" => Some(rainfall_plan(ctx, b)),
|
||||
"ec.sim.ccsnowfall" => Some(snowfall_plan(ctx, b)),
|
||||
"ec.sim.ccstarburst" => Some(star_burst_plan(ctx, b)),
|
||||
"ec.sim.cchair" => crate::sim3::hair_plan(ctx, b),
|
||||
"ec.sim.waveworld" => crate::sim2::wave_world_plan(ctx, b).and_then(|p| match p {
|
||||
crate::sim2::WavePlan::Wire(sp) => Some(sp),
|
||||
crate::sim2::WavePlan::Height { .. } => None,
|
||||
}),
|
||||
"ec.sim.foam" => crate::sim2::foam_plan(ctx, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn sprite_bbox(s: &Sprite) -> Option<[f32; 4]> {
|
||||
if s.c[3] <= 1e-5 || s.r <= 0.0 {
|
||||
return None;
|
||||
@@ -316,6 +422,12 @@ fn wrap(v: f32, lo: f32, span: f32) -> f32 {
|
||||
}
|
||||
|
||||
fn rainfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
let mut plan = rainfall_plan(ctx, &b);
|
||||
plan.resolve(&b.img);
|
||||
plan.finish(b)
|
||||
}
|
||||
|
||||
fn rainfall_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||||
let pr = ctx.params;
|
||||
let n = pr.f("drops").clamp(0.0, 100_000.0) as u32;
|
||||
let size = pr.f("size").max(0.0) as f32;
|
||||
@@ -334,11 +446,10 @@ fn rainfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let s = b.scale as f32;
|
||||
let t = ctx.time as f32;
|
||||
let src = &b.img;
|
||||
let (ox, oy) = (off[0] as f32 - lw * 0.5, off[1] as f32 - lh * 0.5);
|
||||
let sprites: Vec<Sprite> = (0..n)
|
||||
let (sprites, tints): (Vec<Sprite>, Vec<Tint>) = (0..n)
|
||||
.into_par_iter()
|
||||
.filter_map(|i| {
|
||||
.map(|i| {
|
||||
let z = h(i, 1, seed);
|
||||
let k = depth_k(z, depth);
|
||||
let vy = speed / 4000.0 * lh * 2.2 * k;
|
||||
@@ -349,29 +460,25 @@ fn rainfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
let y = wrap(h(i, 3, seed) * (lh + 2.0 * margin) + vy * t + oy, -margin, lh + 2.0 * margin);
|
||||
let x = wrap(h(i, 4, seed) * (lw + 2.0 * margin) + w * t + ox, -margin, lw + 2.0 * margin);
|
||||
let (bx, by) = b.to_px([x as f64, y as f64]);
|
||||
let mut c = [color[0], color[1], color[2], opacity * (0.35 + 0.65 * k)];
|
||||
if refract {
|
||||
// Refracting drops show the scene behind them, brightened and offset.
|
||||
let (sc, sa) = unpremul(src.sample_bilinear(bx + 2.0 * s as f64, by));
|
||||
let m = |a: f32, cc: f32| (a * 0.6 + 0.5) * cc;
|
||||
if sa > 0.0 {
|
||||
c = [m(sc[0], color[0]), m(sc[1], color[1]), m(sc[2], color[2]), c[3]];
|
||||
}
|
||||
}
|
||||
let c = [color[0], color[1], color[2], opacity * (0.35 + 0.65 * k)];
|
||||
// Refracting drops show the scene behind them, brightened and offset.
|
||||
let tint = if refract { Tint::Refract { x: bx + 2.0 * s as f64, y: by } } else { Tint::Fixed };
|
||||
let r = (size * 0.6 * k * s).max(0.25);
|
||||
Some(Sprite::new(bx as f32, by as f32, r, c, Shape::Line { dx: -lenx * s, dy: -len * s }))
|
||||
(Sprite::new(bx as f32, by as f32, r, c, Shape::Line { dx: -lenx * s, dy: -len * s }), tint)
|
||||
})
|
||||
.collect();
|
||||
weather_out(b, &sprites, mode, with_orig)
|
||||
.unzip();
|
||||
SpritePlan { sprites, tints, acc: Acc::Over, post: weather_post(mode, with_orig) }
|
||||
}
|
||||
|
||||
fn weather_out(mut b: Buf, sprites: &[Sprite], mode: u32, with_orig: bool) -> Buf {
|
||||
let fx = splat(b.img.width, b.img.height, sprites, Acc::Over);
|
||||
b.img = if with_orig { combine(&b.img, &fx, if mode == 1 { 3 } else { 0 }) } else { fx };
|
||||
b
|
||||
fn weather_post(mode: u32, with_orig: bool) -> Post {
|
||||
if with_orig { Post::Combine(if mode == 1 { 3 } else { 0 }) } else { Post::Replace }
|
||||
}
|
||||
|
||||
fn snowfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
snowfall_plan(ctx, &b).finish(b)
|
||||
}
|
||||
|
||||
fn snowfall_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||||
let pr = ctx.params;
|
||||
let n = pr.f("flakes").clamp(0.0, 200_000.0) as u32;
|
||||
let size = pr.f("size").max(0.0) as f32;
|
||||
@@ -414,35 +521,36 @@ fn snowfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
Sprite::new(bx as f32, by as f32, r * s, [color[0], color[1], color[2], opacity * (0.4 + 0.6 * k)], Shape::Soft)
|
||||
})
|
||||
.collect();
|
||||
weather_out(b, &sprites, mode, with_orig)
|
||||
SpritePlan { sprites, tints: vec![], acc: Acc::Over, post: weather_post(mode, with_orig) }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ CC Bubbles
|
||||
|
||||
struct Bubble {
|
||||
x: f32,
|
||||
y: f32,
|
||||
r: f32,
|
||||
/// A CC Bubbles bubble in buffer pixels.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Bubble {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub r: f32,
|
||||
/// Source sample centre (buffer px).
|
||||
sx: f32,
|
||||
sy: f32,
|
||||
pub sx: f32,
|
||||
pub sy: f32,
|
||||
}
|
||||
|
||||
fn bubbles(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// CC Bubbles' bubbles this frame (closed-form in time) for buffer geometry `b`.
|
||||
pub fn bubble_list(ctx: &EffectCtx, b: &Buf) -> Vec<Bubble> {
|
||||
let pr = ctx.params;
|
||||
let n = pr.f("bubbleAmount").clamp(0.0, 10_000.0) as u32;
|
||||
let speed = pr.f("bubbleSpeed") as f32;
|
||||
let wamp = pr.f("wobbleAmplitude") as f32;
|
||||
let wfreq = pr.f("wobbleFrequency") as f32;
|
||||
let bsize = pr.f("bubbleSize").max(0.0) as f32;
|
||||
let metal = pr.e("reflectionType") == 1;
|
||||
let shading = pr.e("shadingType");
|
||||
let seed = ctx.seed.wrapping_mul(0x27d4);
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let s = b.scale as f32;
|
||||
let t = ctx.time as f32;
|
||||
let base = lw.min(lh) * 0.035 * bsize;
|
||||
let list: Vec<Bubble> = (0..n)
|
||||
(0..n)
|
||||
.map(|i| {
|
||||
let r = base * (0.5 + h(i, 1, seed));
|
||||
let travel = lh + 2.0 * r;
|
||||
@@ -454,7 +562,14 @@ fn bubbles(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let (sx, sy) = b.to_px([x0 as f64, y as f64]);
|
||||
Bubble { x: bx as f32, y: by as f32, r: r * s, sx: sx as f32, sy: sy as f32 }
|
||||
})
|
||||
.collect();
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn bubbles(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let metal = pr.e("reflectionType") == 1;
|
||||
let shading = pr.e("shadingType");
|
||||
let list = bubble_list(ctx, &b);
|
||||
let src = b.img.clone();
|
||||
let mut out = Image::new(src.width, src.height);
|
||||
raster(
|
||||
@@ -495,28 +610,20 @@ fn bubbles(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
|
||||
// ------------------------------------------------------------------ CC Drizzle
|
||||
|
||||
fn drizzle(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// CC Drizzle's live drops this frame (buffer px: centre x, y, ring radius, amplitude);
|
||||
/// `None` = no drops yet (the layer passes through).
|
||||
pub fn drizzle_drops(ctx: &EffectCtx, b: &Buf) -> Option<Vec<[f32; 4]>> {
|
||||
let pr = ctx.params;
|
||||
let rate = pr.f("dripRate").max(0.0);
|
||||
let life = pr.f("longevity").max(0.01);
|
||||
let rippling = (pr.f("rippling") / 360.0).max(0.05) as f32;
|
||||
let disp = pr.f("displacement") as f32;
|
||||
let height = pr.f("rippleHeight") as f32 / 100.0;
|
||||
let spreading = pr.f("spreading") as f32;
|
||||
let li = pr.f("light/lightIntensity") as f32 / 100.0;
|
||||
let lc = pr.color("light/lightColor");
|
||||
let lheight = pr.f("light/lightHeight") as f32 / 100.0;
|
||||
let ldir = (pr.f("light/lightDirection") as f32).to_radians();
|
||||
let ambient = pr.f("shading/ambient") as f32 / 100.0;
|
||||
let diffuse = pr.f("shading/diffuse") as f32 / 100.0;
|
||||
let specular = pr.f("shading/specular") as f32 / 100.0;
|
||||
let rough = pr.f("shading/roughness").max(0.001) as f32;
|
||||
let height = pr.f("rippleHeight") as f32 / 100.0;
|
||||
let seed = ctx.seed.wrapping_mul(0x165667b1);
|
||||
let t = ctx.time;
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let s = b.scale as f32;
|
||||
if rate <= 0.0 || t < 0.0 {
|
||||
return b;
|
||||
return None;
|
||||
}
|
||||
// Drops born at k / rate with hashed positions; alive while younger than the longevity.
|
||||
let k0 = ((t - life) * rate).floor().max(0.0) as u32;
|
||||
@@ -531,13 +638,31 @@ fn drizzle(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let (x, y) = b.to_px([(h(k, 1, seed) * lw) as f64, (h(k, 2, seed) * lh) as f64]);
|
||||
let radius = spreading * age * lh / 400.0 * s;
|
||||
let amp = height * (1.0 - age / life as f32) * (0.6 + 0.4 * h(k, 3, seed));
|
||||
drops.push((x as f32, y as f32, radius, amp));
|
||||
drops.push([x as f32, y as f32, radius, amp]);
|
||||
}
|
||||
Some(drops)
|
||||
}
|
||||
|
||||
fn drizzle(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let Some(drops) = drizzle_drops(ctx, &b) else { return b };
|
||||
let rippling = (pr.f("rippling") / 360.0).max(0.05) as f32;
|
||||
let disp = pr.f("displacement") as f32;
|
||||
let li = pr.f("light/lightIntensity") as f32 / 100.0;
|
||||
let lc = pr.color("light/lightColor");
|
||||
let lheight = pr.f("light/lightHeight") as f32 / 100.0;
|
||||
let ldir = (pr.f("light/lightDirection") as f32).to_radians();
|
||||
let ambient = pr.f("shading/ambient") as f32 / 100.0;
|
||||
let diffuse = pr.f("shading/diffuse") as f32 / 100.0;
|
||||
let specular = pr.f("shading/specular") as f32 / 100.0;
|
||||
let rough = pr.f("shading/roughness").max(0.001) 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 field = |x: f32, y: f32| -> f32 {
|
||||
let mut v = 0.0;
|
||||
for &(dx, dy, r, a) in &drops {
|
||||
for &[dx, dy, r, a] in &drops {
|
||||
let d = ((x - dx).powi(2) + (y - dy).powi(2)).sqrt();
|
||||
let u = (d - r) / width;
|
||||
if u.abs() < 1.0 {
|
||||
@@ -586,7 +711,13 @@ fn drizzle(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
|
||||
// ------------------------------------------------------------------ CC Star Burst
|
||||
|
||||
fn star_burst(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn star_burst(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
let mut plan = star_burst_plan(ctx, &b);
|
||||
plan.resolve(&b.img);
|
||||
plan.finish(b)
|
||||
}
|
||||
|
||||
fn star_burst_plan(ctx: &EffectCtx, b: &Buf) -> SpritePlan {
|
||||
let pr = ctx.params;
|
||||
let scatter = pr.f("scatter") as f32;
|
||||
let speed = pr.f("speed") as f32;
|
||||
@@ -600,17 +731,12 @@ fn star_burst(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let t = ctx.time as f32;
|
||||
let nx = (lw / spacing).ceil().max(1.0) as u32;
|
||||
let ny = (lh / spacing).ceil().max(1.0) as u32;
|
||||
let src = b.img.clone();
|
||||
let (cx, cy) = (lw * 0.5, lh * 0.5);
|
||||
let mut list: Vec<(f32, Sprite)> = (0..nx * ny)
|
||||
let mut list: Vec<(f32, Sprite, Tint)> = (0..nx * ny)
|
||||
.into_par_iter()
|
||||
.filter_map(|i| {
|
||||
.map(|i| {
|
||||
let (gx, gy) = ((i % nx) as f32 * spacing + spacing * 0.5, (i / nx) as f32 * spacing + spacing * 0.5);
|
||||
let (sx, sy) = b.to_px([gx as f64, gy as f64]);
|
||||
let (c, a) = unpremul(src.sample_bilinear(sx, sy));
|
||||
if a <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
// Depth cycles towards the viewer: z in [0, 1), perspective 1 / (1 - 0.9 z).
|
||||
let z = (h(i, 1, seed) + t * speed * 0.25 + phase).rem_euclid(1.0);
|
||||
let persp = 1.0 / (1.0 - 0.9 * z);
|
||||
@@ -621,19 +747,13 @@ fn star_burst(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let (bx, by) = b.to_px([x as f64, y as f64]);
|
||||
let r = spacing * 0.5 * size * persp * s;
|
||||
let fade = (z * 8.0).min(1.0) * ((1.0 - z) * 6.0).min(1.0);
|
||||
Some((z, Sprite::new(bx as f32, by as f32, r, [c[0], c[1], c[2], a * fade], Shape::Disc)))
|
||||
// The layer's colour at the grid point (none where it is clear).
|
||||
(z, Sprite::new(bx as f32, by as f32, r, [0.0, 0.0, 0.0, fade], Shape::Disc), Tint::Source { x: sx, y: sy })
|
||||
})
|
||||
.collect();
|
||||
list.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||
let sprites: Vec<Sprite> = list.into_iter().map(|(_, s)| s).collect();
|
||||
let fx = splat(src.width, src.height, &sprites, Acc::Over);
|
||||
b.img = fx;
|
||||
if blend > 0.0 {
|
||||
b.img.data.par_iter_mut().zip(src.data.par_iter()).for_each(|(o, s0)| {
|
||||
*o = crate::util::lerp4(*o, *s0, blend);
|
||||
});
|
||||
}
|
||||
b
|
||||
let (sprites, tints) = list.into_iter().map(|(_, s, t)| (s, t)).unzip();
|
||||
SpritePlan { sprites, tints, acc: Acc::Over, post: if blend > 0.0 { Post::Lerp(blend) } else { Post::Replace } }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ particle engine
|
||||
@@ -1067,13 +1187,16 @@ fn particle_systems2(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
|
||||
static MERCURY_CACHE: SimCache<PState> = SimCache::new(6);
|
||||
|
||||
struct Blob {
|
||||
x: f32,
|
||||
y: f32,
|
||||
r: f32,
|
||||
/// A CC Mr. Mercury blob (buffer px).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Blob {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
pub r: f32,
|
||||
}
|
||||
|
||||
fn mr_mercury(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// CC Mr. Mercury's blobs this frame, from its particle simulation (for buffer geometry `b`).
|
||||
pub fn mercury_blobs(ctx: &EffectCtx, b: &Buf) -> Vec<Blob> {
|
||||
let pr = ctx.params;
|
||||
let lh = ctx.layer_size[1] as f32;
|
||||
let unit = lh.max(1.0);
|
||||
@@ -1097,10 +1220,8 @@ fn mr_mercury(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
};
|
||||
let st = simulate(&MERCURY_CACHE, params_key(ctx, &Buf { img: Image::new(0, 0), offset: [0.0; 2], scale: 1.0 }, 3), ctx.time, &phys);
|
||||
let (bs, ds) = (pr.f("blobBirthSize") as f32, pr.f("blobDeathSize") as f32);
|
||||
let influence = pr.f("blobInfluence") as f32 / 100.0;
|
||||
let s = b.scale as f32;
|
||||
let blobs: Vec<Blob> = st
|
||||
.parts
|
||||
st.parts
|
||||
.iter()
|
||||
.map(|q| {
|
||||
let u = (q.age / q.life.max(1e-4)).clamp(0.0, 1.0);
|
||||
@@ -1109,7 +1230,14 @@ fn mr_mercury(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
Blob { x: bx as f32, y: by as f32, r: (size * unit * 0.08 * s).max(0.0) }
|
||||
})
|
||||
.filter(|q| q.r > 0.1)
|
||||
.collect();
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn mr_mercury(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let pr = ctx.params;
|
||||
let blobs = mercury_blobs(ctx, &b);
|
||||
let influence = pr.f("blobInfluence") as f32 / 100.0;
|
||||
let s = b.scale as f32;
|
||||
let (w, hh) = (b.img.width, b.img.height);
|
||||
// Field and gradient accumulate additively: [f, gx, gy, _].
|
||||
let mut field = Image::new(w, hh);
|
||||
|
||||
+286
-82
@@ -80,31 +80,78 @@ impl Cam {
|
||||
}
|
||||
|
||||
/// A planar textured piece ready to draw.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct Piece {
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct Piece {
|
||||
/// Screen (buffer px, homogeneous) → piece-local (a, b, w).
|
||||
inv: M3,
|
||||
pub inv: [[f64; 3]; 3],
|
||||
/// Rest centre of the piece in texture pixels.
|
||||
c0: [f32; 2],
|
||||
pub c0: [f32; 2],
|
||||
/// Convex polygon (texture pixels), counter-clockwise in y-down space.
|
||||
poly: [[f32; 2]; 6],
|
||||
n: u8,
|
||||
bbox: [f32; 4],
|
||||
shade: f32,
|
||||
pub poly: [[f32; 2]; 6],
|
||||
pub n: u8,
|
||||
pub bbox: [f32; 4],
|
||||
pub shade: f32,
|
||||
/// Lighting: colour multiplier and additive specular highlight.
|
||||
tint: [f32; 3],
|
||||
spec: [f32; 3],
|
||||
pub(crate) alpha: f32,
|
||||
pub tint: [f32; 3],
|
||||
pub spec: [f32; 3],
|
||||
pub alpha: f32,
|
||||
/// Flat straight colour instead of the texture.
|
||||
pub(crate) flat: Option<[f32; 3]>,
|
||||
pub flat: Option<[f32; 3]>,
|
||||
/// Facing away from the camera (draw the back texture).
|
||||
pub(crate) back: bool,
|
||||
pub back: bool,
|
||||
/// Back faces sample their texture mirrored about the piece centre: 0 no, 1 vertically
|
||||
/// (flipped about the X axis), 2 horizontally (about the Y axis).
|
||||
pub(crate) back_mirror: u8,
|
||||
pub back_mirror: u8,
|
||||
depth: f64,
|
||||
}
|
||||
|
||||
/// A piece texture: the layer itself or another image (buffer-aligned).
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum PieceTex {
|
||||
Layer,
|
||||
Image(Image),
|
||||
}
|
||||
|
||||
/// What a piece-drawing effect (Card Dance, Shatter, Card Wipe) draws this frame: pieces sorted
|
||||
/// far to near over a transparent frame, textured by `front` (and `back` for pieces facing
|
||||
/// away; none = they take `front`).
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PiecePlan {
|
||||
pub pieces: Vec<Piece>,
|
||||
pub front: PieceTex,
|
||||
pub back: Option<PieceTex>,
|
||||
}
|
||||
|
||||
impl PiecePlan {
|
||||
/// Rasterise over a transparent frame of the layer `b`'s size.
|
||||
pub fn finish(&self, mut b: Buf) -> Buf {
|
||||
let mut out = Image::new(b.img.width, b.img.height);
|
||||
let front = match &self.front {
|
||||
PieceTex::Layer => &b.img,
|
||||
PieceTex::Image(i) => i,
|
||||
};
|
||||
let back = self.back.as_ref().map(|t| match t {
|
||||
PieceTex::Layer => &b.img,
|
||||
PieceTex::Image(i) => i,
|
||||
});
|
||||
draw_pieces(&mut out, &self.pieces, front, back);
|
||||
b.img = out;
|
||||
b
|
||||
}
|
||||
}
|
||||
|
||||
/// The piece plan of Card Dance, Shatter (Rendered view) or Card Wipe for layer `b` (its pixels
|
||||
/// may be read: gradient maps and textures default to the layer), `None` for other effects or
|
||||
/// views drawn otherwise.
|
||||
pub fn piece_plan(id: &str, ctx: &EffectCtx, b: &Buf) -> Option<PiecePlan> {
|
||||
match id {
|
||||
"ec.sim.carddance" => Some(card_dance_plan(ctx, b)),
|
||||
"ec.sim.shatter" if ctx.params.e("view") == 0 => shatter_impl(ctx, b, true).err(),
|
||||
"ec.transition.cardwipe" => crate::transition::card_wipe_plan(ctx, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl Piece {
|
||||
/// Place polygon `poly` (texture px, around rest centre `c0`) with rotation `r` and its
|
||||
/// centre at 3D `pos` (buffer px; z away from the camera), seen through `cam`.
|
||||
@@ -462,22 +509,25 @@ fn card_source(src: u32, g1: [f32; 4], g2: [f32; 4]) -> f32 {
|
||||
}
|
||||
}
|
||||
|
||||
fn card_dance(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn card_dance(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
card_dance_plan(ctx, &b).finish(b)
|
||||
}
|
||||
|
||||
fn card_dance_plan(ctx: &EffectCtx, b: &Buf) -> PiecePlan {
|
||||
let pr = ctx.params;
|
||||
let rows = pr.f("rows").clamp(1.0, 1000.0) as u32;
|
||||
let cols = if pr.e("rowsColumns") == 1 { rows } else { pr.f("columns").clamp(1.0, 1000.0) as u32 };
|
||||
let g1 = layer_or_self(ctx, &b, "gradientLayer1", true, true);
|
||||
let g2 = layer_or_self(ctx, &b, "gradientLayer2", true, true);
|
||||
let back = ctx.layer_param("backLayer", true).map(|o| crate::util::fit_layer(ctx, &b, &o, true));
|
||||
let g1 = layer_or_self(ctx, b, "gradientLayer1", true, true);
|
||||
let g2 = layer_or_self(ctx, b, "gradientLayer2", true, true);
|
||||
let back = ctx.layer_param("backLayer", true).map(|o| crate::util::fit_layer(ctx, b, &o, true));
|
||||
let [lw, lh] = ctx.layer_size;
|
||||
let s = b.scale;
|
||||
let (cw, ch) = (lw / cols as f64, lh / rows as f64);
|
||||
let cam = crate::card3d::projection(ctx, &b);
|
||||
let cam = crate::card3d::projection(ctx, b);
|
||||
let eye = cam.eye();
|
||||
let light = Lighting::from(ctx, &b);
|
||||
let light = Lighting::from(ctx, b);
|
||||
let props: Vec<(u32, f64, f64)> =
|
||||
CD_PROPS.iter().map(|(id, g)| (pr.e(&format!("{g}/{id}Source")), pr.f(&format!("{g}/{id}Multiplier")), pr.f(&format!("{g}/{id}Offset")))).collect();
|
||||
let src = b.img.clone();
|
||||
let mut pieces: Vec<Piece> = (0..rows * cols)
|
||||
.into_par_iter()
|
||||
.filter_map(|i| {
|
||||
@@ -502,10 +552,7 @@ fn card_dance(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
})
|
||||
.collect();
|
||||
sort_far_first(&mut pieces);
|
||||
let mut out = Image::new(src.width, src.height);
|
||||
draw_pieces(&mut out, &pieces, &src, back.as_ref());
|
||||
b.img = out;
|
||||
b
|
||||
PiecePlan { pieces, front: PieceTex::Layer, back: back.map(PieceTex::Image) }
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Shatter
|
||||
@@ -638,7 +685,15 @@ fn map_key(c: [f32; 4]) -> u32 {
|
||||
q(c[0]) << 8 | q(c[1]) << 4 | q(c[2])
|
||||
}
|
||||
|
||||
fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn shatter(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
match shatter_impl(ctx, &b, false) {
|
||||
Ok(img) => Buf { img, ..b },
|
||||
Err(plan) => plan.finish(b),
|
||||
}
|
||||
}
|
||||
|
||||
/// Shatter: the frame (`Ok`), or with `plan_only` in the Rendered view its pieces (`Err`).
|
||||
fn shatter_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result<Image, PiecePlan> {
|
||||
let pr = ctx.params;
|
||||
let view = pr.e("view");
|
||||
let render = pr.e("render");
|
||||
@@ -666,21 +721,21 @@ fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let unit = lw * 0.1;
|
||||
let cell = lw / reps;
|
||||
let extrude = pr.f("shape/extrusionDepth").max(0.0) * cell * s;
|
||||
let cam = crate::card3d::projection(ctx, &b);
|
||||
let cam = crate::card3d::projection(ctx, b);
|
||||
let eye = cam.eye();
|
||||
let light = Lighting::from(ctx, &b);
|
||||
let light = Lighting::from(ctx, b);
|
||||
// Gradient: with a gradient layer only pieces at least as bright as 1 − Shatter Threshold
|
||||
// (Invert Gradient: as dark) break.
|
||||
let gradient = ctx.layer_param("gradient/gradientLayer", true).map(|o| crate::util::fit_layer(ctx, &b, &o, true));
|
||||
let gradient = ctx.layer_param("gradient/gradientLayer", true).map(|o| crate::util::fit_layer(ctx, b, &o, true));
|
||||
let threshold = pr.get("gradient/shatterThreshold").map(Value::as_f64).unwrap_or(0.0) / 100.0;
|
||||
let invert_gradient = pr.b("gradient/invertGradient");
|
||||
// Textures.
|
||||
let tex_color = pr.get("textures/color").map(|v| v.as_color()).unwrap_or([1.0; 4]);
|
||||
let tex_opacity = pr.get("textures/opacity").map(Value::as_f64).unwrap_or(1.0).clamp(0.0, 1.0) as f32;
|
||||
let front = Face::from(ctx, &b, "textures/frontMode", "textures/frontLayer", 1);
|
||||
let side = Face::from(ctx, &b, "textures/sideMode", "textures/sideLayer", 0);
|
||||
let backf = Face::from(ctx, &b, "textures/backMode", "textures/backLayer", 1);
|
||||
let custom_map = custom.then(|| layer_or_self(ctx, &b, "shape/customShatterMap", true, true));
|
||||
let front = Face::from(ctx, b, "textures/frontMode", "textures/frontLayer", 1);
|
||||
let side = Face::from(ctx, b, "textures/sideMode", "textures/sideLayer", 0);
|
||||
let backf = Face::from(ctx, b, "textures/backMode", "textures/backLayer", 1);
|
||||
let custom_map = custom.then(|| layer_or_self(ctx, b, "shape/customShatterMap", true, true));
|
||||
let white_fixed = pr.b("shape/whiteTilesFixed");
|
||||
let (sd, cd) = dir.sin_cos();
|
||||
let to_layer = |q: [f64; 2]| [origin[0] + q[0] * cd - q[1] * sd, origin[1] + q[0] * sd + q[1] * cd];
|
||||
@@ -843,12 +898,18 @@ fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
})
|
||||
.collect();
|
||||
sort_far_first(&mut pieces);
|
||||
let mut out = Image::new(src.width, src.height);
|
||||
let front_img = front.img.as_ref().unwrap_or(&src);
|
||||
let back_img = backf.img.as_ref().unwrap_or(front_img);
|
||||
if view == 0 {
|
||||
draw_pieces(&mut out, &pieces, front_img, Some(back_img));
|
||||
} else {
|
||||
let tex = |i: &Option<Image>| i.clone().map(PieceTex::Image);
|
||||
let front_t = tex(&front.img).unwrap_or(PieceTex::Layer);
|
||||
let back_t = tex(&backf.img).unwrap_or_else(|| front_t.clone());
|
||||
let plan = PiecePlan { pieces, front: front_t, back: Some(back_t) };
|
||||
if plan_only {
|
||||
return Err(plan);
|
||||
}
|
||||
return Ok(plan.finish(Buf { img: src, offset: b.offset, scale: b.scale }).img);
|
||||
}
|
||||
let out;
|
||||
{
|
||||
// Wireframes: piece outlines (front view: at rest; otherwise animated), plus forces.
|
||||
let animated = view == 2 || view == 4;
|
||||
let mut lines = Vec::new();
|
||||
@@ -889,8 +950,7 @@ fn shatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
}
|
||||
out = splat(src.width, src.height, &lines, Acc::Over);
|
||||
}
|
||||
b.img = out;
|
||||
b
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Caustics
|
||||
@@ -911,35 +971,62 @@ fn sample_repeat(img: &Image, x: f64, y: f64, mode: u32) -> Px {
|
||||
}
|
||||
}
|
||||
|
||||
fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// Everything Caustics' per-pixel pass needs, prepared on the CPU (the other layers it reads
|
||||
/// and the parameter conversions); the layer's own pixels are not read.
|
||||
pub struct CausticsSetup {
|
||||
/// The Bottom layer fitted to the buffer, `None` = the layer itself.
|
||||
pub bottom: Option<Image>,
|
||||
/// Gaussian σ (buffer px) blurring the bottom (transparent edges), 0 = none.
|
||||
pub bottom_sigma: f64,
|
||||
/// The water surface's height field (smoothed luminance), `None` = flat.
|
||||
pub height: Option<Plane>,
|
||||
/// The Sky layer fitted to the buffer.
|
||||
pub sky: Option<Image>,
|
||||
pub scaling: f64,
|
||||
pub repeat: u32,
|
||||
pub wave_h: f64,
|
||||
/// Displacement per unit of height gradient.
|
||||
pub k: f64,
|
||||
/// Layer centre (buffer px).
|
||||
pub cx: f64,
|
||||
pub cy: f64,
|
||||
/// Layer width (buffer px).
|
||||
pub lw: f64,
|
||||
pub surf: [f32; 4],
|
||||
pub surf_op: f32,
|
||||
pub cstr: f32,
|
||||
pub lc: [f32; 4],
|
||||
pub li: f32,
|
||||
/// Light position (buffer px) and height (layer widths); `point` = from the position to
|
||||
/// each pixel, else one direction from above the layer centre.
|
||||
pub lx: f64,
|
||||
pub ly: f64,
|
||||
pub lheight: f64,
|
||||
pub point: bool,
|
||||
pub ambient: f32,
|
||||
pub diffuse: f32,
|
||||
pub specular: f32,
|
||||
pub sharp: f32,
|
||||
pub sky_scaling: f64,
|
||||
pub sky_repeat: u32,
|
||||
pub sky_int: f32,
|
||||
pub convergence: f64,
|
||||
}
|
||||
|
||||
/// Caustics' setup for layer geometry `b` (see [`CausticsSetup`]).
|
||||
pub fn caustics_setup(ctx: &EffectCtx, b: &Buf) -> CausticsSetup {
|
||||
let pr = ctx.params;
|
||||
let s = b.scale;
|
||||
let mut bottom = layer_or_self(ctx, &b, "bottom/bottom", true, pr.e("bottom/bottomSizeDiffers") == 1);
|
||||
let bottom = ctx.layer_param("bottom/bottom", true).map(|o| crate::util::fit_layer(ctx, b, &o, pr.e("bottom/bottomSizeDiffers") == 1));
|
||||
let blur = pr.f("bottom/blur") * s;
|
||||
if blur > 0.05 {
|
||||
bottom = effectcraft_raster::gaussian_blur(&bottom, blur * 0.5, blur * 0.5, false);
|
||||
}
|
||||
let scaling = pr.f("bottom/scaling").max(0.01);
|
||||
let repeat = pr.e("bottom/repeatMode");
|
||||
let wave_h = pr.f("water/waveHeight");
|
||||
let smoothing = pr.f("water/smoothing") * s;
|
||||
let depth = pr.f("water/waterDepth");
|
||||
let ior = pr.f("water/refractiveIndex").max(1.0);
|
||||
let surf = pr.color("water/surfaceColor");
|
||||
let surf_op = pr.f("water/surfaceOpacity") as f32;
|
||||
let cstr = pr.f("water/causticsStrength") as f32;
|
||||
let li = pr.f("lighting/lightIntensity") as f32;
|
||||
let lc = pr.color("lighting/lightColor");
|
||||
let lpos = pr.v2("lighting/lightPosition");
|
||||
let lheight = pr.f("lighting/lightHeight").max(0.01);
|
||||
let ambient = pr.f("lighting/ambientLight") as f32;
|
||||
let diffuse = pr.f("material/diffuse") as f32;
|
||||
let specular = pr.f("material/specular") as f32;
|
||||
let sharp = pr.f("material/highlightSharpness").max(1.0) as f32;
|
||||
let (w, hh) = (b.img.width as usize, b.img.height as usize);
|
||||
// Height field of the water surface (luminance of the chosen layer; none = flat).
|
||||
let height = ctx.layer_param("water/waterSurface", true).map(|o| {
|
||||
let img = crate::util::fit_layer(ctx, &b, &o, true);
|
||||
let img = crate::util::fit_layer(ctx, b, &o, true);
|
||||
let pl = Plane::luma(&img);
|
||||
if smoothing > 0.05 { gauss_plane(&pl, smoothing * 0.5, smoothing * 0.5) } else { pl }
|
||||
});
|
||||
@@ -947,6 +1034,7 @@ fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let k = wave_h * depth * (1.0 - 1.0 / ior) * lw * 2.0;
|
||||
let (cx, cy) = (b.offset[0] + ctx.layer_size[0] * 0.5 * s, b.offset[1] + ctx.layer_size[1] * 0.5 * s);
|
||||
let (lx, ly) = b.to_px(lpos);
|
||||
let (lc, li, lheight) = (pr.color("lighting/lightColor"), pr.f("lighting/lightIntensity") as f32, pr.f("lighting/lightHeight").max(0.01));
|
||||
// Light Type: Distant Source (one direction, from the light position above the layer
|
||||
// centre), Point Source (from the light position to each pixel) or First Comp Light.
|
||||
let light_type = pr.e("lighting/lightType");
|
||||
@@ -962,6 +1050,75 @@ fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
(1, _) => (lc, li, lx, ly, lheight, true),
|
||||
_ => (lc, li, lx, ly, lheight, false),
|
||||
};
|
||||
// Sky: the layer the water surface reflects (Surface Opacity 1 = a mirror of it).
|
||||
let sky = ctx.layer_param("sky/sky", true).map(|o| crate::util::fit_layer(ctx, b, &o, pr.e("sky/skySizeDiffers") == 1));
|
||||
CausticsSetup {
|
||||
bottom,
|
||||
bottom_sigma: if blur > 0.05 { blur * 0.5 } else { 0.0 },
|
||||
height,
|
||||
sky,
|
||||
scaling: pr.f("bottom/scaling").max(0.01),
|
||||
repeat: pr.e("bottom/repeatMode"),
|
||||
wave_h,
|
||||
k,
|
||||
cx,
|
||||
cy,
|
||||
lw,
|
||||
surf: pr.color("water/surfaceColor"),
|
||||
surf_op: pr.f("water/surfaceOpacity") as f32,
|
||||
cstr: pr.f("water/causticsStrength") as f32,
|
||||
lc,
|
||||
li,
|
||||
lx,
|
||||
ly,
|
||||
lheight,
|
||||
point,
|
||||
ambient: pr.f("lighting/ambientLight") as f32,
|
||||
diffuse: pr.f("material/diffuse") as f32,
|
||||
specular: pr.f("material/specular") as f32,
|
||||
sharp: pr.f("material/highlightSharpness").max(1.0) as f32,
|
||||
sky_scaling: pr.get("sky/scaling").map(Value::as_f64).unwrap_or(1.0).max(0.01),
|
||||
sky_repeat: pr.e("sky/repeatMode"),
|
||||
sky_int: pr.get("sky/intensity").map(Value::as_f64).unwrap_or(0.3) as f32,
|
||||
convergence: pr.get("sky/convergence").map(Value::as_f64).unwrap_or(0.5).clamp(0.0, 1.0),
|
||||
}
|
||||
}
|
||||
|
||||
fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let st = caustics_setup(ctx, &b);
|
||||
let mut bottom = st.bottom.clone().unwrap_or_else(|| b.img.clone());
|
||||
if st.bottom_sigma > 0.0 {
|
||||
bottom = effectcraft_raster::gaussian_blur(&bottom, st.bottom_sigma, st.bottom_sigma, false);
|
||||
}
|
||||
let CausticsSetup {
|
||||
scaling,
|
||||
repeat,
|
||||
wave_h,
|
||||
k,
|
||||
cx,
|
||||
cy,
|
||||
lw,
|
||||
surf,
|
||||
surf_op,
|
||||
cstr,
|
||||
lc,
|
||||
li,
|
||||
lx,
|
||||
ly,
|
||||
lheight,
|
||||
point,
|
||||
ambient,
|
||||
diffuse,
|
||||
specular,
|
||||
sharp,
|
||||
sky_scaling,
|
||||
sky_repeat,
|
||||
sky_int,
|
||||
convergence,
|
||||
..
|
||||
} = st;
|
||||
let (height, sky) = (&st.height, &st.sky);
|
||||
let (w, hh) = (b.img.width as usize, b.img.height as usize);
|
||||
let norm3 = |v: [f64; 3]| {
|
||||
let n = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-12);
|
||||
[(v[0] / n) as f32, (v[1] / n) as f32, (v[2] / n) as f32]
|
||||
@@ -973,14 +1130,8 @@ fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
norm3([(lx - cx) / lw.max(1.0), (ly - cy) / lw.max(1.0), lheight])
|
||||
}
|
||||
};
|
||||
// Sky: the layer the water surface reflects (Surface Opacity 1 = a mirror of it).
|
||||
let sky = ctx.layer_param("sky/sky", true).map(|o| crate::util::fit_layer(ctx, &b, &o, pr.e("sky/skySizeDiffers") == 1));
|
||||
let sky_scaling = pr.get("sky/scaling").map(Value::as_f64).unwrap_or(1.0).max(0.01);
|
||||
let sky_repeat = pr.e("sky/repeatMode");
|
||||
let sky_int = pr.get("sky/intensity").map(Value::as_f64).unwrap_or(0.3) as f32;
|
||||
let convergence = pr.get("sky/convergence").map(Value::as_f64).unwrap_or(0.5).clamp(0.0, 1.0);
|
||||
let grad = |x: usize, y: usize| -> (f64, f64) {
|
||||
match &height {
|
||||
match height {
|
||||
Some(pl) => {
|
||||
let gx = (pl.get_clamped(x as i64 + 1, y as i64) - pl.get_clamped(x as i64 - 1, y as i64)) as f64 * 0.5;
|
||||
let gy = (pl.get_clamped(x as i64, y as i64 + 1) - pl.get_clamped(x as i64, y as i64 - 1)) as f64 * 0.5;
|
||||
@@ -1052,12 +1203,13 @@ fn caustics(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
|
||||
// ------------------------------------------------------------------ Wave World
|
||||
|
||||
/// Wave World's simulated surface: an `nx` × `ny` grid of heights `u` and velocities `v`.
|
||||
#[derive(Clone, Debug)]
|
||||
struct Waves {
|
||||
nx: usize,
|
||||
ny: usize,
|
||||
u: Vec<f32>,
|
||||
v: Vec<f32>,
|
||||
pub struct Waves {
|
||||
pub nx: usize,
|
||||
pub ny: usize,
|
||||
pub u: Vec<f32>,
|
||||
pub v: Vec<f32>,
|
||||
}
|
||||
|
||||
struct Producer {
|
||||
@@ -1091,7 +1243,27 @@ impl Producer {
|
||||
|
||||
static WAVE_CACHE: SimCache<Waves> = SimCache::new(4);
|
||||
|
||||
fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// What Wave World draws: the Height Map view of the simulated surface (grid heights and,
|
||||
/// with a ground layer, water depths per grid point), or the wireframe preview's lines.
|
||||
pub enum WavePlan {
|
||||
Height { st: std::sync::Arc<Waves>, depth: Option<Vec<f32>> },
|
||||
Wire(crate::sim::SpritePlan),
|
||||
}
|
||||
|
||||
fn wave_world(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
match wave_world_impl(ctx, &b, false) {
|
||||
Ok(img) => Buf { img, ..b },
|
||||
Err(WavePlan::Wire(plan)) => plan.finish(b),
|
||||
Err(WavePlan::Height { .. }) => b,
|
||||
}
|
||||
}
|
||||
|
||||
/// Wave World's simulation state for the frame, as a [`WavePlan`] (its pixels are not read).
|
||||
pub fn wave_world_plan(ctx: &EffectCtx, b: &Buf) -> Option<WavePlan> {
|
||||
wave_world_impl(ctx, b, true).err()
|
||||
}
|
||||
|
||||
fn wave_world_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result<Image, WavePlan> {
|
||||
let pr = ctx.params;
|
||||
let [lw, lh] = ctx.layer_size;
|
||||
let res = pr.f("simulation/gridResolution").clamp(1.0, 400.0) as usize;
|
||||
@@ -1239,7 +1411,11 @@ fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let c = g(x0, y1) + (g(x1, y1) - g(x0, y1)) * tx;
|
||||
a + (c - a) * ty
|
||||
};
|
||||
if plan_only && pr.e("view") == 1 {
|
||||
return Err(WavePlan::Height { st, depth });
|
||||
}
|
||||
if pr.e("view") == 1 {
|
||||
let mut img = Image::new(bw, bh);
|
||||
let bright = pr.f("heightMapControls/brightness") as f32;
|
||||
let contrast = pr.f("heightMapControls/contrast") as f32;
|
||||
let gamma = pr.f("heightMapControls/gamma").max(0.01) as f32;
|
||||
@@ -1251,7 +1427,7 @@ fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let gy = (y / lw.max(1.0) * (st.nx - 1) as f64).round().clamp(0.0, (st.ny - 1) as f64) as usize;
|
||||
d[gy * st.nx + gx] <= 0.0
|
||||
};
|
||||
b.img.rows_mut().for_each(|(y, row)| {
|
||||
img.rows_mut().for_each(|(y, row)| {
|
||||
for (x, px) in row.iter_mut().enumerate() {
|
||||
let lx = (x as f64 + 0.5 - b.offset[0]) / s;
|
||||
let ly = (y as f64 + 0.5 - b.offset[1]) / s;
|
||||
@@ -1260,6 +1436,7 @@ fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
*px = [v * al, v * al, v * al, al];
|
||||
}
|
||||
});
|
||||
Ok(img)
|
||||
} else {
|
||||
// Wireframe preview: the grid turned by Horizontal Rotation about the vertical axis,
|
||||
// tilted by Vertical Rotation, heights scaled by Vertical Scale.
|
||||
@@ -1291,10 +1468,12 @@ fn wave_world(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
}
|
||||
}
|
||||
}
|
||||
let wire = splat(bw, bh, &lines, Acc::Over);
|
||||
b.img = crate::sim::combine(&Image::filled(bw, bh, [0.0, 0.0, 0.0, 1.0]), &wire, 0);
|
||||
let plan = crate::sim::SpritePlan { sprites: lines, tints: vec![], acc: Acc::Over, post: crate::sim::Post::OverBlack };
|
||||
if plan_only {
|
||||
return Err(WavePlan::Wire(plan));
|
||||
}
|
||||
Ok(plan.finish(Buf { img: Image::new(bw, bh), offset: b.offset, scale: b.scale }).img)
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ Foam
|
||||
@@ -1336,7 +1515,29 @@ fn foam_pop_age(id: u32, lifespan: f32, frailty: f32, seed: u32) -> f32 {
|
||||
/// other, wobble, and pop at the end of their (Strength-shortened) lifespan, pushing their
|
||||
/// neighbours away at Pop Velocity. Rendered bubbles use a built-in texture or the Bubble
|
||||
/// Texture Layer, oriented by Bubble Orientation, and reflect the Environment Map.
|
||||
fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn foam(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
match foam_impl(ctx, &b, false) {
|
||||
Ok(img) => Buf { img, ..b },
|
||||
Err(plan) => plan.finish(b),
|
||||
}
|
||||
}
|
||||
|
||||
/// Foam's bubbles as sprites, `None` when it draws more than sprites (a User Defined bubble
|
||||
/// texture, an Environment Map reflection or the Draft + Flow Map view's flow map).
|
||||
pub(crate) fn foam_plan(ctx: &EffectCtx, b: &Buf) -> Option<crate::sim::SpritePlan> {
|
||||
let pr = ctx.params;
|
||||
let set = |id: &str| matches!(pr.get(id), Some(Value::Layer(Some(_))));
|
||||
let view = pr.e("view");
|
||||
if (view == 2 && pr.e("rendering/bubbleTexture") == 5)
|
||||
|| (view == 2 && pr.f("rendering/reflectionStrength") > 0.0 && set("rendering/environmentMap"))
|
||||
|| (view == 1 && set("flowMap/flowMap"))
|
||||
{
|
||||
return None;
|
||||
}
|
||||
foam_impl(ctx, b, true).err()
|
||||
}
|
||||
|
||||
fn foam_impl(ctx: &EffectCtx, b: &Buf, plan_only: bool) -> Result<Image, crate::sim::SpritePlan> {
|
||||
let pr = ctx.params;
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
let unit = lw * 0.01;
|
||||
@@ -1558,7 +1759,11 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
sp
|
||||
})
|
||||
.collect();
|
||||
let mut out = splat(b.img.width, b.img.height, &sprites, if blend == 0 && view == 2 { Acc::Add } else { Acc::Over });
|
||||
let acc = if blend == 0 && view == 2 { Acc::Add } else { Acc::Over };
|
||||
if plan_only {
|
||||
return Err(crate::sim::SpritePlan { sprites, tints: vec![], acc, post: crate::sim::Post::Replace });
|
||||
}
|
||||
let mut out = splat(b.img.width, b.img.height, &sprites, acc);
|
||||
// User Defined: the texture layer fills each bubble's disc.
|
||||
if let Some(tex) = &user_tex {
|
||||
for q in &order {
|
||||
@@ -1581,7 +1786,7 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
&& strength > 0.0
|
||||
&& let Some(env) = ctx.layer_param("rendering/environmentMap", true)
|
||||
{
|
||||
let env_img = crate::util::fit_layer(ctx, &b, &env, true);
|
||||
let env_img = crate::util::fit_layer(ctx, b, &env, true);
|
||||
let conv = pr.f("rendering/reflectionConvergence").clamp(0.0, 1.0);
|
||||
let (w, h) = (b.img.width as f64, b.img.height as f64);
|
||||
for q in &order {
|
||||
@@ -1602,7 +1807,7 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
if view == 1
|
||||
&& let Some(lp) = &flow
|
||||
{
|
||||
let fm = crate::util::fit_layer(ctx, &b, lp, true);
|
||||
let fm = crate::util::fit_layer(ctx, b, lp, true);
|
||||
out.data.par_iter_mut().zip(fm.data.par_iter()).for_each(|(o, f)| {
|
||||
let k = 1.0 - o[3];
|
||||
for c in 0..4 {
|
||||
@@ -1610,8 +1815,7 @@ fn foam(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
}
|
||||
});
|
||||
}
|
||||
b.img = out;
|
||||
b
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Add `f(u, v)` (premultiplied; u, v ∈ −1..1 across the disc) over the disc of radius `r` at
|
||||
|
||||
+21
-21
@@ -27,7 +27,7 @@ use effectcraft_project::ParamUi;
|
||||
use effectcraft_raster::{Image, Px};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::sim::{Acc, SPS, Shape, Sprite, combine, splat, steps_at};
|
||||
use crate::sim::{Acc, Post, SPS, Shape, Sprite, SpritePlan, Tint, combine, splat, steps_at};
|
||||
use crate::util::{SimCache, hash1, params_key, point_in_poly, unpremul};
|
||||
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||||
|
||||
@@ -897,12 +897,20 @@ fn particle_playground(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
|
||||
// ---------------------------------------------------------------- CC Hair
|
||||
|
||||
fn cc_hair(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn cc_hair(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
let Some(mut plan) = hair_plan(ctx, &b) else { return b };
|
||||
plan.resolve(&b.img);
|
||||
plan.finish(b)
|
||||
}
|
||||
|
||||
/// CC Hair's strands as line sprites (each coloured from its root, see [`Tint::Root`]);
|
||||
/// `None` = no hair (the layer passes through).
|
||||
pub(crate) fn hair_plan(ctx: &EffectCtx, b: &Buf) -> Option<SpritePlan> {
|
||||
let pr = ctx.params;
|
||||
let len = pr.f("length").max(0.0) as f32;
|
||||
let density = pr.f("density").max(0.0) as f32 / 100.0;
|
||||
if len <= 0.0 || density <= 0.0 {
|
||||
return b;
|
||||
return None;
|
||||
}
|
||||
let thick = pr.f("thickness").max(0.05) as f32;
|
||||
let weight = pr.f("weight") as f32;
|
||||
@@ -924,19 +932,13 @@ fn cc_hair(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let (lw, lh) = (ctx.layer_size[0] as f32, ctx.layer_size[1] as f32);
|
||||
// Roots: a jittered grid over the layer, density per 1 000 px².
|
||||
let count = ((lw * lh / 1000.0) * density * 10.0).min(200_000.0) as u32;
|
||||
let src = b.img.clone();
|
||||
let segs = 6u32;
|
||||
let strands: Vec<Vec<Sprite>> = (0..count)
|
||||
let strands: Vec<Vec<(Sprite, Tint)>> = (0..count)
|
||||
.into_par_iter()
|
||||
.filter_map(|i| {
|
||||
.map(|i| {
|
||||
let rx = hash1(i, 1, seed) * lw;
|
||||
let ry = hash1(i, 2, seed) * lh;
|
||||
let (bx, by) = b.to_px([rx as f64, ry as f64]);
|
||||
let root = src.sample_bilinear(bx, by);
|
||||
if root[3] < 0.5 {
|
||||
return None;
|
||||
}
|
||||
let (rc, _) = unpremul(root);
|
||||
let mut l = len * (0.75 + 0.5 * hash1(i, 3, seed));
|
||||
// Lean: random, plus the map's local gradient (hairfall map).
|
||||
let mut lean = [(hash1(i, 4, seed) * 2.0 - 1.0) * 0.6, -1.0 + hash1(i, 5, seed) * 0.4];
|
||||
@@ -957,7 +959,6 @@ fn cc_hair(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let dir = [lean[0] / ll, lean[1] / ll];
|
||||
// Droop: heavier (or thinner with Constant Mass off) hair bends more.
|
||||
let droop = weight * if const_mass { 1.0 } else { 1.0 / thick.max(0.2) } * l;
|
||||
let base = [rc[0] + (hc[0] - rc[0]) * (1.0 - inherit), rc[1] + (hc[1] - rc[1]) * (1.0 - inherit), rc[2] + (hc[2] - rc[2]) * (1.0 - inherit)];
|
||||
let pt = |s: f32| [rx + dir[0] * l * s, ry + dir[1] * l * s + droop * s * s];
|
||||
let mut out = Vec::with_capacity(segs as usize);
|
||||
for k in 0..segs {
|
||||
@@ -974,24 +975,23 @@ fn cc_hair(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let spec = (1.0 - tl.abs()).powf(1.0 / rough) * spe;
|
||||
let shade = (amb + dif * diff * light_i) * bright;
|
||||
let tip = 1.0 - 0.6 * s1;
|
||||
let c3 = [base[0] * shade + spec * light_i, base[1] * shade + spec * light_i, base[2] * shade + spec * light_i];
|
||||
let (pa, pc) = (b.to_px([a[0] as f64, a[1] as f64]), b.to_px([c[0] as f64, c[1] as f64]));
|
||||
out.push(Sprite {
|
||||
let sp = Sprite {
|
||||
x: pa.0 as f32,
|
||||
y: pa.1 as f32,
|
||||
r: thick * 0.5 * b.scale as f32 * tip,
|
||||
c: [c3[0], c3[1], c3[2], opacity],
|
||||
c: [0.0, 0.0, 0.0, opacity],
|
||||
shape: Shape::Line { dx: (pc.0 - pa.0) as f32, dy: (pc.1 - pa.1) as f32 },
|
||||
rot: 0.0,
|
||||
});
|
||||
};
|
||||
// The root's colour mixed with the hair colour (no strand where it is clear).
|
||||
out.push((sp, Tint::Root { x: bx, y: by, hair: [hc[0], hc[1], hc[2]], inherit, shade, spec: spec * light_i }));
|
||||
}
|
||||
Some(out)
|
||||
out
|
||||
})
|
||||
.collect();
|
||||
let sprites: Vec<Sprite> = strands.into_iter().flatten().collect();
|
||||
let fx = splat(b.img.width, b.img.height, &sprites, Acc::Over);
|
||||
b.img = combine(&b.img, &fx, 0);
|
||||
b
|
||||
let (sprites, tints) = strands.into_iter().flatten().unzip();
|
||||
Some(SpritePlan { sprites, tints, acc: Acc::Over, post: Post::Combine(0) })
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<EffectSpec> {
|
||||
|
||||
@@ -9,7 +9,7 @@ use effectcraft_project::ParamUi;
|
||||
use effectcraft_raster::Px;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::sim2::{Piece, draw_pieces, rot_axis, sort_far_first};
|
||||
use crate::sim2::{Piece, PiecePlan, PieceTex, rot_axis, sort_far_first};
|
||||
use crate::util::{Plane, gauss_plane, hash1, layer_rect, remap, unpremul};
|
||||
use crate::{Buf, EffectCtx, EffectSpec, ParamSpec, col, num, p, popup, slider};
|
||||
|
||||
@@ -175,7 +175,15 @@ fn iris_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
/// through a 180° turn about Flip Axis, as textured planes in 3D seen through the Camera
|
||||
/// System and lit by Lighting / Material. The flipped side shows Back Layer (none: the cards
|
||||
/// vanish as they turn away). Position / Rotation Jitter wobble the cards over time.
|
||||
fn card_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
fn card_wipe(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
match card_wipe_plan(ctx, &b) {
|
||||
Some(plan) => plan.finish(b),
|
||||
None => b,
|
||||
}
|
||||
}
|
||||
|
||||
/// Card Wipe's cards (`None` = nothing has moved: the layer as is).
|
||||
pub(crate) fn card_wipe_plan(ctx: &EffectCtx, b: &Buf) -> Option<PiecePlan> {
|
||||
let done = completion(ctx);
|
||||
let pr = ctx.params;
|
||||
let f = |id: &str| pr.get(id).map(Value::as_f64).unwrap_or(0.0);
|
||||
@@ -199,7 +207,7 @@ fn card_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
// Nothing has moved: the layer as is (exactly).
|
||||
let xy = pr.get("cameraPosition/xyPosition").map(|v| v.as_vec2());
|
||||
if xy.is_none_or(|p| (p[0] - ctx.layer_size[0] * 0.5).abs() < 1e-9 && (p[1] - ctx.layer_size[1] * 0.5).abs() < 1e-9) {
|
||||
return b;
|
||||
return None;
|
||||
}
|
||||
}
|
||||
let tw = (pr.f("transitionWidth") / 100.0).clamp(0.01, 1.0);
|
||||
@@ -212,25 +220,25 @@ fn card_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let order = pr.e("flipOrder");
|
||||
let jitter = pr.f("timingRandomness").clamp(0.0, 1.0);
|
||||
let seed = pr.f("randomSeed") as u32 ^ 0xca7d;
|
||||
let (x0, y0, w, h) = layer_rect(ctx, &b);
|
||||
let (x0, y0, w, h) = layer_rect(ctx, b);
|
||||
let (cw, ch) = (w / cols as f64, h / rows as f64);
|
||||
let nx = |i: i64| if cols > 1 { i as f64 / (cols - 1) as f64 } else { 0.0 };
|
||||
let ny = |j: i64| if rows > 1 { j as f64 / (rows - 1) as f64 } else { 0.0 };
|
||||
// Flip Order "Gradient": cards flip first where the gradient layer (default: this layer) is dark.
|
||||
let grad = (order == 8).then(|| match ctx.layer_param("gradientLayer", false) {
|
||||
Some(o) => place_layer(ctx, &b, &o, 2),
|
||||
Some(o) => place_layer(ctx, b, &o, 2),
|
||||
None => b.img.clone(),
|
||||
});
|
||||
// Back Layer: a chosen layer (stretched to fit), or this layer for projects saved with the
|
||||
// old "Self" choice; none hides the cards' backs.
|
||||
let back = match ctx.layer_param("backLayer", true) {
|
||||
Some(o) => Some(crate::util::fit_layer(ctx, &b, &o, true)),
|
||||
None if pr.b("backSelf") => Some(b.img.clone()),
|
||||
Some(o) => Some(PieceTex::Image(crate::util::fit_layer(ctx, b, &o, true))),
|
||||
None if pr.b("backSelf") => Some(PieceTex::Layer),
|
||||
None => None,
|
||||
};
|
||||
let cam = crate::card3d::projection(ctx, &b);
|
||||
let cam = crate::card3d::projection(ctx, b);
|
||||
let eye = cam.eye();
|
||||
let light = crate::card3d::Lighting::from(ctx, &b);
|
||||
let light = crate::card3d::Lighting::from(ctx, b);
|
||||
let t = ctx.time;
|
||||
let jit = |i: i64, j: i64, k: u32, speed: f64| -> f64 {
|
||||
let ph = hash1(i as u32, j as u32, seed ^ k) as f64 * std::f64::consts::TAU;
|
||||
@@ -303,10 +311,7 @@ fn card_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
})
|
||||
.collect();
|
||||
sort_far_first(&mut pieces);
|
||||
let mut out = effectcraft_raster::Image::new(b.img.width, b.img.height);
|
||||
draw_pieces(&mut out, &pieces, &b.img, back.as_ref());
|
||||
b.img = out;
|
||||
b
|
||||
Some(PiecePlan { pieces, front: PieceTex::Layer, back })
|
||||
}
|
||||
|
||||
fn grid_wipe(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
|
||||
Reference in New Issue
Block a user