From 5df9cde182c8bfce305ce023a6c8814000b756bc Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sun, 4 Oct 2026 01:33:54 -0400 Subject: [PATCH] M13.28: GPU pixel ports (fx_pixel2): CC Cross/Radial/Vector Blur, Reduce Interlace Flicker, CC Composite, Color Link, Cineon, HDR Compander/Highlight Compression, Grow Bounds, CC Overbrights, CC Block Load, CC Burn Film, CC Glass/Plastic, CC HexTile, CC Mr. Smoothie, Inner/Outer Key, CC Simple Wire Removal, Basic 3D Inner/Outer Key's trimap and Cleanup strokes, Color Link's sample and CC Glass/Plastic's light are shared with the CPU effects (CPU output unchanged: gen_golden). CC Plastic now reads its Bump Layer (it read the nonexistent bumpMap id). Co-Authored-By: Claude Opus 5.5 --- crates/effects/src/color2.rs | 23 +- crates/effects/src/keying2.rs | 102 ++- crates/effects/src/lib.rs | 25 +- crates/effects/src/stylize3.rs | 42 +- crates/gpu/src/context.rs | 2 + crates/gpu/src/effects.rs | 1 + crates/gpu/src/fx_key.rs | 2 +- crates/gpu/src/fx_pixel2.rs | 518 ++++++++++++++ crates/gpu/src/lib.rs | 5 +- crates/gpu/src/shaders/fx_pixel2.wgsl | 947 ++++++++++++++++++++++++++ crates/gpu/src/tests_fx_pixel2.rs | 245 +++++++ docs/effects.md | 42 +- 12 files changed, 1877 insertions(+), 77 deletions(-) create mode 100644 crates/gpu/src/fx_pixel2.rs create mode 100644 crates/gpu/src/shaders/fx_pixel2.wgsl create mode 100644 crates/gpu/src/tests_fx_pixel2.rs diff --git a/crates/effects/src/color2.rs b/crates/effects/src/color2.rs index 8f929874..71dee87b 100644 --- a/crates/effects/src/color2.rs +++ b/crates/effects/src/color2.rs @@ -716,13 +716,26 @@ fn sample_color(img: &Image, how: u32, clip: f64) -> ([f32; 3], f32) { /// Color Link: colours the layer with a colour sampled from the Source Layer (its source, no /// masks or effects) or, with no source layer, from the layer itself. -fn color_link(ctx: &EffectCtx, mut b: Buf) -> Buf { +/// Color Link's sampled colour (RGB) and alpha: from the Source Layer, or from `img` (the +/// layer's own pixels; `None` = the caller has not read them back) when no source is chosen. +/// `None` when the own pixels are needed but not given. +pub fn color_link_sample(ctx: &EffectCtx, img: Option<&Image>) -> Option<([f32; 3], f32)> { let how = ctx.params.e("sampleSource"); let clip = ctx.params.f("clip") / 100.0; - let (c, sa) = match ctx.layer_param("sourceLayer", false) { + Some(match ctx.layer_param("sourceLayer", false) { Some(lp) => sample_color(&lp.buf.img, how, clip), - None => sample_color(&b.img, how, clip), - }; + None => sample_color(img?, how, clip), + }) +} + +/// Color Link's Blending Mode. +pub fn color_link_mode(ctx: &EffectCtx) -> BlendMode { + LINK_MODES[(ctx.params.e("blendingMode") as usize).min(LINK_MODES.len() - 1)].1 +} + +fn color_link(ctx: &EffectCtx, mut b: Buf) -> Buf { + let how = ctx.params.e("sampleSource"); + let Some((c, sa)) = color_link_sample(ctx, Some(&b.img)) else { return b }; let op = (ctx.params.f("opacity") / 100.0).clamp(0.0, 1.0) as f32; let stencil = ctx.params.b("stencilOriginalAlpha"); if how >= 6 { @@ -737,7 +750,7 @@ fn color_link(ctx: &EffectCtx, mut b: Buf) -> Buf { }); return b; } - let mode = LINK_MODES[(ctx.params.e("blendingMode") as usize).min(LINK_MODES.len() - 1)].1; + let mode = color_link_mode(ctx); let src = [c[0] * op, c[1] * op, c[2] * op, op]; b.img.data.par_iter_mut().for_each(|px| { let a0 = px[3]; diff --git a/crates/effects/src/keying2.rs b/crates/effects/src/keying2.rs index 4467318b..07d14c6e 100644 --- a/crates/effects/src/keying2.rs +++ b/crates/effects/src/keying2.rs @@ -101,24 +101,40 @@ fn mask_sdf(ctx: &EffectCtx, b: &Buf, idx: &[u32]) -> Option { Some(pl) } -fn inner_outer_key(ctx: &EffectCtx, mut b: Buf) -> Buf { +/// Inner/Outer Key's geometry (shared with the GPU compositor, effectcraft-gpu `fx_pixel2`): +/// the trimap of the selected masks and the Cleanup brush strokes, in buffer pixels. +pub struct InnerOuterPlan { + /// 1 = known foreground, 0 = known background, 0.5 = unknown. + pub tri: Plane, + /// Some pixel is unknown (edge colours are estimated and decontaminated). + pub unknown: bool, + /// Blur radius of the local foreground / background colour estimates. + pub sigma: f64, + /// Cleanup strokes in order: (matte target, coverage per pixel; negative = beyond the brush). + pub strokes: Vec<(f32, Vec)>, + /// Edge Thin, Edge Feather (buffer px), Edge Threshold, Invert Extraction, Blend with Original. + pub thin: f64, + pub feather: f64, + pub threshold: f32, + pub invert: bool, + pub original: f32, +} + +/// [`InnerOuterPlan`] of an Inner/Outer Key instance on `b`'s geometry (`None` = no foreground +/// mask: the layer passes through). +pub fn inner_outer_plan(ctx: &EffectCtx, b: &Buf) -> Option { // Foreground (Inside) plus Additional Foreground 1–10; likewise for the background. let fg_idx: Vec = std::iter::once(ctx.params.e("foreground")).chain((1..=10).map(|k| ctx.params.e(&format!("additionalForeground/foreground{k}")))).collect(); let bg_idx: Vec = std::iter::once(ctx.params.e("background")).chain((1..=10).map(|k| ctx.params.e(&format!("additionalBackground/background{k}")))).collect(); let radius = (ctx.params.f("singleMaskHighlightRadius") * b.scale) as f32; - let thin = ctx.params.f("edgeThin") * b.scale; - let feather = ctx.params.f("edgeFeather") * b.scale; - let thr = (ctx.params.f("edgeThreshold") / 100.0) as f32; - let invert = ctx.params.b("invertExtraction"); - let orig = (ctx.params.f("blendWithOriginal") / 100.0) as f32; let (w, h) = (b.img.width as usize, b.img.height as usize); // Trimap: 1 = known foreground, 0 = known background, 0.5 = unknown. - let fg = mask_plane(ctx, &b, &fg_idx); - let bg = mask_plane(ctx, &b, &bg_idx); + let fg = mask_plane(ctx, b, &fg_idx); + let bg = mask_plane(ctx, b, &bg_idx); let tri: Plane = match (fg, bg) { - (None, _) => return b, + (None, _) => return None, (Some(f), Some(bk)) => f.zip_map(&bk, |f, bk| { if f > 0.5 { 1.0 @@ -132,7 +148,7 @@ fn inner_outer_key(ctx: &EffectCtx, mut b: Buf) -> Buf { if radius <= 0.0 { f } else { - let sdf = mask_sdf(ctx, &b, &fg_idx).unwrap_or(f); + let sdf = mask_sdf(ctx, b, &fg_idx).unwrap_or(f); sdf.map(|d| { if d > radius { 1.0 @@ -146,11 +162,50 @@ fn inner_outer_key(ctx: &EffectCtx, mut b: Buf) -> Buf { } }; let unknown = tri.data.par_iter().any(|&t| t > 0.25 && t < 0.75); + let sigma = (radius.max(4.0) as f64).max(w.min(h) as f64 * 0.02); + // Cleanup Foreground / Background: brush strokes along mask paths that paint the matte + // towards opaque / transparent (Brush Radius, Brush Pressure). + let mut strokes = Vec::new(); + for (side, target) in [("Foreground", 1.0f32), ("Background", 0.0)] { + for k in 1..=10 { + let g = format!("cleanup{side}/cleanup{side}{k}"); + let idx = ctx.params.e(&format!("{g}/path")); + let Some(m) = (idx > 0).then(|| ctx.env.masks.get(idx as usize - 1)).flatten() else { continue }; + let rad = (ctx.params.f(&format!("{g}/brushRadius")) * b.scale).max(0.5); + let pressure = (ctx.params.f(&format!("{g}/brushPressure")) / 100.0).clamp(0.0, 1.0) as f32; + let pts: Vec<[f64; 2]> = m.points.iter().map(|q| [q[0] * b.scale + b.offset[0], q[1] * b.scale + b.offset[1]]).collect(); + let cov: Vec = (0..w * h) + .into_par_iter() + .map(|i| { + let (x, y) = ((i % w) as f64 + 0.5, (i / w) as f64 + 0.5); + let d = dist_to_poly(&pts, m.closed, x, y); + if d < rad + 1.0 { ((rad - d) / rad.max(1.0) * 2.0).clamp(0.0, 1.0) as f32 * pressure } else { -1.0 } + }) + .collect(); + strokes.push((target, cov)); + } + } + Some(InnerOuterPlan { + tri, + unknown, + sigma, + strokes, + thin: ctx.params.f("edgeThin") * b.scale, + feather: ctx.params.f("edgeFeather") * b.scale, + threshold: (ctx.params.f("edgeThreshold") / 100.0) as f32, + invert: ctx.params.b("invertExtraction"), + original: (ctx.params.f("blendWithOriginal") / 100.0) as f32, + }) +} + +fn inner_outer_key(ctx: &EffectCtx, mut b: Buf) -> Buf { + let Some(plan) = inner_outer_plan(ctx, &b) else { return b }; + let InnerOuterPlan { tri, unknown, sigma, strokes, thin, feather, threshold: thr, invert, original: orig } = plan; + let (w, h) = (b.img.width as usize, b.img.height as usize); let mut alpha = tri.clone(); let mut colours: Option> = None; if unknown { // Local foreground / background colour estimates: normalised blurs over known regions. - let sigma = (radius.max(4.0) as f64).max(w.min(h) as f64 * 0.02); let known_f = tri.map(|t| if t > 0.75 { 1.0 } else { 0.0 }); let known_b = tri.map(|t| if t < 0.25 { 1.0 } else { 0.0 }); let straight: Vec<[f32; 3]> = b.img.data.par_iter().map(|&px| unpremul(px).0).collect(); @@ -190,25 +245,12 @@ fn inner_outer_key(ctx: &EffectCtx, mut b: Buf) -> Buf { .collect(), ); } - // Cleanup Foreground / Background: brush strokes along mask paths that paint the matte - // towards opaque / transparent (Brush Radius, Brush Pressure). - for (side, target) in [("Foreground", 1.0f32), ("Background", 0.0)] { - for k in 1..=10 { - let g = format!("cleanup{side}/cleanup{side}{k}"); - let idx = ctx.params.e(&format!("{g}/path")); - let Some(m) = (idx > 0).then(|| ctx.env.masks.get(idx as usize - 1)).flatten() else { continue }; - let rad = (ctx.params.f(&format!("{g}/brushRadius")) * b.scale).max(0.5); - let pressure = (ctx.params.f(&format!("{g}/brushPressure")) / 100.0).clamp(0.0, 1.0) as f32; - let pts: Vec<[f64; 2]> = m.points.iter().map(|q| [q[0] * b.scale + b.offset[0], q[1] * b.scale + b.offset[1]]).collect(); - alpha.data.par_iter_mut().enumerate().for_each(|(i, a)| { - let (x, y) = ((i % w) as f64 + 0.5, (i / w) as f64 + 0.5); - let d = dist_to_poly(&pts, m.closed, x, y); - if d < rad + 1.0 { - let cov = ((rad - d) / rad.max(1.0) * 2.0).clamp(0.0, 1.0) as f32 * pressure; - *a += (target - *a) * cov; - } - }); - } + for (target, cov) in &strokes { + alpha.data.par_iter_mut().zip(cov.par_iter()).for_each(|(a, &c)| { + if c >= 0.0 { + *a += (target - *a) * c; + } + }); } if thin.abs() > 0.01 { alpha = morph_frac(&alpha, thin.abs(), thin < 0.0); diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index ac84d942..37f792cd 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -71,7 +71,7 @@ pub use color_fx::{ HUESAT_CHANNELS, HUESAT_RANGES, LEVELS_CHANNELS, PHOTO_FILTER_CUSTOM, PHOTO_FILTERS, exposure_settings, fill_uses_masks, huesat_ranges_identity, levels_channel_ids, levels_channel_settings, levels_channels_identity, levels_clip, }; -pub use color2::{Curve, auto_correct_settings, equalize_tables, luma_clip_points, shadow_highlight_amounts}; +pub use color2::{Curve, auto_correct_settings, color_link_mode, color_link_sample, equalize_tables, luma_clip_points, shadow_highlight_amounts}; pub use color3::OffsetCurve; pub use distort::transform_shutter; pub use distort2::{MAGNIFY_MODES, bezier_grid, parse_mesh}; @@ -92,8 +92,10 @@ pub use generate2::pattern_kind as cell_pattern_kind; pub use noise::GrainLook; pub use transition::place_layer; // effectcraft-gpu fx_noise. +pub use keying2::{InnerOuterPlan, inner_outer_plan}; pub use noise::remove_grain_level; pub use noise3::{FractalGpu, fractal_gpu}; +pub use stylize3::{BumpLight, bump_layer_id}; pub use textfx::{TextLayer, text_layer}; pub use time_fx::{PixelMotion, TimewarpPlan, TwStep, posterized_time, time_frames, timewarp_crop, timewarp_plan}; // effectcraft-gpu fx_tone. @@ -1017,6 +1019,27 @@ pub const GPU_EFFECTS: &[&str] = &[ "ec.time.ccwidetime", "ec.time.pixelmotionblur", "ec.time.timewarp", + // effectcraft-gpu fx_pixel2 (pixel ports, part C) + "ec.blur.cccross", + "ec.blur.ccradial", + "ec.blur.ccvector", + "ec.blur.reduceflicker", + "ec.channel.cccomposite", + "ec.color.colorlink", + "ec.utility.cineon", + "ec.utility.hdrcompander", + "ec.utility.hdrcompression", + "ec.utility.growbounds", + "ec.utility.ccoverbrights", + "ec.stylize.ccblockload", + "ec.stylize.ccburnfilm", + "ec.stylize.ccglass", + "ec.stylize.cchextile", + "ec.stylize.ccmrsmoothie", + "ec.stylize.ccplastic", + "ec.key.innerouter", + "ec.key.ccsimplewireremoval", + "ec.obsolete.basic3d", ]; /// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated diff --git a/crates/effects/src/stylize3.rs b/crates/effects/src/stylize3.rs index df46c97e..403c325c 100644 --- a/crates/effects/src/stylize3.rs +++ b/crates/effects/src/stylize3.rs @@ -180,9 +180,14 @@ fn burn_film(ctx: &EffectCtx, mut b: Buf) -> Buf { const BUMP_PROPS: [&str; 6] = ["Red", "Green", "Blue", "Alpha", "Luminance", "Lightness"]; +/// The bump-map layer parameter: CC Glass's Bump Map, CC Plastic's Bump Layer. +pub fn bump_layer_id(ctx: &EffectCtx) -> &'static str { + if ctx.params.get("bumpLayer").is_some() { "bumpLayer" } else { "bumpMap" } +} + /// Height field (0..1) from the bump-map layer (or the layer itself), softened. fn height_field(ctx: &EffectCtx, b: &Buf) -> Plane { - let src = layer_or_self(ctx, b, "bumpMap", true, false); + let src = layer_or_self(ctx, b, bump_layer_id(ctx), true, false); let prop = ctx.params.e("property"); let mut pl = Plane::from_image(&src, |p| { let (c, a) = unpremul(p); @@ -203,30 +208,31 @@ fn height_field(ctx: &EffectCtx, b: &Buf) -> Plane { pl } -struct Light { - intensity: f32, - color: [f32; 3], - point: bool, - pos: (f64, f64), +/// The effect light and surface of CC Glass / CC Plastic (shared with the GPU compositor). +pub struct BumpLight { + pub intensity: f32, + pub color: [f32; 3], + pub point: bool, + pub pos: (f64, f64), /// Distant light direction (towards the light). - dir: [f32; 3], - height: f32, - ambient: f32, - diffuse: f32, - specular: f32, - shininess: f32, - metal: f32, + pub dir: [f32; 3], + pub height: f32, + pub ambient: f32, + pub diffuse: f32, + pub specular: f32, + pub shininess: f32, + pub metal: f32, } -impl Light { - fn from(ctx: &EffectCtx, b: &Buf) -> Light { +impl BumpLight { + pub fn from(ctx: &EffectCtx, b: &Buf) -> BumpLight { let pr = &ctx.params; let elev = (pr.f("lightHeight").clamp(-100.0, 100.0) / 100.0 * 90.0).to_radians(); let az = pr.f("lightDirection").to_radians(); let dir = [(az.sin() * elev.cos()) as f32, (-az.cos() * elev.cos()) as f32, elev.sin() as f32]; let c = pr.color("lightColor"); let rough = pr.f("roughness").clamp(0.001, 1.0) as f32; - Light { + BumpLight { intensity: (pr.f("lightIntensity") / 100.0) as f32, color: [c[0], c[1], c[2]], point: pr.e("lightType") == 1, @@ -288,7 +294,7 @@ fn glass(ctx: &EffectCtx, mut b: Buf) -> Buf { let height = ctx.params.f("height") as f32 / 100.0; let k = height * 25.0; let disp = ctx.params.f("displacement") * b.scale * 0.25; - let light = Light::from(ctx, &b); + let light = BumpLight::from(ctx, &b); let src = b.img.clone(); b.img.rows_mut().for_each(|(y, row)| { for (x, px) in row.iter_mut().enumerate() { @@ -321,7 +327,7 @@ fn plastic(ctx: &EffectCtx, mut b: Buf) -> Buf { hf = hf.map(|v| v.clamp(lo.min(hi), hi.max(lo))); } let k = ctx.params.f("height") as f32 / 100.0 * 25.0; - let light = Light::from(ctx, &b); + let light = BumpLight::from(ctx, &b); b.img.rows_mut().for_each(|(y, row)| { for (x, px) in row.iter_mut().enumerate() { if px[3] <= 0.0 { diff --git a/crates/gpu/src/context.rs b/crates/gpu/src/context.rs index 004eecf5..65d45594 100644 --- a/crates/gpu/src/context.rs +++ b/crates/gpu/src/context.rs @@ -324,6 +324,7 @@ impl GpuContext { include_str!("shaders/fx_transition.wgsl"), include_str!("shaders/fx_text.wgsl"), include_str!("shaders/fx_time.wgsl"), + include_str!("shaders/fx_pixel2.wgsl"), include_str!("shaders/sky.wgsl"), ] .concat(); @@ -392,6 +393,7 @@ impl GpuContext { .chain(crate::fx_transition::KERNELS) .chain(crate::fx_text::KERNELS) .chain(crate::fx_time::KERNELS) + .chain(crate::fx_pixel2::KERNELS) .chain(crate::adv3d::SKY_KERNELS) .map(|e| (e, &layout)) .chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext))) diff --git a/crates/gpu/src/effects.rs b/crates/gpu/src/effects.rs index 197d3bb8..2d09aaba 100644 --- a/crates/gpu/src/effects.rs +++ b/crates/gpu/src/effects.rs @@ -175,6 +175,7 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { _ 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), _ if crate::fx_time::IDS.contains(&id) => crate::fx_time::apply(e, id, ctx, b), + _ if crate::fx_pixel2::IDS.contains(&id) => crate::fx_pixel2::apply(e, id, ctx, b), _ => pointwise(e, id, ctx, b), } } diff --git a/crates/gpu/src/fx_key.rs b/crates/gpu/src/fx_key.rs index b3a3ecfe..1099bccd 100644 --- a/crates/gpu/src/fx_key.rs +++ b/crates/gpu/src/fx_key.rs @@ -88,7 +88,7 @@ fn src_index(s: Src) -> u32 { /// Layer parameter `id` resampled into the buffer's pixel grid (`util::fit_layer`): /// `Some(None)` when no layer is chosen (the effect then uses its own layer). -fn fitted(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str, masks_and_effects: bool, stretch: bool) -> Option> { +pub(crate) fn fitted(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str, masks_and_effects: bool, stretch: bool) -> Option> { let Some(o) = ctx.layer_param(id, masks_and_effects) else { return Some(None) }; let (w, h) = (b.img.width, b.img.height); if o.buf.img.is_empty() { diff --git a/crates/gpu/src/fx_pixel2.rs b/crates/gpu/src/fx_pixel2.rs new file mode 100644 index 00000000..57c2037f --- /dev/null +++ b/crates/gpu/src/fx_pixel2.rs @@ -0,0 +1,518 @@ +//! GPU effects, pixel ports of part C (kernels in `shaders/fx_pixel2.wgsl`, every entry point +//! prefixed `fp2_`): CC Cross Blur, CC Radial Blur, CC Vector Blur, Reduce Interlace Flicker, +//! CC Composite, Color Link, Cineon Converter, HDR Compander, HDR Highlight Compression, Grow +//! Bounds, CC Overbrights, CC Block Load, CC Burn Film, CC Glass, CC HexTile, CC Mr. Smoothie, +//! CC Plastic, Inner/Outer Key, CC Simple Wire Removal and Basic 3D, with the CPU effects' +//! exact steps (padding, box-blur radii, parameter conversions in f64 on the CPU). +//! +//! What only depends on parameters and geometry is prepared on the CPU and shared with the CPU +//! effect: Inner/Outer Key's trimap and Cleanup strokes (`effects::inner_outer_plan`), CC Glass / +//! Plastic's light (`effects::BumpLight`). Color Link's sample statistics (sorting, trimmed +//! means) run on the CPU over the read-back layer (`effects::color_link_sample`) unless a Source +//! Layer is chosen; the colouring runs here. + +use effectcraft_color::BlendMode; +use effectcraft_effects::EffectCtx; +use effectcraft_raster::Image; + +use crate::context::{Enc, GpuImage, Params}; +use crate::effects::{GBuf, box_passes, gaussian_blur}; +use crate::ops::mode_id; + +/// Compute entry points in `fx_pixel2.wgsl`. +pub(crate) const KERNELS: &[&str] = &[ + "fp2_cross", + "fp2_box", + "fp2_radial", + "fp2_plane", + "fp2_vector", + "fp2_point", + "fp2_block_means", + "fp2_block_load", + "fp2_burn", + "fp2_bump", + "fp2_hextile", + "fp2_smoothie", + "fp2_io_prep", + "fp2_io_known", + "fp2_io_est", + "fp2_io_strokes", + "fp2_io_final", + "fp2_wire", + "fp2_basic3d", +]; + +/// Effect ids implemented here. +pub(crate) const IDS: &[&str] = &[ + "ec.blur.cccross", + "ec.blur.ccradial", + "ec.blur.ccvector", + "ec.blur.reduceflicker", + "ec.channel.cccomposite", + "ec.color.colorlink", + "ec.utility.cineon", + "ec.utility.hdrcompander", + "ec.utility.hdrcompression", + "ec.utility.growbounds", + "ec.utility.ccoverbrights", + "ec.stylize.ccblockload", + "ec.stylize.ccburnfilm", + "ec.stylize.ccglass", + "ec.stylize.cchextile", + "ec.stylize.ccmrsmoothie", + "ec.stylize.ccplastic", + "ec.key.innerouter", + "ec.key.ccsimplewireremoval", + "ec.obsolete.basic3d", +]; + +/// 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.blur.cccross" => cross_blur(e, ctx, b), + "ec.blur.ccradial" => radial_blur(e, ctx, b), + "ec.blur.ccvector" => vector_blur(e, ctx, b), + "ec.blur.reduceflicker" => reduce_flicker(e, ctx, b), + "ec.utility.growbounds" => grow_bounds(e, ctx, b), + "ec.stylize.ccblockload" => block_load(e, ctx, b), + "ec.stylize.ccburnfilm" => burn_film(e, ctx, b), + "ec.stylize.ccglass" | "ec.stylize.ccplastic" => bump(e, id, ctx, b), + "ec.stylize.cchextile" => hextile(e, ctx, b), + "ec.stylize.ccmrsmoothie" => smoothie(e, ctx, b), + "ec.key.innerouter" => inner_outer(e, ctx, b), + "ec.key.ccsimplewireremoval" => wire_removal(e, ctx, b), + "ec.obsolete.basic3d" => basic_3d(e, ctx, b), + _ => point(e, id, ctx, b), + } +} + +/// A same-size per-pixel kernel over the buffer. +fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> Option { + let out = e.scratch(b.img.width, b.img.height); + e.pixels(entry, p, &b.img, aux, &out, data); + b.img = out; + Some(b) +} + +/// A one-value plane (x) of `img` (`fp2_plane` kind / property), softened by util::gauss_plane. +fn plane(e: &mut Enc, img: &GpuImage, kind: u32, prop: u32, soft: f64) -> GpuImage { + let mut p = Params::default(); + p.u[0] = [kind, prop, 0, 0]; + let pl = e.scratch(img.width, img.height); + e.pixels("fp2_plane", &p, img, None, &pl, None); + gauss_plane(e, &pl, soft, soft) +} + +/// util::gauss_plane on every channel: 3 box passes per axis (edges repeated), each window +/// summed directly (the CPU sums in f64; see `fp2_box`). +pub(crate) fn gauss_plane(e: &mut Enc, img: &GpuImage, sx: f64, sy: f64) -> GpuImage { + let mut cur = img.clone(); + for (vertical, sigma) in [(0, sx), (1, sy)] { + if sigma <= 0.05 { + continue; + } + for r in crate::effects::box_radii(sigma, 3) { + if r == 0 { + continue; + } + let mut p = Params::default(); + p.u[0] = [r as u32, vertical, 0, 0]; + let out = e.scratch(cur.width, cur.height); + e.pixels("fp2_box", &p, &cur, None, &out, None); + cur = out; + } + } + cur +} + +// ---------------------------------------------------------------- blurs (blur2, blur3) + +fn cross_blur(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let rx = (ctx.params.f("radiusX").max(0.0) * b.scale).round() as usize; + let ry = (ctx.params.f("radiusY").max(0.0) * b.scale).round() as usize; + if rx == 0 && ry == 0 { + return Some(b); + } + if !ctx.adjustment { + b.pad(e, (rx.max(ry) * 3) as u32 + 1)?; + } + let repeat = ctx.adjustment; + let hz = box_passes(e, &b.img, &[rx; 3], &[], repeat); + let vt = box_passes(e, &b.img, &[], &[ry; 3], repeat); + let mut p = Params::default(); + p.u[0][0] = ctx.params.e("transferMode"); + let out = e.scratch(b.img.width, b.img.height); + e.pixels("fp2_cross", &p, &hz, Some(&vt), &out, None); + b.img = out; + Some(b) +} + +fn radial_blur(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let amount = ctx.params.f("amount"); + if amount == 0.0 { + return Some(b); + } + let c = b.to_px(ctx.params.v2("center")); + let mut p = Params::default(); + p.u[0][0] = ctx.params.e("type"); + p.f[0] = [c.0 as f32, c.1 as f32, ((amount / 100.0).clamp(-1.0, 1.0) * 0.5) as f32, (amount.to_radians() * 0.25) as f32]; + p.f[1][0] = ctx.params.f("quality").clamp(1.0, 100.0) as f32; + run(e, "fp2_radial", &p, b, None, None) +} + +fn vector_blur(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let amount = ctx.params.f("amount").max(0.0) * b.scale; + if amount < 0.5 { + return Some(b); + } + let soft = ctx.params.f("mapSoftness").max(0.0) * 0.5 * b.scale; + let map = plane(e, &b.img, 0, 0, soft); + let mut p = Params::default(); + p.u[0][0] = ctx.params.e("type"); + p.f[0] = [amount as f32, ctx.params.f("angleOffset").to_radians() as f32, 0.0, 0.0]; + run(e, "fp2_vector", &p, b, Some(&map), None) +} + +fn reduce_flicker(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let s = ctx.params.f("softness").max(0.0) * b.scale; + if s < 0.05 { + return Some(b); + } + b.img = gaussian_blur(e, &b.img, 0.0, s, true); + Some(b) +} + +fn grow_bounds(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let px = (ctx.params.f("pixels") * b.scale).round().max(0.0) as u32; + if !ctx.adjustment { + b.pad(e, px)?; + } + Some(b) +} + +// ---------------------------------------------------------------- per-pixel colour effects + +/// channel2::cc_composite's Composite Original modes. +fn composite_mode(m: u32) -> BlendMode { + match m { + 2 => BlendMode::Add, + 3 => BlendMode::Multiply, + 4 => BlendMode::Screen, + 5 => BlendMode::Overlay, + 6 => BlendMode::SoftLight, + 7 => BlendMode::HardLight, + 8 => BlendMode::Darken, + 9 => BlendMode::Lighten, + 10 => BlendMode::Difference, + 11 => BlendMode::Hue, + 12 => BlendMode::Saturation, + 13 => BlendMode::Color, + 14 => BlendMode::Luminosity, + 15 => BlendMode::StencilAlpha, + _ => BlendMode::Normal, + } +} + +fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let mut p = Params::default(); + match id { + "ec.utility.cineon" => { + // utility::Cineon's offset and knee, in the CPU's f32. + let black = pr.f("tenBitBlackPoint") as f32; + let white = pr.f("tenBitWhitePoint") as f32; + let rolloff = pr.f("highlightRolloff") as f32; + let offset = |black: f32, white: f32| 10f32.powf((black - white) * 0.002 / 0.6); + let knee = 1.0 - (rolloff / 100.0).clamp(0.0, 0.99); + p.u[0] = [0, pr.e("conversionType"), 0, 0]; + p.f[0] = [offset(black, white), knee, white, pr.f("gamma") as f32]; + p.f[1] = [pr.f("internalBlackPoint") as f32, pr.f("internalWhitePoint") as f32, 0.0, 0.0]; + p.f[2] = [offset(95.0, 685.0), knee, 685.0, 1.7]; + } + "ec.utility.hdrcompander" => { + p.u[0] = [1, (pr.e("mode") == 1) as u32, 0, 0]; + p.f[0] = [pr.f("gain").max(1e-6) as f32, pr.f("gamma").max(1e-3) as f32, 0.0, 0.0]; + } + "ec.utility.hdrcompression" => { + let t = (pr.f("amount") / 100.0).clamp(0.0, 1.0) as f32; + if t <= 0.0 { + return Some(b); + } + p.u[0][0] = 2; + p.f[0][0] = 1.0 - 0.5 * t; + } + "ec.utility.ccoverbrights" => { + p.u[0] = [3, pr.e("channel"), 0, 0]; + p.f[0] = pr.color("highlightColor"); + } + "ec.channel.cccomposite" => { + let m = pr.e("compositeOriginal"); + let mode = if m == 1 { BlendMode::Normal } else { composite_mode(m) }; + p.u[0] = [6, (m == 1) as u32, mode_id(mode), pr.b("rgbOnly") as u32]; + p.f[0][0] = (pr.f("opacity") as f32 / 100.0).clamp(0.0, 1.0); + } + "ec.color.colorlink" => { + // The sample statistics run on the CPU (the layer's own pixels are read back). + let own = if ctx.layer_param("sourceLayer", false).is_none() { Some(e.download(&b.img)?) } else { None }; + let (c, sa) = effectcraft_effects::color_link_sample(ctx, own.as_ref())?; + let op = (pr.f("opacity") / 100.0).clamp(0.0, 1.0) as f32; + let stencil = pr.b("stencilOriginalAlpha") as u32; + if pr.e("sampleSource") >= 6 { + p.u[0] = [4, stencil, 0, 0]; + p.f[0] = [sa, op, 0.0, 0.0]; + } else { + p.u[0] = [5, stencil, mode_id(effectcraft_effects::color_link_mode(ctx)), 0]; + p.f[0] = [c[0] * op, c[1] * op, c[2] * op, op]; + } + } + _ => return None, + } + run(e, "fp2_point", &p, b, None, None) +} + +// ---------------------------------------------------------------- CC Block Load (stylize3) + +fn block_load(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let c = (ctx.params.f("completion") / 100.0).clamp(0.0, 1.0); + if c >= 1.0 { + return Some(b); + } + let (w, h) = (b.img.width as usize, b.img.height as usize); + let nlev = (w.max(h).max(2) as f64).log2().ceil() as u32; + let lp = c * (nlev + 1) as f64; + let done = lp.floor() as u32; + let frac = (lp - done as f64) as f32; + let size = |k: u32| 1usize << (nlev - k.min(nlev)); + let means = |e: &mut Enc, s: usize| { + let mut p = Params::default(); + p.u[0][0] = s as u32; + let g = e.scratch(w.div_ceil(s).max(1) as u32, h.div_ceil(s).max(1) as u32); + e.pixels("fp2_block_means", &p, &b.img, None, &g, None); + g + }; + let s_new = size(done); + let new = means(e, s_new); + let old = (done > 0).then(|| (size(done - 1), means(e, size(done - 1)))); + let flags = ctx.params.b("scanlines") as u32 + | (ctx.params.b("smoothing") as u32) << 1 + | (ctx.params.b("bilinear") as u32) << 2 + | (old.is_some() as u32) << 3 + | (ctx.params.b("startCleared") as u32) << 4; + let mut p = Params::default(); + p.u[0] = [s_new as u32, old.as_ref().map_or(1, |o| o.0 as u32), flags, 0]; + p.f[0] = [frac, h.div_ceil(s_new) as f32, 0.0, 0.0]; + let out = e.scratch(w as u32, h as u32); + e.pixels("fp2_block_load", &p, &new, old.as_ref().map(|o| &o.1), &out, None); + b.img = out; + Some(b) +} + +// ---------------------------------------------------------------- CC Burn Film (stylize3) + +fn burn_film(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let burn = (ctx.params.f("burn") / 100.0).clamp(0.0, 1.0) as f32; + if burn <= 0.0 { + return Some(b); + } + let c = b.to_px(ctx.params.v2("center")); + let seed = ctx.params.f("randomSeed").max(0.0) as u32; + let s = b.scale; + let diag = (ctx.layer_size[0].hypot(ctx.layer_size[1]) * s).max(1.0); + let feat = (60.0 * s).max(1e-3); + let mut p = Params::default(); + p.u[0][0] = seed.wrapping_add(17); + p.f[0] = [c.0 as f32, c.1 as f32, diag as f32, feat as f32]; + p.f[1] = [1.0 - burn * 1.25, 0.12 * (burn * 10.0).min(1.0), 0.0, 0.0]; + run(e, "fp2_burn", &p, b, None, None) +} + +// ---------------------------------------------------------------- CC Glass / CC Plastic (stylize3) + +fn bump(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { + let glass = id == "ec.stylize.ccglass"; + // stylize3::height_field. + let src = crate::fx_key::fitted(e, ctx, &b, effectcraft_effects::bump_layer_id(ctx), true, false)?.unwrap_or_else(|| b.img.clone()); + let soft = ctx.params.f("softness") * b.scale * 0.5; + let mut hf = plane(e, &src, 1, ctx.params.e("property"), soft); + let height = ctx.params.f("height") as f32 / 100.0; + let mut disp = 0.0; + if glass { + disp = (ctx.params.f("displacement") * b.scale * 0.25) as f32; + } else { + let lo = (ctx.params.f("cutMin") / 100.0) as f32; + let hi = (ctx.params.f("cutMax") / 100.0) as f32; + if lo > 0.0 || hi < 1.0 { + hf = clamp_plane(e, &hf, lo.min(hi), hi.max(lo)); + } + } + let l = effectcraft_effects::BumpLight::from(ctx, &geo(&b)); + let mut p = Params::default(); + p.u[0] = [glass as u32, l.point as u32, 0, 0]; + p.f[0] = [height * 25.0, disp, (height != 0.0) as u32 as f32, l.height]; + p.f[1] = [l.intensity, l.ambient, l.diffuse, l.specular]; + p.f[2] = [l.shininess, l.metal, l.pos.0 as f32, l.pos.1 as f32]; + p.f[3] = [l.color[0], l.color[1], l.color[2], 0.0]; + p.f[4] = [l.dir[0], l.dir[1], l.dir[2], 0.0]; + run(e, "fp2_bump", &p, b, Some(&hf), None) +} + +/// An empty CPU buffer with `b`'s geometry (for CPU helpers that only read positions). +fn geo(b: &GBuf) -> effectcraft_effects::Buf { + effectcraft_effects::Buf { img: Image { width: b.img.width, height: b.img.height, data: vec![] }, offset: b.offset, scale: b.scale } +} + +/// Clamp a plane (x) into [lo, hi] (Plane::map with f32::clamp). +fn clamp_plane(e: &mut Enc, pl: &GpuImage, lo: f32, hi: f32) -> GpuImage { + let mut p = Params::default(); + p.u[0] = [3, 0, 0, 0]; + p.f[0] = [lo, hi, 0.0, 0.0]; + let out = e.scratch(pl.width, pl.height); + e.pixels("fp2_plane", &p, pl, None, &out, None); + out +} + +// ---------------------------------------------------------------- CC HexTile / CC Mr. Smoothie (stylize3) + +fn hextile(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let r = (ctx.params.f("radius") * b.scale).max(0.5); + let c = b.to_px(ctx.params.v2("center")); + let (sn, cs) = ctx.params.f("rotate").to_radians().sin_cos(); + let mut p = Params::default(); + p.u[0][0] = ctx.params.e("render"); + p.f[0] = [c.0 as f32, c.1 as f32, r as f32, (ctx.params.f("smearing") / 100.0).clamp(0.0, 1.0) as f32]; + p.f[1] = [sn as f32, cs as f32, 0.0, 0.0]; + run(e, "fp2_hextile", &p, b, None, None) +} + +fn smoothie(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let flow = crate::fx_key::fitted(e, ctx, &b, "flowLayer", true, false)?.unwrap_or_else(|| b.img.clone()); + let sm = ctx.params.f("smoothness") * b.scale * 0.5; + let pl = plane(e, &flow, 2, ctx.params.e("property"), sm); + let a = b.to_px(ctx.params.v2("sampleA")); + let bb = b.to_px(ctx.params.v2("sampleB")); + let mut p = Params::default(); + p.f[0] = [a.0 as f32, a.1 as f32, bb.0 as f32, bb.1 as f32]; + p.f[1] = [(ctx.params.f("phase") / 360.0) as f32, (ctx.params.e("colorLoop") + 1) as f32, 0.0, 0.0]; + run(e, "fp2_smoothie", &p, b, Some(&pl), None) +} + +// ---------------------------------------------------------------- Inner/Outer Key (keying2) + +fn inner_outer(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let Some(plan) = effectcraft_effects::inner_outer_plan(ctx, &geo(&b)) else { return Some(b) }; + let (w, h) = (b.img.width, b.img.height); + let mut tri = Image::new(w, h); + for (px, &t) in tri.data.iter_mut().zip(&plan.tri.data) { + px[0] = t; + } + let tri = e.g.upload_image(&tri)?; + // The matte (x, or w of the estimate) and the estimate's colours. + let (mut matte, est) = if plan.unknown { + let t = e.scratch(w, h); + e.pixels("fp2_io_prep", &Params::default(), &b.img, Some(&tri), &t, None); + let known = |e: &mut Enc, side: u32| { + let mut p = Params::default(); + p.u[0][0] = side; + let k = e.scratch(w, h); + e.pixels("fp2_io_known", &p, &t, None, &k, None); + gauss_plane(e, &k, plan.sigma, plan.sigma) + }; + let fg = known(e, 0); + let bg = known(e, 1); + let (rows, stride) = e.image_rows(&t); + let mut p = Params::default(); + p.u[0][0] = stride; + let est = e.scratch(w, h); + e.pixels("fp2_io_est", &p, &fg, Some(&bg), &est, Some(&rows)); + (est.clone(), Some(est)) + } else { + (tri, None) + }; + let from_w = est.is_some() as u32; + if !plan.strokes.is_empty() || from_w != 0 { + let n = plan.strokes.len(); + let mut data = Vec::with_capacity(n + n * (w * h) as usize); + data.extend(plan.strokes.iter().map(|s| s.0)); + for (_, cov) in &plan.strokes { + data.extend_from_slice(cov); + } + let buf = e.data(&data); + let mut p = Params::default(); + p.u[0] = [n as u32, from_w, 0, 0]; + let out = e.scratch(w, h); + e.pixels("fp2_io_strokes", &p, &matte, None, &out, Some(&buf)); + matte = out; + } + if plan.thin.abs() > 0.01 { + let modes = if plan.thin < 0.0 { crate::fx_color::DILATE_X } else { crate::fx_color::ERODE_X }; + matte = crate::fx_color::morph_frac(e, &matte, plan.thin.abs(), modes); + } + if plan.feather > 0.05 { + matte = gauss_plane(e, &matte, plan.feather * 0.5, plan.feather * 0.5); + } + let mut p = Params::default(); + p.u[0] = [0, from_w, plan.invert as u32, 0]; + p.f[0] = [plan.threshold, plan.original, 0.0, 0.0]; + match est { + Some(est) => { + let (rows, stride) = e.image_rows(&est); + p.u[0][0] = stride; + run(e, "fp2_io_final", &p, b, Some(&matte), Some(&rows)) + } + None => run(e, "fp2_io_final", &p, b, Some(&matte), None), + } +} + +// ---------------------------------------------------------------- CC Simple Wire Removal (keying2) + +fn wire_removal(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let half = (ctx.params.f("thickness") * b.scale * 0.5).max(0.0); + if half <= 0.0 { + return Some(b); + } + let slope = (ctx.params.f("slope") / 100.0).clamp(0.0, 1.0) as f32; + let mirror = (ctx.params.f("mirrorBlend") / 100.0).clamp(0.0, 1.0) as f32; + let a = b.to_px(ctx.params.v2("pointA")); + let z = b.to_px(ctx.params.v2("pointB")); + let (dx, dy) = (z.0 - a.0, z.1 - a.1); + let len = (dx * dx + dy * dy).sqrt(); + if len < 1e-6 { + return Some(b); + } + let (tx, ty) = (dx / len, dy / len); + let mut p = Params::default(); + p.u[0][0] = ctx.params.e("removalStyle"); + p.f[0] = [a.0 as f32, a.1 as f32, -ty as f32, tx as f32]; + p.f[1] = [half as f32, (half * slope as f64) as f32, mirror, 0.0]; + run(e, "fp2_wire", &p, b, None, None) +} + +// ---------------------------------------------------------------- Basic 3D (obsolete) + +fn basic_3d(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let swivel = ctx.params.f("swivel").to_radians(); + let tilt = ctx.params.f("tilt").to_radians(); + let dist = ctx.params.f("distanceToImage") * b.scale; + let spec_on = ctx.params.b("specularHighlight"); + let wire = ctx.params.b("preview/drawPreviewWireframe"); + if swivel == 0.0 && tilt == 0.0 && dist == 0.0 && !spec_on && !wire { + return Some(b); + } + let (lx, ly, lw, lh) = (b.offset[0], b.offset[1], ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale); + let (cx, cy) = (lx + lw * 0.5, ly + lh * 0.5); + let f = lw.max(lh).max(1.0) * 1.4; + let (cs, ss) = (swivel.cos(), swivel.sin()); + let (ct, st) = (tilt.cos(), tilt.sin()); + let v3 = |v: [f64; 3]| [v[0] as f32, v[1] as f32, v[2] as f32, 0.0]; + let l = [-0.5f64, -0.6, -1.0]; + let k = 1.0 / (l[0] * l[0] + l[1] * l[1] + l[2] * l[2]).sqrt(); + let mut p = Params::default(); + p.u[0] = [spec_on as u32, wire as u32, 0, 0]; + p.f[0] = [cx as f32, cy as f32, f as f32, dist as f32]; + p.f[1] = v3([cs, 0.0, -ss]); + p.f[2] = v3([ss * st, ct, cs * st]); + p.f[3] = v3([ss * ct, -st, cs * ct]); + p.f[4] = v3(l.map(|v| v * k)); + p.f[5] = [(lw * 0.5) as f32, (lh * 0.5) as f32, 0.0, 0.0]; + run(e, "fp2_basic3d", &p, b, None, None) +} diff --git a/crates/gpu/src/lib.rs b/crates/gpu/src/lib.rs index 2c23f08c..b08a5843 100644 --- a/crates/gpu/src/lib.rs +++ b/crates/gpu/src/lib.rs @@ -23,7 +23,7 @@ //! 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_depth`, `fx_distort`, `fx_extra`, `fx_generate`, `fx_key`, `fx_light`, -//! `fx_lut`, `fx_noise`, `fx_sim`, `fx_stylize`, `fx_text`, `fx_time`, `fx_tone`, +//! `fx_lut`, `fx_noise`, `fx_pixel2`, `fx_sim`, `fx_stylize`, `fx_text`, `fx_time`, `fx_tone`, //! `fx_transition`, `fx_vr`, `fx_warp`); settings a kernel cannot match fall back to the CPU //! (`catalog::gpu_supported`, or `None` from the family's `apply`). //! The 3D Channel effects read the layer's aux channels as an extra texture (`fx_depth`), the @@ -64,6 +64,7 @@ mod fx_key; mod fx_light; mod fx_lut; mod fx_noise; +mod fx_pixel2; mod fx_sim; mod fx_stylize; mod fx_text; @@ -332,6 +333,8 @@ mod tests_fx_lut; #[cfg(test)] mod tests_fx_noise; #[cfg(test)] +mod tests_fx_pixel2; +#[cfg(test)] mod tests_fx_sim; #[cfg(test)] mod tests_fx_stylize; diff --git a/crates/gpu/src/shaders/fx_pixel2.wgsl b/crates/gpu/src/shaders/fx_pixel2.wgsl new file mode 100644 index 00000000..5916ed86 --- /dev/null +++ b/crates/gpu/src/shaders/fx_pixel2.wgsl @@ -0,0 +1,947 @@ +// GPU effects, pixel ports (part C): see src/fx_pixel2.rs. Every entry point is prefixed +// `fp2_` and mirrors the CPU effect operation for operation; positions are pixel centres +// relative to the effect's centre or anchor so f32 keeps its precision. + +fn fp2_inside(p: vec2) -> bool { + let d = out_dims(); + return p.x < d.x && p.y < d.y; +} + +// util::unpremul (colour 0 below alpha 1e-6). +fn fp2_unpremul(p: vec4) -> vec3 { + if (p.w > 1e-6) { + return p.xyz / p.w; + } + return vec3(0.0); +} + +// util::smoothstep. +fn fp2_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); +} + +// ---------------------------------------------------------------- CC Cross Blur (blur2::cross_blur) +// src = horizontal blur, aux = vertical blur; u[0].x = transfer mode. + +@compute @workgroup_size(16, 16) +fn fp2_cross(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let a = textureLoad(src, p, 0); + let v = textureLoad(aux, p, 0); + var o: vec4; + switch P.u[0].x { + case 1u: { + o = a + v; + } + case 2u: { + o = a + v - a * v; + } + case 3u: { + o = max(a, v); + } + default: { + o = (a + v) * 0.5; + } + } + o.w = clamp(o.w, 0.0, 1.0); + textureStore(out, p, o); +} + +// ---------------------------------------------------------------- CC Radial Blur (blur3::cc_radial_blur) +// u[0].x = type; f[0] = (cx, cy, zoom, arc); f[1].x = quality. + +@compute @workgroup_size(16, 16) +fn fp2_radial(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let kind = P.u[0].x; + let c = P.f[0].xy; + let zoom = P.f[0].z; + let arc = P.f[0].w; + let dx = f32(p.x) + 0.5 - c.x; + let dy = f32(p.y) + 0.5 - c.y; + let r = sqrt(dx * dx + dy * dy); + var span = r * abs(zoom); + if (kind >= 3u) { + span = r * abs(arc); + } + let n = u32(clamp(ceil(span * P.f[1].x / 50.0), 1.0, 256.0)); + if (n <= 1u) { + textureStore(out, p, textureLoad(src, p, 0)); + return; + } + var acc = vec4(0.0); + var wsum = 0.0; + for (var i = 0u; i < n; i++) { + let t = f32(i) / f32(n - 1u); + var s = vec2(0.0); + var w = 1.0; + switch kind { + case 0u: { + let k = 1.0 - zoom * t; + s = vec2(c.x + dx * k, c.y + dy * k); + } + case 1u: { + let k = 1.0 - zoom * t; + s = vec2(c.x + dx * k, c.y + dy * k); + w = (1.0 - t) + 0.05; + } + case 2u: { + let k = 1.0 + zoom * (t - 0.5); + s = vec2(c.x + dx * k, c.y + dy * k); + } + default: { + let a = arc * (t - 0.5); + let sn = sin(a); + let co = cos(a); + if (kind == 4u) { + w = (1.0 - abs(2.0 * t - 1.0)) + 0.05; + } + s = vec2(c.x + dx * co - dy * sn, c.y + dx * sn + dy * co); + } + } + acc += sample_bilinear(src, s.x, s.y) * w; + wsum += w; + } + textureStore(out, p, acc / wsum); +} + +// ---------------------------------------------------------------- util::gauss_plane +// One box pass (edges repeated) summing each window directly: the CPU's planes accumulate +// in f64, so a running f32 sum would lose the small values (normalised blurs divide them). +// u[0] = (radius, vertical). + +@compute @workgroup_size(16, 16) +fn fp2_box(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let r = i32(P.u[0].x); + var acc = vec4(0.0); + for (var i = -r; i <= r; i++) { + if (P.u[0].y != 0u) { + acc += tex_get_clamped(src, p.x, p.y + i); + } else { + acc += tex_get_clamped(src, p.x + i, p.y); + } + } + textureStore(out, p, acc * (1.0 / f32(2 * r + 1))); +} + +// ---------------------------------------------------------------- planes +// One value per pixel into x. u[0] = (kind, property): kind 0 = luminance of the premultiplied +// pixel (blur2::plum); 1 = CC Glass / Plastic height (stylize3::height_field: the property of +// the straight colour times alpha, alpha itself); 2 = CC Mr. Smoothie's flow property (straight); +// 3 = x clamped to [f[0].x, f[0].y] (CC Plastic's cut). + +@compute @workgroup_size(16, 16) +fn fp2_plane(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let px = textureLoad(src, p, 0); + let prop = P.u[0].y; + var v = 0.0; + if (P.u[0].x == 0u) { + v = luminance(px.xyz); + } else if (P.u[0].x == 3u) { + v = clamp(px.x, P.f[0].x, P.f[0].y); + } else { + let c = fp2_unpremul(px); + let a = px.w; + switch prop { + 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).z; + } + case 6u: { + v = rgb_to_hsl(c).x; + } + default: { + v = rgb_to_hsl(c).y; + } + } + if (P.u[0].x == 1u && prop != 3u) { + v = v * a; + } + } + textureStore(out, p, vec4(v, 0.0, 0.0, 0.0)); +} + +// ---------------------------------------------------------------- CC Vector Blur (blur2::vector_blur) +// aux = softened luminance map (x). u[0].x = type; f[0] = (amount, angle offset). + +fn fp2_map(x: i32, y: i32) -> f32 { + return tex_get_clamped(aux, x, y).x; +} + +@compute @workgroup_size(16, 16) +fn fp2_vector(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let d = vec2(textureDimensions(aux)); + let x = p.x; + let y = p.y; + let gx = (fp2_map(min(x + 1, d.x - 1), y) - fp2_map(max(x - 1, 0), y)) * 0.5; + let gy = (fp2_map(x, min(y + 1, d.y - 1)) - fp2_map(x, max(y - 1, 0))) * 0.5; + let mag = sqrt(gx * gx + gy * gy); + let m = fp2_map(x, y); + let kind = P.u[0].x; + let amount = P.f[0].x; + let off = P.f[0].y; + var ang = 0.0; + var len = amount; + if (kind <= 2u) { + if (mag < 1e-6) { + textureStore(out, p, textureLoad(src, p, 0)); + return; + } + let g = atan2(gy, gx); + var base = g + 1.5707963267948966; + if (kind == 2u) { + base = g; + } + if (kind != 1u) { + len = amount * min(mag * 10.0, 1.0); + } + ang = base + off; + } else { + ang = m * 6.283185307179586 + off; + if (kind != 3u) { + len = amount * m; + } + } + if (len < 0.5) { + textureStore(out, p, textureLoad(src, p, 0)); + return; + } + let dx = cos(ang) * len; + let dy = sin(ang) * len; + let n = u32(clamp(ceil(len * 2.0), 2.0, 48.0)); + var acc = vec4(0.0); + for (var i = 0u; i < n; i++) { + let t = f32(i) / f32(n - 1u) - 0.5; + acc += sample_bilinear_clamped(src, f32(x) + 0.5 + dx * t, f32(y) + 0.5 + dy * t) / f32(n); + } + textureStore(out, p, acc); +} + +// ---------------------------------------------------------------- per-pixel colour effects +// u[0].x = effect: 0 Cineon Converter, 1 HDR Compander, 2 HDR Highlight Compression, +// 3 CC Overbrights, 4 Color Link (alpha), 5 Color Link (blend), 6 CC Composite. + +// utility::Cineon::log_to_lin; c = (offset, knee, white, gamma), r = (internal black, white). +fn fp2_log_to_lin(v: f32, c: vec4, r: vec2) -> f32 { + let code = v * 1023.0; + var lin = (pow(10.0, (code - c.z) * 0.002 / 0.6) - c.x) / max(1.0 - c.x, 1e-6); + lin = powz(max(lin, 0.0), 1.7 / max(c.w, 0.01)); + let k = c.y; + if (k < 1.0 && lin > k) { + let w = 1.0 - k; + lin = k + w * (1.0 - exp(-(lin - k) / w)); + } + return r.x + (r.y - r.x) * lin; +} + +// utility::Cineon::lin_to_log. +fn fp2_lin_to_log(v: f32, c: vec4, r: vec2) -> f32 { + let d = r.y - r.x; + var lin = (v - r.x) / select(d, 1e-6, abs(d) < 1e-6); + let k = c.y; + if (k < 1.0 && lin > k) { + let w = 1.0 - k; + lin = k - w * log(max(1.0 - (lin - k) / w, 1e-6)); + } + lin = powz(max(lin, 0.0), max(c.w, 0.01) / 1.7); + let code = c.z + log(max(lin * (1.0 - c.x) + c.x, 1e-9)) * 0.4342944819032518 * 0.6 / 0.002; + return code / 1023.0; +} + +// utility2::spow. +fn fp2_spow(v: f32, e: f32) -> f32 { + return sign(v) * powz(abs(v), e); +} + +fn fp2_cineon(v: f32) -> f32 { + let c = P.f[0]; + let r = P.f[1].xy; + switch P.u[0].y { + case 0u: { + return fp2_lin_to_log(v, c, r); + } + case 1u: { + return fp2_log_to_lin(v, c, r); + } + default: { + return fp2_lin_to_log(fp2_log_to_lin(v, c, r), P.f[2], vec2(0.0, 1.0)); + } + } +} + +@compute @workgroup_size(16, 16) +fn fp2_point(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let px = textureLoad(src, p, 0); + var o = px; + switch P.u[0].x { + // f[0], f[1], f[2]: see fp2_cineon; u[0].y = conversion type. + case 0u: { + if (px.w > 0.0) { + let c = px.xyz / px.w; + o = vec4(vec3(fp2_cineon(c.x), fp2_cineon(c.y), fp2_cineon(c.z)) * px.w, px.w); + } + } + // u[0].y = expand; f[0] = (gain, gamma). + case 1u: { + if (px.w > 0.0) { + let c = fp2_unpremul(px); + let g = P.f[0].x; + let e = P.f[0].y; + var r = vec3(0.0); + if (P.u[0].y != 0u) { + r = vec3(fp2_spow(c.x, e), fp2_spow(c.y, e), fp2_spow(c.z, e)) * g; + } else { + r = vec3(fp2_spow(c.x / g, 1.0 / e), fp2_spow(c.y / g, 1.0 / e), fp2_spow(c.z / g, 1.0 / e)); + } + o = vec4(r * px.w, px.w); + } + } + // f[0].x = knee. + case 2u: { + if (px.w > 0.0) { + let c = px.xyz / px.w; + let m = max(max(c.x, c.y), c.z); + let knee = P.f[0].x; + if (m > knee) { + let w = 1.0 - knee; + let k = (knee + w * (m - knee) / ((m - knee) + w)) / m; + o = vec4(c * k * px.w, px.w); + } + } + } + // u[0].y = channel; f[0] = highlight colour. + case 3u: { + if (px.w > 0.0) { + let c = fp2_unpremul(px); + var v = max(max(c.x, c.y), c.z); + switch P.u[0].y { + case 1u: { + v = c.x; + } + case 2u: { + v = c.y; + } + case 3u: { + v = c.z; + } + case 4u: { + v = px.w; + } + case 5u: { + v = luminance(c); + } + default: {} + } + var r = vec3(v * 0.35); + if (v > 1.0) { + r = P.f[0].xyz; + } else if (v < 0.0) { + r = vec3(1.0) - P.f[0].xyz; + } + o = vec4(r * px.w, px.w); + } + } + // f[0] = (sampled alpha, opacity); u[0].y = stencil. + case 4u: { + let c = fp2_unpremul(px); + let a0 = select(max(px.w, 0.0), px.w, px.w > 1e-6); + var na = a0 + (P.f[0].x - a0) * P.f[0].y; + if (P.u[0].y != 0u) { + na = min(na, a0); + } + let a = clamp(na, 0.0, 1.0); + o = vec4(c * a, a); + } + // f[0] = premultiplied sampled colour; u[0] = (stencil, mode). + case 5u: { + o = blend_pixel(P.u[0].z, px, P.f[0], 0.5); + o.w = clamp(o.w, 0.0, 1.0); + if (P.u[0].y != 0u) { + o = vec4(fp2_unpremul(o) * px.w, px.w); + } + } + // u[0] = (_, composite original, mode, rgb only); f[0].x = opacity. + default: { + let orig = px * P.f[0].x; + if (P.u[0].y == 1u) { + o = blend_pixel(P.u[0].z, orig, px, 0.5); + } else { + o = blend_pixel(P.u[0].z, px, orig, 0.5); + } + o.w = clamp(o.w, 0.0, 1.0); + if (P.u[0].w != 0u) { + o = vec4(fp2_unpremul(o) * px.w, px.w); + } + } + } + textureStore(out, p, o); +} + +// ---------------------------------------------------------------- CC Block Load (stylize3::block_load) + +// Mean colour of every s×s block: one invocation per block (the CPU's summation order). +// u[0].x = s. +@compute @workgroup_size(16, 16) +fn fp2_block_means(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let s = i32(P.u[0].x); + let d = vec2(textureDimensions(src)); + var acc = vec4(0.0); + var n = 0.0; + for (var y = p.y * s; y < min((p.y + 1) * s, d.y); y++) { + for (var x = p.x * s; x < min((p.x + 1) * s, d.x); x++) { + acc += textureLoad(src, vec2(x, y), 0); + n += 1.0; + } + } + if (n > 0.0) { + acc = acc / n; + } + textureStore(out, p, acc); +} + +// stylize3::block_at on grid `t` (block size `s`). +fn fp2_block_at(t: texture_2d, s: u32, x: i32, y: i32, bilinear: bool) -> vec4 { + let g = vec2(textureDimensions(t)); + if (!bilinear || s == 1u) { + return textureLoad(t, vec2(min(x / i32(s), g.x - 1), min(y / i32(s), g.y - 1)), 0); + } + let fx = (f32(x) + 0.5) / f32(s) - 0.5; + let fy = (f32(y) + 0.5) / f32(s) - 0.5; + let x0 = floor(fx); + let y0 = floor(fy); + let tx = fx - x0; + let ty = fy - y0; + let xa = clamp(i32(x0), 0, g.x - 1); + let xb = clamp(i32(x0 + 1.0), 0, g.x - 1); + let ya = clamp(i32(y0), 0, g.y - 1); + let yb = clamp(i32(y0 + 1.0), 0, g.y - 1); + let a = textureLoad(t, vec2(xa, ya), 0); + let b = textureLoad(t, vec2(xb, ya), 0); + let c = textureLoad(t, vec2(xa, yb), 0); + let e = textureLoad(t, vec2(xb, yb), 0); + let top = a + (b - a) * tx; + let bot = c + (e - c) * tx; + return top + (bot - top) * ty; +} + +// src = the new level's grid, aux = the previous level's grid (or the new one). +// u[0] = (new block size, old block size, flags: 1 scanlines, 2 smoothing, 4 bilinear, +// 8 previous level exists, 16 start cleared); f[0] = (frac, rows of the new level). +@compute @workgroup_size(16, 16) +fn fp2_block_load(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let s_new = P.u[0].x; + let flags = P.u[0].z; + let scan = (flags & 1u) != 0u; + let smoothing = (flags & 2u) != 0u; + let bilinear = (flags & 4u) != 0u; + let frac = P.f[0].x; + let soft = select(0.0, 1.5, smoothing); + var t = 0.0; + if (scan) { + let front = frac * (P.f[0].y + soft) - soft * 0.5; + let r = f32(p.y / i32(s_new)) + 0.5; + if (soft > 0.0) { + t = fp2_smoothstep(r - soft * 0.5, r + soft * 0.5, front); + } else if (r < front) { + t = 1.0; + } + } else if (smoothing) { + t = frac; + } + var n = vec4(0.0); + if (t > 0.0) { + n = fp2_block_at(src, s_new, p.x, p.y, bilinear); + } + var prev = vec4(0.0); + if ((flags & 8u) != 0u) { + prev = fp2_block_at(aux, P.u[0].y, p.x, p.y, bilinear); + } else if ((flags & 16u) == 0u) { + prev = fp2_block_at(src, s_new, p.x, p.y, bilinear); + } + var o = prev; + if (t >= 1.0) { + o = n; + } else if (t > 0.0) { + o = prev + (n - prev) * t; + } + textureStore(out, p, o); +} + +// ---------------------------------------------------------------- CC Burn Film (stylize3::burn_film) +// u[0].x = seed + 17; f[0] = (cx, cy, diagonal, feature size); f[1] = (threshold, band). + +@compute @workgroup_size(16, 16) +fn fp2_burn(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let px = textureLoad(src, p, 0); + let fx = f32(p.x) + 0.5; + let fy = f32(p.y) + 0.5; + let dx = fx - P.f[0].x; + let dy = fy - P.f[0].y; + let d = sqrt(dx * dx + dy * dy) / P.f[0].z; + let n = fxs_fbm(fx / P.f[0].w, fy / P.f[0].w, 0.0, P.u[0].x, 4.0); + let f = n * 0.65 + (1.0 - min(d, 1.0)) * 0.35; + let th = P.f[1].x; + let band = P.f[1].y; + var o = px; + if (f > th + band) { + o = vec4(0.0); + } else if (f > th - band * 2.0) { + let c = fp2_unpremul(px); + let k = fp2_smoothstep(th - band * 2.0, th, f); + let hot = fp2_smoothstep(th, th + band, f); + let chr = 1.0 - k * 0.85; + var r = c * chr; + r = vec3(r.x + (1.0 - r.x) * hot * 0.9, r.y + (0.45 - r.y) * hot * 0.9, r.z * (1.0 - hot)); + let na = px.w * (1.0 - hot * 0.6); + o = vec4(r * na, na); + } + textureStore(out, p, o); +} + +// ---------------------------------------------------------------- CC Glass / CC Plastic (stylize3) +// aux = height field (x). u[0] = (glass, point light); f[0] = (height scale k, displacement, +// height != 0, light height); f[1] = (intensity, ambient, diffuse, specular); +// f[2] = (shininess, metal, light x, light y); f[3] = light colour; f[4] = distant direction. + +fn fp2_h(x: i32, y: i32) -> f32 { + return tex_get_clamped(aux, x, y).x; +} + +// stylize3::BumpLight::shade. +fn fp2_shade(c: vec3, n: vec3, l: vec3) -> vec3 { + let intensity = P.f[1].x; + let ambient = P.f[1].y; + let diffuse = P.f[1].z; + let specular = P.f[1].w; + let ndl = max(dot(n, l), 0.0); + let flat_z = max(l.z, 0.0); + let base = ambient + diffuse * flat_z; + var lit = 1.0; + if (base > 1e-4) { + lit = (ambient + diffuse * ndl) / base; + } + lit = 1.0 + (lit - 1.0) * intensity; + let hv = vec3(l.x, l.y, l.z + 1.0); + let hl = max(sqrt(hv.x * hv.x + hv.y * hv.y + hv.z * hv.z), 1e-6); + let ndh = max((n.x * hv.x + n.y * hv.y + n.z * hv.z) / hl, 0.0); + let spec = specular * intensity * powz(ndh, P.f[2].x); + let lc = P.f[3].xyz; + let metal = P.f[2].y; + let hl_col = lc * (1.0 - metal) + c * lc * metal; + return c * lit * (vec3(1.0) + (lc - vec3(1.0)) * min(intensity, 1.0)) + spec * hl_col; +} + +@compute @workgroup_size(16, 16) +fn fp2_bump(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let px = textureLoad(src, p, 0); + let glass = P.u[0].x != 0u; + if (px.w <= 0.0) { + textureStore(out, p, select(px, vec4(0.0), glass)); + return; + } + let k = P.f[0].x; + let gx = (fp2_h(p.x + 1, p.y) - fp2_h(p.x - 1, p.y)) * 0.5 * k; + let gy = (fp2_h(p.x, p.y + 1) - fp2_h(p.x, p.y - 1)) * 0.5 * k; + let ln = sqrt(gx * gx + gy * gy + 1.0); + let n = vec3(-gx / ln, -gy / ln, 1.0 / ln); + let fx = f32(p.x) + 0.5; + let fy = f32(p.y) + 0.5; + var l = P.f[4].xyz; + if (P.u[0].y != 0u) { + let v = vec3(P.f[2].z - fx, P.f[2].w - fy, max(P.f[0].w, 1.0)); + let vl = max(sqrt(v.x * v.x + v.y * v.y + v.z * v.z), 1e-6); + l = v / vl; + } + var c = fp2_unpremul(px); + if (glass) { + let disp = P.f[0].y; + if (disp != 0.0 && P.f[0].z != 0.0) { + let s = sample_bilinear_clamped(src, fx + n.x * disp, fy + n.y * disp); + if (s.w > 0.0) { + c = fp2_unpremul(s); + } + } + } + let o = fp2_shade(c, n, l); + textureStore(out, p, vec4(o * px.w, px.w)); +} + +// ---------------------------------------------------------------- CC HexTile (stylize3::hextile) +// u[0].x = render mode; f[0] = (cx, cy, radius, smearing); f[1] = (sin, cos). + +@compute @workgroup_size(16, 16) +fn fp2_hextile(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let cx = P.f[0].x; + let cy = P.f[0].y; + let r = P.f[0].z; + let smear = P.f[0].w; + let sn = P.f[1].x; + let cs = P.f[1].y; + let mode = P.u[0].x; + let dx = f32(p.x) + 0.5 - cx; + let dy = f32(p.y) + 0.5 - cy; + let u = dx * cs + dy * sn; + let v = -dx * sn + dy * cs; + // stylize3::hex_cell (cube rounding). + let q = (0.5773502691896257 * u - v / 3.0) / r; + let rr = (2.0 / 3.0 * v) / r; + let ccy = -q - rr; + var rx = round_away(q); + let ry = round_away(ccy); + var rz = round_away(rr); + let ddx = abs(rx - q); + let ddy = abs(ry - ccy); + let ddz = abs(rz - rr); + if (ddx > ddy && ddx > ddz) { + rx = -ry - rz; + } else if (ddy <= ddz) { + rz = -rx - ry; + } + let hx = r * 1.7320508075688772 * (rx + rz / 2.0); + let hy = r * 1.5 * rz; + var lu = u - hx; + var lv = v - hy; + let odd = ((i32(rx) + i32(rz)) % 2 + 2) % 2 == 1; + if ((mode == 1u || mode == 2u) && odd) { + lu = -lu; + } + if (mode == 2u && odd) { + lv = -lv; + } + let ox = lu * cs - lv * sn; + let oy = lu * sn + lv * cs; + let tx = cx + ox; + let ty = cy + oy; + let sx = tx + (f32(p.x) + 0.5 - tx) * smear; + let sy = ty + (f32(p.y) + 0.5 - ty) * smear; + textureStore(out, p, sample_bilinear_clamped(src, sx, sy)); +} + +// ---------------------------------------------------------------- CC Mr. Smoothie (stylize3::smoothie) +// aux = flow plane (x). f[0] = (ax, ay, bx, by); f[1] = (phase, loops). + +fn fp2_pal(i: f32) -> vec4 { + let t = i / 255.0; + let a = P.f[0].xy; + let b = P.f[0].zw; + let s = sample_bilinear_clamped(src, a.x + (b.x - a.x) * t, a.y + (b.y - a.y) * t); + return vec4(fp2_unpremul(s), 1.0); +} + +@compute @workgroup_size(16, 16) +fn fp2_smoothie(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let px = textureLoad(src, p, 0); + if (px.w <= 0.0) { + textureStore(out, p, px); + return; + } + let v = textureLoad(aux, p, 0).x; + var t = fract_euclid(v * P.f[1].y + P.f[1].x); + if (t < 0.5) { + t = t * 2.0; + } else { + t = 2.0 - t * 2.0; + } + let f = t * 255.0; + let i = min(floor(f), 254.0); + let a = fp2_pal(i); + let b = fp2_pal(i + 1.0); + let c = a + (b - a) * (f - i); + textureStore(out, p, vec4(c.xyz * px.w, px.w)); +} + +// ---------------------------------------------------------------- Inner/Outer Key (keying2) + +// T = (straight colour, trimap) from src = the layer and aux = the trimap (x). +@compute @workgroup_size(16, 16) +fn fp2_io_prep(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + textureStore(out, p, vec4(fp2_unpremul(textureLoad(src, p, 0)), textureLoad(aux, p, 0).x)); +} + +// A known region (u[0].x: 0 foreground, 1 background) of T: (colour × known, known). +@compute @workgroup_size(16, 16) +fn fp2_io_known(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let t = textureLoad(src, p, 0); + var m = select(0.0, 1.0, t.w > 0.75); + if (P.u[0].x == 1u) { + m = select(0.0, 1.0, t.w < 0.25); + } + textureStore(out, p, vec4(t.xyz * m, m)); +} + +// src = blurred foreground, aux = blurred background, data = T's rows (row length u[0].x): +// the estimated matte with the decontaminated colours (colour, alpha). +@compute @workgroup_size(16, 16) +fn fp2_io_est(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let at = (u32(p.y) * P.u[0].x + u32(p.x)) * 4u; + let c = vec3(data[at], data[at + 1u], data[at + 2u]); + let tri = data[at + 3u]; + var a = tri; + if (!(tri > 0.25 && tri < 0.75)) { + textureStore(out, p, vec4(c, a)); + return; + } + let fs = textureLoad(src, p, 0); + let bs = textureLoad(aux, p, 0); + let fc = fs.xyz / max(fs.w, 1e-6); + let bc = bs.xyz / max(bs.w, 1e-6); + let d = fc - bc; + let dd = d.x * d.x + d.y * d.y + d.z * d.z; + if (dd < 1e-6) { + a = 0.5; + } else { + a = clamp(((c.x - bc.x) * d.x + (c.y - bc.y) * d.y + (c.z - bc.z) * d.z) / dd, 0.0, 1.0); + } + var col = c; + if (a > 0.02) { + col = clamp((c - (1.0 - a) * bc) / a, vec3(0.0), vec3(1.0)); + } + textureStore(out, p, vec4(col, a)); +} + +// The matte (src.x, or src.w with u[0].y) after the Cleanup strokes: data = targets (u[0].x of +// them) then each stroke's coverage plane (negative = untouched). +@compute @workgroup_size(16, 16) +fn fp2_io_strokes(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let s = textureLoad(src, p, 0); + var a = select(s.x, s.w, P.u[0].y != 0u); + let n = P.u[0].x; + let d = out_dims(); + let plane = u32(d.x * d.y); + let i = u32(p.y * d.x + p.x); + for (var k = 0u; k < n; k++) { + let c = data[n + k * plane + i]; + if (c >= 0.0) { + a += (data[k] - a) * c; + } + } + textureStore(out, p, vec4(a, 0.0, 0.0, 0.0)); +} + +// src = the layer, aux = the final matte (x), data = estimated colours' rows (row length u[0].x, +// colours used when u[0].y). u[0].z = invert; f[0] = (threshold, blend with original). +@compute @workgroup_size(16, 16) +fn fp2_io_final(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let px = textureLoad(src, p, 0); + var m = textureLoad(aux, p, 0).x; + let thr = P.f[0].x; + if (thr > 0.0 && m < thr) { + m = 0.0; + } + if (P.u[0].z != 0u) { + m = 1.0 - m; + } + var c = fp2_unpremul(px); + if (P.u[0].y != 0u) { + let at = (u32(p.y) * P.u[0].x + u32(p.x)) * 4u; + c = vec3(data[at], data[at + 1u], data[at + 2u]); + } + let a = max(px.w, 0.0); + let keyed = vec4(c * (a * m), a * m); + textureStore(out, p, keyed + (px - keyed) * P.f[0].y); +} + +// ---------------------------------------------------------------- CC Simple Wire Removal (keying2::wire_removal) +// u[0].x = style; f[0] = (ax, ay, nx, ny); f[1] = (hw thickness, soft width, mirror). + +fn fp2_wire_pos(base: vec2, nrm: vec2, s: f32) -> vec4 { + return sample_bilinear_clamped(src, base.x + nrm.x * s, base.y + nrm.y * s); +} + +@compute @workgroup_size(16, 16) +fn fp2_wire(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let px = textureLoad(src, p, 0); + let a = P.f[0].xy; + let nrm = P.f[0].zw; + let hw = P.f[1].x; + let soft_w = P.f[1].y; + let mirror = P.f[1].z; + let fx = f32(p.x) + 0.5; + let fy = f32(p.y) + 0.5; + let q = vec2(fx - a.x, fy - a.y); + let d = q.x * nrm.x + q.y * nrm.y; + let ad = abs(d); + if (ad > hw + soft_w) { + textureStore(out, p, px); + return; + } + let base = vec2(fx - d * nrm.x, fy - d * nrm.y); + let edge = hw + 1.0; + var repl: vec4; + switch P.u[0].x { + case 0u: { + let t = clamp((d + edge) / (2.0 * edge), 0.0, 1.0); + let l = fp2_wire_pos(base, nrm, -edge); + repl = l + (fp2_wire_pos(base, nrm, edge) - l) * t; + } + case 1u, 2u: { + var s = d - 2.0 * (hw - ad) - 1.0; + if (d >= 0.0) { + s = d + 2.0 * (hw - ad) + 1.0; + } + let sh = fp2_wire_pos(base, nrm, s); + repl = sh; + if (mirror > 0.0) { + repl = sh + (fp2_wire_pos(base, nrm, -s) - sh) * (mirror * 0.5); + } + } + default: { + var sx = 0.0; + if (abs(nrm.x) > 1e-6) { + sx = (hw - ad + 1.0) / abs(nrm.x); + } + let dir = select(-1.0, 1.0, d * nrm.x >= 0.0); + let sh = sample_bilinear_clamped(src, fx + dir * sx, fy); + let mi = sample_bilinear_clamped(src, fx - dir * sx, fy); + repl = sh; + if (mirror > 0.0) { + repl = sh + (mi - sh) * (mirror * 0.5); + } + } + } + var k = 1.0; + if (ad > hw) { + k = 1.0 - fp2_smoothstep(hw, hw + soft_w, ad); + } + textureStore(out, p, px + (repl - px) * k); +} + +// ---------------------------------------------------------------- Basic 3D (obsolete::basic_3d) +// u[0] = (specular, wireframe); f[0] = (cx, cy, focal, distance); f[1] = ux; f[2] = uy; +// f[3] = normal; f[4] = light; f[5] = (hw width, hw height). + +@compute @workgroup_size(16, 16) +fn fp2_basic3d(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fp2_inside(p)) { + return; + } + let cx = P.f[0].x; + let cy = P.f[0].y; + let f = P.f[0].z; + let dist = P.f[0].w; + let ux = P.f[1].xyz; + let uy = P.f[2].xyz; + let n = P.f[3].xyz; + let d = vec3(f32(p.x) + 0.5 - cx, f32(p.y) + 0.5 - cy, f); + let denom = dot(d, n); + if (abs(denom) < 1e-9) { + textureStore(out, p, vec4(0.0)); + return; + } + // Eye at (0, 0, -f), plane through (0, 0, dist). + let t = (dist + f) * n.z / denom; + if (t <= 0.0) { + textureStore(out, p, vec4(0.0)); + return; + } + let hit = vec3(d.x * t, d.y * t, -f + d.z * t - dist); + let u = dot(hit, ux); + let v = dot(hit, uy); + var c = sample_bilinear(src, cx + u, cy + v); + if (P.u[0].x != 0u && c.w > 0.0) { + let dv = d / sqrt(dot(d, d)); + let k = 2.0 * dot(dv, n); + let r = dv - k * n; + let s = powz(max(-dot(r, P.f[4].xyz), 0.0), 40.0); + let a = c.w; + c = vec4(c.xyz + s * a * max(vec3(1.0) - c.xyz / max(a, 1e-6), vec3(0.0)), a); + } + if (P.u[0].y != 0u) { + let hw = P.f[5].x; + let hh = P.f[5].y; + let e = min(abs(abs(u) - hw), abs(abs(v) - hh)); + if (e < 0.75 && abs(u) <= hw + 0.75 && abs(v) <= hh + 0.75) { + c = vec4(1.0); + } + } + textureStore(out, p, c); +} diff --git a/crates/gpu/src/tests_fx_pixel2.rs b/crates/gpu/src/tests_fx_pixel2.rs new file mode 100644 index 00000000..3a85ac84 --- /dev/null +++ b/crates/gpu/src/tests_fx_pixel2.rs @@ -0,0 +1,245 @@ +//! GPU pixel ports of part C (CC Cross / Radial / Vector Blur, Reduce Interlace Flicker, CC +//! Composite, Color Link, Cineon Converter, HDR Compander, HDR Highlight Compression, Grow +//! Bounds, CC Overbrights, CC Block Load, CC Burn Film, CC Glass, CC HexTile, CC Mr. Smoothie, +//! CC Plastic, Inner/Outer Key, CC Simple Wire Removal, Basic 3D) vs the CPU effects (the +//! oracle): direct on a buffer at full and half resolution, as adjustment, and composited at 8 +//! and 32 bpc. Also the helpers for effects that read masks, other layers and audio. + +use effectcraft_effects::{Buf, EffectEnv, EffectHost, LayerPixels, MaskShape}; +use effectcraft_keyframe::{ShapePath, Value}; +use effectcraft_project::build::{self, Ids}; +use effectcraft_project::{BitDepth, MaskMode}; +use effectcraft_render::RenderOpts; +use effectcraft_time::Tick; + +use crate::tests::{Scene, c, check, compare_at, diff, effect_case, gpu, n, opts, pattern, set}; + +pub(crate) fn e(v: u32) -> Value { + Value::Enum(v) +} + +pub(crate) fn on() -> Value { + Value::Bool(true) +} + +pub(crate) fn off() -> Value { + Value::Bool(false) +} + +pub(crate) fn pt(x: f64, y: f64) -> Value { + Value::Vec2([x, y]) +} + +/// Size of the test layer (layer pixels). +const SIZE: [f64; 2] = [70.0, 44.0]; + +/// The test layer's masks (layer pixels): a closed pentagon, a closed quad and an open curve. +pub(crate) fn mask_points() -> Vec<(Vec<[f64; 2]>, bool)> { + let pent = (0..5) + .map(|i| { + let a = i as f64 * std::f64::consts::TAU / 5.0 - 1.3; + [33.3 + a.cos() * 17.1, 22.4 + a.sin() * 15.2] + }) + .collect(); + vec![ + (pent, true), + (vec![[6.3, 5.1], [62.2, 7.7], [59.4, 39.6], [9.1, 37.2]], true), + (vec![[4.2, 30.3], [18.7, 12.4], [33.1, 28.9], [47.6, 9.8], [64.2, 25.5]], false), + ] +} + +/// Another layer (`id` 9) and an audio track for layer parameters. +pub(crate) struct Host; +impl EffectHost for Host { + fn layer(&self, id: u64, _: bool) -> Option { + Some(LayerPixels { buf: Buf { img: pattern(id as u32 + 3, 70, 44), offset: [0.0; 2], scale: 1.0 }, size: SIZE }) + } + fn audio(&self, _: u64, start: f64, frames: usize, rate: u32) -> Option> { + Some( + (0..frames) + .flat_map(|i| { + let t = start + i as f64 / rate as f64; + let l = 0.5 * (t * 440.0 * std::f64::consts::TAU).sin() + 0.2 * (t * 1300.0 * std::f64::consts::TAU).sin(); + let r = 0.4 * (t * (200.0 + 300.0 * t) * std::f64::consts::TAU).sin(); + [l as f32, r as f32] + }) + .collect(), + ) + } +} + +/// The effect alone on a buffer with the test masks and [`Host`]: GPU vs CPU (32-bit float) +/// at full and half resolution (the half buffer padded and offset), direct and as adjustment. +/// `allow` = fraction of pixels allowed over 1e-3. +pub(crate) fn direct_env(id: &str, vals: &[(&str, Value)], allow: f64) { + let Some(g) = gpu() else { return }; + let spec = effectcraft_effects::find(id).unwrap(); + let mut params = + effectcraft_effects::Params { values: spec.params.iter().map(|ps| (ps.id.to_string(), effectcraft_effects::default_value(ps, SIZE))).collect() }; + for (k, v) in vals { + assert!(params.values.contains_key(*k), "{id}: no {k}"); + params.values.insert(k.to_string(), v.clone()); + } + let masks: Vec = mask_points() + .into_iter() + .enumerate() + .map(|(i, (points, closed))| MaskShape { name: format!("Mask {}", i + 1), points, closed, inverted: false }) + .collect(); + let host = Host; + for adjustment in [false, true] { + for scale in [1.0, 0.5] { + let img = pattern(7, (SIZE[0] * scale) as u32, (SIZE[1] * scale) as u32); + let mut buf = Buf { img, offset: [0.0, 0.0], scale }; + if scale < 1.0 { + buf.pad(3); + } + let env = EffectEnv { masks: &masks, host: Some(&host), comp_time: 0.25, frame_rate: 30.0, ..Default::default() }; + let ctx = || effectcraft_effects::EffectCtx { params: ¶ms, time: 0.25, layer_size: SIZE, seed: 11, adjustment, env }; + let cpu = (spec.render)(&ctx(), buf.clone()); + let out = + effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).expect("the GPU runs the effect"); + assert_eq!((out.offset, out.scale), (cpu.offset, cpu.scale), "{id}: geometry"); + let d = diff(&cpu.img, &out.img, 1e-3); + assert!( + d.over as f64 <= allow * d.total as f64, + "{id} {vals:?} adj {adjustment} scale {scale}: {} of {} pixels over 1e-3 (max {}); worst {:?}", + d.over, + d.total, + d.max, + d.worst + ); + } + } +} + +/// [`direct_env`], then a footage layer with the test masks (mode None) and the effect, +/// composited over a background at 8 and 32 bpc, full and half resolution. +pub(crate) fn env_case(id: &str, vals: &[(&str, Value)]) { + env_case_allow(id, vals, 0.0); +} + +/// [`env_case`] allowing a fraction of pixels over the tolerance (documented exceptions). +pub(crate) fn env_case_allow(id: &str, vals: &[(&str, Value)], allow: f64) { + direct_env(id, vals, allow); + for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { + let mut s = Scene::new(depth); + let bg = s.solid([0.15, 0.1, 0.2], 97, 61); + s.push(bg); + let mut l = s.footage(70, 44); + let mut next = s.p.next_id; + for (i, (pts, closed)) in mask_points().into_iter().enumerate() { + let m = build::mask(&mut Ids(&mut next), &format!("Mask {}", i + 1), ShapePath::polygon(&pts, closed), MaskMode::None, [255, 255, 0]); + l.props.sub_mut("masks").unwrap().children.push(m.into()); + } + s.p.next_id = next; + s.effect(&mut l, id, vals); + set(&mut l, "transform/rotation", Value::Scalar(8.0)); + s.push(l); + check(&format!("{id} {vals:?} {depth:?}"), compare_at(&s, opts(), Tick::ZERO), allow); + check(&format!("{id} {vals:?} {depth:?} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), allow); + } +} + +#[test] +fn blurs() { + effect_case("ec.blur.cccross", &[]); + effect_case("ec.blur.cccross", &[("radiusX", n(4.0)), ("radiusY", n(0.0)), ("transferMode", e(1))]); + effect_case("ec.blur.cccross", &[("radiusX", n(3.0)), ("radiusY", n(7.0)), ("transferMode", e(2))]); + effect_case("ec.blur.cccross", &[("radiusX", n(6.0)), ("radiusY", n(2.0)), ("transferMode", e(3))]); + effect_case("ec.blur.ccradial", &[]); + effect_case("ec.blur.ccradial", &[("type", e(0)), ("amount", n(-40.0)), ("center", pt(20.0, 30.0))]); + effect_case("ec.blur.ccradial", &[("type", e(2)), ("amount", n(60.0)), ("quality", n(20.0))]); + effect_case("ec.blur.ccradial", &[("type", e(3)), ("amount", n(30.0))]); + effect_case("ec.blur.ccradial", &[("type", e(4)), ("amount", n(-50.0)), ("quality", n(80.0))]); + effect_case("ec.blur.ccvector", &[]); + effect_case("ec.blur.ccvector", &[("type", e(1)), ("amount", n(6.0)), ("angleOffset", n(30.0))]); + effect_case("ec.blur.ccvector", &[("type", e(2)), ("mapSoftness", n(0.0))]); + effect_case("ec.blur.ccvector", &[("type", e(3)), ("amount", n(5.0))]); + effect_case("ec.blur.ccvector", &[("type", e(4)), ("amount", n(12.0)), ("mapSoftness", n(40.0))]); + effect_case("ec.blur.reduceflicker", &[("softness", n(3.0))]); + effect_case("ec.blur.reduceflicker", &[("softness", n(0.5))]); +} + +#[test] +fn composites_and_colour() { + for m in [0, 1, 2, 3, 5, 8, 11, 14, 15] { + effect_case("ec.channel.cccomposite", &[("compositeOriginal", e(m)), ("opacity", n(70.0))]); + } + effect_case("ec.channel.cccomposite", &[("compositeOriginal", e(4)), ("rgbOnly", on())]); + effect_case("ec.color.colorlink", &[]); + effect_case("ec.color.colorlink", &[("sampleSource", e(2)), ("clip", n(10.0)), ("blendingMode", e(3)), ("opacity", n(60.0))]); + effect_case("ec.color.colorlink", &[("sampleSource", e(1)), ("blendingMode", e(11)), ("stencilOriginalAlpha", off())]); + effect_case("ec.color.colorlink", &[("sampleSource", e(9)), ("clip", n(20.0))]); + effect_case("ec.color.colorlink", &[("sampleSource", e(6)), ("opacity", n(50.0)), ("stencilOriginalAlpha", off())]); + env_case("ec.color.colorlink", &[("sourceLayer", Value::Layer(Some(9))), ("sampleSource", e(4)), ("clip", n(5.0))]); + effect_case("ec.utility.cineon", &[]); + effect_case("ec.utility.cineon", &[("conversionType", e(0)), ("gamma", n(2.2))]); + effect_case("ec.utility.cineon", &[("conversionType", e(2)), ("highlightRolloff", n(60.0)), ("tenBitWhitePoint", n(800.0))]); + effect_case("ec.utility.hdrcompander", &[("gain", n(4.0)), ("gamma", n(2.0))]); + effect_case("ec.utility.hdrcompander", &[("mode", e(1)), ("gain", n(3.0)), ("gamma", n(1.5))]); + effect_case("ec.utility.hdrcompression", &[]); + effect_case("ec.utility.hdrcompression", &[("amount", n(40.0))]); + effect_case("ec.utility.growbounds", &[("pixels", n(9.0))]); + effect_case("ec.utility.ccoverbrights", &[]); + for ch in 1..=5 { + effect_case("ec.utility.ccoverbrights", &[("channel", e(ch)), ("highlightColor", c(0.2, 0.9, 0.4))]); + } +} + +#[test] +fn stylize() { + for (done, flags) in + [(30.0, [true, true, true, false]), (55.0, [false, true, false, true]), (70.0, [true, false, false, false]), (10.0, [false, false, true, true])] + { + effect_case( + "ec.stylize.ccblockload", + &[ + ("completion", n(done)), + ("scanlines", Value::Bool(flags[0])), + ("smoothing", Value::Bool(flags[1])), + ("startCleared", Value::Bool(flags[2])), + ("bilinear", Value::Bool(flags[3])), + ], + ); + } + effect_case("ec.stylize.ccburnfilm", &[("burn", n(30.0))]); + effect_case("ec.stylize.ccburnfilm", &[("burn", n(60.0)), ("center", pt(20.0, 15.0)), ("randomSeed", n(4.0))]); + effect_case("ec.stylize.ccglass", &[]); + effect_case("ec.stylize.ccglass", &[("lightType", e(1)), ("displacement", n(-60.0)), ("property", e(4)), ("softness", n(4.0))]); + effect_case("ec.stylize.ccglass", &[("height", n(0.0)), ("lightColor", c(1.0, 0.8, 0.5)), ("metal", n(60.0))]); + effect_case("ec.stylize.ccplastic", &[]); + effect_case("ec.stylize.ccplastic", &[("lightType", e(1)), ("cutMin", n(20.0)), ("cutMax", n(70.0)), ("property", e(5))]); + env_case("ec.stylize.ccplastic", &[("bumpLayer", Value::Layer(Some(9))), ("property", e(0)), ("roughness", n(0.2))]); + effect_case("ec.stylize.cchextile", &[("radius", n(9.3))]); + effect_case("ec.stylize.cchextile", &[("radius", n(7.7)), ("render", e(1)), ("rotate", n(17.0)), ("smearing", n(30.0))]); + effect_case("ec.stylize.cchextile", &[("radius", n(12.1)), ("render", e(2)), ("center", pt(31.3, 19.7))]); + effect_case("ec.stylize.ccmrsmoothie", &[]); + effect_case("ec.stylize.ccmrsmoothie", &[("property", e(6)), ("smoothness", n(5.0)), ("colorLoop", e(2)), ("phase", n(40.0))]); + env_case("ec.stylize.ccmrsmoothie", &[("flowLayer", Value::Layer(Some(9))), ("property", e(1))]); +} + +#[test] +fn keys_and_3d() { + env_case("ec.key.innerouter", &[]); + env_case("ec.key.innerouter", &[("foreground", e(1))]); + direct_env("ec.key.innerouter", &[("foreground", e(1)), ("background", e(2))], 0.0); + env_case("ec.key.innerouter", &[("foreground", e(1)), ("background", e(2)), ("edgeFeather", n(3.0)), ("edgeThin", n(1.5))]); + env_case("ec.key.innerouter", &[("foreground", e(1)), ("singleMaskHighlightRadius", n(4.0)), ("invertExtraction", on()), ("blendWithOriginal", n(20.0))]); + env_case( + "ec.key.innerouter", + &[ + ("foreground", e(2)), + ("edgeThin", n(-2.3)), + ("edgeThreshold", n(30.0)), + ("cleanupForeground/cleanupForeground1/path", e(3)), + ("cleanupBackground/cleanupBackground1/path", e(1)), + ], + ); + effect_case("ec.key.ccsimplewireremoval", &[]); + effect_case("ec.key.ccsimplewireremoval", &[("removalStyle", e(0)), ("thickness", n(8.0)), ("slope", n(50.0))]); + effect_case("ec.key.ccsimplewireremoval", &[("removalStyle", e(1)), ("mirrorBlend", n(60.0)), ("pointB", pt(60.0, 10.0))]); + effect_case("ec.key.ccsimplewireremoval", &[("removalStyle", e(3)), ("mirrorBlend", n(30.0)), ("slope", n(20.0))]); + effect_case("ec.obsolete.basic3d", &[("swivel", n(25.0))]); + effect_case("ec.obsolete.basic3d", &[("tilt", n(-30.0)), ("swivel", n(10.0)), ("specularHighlight", on())]); + effect_case("ec.obsolete.basic3d", &[("distanceToImage", n(20.0)), ("preview/drawPreviewWireframe", on()), ("tilt", n(15.0))]); +} diff --git a/docs/effects.md b/docs/effects.md index 0b58947d..f762eb06 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**: 236 effects also run on the GPU compositor with identical results. +- **GPU**: 256 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). @@ -47,10 +47,10 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| | Bilateral Blur | `ec.blur.bilateral` | 3 | GPU | 32 | Implemented | -| CC Cross Blur | `ec.blur.cccross` | 3 | | 32 | Implemented | -| CC Radial Blur | `ec.blur.ccradial` | 4 | | 32 | Implemented | +| CC Cross Blur | `ec.blur.cccross` | 3 | GPU | 32 | Implemented | +| CC Radial Blur | `ec.blur.ccradial` | 4 | GPU | 32 | Implemented | | CC Radial Fast Blur | `ec.blur.ccradialfast` | 3 | GPU | 32 | Implemented | -| CC Vector Blur | `ec.blur.ccvector` | 4 | | 32 | Implemented | +| CC Vector Blur | `ec.blur.ccvector` | 4 | GPU | 32 | Implemented | | Camera Lens Blur | `ec.blur.cameralens` | 16 | GPU | 32 | Implemented | | Camera-Shake Deblur | `ec.blur.camerashakedeblur` | 8 | | 32 | Implemented | | Channel Blur | `ec.blur.channel` | 6 | GPU | 32 | Implemented | @@ -69,7 +69,7 @@ Effects are addressed by id (`ec..`) or display name in commands |---|---|---:|:-:|:-:|---| | Arithmetic | `ec.channel.arithmetic` | 5 | GPU | 32 | Implemented | | Blend | `ec.channel.blend` | 4 | GPU | 32 | Implemented | -| CC Composite | `ec.channel.cccomposite` | 3 | | 32 | Implemented | +| CC Composite | `ec.channel.cccomposite` | 3 | GPU | 32 | Implemented | | Calculations | `ec.channel.calculations` | 9 | GPU | 32 | Implemented | | Channel Combiner | `ec.channel.combiner` | 6 | GPU | 32 | Implemented | | Compound Arithmetic | `ec.channel.compoundarithmetic` | 6 | GPU | 32 | Implemented | @@ -100,7 +100,7 @@ Effects are addressed by id (`ec..`) or display name in commands | Channel Mixer | `ec.color.channelmixer` | 13 | GPU | 32 | Implemented | | Color Balance | `ec.color.colorbalance` | 10 | GPU | 32 | Implemented | | Color Balance (HLS) | `ec.color.colorbalancehls` | 3 | GPU | 32 | Implemented | -| Color Link | `ec.color.colorlink` | 6 | | 32 | Implemented | +| Color Link | `ec.color.colorlink` | 6 | GPU | 32 | Implemented | | Color Stabilizer | `ec.color.colorstabilizer` | 6 | GPU | 32 | Implemented | | Colorama | `ec.color.colorama` | 20 | GPU | 32 | Implemented | | Curves | `ec.color.curves` | 0 | GPU | 32 | Implemented | @@ -237,12 +237,12 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| | Advanced Spill Suppressor | `ec.key.advancedspill` | 8 | GPU | 32 | Implemented | -| CC Simple Wire Removal | `ec.key.ccsimplewireremoval` | 7 | | 32 | Implemented | +| CC Simple Wire Removal | `ec.key.ccsimplewireremoval` | 7 | GPU | 32 | Implemented | | Color Difference Key | `ec.key.colordifference` | 16 | GPU | 32 | Implemented | | Color Range | `ec.key.colorrange` | 8 | GPU | 32 | Implemented | | Difference Matte | `ec.key.differencematte` | 6 | GPU | 32 | Implemented | | Extract | `ec.key.extract` | 6 | GPU | 32 | Implemented | -| Inner/Outer Key | `ec.key.innerouter` | 88 | | 32 | Implemented | +| Inner/Outer Key | `ec.key.innerouter` | 88 | GPU | 32 | Implemented | | Key Cleaner | `ec.key.keycleaner` | 4 | GPU | 32 | Implemented | | Key Light | `ec.keying.keylight` | 56 | GPU | 32 | Implemented | | Linear Color Key | `ec.key.linearcolor` | 6 | GPU | 32 | Implemented | @@ -282,14 +282,14 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| -| Basic 3D | `ec.obsolete.basic3d` | 5 | | 32 | Implemented | +| Basic 3D | `ec.obsolete.basic3d` | 5 | GPU | 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 | 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 | -| Reduce Interlace Flicker | `ec.blur.reduceflicker` | 1 | | 32 | Implemented | +| Reduce Interlace Flicker | `ec.blur.reduceflicker` | 1 | GPU | 32 | Implemented | | Spill Suppressor | `ec.key.spill` | 3 | GPU | 32 | Implemented | ## Paint @@ -341,13 +341,13 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| | Brush Strokes | `ec.stylize.brushstrokes` | 7 | GPU | 32 | Implemented | -| CC Block Load | `ec.stylize.ccblockload` | 5 | | 32 | Implemented | -| CC Burn Film | `ec.stylize.ccburnfilm` | 3 | | 32 | Implemented | -| CC Glass | `ec.stylize.ccglass` | 17 | | 32 | Implemented | -| CC HexTile | `ec.stylize.cchextile` | 5 | | 32 | Implemented | +| CC Block Load | `ec.stylize.ccblockload` | 5 | GPU | 32 | Implemented | +| CC Burn Film | `ec.stylize.ccburnfilm` | 3 | GPU | 32 | Implemented | +| CC Glass | `ec.stylize.ccglass` | 17 | GPU | 32 | Implemented | +| CC HexTile | `ec.stylize.cchextile` | 5 | GPU | 32 | Implemented | | CC Kaleida | `ec.stylize.cckaleida` | 4 | GPU | 32 | Implemented | -| CC Mr. Smoothie | `ec.stylize.ccmrsmoothie` | 7 | | 32 | Implemented | -| CC Plastic | `ec.stylize.ccplastic` | 18 | | 32 | Implemented | +| CC Mr. Smoothie | `ec.stylize.ccmrsmoothie` | 7 | GPU | 32 | Implemented | +| CC Plastic | `ec.stylize.ccplastic` | 18 | GPU | 32 | Implemented | | CC RepeTile | `ec.stylize.ccrepetile` | 5 | GPU | 32 | Implemented | | CC Threshold | `ec.stylize.ccthreshold` | 4 | GPU | 32 | Implemented | | CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | GPU | 32 | Implemented | @@ -413,12 +413,12 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| | Apply Color LUT | `ec.utility.applylut` | 0 | GPU | 32 | Implemented | -| CC Overbrights | `ec.utility.ccoverbrights` | 2 | | 32 | Implemented | -| Cineon Converter | `ec.utility.cineon` | 7 | | 32 | Implemented | +| CC Overbrights | `ec.utility.ccoverbrights` | 2 | GPU | 32 | Implemented | +| Cineon Converter | `ec.utility.cineon` | 7 | GPU | 32 | Implemented | | Color Profile Converter | `ec.utility.colorprofileconverter` | 7 | GPU | 32 | Implemented | | Face Measurements | `ec.utility.facemeasurements` | 14 | | 32 | Implemented | | Face Track Points | `ec.utility.facetrackpoints` | 26 | | 32 | Implemented | -| Grow Bounds | `ec.utility.growbounds` | 1 | | 32 | Implemented | -| HDR Compander | `ec.utility.hdrcompander` | 3 | | 32 | Implemented | -| HDR Highlight Compression | `ec.utility.hdrcompression` | 1 | | 32 | Implemented | +| Grow Bounds | `ec.utility.growbounds` | 1 | GPU | 32 | Implemented | +| HDR Compander | `ec.utility.hdrcompander` | 3 | GPU | 32 | Implemented | +| HDR Highlight Compression | `ec.utility.hdrcompression` | 1 | GPU | 32 | Implemented |