effectcraft/crates/gpu/src/fx_transition.rs

603 lines
24 KiB
Rust

//! GPU effects, transitions and perspective (kernels in `shaders/fx_transition.wgsl`, every
//! entry point prefixed `ftr_`): Block Dissolve, CC Glass Wipe, CC Grid Wipe, CC Image Wipe,
//! CC Jaws, CC Line Sweep, CC Radial ScaleWipe, CC Scale Wipe, CC Twister, CC WarpoMatic,
//! Radial Shadow, CC Bender, CC Blobbylize, CC Cylinder, CC Sphere, CC Spotlight, CC
//! Environment and 3D Glasses, with the CPU effects' exact steps.
//!
//! Scalars (corner distances, projected extents, light vectors, rotation matrices) are computed
//! on the CPU in f64 as the CPU effect does; Block Dissolve's block indices too (one per column
//! and row sub-sample), so the hash sees the CPU's exact integers. Other layers (gradient,
//! reveal, backside, environment, stereo views) are fetched and fitted on the CPU
//! (`util::fit_layer`) and uploaded. Gradient planes are blurred with the CPU's
//! `util::gauss_plane` steps (box passes, edges repeated). Card Wipe (a 3D card renderer
//! shared with Card Dance and Shatter) stays on the CPU.
use effectcraft_effects::{Buf, EffectCtx};
use effectcraft_raster::Image;
use crate::context::{Enc, GpuImage, Params};
use crate::effects::{GBuf, gaussian_blur};
/// Compute entry points in `fx_transition.wgsl`.
pub(crate) const KERNELS: &[&str] = &[
"ftr_plane",
"ftr_mul",
"ftr_under",
"ftr_blocks",
"ftr_grid",
"ftr_radial_scale",
"ftr_scale",
"ftr_glass",
"ftr_image_wipe",
"ftr_jaws",
"ftr_line_sweep",
"ftr_twister",
"ftr_react_contrast",
"ftr_react_local",
"ftr_warpo",
"ftr_rshadow",
"ftr_bender",
"ftr_blob",
"ftr_cylinder",
"ftr_sphere",
"ftr_spot",
"ftr_env",
"ftr_shift",
"ftr_glasses",
];
/// Effect ids implemented here.
pub(crate) const IDS: &[&str] = &[
"ec.transition.blockdissolve",
"ec.transition.ccglasswipe",
"ec.transition.ccgridwipe",
"ec.transition.ccimagewipe",
"ec.transition.ccjaws",
"ec.transition.cclinesweep",
"ec.transition.ccradialscalewipe",
"ec.transition.ccscalewipe",
"ec.transition.cctwister",
"ec.transition.ccwarpomatic",
"ec.perspective.radialshadow",
"ec.distort.ccbender",
"ec.distort.ccblobbylize",
"ec.perspective.cccylinder",
"ec.perspective.ccsphere",
"ec.perspective.ccspotlight",
"ec.perspective.ccenvironment",
"ec.perspective.3dglasses",
];
/// 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> {
match id {
"ec.transition.blockdissolve" => block_dissolve(e, ctx, b),
"ec.transition.ccglasswipe" => glass_wipe(e, ctx, b),
"ec.transition.ccgridwipe" => grid_wipe(e, ctx, b),
"ec.transition.ccimagewipe" => image_wipe(e, ctx, b),
"ec.transition.ccjaws" => jaws(e, ctx, b),
"ec.transition.cclinesweep" => line_sweep(e, ctx, b),
"ec.transition.ccradialscalewipe" => radial_scale_wipe(e, ctx, b),
"ec.transition.ccscalewipe" => scale_wipe(e, ctx, b),
"ec.transition.cctwister" => twister(e, ctx, b),
"ec.transition.ccwarpomatic" => warpomatic(e, ctx, b),
"ec.perspective.radialshadow" => radial_shadow(e, ctx, b),
"ec.distort.ccbender" => bender(e, ctx, b),
"ec.distort.ccblobbylize" => blobbylize(e, ctx, b),
"ec.perspective.cccylinder" => cylinder(e, ctx, b),
"ec.perspective.ccsphere" => sphere(e, ctx, b),
"ec.perspective.ccspotlight" => spotlight(e, ctx, b),
"ec.perspective.ccenvironment" => environment(e, ctx, b),
"ec.perspective.3dglasses" => glasses(e, ctx, b),
_ => None,
}
}
// ---------------------------------------------------------------- helpers
/// A same-size per-pixel kernel over `src` (the buffer's size).
fn kernel(e: &mut Enc, entry: &str, p: &Params, src: &GpuImage, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> GpuImage {
let out = e.scratch(src.width, src.height);
e.pixels(entry, p, src, aux, &out, data);
out
}
fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> Option<GBuf> {
b.img = kernel(e, entry, p, &b.img, aux, data);
Some(b)
}
fn f4(v: [f64; 4]) -> [f32; 4] {
v.map(|x| x as f32)
}
fn rgb(c: [f32; 4], w: f32) -> [f32; 4] {
[c[0], c[1], c[2], w]
}
/// The CPU buffer geometry of `b` (no pixels: `fit_layer` reads only size, offset and scale).
fn shape(b: &GBuf) -> Buf {
Buf { img: Image { width: b.img.width, height: b.img.height, data: vec![] }, offset: b.offset, scale: b.scale }
}
/// The layer in layer parameter `id` fitted to `b` (`util::fit_layer`) and uploaded; `Some(None)`
/// when none is chosen.
fn fitted(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str, masks_and_effects: bool, stretch: bool) -> Option<Option<GpuImage>> {
match ctx.layer_param(id, masks_and_effects) {
Some(o) => Some(Some(e.g.upload_image(&effectcraft_effects::util::fit_layer(ctx, &shape(b), &o, stretch))?)),
None => Some(None),
}
}
/// `util::layer_or_self`: the fitted layer, or the buffer itself.
fn layer_or_self(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str) -> Option<GpuImage> {
Some(fitted(e, ctx, b, id, true, false)?.unwrap_or_else(|| b.img.clone()))
}
/// A plane (x) of `img`: `util::Src` index `src` times alpha (alpha itself unscaled); 11 is CC
/// Environment's relief.
fn plane(e: &mut Enc, img: &GpuImage, src: u32) -> GpuImage {
let mut p = Params::default();
p.u[0][0] = src;
kernel(e, "ftr_plane", &p, img, None, None)
}
/// transition2::prop_plane's property → `util::Src` index.
fn prop_src(prop: u32) -> u32 {
match prop {
5 => 6,
6 => 7,
p => p.min(4),
}
}
/// `img` as RGBA rows for the kernels' third image (`P.u[3]` = stride, width, height).
fn rows(e: &mut Enc, p: &mut Params, img: &GpuImage) -> wgpu::Buffer {
let (buf, stride) = e.image_rows(img);
p.u[3] = [stride, img.width, img.height, 0];
buf
}
fn completion(ctx: &EffectCtx) -> f64 {
(ctx.params.f("completion") / 100.0).clamp(0.0, 1.0)
}
// ---------------------------------------------------------------- Block Dissolve (transition.rs)
fn block_dissolve(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let done = completion(ctx) as f32;
if done <= 0.0 {
return Some(b);
}
let pr = ctx.params;
let bw = (pr.f("blockWidth") * b.scale).max(1e-3);
let bh = (pr.f("blockHeight") * b.scale).max(1e-3);
let feather = pr.f("feather") * b.scale;
let subs: Vec<f64> = if pr.b("softEdges") { (0..4).map(|k| (k as f64 + 0.5) / 4.0).collect() } else { vec![0.5] };
// Block indices per column / row sub-sample, as the CPU computes them (f64, then u32).
let index =
|n: u32, o: f64, size: f64| (0..n).flat_map(|i| subs.iter().map(move |s| (((i as f64 + s - o) / size).floor() as i64) as u32)).collect::<Vec<_>>();
let cols = index(b.img.width, b.offset[0], bw);
let table: Vec<u8> = cols.iter().chain(&index(b.img.height, b.offset[1], bh)).flat_map(|v| v.to_le_bytes()).collect();
let data = e.bytes(table);
let mut p = Params::default();
p.u[0] = [subs.len() as u32, ctx.seed ^ 0xb10c, cols.len() as u32, 0];
p.f[0][0] = done;
let mut mask = kernel(e, "ftr_blocks", &p, &b.img, None, Some(&data));
if feather > 0.0 {
mask = gaussian_blur(e, &mask, feather * 0.5, feather * 0.5, true);
}
run(e, "ftr_mul", &Params::default(), b, Some(&mask), None)
}
// ---------------------------------------------------------------- CC Grid Wipe / Radial ScaleWipe / Scale Wipe (transition.rs)
/// transition::max_corner_dist.
fn max_corner_dist(b: &GBuf, c: (f64, f64), metric: impl Fn(f64, f64) -> f64) -> f64 {
let (w, h) = (b.img.width as f64, b.img.height as f64);
[(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)].iter().map(|(x, y)| metric(x - c.0, y - c.1)).fold(1e-6, f64::max)
}
fn grid_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let done = completion(ctx);
if done <= 0.0 {
return Some(b);
}
let pr = ctx.params;
let c = b.to_px(pr.v2("center"));
let rot = pr.f("rotation").to_radians();
let border = pr.f("border").max(0.0) * b.scale;
let tiles = pr.f("tiles").round().clamp(1.0, 500.0);
let shape = pr.e("shape");
let cs = (ctx.layer_size[0] * b.scale / tiles).max(1.0);
let (s, co) = rot.sin_cos();
let metric = |dx: f64, dy: f64| match shape {
0 => dx.abs(),
2 => dx.abs().max(dy.abs()),
_ => dx.hypot(dy),
};
let maxd = max_corner_dist(&b, c, |dx, dy| metric(dx * co + dy * s, -dx * s + dy * co));
let mut p = Params::default();
p.u[0] = [shape, pr.b("reverse") as u32, 0, 0];
p.f[0] = f4([c.0, c.1, s, co]);
p.f[1] = f4([cs, maxd, done, border]);
run(e, "ftr_grid", &p, b, None, None)
}
fn radial_scale_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let done = completion(ctx);
if done <= 0.0 {
return Some(b);
}
let c = b.to_px(ctx.params.v2("center"));
let maxd = max_corner_dist(&b, c, f64::hypot);
let mut p = Params::default();
p.u[0][0] = ctx.params.b("reverse") as u32;
p.f[0] = f4([c.0, c.1, maxd, done]);
run(e, "ftr_radial_scale", &p, b, None, None)
}
fn scale_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let stretch = ctx.params.f("stretch").max(0.0);
if stretch <= 0.0 {
return Some(b);
}
let c = b.to_px(ctx.params.v2("center"));
let a = ctx.params.f("direction").to_radians();
let mut p = Params::default();
p.f[0] = f4([c.0, c.1, a.sin(), -a.cos()]);
p.f[1][0] = stretch as f32;
run(e, "ftr_scale", &p, b, None, None)
}
// ---------------------------------------------------------------- CC Glass Wipe / Image Wipe / WarpoMatic (transition2.rs)
fn glass_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let c = completion(ctx) as f32;
if c <= 0.0 {
return Some(b);
}
let pr = ctx.params;
let gimg = layer_or_self(e, ctx, &b, "gradientLayer")?;
let mut g = plane(e, &gimg, 4);
let sm = pr.f("softness") * b.scale * 0.2;
if sm > 0.05 {
g = gaussian_blur(e, &g, sm, sm, true);
}
let soft = (pr.f("softness") / 100.0) as f32;
let rev = fitted(e, ctx, &b, "layerToReveal", true, false)?;
let mut p = Params::default();
let data = rev.as_ref().map(|r| rows(e, &mut p, r));
p.u[0][0] = rev.is_some() as u32;
p.f[0] = [c, soft * 0.5 + 0.01, (pr.f("displacementAmount") * b.scale * 4.0) as f32, 0.0];
run(e, "ftr_glass", &p, b, Some(&g), data.as_ref())
}
fn image_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let c = completion(ctx) as f32;
if c <= 0.0 {
return Some(b);
}
let pr = ctx.params;
let gimg = layer_or_self(e, ctx, &b, "layer")?;
let mut g = plane(e, &gimg, prop_src(pr.e("property")));
let bl = pr.f("blur") * b.scale * 0.5;
if bl > 0.05 {
g = gaussian_blur(e, &g, bl, bl, true);
}
let mut w = (pr.f("borderSoftness") / 100.0) as f32 * 0.5;
if pr.b("autoSoftness") {
w = w.max(0.02);
}
let mut p = Params::default();
p.u[0][0] = pr.b("inverseGradient") as u32;
p.f[0] = [c, w, 0.0, 0.0];
run(e, "ftr_image_wipe", &p, b, Some(&g), None)
}
fn warpomatic(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let c = completion(ctx) as f32;
if c <= 0.0 {
return Some(b);
}
let pr = ctx.params;
let rimg = layer_or_self(e, ctx, &b, "reactorLayer")?;
let mut m = plane(e, &rimg, 4);
let sm = pr.f("smoothness") * b.scale * 0.5;
if sm > 0.05 {
m = gaussian_blur(e, &m, sm, sm, true);
}
m = match pr.e("reactor") {
1 => {
let mean = gaussian_blur(e, &m, 8.0 * b.scale, 8.0 * b.scale, true);
kernel(e, "ftr_react_contrast", &Params::default(), &m, Some(&mean), None)
}
2 => kernel(e, "ftr_react_local", &Params::default(), &m, Some(&m), None),
_ => m,
};
let rev = fitted(e, ctx, &b, "layerToReveal", true, false)?;
let mut p = Params::default();
let data = rev.as_ref().map(|r| rows(e, &mut p, r));
p.u[0] = [pr.e("warpDirection"), rev.is_some() as u32, 0, 0];
p.f[0] = [c, (pr.f("blendSpan") / 100.0) as f32 * 0.5, (pr.f("warpAmount") * b.scale) as f32 * 20.0, 0.0];
run(e, "ftr_warpo", &p, b, Some(&m), data.as_ref())
}
// ---------------------------------------------------------------- CC Jaws / Line Sweep / Twister (transition2.rs)
fn jaws(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let c = completion(ctx);
if c <= 0.0 {
return Some(b);
}
let pr = ctx.params;
let (cx, cy) = b.to_px(pr.v2("center"));
let (sn, cs) = pr.f("direction").to_radians().sin_cos();
let width = (pr.f("width") * b.scale).max(1.0);
let amp = pr.f("height") / 100.0 * width * 0.5;
let (bw, bh) = (b.img.width as f64, b.img.height as f64);
let vmax = [(0.0, 0.0), (bw, 0.0), (0.0, bh), (bw, bh)].iter().map(|&(x, y)| (-(x - cx) * sn + (y - cy) * cs).abs()).fold(0.0, f64::max);
let d = c * (vmax + 2.0 * amp + 2.0);
let mut p = Params::default();
p.u[0][0] = pr.e("shape");
p.f[0] = f4([cx, cy, sn, cs]);
p.f[1] = f4([width, amp, d, 0.0]);
run(e, "ftr_jaws", &p, b, None, None)
}
fn line_sweep(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let c = completion(ctx);
if c <= 0.0 {
return Some(b);
}
let pr = ctx.params;
let (sn, cs) = pr.f("direction").to_radians().sin_cos();
let thick = (pr.f("thickness") * b.scale).max(1.0);
let slant = pr.f("slant") * b.scale;
let (w, h) = (b.img.width as f64, b.img.height as f64);
let (mut u0, mut u1, mut v0, mut v1) = (f64::MAX, f64::MIN, f64::MAX, f64::MIN);
for (x, y) in [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)] {
let (u, v) = (x * cs + y * sn, -x * sn + y * cs);
u0 = u0.min(u);
u1 = u1.max(u);
v0 = v0.min(v);
v1 = v1.max(v);
}
let nstr = ((v1 - v0) / thick).ceil().max(1.0);
let total = (u1 - u0) + slant.abs() * nstr + 1.0;
let mut p = Params::default();
p.u[0] = [pr.b("flipDirection") as u32, (slant >= 0.0) as u32, 0, 0];
p.f[0] = f4([sn, cs, v0, u0]);
p.f[1] = f4([thick, slant.abs(), nstr, total]);
p.f[2][0] = c as f32;
run(e, "ftr_line_sweep", &p, b, None, None)
}
fn twister(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let c = completion(ctx);
if c <= 0.0 {
return Some(b);
}
let pr = ctx.params;
let (cx, cy) = b.to_px(pr.v2("center"));
let (sn, cs) = pr.f("axis").to_radians().sin_cos();
let half = (b.img.width as f64).hypot(b.img.height as f64) * 0.5;
let back = fitted(e, ctx, &b, "backside", true, false)?;
let mut p = Params::default();
p.u[0] = [back.is_some() as u32, pr.b("shading") as u32, 0, 0];
p.f[0] = f4([cx, cy, sn, cs]);
p.f[1] = f4([half, c, 0.0, 0.0]);
run(e, "ftr_twister", &p, b, back.as_ref(), None)
}
// ---------------------------------------------------------------- Radial Shadow / CC Cylinder / CC Sphere / CC Spotlight (perspective.rs)
fn radial_shadow(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let color = pr.color("color");
let soft = pr.f("softness").max(0.0) * b.scale;
let k = 1.0 + pr.f("projectionDistance").max(0.0) / 100.0;
let lp0 = pr.v2("lightSource");
if pr.b("resizeLayer") && !ctx.adjustment {
let l = b.to_px(lp0);
let (bw, bh) = (b.img.width as f64, b.img.height as f64);
let grow = [(0.0, 0.0), (bw, 0.0), (0.0, bh), (bw, bh)].iter().map(|(cx, cy)| ((cx - l.0).hypot(cy - l.1)) * (k - 1.0)).fold(0.0, f64::max);
b.pad(e, (grow + soft * 1.5).ceil() as u32 + 1)?;
}
let l = b.to_px(lp0);
let mut p = Params::default();
p.u[0][0] = (pr.e("render") == 1) as u32;
p.f[0] = [l.0 as f32, l.1 as f32, k as f32, pr.f("opacity") as f32 / 100.0 * color[3]];
p.f[1] = rgb(color, pr.f("colorInfluence") as f32 / 100.0);
let mut sh = kernel(e, "ftr_rshadow", &p, &b.img, None, None);
if soft > 0.0 {
sh = gaussian_blur(e, &sh, soft * 0.5, soft * 0.5, false);
}
if !pr.b("shadowOnly") {
sh = kernel(e, "ftr_under", &Params::default(), &sh, Some(&b.img), None);
}
b.img = sh;
Some(b)
}
fn norm3(v: [f64; 3]) -> [f64; 3] {
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-12);
[v[0] / l, v[1] / l, v[2] / l]
}
/// perspective::Shading into f[3..7]; the layer rectangle into f[1].
fn shading(ctx: &EffectCtx, b: &GBuf, p: &mut Params) {
let pr = ctx.params;
let (a, el) = (pr.f("lightDirection").to_radians(), pr.f("lightHeight").to_radians());
let l = norm3([a.sin() * el.cos(), -a.cos() * el.cos(), el.sin()]);
let hv = norm3([l[0], l[1], l[2] + 1.0]);
p.f[1] = f4([b.offset[0], b.offset[1], ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale]);
p.f[3] = [l[0] as f32, l[1] as f32, l[2] as f32, pr.f("lightIntensity") as f32 / 100.0];
p.f[4] = rgb(pr.color("lightColor"), pr.f("ambient") as f32 / 100.0);
p.f[5] = [pr.f("diffuse") as f32 / 100.0, pr.f("specular") as f32 / 100.0, 1.0 / (pr.f("roughness") as f32).max(0.001), 0.0];
p.f[6] = [hv[0] as f32, hv[1] as f32, hv[2] as f32, 0.0];
}
fn cylinder(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let (w, h) = (ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
let r = (pr.f("radius") / 100.0 * w / std::f64::consts::TAU).max(0.5);
let c = b.to_px(pr.v2("position"));
let mut p = Params::default();
shading(ctx, &b, &mut p);
p.u[0][0] = pr.e("render");
p.f[0] = f4([c.0, c.1, r, h]);
p.f[2][0] = pr.f("rotation").to_radians() as f32;
let (rx, ry, rz) = (pr.f("rotationX"), pr.f("rotationY"), pr.f("rotationZ"));
p.u[0][1] = (rx != 0.0 || ry != 0.0 || rz != 0.0) as u32;
let m = rot_xyz(rx, ry, rz);
// Matrix rows, as for CC Sphere; the kernel uses Rᵀ for mapping.
for (i, row) in m.iter().enumerate() {
p.f[7 + i] = f4([row[0], row[1], row[2], 0.0]);
}
run(e, "ftr_cylinder", &p, b, None, None)
}
/// perspective::rot_xyz (R = Rz · Ry · Rx).
fn rot_xyz(rx: f64, ry: f64, rz: f64) -> [[f64; 3]; 3] {
let (sx, cx) = rx.to_radians().sin_cos();
let (sy, cy) = ry.to_radians().sin_cos();
let (sz, cz) = rz.to_radians().sin_cos();
let mx = [[1.0, 0.0, 0.0], [0.0, cx, -sx], [0.0, sx, cx]];
let my = [[cy, 0.0, sy], [0.0, 1.0, 0.0], [-sy, 0.0, cy]];
let mz = [[cz, -sz, 0.0], [sz, cz, 0.0], [0.0, 0.0, 1.0]];
let mul = |a: [[f64; 3]; 3], b: [[f64; 3]; 3]| {
let mut o = [[0.0; 3]; 3];
for i in 0..3 {
for j in 0..3 {
o[i][j] = (0..3).map(|k| a[i][k] * b[k][j]).sum();
}
}
o
};
mul(mz, mul(my, mx))
}
fn sphere(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let r = (pr.f("radius") * b.scale).max(0.5);
let c = b.to_px(pr.v2("offset"));
let m = rot_xyz(pr.f("rotationX"), pr.f("rotationY"), pr.f("rotationZ"));
let mut p = Params::default();
shading(ctx, &b, &mut p);
p.u[0][0] = pr.e("render");
p.f[0] = f4([c.0, c.1, r, 0.0]);
// Rows of M: the kernel computes Mᵀ · q as q.x·row0 + q.y·row1 + q.z·row2.
for (i, row) in m.iter().enumerate() {
p.f[7 + i] = f4([row[0], row[1], row[2], 0.0]);
}
run(e, "ftr_sphere", &p, b, None, None)
}
fn spotlight(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let (w, h) = (ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
let from = b.to_px(pr.v2("from"));
let to = b.to_px(pr.v2("to"));
let height = (pr.f("height") / 100.0 * w.max(h)).max(1e-3);
let cone = pr.f("coneAngle").clamp(0.1, 89.9).to_radians();
let soft = (pr.f("edgeSoftness") / 100.0).clamp(0.0, 1.0);
let axis = norm3([to.0 - from.0, to.1 - from.1, -height]);
let mut p = Params::default();
p.u[0][0] = (pr.e("render") == 1) as u32;
p.f[0] = [from.0 as f32, from.1 as f32, height as f32, pr.f("intensity") as f32 / 100.0];
p.f[1] = f4([axis[0], axis[1], axis[2], cone * (1.0 - soft)]);
p.f[2] = rgb(pr.color("color"), cone as f32);
run(e, "ftr_spot", &p, b, None, None)
}
// ---------------------------------------------------------------- CC Environment / 3D Glasses (perspective2.rs)
fn environment(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let (mut env, es, eo, esz) = match ctx.layer_param("environment", true) {
Some(o) => (e.g.upload_image(&o.buf.img)?, o.buf.scale, o.buf.offset, o.size),
None => (b.img.clone(), b.scale, b.offset, ctx.layer_size),
};
if pr.b("filterEnvironment") {
env = gaussian_blur(e, &env, 2.0 * es, 2.0 * es, true);
}
let relief = plane(e, &b.img, 11);
let soft = 1.5 * b.scale.max(0.25);
let hmap = gaussian_blur(e, &relief, soft, soft, true);
let mut p = Params::default();
let data = rows(e, &mut p, &env);
p.u[0][0] = pr.e("mapping");
p.f[0] = f4([1.0 / b.scale.max(1e-6), pr.f("height") / 100.0 * 20.0, es, 0.0]);
p.f[1] = f4([eo[0], eo[1], esz[0], esz[1]]);
run(e, "ftr_env", &p, b, Some(&hmap), Some(&data))
}
fn glasses(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let mut l = fitted(e, ctx, &b, "leftView", true, false)?.unwrap_or_else(|| b.img.clone());
let mut r = fitted(e, ctx, &b, "rightView", true, false)?.unwrap_or_else(|| b.img.clone());
if pr.b("leftRightSwap") {
std::mem::swap(&mut l, &mut r);
}
let unit = if pr.e("units") == 1 { ctx.layer_size[0] / 100.0 } else { 1.0 } * b.scale;
let conv = pr.f("sceneConvergence") * unit;
let va = pr.f("verticalAlignment") * unit;
let mut shift = |img: GpuImage, dx: f64, dy: f64| {
if dx == 0.0 && dy == 0.0 {
return img;
}
let mut p = Params::default();
p.f[0] = f4([dx, dy, 0.0, 0.0]);
kernel(e, "ftr_shift", &p, &img, None, None)
};
let l = shift(l, conv * 0.5, 0.0);
let r = shift(r, -conv * 0.5, va);
let mut p = Params::default();
p.u[0][0] = pr.e("view3d");
p.f[0] = [b.img.width as f32, b.img.height as f32, (pr.f("balance") / 20.0).clamp(0.0, 1.0) as f32, 0.0];
let out = kernel(e, "ftr_glasses", &p, &l, Some(&r), None);
Some(GBuf { img: out, ..b })
}
// ---------------------------------------------------------------- CC Bender / CC Blobbylize (distort3.rs)
fn bender(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let amount = pr.f("amount") / 100.0;
if amount == 0.0 {
return Some(b);
}
let top = b.to_px(pr.v2("top"));
let base = b.to_px(pr.v2("base"));
let (ax, ay) = (top.0 - base.0, top.1 - base.1);
let len = (ax * ax + ay * ay).sqrt().max(1e-6);
let (ux, uy) = (ax / len, ay / len);
let reference = if pr.b("adjustToDistance") { len } else { ctx.layer_size[1] * b.scale };
let mut p = Params::default();
p.u[0][0] = pr.e("style");
p.f[0] = f4([base.0, base.1, ux, uy]);
p.f[1] = f4([len, -uy, ux, amount * reference]);
run(e, "ftr_bender", &p, b, None, None)
}
fn blobbylize(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let soft = pr.f("softness").max(0.0) * b.scale;
let blob = layer_or_self(e, ctx, &b, "blobLayer")?;
let raw = plane(e, &blob, pr.e("property").min(10));
let h = gaussian_blur(e, &raw, soft, soft, true);
let lpos = b.to_px(pr.v2("lightPosition"));
let lheight = pr.f("lightHeight");
let (d, el) = (pr.f("lightDirection").to_radians(), (lheight / 100.0).clamp(0.0, 1.0) * std::f64::consts::FRAC_PI_2);
let ldir = [d.sin() * el.cos(), -d.cos() * el.cos(), el.sin()];
let span = (b.img.width.max(b.img.height) as f64).max(1.0);
let mut p = Params::default();
p.f[0] = f4([(soft * 0.5).max(1.0), (pr.f("cutAway") / 100.0).clamp(0.0, 0.99), pr.f("lightIntensity") / 100.0, pr.f("ambient") / 100.0]);
p.f[1] = f4([pr.f("diffuse") / 100.0, pr.f("specular") / 100.0, (1.0 / pr.f("roughness").clamp(0.001, 1.0)).min(500.0), pr.f("metal") / 100.0]);
p.f[2] = f4([lpos.0, lpos.1, lheight / 100.0 * span, (pr.e("lightType") == 1) as u32 as f64]);
p.f[3] = f4([ldir[0], ldir[1], ldir[2], 0.0]);
p.f[4] = rgb(pr.color("lightColor"), 0.0);
run(e, "ftr_blob", &p, b, Some(&h), None)
}