mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-01 14:08:38 -06:00
Refraction samples are clamped to the layer rectangle instead of the buffer, so a sample past the layer's edge in a buffer with transparent padding (a half-resolution preview's, a text or shape layer's) repeats the layer's edge pixels rather than reaching the pale colour that stands in for transparent ones. Transparent pixels inside the layer still show it. Same bounds on the GPU path. Re-pins the four half-resolution CC Bubbles golden hashes (aarch64 macOS); the full-resolution ones are unchanged. Adds a CPU test and a GPU-vs-CPU parity test on padded buffers.
365 lines
16 KiB
Rust
365 lines
16 KiB
Rust
use super::common::{h, hs};
|
|
use super::sprites::{Acc, Post, Shape, Sprite, SpritePlan, Tint, aa, raster};
|
|
use super::{Buf, EffectCtx, Image, IntoParallelIterator, ParallelIterator, unpremul};
|
|
use crate::util::layer_rect_at;
|
|
|
|
// ------------------------------------------------------------------ CC Rainfall / CC Snowfall
|
|
|
|
/// Shared weather layout: per-item depth factor `k` in (0, 1] (1 = nearest) from Scene Depth.
|
|
#[inline]
|
|
fn depth_k(z: f32, scene_depth: f32) -> f32 {
|
|
1.0 / (1.0 + z * scene_depth / 2500.0)
|
|
}
|
|
|
|
/// Wrap `v` into `[lo, lo + span)`.
|
|
#[inline]
|
|
fn wrap(v: f32, lo: f32, span: f32) -> f32 {
|
|
if span <= 0.0 { v } else { lo + (v - lo).rem_euclid(span) }
|
|
}
|
|
|
|
pub(super) fn rainfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
|
let mut plan = rainfall_plan(ctx, &b);
|
|
plan.resolve(&b.img);
|
|
plan.finish(b)
|
|
}
|
|
|
|
pub(super) 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;
|
|
let depth = pr.f("sceneDepth").max(0.0) as f32;
|
|
let speed = pr.f("speed") as f32;
|
|
let wind = pr.f("wind") as f32;
|
|
let wvar = pr.f("windVariation") as f32 / 100.0;
|
|
let spread = pr.f("spread") as f32;
|
|
let color = pr.color("color");
|
|
let opacity = pr.f("opacity") as f32 / 100.0;
|
|
let mode = pr.e("transferMode");
|
|
let with_orig = pr.b("compositeWithOriginal");
|
|
let refract = pr.e("extras/appearance") == 0;
|
|
let off = pr.v2("extras/offset");
|
|
let seed = pr.f("extras/randomSeed") as u32 ^ ctx.seed.wrapping_mul(0x9e37);
|
|
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 (ox, oy) = (off[0] as f32 - lw * 0.5, off[1] as f32 - lh * 0.5);
|
|
let (sprites, tints): (Vec<Sprite>, Vec<Tint>) = (0..n)
|
|
.into_par_iter()
|
|
.map(|i| {
|
|
let z = h(i, 1, seed);
|
|
let k = depth_k(z, depth);
|
|
let vy = speed / 4000.0 * lh * 2.2 * k;
|
|
let w = wind / 1000.0 * lh * (1.0 + wvar * hs(i, 2, seed)) * k + spread * hs(i, 7, seed) * k;
|
|
let len = (vy * 0.04 * size.max(0.2)).max(1.0);
|
|
let lenx = w * 0.04 * size.max(0.2);
|
|
let margin = len + lenx.abs() + 4.0;
|
|
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 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);
|
|
(Sprite::new(bx as f32, by as f32, r, c, Shape::Line { dx: -lenx * s, dy: -len * s }), tint)
|
|
})
|
|
.unzip();
|
|
SpritePlan { sprites, tints, acc: Acc::Over, post: weather_post(mode, with_orig) }
|
|
}
|
|
|
|
fn weather_post(mode: u32, with_orig: bool) -> Post {
|
|
if with_orig { Post::Combine(if mode == 1 { 3 } else { 0 }) } else { Post::Replace }
|
|
}
|
|
|
|
pub(super) fn snowfall(ctx: &EffectCtx, b: Buf) -> Buf {
|
|
snowfall_plan(ctx, &b).finish(b)
|
|
}
|
|
|
|
pub(super) 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;
|
|
let svar = pr.f("sizeVariation") as f32 / 100.0;
|
|
let depth = pr.f("sceneDepth").max(0.0) as f32;
|
|
let speed = pr.f("speed") as f32;
|
|
let spvar = pr.f("speedVariation") as f32 / 100.0;
|
|
let wind = pr.f("wind") as f32;
|
|
let wvar = pr.f("windVariation") as f32 / 100.0;
|
|
let spread = pr.f("spread") as f32;
|
|
let wig = pr.f("wiggle/wiggleAmount") as f32;
|
|
let wigvar = pr.f("wiggle/wiggleAmountVariation") as f32 / 100.0;
|
|
let wfreq = pr.f("wiggle/wiggleFrequency") as f32;
|
|
let wfvar = pr.f("wiggle/wiggleFrequencyVariation") as f32 / 100.0;
|
|
let color = pr.color("color");
|
|
let opacity = pr.f("opacity") as f32 / 100.0;
|
|
let mode = pr.e("transferMode");
|
|
let with_orig = pr.b("compositeWithOriginal");
|
|
let off = pr.v2("extras/offset");
|
|
let seed = pr.f("extras/randomSeed") as u32 ^ ctx.seed.wrapping_mul(0x85eb);
|
|
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 (ox, oy) = (off[0] as f32 - lw * 0.5, off[1] as f32 - lh * 0.5);
|
|
let sprites: Vec<Sprite> = (0..n)
|
|
.into_par_iter()
|
|
.map(|i| {
|
|
let z = h(i, 1, seed);
|
|
let k = depth_k(z, depth);
|
|
let r = (size * 0.5 * k * (1.0 + svar * hs(i, 2, seed))).max(0.05);
|
|
let vy = speed * (lh / 600.0) * k * (1.0 + spvar * hs(i, 3, seed));
|
|
let vx = wind * (lh / 1200.0) * k * (1.0 + wvar * hs(i, 4, seed)) + spread * hs(i, 8, seed) * k;
|
|
let amp = wig * 2.0 * k * (1.0 + wigvar * hs(i, 5, seed));
|
|
let fr = wfreq * (1.0 + wfvar * hs(i, 6, seed));
|
|
let ph = h(i, 9, seed) * std::f32::consts::TAU;
|
|
let margin = r + amp.abs() + 2.0;
|
|
let y = wrap(h(i, 10, seed) * (lh + 2.0 * margin) + vy * t + oy, -margin, lh + 2.0 * margin);
|
|
let x = wrap(h(i, 11, seed) * (lw + 2.0 * margin) + vx * t + ox, -margin, lw + 2.0 * margin) + amp * (fr * t * std::f32::consts::TAU + ph).sin();
|
|
let (bx, by) = b.to_px([x as f64, y as f64]);
|
|
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();
|
|
SpritePlan { sprites, tints: vec![], acc: Acc::Over, post: weather_post(mode, with_orig) }
|
|
}
|
|
|
|
// ------------------------------------------------------------------ CC Bubbles
|
|
|
|
/// 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).
|
|
pub sx: f32,
|
|
pub sy: f32,
|
|
}
|
|
|
|
/// 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 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;
|
|
(0..n)
|
|
.map(|i| {
|
|
let r = base * (0.5 + h(i, 1, seed));
|
|
let travel = lh + 2.0 * r;
|
|
let v = speed * lh * 0.3 * (0.6 + 0.8 * h(i, 2, seed));
|
|
let y = lh + r - (h(i, 3, seed) * travel + v * t).rem_euclid(travel);
|
|
let x0 = h(i, 4, seed) * lw;
|
|
let x = x0 + wamp * (r / base.max(1e-3)) * (t * wfreq * std::f32::consts::TAU + h(i, 5, seed) * std::f32::consts::TAU).sin();
|
|
let (bx, by) = b.to_px([x as f64, y as f64]);
|
|
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()
|
|
}
|
|
|
|
/// CC Bubbles' refraction sample bounds in the pixels of a buffer with `offset` and `scale`: the
|
|
/// layer rectangle's first and last pixel centres (x0, y0, x1, y1). Samples clamped to them repeat
|
|
/// the layer's edge pixels instead of reaching transparent padding around it (#2024).
|
|
pub fn bubble_sample_bounds(ctx: &EffectCtx, offset: [f64; 2], scale: f64) -> [f32; 4] {
|
|
let (lx, ly, lw, lh) = layer_rect_at(ctx, offset, scale);
|
|
let (x0, y0) = (lx + 0.5, ly + 0.5);
|
|
[x0 as f32, y0 as f32, (lx + lw - 0.5).max(x0) as f32, (ly + lh - 0.5).max(y0) as f32]
|
|
}
|
|
|
|
pub(super) 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 [x0, y0, x1, y1] = bubble_sample_bounds(ctx, b.offset, b.scale);
|
|
let src = b.img.clone();
|
|
let mut out = Image::new(src.width, src.height);
|
|
raster(
|
|
&mut out,
|
|
&list,
|
|
|q| 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]),
|
|
|q, x, y| {
|
|
let (dx, dy) = ((x - q.x) / q.r.max(0.5), (y - q.y) / q.r.max(0.5));
|
|
let d2 = dx * dx + dy * dy;
|
|
let d = d2.sqrt();
|
|
let cov = aa((1.0 - d) * q.r);
|
|
if cov <= 0.0 {
|
|
return None;
|
|
}
|
|
// Liquid refracts (inverted, magnified), metal reflects (wide-angle mirror).
|
|
let k = if metal { 1.6 } else { -0.7 };
|
|
let nz = (1.0 - d2).max(0.0).sqrt();
|
|
let (sx, sy) = ((q.sx + dx * q.r * k).max(x0).min(x1), (q.sy + dy * q.r * k).max(y0).min(y1));
|
|
let p = src.sample_bilinear_clamped(sx as f64, sy as f64);
|
|
let (c, sa) = unpremul(p);
|
|
let mut c = if sa <= 0.0 { [0.6, 0.7, 0.8] } else { c };
|
|
let mut a = cov;
|
|
match shading {
|
|
1 => c = c.map(|v| v * 0.7 + 0.3 * (1.0 - nz)),
|
|
2 => c = c.map(|v| v * (0.5 + 0.5 * nz)),
|
|
3 => a *= nz.powf(0.5),
|
|
4 => a *= 1.0 - nz * 0.8,
|
|
_ => {}
|
|
}
|
|
let hl = (1.0 - ((dx + 0.35).powi(2) + (dy + 0.35).powi(2)) / 0.05).max(0.0) * 0.8;
|
|
let c = c.map(|v| v + hl);
|
|
Some([c[0] * a, c[1] * a, c[2] * a, a])
|
|
},
|
|
Acc::Over,
|
|
);
|
|
b.img = out;
|
|
b
|
|
}
|
|
|
|
// ------------------------------------------------------------------ CC Drizzle
|
|
|
|
/// 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 spreading = pr.f("spreading") 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 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;
|
|
let k1 = (t * rate).floor() as u32;
|
|
let mut drops = Vec::new();
|
|
for k in k0..=k1 {
|
|
let born = k as f64 / rate;
|
|
let age = (t - born) as f32;
|
|
if age < 0.0 || age as f64 >= life {
|
|
continue;
|
|
}
|
|
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]);
|
|
}
|
|
Some(drops)
|
|
}
|
|
|
|
pub(super) 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 {
|
|
let d = ((x - dx).powi(2) + (y - dy).powi(2)).sqrt();
|
|
let u = (d - r) / width;
|
|
if u.abs() < 1.0 {
|
|
let env = 0.5 + 0.5 * (u * std::f32::consts::PI).cos();
|
|
v += a * env * ((d - r) / wavelen * std::f32::consts::TAU).cos();
|
|
}
|
|
}
|
|
v
|
|
};
|
|
let src = b.img.clone();
|
|
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]
|
|
};
|
|
b.img.rows_mut().for_each(|(y, row)| {
|
|
for (x, px) in row.iter_mut().enumerate() {
|
|
let (fx, fy) = (x as f32 + 0.5, y as f32 + 0.5);
|
|
let gx = field(fx + 1.0, fy) - field(fx - 1.0, fy);
|
|
let gy = field(fx, fy + 1.0) - field(fx, fy - 1.0);
|
|
if gx == 0.0 && gy == 0.0 {
|
|
continue;
|
|
}
|
|
let p = src.sample_bilinear_clamped((fx - gx * disp * s * 4.0) as f64, (fy - gy * disp * s * 4.0) as f64);
|
|
let n = {
|
|
let v = [-gx * 6.0, -gy * 6.0, 1.0];
|
|
let m = (v[0] * v[0] + v[1] * v[1] + 1.0).sqrt();
|
|
[v[0] / m, v[1] / m, v[2] / m]
|
|
};
|
|
let nl = (n[0] * l[0] + n[1] * l[1] + n[2] * l[2]).max(0.0);
|
|
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 sp = (n[0] * hv[0] + n[1] * hv[1] + n[2] * hv[2]).max(0.0).powf(1.0 / rough) * specular * li;
|
|
let shade = 1.0 + (ambient + diffuse * nl - diffuse * l[2]) * li;
|
|
let a = p[3];
|
|
*px = [p[0] * shade + sp * lc[0] * a, p[1] * shade + sp * lc[1] * a, p[2] * shade + sp * lc[2] * a, a];
|
|
}
|
|
});
|
|
b
|
|
}
|
|
|
|
// ------------------------------------------------------------------ CC Star Burst
|
|
|
|
pub(super) fn star_burst(ctx: &EffectCtx, b: Buf) -> Buf {
|
|
let mut plan = star_burst_plan(ctx, &b);
|
|
plan.resolve(&b.img);
|
|
plan.finish(b)
|
|
}
|
|
|
|
pub(super) 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;
|
|
let phase = pr.f("phase") as f32 / 360.0;
|
|
let spacing = pr.f("gridSpacing").max(1.0) as f32;
|
|
let size = pr.f("size") as f32 / 100.0;
|
|
let blend = pr.f("blendWithOriginal") as f32 / 100.0;
|
|
let seed = ctx.seed.wrapping_mul(0x3c6e);
|
|
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 nx = (lw / spacing).ceil().max(1.0) as u32;
|
|
let ny = (lh / spacing).ceil().max(1.0) as u32;
|
|
let (cx, cy) = (lw * 0.5, lh * 0.5);
|
|
let mut list: Vec<(f32, Sprite, Tint)> = (0..nx * ny)
|
|
.into_par_iter()
|
|
.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]);
|
|
// 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);
|
|
let jx = hs(i, 2, seed) * scatter * 0.01 * lw * 0.5;
|
|
let jy = hs(i, 3, seed) * scatter * 0.01 * lh * 0.5;
|
|
let x = cx + (gx - cx + jx) * persp;
|
|
let y = cy + (gy - cy + jy) * persp;
|
|
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);
|
|
// 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, 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 } }
|
|
}
|