diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 52f19e40..4b82cf20 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -94,6 +94,7 @@ pub use transition::place_layer; // effectcraft-gpu fx_noise. pub use noise::remove_grain_level; pub use noise3::{FractalGpu, fractal_gpu}; +pub use textfx::{TextLayer, text_layer}; pub use time_fx::{posterized_time, time_frames}; // effectcraft-gpu fx_tone. pub use ocio::color_stabilizer_maps; @@ -936,6 +937,28 @@ pub const GPU_EFFECTS: &[&str] = &[ "ec.generate.cclightburst", "ec.generate.cclightsweep", "ec.transition.cclightwipe", + // effectcraft-gpu fx_transition (transitions, perspective) + "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", + // effectcraft-gpu fx_text (glyph coverage rasterised on the CPU) + "ec.text.numbers", + "ec.text.timecode", ]; /// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated diff --git a/crates/effects/src/perspective2.rs b/crates/effects/src/perspective2.rs index a698ed5b..13846607 100644 --- a/crates/effects/src/perspective2.rs +++ b/crates/effects/src/perspective2.rs @@ -90,8 +90,10 @@ fn cc_environment(ctx: &EffectCtx, mut b: Buf) -> Buf { let ex = u.rem_euclid(1.0) * esz[0] * es + eo[0]; let ey = v.clamp(0.0, 1.0) * esz[1] * es + eo[1]; let e = env_img.sample_bilinear_clamped(ex, ey); - let (ec, _) = unpremul(e); - *px = premul(ec, a); + // Straight colour, with nearly transparent texels fading to black continuously + // (a hard 1e-6 cut-off would turn the float noise of the filtered map into colour). + let k = 1.0 / e[3].max(1e-3); + *px = premul([e[0] * k, e[1] * k, e[2] * k], a); } }); b diff --git a/crates/effects/src/textfx.rs b/crates/effects/src/textfx.rs index 87364f6b..8ffd686e 100644 --- a/crates/effects/src/textfx.rs +++ b/crates/effects/src/textfx.rs @@ -192,9 +192,9 @@ fn tint(c: [f32; 4], a: f32) -> Px { [c[0] * a, c[1] * a, c[2] * a, a] } -/// Draw text polylines with the given look (`r` = glyph stroke radius in px). -fn draw_text(b: &mut Buf, polys: &[Vec<[f64; 2]>], r: f64, look: &TextLook) { - let (w, h) = (b.img.width as usize, b.img.height as usize); +/// Coverage of text polylines (`r` = glyph stroke radius in px): the fill and, when the look +/// strokes, the stroke ring. +fn text_coverage(w: usize, h: usize, polys: &[Vec<[f64; 2]>], r: f64, look: &TextLook) -> (Plane, Option) { let segs_at = |rad: f64| { let mut s = Vec::new(); for pl in polys { @@ -211,24 +211,35 @@ fn draw_text(b: &mut Buf, polys: &[Vec<[f64; 2]>], r: f64, look: &TextLook) { let inner = if r - sw * 0.5 > 0.05 { raster_segs(w, h, &segs_at(r - sw * 0.5), 1.0) } else { Plane::new(w, h) }; Some(outer.zip_map(&inner, |a, b| (a - b).max(0.0))) }; + (fill, ring) +} + +/// One pixel of drawn text: `px` under the fill / stroke coverages `f`, `s` (opacity applied). +fn text_px(px: Px, f: f32, s: f32, look: &TextLook) -> Px { + let mut out = if look.on_original { px } else { [0.0; 4] }; + match look.display { + 1 => out = over(out, tint(look.stroke, s)), + 2 => { + out = over(out, tint(look.stroke, s)); + out = over(out, tint(look.fill, f)); + } + 3 => { + out = over(out, tint(look.fill, f)); + out = over(out, tint(look.stroke, s)); + } + _ => out = over(out, tint(look.fill, f)), + } + out +} + +/// Draw text polylines with the given look (`r` = glyph stroke radius in px). +fn draw_text(b: &mut Buf, polys: &[Vec<[f64; 2]>], r: f64, look: &TextLook) { + let (fill, ring) = text_coverage(b.img.width as usize, b.img.height as usize, polys, r, look); let op = look.opacity; b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| { - let mut out = if look.on_original { *px } else { [0.0; 4] }; let f = fill.data[i] * op; let s = ring.as_ref().map(|r| r.data[i]).unwrap_or(0.0) * op; - match look.display { - 1 => out = over(out, tint(look.stroke, s)), - 2 => { - out = over(out, tint(look.stroke, s)); - out = over(out, tint(look.fill, f)); - } - 3 => { - out = over(out, tint(look.fill, f)); - out = over(out, tint(look.stroke, s)); - } - _ => out = over(out, tint(look.fill, f)), - } - *px = out; + *px = text_px(*px, f, s, look); }); } @@ -337,7 +348,18 @@ fn numbers_text(ctx: &EffectCtx) -> String { } } -fn numbers(ctx: &EffectCtx, mut b: Buf) -> Buf { +/// What Numbers or Timecode draws: glyph polylines, stroke radius and look, whether the layer +/// stays under the text, and Timecode's box (`[x0, x1, y0, y1]` in buffer pixels, colour, +/// opacity × colour alpha). +struct TextPlan { + polys: Vec>, + r: f64, + look: TextLook, + keep: bool, + boxed: Option<([f64; 4], [f32; 4], f32)>, +} + +fn numbers_plan(ctx: &EffectCtx, b: &Buf) -> TextPlan { let flat = ungroup(ctx.params); let ctx = &EffectCtx { params: &flat, time: ctx.time, layer_size: ctx.layer_size, seed: ctx.seed, adjustment: ctx.adjustment, env: ctx.env }; let pr = ctx.params; @@ -349,11 +371,81 @@ fn numbers(ctx: &EffectCtx, mut b: Buf) -> Buf { let wdt = line_width(&text, size, tracking, prop); let mut polys = Vec::new(); layout_line(&text, [pos.0 - wdt * 0.5, pos.1 + unit(size) * 3.0], size, tracking, prop, &mut polys); - let look = look_from(ctx, &b, "compositeOnOriginal"); - draw_text(&mut b, &polys, weight(size), &look); + let mut look = look_from(ctx, b, "compositeOnOriginal"); + let keep = look.on_original; + look.on_original = true; + TextPlan { polys, r: weight(size), look, keep, boxed: None } +} + +/// Timecode's box over pixel (`x`, `y`). +fn box_px(px: Px, x: usize, y: usize, (r, bc, a): ([f64; 4], [f32; 4], f32)) -> Px { + let [x0, x1, y0, y1] = r; + let fy = y as f64 + 0.5; + let cy = ((fy - y0 + 0.5).clamp(0.0, 1.0) * (y1 - fy + 0.5).clamp(0.0, 1.0)) as f32; + if cy <= 0.0 { + return px; + } + let fx = x as f64 + 0.5; + let cx = ((fx - x0 + 0.5).clamp(0.0, 1.0) * (x1 - fx + 0.5).clamp(0.0, 1.0)) as f32; + let k = cx * cy * a; + if k > 0.0 { over(px, [bc[0] * k, bc[1] * k, bc[2] * k, k]) } else { px } +} + +fn draw_plan(mut b: Buf, plan: &TextPlan) -> Buf { + let w = b.img.width as usize; + let (fill, ring) = text_coverage(w, b.img.height as usize, &plan.polys, plan.r, &plan.look); + let op = plan.look.opacity; + b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| { + let mut o = if plan.keep { *px } else { [0.0; 4] }; + if let Some(bx) = plan.boxed { + o = box_px(o, i % w.max(1), i / w.max(1), bx); + } + let f = fill.data[i] * op; + let s = ring.as_ref().map(|r| r.data[i]).unwrap_or(0.0) * op; + *px = text_px(o, f, s, &plan.look); + }); b } +fn numbers(ctx: &EffectCtx, b: Buf) -> Buf { + let plan = numbers_plan(ctx, &b); + draw_plan(b, &plan) +} + +/// Numbers and Timecode for the GPU compositor (effectcraft-gpu `fx_text`): the glyph +/// coverage is rasterised here (the fill in x, the stroke ring in y, opacity applied), the +/// kernel composites it. +#[derive(Clone, Debug)] +pub struct TextLayer { + pub coverage: Image, + /// Display Options (0 fill only, 1 stroke only, 2 fill over stroke, 3 stroke over fill). + pub display: u32, + pub fill: [f32; 4], + pub stroke: [f32; 4], + /// The layer stays under the text (else the text is drawn on transparency). + pub keep: bool, + /// Timecode's box: `[x0, x1, y0, y1]` (buffer pixels), colour, opacity × colour alpha. + pub boxed: Option<([f64; 4], [f32; 4], f32)>, +} + +/// [`TextLayer`] of Numbers (`ec.text.numbers`) or Timecode (`ec.text.timecode`) on a buffer +/// of `b`'s geometry. +pub fn text_layer(id: &str, ctx: &EffectCtx, b: &Buf) -> Option { + let plan = match id { + "ec.text.numbers" => numbers_plan(ctx, b), + "ec.text.timecode" => timecode_plan(ctx, b), + _ => return None, + }; + let (w, h) = (b.img.width as usize, b.img.height as usize); + let (fill, ring) = text_coverage(w, h, &plan.polys, plan.r, &plan.look); + let op = plan.look.opacity; + let mut coverage = Image::new(b.img.width, b.img.height); + coverage.data.par_iter_mut().enumerate().for_each(|(i, px)| { + *px = [fill.data[i] * op, ring.as_ref().map(|r| r.data[i]).unwrap_or(0.0) * op, 0.0, 0.0]; + }); + Some(TextLayer { coverage, display: plan.look.display, fill: plan.look.fill, stroke: plan.look.stroke, keep: plan.keep, boxed: plan.boxed }) +} + // ---------------------------------------------------------------- Timecode /// SMPTE drop-frame label for nominal 30/60 fps rates. @@ -368,7 +460,7 @@ fn drop_frame(frames: i64, nominal: i64) -> String { format!("{:02};{:02};{:02};{:02}", adj / (nominal * 3600), (adj / (nominal * 60)) % 60, (adj / nominal) % 60, adj % nominal) } -fn timecode_fx(ctx: &EffectCtx, mut b: Buf) -> Buf { +fn timecode_plan(ctx: &EffectCtx, b: &Buf) -> TextPlan { let pr = ctx.params; let src = pr.e("timeSource"); let (t, fps) = match src { @@ -397,36 +489,20 @@ fn timecode_fx(ctx: &EffectCtx, mut b: Buf) -> Buf { let pos = b.to_px(pr.v2("textPosition")); let wdt = line_width(&text, size, 0.0, false); let op = (pr.f("opacity") / 100.0) as f32; - if !pr.b("renderOnOriginal") { - b.img = Image::new(b.img.width, b.img.height); - } - if pr.b("showBox") { + let boxed = pr.b("showBox").then(|| { let bc = pr.color("boxColor"); let pad = size * 0.25; - let (x0, x1) = (pos.0 - wdt * 0.5 - pad, pos.0 + wdt * 0.5 + pad); - let (y0, y1) = (pos.1 - size * 0.5, pos.1 + size * 0.5); - let a = op * bc[3]; - b.img.rows_mut().for_each(|(y, row)| { - let fy = y as f64 + 0.5; - let cy = ((fy - y0 + 0.5).clamp(0.0, 1.0) * (y1 - fy + 0.5).clamp(0.0, 1.0)) as f32; - if cy <= 0.0 { - return; - } - for (x, px) in row.iter_mut().enumerate() { - let fx = x as f64 + 0.5; - let cx = ((fx - x0 + 0.5).clamp(0.0, 1.0) * (x1 - fx + 0.5).clamp(0.0, 1.0)) as f32; - let k = cx * cy * a; - if k > 0.0 { - *px = over(*px, [bc[0] * k, bc[1] * k, bc[2] * k, k]); - } - } - }); - } + ([pos.0 - wdt * 0.5 - pad, pos.0 + wdt * 0.5 + pad, pos.1 - size * 0.5, pos.1 + size * 0.5], bc, op * bc[3]) + }); let mut polys = Vec::new(); layout_line(&text, [pos.0 - wdt * 0.5, pos.1 + unit(size) * 3.0], size, 0.0, false, &mut polys); let look = TextLook { display: 0, fill: pr.color("textColor"), stroke: [0.0; 4], stroke_w: 0.0, on_original: true, opacity: op }; - draw_text(&mut b, &polys, weight(size), &look); - b + TextPlan { polys, r: weight(size), look, keep: pr.b("renderOnOriginal"), boxed } +} + +fn timecode_fx(ctx: &EffectCtx, b: Buf) -> Buf { + let plan = timecode_plan(ctx, &b); + draw_plan(b, &plan) } // ---------------------------------------------------------------- Basic Text diff --git a/crates/gpu/src/context.rs b/crates/gpu/src/context.rs index 4862d7b2..28cc8b94 100644 --- a/crates/gpu/src/context.rs +++ b/crates/gpu/src/context.rs @@ -315,6 +315,8 @@ impl GpuContext { include_str!("shaders/fx_warp.wgsl"), include_str!("shaders/fx_extra.wgsl"), include_str!("shaders/fx_light.wgsl"), + include_str!("shaders/fx_transition.wgsl"), + include_str!("shaders/fx_text.wgsl"), ] .concat(); let module = @@ -375,6 +377,8 @@ impl GpuContext { .chain(crate::fx_warp::KERNELS) .chain(crate::fx_extra::KERNELS) .chain(crate::fx_light::KERNELS) + .chain(crate::fx_transition::KERNELS) + .chain(crate::fx_text::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 c72f9470..a28920e9 100644 --- a/crates/gpu/src/effects.rs +++ b/crates/gpu/src/effects.rs @@ -134,6 +134,8 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { _ 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), + _ if crate::fx_transition::IDS.contains(&id) => crate::fx_transition::apply(e, id, ctx, b), + _ if crate::fx_text::IDS.contains(&id) => crate::fx_text::apply(e, id, ctx, b), _ => pointwise(e, id, ctx, b), } } diff --git a/crates/gpu/src/fx_text.rs b/crates/gpu/src/fx_text.rs new file mode 100644 index 00000000..a5196957 --- /dev/null +++ b/crates/gpu/src/fx_text.rs @@ -0,0 +1,34 @@ +//! GPU effects, Numbers and Timecode (kernel `ftx_text` in `shaders/fx_text.wgsl`). The text +//! is laid out and its glyph strokes rasterised on the CPU (`effectcraft_effects::text_layer`, +//! the CPU effect's own coverage: fill and stroke ring), uploaded as one image and composited +//! on the GPU with the layer, Timecode's box and the fill / stroke colours. + +use effectcraft_effects::{Buf, EffectCtx}; +use effectcraft_raster::Image; + +use crate::context::{Enc, Params}; +use crate::effects::GBuf; + +/// Compute entry points in `fx_text.wgsl`. +pub(crate) const KERNELS: &[&str] = &["ftx_text"]; + +/// Effect ids implemented here. +pub(crate) const IDS: &[&str] = &["ec.text.numbers", "ec.text.timecode"]; + +/// 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 { + let shape = Buf { img: Image { width: b.img.width, height: b.img.height, data: vec![] }, offset: b.offset, scale: b.scale }; + let t = effectcraft_effects::text_layer(id, ctx, &shape)?; + let cov = e.g.upload_image(&t.coverage)?; + let mut p = Params::default(); + p.u[0] = [t.display, t.keep as u32, t.boxed.is_some() as u32, 0]; + p.f[0] = t.fill; + p.f[1] = t.stroke; + if let Some((r, c, a)) = t.boxed { + p.f[2] = r.map(|v| v as f32); + p.f[3] = [c[0], c[1], c[2], a]; + } + let out = e.scratch(b.img.width, b.img.height); + e.pixels("ftx_text", &p, &b.img, Some(&cov), &out, None); + Some(GBuf { img: out, ..b }) +} diff --git a/crates/gpu/src/fx_transition.rs b/crates/gpu/src/fx_transition.rs new file mode 100644 index 00000000..b74607a0 --- /dev/null +++ b/crates/gpu/src/fx_transition.rs @@ -0,0 +1,595 @@ +//! 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 { + 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 { + 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> { + 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 { + 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 { + 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 = 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::>(); + let cols = index(b.img.width, b.offset[0], bw); + let table: Vec = 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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; + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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) +} diff --git a/crates/gpu/src/lib.rs b/crates/gpu/src/lib.rs index a923b893..d6ee1c98 100644 --- a/crates/gpu/src/lib.rs +++ b/crates/gpu/src/lib.rs @@ -55,7 +55,9 @@ mod fx_key; mod fx_light; mod fx_noise; mod fx_stylize; +mod fx_text; mod fx_tone; +mod fx_transition; mod fx_warp; mod ops; mod particles; @@ -216,8 +218,12 @@ mod tests_fx_noise; #[cfg(test)] mod tests_fx_stylize; #[cfg(test)] +mod tests_fx_text; +#[cfg(test)] mod tests_fx_tone; #[cfg(test)] +mod tests_fx_transition; +#[cfg(test)] mod tests_fx_warp; #[cfg(test)] mod tests_particles; diff --git a/crates/gpu/src/shaders/fx_text.wgsl b/crates/gpu/src/shaders/fx_text.wgsl new file mode 100644 index 00000000..5ec3232a --- /dev/null +++ b/crates/gpu/src/shaders/fx_text.wgsl @@ -0,0 +1,59 @@ +// GPU effects, Numbers and Timecode: see src/fx_text.rs. The glyph coverage is rasterised on +// the CPU (aux: fill in x, stroke ring in y, opacity applied); this kernel composites it as +// textfx::draw_plan does: the layer or transparency, Timecode's box, then fill and stroke. +// u[0] = (display options, keep the layer, box); f[0] = fill colour; f[1] = stroke colour; +// f[2] = box (x0, x1, y0, y1); f[3] = (box colour, box opacity). + +fn ftx_over(dst: vec4, s: vec4) -> vec4 { + return s + dst * (1.0 - s.w); +} + +// textfx::tint. +fn ftx_tint(c: vec4, a0: f32) -> vec4 { + let a = clamp(a0 * c.w, 0.0, 1.0); + return vec4(c.xyz * a, a); +} + +@compute @workgroup_size(16, 16) +fn ftx_text(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + let dims = out_dims(); + if (p.x >= dims.x || p.y >= dims.y) { + return; + } + var o = vec4(0.0); + if (P.u[0].y != 0u) { + o = textureLoad(src, p, 0); + } + if (P.u[0].z != 0u) { + let r = P.f[2]; + let fy = f32(p.y) + 0.5; + let cy = clamp(fy - r.z + 0.5, 0.0, 1.0) * clamp(r.w - fy + 0.5, 0.0, 1.0); + if (cy > 0.0) { + let fx = f32(p.x) + 0.5; + let cx = clamp(fx - r.x + 0.5, 0.0, 1.0) * clamp(r.y - fx + 0.5, 0.0, 1.0); + let k = cx * cy * P.f[3].w; + if (k > 0.0) { + o = ftx_over(o, vec4(P.f[3].xyz * k, k)); + } + } + } + let cov = textureLoad(aux, p, 0); + let fill = ftx_tint(P.f[0], cov.x); + let stroke = ftx_tint(P.f[1], cov.y); + switch P.u[0].x { + case 1u: { + o = ftx_over(o, stroke); + } + case 2u: { + o = ftx_over(ftx_over(o, stroke), fill); + } + case 3u: { + o = ftx_over(ftx_over(o, fill), stroke); + } + default: { + o = ftx_over(o, fill); + } + } + textureStore(out, p, o); +} diff --git a/crates/gpu/src/shaders/fx_transition.wgsl b/crates/gpu/src/shaders/fx_transition.wgsl new file mode 100644 index 00000000..2b9a0b9f --- /dev/null +++ b/crates/gpu/src/shaders/fx_transition.wgsl @@ -0,0 +1,883 @@ +// GPU effects, transitions and perspective: see src/fx_transition.rs. Block Dissolve, the CC +// transitions (Glass Wipe, Grid Wipe, Image Wipe, Jaws, Line Sweep, Radial ScaleWipe, Scale +// Wipe, Twister, WarpoMatic), Radial Shadow, CC Bender, CC Blobbylize, CC Cylinder, CC Sphere, +// CC Spotlight, CC Environment and 3D Glasses, each mirroring the CPU effect operation for +// operation. Coordinates are pixel centres relative to the effect's centre where it has one. + +fn ftr_inside(p: vec2) -> bool { + let d = out_dims(); + return p.x < d.x && p.y < d.y; +} + +fn ftr_centre(p: vec2) -> vec2 { + return vec2(f32(p.x) + 0.5, f32(p.y) + 0.5); +} + +// util::smoothstep. +fn ftr_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 ftr_unpremul(p: vec4) -> vec3 { + if (p.w > 1e-6) { + return p.xyz / p.w; + } + return vec3(0.0); +} + +// The third image, copied into `data` as RGBA f32 rows: P.u[3] = (row stride, width, height). +fn ftr_row(x: i32, y: i32) -> vec4 { + if (x < 0 || y < 0 || x >= i32(P.u[3].y) || y >= i32(P.u[3].z)) { + return vec4(0.0); + } + let i = (u32(y) * P.u[3].x + u32(x)) * 4u; + return vec4(data[i], data[i + 1u], data[i + 2u], data[i + 3u]); +} + +fn ftr_row_clamped(x: i32, y: i32) -> vec4 { + return ftr_row(clamp(x, 0, i32(P.u[3].y) - 1), clamp(y, 0, i32(P.u[3].z) - 1)); +} + +// Image::sample_bilinear on the third image. +fn ftr_row_bilinear(x: f32, y: f32) -> vec4 { + let fx = x - 0.5; + let fy = y - 0.5; + let x0 = floor(fx); + let y0 = floor(fy); + let tx = fx - x0; + let ty = fy - y0; + let xi = i32(x0); + let yi = i32(y0); + let a = ftr_row(xi, yi); + let b = ftr_row(xi + 1, yi); + let c = ftr_row(xi, yi + 1); + let d = ftr_row(xi + 1, yi + 1); + let top = a + (b - a) * tx; + let bot = c + (d - c) * tx; + return top + (bot - top) * ty; +} + +// Image::sample_bilinear_clamped on the third image. +fn ftr_row_bilinear_clamped(x: f32, y: f32) -> vec4 { + let fx = x - 0.5; + let fy = y - 0.5; + let x0 = floor(fx); + let y0 = floor(fy); + let tx = fx - x0; + let ty = fy - y0; + let xi = i32(x0); + let yi = i32(y0); + let a = ftr_row_clamped(xi, yi); + let b = ftr_row_clamped(xi + 1, yi); + let c = ftr_row_clamped(xi, yi + 1); + let d = ftr_row_clamped(xi + 1, yi + 1); + let top = a + (b - a) * tx; + let bot = c + (d - c) * tx; + return top + (bot - top) * ty; +} + +// Central differences of the aux plane (x), edges clamped (transition2::grad). +fn ftr_grad(p: vec2) -> vec2 { + let gx = (tex_get_clamped(aux, p.x + 1, p.y).x - tex_get_clamped(aux, p.x - 1, p.y).x) * 0.5; + let gy = (tex_get_clamped(aux, p.x, p.y + 1).x - tex_get_clamped(aux, p.x, p.y - 1).x) * 0.5; + return vec2(gx, gy); +} + +// transition2::wipe_t. +fn ftr_wipe_t(g: f32, c: f32, w0: f32) -> f32 { + let w = max(w0, 1e-3); + let edge = c * (1.0 + 2.0 * w) - w; + return 1.0 - ftr_smoothstep(edge - w, edge + w, g); +} + +// ---------------------------------------------------------------- planes + +// A plane (x) from the source: u[0].x = util::Src index (Red, Green, Blue, Alpha, Luminance, +// Hue, Lightness, Saturation, Full, Half, Off) times alpha except for Alpha itself +// (transition2::prop_plane, CC Blobbylize); 11 = CC Environment's relief. +@compute @workgroup_size(16, 16) +fn ftr_plane(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let px = textureLoad(src, p, 0); + let c = ftr_unpremul(px); + let a = max(px.w, 0.0); + var v: f32; + switch P.u[0].x { + case 0u: { + v = c.x; + } + case 1u: { + v = c.y; + } + case 2u: { + v = c.z; + } + case 3u: { + v = a; + } + case 4u: { + v = luminance(c); + } + case 5u: { + v = rgb_to_hsl(c).x; + } + case 6u: { + v = rgb_to_hsl(c).z; + } + case 7u: { + v = rgb_to_hsl(c).y; + } + case 8u: { + v = 1.0; + } + case 9u: { + v = 0.5; + } + case 11u: { + v = luminance(c) * 0.5 + 0.5; + } + default: { + v = 0.0; + } + } + if (P.u[0].x != 3u) { + v *= a; + } + textureStore(out, p, vec4(v, 0.0, 0.0, 0.0)); +} + +// src × aux.x (Image::mul_mask). +@compute @workgroup_size(16, 16) +fn ftr_mul(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + textureStore(out, p, textureLoad(src, p, 0) * textureLoad(aux, p, 0).x); +} + +// aux + src × (1 − aux.a): src composited under aux. +@compute @workgroup_size(16, 16) +fn ftr_under(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let o = textureLoad(aux, p, 0); + textureStore(out, p, o + textureLoad(src, p, 0) * (1.0 - o.w)); +} + +// ---------------------------------------------------------------- Block Dissolve (transition::block_dissolve) +// The kept-block mask (x). u[0] = (sub-samples per axis, seed, column table length); +// f[0].x = completion; data = block columns (width × n, u32) then block rows (height × n). + +@compute @workgroup_size(16, 16) +fn ftr_blocks(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let n = P.u[0].x; + var hits = 0u; + for (var j = 0u; j < n; j++) { + let by = bitcast(data[P.u[0].z + u32(p.y) * n + j]); + for (var i = 0u; i < n; i++) { + let bx = bitcast(data[u32(p.x) * n + i]); + if (hash_noise(bx, by, P.u[0].y) >= P.f[0].x) { + hits += 1u; + } + } + } + textureStore(out, p, vec4(f32(hits) / f32(n * n), 0.0, 0.0, 0.0)); +} + +// ---------------------------------------------------------------- CC Grid Wipe (transition::grid_wipe) +// u[0] = (shape, reverse); f[0] = (cx, cy, sin, cos); f[1] = (cell size, max distance, +// completion, border). + +fn ftr_metric(shape: u32, d: vec2) -> f32 { + switch shape { + case 0u: { + return abs(d.x); + } + case 2u: { + return max(abs(d.x), abs(d.y)); + } + default: { + return length(d); + } + } +} + +@compute @workgroup_size(16, 16) +fn ftr_grid(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let px = textureLoad(src, p, 0); + let d = ftr_centre(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); + let cs = P.f[1].x; + let cc = (floor(q / cs) + 0.5) * cs; + var o = clamp(ftr_metric(P.u[0].x, cc) / P.f[1].y, 0.0, 1.0); + if (P.u[0].y != 0u) { + o = 1.0 - o; + } + let spread = 0.25; + let pr = clamp((P.f[1].z * (1.0 + spread) - o) / spread, 0.0, 1.0); + if (pr <= 0.0) { + textureStore(out, p, px); + return; + } + let half = cs * 0.5 * (1.0 - pr); + let m = max(abs(q.x - cc.x), abs(q.y - cc.y)); + var k = select(0.0, 1.0, m < half); + if (P.f[1].w > 0.0) { + k = clamp((half - m) / P.f[1].w + 0.5, 0.0, 1.0); + } + textureStore(out, p, px * k); +} + +// ---------------------------------------------------------------- CC Radial ScaleWipe / CC Scale Wipe (transition.rs) +// Radial: u[0].x = reverse; f[0] = (cx, cy, max distance, completion). + +@compute @workgroup_size(16, 16) +fn ftr_radial_scale(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let c = P.f[0].xy; + let maxd = P.f[0].z; + let done = P.f[0].w; + let d = ftr_centre(p) - c; + let r = length(d); + var o = vec4(0.0); + if (P.u[0].x != 0u) { + let lim = (1.0 - done) * maxd; + if (!(r >= lim || lim <= 1e-9)) { + let k = maxd / lim; + o = sample_bilinear(src, c.x + d.x * k, c.y + d.y * k); + } + } else { + let hole = done * maxd; + if (!(r <= hole || maxd - hole <= 1e-9)) { + let rs = (r - hole) * maxd / (maxd - hole); + o = sample_bilinear(src, c.x + d.x / r * rs, c.y + d.y / r * rs); + } + } + textureStore(out, p, o); +} + +// Scale Wipe: f[0] = (cx, cy, direction x, direction y); f[1].x = stretch. +@compute @workgroup_size(16, 16) +fn ftr_scale(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let f = ftr_centre(p); + let dir = P.f[0].zw; + let u = dot(f - P.f[0].xy, dir); + var q = f; + if (u > 0.0) { + let back = u - u / (1.0 + P.f[1].x); + q = f - dir * back; + } + textureStore(out, p, sample_bilinear_clamped(src, q.x, q.y)); +} + +// ---------------------------------------------------------------- CC Glass Wipe (transition2::glass_wipe) +// aux = the gradient plane (x); data = the layer to reveal (rows). u[0].x = has reveal; +// f[0] = (completion, band half-width, displacement). + +@compute @workgroup_size(16, 16) +fn ftr_glass(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let t = ftr_wipe_t(textureLoad(aux, p, 0).x, P.f[0].x, P.f[0].y); + let band = 4.0 * t * (1.0 - t); + let g = ftr_grad(p); + let f = ftr_centre(p); + let o = g * P.f[0].z * band * 10.0; + var a = textureLoad(src, p, 0); + var r = vec4(0.0); + if (band > 1e-4) { + a = sample_bilinear(src, f.x + o.x, f.y + o.y); + if (P.u[0].x != 0u) { + r = ftr_row_bilinear(f.x - o.x, f.y - o.y); + } + } else if (P.u[0].x != 0u) { + r = ftr_row(p.x, p.y); + } + textureStore(out, p, a + (r - a) * t); +} + +// ---------------------------------------------------------------- CC Image Wipe (transition2::image_wipe) +// aux = the gradient plane (x). u[0].x = inverse; f[0] = (completion, band half-width). + +@compute @workgroup_size(16, 16) +fn ftr_image_wipe(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + var g = textureLoad(aux, p, 0).x; + if (P.u[0].x != 0u) { + g = 1.0 - g; + } + let k = 1.0 - ftr_wipe_t(g, P.f[0].x, P.f[0].y); + textureStore(out, p, textureLoad(src, p, 0) * k); +} + +// ---------------------------------------------------------------- CC Jaws (transition2::jaws) +// u[0].x = shape; f[0] = (cx, cy, sin, cos); f[1] = (tooth width, amplitude, travel). + +@compute @workgroup_size(16, 16) +fn ftr_jaws(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let c = P.f[0].xy; + let sn = P.f[0].z; + let cs = P.f[0].w; + let width = P.f[1].x; + let amp = P.f[1].y; + let dd = P.f[1].z; + let d = ftr_centre(p) - c; + let u = d.x * cs + d.y * sn; + let v = -d.x * sn + d.y * cs; + let ph = fract_euclid(u / width); + var f: f32; + switch P.u[0].x { + case 0u: { + f = amp * (1.0 - 4.0 * abs(ph - 0.5)); + } + case 1u: { + f = amp * clamp((1.0 - 4.0 * abs(ph - 0.5)) * 2.0, -1.0, 1.0); + } + case 2u: { + f = select(-amp, amp, ph < 0.5); + } + default: { + f = amp * sin(ph * 6.283185307179586); + } + } + var vs = 0.0; + var hit = true; + if (v + dd < f) { + vs = v + dd; + } else if (v - dd >= f) { + vs = v - dd; + } else { + hit = false; + } + var o = vec4(0.0); + if (hit) { + o = sample_bilinear(src, u * cs - vs * sn + c.x, u * sn + vs * cs + c.y); + } + textureStore(out, p, o); +} + +// ---------------------------------------------------------------- CC Line Sweep (transition2::line_sweep) +// u[0] = (flip, slant ≥ 0); f[0] = (sin, cos, v0, u0); f[1] = (thickness, |slant|, strips, +// total travel); f[2].x = completion. + +@compute @workgroup_size(16, 16) +fn ftr_line_sweep(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let f = ftr_centre(p); + let sn = P.f[0].x; + let cs = P.f[0].y; + let u = f.x * cs + f.y * sn; + let v = -f.x * sn + f.y * cs; + let nstr = P.f[1].z; + var k = floor((v - P.f[0].z) / P.f[1].x); + if (P.u[0].x != 0u) { + k = nstr - 1.0 - k; + } + let stagger = select(nstr - 1.0 - k, k, P.u[0].y != 0u); + let front = P.f[0].w + P.f[2].x * P.f[1].w - P.f[1].y * stagger; + textureStore(out, p, select(textureLoad(src, p, 0), vec4(0.0), u < front)); +} + +// ---------------------------------------------------------------- CC Twister (transition2::twister) +// aux = the backside layer (u[0].x = has one); u[0].y = shading; f[0] = (cx, cy, sin, cos); +// f[1] = (half diagonal, completion). + +@compute @workgroup_size(16, 16) +fn ftr_twister(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let c = P.f[0].xy; + let sn = P.f[0].z; + let cs = P.f[0].w; + let d = ftr_centre(p) - c; + let u = d.x * cs + d.y * sn; + let v = -d.x * sn + d.y * cs; + let s01 = clamp((u / P.f[1].x) * 0.5 + 0.5, 0.0, 1.0); + let th = 3.141592653589793 * clamp(P.f[1].y * 2.0 - s01, 0.0, 1.0); + let k = cos(th); + if (abs(k) < 1e-4) { + textureStore(out, p, vec4(0.0)); + return; + } + let vs = v / k; + var o: vec4; + if (k > 0.0 || P.u[0].x == 0u) { + o = sample_bilinear(src, u * cs - vs * sn + c.x, u * sn + vs * cs + c.y); + } else { + o = sample_bilinear(aux, u * cs + vs * sn + c.x, u * sn - vs * cs + c.y); + } + if (P.u[0].y != 0u) { + let l = 0.4 + 0.6 * abs(k); + o = vec4(o.xyz * l, o.w); + } + textureStore(out, p, o); +} + +// ---------------------------------------------------------------- CC WarpoMatic (transition2::warpomatic) + +// Reactor "Contrast Differences": src = the plane, aux = its wide mean. +@compute @workgroup_size(16, 16) +fn ftr_react_contrast(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let a = textureLoad(src, p, 0).x; + let b = textureLoad(aux, p, 0).x; + textureStore(out, p, vec4(min(abs(a - b) * 4.0, 1.0), 0.0, 0.0, 0.0)); +} + +// Reactor "Local Differences": aux = the plane; its gradient magnitude. +@compute @workgroup_size(16, 16) +fn ftr_react_local(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let g = ftr_grad(p); + textureStore(out, p, vec4(min(sqrt(g.x * g.x + g.y * g.y) * 8.0, 1.0), 0.0, 0.0, 0.0)); +} + +// aux = the reactor plane (x); data = the layer to reveal (rows). u[0] = (warp direction, +// has reveal); f[0] = (completion, blend span, warp amount). +@compute @workgroup_size(16, 16) +fn ftr_warpo(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let q = ftr_wipe_t(textureLoad(aux, p, 0).x, P.f[0].x, P.f[0].y); + let g = ftr_grad(p); + let f = ftr_centre(p); + let amt = P.f[0].z; + let o = g * amt * q; + var i = g * amt * (1.0 - q); + if (P.u[0].x == 1u) { + i = -i; + } else if (P.u[0].x == 2u) { + i = vec2(-i.y, i.x); + } + var a = vec4(0.0); + if (q < 1.0) { + a = sample_bilinear(src, f.x + o.x, f.y + o.y); + } + var inc = vec4(0.0); + if (P.u[0].y != 0u && q > 0.0) { + inc = ftr_row_bilinear(f.x + i.x, f.y + i.y); + } + textureStore(out, p, a + (inc - a) * q); +} + +// ---------------------------------------------------------------- Radial Shadow (perspective::radial_shadow) +// The shadow before its blur. u[0].x = glass edge; f[0] = (light x, y, projection k, +// opacity × colour alpha); f[1] = (colour, colour influence). + +@compute @workgroup_size(16, 16) +fn ftr_rshadow(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let l = P.f[0].xy; + let q = ftr_centre(p); + let s = sample_bilinear(src, l.x + (q.x - l.x) / P.f[0].z, l.y + (q.y - l.y) / P.f[0].z); + let sc = ftr_unpremul(s); + let sa = max(s.w, 0.0); + var a = sa; + if (P.u[0].x != 0u) { + a = clamp(sa * (1.0 - sa) * 4.0, 0.0, 1.0); + } + a *= P.f[0].w; + let col = P.f[1].xyz; + let c = col + (sc - col) * P.f[1].w; + textureStore(out, p, vec4(c * a, a)); +} + +// ---------------------------------------------------------------- CC Bender (distort3::cc_bender) +// u[0].x = style; f[0] = (base x, y, axis x, y); f[1] = (axis length, normal x, y, amount × +// reference). + +@compute @workgroup_size(16, 16) +fn ftr_bender(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let f = ftr_centre(p); + let r = f - P.f[0].xy; + let t = clamp(dot(r, P.f[0].zw) / P.f[1].x, 0.0, 1.0); + let pi = 3.141592653589793; + var b: f32; + switch P.u[0].x { + case 1u: { + b = 0.5 * sin(pi * t) * t; + } + case 2u: { + b = 0.5 * t; + } + case 3u: { + b = 0.25 * (1.0 - cos(pi * t)) - 0.1 * sin(2.0 * pi * t); + } + default: { + b = 0.5 * t * t; + } + } + let d = P.f[1].w * b; + textureStore(out, p, sample_bilinear(src, f.x - P.f[1].y * d, f.y - P.f[1].z * d)); +} + +// ---------------------------------------------------------------- CC Blobbylize (distort3::cc_blobbylize) +// aux = the blurred blob plane (x). f[0] = (depth, cut away, intensity, ambient); +// f[1] = (diffuse, specular, shininess, metal); f[2] = (light x, y, point light height, +// point light); f[3] = distant light direction; f[4] = light colour. + +@compute @workgroup_size(16, 16) +fn ftr_blob(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let depth = P.f[0].x; + let cut = P.f[0].y; + let hv = textureLoad(aux, p, 0).x; + let g = ftr_grad(p); + let f = ftr_centre(p); + let s = sample_bilinear(src, f.x - g.x * depth * 2.0, f.y - g.y * depth * 2.0); + let c = ftr_unpremul(s); + let a = max(s.w, 0.0); + let m = clamp((hv - cut) / max((1.0 - cut) * 0.5, 1e-3), 0.0, 1.0); + let alpha = max(a, s.w) * m; + if (alpha <= 0.0) { + textureStore(out, p, vec4(0.0)); + return; + } + let n = normalize(vec3(-g.x * depth, -g.y * depth, 1.0)); + var l = P.f[3].xyz; + if (P.f[2].w != 0.0) { + l = normalize(vec3(P.f[2].x - f32(p.x), P.f[2].y - f32(p.y), P.f[2].z)); + } + let intensity = P.f[0].z; + let ndl = max(dot(n, l), 0.0); + let h3 = normalize(vec3(l.x, l.y, l.z + 1.0)); + let sp = powz(max(dot(n, h3), 0.0), P.f[1].z) * P.f[1].y; + let lc = P.f[4].xyz; + let metal = P.f[1].w; + let lit = (P.f[0].w + P.f[1].x * ndl * intensity) * lc; + let spec_col = (1.0 - metal) * lc + metal * c * lc; + let o = c * lit + (sp * intensity) * spec_col; + textureStore(out, p, vec4(o * alpha, alpha)); +} + +// 0.5 + atan2(x, z) / 2π, kept on the side of the ±π seam that the sign of `x` puts it on (a +// GPU's approximate atan2 can land just across it, which wraps to the opposite edge of the +// texture). x = ±0 takes the CPU's −π (its zero is negative there). +fn ftr_lon_u(x: f32, z: f32) -> f32 { + let u = 0.5 + atan2(x, z) / 6.283185307179586; + if (x <= 0.0) { + return clamp(u, 0.0, 0.5); + } + return clamp(u, 0.5, 0.99999994); +} + +// ---------------------------------------------------------------- CC Cylinder / CC Sphere (perspective.rs) +// Shading (perspective::Shading): f[3] = (light direction, intensity); f[4] = (light colour, +// ambient); f[5] = (diffuse, specular, shininess); f[6] = half vector. The layer rectangle in +// f[1] = (x0, y0, width, height). + +fn ftr_shade(px: vec4, n: vec3) -> vec4 { + let c = ftr_unpremul(px); + let a = max(px.w, 0.0); + if (a <= 0.0) { + return vec4(0.0); + } + let nl = max(dot(n, P.f[3].xyz), 0.0); + var spec = 0.0; + if (nl > 0.0) { + spec = powz(max(dot(n, P.f[6].xyz), 0.0), P.f[5].z) * P.f[5].y; + } + let lc = P.f[4].xyz * P.f[3].w; + let o = c * (P.f[4].w + P.f[5].x * nl * lc) + spec * lc; + return vec4(o * a, a); +} + +// perspective::texel: the layer as a texture at (u, v), wrapping horizontally. +fn ftr_texel(u0: f32, v: f32) -> vec4 { + let u = fract_euclid(u0); + if (v < 0.0 || v > 1.0) { + return vec4(0.0); + } + let r = P.f[1]; + return sample_bilinear_clamped(src, r.x + u * r.z, r.y + v * r.w); +} + +fn ftr_over(top: vec4, bottom: vec4) -> vec4 { + return top + bottom * (1.0 - top.w); +} + +fn ftr_faces(mode: u32, front: vec4, back: vec4) -> vec4 { + if (mode == 1u) { + return front; + } + if (mode == 2u) { + return back; + } + return ftr_over(front, back); +} + +// Cylinder: u[0].x = render; f[0] = (cx, cy, radius, layer height); f[2].x = rotation. +@compute @workgroup_size(16, 16) +fn ftr_cylinder(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let d = ftr_centre(p) - P.f[0].xy; + let v = d.y / P.f[0].w + 0.5; + let s = d.x / P.f[0].z; + if (abs(s) >= 1.0) { + textureStore(out, p, vec4(0.0)); + return; + } + let tau = 6.283185307179586; + let th = asin(s); + let cz = cos(th); + let spin = P.f[2].x; + let front = ftr_shade(ftr_texel((th + spin) / tau + 0.5, v), vec3(s, 0.0, cz)); + let back = ftr_shade(ftr_texel((3.141592653589793 - th + spin) / tau + 0.5, v), vec3(-s, 0.0, cz)); + textureStore(out, p, ftr_faces(P.u[0].x, front, back)); +} + +// Sphere: u[0].x = render; f[0] = (cx, cy, radius); f[7..10] = the rotation matrix rows. +fn ftr_sphere_tex(q: vec3) -> vec4 { + let m0 = P.f[7].xyz; + let m1 = P.f[8].xyz; + let m2 = P.f[9].xyz; + let t = m0 * q.x + m1 * q.y + m2 * q.z; + let lat = asin(clamp(t.y, -1.0, 1.0)); + return ftr_texel(ftr_lon_u(t.x, t.z), lat / 3.141592653589793 + 0.5); +} + +@compute @workgroup_size(16, 16) +fn ftr_sphere(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let sxy = (ftr_centre(p) - P.f[0].xy) / P.f[0].z; + let d2 = sxy.x * sxy.x + sxy.y * sxy.y; + if (d2 >= 1.0) { + textureStore(out, p, vec4(0.0)); + return; + } + let z = sqrt(1.0 - d2); + let front = ftr_shade(ftr_sphere_tex(vec3(sxy, z)), vec3(sxy, z)); + let back = ftr_shade(ftr_sphere_tex(vec3(sxy, -z)), vec3(-sxy, z)); + textureStore(out, p, ftr_faces(P.u[0].x, front, back)); +} + +// ---------------------------------------------------------------- CC Spotlight (perspective::spotlight) +// u[0].x = light only; f[0] = (from x, y, height, intensity); f[1] = (axis, inner angle); +// f[2] = (colour, cone angle). + +@compute @workgroup_size(16, 16) +fn ftr_spot(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let d = ftr_centre(p) - P.f[0].xy; + let v = normalize(vec3(d, -P.f[0].z)); + let axis = P.f[1].xyz; + // acos(v · axis), well conditioned near the axis. + let ang = atan2(length(cross(v, axis)), dot(v, axis)); + let m = (1.0 - ftr_smoothstep(P.f[1].w, P.f[2].w, ang)) * P.f[0].w; + let px = textureLoad(src, p, 0); + if (P.u[0].x != 0u) { + let a = clamp(m, 0.0, 1.0); + textureStore(out, p, vec4(P.f[2].xyz * a, a)); + } else { + textureStore(out, p, vec4(px.xyz * (m * P.f[2].xyz), px.w)); + } +} + +// ---------------------------------------------------------------- CC Environment (perspective2::cc_environment) +// aux = the relief height map (x); data = the environment image (rows). u[0].x = mapping; +// f[0] = (1 / buffer scale, height, environment scale); f[1] = (environment offset, size). + +fn ftr_env_uv(mapping: u32, r: vec3) -> vec2 { + if (mapping == 1u) { + let m = 2.0 * max(sqrt(r.x * r.x + r.y * r.y + (r.z + 1.0) * (r.z + 1.0)), 1e-9); + return vec2(0.5 + r.x / m, 0.5 + r.y / m); + } + if (mapping == 2u) { + let ax = abs(r.x); + let ay = abs(r.y); + let az = abs(r.z); + var cr: vec4; + if (ax >= ay && ax >= az) { + if (r.x > 0.0) { + cr = vec4(2.0, 1.0, -r.z / ax, r.y / ax); + } else { + cr = vec4(0.0, 1.0, r.z / ax, r.y / ax); + } + } else if (ay >= az) { + if (r.y < 0.0) { + cr = vec4(1.0, 0.0, r.x / ay, -r.z / ay); + } else { + cr = vec4(1.0, 2.0, r.x / ay, r.z / ay); + } + } else if (r.z > 0.0) { + cr = vec4(1.0, 1.0, r.x / az, r.y / az); + } else { + cr = vec4(1.0, 3.0, r.x / az, -r.y / az); + } + return vec2((cr.x + (cr.z + 1.0) * 0.5) / 3.0, (cr.y + (cr.w + 1.0) * 0.5) / 4.0); + } + let lat = asin(clamp(r.y, -1.0, 1.0)); + return vec2(ftr_lon_u(r.x, r.z), 0.5 + lat / 3.141592653589793); +} + +@compute @workgroup_size(16, 16) +fn ftr_env(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let a = textureLoad(src, p, 0).w; + if (a <= 0.0) { + textureStore(out, p, vec4(0.0)); + return; + } + let g = ftr_grad(p) * P.f[0].x; + let height = P.f[0].y; + let n = normalize(vec3(-g.x * height, -g.y * height, 1.0)); + let d = -n.z; + let r = vec3(-2.0 * d * n.x, -2.0 * d * n.y, -(-1.0 - 2.0 * d * n.z)); + let uv = ftr_env_uv(P.u[0].x, r); + let es = P.f[0].z; + let ex = fract_euclid(uv.x) * P.f[1].z * es + P.f[1].x; + let ey = clamp(uv.y, 0.0, 1.0) * P.f[1].w * es + P.f[1].y; + let env = ftr_row_bilinear_clamped(ex, ey); + let ec = env.xyz * (1.0 / max(env.w, 1e-3)); + textureStore(out, p, vec4(ec * a, a)); +} + +// ---------------------------------------------------------------- 3D Glasses (perspective2::glasses_3d) + +// Translate by f[0].xy (perspective2::shift). +@compute @workgroup_size(16, 16) +fn ftr_shift(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let f = ftr_centre(p) - P.f[0].xy; + textureStore(out, p, sample_bilinear(src, f.x, f.y)); +} + +// src = left view, aux = right view. u[0].x = 3D view; f[0] = (width, height, balance). +@compute @workgroup_size(16, 16) +fn ftr_glasses(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!ftr_inside(p)) { + return; + } + let f = ftr_centre(p); + let w = P.f[0].x; + let h = P.f[0].y; + let k = P.f[0].z; + let view = P.u[0].x; + var o: vec4; + if (view == 0u) { + if (f.x < w * 0.5) { + o = sample_bilinear(src, f.x * 2.0, f.y); + } else { + o = sample_bilinear(aux, (f.x - w * 0.5) * 2.0, f.y); + } + } else if (view == 1u) { + if (f.y < h * 0.5) { + o = sample_bilinear(src, f.x, f.y * 2.0); + } else { + o = sample_bilinear(aux, f.x, (f.y - h * 0.5) * 2.0); + } + } else if (view == 2u) { + if (p.y % 2 == 0) { + o = textureLoad(src, p, 0); + } else { + o = textureLoad(aux, p, 0); + } + } else { + let lp = textureLoad(src, p, 0); + let rp = textureLoad(aux, p, 0); + let lc = ftr_unpremul(lp); + let rc = ftr_unpremul(rp); + let a = max(max(lp.w, 0.0), max(rp.w, 0.0)); + let ll = luminance(lc); + let rl = luminance(rc); + var c: vec3; + switch view { + case 3u: { + c = vec3(ll, rl, 0.0); + } + case 4u: { + c = vec3(ll, 0.0, rl); + } + case 5u: { + c = vec3(lc.x + (ll - lc.x) * k, rc.y, rc.z); + } + case 6u: { + c = vec3(ll * (1.0 - 0.5 * k) + lc.x * 0.5 * k, rl, 0.0); + } + case 7u: { + c = vec3(ll * (1.0 - 0.5 * k) + lc.x * 0.5 * k, 0.0, rl); + } + default: { + c = abs(lc - rc); + } + } + o = vec4(c * a, a); + } + textureStore(out, p, o); +} diff --git a/crates/gpu/src/tests_fx_text.rs b/crates/gpu/src/tests_fx_text.rs new file mode 100644 index 00000000..ab165e80 --- /dev/null +++ b/crates/gpu/src/tests_fx_text.rs @@ -0,0 +1,44 @@ +//! GPU Numbers and Timecode 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 pt(x: f64, y: f64) -> Value { + Value::Vec2([x, y]) +} + +#[test] +fn numbers() { + effect_case("ec.text.numbers", &[("format/value", n(42.5)), ("size", n(14.0))]); + effect_case( + "ec.text.numbers", + &[("format/value", n(-7.0)), ("size", n(12.0)), ("fillAndStroke/displayOptions", e(2)), ("fillAndStroke/strokeWidth", n(1.5)), ("tracking", n(2.0))], + ); + effect_case( + "ec.text.numbers", + &[ + ("format/value", n(3.25)), + ("size", n(16.0)), + ("fillAndStroke/displayOptions", e(3)), + ("fillAndStroke/fillColor", c(0.2, 0.6, 1.0)), + ("compositeOnOriginal", Value::Bool(false)), + ], + ); + effect_case("ec.text.numbers", &[("size", n(10.0)), ("fillAndStroke/displayOptions", e(1)), ("fillAndStroke/position", pt(20.0, 15.0))]); +} + +#[test] +fn timecode() { + effect_case("ec.text.timecode", &[("textSize", n(10.0)), ("textPosition", pt(35.0, 22.0))]); + effect_case( + "ec.text.timecode", + &[("textSize", n(9.0)), ("displayFormat", e(1)), ("renderOnOriginal", Value::Bool(false)), ("textColor", c(1.0, 0.8, 0.1))], + ); + effect_case("ec.text.timecode", &[("textSize", n(8.0)), ("showBox", Value::Bool(true)), ("boxColor", c(0.1, 0.1, 0.4)), ("opacity", n(70.0))]); +} diff --git a/crates/gpu/src/tests_fx_transition.rs b/crates/gpu/src/tests_fx_transition.rs new file mode 100644 index 00000000..ad6ebdf5 --- /dev/null +++ b/crates/gpu/src/tests_fx_transition.rs @@ -0,0 +1,157 @@ +//! GPU transition and perspective effects (Block Dissolve, the CC transitions, Radial Shadow, +//! CC Bender, CC Blobbylize, CC Cylinder, CC Sphere, CC Spotlight, CC Environment, 3D Glasses) +//! vs the CPU effects (the oracle): direct on a buffer at full and half resolution, as +//! adjustment, and composited at 8 and 32 bpc; effects reading other layers also with a layer +//! chosen. + +use effectcraft_keyframe::Value; +use effectcraft_project::BitDepth; +use effectcraft_render::RenderOpts; +use effectcraft_time::Tick; + +use crate::tests::{Scene, c, check, compare_at, effect_case, n, opts, set}; + +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 block_dissolve() { + effect_case("ec.transition.blockdissolve", &[("completion", n(40.0))]); + effect_case("ec.transition.blockdissolve", &[("completion", n(55.0)), ("blockWidth", n(7.0)), ("blockHeight", n(5.5)), ("feather", n(3.0))]); + effect_case("ec.transition.blockdissolve", &[("completion", n(30.0)), ("blockWidth", n(4.3)), ("blockHeight", n(9.0)), ("softEdges", Value::Bool(false))]); +} + +#[test] +fn grid_and_scale_wipes() { + effect_case("ec.transition.ccgridwipe", &[("completion", n(40.0)), ("center", pt(33.0, 21.0))]); + effect_case("ec.transition.ccgridwipe", &[("completion", n(60.0)), ("rotation", n(20.0)), ("border", n(3.0)), ("tiles", n(7.0)), ("shape", e(0))]); + effect_case("ec.transition.ccgridwipe", &[("completion", n(35.0)), ("shape", e(2)), ("reverse", on()), ("tiles", n(5.0))]); + effect_case("ec.transition.ccradialscalewipe", &[("completion", n(40.0)), ("center", pt(30.0, 20.0))]); + effect_case("ec.transition.ccradialscalewipe", &[("completion", n(30.0)), ("center", pt(40.0, 25.0)), ("reverse", on())]); + effect_case("ec.transition.ccscalewipe", &[("stretch", n(2.0)), ("center", pt(30.0, 20.0))]); + effect_case("ec.transition.ccscalewipe", &[("stretch", n(0.7)), ("direction", n(200.0))]); +} + +#[test] +fn gradient_driven_wipes() { + effect_case("ec.transition.ccglasswipe", &[("completion", n(45.0))]); + effect_case("ec.transition.ccglasswipe", &[("completion", n(30.0)), ("softness", n(40.0)), ("displacementAmount", n(25.0))]); + effect_case("ec.transition.ccimagewipe", &[("completion", n(50.0))]); + effect_case( + "ec.transition.ccimagewipe", + &[("completion", n(40.0)), ("property", e(5)), ("blur", n(4.0)), ("inverseGradient", on()), ("borderSoftness", n(20.0))], + ); + effect_case("ec.transition.ccimagewipe", &[("completion", n(60.0)), ("property", e(6)), ("autoSoftness", Value::Bool(false))]); + effect_case("ec.transition.ccwarpomatic", &[("completion", n(40.0))]); + effect_case("ec.transition.ccwarpomatic", &[("completion", n(55.0)), ("reactor", e(1)), ("warpDirection", e(1))]); + effect_case("ec.transition.ccwarpomatic", &[("completion", n(35.0)), ("reactor", e(2)), ("warpDirection", e(2)), ("blendSpan", n(30.0))]); +} + +#[test] +fn geometric_transitions() { + effect_case("ec.transition.ccjaws", &[("completion", n(30.0)), ("center", pt(33.0, 21.0))]); + for shape in 1..4 { + effect_case("ec.transition.ccjaws", &[("completion", n(20.0)), ("shape", e(shape)), ("direction", n(25.0)), ("width", n(13.0))]); + } + effect_case("ec.transition.cclinesweep", &[("completion", n(40.0))]); + effect_case( + "ec.transition.cclinesweep", + &[("completion", n(50.0)), ("direction", n(35.0)), ("slant", n(-12.0)), ("thickness", n(7.0)), ("flipDirection", on())], + ); + effect_case("ec.transition.cctwister", &[("completion", n(30.0)), ("center", pt(33.0, 21.0))]); + effect_case("ec.transition.cctwister", &[("completion", n(60.0)), ("axis", n(10.0)), ("shading", Value::Bool(false))]); +} + +#[test] +fn perspective() { + effect_case("ec.perspective.radialshadow", &[("lightSource", pt(20.0, 10.0)), ("projectionDistance", n(15.0))]); + effect_case( + "ec.perspective.radialshadow", + &[ + ("lightSource", pt(50.0, 15.0)), + ("softness", n(6.0)), + ("render", e(1)), + ("colorInfluence", n(60.0)), + ("resizeLayer", on()), + ("color", c(0.3, 0.0, 0.5)), + ], + ); + effect_case("ec.perspective.radialshadow", &[("shadowOnly", on()), ("opacity", n(80.0)), ("resizeLayer", on())]); + effect_case("ec.perspective.cccylinder", &[("position", pt(35.0, 22.0)), ("rotation", n(30.0))]); + effect_case("ec.perspective.cccylinder", &[("radius", n(60.0)), ("render", e(1)), ("lightDirection", n(70.0)), ("roughness", n(0.2))]); + effect_case("ec.perspective.cccylinder", &[("render", e(2)), ("lightHeight", n(20.0)), ("specular", n(60.0))]); + effect_case("ec.perspective.ccsphere", &[("offset", pt(35.0, 22.0)), ("radius", n(18.0)), ("rotationX", n(20.0)), ("rotationY", n(35.0))]); + effect_case("ec.perspective.ccsphere", &[("radius", n(20.0)), ("rotationZ", n(50.0)), ("render", e(2))]); + effect_case("ec.perspective.ccspotlight", &[("from", pt(15.0, 8.0)), ("to", pt(35.0, 22.0))]); + effect_case( + "ec.perspective.ccspotlight", + &[("from", pt(50.0, 5.0)), ("to", pt(30.0, 30.0)), ("coneAngle", n(35.0)), ("edgeSoftness", n(60.0)), ("render", e(1))], + ); + effect_case("ec.perspective.ccspotlight", &[("to", pt(35.0, 22.0)), ("edgeSoftness", n(0.0)), ("color", c(1.0, 0.7, 0.3)), ("intensity", n(150.0))]); + for mapping in 0..3 { + effect_case("ec.perspective.ccenvironment", &[("mapping", e(mapping)), ("height", n(40.0))]); + } + effect_case("ec.perspective.ccenvironment", &[("filterEnvironment", Value::Bool(false))]); + for view in 0..9 { + effect_case("ec.perspective.3dglasses", &[("view3d", e(view)), ("sceneConvergence", n(4.0)), ("verticalAlignment", n(1.5))]); + } + effect_case("ec.perspective.3dglasses", &[("units", e(1)), ("sceneConvergence", n(3.0)), ("leftRightSwap", on()), ("balance", n(12.0))]); +} + +#[test] +fn distort() { + effect_case("ec.distort.ccbender", &[("amount", n(40.0)), ("top", pt(35.0, 0.0)), ("base", pt(35.0, 44.0))]); + for style in 1..4 { + effect_case( + "ec.distort.ccbender", + &[("amount", n(-30.0)), ("style", e(style)), ("top", pt(20.0, 5.0)), ("base", pt(40.0, 40.0)), ("adjustToDistance", on())], + ); + } + effect_case("ec.distort.ccblobbylize", &[]); + effect_case( + "ec.distort.ccblobbylize", + &[("property", e(4)), ("softness", n(3.0)), ("cutAway", n(30.0)), ("lightType", e(1)), ("lightPosition", pt(20.0, 10.0))], + ); + effect_case("ec.distort.ccblobbylize", &[("property", e(5)), ("metal", n(30.0)), ("roughness", n(0.2))]); +} + +/// Effects reading another layer (gradient, reveal, backside, environment, stereo views). +#[test] +fn with_other_layers() { + for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { + let cases: Vec<(&str, Vec<(&str, Value)>)> = vec![ + ("ec.transition.ccglasswipe", vec![("completion", n(40.0)), ("gradientLayer", Value::Layer(None)), ("layerToReveal", Value::Layer(None))]), + ("ec.transition.ccimagewipe", vec![("completion", n(45.0)), ("layer", Value::Layer(None))]), + ("ec.transition.ccwarpomatic", vec![("completion", n(40.0)), ("reactorLayer", Value::Layer(None)), ("layerToReveal", Value::Layer(None))]), + ("ec.transition.cctwister", vec![("completion", n(70.0)), ("backside", Value::Layer(None))]), + ("ec.perspective.ccenvironment", vec![("environment", Value::Layer(None))]), + ("ec.perspective.3dglasses", vec![("view3d", e(5)), ("leftView", Value::Layer(None)), ("sceneConvergence", n(3.0))]), + ("ec.distort.ccblobbylize", vec![("blobLayer", Value::Layer(None))]), + ]; + for (id, vals) in cases { + let mut s = Scene::new(depth); + let bg = s.solid([0.15, 0.1, 0.2], 97, 61); + s.push(bg); + let mut other = s.footage(50, 36); + other.switches.video = false; + let oid = s.push(other); + let vals: Vec<(&str, Value)> = vals.into_iter().map(|(k, v)| (k, if v == Value::Layer(None) { Value::Layer(Some(oid.0)) } else { v })).collect(); + let mut l = s.footage(70, 44); + s.effect(&mut l, id, &vals); + set(&mut l, "transform/rotation", Value::Scalar(8.0)); + s.push(l); + let label = format!("{id} with a layer {depth:?}"); + check(&label, compare_at(&s, opts(), Tick::ZERO), 0.0); + check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0); + } + } +} diff --git a/docs/effects.md b/docs/effects.md index 37a4882d..a44dfb90 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**: 170 effects also run on the GPU compositor with identical results. +- **GPU**: 190 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). @@ -133,8 +133,8 @@ Effects are addressed by id (`ec..`) or display name in commands | Bezier Warp | `ec.distort.bezierwarp` | 13 | GPU | 32 | Implemented | | Bulge | `ec.distort.bulge` | 6 | GPU | 32 | Implemented | | CC Bend It | `ec.distort.ccbendit` | 4 | GPU | 32 | Implemented | -| CC Bender | `ec.distort.ccbender` | 5 | | 32 | Implemented | -| CC Blobbylize | `ec.distort.ccblobbylize` | 15 | | 32 | Implemented | +| CC Bender | `ec.distort.ccbender` | 5 | GPU | 32 | Implemented | +| CC Blobbylize | `ec.distort.ccblobbylize` | 15 | GPU | 32 | Implemented | | CC Flo Motion | `ec.distort.ccflomotion` | 7 | GPU | 32 | Implemented | | CC Griddler | `ec.distort.ccgriddler` | 5 | GPU | 32 | Implemented | | CC Lens | `ec.distort.cclens` | 3 | GPU | 32 | Implemented | @@ -303,15 +303,15 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| | 3D Camera Tracker | `ec.perspective.cameratracker` | 12 | | 32 | Implemented | -| 3D Glasses | `ec.perspective.3dglasses` | 8 | | 32 | Implemented | +| 3D Glasses | `ec.perspective.3dglasses` | 8 | GPU | 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 | -| CC Spotlight | `ec.perspective.ccspotlight` | 8 | | 32 | Implemented | +| CC Cylinder | `ec.perspective.cccylinder` | 12 | GPU | 32 | Implemented | +| CC Environment | `ec.perspective.ccenvironment` | 4 | GPU | 32 | Implemented | +| CC Sphere | `ec.perspective.ccsphere` | 14 | GPU | 32 | Implemented | +| CC Spotlight | `ec.perspective.ccspotlight` | 8 | GPU | 32 | Implemented | | Drop Shadow | `ec.perspective.dropshadow` | 6 | GPU | 32 | Implemented | -| Radial Shadow | `ec.perspective.radialshadow` | 9 | | 32 | Implemented | +| Radial Shadow | `ec.perspective.radialshadow` | 9 | GPU | 32 | Implemented | ## Simulation @@ -370,8 +370,8 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| -| Numbers | `ec.text.numbers` | 14 | | 32 | Implemented | -| Timecode | `ec.text.timecode` | 12 | | 32 | Implemented | +| Numbers | `ec.text.numbers` | 14 | GPU | 32 | Implemented | +| Timecode | `ec.text.timecode` | 12 | GPU | 32 | Implemented | ## Time @@ -390,17 +390,17 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| -| Block Dissolve | `ec.transition.blockdissolve` | 5 | | 32 | Implemented | -| CC Glass Wipe | `ec.transition.ccglasswipe` | 5 | | 32 | Implemented | -| 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 | +| Block Dissolve | `ec.transition.blockdissolve` | 5 | GPU | 32 | Implemented | +| CC Glass Wipe | `ec.transition.ccglasswipe` | 5 | GPU | 32 | Implemented | +| CC Grid Wipe | `ec.transition.ccgridwipe` | 7 | GPU | 32 | Implemented | +| CC Image Wipe | `ec.transition.ccimagewipe` | 7 | GPU | 32 | Implemented | +| CC Jaws | `ec.transition.ccjaws` | 6 | GPU | 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 | -| CC Twister | `ec.transition.cctwister` | 5 | | 32 | Implemented | -| CC WarpoMatic | `ec.transition.ccwarpomatic` | 8 | | 32 | Implemented | +| CC Line Sweep | `ec.transition.cclinesweep` | 5 | GPU | 32 | Implemented | +| CC Radial ScaleWipe | `ec.transition.ccradialscalewipe` | 3 | GPU | 32 | Implemented | +| CC Scale Wipe | `ec.transition.ccscalewipe` | 3 | GPU | 32 | Implemented | +| CC Twister | `ec.transition.cctwister` | 5 | GPU | 32 | Implemented | +| CC WarpoMatic | `ec.transition.ccwarpomatic` | 8 | GPU | 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 | GPU | 32 | Implemented |