diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index feed53bf..52f19e40 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -931,6 +931,11 @@ pub const GPU_EFFECTS: &[&str] = &[ "ec.perspective.bevelalpha", "ec.perspective.beveledges", "ec.obsolete.gaussianlegacy", + // effectcraft-gpu fx_light (the CC light family) + "ec.generate.cclightrays", + "ec.generate.cclightburst", + "ec.generate.cclightsweep", + "ec.transition.cclightwipe", ]; /// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated diff --git a/crates/gpu/src/context.rs b/crates/gpu/src/context.rs index 3d8d2916..4862d7b2 100644 --- a/crates/gpu/src/context.rs +++ b/crates/gpu/src/context.rs @@ -314,6 +314,7 @@ impl GpuContext { include_str!("shaders/fx_tone.wgsl"), include_str!("shaders/fx_warp.wgsl"), include_str!("shaders/fx_extra.wgsl"), + include_str!("shaders/fx_light.wgsl"), ] .concat(); let module = @@ -373,6 +374,7 @@ impl GpuContext { .chain(crate::fx_tone::KERNELS) .chain(crate::fx_warp::KERNELS) .chain(crate::fx_extra::KERNELS) + .chain(crate::fx_light::KERNELS) .map(|e| (e, &layout)) .chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext))) .map(|(e, layout)| { diff --git a/crates/gpu/src/effects.rs b/crates/gpu/src/effects.rs index d8ab4746..c72f9470 100644 --- a/crates/gpu/src/effects.rs +++ b/crates/gpu/src/effects.rs @@ -133,6 +133,7 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { _ if crate::fx_tone::IDS.contains(&id) => crate::fx_tone::apply(e, id, ctx, b), _ if crate::fx_warp::IDS.contains(&id) => crate::fx_warp::apply(e, id, ctx, b), _ if crate::fx_extra::IDS.contains(&id) => crate::fx_extra::apply(e, id, ctx, b), + _ if crate::fx_light::IDS.contains(&id) => crate::fx_light::apply(e, id, ctx, b), _ => pointwise(e, id, ctx, b), } } diff --git a/crates/gpu/src/fx_light.rs b/crates/gpu/src/fx_light.rs new file mode 100644 index 00000000..6d96d0e7 --- /dev/null +++ b/crates/gpu/src/fx_light.rs @@ -0,0 +1,126 @@ +//! GPU effects, the CC light family (kernels in `shaders/fx_light.wgsl`, every entry point +//! prefixed `flt_`): CC Light Rays, CC Light Burst 2.5, CC Light Sweep and CC Light Wipe, with +//! the CPU effects' exact steps. Centres, radii, the wipe's corner distance and ramp are +//! computed on the CPU in f64 as the CPU effect does; the kernels work relative to the centre. +//! Light Sweep's softened alpha plane is the CPU's `gauss_plane` (box passes, edges repeated). + +use effectcraft_effects::EffectCtx; + +use crate::context::{Enc, GpuImage, Params}; +use crate::effects::{GBuf, gaussian_blur}; + +/// Compute entry points in `fx_light.wgsl`. +pub(crate) const KERNELS: &[&str] = &["flt_rays", "flt_burst", "flt_sweep", "flt_wipe"]; + +/// Effect ids implemented here. +pub(crate) const IDS: &[&str] = &["ec.generate.cclightrays", "ec.generate.cclightburst", "ec.generate.cclightsweep", "ec.transition.cclightwipe"]; + +/// 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 { + match id { + "ec.generate.cclightrays" => light_rays(e, ctx, b), + "ec.generate.cclightburst" => light_burst(e, ctx, b), + "ec.generate.cclightsweep" => light_sweep(e, ctx, b), + "ec.transition.cclightwipe" => light_wipe(e, ctx, b), + _ => None, + } +} + +/// A same-size per-pixel kernel over the buffer. +fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, aux: Option<&GpuImage>) -> Option { + let out = e.scratch(b.img.width, b.img.height); + e.pixels(entry, p, &b.img, aux, &out, None); + b.img = out; + Some(b) +} + +fn rgb(c: [f32; 4]) -> [f32; 4] { + [c[0], c[1], c[2], 0.0] +} + +// ---------------------------------------------------------------- CC Light Rays / Burst (generate2.rs) + +fn light_rays(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let c = b.to_px(pr.v2("center")); + let k = (pr.f("intensity") / 100.0 * 0.5).clamp(0.0, 0.95); + let gain = (pr.f("intensity") / 100.0 * 2.0) as f32; + let rad = (pr.f("radius") * b.scale).max(0.5); + let ws = (pr.f("warpSoftness") / 50.0).max(0.0); + let mut p = Params::default(); + p.u[0] = [(pr.e("shape") == 1) as u32, pr.b("colorFromSource") as u32, pr.e("transferMode"), 0]; + p.f[0] = [c.0 as f32, c.1 as f32, k as f32, gain]; + p.f[1] = [rad as f32, (rad * (1.0 + ws)) as f32 + 1e-3, 0.0, 0.0]; + p.f[2] = rgb(pr.color("color")); + run(e, "flt_rays", &p, b, None) +} + +fn light_burst(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let c = b.to_px(pr.v2("center")); + let inten = (pr.f("intensity") / 100.0) as f32; + let k = (pr.f("rayLength") / 100.0).clamp(0.0, 1.0); + let set_color = pr.b("setColor"); + if k <= 0.0 && (inten - 1.0).abs() < 1e-6 && !set_color { + return Some(b); + } + let mut p = Params::default(); + p.u[0] = [pr.e("burst"), set_color as u32, 0, 0]; + p.f[0] = [c.0 as f32, c.1 as f32, k as f32, inten]; + p.f[1] = rgb(pr.color("color")); + run(e, "flt_burst", &p, b, None) +} + +// ---------------------------------------------------------------- CC Light Sweep (generate2.rs) + +fn light_sweep(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let c = b.to_px(pr.v2("center")); + let dir = pr.f("direction").to_radians(); + let half = (pr.f("width") * b.scale * 0.5).max(0.5); + let edge_t = pr.f("edgeThickness") * b.scale; + // The alpha plane (x), softened by util::gauss_plane (edges repeated) when thick enough. + let alpha = e.scratch(b.img.width, b.img.height); + e.pixels("fxe_alpha", &Params::default(), &b.img, None, &alpha, None); + let soft = if edge_t > 0.05 { gaussian_blur(e, &alpha, edge_t * 0.5, edge_t * 0.5, true) } else { alpha }; + let mut p = Params::default(); + p.u[0] = [pr.e("shape"), pr.e("lightReceptionMode"), 0, 0]; + p.f[0] = [c.0 as f32, c.1 as f32, dir.cos() as f32, dir.sin() as f32]; + p.f[1] = [half as f32, (pr.f("sweepIntensity") / 100.0) as f32, (pr.f("edgeIntensity") / 100.0) as f32, 0.0]; + p.f[2] = rgb(pr.color("lightColor")); + run(e, "flt_sweep", &p, b, Some(&soft)) +} + +// ---------------------------------------------------------------- CC Light Wipe (transition.rs) + +fn light_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let done = (pr.f("completion") / 100.0).clamp(0.0, 1.0); + if done <= 0.0 { + return Some(b); + } + let c = b.to_px(pr.v2("center")); + let shape = pr.e("shape"); + let (s, co) = pr.f("direction").to_radians().sin_cos(); + let reverse = pr.b("reverse"); + let metric = |dx: f64, dy: f64| { + let (qx, qy) = (dx * co + dy * s, -dx * s + dy * co); + match shape { + 0 => qx.abs(), + 2 => qx.abs().max(qy.abs()), + _ => qx.hypot(qy), + } + }; + // transition::max_corner_dist. + let (w, h) = (b.img.width as f64, b.img.height as f64); + let maxd = [(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); + let edge = if reverse { (1.0 - done) * maxd } else { done * maxd }; + let band = (maxd * 0.06).max(2.0); + let ramp = (done * 20.0).min(1.0) as f32 * ((1.0 - done) * 20.0).min(1.0) as f32; + let mut p = Params::default(); + p.u[0] = [shape, pr.b("colorFromSource") as u32, reverse as u32, 0]; + p.f[0] = [c.0 as f32, c.1 as f32, s as f32, co as f32]; + p.f[1] = [edge as f32, band as f32, pr.f("intensity") as f32 / 100.0 * ramp, 0.0]; + p.f[2] = rgb(pr.color("color")); + run(e, "flt_wipe", &p, b, None) +} diff --git a/crates/gpu/src/lib.rs b/crates/gpu/src/lib.rs index ca9bdbfb..a923b893 100644 --- a/crates/gpu/src/lib.rs +++ b/crates/gpu/src/lib.rs @@ -52,6 +52,7 @@ mod fx_distort; mod fx_extra; mod fx_generate; mod fx_key; +mod fx_light; mod fx_noise; mod fx_stylize; mod fx_tone; @@ -209,6 +210,8 @@ mod tests_fx_generate; #[cfg(test)] mod tests_fx_key; #[cfg(test)] +mod tests_fx_light; +#[cfg(test)] mod tests_fx_noise; #[cfg(test)] mod tests_fx_stylize; diff --git a/crates/gpu/src/shaders/fx_light.wgsl b/crates/gpu/src/shaders/fx_light.wgsl new file mode 100644 index 00000000..d957ab39 --- /dev/null +++ b/crates/gpu/src/shaders/fx_light.wgsl @@ -0,0 +1,231 @@ +// GPU effects, the CC light family: see src/fx_light.rs. CC Light Rays, CC Light Burst 2.5, +// CC Light Sweep and CC Light Wipe, each mirroring the CPU effect operation for operation. +// Coordinates are pixel centres relative to the effect's centre, so f32 keeps its precision. + +fn flt_inside(p: vec2) -> bool { + let d = out_dims(); + return p.x < d.x && p.y < d.y; +} + +fn flt_rel(p: vec2, c: vec2) -> vec2 { + return vec2(f32(p.x) + 0.5, f32(p.y) + 0.5) - c; +} + +// util::smoothstep. +fn flt_smoothstep(e0: f32, e1: f32, x: f32) -> f32 { + if (abs(e1 - e0) < 1e-9) { + return select(1.0, 0.0, x < e0); + } + let t = clamp((x - e0) / (e1 - e0), 0.0, 1.0); + return t * t * (3.0 - 2.0 * t); +} + +// util::unpremul's colour. +fn flt_unpremul(p: vec4) -> vec3 { + if (p.w > 1e-6) { + return p.xyz / p.w; + } + return vec3(0.0); +} + +// ---------------------------------------------------------------- CC Light Rays (generate2::light_rays) +// u[0] = (square, colour from source, transfer mode); f[0] = (cx, cy, k, gain); +// f[1] = (radius, outer radius + 1e-3); f[2] = colour. + +fn flt_ray_mask(q: vec2) -> f32 { + let d = abs(q); + var r = length(d); + if (P.u[0].x != 0u) { + r = max(d.x, d.y); + } + return 1.0 - flt_smoothstep(P.f[1].x, P.f[1].y, r); +} + +@compute @workgroup_size(16, 16) +fn flt_rays(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!flt_inside(p)) { + return; + } + let c = P.f[0].xy; + let rel = flt_rel(p, c); + var acc = vec4(0.0); + for (var i = 0u; i < 32u; i++) { + let s = 1.0 - P.f[0].z * f32(i) / 32.0; + let q = rel * s; + let m = flt_ray_mask(q); + if (m <= 0.0) { + continue; + } + acc += sample_bilinear(src, c.x + q.x, c.y + q.y) * m; + } + var ray = acc / 32.0 * P.f[0].w; + if (P.u[0].y == 0u) { + let l = luminance(flt_unpremul(ray)) * ray.w; + ray = vec4(P.f[2].xyz * l, ray.w); + } + let la = min(ray.w, 1.0); + let px = textureLoad(src, p, 0); + var o: vec4; + switch P.u[0].z { + case 1u: { + o = vec4(max(px.xyz, ray.xyz), max(px.w, la)); + } + case 2u: { + let r4 = vec4(ray.xyz, la); + o = px + r4 - px * r4; + } + case 3u: { + o = vec4(ray.xyz, la); + } + default: { + let a = clamp(la, 0.0, 1.0); + o = vec4(px.xyz + ray.xyz, clamp(px.w + a * (1.0 - px.w), 0.0, 1.0)); + } + } + textureStore(out, p, o); +} + +// ---------------------------------------------------------------- CC Light Burst 2.5 (generate2::light_burst) +// u[0] = (burst, set colour); f[0] = (cx, cy, ray length k, intensity); f[1] = colour. + +@compute @workgroup_size(16, 16) +fn flt_burst(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!flt_inside(p)) { + return; + } + let c = P.f[0].xy; + let rel = flt_rel(p, c); + let mode = P.u[0].x; + var acc = vec4(0.0); + var wsum = 0.0; + var best = vec4(0.0); + var best_l = -1.0; + for (var i = 0u; i < 32u; i++) { + let f = f32(i) / 32.0; + let s = 1.0 - P.f[0].z * f; + let q = rel * s; + let v = sample_bilinear(src, c.x + q.x, c.y + q.y); + if (mode == 0u) { + let l = luminance(v.xyz) + v.w * 1e-3; + if (l > best_l) { + best_l = l; + best = v; + } + } else if (mode == 1u) { + let w = 1.0 - f; + acc += v * w; + wsum += w; + } else { + acc += v; + wsum += 1.0; + } + } + var o = best; + if (mode != 0u) { + o = acc / max(wsum, 1e-6); + } + if (P.u[0].y != 0u) { + let a = max(o.w, 0.0); + let l = luminance(flt_unpremul(o)) * a; + o = vec4(P.f[1].xyz * l, a); + } + let k = P.f[0].w; + textureStore(out, p, vec4(o.xyz * k, clamp(o.w * k, 0.0, 1.0))); +} + +// ---------------------------------------------------------------- CC Light Sweep (generate2::light_sweep) +// aux = the (softened) alpha plane in x. u[0] = (shape, reception mode); f[0] = (cx, cy, nx, ny); +// f[1] = (half width, sweep, edge intensity); f[2] = light colour. + +@compute @workgroup_size(16, 16) +fn flt_sweep(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!flt_inside(p)) { + return; + } + let px = textureLoad(src, p, 0); + let rel = flt_rel(p, P.f[0].xy); + let d = abs(rel.x * P.f[0].z + rel.y * P.f[0].w); + let half = P.f[1].x; + let mode = P.u[0].y; + var band: f32; + switch P.u[0].x { + case 0u: { + band = max(1.0 - d / half, 0.0); + } + case 2u: { + band = clamp(half - d + 0.5, 0.0, 1.0); + } + default: { + band = flt_smoothstep(half, 0.0, d); + } + } + if (band <= 0.0) { + textureStore(out, p, select(px, vec4(0.0), mode == 2u)); + return; + } + let gx = tex_get_clamped(aux, p.x + 1, p.y).x - tex_get_clamped(aux, p.x - 1, p.y).x; + let gy = tex_get_clamped(aux, p.x, p.y + 1).x - tex_get_clamped(aux, p.x, p.y - 1).x; + let edge = min(sqrt(gx * gx + gy * gy) * 2.0, 1.0); + let light = band * P.f[1].y + band * edge * P.f[1].z; + let lc = P.f[2].xyz; + let a = px.w; + var o: vec4; + if (mode == 1u) { + let la = min(band * P.f[1].y, 1.0); + o = vec4(px.xyz + lc * light, min(a + la * (1.0 - a), 1.0)); + } else if (mode == 2u) { + let la = min(light * a, 1.0); + o = vec4(lc * la, la); + } else { + o = vec4(px.xyz + lc * light * a, a); + } + textureStore(out, p, o); +} + +// ---------------------------------------------------------------- CC Light Wipe (transition::light_wipe) +// u[0] = (shape, colour from source, reverse); f[0] = (cx, cy, sin, cos); +// f[1] = (edge, band, intensity × ramp); f[2] = colour. + +@compute @workgroup_size(16, 16) +fn flt_wipe(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!flt_inside(p)) { + return; + } + var px = textureLoad(src, p, 0); + let d = flt_rel(p, P.f[0].xy); + let s = P.f[0].z; + let co = P.f[0].w; + let q = vec2(d.x * co + d.y * s, -d.x * s + d.y * co); + var m: f32; + switch P.u[0].x { + case 0u: { + m = abs(q.x); + } + case 2u: { + m = max(abs(q.x), abs(q.y)); + } + default: { + m = length(q); + } + } + let edge = P.f[1].x; + let visible = select(m > edge, m < edge, P.u[0].z != 0u); + if (!visible) { + textureStore(out, p, vec4(0.0)); + return; + } + let z = (m - edge) / P.f[1].y; + let g = exp(-(z * z)) * P.f[1].z; + if (g > 0.0) { + var lc = P.f[2].xyz; + if (P.u[0].y != 0u) { + lc = flt_unpremul(px); + } + px = vec4(px.xyz + lc * g * px.w, px.w); + } + textureStore(out, p, px); +} diff --git a/crates/gpu/src/tests_fx_light.rs b/crates/gpu/src/tests_fx_light.rs new file mode 100644 index 00000000..152f0e5a --- /dev/null +++ b/crates/gpu/src/tests_fx_light.rs @@ -0,0 +1,56 @@ +//! GPU CC light effects (CC Light Rays, CC Light Burst 2.5, CC Light Sweep, CC Light Wipe) vs +//! the CPU effects (the oracle): direct on a buffer at full and half resolution, as +//! adjustment, and composited at 8 and 32 bpc. + +use effectcraft_keyframe::Value; + +use crate::tests::{c, effect_case, n}; + +fn e(v: u32) -> Value { + Value::Enum(v) +} + +fn on() -> Value { + Value::Bool(true) +} + +fn pt(x: f64, y: f64) -> Value { + Value::Vec2([x, y]) +} + +#[test] +fn light_rays() { + effect_case("ec.generate.cclightrays", &[("center", pt(30.0, 20.0))]); + effect_case( + "ec.generate.cclightrays", + &[("center", pt(40.0, 18.0)), ("intensity", n(250.0)), ("radius", n(15.0)), ("shape", e(1)), ("transferMode", e(1))], + ); + effect_case( + "ec.generate.cclightrays", + &[("intensity", n(80.0)), ("colorFromSource", Value::Bool(false)), ("color", c(1.0, 0.6, 0.2)), ("transferMode", e(2)), ("warpSoftness", n(120.0))], + ); + effect_case("ec.generate.cclightrays", &[("intensity", n(150.0)), ("transferMode", e(3)), ("radius", n(25.0))]); +} + +#[test] +fn light_burst() { + effect_case("ec.generate.cclightburst", &[("center", pt(30.0, 20.0)), ("burst", e(1))]); + effect_case("ec.generate.cclightburst", &[("rayLength", n(80.0)), ("burst", e(2)), ("intensity", n(150.0))]); + effect_case("ec.generate.cclightburst", &[("rayLength", n(30.0)), ("setColor", on()), ("color", c(0.3, 0.7, 1.0)), ("burst", e(1))]); + effect_case("ec.generate.cclightburst", &[("rayLength", n(40.0))]); +} + +#[test] +fn light_sweep() { + effect_case("ec.generate.cclightsweep", &[("center", pt(30.0, 20.0))]); + effect_case("ec.generate.cclightsweep", &[("shape", e(0)), ("width", n(30.0)), ("edgeThickness", n(4.0)), ("lightReceptionMode", e(1))]); + effect_case("ec.generate.cclightsweep", &[("shape", e(2)), ("direction", n(60.0)), ("lightReceptionMode", e(2)), ("lightColor", c(1.0, 0.8, 0.4))]); + effect_case("ec.generate.cclightsweep", &[("edgeThickness", n(0.0)), ("sweepIntensity", n(120.0))]); +} + +#[test] +fn light_wipe() { + effect_case("ec.transition.cclightwipe", &[("completion", n(40.0)), ("center", pt(30.0, 20.0))]); + effect_case("ec.transition.cclightwipe", &[("completion", n(25.0)), ("shape", e(0)), ("direction", n(30.0)), ("colorFromSource", on())]); + effect_case("ec.transition.cclightwipe", &[("completion", n(60.0)), ("shape", e(2)), ("reverse", on()), ("color", c(0.2, 0.9, 0.5))]); +} diff --git a/docs/effects.md b/docs/effects.md index 1ad51686..37a4882d 100644 --- a/docs/effects.md +++ b/docs/effects.md @@ -5,7 +5,7 @@ EffectCraft ships 306 effects with 3009 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over. -- **GPU**: 166 effects also run on the GPU compositor with identical results. +- **GPU**: 170 effects also run on the GPU compositor with identical results. - **32**: 306 effects process 32-bit float (HDR, overbright) pixels without clamping. - **Status**: 306 implemented in full, 0 partial (what is missing is listed). @@ -194,9 +194,9 @@ Effects are addressed by id (`ec..`) or display name in commands | Audio Waveform | `ec.generate.audiowaveform` | 17 | | 32 | Implemented | | Beam | `ec.generate.beam` | 11 | | 32 | Implemented | | CC Glue Gun | `ec.generate.ccgluegun` | 7 | | 32 | Implemented | -| CC Light Burst 2.5 | `ec.generate.cclightburst` | 6 | | 32 | Implemented | -| CC Light Rays | `ec.generate.cclightrays` | 8 | | 32 | Implemented | -| CC Light Sweep | `ec.generate.cclightsweep` | 9 | | 32 | Implemented | +| CC Light Burst 2.5 | `ec.generate.cclightburst` | 6 | GPU | 32 | Implemented | +| CC Light Rays | `ec.generate.cclightrays` | 8 | GPU | 32 | Implemented | +| CC Light Sweep | `ec.generate.cclightsweep` | 9 | GPU | 32 | Implemented | | CC Threads | `ec.generate.ccthreads` | 8 | | 32 | Implemented | | Cell Pattern | `ec.generate.cellpattern` | 14 | GPU | 32 | Implemented | | Checkerboard | `ec.generate.checkerboard` | 10 | GPU | 32 | Implemented | @@ -395,7 +395,7 @@ Effects are addressed by id (`ec..`) or display name in commands | CC Grid Wipe | `ec.transition.ccgridwipe` | 7 | | 32 | Implemented | | CC Image Wipe | `ec.transition.ccimagewipe` | 7 | | 32 | Implemented | | CC Jaws | `ec.transition.ccjaws` | 6 | | 32 | Implemented | -| CC Light Wipe | `ec.transition.cclightwipe` | 8 | | 32 | Implemented | +| CC Light Wipe | `ec.transition.cclightwipe` | 8 | GPU | 32 | Implemented | | CC Line Sweep | `ec.transition.cclinesweep` | 5 | | 32 | Implemented | | CC Radial ScaleWipe | `ec.transition.ccradialscalewipe` | 3 | | 32 | Implemented | | CC Scale Wipe | `ec.transition.ccscalewipe` | 3 | | 32 | Implemented |