mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-29 04:26:55 -06:00
Use the measured Softness kernel (sigma = 0.42 * Softness), threshold the blurred height with a ~1 px antialias, and allow Cut Away 100 to empty the matte. Split the source-matte displacement from the height-field RGB refraction: the matte uses a z=1 bevel normal fitted to the T2 step edges, while RGB follows the measured z=height refraction. Align the documented AE defaults and add CPU regression tests for the cut threshold, Cut Away 100, and the inward matte warp. Update the GPU kernel and parameters to match, keeping CPU/GPU parity.
605 lines
24 KiB
Rust
605 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 * 0.42, soft * 0.42, 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([(pr.f("cutAway") / 100.0).clamp(0.0, 1.0), pr.f("lightIntensity") / 100.0, pr.f("ambient") / 100.0, soft * 1.5]);
|
|
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);
|
|
p.f[5] = f4([if soft > 0.05 { span / 64.0 } else { 0.0 }, span * 0.34 * b.scale, (soft * 0.65).max(1.0), 1.25]);
|
|
p.f[6] = f4([span * 0.5, 0.0, 0.0, 0.0]);
|
|
run(e, "ftr_blob", &p, b, Some(&h), None)
|
|
}
|