mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
M13.13: GPU Circle, Ellipse, Iris Wipe, Bevel Alpha, Bevel Edges, Gaussian Blur (Legacy)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
ea292a6e81
commit
750868244a
9 files changed
+486
-11
No files matched your search
@@ -922,6 +922,13 @@ pub const GPU_EFFECTS: &[&str] = &[
|
||||
"ec.distort.reshape",
|
||||
"ec.stylize.coloremboss",
|
||||
"ec.stylize.cartoon",
|
||||
// effectcraft-gpu fx_extra (shapes, bevels)
|
||||
"ec.generate.circle",
|
||||
"ec.generate.ellipse",
|
||||
"ec.transition.iriswipe",
|
||||
"ec.perspective.bevelalpha",
|
||||
"ec.perspective.beveledges",
|
||||
"ec.obsolete.gaussianlegacy",
|
||||
];
|
||||
|
||||
/// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated
|
||||
|
||||
@@ -313,6 +313,7 @@ impl GpuContext {
|
||||
include_str!("shaders/fx_noise.wgsl"),
|
||||
include_str!("shaders/fx_tone.wgsl"),
|
||||
include_str!("shaders/fx_warp.wgsl"),
|
||||
include_str!("shaders/fx_extra.wgsl"),
|
||||
]
|
||||
.concat();
|
||||
let module =
|
||||
@@ -371,6 +372,7 @@ impl GpuContext {
|
||||
.chain(crate::fx_noise::KERNELS)
|
||||
.chain(crate::fx_tone::KERNELS)
|
||||
.chain(crate::fx_warp::KERNELS)
|
||||
.chain(crate::fx_extra::KERNELS)
|
||||
.map(|e| (e, &layout))
|
||||
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
|
||||
.map(|(e, layout)| {
|
||||
|
||||
@@ -132,6 +132,7 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
_ if crate::fx_noise::IDS.contains(&id) => crate::fx_noise::apply(e, id, ctx, b),
|
||||
_ 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),
|
||||
_ => pointwise(e, id, ctx, b),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
//! GPU effects, shapes and bevels (kernels in `shaders/fx_extra.wgsl`, every entry point
|
||||
//! prefixed `fxe_`): Circle, Ellipse, Iris Wipe, Bevel Alpha, Bevel Edges and Gaussian Blur
|
||||
//! (Legacy), with the CPU effects' exact steps. Radii, feathers, light directions and Iris
|
||||
//! Wipe's polygon are computed on the CPU in f64 as the CPU effect does; the kernels work
|
||||
//! relative to the shape's centre.
|
||||
|
||||
use effectcraft_effects::EffectCtx;
|
||||
|
||||
use crate::context::{Enc, Params};
|
||||
use crate::effects::{GBuf, gaussian_blur};
|
||||
|
||||
/// Compute entry points in `fx_extra.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &["fxe_circle", "fxe_ellipse", "fxe_iris", "fxe_alpha", "fxe_bevel_alpha", "fxe_bevel_edges"];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] = &[
|
||||
"ec.generate.circle",
|
||||
"ec.generate.ellipse",
|
||||
"ec.transition.iriswipe",
|
||||
"ec.perspective.bevelalpha",
|
||||
"ec.perspective.beveledges",
|
||||
"ec.obsolete.gaussianlegacy",
|
||||
];
|
||||
|
||||
/// 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.generate.circle" => circle(e, ctx, b),
|
||||
"ec.generate.ellipse" => ellipse(e, ctx, b),
|
||||
"ec.transition.iriswipe" => iris_wipe(e, ctx, b),
|
||||
"ec.perspective.bevelalpha" => bevel_alpha(e, ctx, b),
|
||||
"ec.perspective.beveledges" => bevel_edges(e, ctx, b),
|
||||
"ec.obsolete.gaussianlegacy" => gaussian_legacy(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<&crate::context::GpuImage>, data: Option<&wgpu::Buffer>) -> Option<GBuf> {
|
||||
let out = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels(entry, p, &b.img, aux, &out, data);
|
||||
b.img = out;
|
||||
Some(b)
|
||||
}
|
||||
|
||||
fn f4(v: [f64; 4]) -> [f32; 4] {
|
||||
v.map(|x| x as f32)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Circle (generate::circle)
|
||||
|
||||
fn circle(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let c = b.to_px(pr.v2("center"));
|
||||
let r = pr.f("radius") * b.scale;
|
||||
let thick = pr.f("thickness").max(0.0);
|
||||
let mut f_out = pr.f("feather/feather").max(0.0) * b.scale;
|
||||
let mut f_in = pr.f("feather/featherInner").max(0.0) * b.scale;
|
||||
let inner = match pr.e("edge") {
|
||||
1 => Some(pr.f("edgeRadius") * b.scale),
|
||||
2 => Some(r - thick * b.scale),
|
||||
3 => Some(r - thick / 100.0 * r),
|
||||
4 => {
|
||||
f_out = pr.f("feather/feather").max(0.0) / 100.0 * r;
|
||||
f_in = pr.f("feather/featherInner").max(0.0) / 100.0 * r;
|
||||
Some(r - thick / 100.0 * r)
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let (ro, ri) = match inner {
|
||||
Some(i) => (r.max(i), Some(r.min(i).max(0.0))),
|
||||
None => (r, None),
|
||||
};
|
||||
let (f_out, f_in) = (f_out.max(0.5), f_in.max(0.5));
|
||||
let color = pr.color("color");
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [crate::fx_generate::gen_mode(pr.e("blendingMode")), ri.is_some() as u32, pr.b("invert") as u32, 0];
|
||||
p.f[0] = f4([c.0, c.1, ro, ri.unwrap_or(0.0)]);
|
||||
p.f[1] = [f_out as f32, f_in as f32, pr.f("opacity") as f32 / 100.0, 0.0];
|
||||
p.f[2] = [color[0], color[1], color[2], 0.0];
|
||||
run(e, "fxe_circle", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Ellipse (generate2::ellipse)
|
||||
|
||||
fn ellipse(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let c = b.to_px(pr.v2("center"));
|
||||
let rx = (pr.f("width") * b.scale * 0.5).max(0.5);
|
||||
let ry = (pr.f("height") * b.scale * 0.5).max(0.5);
|
||||
let half = (pr.f("thickness") * b.scale * 0.5).max(0.25);
|
||||
let soft = (pr.f("softness") / 100.0).clamp(0.0, 1.0);
|
||||
let (inside, outside) = (pr.color("insideColor"), pr.color("outsideColor"));
|
||||
let mut p = Params::default();
|
||||
p.u[0][1] = pr.b("compositeOnOriginal") as u32;
|
||||
p.f[0] = f4([c.0, c.1, rx, ry]);
|
||||
p.f[1] = [half as f32, soft as f32, 0.0, 0.0];
|
||||
p.f[2] = [inside[0], inside[1], inside[2], 0.0];
|
||||
p.f[3] = [outside[0], outside[1], outside[2], 0.0];
|
||||
run(e, "fxe_ellipse", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Iris Wipe (transition::iris_wipe)
|
||||
|
||||
fn iris_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let outer = pr.f("outerRadius").max(0.0) * b.scale;
|
||||
if outer <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let n = pr.f("points").round().clamp(3.0, 64.0) as usize;
|
||||
let star = pr.b("useInnerRadius");
|
||||
let inner = if star { pr.f("innerRadius").max(0.0) * b.scale } else { outer };
|
||||
let rot = pr.f("rotation").to_radians();
|
||||
let c = b.to_px(pr.v2("center"));
|
||||
let m = if star { n * 2 } else { n };
|
||||
let step = std::f64::consts::TAU / m as f64;
|
||||
let verts: Vec<f32> = (0..m)
|
||||
.flat_map(|i| {
|
||||
let r = if star && i % 2 == 1 { inner } else { outer };
|
||||
let a = rot + i as f64 * step;
|
||||
[(r * a.sin()) as f32, (-r * a.cos()) as f32]
|
||||
})
|
||||
.collect();
|
||||
let buf = e.data(&verts);
|
||||
let mut p = Params::default();
|
||||
p.u[0][1] = m as u32;
|
||||
p.f[0] = f4([c.0, c.1, rot, step]);
|
||||
p.f[1] = f4([pr.f("feather") * b.scale, outer.min(inner), outer, 0.0]);
|
||||
run(e, "fxe_iris", &p, b, None, Some(&buf))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Bevels (perspective.rs)
|
||||
|
||||
/// perspective::light_dir.
|
||||
fn light_dir(angle_deg: f64, elevation_deg: f64) -> [f64; 3] {
|
||||
let (a, el) = (angle_deg.to_radians(), elevation_deg.to_radians());
|
||||
let v = [a.sin() * el.cos(), -a.cos() * el.cos(), el.sin()];
|
||||
let l = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt().max(1e-12);
|
||||
v.map(|x| x / l)
|
||||
}
|
||||
|
||||
fn bevel_alpha(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let t = pr.f("edgeThickness") * b.scale;
|
||||
let inten = pr.f("lightIntensity") as f32;
|
||||
if t <= 0.0 || inten == 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let light = pr.color("lightColor");
|
||||
let l = light_dir(pr.f("lightAngle"), 45.0);
|
||||
// The alpha plane's Gaussian (util::gauss_plane: box passes, edges repeated); kernels read x.
|
||||
let alpha = e.scratch(b.img.width, b.img.height);
|
||||
e.pixels("fxe_alpha", &Params::default(), &b.img, None, &alpha, None);
|
||||
let h = gaussian_blur(e, &alpha, t * 0.5, t * 0.5, true);
|
||||
let mut p = Params::default();
|
||||
p.f[0] = f4([l[0], l[1], l[2], t * 1.25]);
|
||||
p.f[1] = [light[0], light[1], light[2], inten];
|
||||
run(e, "fxe_bevel_alpha", &p, b, Some(&h), None)
|
||||
}
|
||||
|
||||
fn bevel_edges(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
let (x0, y0, w, h) = (b.offset[0], b.offset[1], ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
|
||||
let t = pr.f("edgeThickness").clamp(0.0, 0.5) * w.min(h);
|
||||
let inten = pr.f("lightIntensity") as f32;
|
||||
if t <= 0.0 || inten == 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let light = pr.color("lightColor");
|
||||
let l = light_dir(pr.f("lightAngle"), 45.0);
|
||||
let mut p = Params::default();
|
||||
p.f[0] = f4([l[0], l[1], l[2], t]);
|
||||
p.f[1] = [light[0], light[1], light[2], inten];
|
||||
p.f[2] = f4([x0, y0, w, h]);
|
||||
run(e, "fxe_bevel_edges", &p, b, None, None)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Gaussian Blur (Legacy) (obsolete.rs)
|
||||
|
||||
fn gaussian_legacy(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
let s = ctx.params.f("blurriness").max(0.0) * 0.5 * b.scale;
|
||||
if s <= 0.0 {
|
||||
return Some(b);
|
||||
}
|
||||
let (kx, ky) = match ctx.params.e("blurDimensions") {
|
||||
1 => (1.0, 0.0),
|
||||
2 => (0.0, 1.0),
|
||||
_ => (1.0, 1.0),
|
||||
};
|
||||
if !ctx.adjustment {
|
||||
b.pad(e, (s * 3.0).ceil() as u32)?;
|
||||
}
|
||||
b.img = gaussian_blur(e, &b.img, s * kx, s * ky, ctx.adjustment);
|
||||
Some(b)
|
||||
}
|
||||
@@ -83,7 +83,7 @@ fn hl(v: f64) -> [f32; 2] {
|
||||
}
|
||||
|
||||
/// Generate blending mode index → `BlendMode::ALL` index (255 = None: replace).
|
||||
fn gen_mode(i: u32) -> u32 {
|
||||
pub(crate) fn gen_mode(i: u32) -> u32 {
|
||||
match effectcraft_effects::gen_mode(i) {
|
||||
None => 255,
|
||||
Some(m) => BlendMode::ALL.iter().position(|&x| x == m).unwrap_or(0) as u32,
|
||||
|
||||
+15
-3
@@ -17,9 +17,18 @@
|
||||
//! GPU effects ([`effectcraft_effects::GPU_EFFECTS`]) run as compute kernels with the CPU
|
||||
//! effect's exact steps (padding, box-blur radii, parameter conversions); chains of them are
|
||||
//! uploaded and read back once. Each family lives in its own module with its own WGSL file
|
||||
//! (`fx_color`, `fx_distort`, `fx_generate`, `fx_key`, `fx_noise`, `fx_stylize`, `fx_tone`);
|
||||
//! settings a kernel cannot match fall back to the CPU (`catalog::gpu_supported`, or `None`
|
||||
//! from the family's `apply`).
|
||||
//! (`fx_color`, `fx_distort`, `fx_extra`, `fx_generate`, `fx_key`, `fx_noise`, `fx_stylize`,
|
||||
//! `fx_tone`, `fx_warp`); settings a kernel cannot match fall back to the CPU
|
||||
//! (`catalog::gpu_supported`, or `None` from the family's `apply`).
|
||||
//!
|
||||
//! Working textures come from a pool (`context::Pool`): a frame allocates several full-frame
|
||||
//! RGBA f32 images per layer, and creating and zeroing those cost more than compositing a small
|
||||
//! layer. A released texture is reused once every encoder that could still read it has been
|
||||
//! submitted. 8/16 bpc quantisation after each layer is fused into that layer's composite.
|
||||
//! [`Backend::Auto`](effectcraft_render::Backend) renders each comp on whichever compositor
|
||||
//! measured faster for it ([`effectcraft_render::AutoPick`], kept by [`Gpu`]): a light comp
|
||||
//! that the CPU composites in a millisecond stays there rather than paying for a full-frame
|
||||
//! readback.
|
||||
//!
|
||||
//! GPU particles (`particles`): the stepped particle effects hand their simulation to
|
||||
//! [`effectcraft_effects::psim::ParticleSim`], implemented here with one invocation per particle
|
||||
@@ -37,6 +46,7 @@ mod context;
|
||||
mod effects;
|
||||
mod fx_color;
|
||||
mod fx_distort;
|
||||
mod fx_extra;
|
||||
mod fx_generate;
|
||||
mod fx_key;
|
||||
mod fx_noise;
|
||||
@@ -186,6 +196,8 @@ mod tests_fx_color;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_distort;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_extra;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_generate;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_key;
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
// GPU effects (shapes and bevels): see src/fx_extra.rs. Circle, Ellipse, Iris Wipe, Bevel Alpha
|
||||
// and Bevel Edges, each mirroring the CPU effect operation for operation. Coordinates are pixel
|
||||
// centres relative to the shape's centre (or the layer's corner), so f32 keeps its precision.
|
||||
|
||||
fn fxe_inside(p: vec2<i32>) -> bool {
|
||||
let d = out_dims();
|
||||
return p.x < d.x && p.y < d.y;
|
||||
}
|
||||
|
||||
fn fxe_rel(p: vec2<i32>, c: vec2<f32>) -> vec2<f32> {
|
||||
return vec2<f32>(f32(p.x) + 0.5, f32(p.y) + 0.5) - c;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Circle (generate::circle)
|
||||
// u[0] = (gen mode, has inner, invert); f[0] = (cx, cy, outer radius, inner radius);
|
||||
// f[1] = (outer feather, inner feather, opacity); f[2] = colour.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxe_circle(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxe_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let d = length(fxe_rel(p, P.f[0].xy));
|
||||
var cov = clamp((P.f[0].z - d) / P.f[1].x + 0.5, 0.0, 1.0);
|
||||
if (P.u[0].y != 0u) {
|
||||
cov *= clamp((d - P.f[0].w) / P.f[1].y + 0.5, 0.0, 1.0);
|
||||
}
|
||||
if (P.u[0].z != 0u) {
|
||||
cov = 1.0 - cov;
|
||||
}
|
||||
let a = cov * P.f[1].z;
|
||||
textureStore(out, p, gen_blend(textureLoad(src, p, 0), vec4<f32>(P.f[2].xyz * a, a), P.u[0].x));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Ellipse (generate2::ellipse)
|
||||
// u[0].y = composite on original; f[0] = (cx, cy, rx, ry); f[1] = (half thickness, softness);
|
||||
// f[2] = inside colour; f[3] = outside colour.
|
||||
|
||||
fn fxe_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);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxe_ellipse(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxe_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let d = fxe_rel(p, P.f[0].xy);
|
||||
let r = P.f[0].zw;
|
||||
let q = length(d / r);
|
||||
var dist = min(r.x, r.y);
|
||||
if (q >= 1e-9) {
|
||||
let g = d / (r * r) / q;
|
||||
dist = abs((q - 1.0) / max(length(g), 1e-9));
|
||||
}
|
||||
let half = P.f[1].x;
|
||||
let soft = P.f[1].y;
|
||||
let t = dist / half;
|
||||
var a = clamp(half - dist + 0.5, 0.0, 1.0);
|
||||
if (soft > 1e-3) {
|
||||
a = 1.0 - fxe_smoothstep(1.0 - soft, 1.0, t);
|
||||
}
|
||||
let k = fxe_smoothstep(0.0, 1.0, t);
|
||||
let cc = P.f[2].xyz + (P.f[3].xyz - P.f[2].xyz) * k;
|
||||
let g4 = vec4<f32>(cc * a, a);
|
||||
if (P.u[0].y != 0u) {
|
||||
let o = textureLoad(src, p, 0);
|
||||
textureStore(out, p, g4 + o * (1.0 - g4.w));
|
||||
} else {
|
||||
textureStore(out, p, g4);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Iris Wipe (transition::iris_wipe)
|
||||
// data = polygon vertices relative to the centre; u[0] = (_, vertex count);
|
||||
// f[0] = (cx, cy, rotation, angle step); f[1] = (feather, radius at the centre, outer radius).
|
||||
|
||||
fn fxe_cross(a: vec2<f32>, b: vec2<f32>) -> f32 {
|
||||
return a.x * b.y - a.y * b.x;
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxe_iris(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxe_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let d = fxe_rel(p, P.f[0].xy);
|
||||
let r = length(d);
|
||||
let m = P.u[0].y;
|
||||
let rot = P.f[0].z;
|
||||
let step = P.f[0].w;
|
||||
var rb = P.f[1].y;
|
||||
if (r >= 1e-9) {
|
||||
let tau = 6.2831855;
|
||||
let a0 = atan2(d.x, -d.y) - rot;
|
||||
let a = a0 - floor(a0 / tau) * tau;
|
||||
let i = min(u32(floor(a / step)), m - 1u);
|
||||
let vi = vec2<f32>(data[2u * i], data[2u * i + 1u]);
|
||||
let j = (i + 1u) % m;
|
||||
let vj = vec2<f32>(data[2u * j], data[2u * j + 1u]);
|
||||
let e = vj - vi;
|
||||
let den = fxe_cross(d / r, e);
|
||||
rb = select(fxe_cross(vi, e) / den, P.f[1].z, abs(den) < 1e-12);
|
||||
}
|
||||
let feather = P.f[1].x;
|
||||
var k = select(0.0, 1.0, r >= rb);
|
||||
if (feather > 0.0) {
|
||||
k = clamp((r - rb) / feather + 0.5, 0.0, 1.0);
|
||||
}
|
||||
textureStore(out, p, textureLoad(src, p, 0) * k);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Bevels (perspective.rs)
|
||||
|
||||
// perspective::apply_shade.
|
||||
fn fxe_shade(px: vec4<f32>, s: f32, light: vec3<f32>) -> vec4<f32> {
|
||||
if (s > 0.0) {
|
||||
return vec4<f32>(px.xyz + light * s * px.w, px.w);
|
||||
}
|
||||
if (s < 0.0) {
|
||||
return vec4<f32>(px.xyz * max(1.0 + s, 0.0), px.w);
|
||||
}
|
||||
return px;
|
||||
}
|
||||
|
||||
// Plane::alpha (in x).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxe_alpha(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxe_inside(p)) {
|
||||
return;
|
||||
}
|
||||
textureStore(out, p, vec4<f32>(textureLoad(src, p, 0).w, 0.0, 0.0, 0.0));
|
||||
}
|
||||
|
||||
// Bevel Alpha: src = the layer, aux = its blurred alpha (x). f[0] = (light direction, normal
|
||||
// scale k); f[1] = (light colour, intensity).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxe_bevel_alpha(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxe_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
if (px.w <= 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
let dx = (tex_get_clamped(aux, p.x + 1, p.y).x - tex_get_clamped(aux, p.x - 1, p.y).x) * 0.5;
|
||||
let dy = (tex_get_clamped(aux, p.x, p.y + 1).x - tex_get_clamped(aux, p.x, p.y - 1).x) * 0.5;
|
||||
let k = P.f[0].w;
|
||||
let n = normalize(vec3<f32>(-dx * k, -dy * k, 1.0));
|
||||
let l = P.f[0].xyz;
|
||||
let s = (dot(n, l) - l.z) * P.f[1].w * 2.0;
|
||||
textureStore(out, p, fxe_shade(px, s, P.f[1].xyz));
|
||||
}
|
||||
|
||||
// Bevel Edges: f[0] = (light direction, thickness); f[1] = (light colour, intensity);
|
||||
// f[2] = layer rectangle (x0, y0, w, h).
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxe_bevel_edges(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!fxe_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let px = textureLoad(src, p, 0);
|
||||
let rc = P.f[2];
|
||||
let q = vec2<f32>(f32(p.x) + 0.5, f32(p.y) + 0.5);
|
||||
let d = vec4<f32>(q.x - rc.x, rc.x + rc.z - q.x, q.y - rc.y, rc.y + rc.w - q.y);
|
||||
if (d.x < 0.0 || d.y < 0.0 || d.z < 0.0 || d.w < 0.0) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
var mi = 0;
|
||||
var mv = d.x;
|
||||
for (var i = 1; i < 4; i++) {
|
||||
if (d[i] < mv) {
|
||||
mv = d[i];
|
||||
mi = i;
|
||||
}
|
||||
}
|
||||
if (mv >= P.f[0].w) {
|
||||
textureStore(out, p, px);
|
||||
return;
|
||||
}
|
||||
var n2 = vec2<f32>(-1.0, 0.0);
|
||||
if (mi == 1) {
|
||||
n2 = vec2<f32>(1.0, 0.0);
|
||||
} else if (mi == 2) {
|
||||
n2 = vec2<f32>(0.0, -1.0);
|
||||
} else if (mi == 3) {
|
||||
n2 = vec2<f32>(0.0, 1.0);
|
||||
}
|
||||
let tilt = 0.70710677;
|
||||
let n = vec3<f32>(n2 * tilt, tilt);
|
||||
let l = P.f[0].xyz;
|
||||
let s = (dot(n, l) - l.z) * P.f[1].w * 2.0;
|
||||
textureStore(out, p, fxe_shade(px, s, P.f[1].xyz));
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
//! GPU shape and bevel effects (Circle, Ellipse, Iris Wipe, Bevel Alpha, Bevel Edges, Gaussian
|
||||
//! Blur (Legacy)) 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 circle_and_ellipse() {
|
||||
effect_case("ec.generate.circle", &[("center", pt(30.0, 20.0)), ("radius", n(15.0))]);
|
||||
effect_case("ec.generate.circle", &[("radius", n(18.0)), ("edge", e(2)), ("thickness", n(5.0)), ("feather/feather", n(3.0)), ("blendingMode", e(3))]);
|
||||
effect_case(
|
||||
"ec.generate.circle",
|
||||
&[("radius", n(20.0)), ("edge", e(4)), ("thickness", n(30.0)), ("feather/featherInner", n(10.0)), ("invert", on()), ("opacity", n(60.0))],
|
||||
);
|
||||
effect_case("ec.generate.circle", &[("radius", n(12.0)), ("edge", e(1)), ("edgeRadius", n(25.0)), ("color", c(0.2, 0.8, 0.4))]);
|
||||
effect_case("ec.generate.ellipse", &[("center", pt(35.0, 22.0)), ("width", n(50.0)), ("height", n(30.0)), ("thickness", n(6.0))]);
|
||||
effect_case("ec.generate.ellipse", &[("width", n(40.0)), ("height", n(60.0)), ("softness", n(40.0)), ("compositeOnOriginal", on())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn iris_wipe() {
|
||||
effect_case("ec.transition.iriswipe", &[("outerRadius", n(20.0)), ("center", pt(35.0, 22.0))]);
|
||||
effect_case(
|
||||
"ec.transition.iriswipe",
|
||||
&[("outerRadius", n(25.0)), ("points", n(5.0)), ("useInnerRadius", on()), ("innerRadius", n(10.0)), ("rotation", n(20.0)), ("feather", n(4.0))],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bevels_and_legacy_gaussian() {
|
||||
effect_case("ec.perspective.bevelalpha", &[("edgeThickness", n(4.0)), ("lightIntensity", n(0.6))]);
|
||||
effect_case("ec.perspective.bevelalpha", &[("edgeThickness", n(2.0)), ("lightAngle", n(200.0)), ("lightColor", c(1.0, 0.8, 0.5))]);
|
||||
effect_case("ec.perspective.beveledges", &[("edgeThickness", n(0.1)), ("lightIntensity", n(0.5))]);
|
||||
effect_case("ec.perspective.beveledges", &[("edgeThickness", n(0.3)), ("lightAngle", n(130.0))]);
|
||||
effect_case("ec.obsolete.gaussianlegacy", &[("blurriness", n(7.0))]);
|
||||
effect_case("ec.obsolete.gaussianlegacy", &[("blurriness", n(5.0)), ("blurDimensions", e(2))]);
|
||||
}
|
||||
+7
-7
@@ -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**: 160 effects also run on the GPU compositor with identical results.
|
||||
- **GPU**: 166 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).
|
||||
|
||||
@@ -200,8 +200,8 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| 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 |
|
||||
| Circle | `ec.generate.circle` | 11 | | 32 | Implemented |
|
||||
| Ellipse | `ec.generate.ellipse` | 8 | | 32 | Implemented |
|
||||
| Circle | `ec.generate.circle` | 11 | GPU | 32 | Implemented |
|
||||
| Ellipse | `ec.generate.ellipse` | 8 | GPU | 32 | Implemented |
|
||||
| Eyedropper Fill | `ec.generate.eyedropperfill` | 5 | | 32 | Implemented |
|
||||
| Fill | `ec.generate.fill` | 7 | GPU | 32 | Implemented |
|
||||
| Fractal | `ec.generate.fractal` | 21 | | 32 | Implemented |
|
||||
@@ -285,7 +285,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Basic 3D | `ec.obsolete.basic3d` | 5 | | 32 | Implemented |
|
||||
| Basic Text | `ec.obsolete.basictext` | 11 | | 32 | Implemented |
|
||||
| Color Key | `ec.key.colorkey` | 4 | GPU | 32 | Implemented |
|
||||
| Gaussian Blur (Legacy) | `ec.obsolete.gaussianlegacy` | 2 | | 32 | Implemented |
|
||||
| Gaussian Blur (Legacy) | `ec.obsolete.gaussianlegacy` | 2 | GPU | 32 | Implemented |
|
||||
| Lightning | `ec.obsolete.lightning` | 25 | | 32 | Implemented |
|
||||
| Luma Key | `ec.key.luma` | 5 | GPU | 32 | Implemented |
|
||||
| Path Text | `ec.obsolete.pathtext` | 32 | | 32 | Implemented |
|
||||
@@ -304,8 +304,8 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| 3D Camera Tracker | `ec.perspective.cameratracker` | 12 | | 32 | Implemented |
|
||||
| 3D Glasses | `ec.perspective.3dglasses` | 8 | | 32 | Implemented |
|
||||
| Bevel Alpha | `ec.perspective.bevelalpha` | 4 | | 32 | Implemented |
|
||||
| Bevel Edges | `ec.perspective.beveledges` | 4 | | 32 | Implemented |
|
||||
| Bevel Alpha | `ec.perspective.bevelalpha` | 4 | GPU | 32 | Implemented |
|
||||
| Bevel Edges | `ec.perspective.beveledges` | 4 | GPU | 32 | Implemented |
|
||||
| CC Cylinder | `ec.perspective.cccylinder` | 12 | | 32 | Implemented |
|
||||
| CC Environment | `ec.perspective.ccenvironment` | 4 | | 32 | Implemented |
|
||||
| CC Sphere | `ec.perspective.ccsphere` | 14 | | 32 | Implemented |
|
||||
@@ -403,7 +403,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| CC WarpoMatic | `ec.transition.ccwarpomatic` | 8 | | 32 | Implemented |
|
||||
| Card Wipe | `ec.transition.cardwipe` | 44 | | 32 | Implemented |
|
||||
| Gradient Wipe | `ec.transition.gradientwipe` | 5 | GPU | 32 | Implemented |
|
||||
| Iris Wipe | `ec.transition.iriswipe` | 7 | | 32 | Implemented |
|
||||
| Iris Wipe | `ec.transition.iriswipe` | 7 | GPU | 32 | Implemented |
|
||||
| Linear Wipe | `ec.transition.linearwipe` | 3 | GPU | 32 | Implemented |
|
||||
| Radial Wipe | `ec.transition.radialwipe` | 5 | GPU | 32 | Implemented |
|
||||
| Venetian Blinds | `ec.transition.venetian` | 4 | GPU | 32 | Implemented |
|
||||
|
||||
Reference in new issue
Block a user