Merge branch 'worktree-agent-a0fac3acf963642ce'
This commit is contained in:
commit
f5a136e346
|
|
@ -87,6 +87,10 @@ const CPU_ONLY_CONTROLS: &[(&str, &[&str])] = &[
|
||||||
),
|
),
|
||||||
("ec.noise.addgrain", &["viewingMode", "preset", "color/redBalance", "color/greenBalance", "color/blueBalance", "previewRegion/"]),
|
("ec.noise.addgrain", &["viewingMode", "preset", "color/redBalance", "color/greenBalance", "color/blueBalance", "previewRegion/"]),
|
||||||
("ec.blur.cameralens", &["irisProperties/diffractionFringe", "blurMap/blurMapLayer"]),
|
("ec.blur.cameralens", &["irisProperties/diffractionFringe", "blurMap/blurMapLayer"]),
|
||||||
|
("ec.noise.removegrain", &["temporalFiltering/enabled"]),
|
||||||
|
("ec.distort.liquify", &["viewFreezeAreaMask", "viewMesh"]),
|
||||||
|
("ec.matte.refinesoft", &["reduceChatter", "useMotionBlur", "invert"]),
|
||||||
|
("ec.matte.refinehard", &["reduceChatter", "useMotionBlur", "invert"]),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Whether the GPU kernel of effect `id` covers the instance's current settings (see
|
/// Whether the GPU kernel of effect `id` covers the instance's current settings (see
|
||||||
|
|
|
||||||
|
|
@ -330,10 +330,21 @@ fn stretch(v: f32, lo: f32, hi: f32) -> f32 {
|
||||||
|
|
||||||
/// 0 = Auto Levels (per channel), 1 = Auto Contrast (shared), 2 = Auto Color (per channel + neutral mids).
|
/// 0 = Auto Levels (per channel), 1 = Auto Contrast (shared), 2 = Auto Color (per channel + neutral mids).
|
||||||
fn auto_correct(ctx: &EffectCtx, mut b: Buf, kind: u32) -> Buf {
|
fn auto_correct(ctx: &EffectCtx, mut b: Buf, kind: u32) -> Buf {
|
||||||
|
let blend = (ctx.params.f("blend") / 100.0).clamp(0.0, 1.0) as f32;
|
||||||
|
let (ranges, gam) = auto_correct_settings(ctx, &b.img, kind);
|
||||||
|
map_ca(&mut b.img, |c, a| {
|
||||||
|
let o = [0, 1, 2].map(|k| stretch(c[k], ranges[k].0, ranges[k].1).powf(gam[k]));
|
||||||
|
(lerp3(o, c, blend), a)
|
||||||
|
});
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What Auto Levels (`kind` 0), Auto Contrast (1) and Auto Color (2) measure on `img`: each
|
||||||
|
/// channel's input range (low, high) and gamma. (The GPU effects apply them.)
|
||||||
|
pub fn auto_correct_settings(ctx: &EffectCtx, img: &Image, kind: u32) -> ([(f32, f32); 3], [f32; 3]) {
|
||||||
let black = ctx.params.f("blackClip").clamp(0.0, 49.0) / 100.0;
|
let black = ctx.params.f("blackClip").clamp(0.0, 49.0) / 100.0;
|
||||||
let white = ctx.params.f("whiteClip").clamp(0.0, 49.0) / 100.0;
|
let white = ctx.params.f("whiteClip").clamp(0.0, 49.0) / 100.0;
|
||||||
let blend = (ctx.params.f("blend") / 100.0).clamp(0.0, 1.0) as f32;
|
let h = smoothed_histograms(ctx, img);
|
||||||
let h = smoothed_histograms(ctx, &b.img);
|
|
||||||
let ranges: [(f32, f32); 3] = if kind == 1 {
|
let ranges: [(f32, f32); 3] = if kind == 1 {
|
||||||
let mut combined = vec![0.0; BINS];
|
let mut combined = vec![0.0; BINS];
|
||||||
for k in 0..3 {
|
for k in 0..3 {
|
||||||
|
|
@ -350,7 +361,7 @@ fn auto_correct(ctx: &EffectCtx, mut b: Buf, kind: u32) -> Buf {
|
||||||
let mut gam = [1.0f32; 3];
|
let mut gam = [1.0f32; 3];
|
||||||
if kind == 2 && ctx.params.b("snapNeutralMidtones") {
|
if kind == 2 && ctx.params.b("snapNeutralMidtones") {
|
||||||
let mut sums = [0.0f64; 4];
|
let mut sums = [0.0f64; 4];
|
||||||
for px in &b.img.data {
|
for px in &img.data {
|
||||||
let (c, a) = unpremul(*px);
|
let (c, a) = unpremul(*px);
|
||||||
if a > 1e-4 {
|
if a > 1e-4 {
|
||||||
for k in 0..3 {
|
for k in 0..3 {
|
||||||
|
|
@ -369,11 +380,7 @@ fn auto_correct(ctx: &EffectCtx, mut b: Buf, kind: u32) -> Buf {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
map_ca(&mut b.img, |c, a| {
|
(ranges, gam)
|
||||||
let o = [0, 1, 2].map(|k| stretch(c[k], ranges[k].0, ranges[k].1).powf(gam[k]));
|
|
||||||
(lerp3(o, c, blend), a)
|
|
||||||
});
|
|
||||||
b
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn auto_levels(ctx: &EffectCtx, b: Buf) -> Buf {
|
fn auto_levels(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||||
|
|
@ -397,24 +404,12 @@ fn cdf(h: &[f64]) -> Vec<f32> {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn equalize(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
/// Equalize's lookup tables (`BINS` entries each) for `img`: the R, G and B cumulative
|
||||||
let amt = (ctx.params.f("amount") / 100.0).clamp(0.0, 1.0) as f32;
|
/// histograms (Style RGB), the luminance one (Brightness) or the combined one (Photoshop Style).
|
||||||
if amt <= 0.0 {
|
pub fn equalize_tables(img: &Image, style: u32) -> Vec<Vec<f32>> {
|
||||||
return b;
|
let h = histograms(img);
|
||||||
}
|
|
||||||
let style = ctx.params.e("style");
|
|
||||||
let h = histograms(&b.img);
|
|
||||||
let look = |c: &Vec<f32>, v: f32| c[((v.clamp(0.0, 1.0) * (BINS - 1) as f32).round() as usize).min(BINS - 1)];
|
|
||||||
match style {
|
match style {
|
||||||
1 => {
|
1 => vec![cdf(&h[3])],
|
||||||
let c = cdf(&h[3]);
|
|
||||||
map_ca(&mut b.img, |col, a| {
|
|
||||||
let l = luminance(col[0], col[1], col[2]);
|
|
||||||
let nl = look(&c, l);
|
|
||||||
let o = if l > 1e-5 { col.map(|v| v * nl / l) } else { [nl; 3] };
|
|
||||||
(lerp3(col, o, amt), a)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
2 => {
|
2 => {
|
||||||
let mut combined = vec![0.0; BINS];
|
let mut combined = vec![0.0; BINS];
|
||||||
for k in 0..3 {
|
for k in 0..3 {
|
||||||
|
|
@ -422,12 +417,34 @@ fn equalize(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||||
combined[i] += h[k][i];
|
combined[i] += h[k][i];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let c = cdf(&combined);
|
vec![cdf(&combined)]
|
||||||
map_ca(&mut b.img, |col, a| (lerp3(col, col.map(|v| look(&c, v)), amt), a));
|
}
|
||||||
|
_ => vec![cdf(&h[0]), cdf(&h[1]), cdf(&h[2])],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn equalize(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||||
|
let amt = (ctx.params.f("amount") / 100.0).clamp(0.0, 1.0) as f32;
|
||||||
|
if amt <= 0.0 {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
let style = ctx.params.e("style");
|
||||||
|
let t = equalize_tables(&b.img, style);
|
||||||
|
let look = |c: &Vec<f32>, v: f32| c[((v.clamp(0.0, 1.0) * (BINS - 1) as f32).round() as usize).min(BINS - 1)];
|
||||||
|
match style {
|
||||||
|
1 => {
|
||||||
|
map_ca(&mut b.img, |col, a| {
|
||||||
|
let l = luminance(col[0], col[1], col[2]);
|
||||||
|
let nl = look(&t[0], l);
|
||||||
|
let o = if l > 1e-5 { col.map(|v| v * nl / l) } else { [nl; 3] };
|
||||||
|
(lerp3(col, o, amt), a)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
2 => {
|
||||||
|
map_ca(&mut b.img, |col, a| (lerp3(col, col.map(|v| look(&t[0], v)), amt), a));
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
let cs = [cdf(&h[0]), cdf(&h[1]), cdf(&h[2])];
|
map_ca(&mut b.img, |col, a| (lerp3(col, [0, 1, 2].map(|k| look(&t[k], col[k])), amt), a));
|
||||||
map_ca(&mut b.img, |col, a| (lerp3(col, [0, 1, 2].map(|k| look(&cs[k], col[k])), amt), a));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
b
|
b
|
||||||
|
|
@ -436,17 +453,29 @@ fn equalize(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||||
// ---------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------
|
||||||
// Shadow/Highlight
|
// Shadow/Highlight
|
||||||
|
|
||||||
fn shadow_highlight(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
/// Shadow/Highlight's shadow and highlight amounts for `img` (Auto Amounts measures them).
|
||||||
|
pub fn shadow_highlight_amounts(ctx: &EffectCtx, img: &Image) -> (f32, f32) {
|
||||||
let (mut s_amt, mut h_amt) = (ctx.params.f("shadowAmount") as f32 / 100.0, ctx.params.f("highlightAmount") as f32 / 100.0);
|
let (mut s_amt, mut h_amt) = (ctx.params.f("shadowAmount") as f32 / 100.0, ctx.params.f("highlightAmount") as f32 / 100.0);
|
||||||
let luma = Plane::luma(&b.img);
|
|
||||||
let src_hist = histograms(&b.img);
|
|
||||||
if ctx.params.b("autoAmounts") {
|
if ctx.params.b("autoAmounts") {
|
||||||
// Temporal Smoothing / Scene Detect average the analysis over neighbouring frames.
|
// Temporal Smoothing / Scene Detect average the analysis over neighbouring frames.
|
||||||
let mean =
|
let mean = if ctx.params.f("temporalSmoothing") > 0.0 { hist_mean(&smoothed_histograms(ctx, img)[3]) } else { hist_mean(&histograms(img)[3]) } as f32;
|
||||||
if ctx.params.f("temporalSmoothing") > 0.0 { hist_mean(&smoothed_histograms(ctx, &b.img)[3]) as f32 } else { hist_mean(&src_hist[3]) as f32 };
|
|
||||||
s_amt = ((0.6 - mean) * 1.5).clamp(0.0, 1.0) * 0.8 + 0.1;
|
s_amt = ((0.6 - mean) * 1.5).clamp(0.0, 1.0) * 0.8 + 0.1;
|
||||||
h_amt = ((mean - 0.6) * 1.5).clamp(0.0, 1.0) * 0.8;
|
h_amt = ((mean - 0.6) * 1.5).clamp(0.0, 1.0) * 0.8;
|
||||||
}
|
}
|
||||||
|
(s_amt, h_amt)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The luminance values clipping `black` / `white` fractions of `img`'s alpha-weighted
|
||||||
|
/// luminance histogram (Shadow/Highlight's Black / White Clip).
|
||||||
|
pub fn luma_clip_points(img: &Image, black: f64, white: f64) -> (f32, f32) {
|
||||||
|
clip_points(&histograms(img)[3], black, white)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shadow_highlight(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||||
|
let (s_amt, h_amt) = shadow_highlight_amounts(ctx, &b.img);
|
||||||
|
let luma = Plane::luma(&b.img);
|
||||||
|
let (bc, wc) = (ctx.params.f("moreOptions/blackClip").clamp(0.0, 49.0) / 100.0, ctx.params.f("moreOptions/whiteClip").clamp(0.0, 49.0) / 100.0);
|
||||||
|
let clip0 = (bc > 0.0 || wc > 0.0).then(|| luma_clip_points(&b.img, bc, wc));
|
||||||
let s_tw = (ctx.params.f("moreOptions/shadowTonalWidth") as f32 / 100.0).max(0.01);
|
let s_tw = (ctx.params.f("moreOptions/shadowTonalWidth") as f32 / 100.0).max(0.01);
|
||||||
let h_tw = (ctx.params.f("moreOptions/highlightTonalWidth") as f32 / 100.0).max(0.01);
|
let h_tw = (ctx.params.f("moreOptions/highlightTonalWidth") as f32 / 100.0).max(0.01);
|
||||||
let s_r = ctx.params.f("moreOptions/shadowRadius").max(0.0) * b.scale / 2.0;
|
let s_r = ctx.params.f("moreOptions/shadowRadius").max(0.0) * b.scale / 2.0;
|
||||||
|
|
@ -478,10 +507,8 @@ fn shadow_highlight(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||||
// Black / White Clip: the adjusted image's extremes (ignoring those fractions of pixels)
|
// Black / White Clip: the adjusted image's extremes (ignoring those fractions of pixels)
|
||||||
// are mapped back onto the original's, so lifting shadows or recovering highlights doesn't
|
// are mapped back onto the original's, so lifting shadows or recovering highlights doesn't
|
||||||
// flatten the ends of the tonal range.
|
// flatten the ends of the tonal range.
|
||||||
let (bc, wc) = (ctx.params.f("moreOptions/blackClip").clamp(0.0, 49.0) / 100.0, ctx.params.f("moreOptions/whiteClip").clamp(0.0, 49.0) / 100.0);
|
if let Some((lo0, hi0)) = clip0 {
|
||||||
if bc > 0.0 || wc > 0.0 {
|
let (lo1, hi1) = luma_clip_points(&b.img, bc, wc);
|
||||||
let (lo0, hi0) = clip_points(&src_hist[3], bc, wc);
|
|
||||||
let (lo1, hi1) = clip_points(&histograms(&b.img)[3], bc, wc);
|
|
||||||
if (lo0, hi0) != (lo1, hi1) && hi1 - lo1 > 1e-3 {
|
if (lo0, hi0) != (lo1, hi1) && hi1 - lo1 > 1e-3 {
|
||||||
let k = (hi0 - lo0) / (hi1 - lo1);
|
let k = (hi0 - lo0) / (hi1 - lo1);
|
||||||
map_ca(&mut b.img, |c, a| (c.map(|v| (lo0 + (v - lo1) * k).max(0.0)), a));
|
map_ca(&mut b.img, |c, a| (c.map(|v| (lo0 + (v - lo1) * k).max(0.0)), a));
|
||||||
|
|
|
||||||
|
|
@ -354,7 +354,7 @@ fn magnify(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Magnify's Blending Mode choices after "None".
|
/// Magnify's Blending Mode choices after "None".
|
||||||
const MAGNIFY_MODES: [BlendMode; 17] = [
|
pub const MAGNIFY_MODES: [BlendMode; 17] = [
|
||||||
BlendMode::Normal,
|
BlendMode::Normal,
|
||||||
BlendMode::Add,
|
BlendMode::Add,
|
||||||
BlendMode::Multiply,
|
BlendMode::Multiply,
|
||||||
|
|
|
||||||
|
|
@ -325,7 +325,9 @@ pub fn build_mesh(strokes: &[LiquifyStroke], w: f64, h: f64, frozen: &(dyn Fn(f6
|
||||||
|
|
||||||
type MeshCache = Mutex<HashMap<u64, Arc<Mesh>>>;
|
type MeshCache = Mutex<HashMap<u64, Arc<Mesh>>>;
|
||||||
|
|
||||||
fn cached_mesh(ctx: &EffectCtx) -> Arc<Mesh> {
|
/// Liquify's displacement mesh for the instance (replayed strokes, cached; also used by the
|
||||||
|
/// GPU kernels).
|
||||||
|
pub fn liquify_mesh(ctx: &EffectCtx) -> Arc<Mesh> {
|
||||||
use std::hash::{Hash, Hasher};
|
use std::hash::{Hash, Hasher};
|
||||||
let text = ctx.params.s("distortionMesh");
|
let text = ctx.params.s("distortionMesh");
|
||||||
let mask = ctx.params.f("freezeAreaMask").round() as usize;
|
let mask = ctx.params.f("freezeAreaMask").round() as usize;
|
||||||
|
|
@ -369,7 +371,7 @@ fn liquify(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||||
if ctx.params.s("distortionMesh").trim().is_empty() && !show_freeze && !show_mesh {
|
if ctx.params.s("distortionMesh").trim().is_empty() && !show_freeze && !show_mesh {
|
||||||
return b;
|
return b;
|
||||||
}
|
}
|
||||||
let mesh = cached_mesh(ctx);
|
let mesh = liquify_mesh(ctx);
|
||||||
let off = ctx.params.v2("distortionMeshOffset");
|
let off = ctx.params.v2("distortionMeshOffset");
|
||||||
let mesh_color = ctx.params.color("meshColor");
|
let mesh_color = ctx.params.color("meshColor");
|
||||||
let src = b.img.clone();
|
let src = b.img.clone();
|
||||||
|
|
|
||||||
|
|
@ -67,11 +67,13 @@ use std::sync::OnceLock;
|
||||||
|
|
||||||
pub use card3d::{CompLight, CompScene};
|
pub use card3d::{CompLight, CompScene};
|
||||||
pub use color_fx::{
|
pub use color_fx::{
|
||||||
HUESAT_CHANNELS, LEVELS_CHANNELS, exposure_settings, fill_uses_masks, huesat_ranges_identity, levels_channel_ids, levels_channels_identity, levels_clip,
|
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;
|
pub use color2::{Curve, auto_correct_settings, equalize_tables, luma_clip_points, shadow_highlight_amounts};
|
||||||
pub use distort::transform_shutter;
|
pub use distort::transform_shutter;
|
||||||
pub use distort2::parse_mesh;
|
pub use distort2::{MAGNIFY_MODES, parse_mesh};
|
||||||
|
pub use distort4::liquify_mesh;
|
||||||
use effectcraft_keyframe::Value;
|
use effectcraft_keyframe::Value;
|
||||||
use effectcraft_project::build::Ids;
|
use effectcraft_project::build::Ids;
|
||||||
use effectcraft_project::{GroupKind, ParamUi, PropGroup, Property};
|
use effectcraft_project::{GroupKind, ParamUi, PropGroup, Property};
|
||||||
|
|
@ -79,12 +81,19 @@ pub use effectcraft_raster::{AuxChannels, Image};
|
||||||
pub use misc::{INVERT_ALPHA, INVERT_CHANNELS, glow_operation};
|
pub use misc::{INVERT_ALPHA, INVERT_CHANNELS, glow_operation};
|
||||||
pub use sim::particle_state;
|
pub use sim::particle_state;
|
||||||
pub use sim3::playground_state;
|
pub use sim3::playground_state;
|
||||||
|
pub use stylize2::strobe_on;
|
||||||
// CPU helpers the GPU kernels share (effectcraft-gpu).
|
// CPU helpers the GPU kernels share (effectcraft-gpu).
|
||||||
pub use blur2::{camera_lens_plain, camera_lens_spans};
|
pub use blur2::{camera_lens_plain, camera_lens_spans};
|
||||||
pub use generate::gen_mode;
|
pub use generate::gen_mode;
|
||||||
pub use generate2::pattern_kind as cell_pattern_kind;
|
pub use generate2::pattern_kind as cell_pattern_kind;
|
||||||
pub use noise::GrainLook;
|
pub use noise::GrainLook;
|
||||||
pub use transition::place_layer;
|
pub use transition::place_layer;
|
||||||
|
// effectcraft-gpu fx_noise.
|
||||||
|
pub use noise::remove_grain_level;
|
||||||
|
pub use noise3::{FractalGpu, fractal_gpu};
|
||||||
|
pub use time_fx::{posterized_time, time_frames};
|
||||||
|
// effectcraft-gpu fx_tone.
|
||||||
|
pub use ocio::color_stabilizer_maps;
|
||||||
|
|
||||||
/// Effect categories in Effects & Presets order.
|
/// Effect categories in Effects & Presets order.
|
||||||
pub const CATEGORIES: &[&str] = &[
|
pub const CATEGORIES: &[&str] = &[
|
||||||
|
|
@ -806,6 +815,103 @@ pub const GPU_EFFECTS: &[&str] = &[
|
||||||
"ec.color.selectivecolor",
|
"ec.color.selectivecolor",
|
||||||
"ec.key.linearcolor",
|
"ec.key.linearcolor",
|
||||||
"ec.keying.keylight",
|
"ec.keying.keylight",
|
||||||
|
// effectcraft-gpu fx_key (keying, matte, channel)
|
||||||
|
"ec.key.colorkey",
|
||||||
|
"ec.key.luma",
|
||||||
|
"ec.key.colorrange",
|
||||||
|
"ec.key.extract",
|
||||||
|
"ec.key.differencematte",
|
||||||
|
"ec.key.colordifference",
|
||||||
|
"ec.key.screen",
|
||||||
|
"ec.key.spill",
|
||||||
|
"ec.key.advancedspill",
|
||||||
|
"ec.key.keycleaner",
|
||||||
|
"ec.key.unmult",
|
||||||
|
"ec.matte.simplechoker",
|
||||||
|
"ec.matte.mattechoker",
|
||||||
|
"ec.matte.refinesoft",
|
||||||
|
"ec.matte.refinehard",
|
||||||
|
"ec.channel.setmatte",
|
||||||
|
"ec.channel.setchannels",
|
||||||
|
"ec.channel.shiftchannels",
|
||||||
|
"ec.channel.removecolormatting",
|
||||||
|
"ec.channel.arithmetic",
|
||||||
|
"ec.channel.solidcomposite",
|
||||||
|
"ec.channel.combiner",
|
||||||
|
"ec.channel.blend",
|
||||||
|
"ec.channel.calculations",
|
||||||
|
"ec.channel.compoundarithmetic",
|
||||||
|
// effectcraft-gpu fx_tone (colour correction)
|
||||||
|
"ec.color.levelsic",
|
||||||
|
"ec.color.gammapedestalgain",
|
||||||
|
"ec.color.photofilter",
|
||||||
|
"ec.color.changecolor",
|
||||||
|
"ec.color.changetocolor",
|
||||||
|
"ec.color.leavecolor",
|
||||||
|
"ec.color.broadcast",
|
||||||
|
"ec.color.colorbalancehls",
|
||||||
|
"ec.color.videolimiter",
|
||||||
|
"ec.color.psarbitrarymap",
|
||||||
|
"ec.color.cctoner",
|
||||||
|
"ec.color.cccoloroffset",
|
||||||
|
"ec.color.cckernel",
|
||||||
|
"ec.color.autolevels",
|
||||||
|
"ec.color.autocontrast",
|
||||||
|
"ec.color.autocolor",
|
||||||
|
"ec.color.equalize",
|
||||||
|
"ec.color.shadowhighlight",
|
||||||
|
"ec.color.cccolorneutralizer",
|
||||||
|
"ec.color.colorstabilizer",
|
||||||
|
// effectcraft-gpu fx_stylize (stylize, distort)
|
||||||
|
"ec.stylize.posterize",
|
||||||
|
"ec.stylize.threshold",
|
||||||
|
"ec.stylize.ccthreshold",
|
||||||
|
"ec.stylize.ccthresholdrgb",
|
||||||
|
"ec.stylize.strobe",
|
||||||
|
"ec.stylize.ccvignette",
|
||||||
|
"ec.stylize.scatter",
|
||||||
|
"ec.stylize.brushstrokes",
|
||||||
|
"ec.stylize.roughenedges",
|
||||||
|
"ec.stylize.texturize",
|
||||||
|
"ec.stylize.motiontile",
|
||||||
|
"ec.stylize.cckaleida",
|
||||||
|
"ec.stylize.ccrepetile",
|
||||||
|
"ec.distort.mirror",
|
||||||
|
"ec.distort.offset",
|
||||||
|
"ec.distort.polar",
|
||||||
|
"ec.distort.spherize",
|
||||||
|
"ec.distort.cornerpin",
|
||||||
|
"ec.distort.opticscompensation",
|
||||||
|
"ec.distort.magnify",
|
||||||
|
"ec.distort.ccslant",
|
||||||
|
"ec.distort.ccsmear",
|
||||||
|
"ec.distort.ccsplit",
|
||||||
|
"ec.distort.ccsplit2",
|
||||||
|
"ec.distort.cctiler",
|
||||||
|
"ec.distort.ccgriddler",
|
||||||
|
"ec.distort.liquify",
|
||||||
|
"ec.distort.twirllegacy",
|
||||||
|
"ec.distort.ccripplepulse",
|
||||||
|
"ec.distort.ccpowerpin",
|
||||||
|
"ec.distort.ccflomotion",
|
||||||
|
// effectcraft-gpu fx_noise (noise, blur, time)
|
||||||
|
"ec.noise.turbulent",
|
||||||
|
"ec.noise.median",
|
||||||
|
"ec.noise.medianlegacy",
|
||||||
|
"ec.noise.dustscratches",
|
||||||
|
"ec.noise.removegrain",
|
||||||
|
"ec.noise.noisealpha",
|
||||||
|
"ec.noise.noisehls",
|
||||||
|
"ec.noise.noisehlsauto",
|
||||||
|
"ec.blur.smart",
|
||||||
|
"ec.blur.bilateral",
|
||||||
|
"ec.blur.sharpen",
|
||||||
|
"ec.blur.unsharp",
|
||||||
|
"ec.blur.compound",
|
||||||
|
"ec.blur.channel",
|
||||||
|
"ec.channel.minimax",
|
||||||
|
"ec.time.echo",
|
||||||
|
"ec.time.posterizetime",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated
|
/// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated
|
||||||
|
|
|
||||||
|
|
@ -477,6 +477,28 @@ pub(crate) fn sampling_boxes(ctx: &EffectCtx, img: &Image, scale: f64) -> Vec<(u
|
||||||
sample_boxes(img, n, s)
|
sample_boxes(img, n, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The grain level (0.5..2) measured in the sample boxes of `img`.
|
||||||
|
fn grain_level(img: &Image, boxes: &[(usize, usize, usize)]) -> f32 {
|
||||||
|
let mut m = vec![false; img.data.len()];
|
||||||
|
let w = img.width as usize;
|
||||||
|
for &(x, y, s) in boxes {
|
||||||
|
for yy in y..y + s {
|
||||||
|
for xx in x..x + s {
|
||||||
|
m[yy * w + xx] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let st = crate::noise2::grain_stats_masked(img, Some(&m));
|
||||||
|
let s = (st.std[0] + st.std[1] + st.std[2]) / 3.0;
|
||||||
|
if boxes.is_empty() { 1.0 } else { (s / 0.02).clamp(0.5, 2.0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove Grain's measured grain level for buffer `b` without Temporal Filtering (the GPU
|
||||||
|
/// kernels' spatial pass, effectcraft-gpu `fx_noise`).
|
||||||
|
pub fn remove_grain_level(ctx: &EffectCtx, b: &Buf) -> f32 {
|
||||||
|
grain_level(&b.img, &sampling_boxes(ctx, &b.img, b.scale))
|
||||||
|
}
|
||||||
|
|
||||||
fn remove_grain(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
fn remove_grain(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||||
let pr = ctx.params;
|
let pr = ctx.params;
|
||||||
let amt = pr.f("noiseReductionSettings/noiseReduction") as f32;
|
let amt = pr.f("noiseReductionSettings/noiseReduction") as f32;
|
||||||
|
|
@ -535,20 +557,7 @@ fn remove_grain(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// The grain level measured in the sample boxes scales the smoothing.
|
// The grain level measured in the sample boxes scales the smoothing.
|
||||||
let level = {
|
let level = grain_level(&b.img, &boxes);
|
||||||
let mut m = vec![false; b.img.data.len()];
|
|
||||||
let w = b.img.width as usize;
|
|
||||||
for &(x, y, s) in &boxes {
|
|
||||||
for yy in y..y + s {
|
|
||||||
for xx in x..x + s {
|
|
||||||
m[yy * w + xx] = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let st = crate::noise2::grain_stats_masked(&b.img, Some(&m));
|
|
||||||
let s = (st.std[0] + st.std[1] + st.std[2]) / 3.0;
|
|
||||||
if boxes.is_empty() { 1.0 } else { (s / 0.02).clamp(0.5, 2.0) }
|
|
||||||
};
|
|
||||||
let passes = pr.f("noiseReductionSettings/passes").round().clamp(1.0, 8.0) as usize;
|
let passes = pr.f("noiseReductionSettings/passes").round().clamp(1.0, 8.0) as usize;
|
||||||
let multichannel = pr.e("noiseReductionSettings/mode") == 0;
|
let multichannel = pr.e("noiseReductionSettings/mode") == 0;
|
||||||
let texture = (pr.f("fineTuning/texture") as f32).clamp(0.0, 1.0);
|
let texture = (pr.f("fineTuning/texture") as f32).clamp(0.0, 1.0);
|
||||||
|
|
|
||||||
|
|
@ -326,6 +326,89 @@ impl Fractal {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fractal / Turbulent Noise resolved for the GPU kernel (effectcraft-gpu `fx_noise`): the
|
||||||
|
/// per-pixel constants and, per octave, the terms [`Fractal::raw`] computes before touching the
|
||||||
|
/// pixel, in the same f32 order.
|
||||||
|
pub struct FractalGpu {
|
||||||
|
pub kind: u32,
|
||||||
|
pub noise_type: u32,
|
||||||
|
pub seed: u32,
|
||||||
|
pub invert: bool,
|
||||||
|
pub overflow: u32,
|
||||||
|
pub contrast: f32,
|
||||||
|
pub brightness: f32,
|
||||||
|
/// Rotation (sin, cos).
|
||||||
|
pub rot: (f32, f32),
|
||||||
|
pub size: [f32; 2],
|
||||||
|
/// Cycle Evolution: (t, cycle, t / cycle).
|
||||||
|
pub cycle: Option<[f32; 3]>,
|
||||||
|
pub sub_rotation: bool,
|
||||||
|
/// Σ amplitude × weight (the normaliser).
|
||||||
|
pub norm: f32,
|
||||||
|
/// Per octave: origin x, y, sub-rotation sin, cos, frequency, amplitude, weight, evolution
|
||||||
|
/// (turbulence applied), z offset, Dynamic Progressive warp factor.
|
||||||
|
pub octaves: Vec<[f32; 10]>,
|
||||||
|
/// Blending Mode (`None` = the noise alone), Opacity and Blend With Original.
|
||||||
|
pub mode: Option<BlendMode>,
|
||||||
|
pub opacity: f32,
|
||||||
|
pub blend: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`FractalGpu`] for a buffer with `offset` / `scale`.
|
||||||
|
pub fn fractal_gpu(ctx: &EffectCtx, offset: [f64; 2], scale: f64, turbulent: bool) -> FractalGpu {
|
||||||
|
let b = Buf { img: effectcraft_raster::Image::new(0, 0), offset, scale };
|
||||||
|
let fr = Fractal::from_params(ctx, &b, turbulent);
|
||||||
|
let n_oct = fr.octaves.ceil() as usize;
|
||||||
|
let frac = fr.octaves - fr.octaves.floor();
|
||||||
|
let (mut norm, mut amp, mut f, mut depth) = (0.0f32, 1.0f32, 1.0f32, 1.0f32);
|
||||||
|
let mut octaves = Vec::with_capacity(n_oct);
|
||||||
|
for o in 0..n_oct {
|
||||||
|
let w = if o + 1 == n_oct && frac > 0.0 { frac } else { 1.0 };
|
||||||
|
let of = o as f32;
|
||||||
|
let k = if fr.perspective { depth } else { 1.0 } * (1.0 + fr.turbulence * of);
|
||||||
|
let mut org = fr.origin;
|
||||||
|
if k != 1.0 {
|
||||||
|
org = [fr.center[0] + (fr.origin[0] - fr.center[0]) * k, fr.center[1] + (fr.origin[1] - fr.center[1]) * k];
|
||||||
|
}
|
||||||
|
if !fr.center_subscale {
|
||||||
|
org = [org[0] + fr.sub_offset[0] * of, org[1] + fr.sub_offset[1] * of];
|
||||||
|
}
|
||||||
|
let (s, c) = (fr.sub_rotation * of).sin_cos();
|
||||||
|
let evo = fr.evolution * (1.0 + fr.turbulence * of);
|
||||||
|
let dk = if fr.kind == 6 { 0.5 + of * 0.35 } else { 1.0 };
|
||||||
|
octaves.push([org[0], org[1], s, c, f, amp, w, evo, of * 7.31, dk]);
|
||||||
|
norm += amp * w;
|
||||||
|
amp *= fr.influence;
|
||||||
|
f /= fr.sub_scaling;
|
||||||
|
depth *= fr.sub_scaling;
|
||||||
|
}
|
||||||
|
let cycle = fr.cycle.map(|c| {
|
||||||
|
let t = fr.evolution.rem_euclid(c);
|
||||||
|
[t, c, t / c]
|
||||||
|
});
|
||||||
|
FractalGpu {
|
||||||
|
kind: fr.kind,
|
||||||
|
noise_type: fr.noise_type,
|
||||||
|
seed: fr.seed,
|
||||||
|
invert: fr.invert,
|
||||||
|
overflow: fr.overflow,
|
||||||
|
contrast: fr.contrast,
|
||||||
|
brightness: fr.brightness,
|
||||||
|
rot: fr.rot,
|
||||||
|
size: fr.size,
|
||||||
|
cycle,
|
||||||
|
sub_rotation: fr.sub_rotation != 0.0,
|
||||||
|
norm,
|
||||||
|
octaves,
|
||||||
|
mode: match ctx.params.get("blendingMode").map(Value::as_enum).unwrap_or(BLEND_NONE) {
|
||||||
|
BLEND_NONE => None,
|
||||||
|
m => Some(BLEND_MODES.get(m as usize).and_then(|l| BlendMode::from_name(l)).unwrap_or(BlendMode::Normal)),
|
||||||
|
},
|
||||||
|
opacity: (ctx.params.f("opacity") as f32 / 100.0).clamp(0.0, 1.0),
|
||||||
|
blend: ctx.params.f("blend") as f32 / 100.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Map a value by an [`OVERFLOWS`] option.
|
/// Map a value by an [`OVERFLOWS`] option.
|
||||||
pub fn overflow(v: f32, mode: u32) -> f32 {
|
pub fn overflow(v: f32, mode: u32) -> f32 {
|
||||||
match mode {
|
match mode {
|
||||||
|
|
|
||||||
|
|
@ -905,6 +905,46 @@ fn curve3(xs: [f64; 3], ys: [f64; 3], v: f64) -> f64 {
|
||||||
a.1 + (c.1 - a.1) * (v - a.0) / (c.0 - a.0).max(1e-9)
|
a.1 + (c.1 - a.1) * (v - a.0) / (c.0 - a.0).max(1e-9)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Color Stabilizer's correction of buffer `b` as one piecewise-linear map per channel (points
|
||||||
|
/// with increasing x; the end segments extend), the same maps [`color_stabilizer`] applies.
|
||||||
|
/// `None` without a host or reference frame (the layer passes through). (The GPU effect.)
|
||||||
|
pub fn color_stabilizer_maps(ctx: &EffectCtx, b: &Buf) -> Option<[Vec<(f64, f64)>; 3]> {
|
||||||
|
let reference = ctx.env.host?.self_at(ctx.params.f("referenceFrame"), ctx.env.effect_index)?;
|
||||||
|
let r = ctx.params.f("sampleSize").max(0.5);
|
||||||
|
let pts = [ctx.params.v2("blackPoint"), ctx.params.v2("midPoint"), ctx.params.v2("whitePoint")];
|
||||||
|
let cur = pts.map(|pt| sample_mean(b, pt, r));
|
||||||
|
let refv = pts.map(|pt| sample_mean(&reference, pt, r));
|
||||||
|
let mode = ctx.params.e("stabilize");
|
||||||
|
Some([0, 1, 2].map(|k| {
|
||||||
|
let offset = |c0: f64, r0: f64| vec![(0.0, r0 - c0), (1.0, 1.0 + r0 - c0)];
|
||||||
|
match mode {
|
||||||
|
0 => offset(cur[0][k], refv[0][k]),
|
||||||
|
1 => {
|
||||||
|
let (c0, c1, r0, r1) = (cur[0][k], cur[2][k], refv[0][k], refv[2][k]);
|
||||||
|
if (c1 - c0).abs() < 1e-6 {
|
||||||
|
offset(c0, r0)
|
||||||
|
} else if c0 < c1 {
|
||||||
|
vec![(c0, r0), (c1, r1)]
|
||||||
|
} else {
|
||||||
|
vec![(c1, r1), (c0, r0)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
let mut v = vec![(0.0, 0.0)];
|
||||||
|
let mut pairs: Vec<(f64, f64)> = (0..3).map(|i| (cur[i][k], refv[i][k])).collect();
|
||||||
|
pairs.sort_by(|a, b| a.0.total_cmp(&b.0));
|
||||||
|
for (x, y) in pairs {
|
||||||
|
if x > v.last().expect("non-empty").0 + 1e-4 && x < 1.0 - 1e-4 {
|
||||||
|
v.push((x, y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v.push((1.0, 1.0));
|
||||||
|
v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
fn color_stabilizer(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
fn color_stabilizer(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||||
let Some(host) = ctx.env.host else { return b };
|
let Some(host) = ctx.env.host else { return b };
|
||||||
let Some(reference) = host.self_at(ctx.params.f("referenceFrame"), ctx.env.effect_index) else { return b };
|
let Some(reference) = host.self_at(ctx.params.f("referenceFrame"), ctx.env.effect_index) else { return b };
|
||||||
|
|
|
||||||
|
|
@ -223,7 +223,8 @@ fn scatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||||
|
|
||||||
// ---- Strobe Light ----
|
// ---- Strobe Light ----
|
||||||
|
|
||||||
pub(crate) fn strobe_on(time: f64, duration: f64, period: f64, prob: f64, seed: u32) -> bool {
|
/// Whether Strobe Light flashes at `time` (also used by the GPU kernels).
|
||||||
|
pub fn strobe_on(time: f64, duration: f64, period: f64, prob: f64, seed: u32) -> bool {
|
||||||
if period <= 0.0 {
|
if period <= 0.0 {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,12 @@ fn fetch(ctx: &EffectCtx, times: &[f64]) -> Option<Frames> {
|
||||||
Some(Frames { grid, imgs })
|
Some(Frames { grid, imgs })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The layer's frames at layer times `times` placed on one grid (the GPU Echo / Posterize Time,
|
||||||
|
/// effectcraft-gpu `fx_noise`): (grid buffer, one image per time). `None` without a host.
|
||||||
|
pub fn time_frames(ctx: &EffectCtx, times: &[f64]) -> Option<(Buf, Vec<Image>)> {
|
||||||
|
fetch(ctx, times).map(|f| (f.grid, f.imgs))
|
||||||
|
}
|
||||||
|
|
||||||
fn with_img(grid: Buf, img: Image) -> Buf {
|
fn with_img(grid: Buf, img: Image) -> Buf {
|
||||||
Buf { img, ..grid }
|
Buf { img, ..grid }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -154,6 +154,10 @@ impl GpuContext {
|
||||||
include_str!("shaders/fx_color.wgsl"),
|
include_str!("shaders/fx_color.wgsl"),
|
||||||
include_str!("shaders/fx_distort.wgsl"),
|
include_str!("shaders/fx_distort.wgsl"),
|
||||||
include_str!("shaders/fx_generate.wgsl"),
|
include_str!("shaders/fx_generate.wgsl"),
|
||||||
|
include_str!("shaders/fx_key.wgsl"),
|
||||||
|
include_str!("shaders/fx_stylize.wgsl"),
|
||||||
|
include_str!("shaders/fx_noise.wgsl"),
|
||||||
|
include_str!("shaders/fx_tone.wgsl"),
|
||||||
]
|
]
|
||||||
.concat();
|
.concat();
|
||||||
let module =
|
let module =
|
||||||
|
|
@ -207,6 +211,10 @@ impl GpuContext {
|
||||||
.chain(crate::fx_color::KERNELS)
|
.chain(crate::fx_color::KERNELS)
|
||||||
.chain(crate::fx_distort::KERNELS)
|
.chain(crate::fx_distort::KERNELS)
|
||||||
.chain(crate::fx_generate::KERNELS)
|
.chain(crate::fx_generate::KERNELS)
|
||||||
|
.chain(crate::fx_key::KERNELS)
|
||||||
|
.chain(crate::fx_stylize::KERNELS)
|
||||||
|
.chain(crate::fx_noise::KERNELS)
|
||||||
|
.chain(crate::fx_tone::KERNELS)
|
||||||
.map(|e| (e, &layout))
|
.map(|e| (e, &layout))
|
||||||
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
|
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
|
||||||
.map(|(e, layout)| {
|
.map(|(e, layout)| {
|
||||||
|
|
|
||||||
|
|
@ -113,14 +113,24 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
"ec.blur.gaussian" => gaussian(e, ctx, b),
|
"ec.blur.gaussian" => gaussian(e, ctx, b),
|
||||||
"ec.blur.fastbox" => box_blur(e, ctx, b),
|
"ec.blur.fastbox" => box_blur(e, ctx, b),
|
||||||
"ec.blur.directional" => directional(e, ctx, b),
|
"ec.blur.directional" => directional(e, ctx, b),
|
||||||
|
"ec.stylize.glow" if crate::fx_stylize::glow_extra(ctx) => crate::fx_stylize::glow(e, ctx, b),
|
||||||
|
"ec.distort.transform" if crate::fx_stylize::transform_blur(ctx) => crate::fx_stylize::transform(e, ctx, b),
|
||||||
"ec.stylize.glow" => glow(e, ctx, b),
|
"ec.stylize.glow" => glow(e, ctx, b),
|
||||||
"ec.perspective.dropshadow" => drop_shadow(e, ctx, b),
|
"ec.perspective.dropshadow" => drop_shadow(e, ctx, b),
|
||||||
"ec.distort.transform" => transform(e, ctx, b),
|
"ec.distort.transform" => transform(e, ctx, b),
|
||||||
"ec.color.curves" => curves(e, ctx, b),
|
"ec.color.curves" => curves(e, ctx, b),
|
||||||
|
"ec.color.huesaturation" if !effectcraft_effects::huesat_ranges_identity(ctx) => crate::fx_tone::hue_saturation(e, ctx, b),
|
||||||
|
"ec.color.levels" if !effectcraft_effects::levels_channels_identity(ctx) => crate::fx_tone::levels(e, ctx, b),
|
||||||
|
"ec.channel.invert" if (4..=11).contains(&ctx.params.e("channel")) => crate::fx_tone::invert(e, ctx, b),
|
||||||
|
"ec.noise.fractal" if crate::fx_noise::fractal_extra(ctx) => crate::fx_noise::fractal(e, ctx, b, false),
|
||||||
_ if id.starts_with("ec.control.") => Some(b),
|
_ if id.starts_with("ec.control.") => Some(b),
|
||||||
_ if crate::fx_color::IDS.contains(&id) => crate::fx_color::apply(e, id, ctx, b),
|
_ if crate::fx_color::IDS.contains(&id) => crate::fx_color::apply(e, id, ctx, b),
|
||||||
_ if crate::fx_distort::IDS.contains(&id) => crate::fx_distort::apply(e, id, ctx, b),
|
_ if crate::fx_distort::IDS.contains(&id) => crate::fx_distort::apply(e, id, ctx, b),
|
||||||
_ if crate::fx_generate::IDS.contains(&id) => crate::fx_generate::apply(e, id, ctx, b),
|
_ if crate::fx_generate::IDS.contains(&id) => crate::fx_generate::apply(e, id, ctx, b),
|
||||||
|
_ if crate::fx_key::IDS.contains(&id) => crate::fx_key::apply(e, id, ctx, b),
|
||||||
|
_ if crate::fx_stylize::IDS.contains(&id) => crate::fx_stylize::apply(e, id, ctx, b),
|
||||||
|
_ if crate::fx_noise::IDS.contains(&id) => crate::fx_noise::apply(e, id, ctx, b),
|
||||||
|
_ if crate::fx_tone::IDS.contains(&id) => crate::fx_tone::apply(e, id, ctx, b),
|
||||||
_ => pointwise(e, id, ctx, b),
|
_ => pointwise(e, id, ctx, b),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -235,7 +235,7 @@ fn morph(e: &mut Enc, img: &GpuImage, r: u32, modes: [u32; 4]) -> GpuImage {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// util::morph_frac (blend between the two nearest integer radii).
|
/// util::morph_frac (blend between the two nearest integer radii).
|
||||||
fn morph_frac(e: &mut Enc, img: &GpuImage, radius: f64, modes: [u32; 4]) -> GpuImage {
|
pub(crate) fn morph_frac(e: &mut Enc, img: &GpuImage, radius: f64, modes: [u32; 4]) -> GpuImage {
|
||||||
if radius <= 0.0 {
|
if radius <= 0.0 {
|
||||||
return img.clone();
|
return img.clone();
|
||||||
}
|
}
|
||||||
|
|
@ -253,8 +253,8 @@ fn morph_frac(e: &mut Enc, img: &GpuImage, radius: f64, modes: [u32; 4]) -> GpuI
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
const ERODE_X: [u32; 4] = [0, 2, 2, 2];
|
pub(crate) const ERODE_X: [u32; 4] = [0, 2, 2, 2];
|
||||||
const DILATE_X: [u32; 4] = [1, 2, 2, 2];
|
pub(crate) const DILATE_X: [u32; 4] = [1, 2, 2, 2];
|
||||||
|
|
||||||
// ---------------------------------------------------------------- Key Light
|
// ---------------------------------------------------------------- Key Light
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,630 @@
|
||||||
|
//! GPU effects, keying, matte and channel family (kernels in `shaders/fx_key.wgsl`, every entry
|
||||||
|
//! point prefixed `fxk_`): the CPU effects' exact steps with the pixel loops as compute kernels.
|
||||||
|
//!
|
||||||
|
//! The keyers (Color Key, Luma Key, Color Range, Extract, Difference Matte, Color Difference
|
||||||
|
//! Key, Screen Key) build their matte in a plane pass, run the CPU's edge thin (morphology) and
|
||||||
|
//! feather (Gaussian) on it, and apply it in a second pass; spill suppression, Unmult and the
|
||||||
|
//! channel effects are one per-pixel pass (`fxk_point`). Simple / Matte Choker and Key Cleaner
|
||||||
|
//! work on the alpha plane and put it back with `util::set_alpha` (straight colour kept, the
|
||||||
|
//! transparent pixels' colour from a small blur). Advanced Spill Suppressor's Standard method
|
||||||
|
//! finds the dominant key primary with a GPU sum reduction (`fxk_reduce`). Other layers (layer
|
||||||
|
//! parameters) are resampled into the buffer like `util::fit_layer` (`fxk_fit`).
|
||||||
|
|
||||||
|
use effectcraft_color::rgb_to_hsl;
|
||||||
|
use effectcraft_effects::EffectCtx;
|
||||||
|
use effectcraft_effects::util::{SRC_ORDER, Src};
|
||||||
|
|
||||||
|
use crate::context::{Enc, GpuImage, Params};
|
||||||
|
use crate::effects::{GBuf, box_passes, gaussian_blur};
|
||||||
|
use crate::fx_color::{DILATE_X, ERODE_X, morph_frac};
|
||||||
|
|
||||||
|
/// Compute entry points in `fx_key.wgsl`.
|
||||||
|
pub(crate) const KERNELS: &[&str] = &["fxk_point", "fxk_fit", "fxk_reduce"];
|
||||||
|
|
||||||
|
/// Effect ids implemented here.
|
||||||
|
pub(crate) const IDS: &[&str] = &[
|
||||||
|
"ec.key.colorkey",
|
||||||
|
"ec.key.luma",
|
||||||
|
"ec.key.colorrange",
|
||||||
|
"ec.key.extract",
|
||||||
|
"ec.key.differencematte",
|
||||||
|
"ec.key.colordifference",
|
||||||
|
"ec.key.screen",
|
||||||
|
"ec.key.spill",
|
||||||
|
"ec.key.advancedspill",
|
||||||
|
"ec.key.keycleaner",
|
||||||
|
"ec.key.unmult",
|
||||||
|
"ec.matte.simplechoker",
|
||||||
|
"ec.matte.mattechoker",
|
||||||
|
"ec.matte.refinesoft",
|
||||||
|
"ec.matte.refinehard",
|
||||||
|
"ec.channel.setmatte",
|
||||||
|
"ec.channel.setchannels",
|
||||||
|
"ec.channel.shiftchannels",
|
||||||
|
"ec.channel.removecolormatting",
|
||||||
|
"ec.channel.arithmetic",
|
||||||
|
"ec.channel.solidcomposite",
|
||||||
|
"ec.channel.combiner",
|
||||||
|
"ec.channel.blend",
|
||||||
|
"ec.channel.calculations",
|
||||||
|
"ec.channel.compoundarithmetic",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
|
||||||
|
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
match id {
|
||||||
|
"ec.key.colorkey" | "ec.key.luma" | "ec.key.colorrange" | "ec.key.extract" => matte_key(e, id, ctx, b),
|
||||||
|
"ec.key.differencematte" => difference_matte(e, ctx, b),
|
||||||
|
"ec.key.screen" => screen_key(e, ctx, b),
|
||||||
|
"ec.key.advancedspill" => advanced_spill(e, ctx, b),
|
||||||
|
"ec.key.keycleaner" => key_cleaner(e, ctx, b),
|
||||||
|
"ec.matte.simplechoker" => simple_choker(e, ctx, b),
|
||||||
|
"ec.matte.mattechoker" => matte_choker(e, ctx, b),
|
||||||
|
"ec.matte.refinesoft" => refine(e, ctx, b, false),
|
||||||
|
"ec.matte.refinehard" => refine(e, ctx, b, true),
|
||||||
|
"ec.channel.setchannels" => set_channels(e, ctx, b),
|
||||||
|
_ => point(e, id, ctx, b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- helpers
|
||||||
|
|
||||||
|
/// One `fxk_point` pass over `src`'s size.
|
||||||
|
fn run(e: &mut Enc, p: &Params, src: &GpuImage, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> GpuImage {
|
||||||
|
let out = e.image(src.width, src.height);
|
||||||
|
e.pixels("fxk_point", p, src, aux, &out, data);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn op(code: u32) -> Params {
|
||||||
|
let mut p = Params::default();
|
||||||
|
p.u[0][0] = code;
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
|
fn src_index(s: Src) -> u32 {
|
||||||
|
SRC_ORDER.iter().position(|x| *x == s).unwrap_or(10) as 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<Option<GpuImage>> {
|
||||||
|
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() {
|
||||||
|
return Some(Some(e.image(w, h)));
|
||||||
|
}
|
||||||
|
let ls = ctx.layer_size;
|
||||||
|
let os = o.size;
|
||||||
|
let (sx, sy) = if stretch && ls[0] > 0.0 && ls[1] > 0.0 { (os[0] / ls[0], os[1] / ls[1]) } else { (1.0, 1.0) };
|
||||||
|
let (dx, dy) = if stretch { (0.0, 0.0) } else { ((os[0] - ls[0]) * 0.5, (os[1] - ls[1]) * 0.5) };
|
||||||
|
let inv = 1.0 / b.scale.max(1e-9);
|
||||||
|
// Per column / row: the other layer's buffer coordinate.
|
||||||
|
let mut data = Vec::with_capacity((w + h) as usize);
|
||||||
|
data.extend((0..w).map(|x| (((x as f64 + 0.5 - b.offset[0]) * inv * sx + dx) * o.buf.scale + o.buf.offset[0]) as f32));
|
||||||
|
data.extend((0..h).map(|y| (((y as f64 + 0.5 - b.offset[1]) * inv * sy + dy) * o.buf.scale + o.buf.offset[1]) as f32));
|
||||||
|
let src = e.g.upload_image(&o.buf.img)?;
|
||||||
|
let buf = e.data(&data);
|
||||||
|
let out = e.image(w, h);
|
||||||
|
e.pixels("fxk_fit", &Params::default(), &src, None, &out, Some(&buf));
|
||||||
|
Some(Some(out))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `util::layer_or_self`.
|
||||||
|
fn layer_or_self(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str, stretch: bool) -> Option<GpuImage> {
|
||||||
|
Some(fitted(e, ctx, b, id, true, stretch)?.unwrap_or_else(|| b.img.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// keying::thin_feather on a matte plane (x channel).
|
||||||
|
fn thin_feather(e: &mut Enc, m: GpuImage, thin: f64, feather: f64) -> GpuImage {
|
||||||
|
let m = if thin > 0.0 {
|
||||||
|
morph_frac(e, &m, thin, ERODE_X)
|
||||||
|
} else if thin < 0.0 {
|
||||||
|
morph_frac(e, &m, -thin, DILATE_X)
|
||||||
|
} else {
|
||||||
|
m
|
||||||
|
};
|
||||||
|
if feather > 0.0 { gaussian_blur(e, &m, feather * 0.5, feather * 0.5, true) } else { m }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// keying::apply_matte (view 0 final, 1 source, 2 matte).
|
||||||
|
fn apply_matte(e: &mut Enc, b: GBuf, m: &GpuImage, view: u32) -> GBuf {
|
||||||
|
if view == 1 {
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
let mut p = op(5);
|
||||||
|
p.u[0][1] = view;
|
||||||
|
let img = run(e, &p, &b.img, Some(m), None);
|
||||||
|
GBuf { img, ..b }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// keying::primary.
|
||||||
|
fn primary(c: [f32; 4]) -> [u32; 3] {
|
||||||
|
if c[1] >= c[0] && c[1] >= c[2] {
|
||||||
|
[1, 0, 2]
|
||||||
|
} else if c[2] >= c[0] && c[2] >= c[1] {
|
||||||
|
[2, 0, 1]
|
||||||
|
} else {
|
||||||
|
[0, 1, 2]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// util::set_alpha with the new alpha in `na`'s x channel.
|
||||||
|
fn set_alpha(e: &mut Enc, img: &GpuImage, na: &GpuImage) -> GpuImage {
|
||||||
|
let fill = gaussian_blur(e, img, 3.0, 3.0, true);
|
||||||
|
let (rows, row) = e.image_rows(&fill);
|
||||||
|
let mut p = op(27);
|
||||||
|
p.u[3][0] = row;
|
||||||
|
run(e, &p, img, Some(na), Some(&rows))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- keyers
|
||||||
|
|
||||||
|
fn matte_key(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let pr = ctx.params;
|
||||||
|
let f = |k: &str| pr.f(k) as f32;
|
||||||
|
let mut p = Params::default();
|
||||||
|
let mut thin = 0.0;
|
||||||
|
let mut feather = 0.0;
|
||||||
|
match id {
|
||||||
|
"ec.key.colorkey" => {
|
||||||
|
let k = pr.color("keyColor");
|
||||||
|
p.u[0][0] = 1;
|
||||||
|
p.f[0] = [k[0], k[1], k[2], f("colorTolerance") / 255.0];
|
||||||
|
thin = pr.f("edgeThin") * b.scale;
|
||||||
|
feather = pr.f("edgeFeather") * b.scale;
|
||||||
|
}
|
||||||
|
"ec.key.luma" => {
|
||||||
|
p.u[0] = [2, pr.e("keyType"), 0, 0];
|
||||||
|
p.f[0] = [f("threshold") / 255.0, f("tolerance") / 255.0, 0.0, 0.0];
|
||||||
|
thin = pr.f("edgeThin") * b.scale;
|
||||||
|
feather = pr.f("edgeFeather") * b.scale;
|
||||||
|
}
|
||||||
|
"ec.key.colorrange" => {
|
||||||
|
p.u[0] = [3, pr.e("colorSpace"), 0, 0];
|
||||||
|
p.f[0] = [f("minL"), f("minA"), f("minB"), 0.0];
|
||||||
|
p.f[1] = [f("maxL"), f("maxA"), f("maxB"), 0.0];
|
||||||
|
p.f[2][0] = f("fuzziness");
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
p.u[0] = [4, pr.e("channel"), pr.b("invert") as u32, 0];
|
||||||
|
p.f[0] = [f("blackPoint"), f("whitePoint"), f("blackSoftness"), f("whiteSoftness")];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let m = run(e, &p, &b.img, None, None);
|
||||||
|
let m = thin_feather(e, m, thin, feather);
|
||||||
|
Some(apply_matte(e, b, &m, 0))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn difference_matte(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let pr = ctx.params;
|
||||||
|
let view = pr.e("view");
|
||||||
|
if view == 1 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let blur = pr.f("blurBeforeDifference") * b.scale;
|
||||||
|
let other = layer_or_self(e, ctx, &b, "differenceLayer", pr.e("ifLayerSizesDiffer") == 1)?;
|
||||||
|
let (src, other) = if blur > 0.05 {
|
||||||
|
(gaussian_blur(e, &b.img, blur * 0.5, blur * 0.5, true), gaussian_blur(e, &other, blur * 0.5, blur * 0.5, true))
|
||||||
|
} else {
|
||||||
|
(b.img.clone(), other)
|
||||||
|
};
|
||||||
|
let mut p = op(6);
|
||||||
|
p.f[0] = [(pr.f("matchingTolerance") / 100.0) as f32, (pr.f("matchingSoftness") / 100.0) as f32, 0.0, 0.0];
|
||||||
|
let m = run(e, &p, &src, Some(&other), None);
|
||||||
|
Some(apply_matte(e, b, &m, if view == 2 { 3 } else { 4 }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// keying::secondary's screen-key matte and composite.
|
||||||
|
fn screen_key(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let pr = ctx.params;
|
||||||
|
let view = pr.e("view");
|
||||||
|
if view == 3 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let sc = pr.color("screenColor");
|
||||||
|
let ix = primary(sc);
|
||||||
|
let (pi, o1, o2) = (ix[0] as usize, ix[1] as usize, ix[2] as usize);
|
||||||
|
let gain = pr.f("screenGain") as f32 / 100.0;
|
||||||
|
let bal = pr.f("screenBalance") as f32 / 100.0;
|
||||||
|
let cb = pr.f("clipBlack") as f32 / 100.0;
|
||||||
|
let cw = pr.f("clipWhite") as f32 / 100.0;
|
||||||
|
let preblur = pr.f("screenPreblur") * b.scale;
|
||||||
|
let sg = pr.f("screenShrinkGrow") * b.scale;
|
||||||
|
let soft = pr.f("screenSoftness") * b.scale;
|
||||||
|
let (hi, lo) = if sc[o1] > sc[o2] { (sc[o1], sc[o2]) } else { (sc[o2], sc[o1]) };
|
||||||
|
let sd = (sc[pi] - (bal * hi + (1.0 - bal) * lo)).max(1e-3);
|
||||||
|
let key_src = if preblur > 0.0 { gaussian_blur(e, &b.img, preblur * 0.5, preblur * 0.5, true) } else { b.img.clone() };
|
||||||
|
let mut p = op(22);
|
||||||
|
p.u[1] = [ix[0], ix[1], ix[2], 0];
|
||||||
|
p.f[0] = [sd, gain, bal, cb];
|
||||||
|
p.f[1][0] = cw;
|
||||||
|
let mut m = run(e, &p, &key_src, None, None);
|
||||||
|
if sg < 0.0 {
|
||||||
|
m = morph_frac(e, &m, -sg, ERODE_X);
|
||||||
|
} else if sg > 0.0 {
|
||||||
|
m = morph_frac(e, &m, sg, DILATE_X);
|
||||||
|
}
|
||||||
|
if soft > 0.0 {
|
||||||
|
m = gaussian_blur(e, &m, soft * 0.5, soft * 0.5, true);
|
||||||
|
}
|
||||||
|
let mut p = op(23);
|
||||||
|
p.u[0] = [23, view, (pr.e("edgeColor") == 1) as u32, 0];
|
||||||
|
p.u[1] = [ix[0], ix[1], ix[2], 0];
|
||||||
|
p.f[0] = [sc[0], sc[1], sc[2], bal];
|
||||||
|
p.f[1][0] = pr.f("despill") as f32 / 100.0;
|
||||||
|
let img = run(e, &p, &b.img, Some(&m), None);
|
||||||
|
Some(GBuf { img, ..b })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advanced_spill(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let pr = ctx.params;
|
||||||
|
let ultra = pr.e("method") == 1;
|
||||||
|
let key = pr.color("ultraSettings/keyColor");
|
||||||
|
let mut p = op(8);
|
||||||
|
let sums = if ultra {
|
||||||
|
let ix = primary(key);
|
||||||
|
p.u[1] = [ix[0], ix[1], ix[2], 0];
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
// Standard: the dominant key primary over the whole buffer (sum reduction).
|
||||||
|
p.u[1][3] = 1;
|
||||||
|
let mut cur = b.img.clone();
|
||||||
|
let mut first = true;
|
||||||
|
while first || cur.width > 1 || cur.height > 1 {
|
||||||
|
let out = e.image(cur.width.div_ceil(16), cur.height.div_ceil(16));
|
||||||
|
let mut rp = Params::default();
|
||||||
|
rp.u[0][0] = first as u32;
|
||||||
|
e.pixels("fxk_reduce", &rp, &cur, None, &out, None);
|
||||||
|
cur = out;
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
Some(cur)
|
||||||
|
};
|
||||||
|
let tol = if ultra { pr.get("ultraSettings/tolerance").map(|v| v.as_f64()).unwrap_or(100.0) as f32 / 100.0 } else { 1.0 };
|
||||||
|
let f = |k: &str| pr.f(k) as f32 / 100.0;
|
||||||
|
let (desat, neutral) = if ultra { (f("ultraSettings/desaturate"), f("ultraSettings/spillColorCorrection")) } else { (0.0, 0.0) };
|
||||||
|
p.f[0] = [f("suppression"), f("ultraSettings/spillRange"), f("ultraSettings/lumaCorrection"), tol];
|
||||||
|
p.f[1] = [desat, neutral, rgb_to_hsl(key[0], key[1], key[2]).0, 0.0];
|
||||||
|
let img = run(e, &p, &b.img, sums.as_ref(), None);
|
||||||
|
Some(GBuf { img, ..b })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn key_cleaner(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let pr = ctx.params;
|
||||||
|
let r = (pr.f("additionalEdgeRadius") * b.scale).max(0.0);
|
||||||
|
let strength = pr.f("strength") as f32 / 100.0;
|
||||||
|
let chatter = pr.b("reduceChatter");
|
||||||
|
let mut a = run(e, &op(24), &b.img, None, None);
|
||||||
|
if chatter {
|
||||||
|
a = gaussian_blur(e, &a, 1.0, 1.0, true);
|
||||||
|
}
|
||||||
|
let est = (r > 0.0 && strength > 0.0).then(|| gaussian_blur(e, &b.img, r * 0.5, r * 0.5, true));
|
||||||
|
let rows = est.as_ref().map(|img| e.image_rows(img));
|
||||||
|
let mut p = op(25);
|
||||||
|
p.u[0][1] = chatter as u32;
|
||||||
|
p.u[3] = [rows.as_ref().map_or(0, |r| r.1), rows.is_some() as u32, 0, 0];
|
||||||
|
p.f[0] = [pr.f("alphaContrast") as f32 / 100.0, strength, 0.0, 0.0];
|
||||||
|
let img = run(e, &p, &b.img, Some(&a), rows.as_ref().map(|r| &r.0));
|
||||||
|
Some(GBuf { img, ..b })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- mattes
|
||||||
|
|
||||||
|
fn simple_choker(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||||
|
let c = ctx.params.f("chokeMatte") * b.scale;
|
||||||
|
if c.abs() > 1e-6 {
|
||||||
|
if c < 0.0 && !ctx.adjustment {
|
||||||
|
b.pad(e, (-c).ceil() as u32 + 1)?;
|
||||||
|
}
|
||||||
|
let a = run(e, &op(24), &b.img, None, None);
|
||||||
|
let na = if c > 0.0 { morph_frac(e, &a, c, ERODE_X) } else { morph_frac(e, &a, -c, DILATE_X) };
|
||||||
|
b.img = set_alpha(e, &b.img, &na);
|
||||||
|
}
|
||||||
|
if ctx.params.e("view") == 1 {
|
||||||
|
b.img = run(e, &op(28), &b.img, None, None);
|
||||||
|
}
|
||||||
|
Some(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matte_choker(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||||
|
let pr = ctx.params;
|
||||||
|
let g1 = pr.f("geometricSoftness1") * b.scale;
|
||||||
|
let c1 = pr.f("choke1") as f32;
|
||||||
|
let s1 = pr.f("grayLevelSoftness1") as f32 / 100.0;
|
||||||
|
let g2 = pr.f("geometricSoftness2") * b.scale;
|
||||||
|
let c2 = pr.f("choke2") as f32;
|
||||||
|
let s2 = pr.f("grayLevelSoftness2") as f32 / 100.0;
|
||||||
|
let it = pr.f("iterations").round().clamp(1.0, 100.0) as usize;
|
||||||
|
if (c1 < 0.0 || c2 < 0.0 || g1 > 0.0 || g2 > 0.0) && !ctx.adjustment {
|
||||||
|
b.pad(e, ((g1 + g2) * 1.5 * it as f64).ceil() as u32 + 1)?;
|
||||||
|
}
|
||||||
|
let mut a = run(e, &op(24), &b.img, None, None);
|
||||||
|
let stage = |e: &mut Enc, a: GpuImage, geo: f64, choke: f32, gray: f32| -> GpuImage {
|
||||||
|
let a = if geo > 0.0 { gaussian_blur(e, &a, geo * 0.5, geo * 0.5, true) } else { a };
|
||||||
|
let t = 0.5 + choke / 255.0 * 0.5;
|
||||||
|
let w = gray.max(1e-3);
|
||||||
|
if (t - 0.5).abs() < 1e-6 && (w - 1.0).abs() < 1e-6 {
|
||||||
|
return a;
|
||||||
|
}
|
||||||
|
let mut p = op(26);
|
||||||
|
p.f[0] = [t, w, 0.0, 0.0];
|
||||||
|
run(e, &p, &a, None, None)
|
||||||
|
};
|
||||||
|
for _ in 0..it {
|
||||||
|
a = stage(e, a, g1, c1, s1);
|
||||||
|
a = stage(e, a, g2, c2, s2);
|
||||||
|
}
|
||||||
|
b.img = set_alpha(e, &b.img, &a);
|
||||||
|
Some(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// matte::refine (Refine Soft / Hard Matte) for one frame: Reduce Chatter, the matte motion
|
||||||
|
/// blur (they read neighbouring frames) and Invert run on the CPU.
|
||||||
|
fn refine(e: &mut Enc, ctx: &EffectCtx, b: GBuf, hard: bool) -> Option<GBuf> {
|
||||||
|
let pr = ctx.params;
|
||||||
|
// Invert gives alpha to the empty areas, whose colour then comes from a blur of nearly
|
||||||
|
// transparent pixels (util::set_alpha): ill-conditioned, so it renders on the CPU.
|
||||||
|
if pr.f("reduceChatter") > 0.0 || pr.b("useMotionBlur") || pr.b("invert") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let dk = |id: &str| if hard { id.to_string() } else { format!("decontamination/{id}") };
|
||||||
|
let r = (pr.f("radius") * b.scale).round().max(1.0) as usize;
|
||||||
|
let smooth = pr.f("smooth") as f32 / 100.0;
|
||||||
|
let feather = pr.f("feather") / 100.0 * r as f64 * 0.5;
|
||||||
|
let choke = (pr.f("choke") - if hard { 0.0 } else { pr.f("shiftEdge") }).clamp(-100.0, 100.0) as f32 / 100.0 * 0.5;
|
||||||
|
let decon = pr.b("decontaminate");
|
||||||
|
let decon_amt = pr.f(&dk("decontaminationAmount")) as f32 / 100.0;
|
||||||
|
let extend = pr.b(&dk("extendWhereSmoothed"));
|
||||||
|
let decon_r = r as f64 + pr.f(&dk("increaseDecontaminationRadius")).max(0.0) * b.scale;
|
||||||
|
let view_map = pr.b(&dk("viewDecontaminationMap"));
|
||||||
|
let contrast = if hard { pr.f("alphaContrast") as f32 / 100.0 } else { 1.0 + pr.f("contrast").max(0.0) as f32 / 100.0 * 3.0 };
|
||||||
|
let edge_details = hard || pr.b("calculateEdgeDetails");
|
||||||
|
let view_edges = !hard && pr.b("viewEdgeRegion");
|
||||||
|
let (w, h) = (b.img.width, b.img.height);
|
||||||
|
|
||||||
|
// (luminance guide, alpha, guide × alpha, guide²); the edge band from the alpha's range.
|
||||||
|
let pack = run(e, &op(29), &b.img, None, None);
|
||||||
|
let hi = morph_frac(e, &pack, r as f64, [2, 1, 2, 2]);
|
||||||
|
let lo = morph_frac(e, &pack, r as f64, [2, 0, 2, 2]);
|
||||||
|
let band = run(e, &op(32), &hi, Some(&lo), None);
|
||||||
|
let mut na = if edge_details {
|
||||||
|
let eps = 1e-4 + smooth * smooth * 0.05;
|
||||||
|
let means = box_passes(e, &pack, &[r], &[r], true);
|
||||||
|
let mut p = op(30);
|
||||||
|
p.f[0][0] = eps;
|
||||||
|
let coef = run(e, &p, &means, None, None);
|
||||||
|
let coef = box_passes(e, &coef, &[r], &[r], true);
|
||||||
|
let (rows, row) = e.image_rows(&band);
|
||||||
|
let mut p = op(31);
|
||||||
|
p.u[3][0] = row;
|
||||||
|
run(e, &p, &coef, Some(&pack), Some(&rows))
|
||||||
|
} else {
|
||||||
|
let a = run(e, &op(24), &b.img, None, None);
|
||||||
|
if smooth > 0.0 {
|
||||||
|
let s = smooth as f64 * r as f64 * 0.5;
|
||||||
|
gaussian_blur(e, &a, s, s, true)
|
||||||
|
} else {
|
||||||
|
a
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if feather > 0.0 {
|
||||||
|
na = gaussian_blur(e, &na, feather, feather, true);
|
||||||
|
}
|
||||||
|
let mut p = op(33);
|
||||||
|
p.u[0][1] = pr.b("invert") as u32;
|
||||||
|
p.f[0] = [choke, contrast, 0.0, 0.0];
|
||||||
|
let na = run(e, &p, &na, None, None);
|
||||||
|
if view_edges {
|
||||||
|
let img = run(e, &op(34), &na, Some(&band), None);
|
||||||
|
return Some(GBuf { img, ..b });
|
||||||
|
}
|
||||||
|
let est = (decon && decon_amt > 0.0 && !view_map).then(|| gaussian_blur(e, &b.img, decon_r * 0.5, decon_r * 0.5, true));
|
||||||
|
let rows = est.as_ref().map(|img| e.image_rows(img));
|
||||||
|
let mut p = op(35);
|
||||||
|
p.u[0] = [35, view_map as u32, (decon && (view_map || decon_amt > 0.0)) as u32, extend as u32];
|
||||||
|
p.u[3] = [rows.as_ref().map_or(0, |r| r.1), rows.is_some() as u32, 0, 0];
|
||||||
|
p.f[0][0] = decon_amt;
|
||||||
|
let img = run(e, &p, &b.img, Some(&na), rows.as_ref().map(|r| &r.0));
|
||||||
|
if view_map {
|
||||||
|
return Some(GBuf { img, ..b });
|
||||||
|
}
|
||||||
|
debug_assert_eq!((img.width, img.height), (w, h));
|
||||||
|
let img = set_alpha(e, &img, &na);
|
||||||
|
Some(GBuf { img, ..b })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- channel
|
||||||
|
|
||||||
|
const SET_CHANNEL_LAYERS: [&str; 4] = ["sourceLayer1", "sourceLayer2", "sourceLayer3", "sourceLayer4"];
|
||||||
|
|
||||||
|
const SHIFT_ORDER: [Src; 11] =
|
||||||
|
[Src::Alpha, Src::Red, Src::Green, Src::Blue, Src::Luminance, Src::Hue, Src::Lightness, Src::Saturation, Src::Full, Src::Half, Src::Off];
|
||||||
|
|
||||||
|
const MATTE_ORDER: [Src; 10] = [Src::Red, Src::Green, Src::Blue, Src::Alpha, Src::Luminance, Src::Hue, Src::Lightness, Src::Saturation, Src::Full, Src::Off];
|
||||||
|
|
||||||
|
fn set_channels(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let pr = ctx.params;
|
||||||
|
let s = ["setRedTo", "setGreenTo", "setBlueTo", "setAlphaTo"].map(|id| src_index(effectcraft_effects::util::src_at(pr.e(id))));
|
||||||
|
let stretch = pr.b("stretchLayersToFit");
|
||||||
|
let mut layers = vec![];
|
||||||
|
for id in SET_CHANNEL_LAYERS {
|
||||||
|
layers.push(fitted(e, ctx, &b, id, true, stretch)?);
|
||||||
|
}
|
||||||
|
if layers.iter().all(Option::is_none) {
|
||||||
|
return route(e, b, s);
|
||||||
|
}
|
||||||
|
let mut vals = e.image(b.img.width, b.img.height);
|
||||||
|
for (k, layer) in layers.iter().enumerate() {
|
||||||
|
let mut p = op(20);
|
||||||
|
p.u[0] = [20, k as u32, s[k], 0];
|
||||||
|
vals = run(e, &p, &vals, Some(layer.as_ref().unwrap_or(&b.img)), None);
|
||||||
|
}
|
||||||
|
let img = run(e, &op(21), &vals, None, None);
|
||||||
|
Some(GBuf { img, ..b })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// channel::route.
|
||||||
|
fn route(e: &mut Enc, b: GBuf, s: [u32; 4]) -> Option<GBuf> {
|
||||||
|
if s == [0, 1, 2, 3] {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let mut p = op(11);
|
||||||
|
p.u[1] = s;
|
||||||
|
let img = run(e, &p, &b.img, None, None);
|
||||||
|
Some(GBuf { img, ..b })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let pr = ctx.params;
|
||||||
|
let f = |k: &str| pr.f(k) as f32;
|
||||||
|
let mut p = Params::default();
|
||||||
|
let mut aux = None;
|
||||||
|
match id {
|
||||||
|
"ec.key.spill" => {
|
||||||
|
let k = pr.color("colorToSuppress");
|
||||||
|
let amt = f("suppression") / 100.0;
|
||||||
|
if pr.e("colorAccuracy") == 1 {
|
||||||
|
let kg = (k[0] + k[1] + k[2]) / 3.0;
|
||||||
|
let kc = [k[0] - kg, k[1] - kg, k[2] - kg];
|
||||||
|
let kn = (kc[0] * kc[0] + kc[1] * kc[1] + kc[2] * kc[2]).sqrt();
|
||||||
|
if kn < 1e-5 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
p.u[0] = [7, 1, 0, 0];
|
||||||
|
p.f[0] = [kc[0] / kn, kc[1] / kn, kc[2] / kn, amt];
|
||||||
|
} else {
|
||||||
|
let ix = primary(k);
|
||||||
|
p.u[0] = [7, 0, 0, 0];
|
||||||
|
p.u[1] = [ix[0], ix[1], ix[2], 0];
|
||||||
|
p.f[0][3] = amt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"ec.key.colordifference" => {
|
||||||
|
let view = pr.e("view");
|
||||||
|
if view == 0 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let accurate = pr.e("colorMatchingAccuracy") == 1;
|
||||||
|
let lin = |c: [f32; 3]| if accurate { c.map(|v| effectcraft_color::srgb_to_linear(v.clamp(0.0, 1.0))) } else { c };
|
||||||
|
let k = pr.color("keyColor");
|
||||||
|
let key = lin([k[0], k[1], k[2]]);
|
||||||
|
let ix = primary([key[0], key[1], key[2], 1.0]);
|
||||||
|
let (pi, o1, o2) = (ix[0] as usize, ix[1] as usize, ix[2] as usize);
|
||||||
|
let lv = |pre: &str| ["InBlack", "InWhite", "Gamma", "OutBlack", "OutWhite"].map(|s| f(&format!("{pre}{s}")));
|
||||||
|
let (pa, pb) = (lv("partialA"), lv("partialB"));
|
||||||
|
p.u[0] = [9, view, accurate as u32, 0];
|
||||||
|
p.u[1] = [ix[0], ix[1], ix[2], 0];
|
||||||
|
p.f[0] = [(key[pi] - key[o1]).max(1e-3), (key[pi] - key[o2]).max(1e-3), f("blackLevel"), f("whiteLevel")];
|
||||||
|
p.f[1] = [pa[0], pa[1], pa[2], pa[3]];
|
||||||
|
p.f[2] = [pa[4], pb[4], f("gamma"), 0.0];
|
||||||
|
p.f[3] = [pb[0], pb[1], pb[2], pb[3]];
|
||||||
|
}
|
||||||
|
"ec.key.unmult" => {
|
||||||
|
let white = pr.e("backgroundColor") == 1;
|
||||||
|
let level = if white { pr.f("whiteLevel") } else { pr.f("blackLevel") }.clamp(0.0, 100.0) as f32 / 100.0;
|
||||||
|
p.u[0] = [10, white as u32, pr.b("removeColorMatting") as u32, pr.b("clipHdrResults") as u32];
|
||||||
|
p.f[0] = [level, pr.f("softness").clamp(0.0, 100.0) as f32 / 100.0, 0.0, 0.0];
|
||||||
|
}
|
||||||
|
"ec.channel.shiftchannels" => {
|
||||||
|
let s = ["takeRedFrom", "takeGreenFrom", "takeBlueFrom", "takeAlphaFrom"].map(|id| src_index(SHIFT_ORDER[(pr.e(id) as usize).min(10)]));
|
||||||
|
return route(e, b, s);
|
||||||
|
}
|
||||||
|
"ec.channel.removecolormatting" => {
|
||||||
|
p.u[0] = [12, pr.b("clipHdr") as u32, 0, 0];
|
||||||
|
p.f[0] = pr.color("backgroundColor");
|
||||||
|
}
|
||||||
|
"ec.channel.arithmetic" => {
|
||||||
|
p.u[0] = [13, pr.e("operator"), pr.b("clip") as u32, 0];
|
||||||
|
p.f[0] = [f("redValue") / 255.0, f("greenValue") / 255.0, f("blueValue") / 255.0, 0.0];
|
||||||
|
}
|
||||||
|
"ec.channel.solidcomposite" => {
|
||||||
|
const MODES: [effectcraft_color::BlendMode; 12] = {
|
||||||
|
use effectcraft_color::BlendMode::*;
|
||||||
|
[Normal, Add, Multiply, Screen, Overlay, SoftLight, HardLight, Darken, Lighten, Difference, Color, Luminosity]
|
||||||
|
};
|
||||||
|
let c = pr.color("color");
|
||||||
|
let opa = (f("opacity") / 100.0 * c[3]).clamp(0.0, 1.0);
|
||||||
|
p.u[0] = [14, crate::ops::mode_id(MODES[(pr.e("blendingMode") as usize).min(11)]), 0, 0];
|
||||||
|
p.f[0] = [c[0] * opa, c[1] * opa, c[2] * opa, opa];
|
||||||
|
p.f[1][0] = f("sourceOpacity") / 100.0;
|
||||||
|
}
|
||||||
|
"ec.channel.setmatte" => {
|
||||||
|
let src = MATTE_ORDER[(pr.e("takeMatteFrom") as usize).min(9)];
|
||||||
|
aux = fitted(e, ctx, &b, "takeMatteFromLayer", true, pr.b("stretchMatteToFit"))?;
|
||||||
|
let pre_ok = !matches!(src, Src::Alpha | Src::Full | Src::Off);
|
||||||
|
let flags = pr.b("invertMatte") as u32
|
||||||
|
| (pr.b("compositeMatteWithOriginal") as u32) << 1
|
||||||
|
| ((pr.b("premultiplyMatteLayer") && pre_ok) as u32) << 2
|
||||||
|
| (aux.is_none() as u32) << 3
|
||||||
|
| ((src == Src::Alpha) as u32) << 4;
|
||||||
|
p.u[0] = [15, src_index(src), flags, 0];
|
||||||
|
}
|
||||||
|
"ec.channel.combiner" => {
|
||||||
|
if pr.b("useSecondLayer") {
|
||||||
|
aux = Some(layer_or_self(e, ctx, &b, "sourceLayer", true)?);
|
||||||
|
}
|
||||||
|
let flags = pr.b("invert") as u32 | (pr.b("solidAlpha") as u32) << 1 | (aux.is_some() as u32) << 2;
|
||||||
|
p.u[0] = [16, pr.e("from"), pr.e("to"), flags];
|
||||||
|
}
|
||||||
|
"ec.channel.blend" => {
|
||||||
|
let orig = (f("blendWithOriginal") / 100.0).clamp(0.0, 1.0);
|
||||||
|
if orig >= 1.0 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
aux = Some(layer_or_self(e, ctx, &b, "blendWithLayer", pr.e("ifLayerSizesDiffer") == 1)?);
|
||||||
|
p.u[0] = [17, pr.e("mode"), 0, 0];
|
||||||
|
p.f[0][0] = orig;
|
||||||
|
}
|
||||||
|
"ec.channel.calculations" => {
|
||||||
|
const MODES: [effectcraft_color::BlendMode; 22] = {
|
||||||
|
use effectcraft_color::BlendMode::*;
|
||||||
|
[
|
||||||
|
Normal,
|
||||||
|
Darken,
|
||||||
|
Multiply,
|
||||||
|
ColorBurn,
|
||||||
|
LinearBurn,
|
||||||
|
Add,
|
||||||
|
Lighten,
|
||||||
|
Screen,
|
||||||
|
ColorDodge,
|
||||||
|
LinearDodge,
|
||||||
|
Overlay,
|
||||||
|
SoftLight,
|
||||||
|
HardLight,
|
||||||
|
LinearLight,
|
||||||
|
VividLight,
|
||||||
|
PinLight,
|
||||||
|
HardMix,
|
||||||
|
Difference,
|
||||||
|
Exclusion,
|
||||||
|
Hue,
|
||||||
|
Saturation,
|
||||||
|
Luminosity,
|
||||||
|
]
|
||||||
|
};
|
||||||
|
if pr.get("secondLayer").and_then(|v| v.as_layer()).is_some() {
|
||||||
|
aux = Some(layer_or_self(e, ctx, &b, "secondLayer", pr.b("stretchSecondLayerToFit"))?);
|
||||||
|
}
|
||||||
|
let flags =
|
||||||
|
pr.b("invertInput") as u32 | (pr.b("invertSecondLayer") as u32) << 1 | (pr.b("preserveTransparency") as u32) << 2 | (aux.is_some() as u32) << 3;
|
||||||
|
let mode = MODES[(pr.e("blendingMode") as usize).min(MODES.len() - 1)];
|
||||||
|
p.u[0] = [18, pr.e("inputChannel"), pr.e("secondLayerChannel"), crate::ops::mode_id(mode)];
|
||||||
|
p.u[1][0] = flags;
|
||||||
|
p.f[0][0] = (f("secondLayerOpacity") / 100.0).clamp(0.0, 1.0);
|
||||||
|
}
|
||||||
|
"ec.channel.compoundarithmetic" => {
|
||||||
|
let orig = (f("blendWithOriginal") / 100.0).clamp(0.0, 1.0);
|
||||||
|
if orig >= 1.0 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
aux = Some(layer_or_self(e, ctx, &b, "secondSource", pr.b("stretchSecondSourceToFit"))?);
|
||||||
|
p.u[0] = [19, pr.e("operator"), pr.e("operateOnChannels"), pr.e("overflowBehavior")];
|
||||||
|
p.f[0][0] = orig;
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
let img = run(e, &p, &b.img, aux.as_ref(), None);
|
||||||
|
Some(GBuf { img, ..b })
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,581 @@
|
||||||
|
//! GPU effects, noise, blur and time family (kernels in `shaders/fx_noise.wgsl`, every entry point prefixed
|
||||||
|
//! `fxn_`): the CPU effects' exact steps with the pixel loops as compute kernels.
|
||||||
|
//!
|
||||||
|
//! Median, Median (Legacy) and Dust & Scratches take the median of the 512-level quantised
|
||||||
|
//! channels by bisection over the window (radius ≤ [`MEDIAN_MAX_R`] on the GPU). Fractal and
|
||||||
|
//! Turbulent Noise resolve every per-octave term on the CPU in the CPU's f32 order
|
||||||
|
//! (`effectcraft_effects::fractal_gpu`) and evaluate the pixels on the GPU. Echo and Posterize
|
||||||
|
//! Time fetch the layer's other frames through the effect host like the CPU does, then
|
||||||
|
//! accumulate on the GPU. Remove Grain measures the grain level in its sample boxes on the CPU
|
||||||
|
//! (one readback) and runs the guided filters, Texture, Unsharp Mask and Preview on the GPU;
|
||||||
|
//! Temporal Filtering and the Noise Samples / Blending Matte views render on the CPU.
|
||||||
|
|
||||||
|
use effectcraft_color::BlendMode;
|
||||||
|
use effectcraft_effects::{Buf, EffectCtx};
|
||||||
|
use effectcraft_keyframe::Value;
|
||||||
|
|
||||||
|
use crate::context::{Enc, GpuImage, Params};
|
||||||
|
use crate::effects::{GBuf, box_passes, gaussian_blur};
|
||||||
|
|
||||||
|
/// Compute entry points in `fx_noise.wgsl`.
|
||||||
|
pub(crate) const KERNELS: &[&str] = &[
|
||||||
|
"fxn_quant",
|
||||||
|
"fxn_median",
|
||||||
|
"fxn_median_out",
|
||||||
|
"fxn_edge",
|
||||||
|
"fxn_smart_edges",
|
||||||
|
"fxn_unsharp",
|
||||||
|
"fxn_pick",
|
||||||
|
"fxn_cblur_out",
|
||||||
|
"fxn_straight",
|
||||||
|
"fxn_premul",
|
||||||
|
"fxn_morph",
|
||||||
|
"fxn_crop",
|
||||||
|
"fxn_cb_map",
|
||||||
|
"fxn_cb_level",
|
||||||
|
"fxn_echo",
|
||||||
|
"fxn_echo_fin",
|
||||||
|
"fxn_fractal",
|
||||||
|
"fxn_noise_alpha",
|
||||||
|
"fxn_noise_hls",
|
||||||
|
"fxn_gf_guide",
|
||||||
|
"fxn_gf",
|
||||||
|
"fxn_rg_texture",
|
||||||
|
"fxn_rg_unsharp",
|
||||||
|
"fxn_rg_out",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Effect ids implemented here (Fractal Noise's other modes come through [`fractal_extra`]).
|
||||||
|
pub(crate) const IDS: &[&str] = &[
|
||||||
|
"ec.noise.turbulent",
|
||||||
|
"ec.noise.median",
|
||||||
|
"ec.noise.medianlegacy",
|
||||||
|
"ec.noise.dustscratches",
|
||||||
|
"ec.noise.removegrain",
|
||||||
|
"ec.noise.noisealpha",
|
||||||
|
"ec.noise.noisehls",
|
||||||
|
"ec.noise.noisehlsauto",
|
||||||
|
"ec.blur.smart",
|
||||||
|
"ec.blur.bilateral",
|
||||||
|
"ec.blur.sharpen",
|
||||||
|
"ec.blur.unsharp",
|
||||||
|
"ec.blur.compound",
|
||||||
|
"ec.blur.channel",
|
||||||
|
"ec.channel.minimax",
|
||||||
|
"ec.time.echo",
|
||||||
|
"ec.time.posterizetime",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Largest median radius (buffer pixels) the GPU takes; larger ones render on the CPU.
|
||||||
|
pub(crate) const MEDIAN_MAX_R: usize = 16;
|
||||||
|
|
||||||
|
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
|
||||||
|
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
match id {
|
||||||
|
"ec.noise.turbulent" => fractal(e, ctx, b, true),
|
||||||
|
"ec.noise.median" | "ec.noise.medianlegacy" | "ec.noise.dustscratches" => median(e, id, ctx, b),
|
||||||
|
"ec.noise.removegrain" => remove_grain(e, ctx, b),
|
||||||
|
"ec.noise.noisealpha" => noise_alpha(e, ctx, b),
|
||||||
|
"ec.noise.noisehls" => noise_hls(e, ctx, b, ctx.params.f("noisePhase") as f32 / 360.0),
|
||||||
|
"ec.noise.noisehlsauto" => noise_hls(e, ctx, b, ctx.time as f32 * ctx.params.f("noiseAnimationSpeed") as f32),
|
||||||
|
"ec.blur.smart" => smart_blur(e, ctx, b),
|
||||||
|
"ec.blur.bilateral" => bilateral(e, ctx, b),
|
||||||
|
"ec.blur.sharpen" => {
|
||||||
|
let s = b.scale;
|
||||||
|
unsharp(e, b, ctx.params.f("amount") as f32 / 50.0, s, 0.0)
|
||||||
|
}
|
||||||
|
"ec.blur.unsharp" => {
|
||||||
|
let s = (ctx.params.f("radius") * b.scale).max(0.1);
|
||||||
|
unsharp(e, b, ctx.params.f("amount") as f32 / 100.0, s, ctx.params.f("threshold") as f32)
|
||||||
|
}
|
||||||
|
"ec.blur.compound" => compound_blur(e, ctx, b),
|
||||||
|
"ec.blur.channel" => channel_blur(e, ctx, b),
|
||||||
|
"ec.channel.minimax" => minimax(e, ctx, b),
|
||||||
|
"ec.time.echo" => echo(e, ctx, b),
|
||||||
|
"ec.time.posterizetime" => posterize_time(e, ctx, b),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A same-size per-pixel kernel.
|
||||||
|
fn run(e: &mut Enc, entry: &str, p: &Params, src: &GpuImage, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> GpuImage {
|
||||||
|
let out = e.image(src.width, src.height);
|
||||||
|
e.pixels(entry, p, src, aux, &out, data);
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_u0(u: [u32; 4]) -> Params {
|
||||||
|
let mut p = Params::default();
|
||||||
|
p.u[0] = u;
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Fractal / Turbulent Noise (noise3.rs)
|
||||||
|
|
||||||
|
/// Fractal Noise settings the `pointwise` kernel (effects.rs) leaves to the CPU, which this
|
||||||
|
/// family's kernel covers: every Noise and Fractal Type, Cycle Evolution, Perspective Offset,
|
||||||
|
/// Sub Rotation / Offset and all Blending Modes.
|
||||||
|
pub(crate) fn fractal_extra(ctx: &EffectCtx) -> bool {
|
||||||
|
let pr = ctx.params;
|
||||||
|
pr.e("noiseType") != 2
|
||||||
|
|| pr.e("fractalType") > 2
|
||||||
|
|| pr.e("blendingMode") > 1
|
||||||
|
|| pr.b("evolutionOptions/cycleEvolution")
|
||||||
|
|| pr.b("transform/perspectiveOffset")
|
||||||
|
|| pr.f("subSettings/subRotation") != 0.0
|
||||||
|
|| (!pr.b("subSettings/centerSubscale") && pr.v2("subSettings/subOffset") != [0.0, 0.0])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// noise3::render.
|
||||||
|
pub(crate) fn fractal(e: &mut Enc, ctx: &EffectCtx, b: GBuf, turbulent: bool) -> Option<GBuf> {
|
||||||
|
let fr = effectcraft_effects::fractal_gpu(ctx, b.offset, b.scale, turbulent);
|
||||||
|
let mode = match fr.mode {
|
||||||
|
None => 255,
|
||||||
|
Some(m) => BlendMode::ALL.iter().position(|&x| x == m).unwrap_or(0) as u32,
|
||||||
|
};
|
||||||
|
let data: Vec<f32> = fr.octaves.iter().flatten().copied().collect();
|
||||||
|
let cycle = fr.cycle.unwrap_or([0.0; 3]);
|
||||||
|
let mut p = Params::default();
|
||||||
|
p.u[0] = [fr.kind, fr.noise_type, fr.octaves.len() as u32, fr.seed];
|
||||||
|
p.u[1] = [fr.sub_rotation as u32, fr.cycle.is_some() as u32, fr.invert as u32, fr.overflow];
|
||||||
|
p.u[2][0] = mode;
|
||||||
|
p.f[0] = [fr.rot.0, fr.rot.1, fr.size[0], fr.size[1]];
|
||||||
|
p.f[1] = [fr.contrast, fr.brightness, fr.opacity, fr.blend];
|
||||||
|
p.f[2] = [cycle[0], cycle[1], cycle[2], fr.norm];
|
||||||
|
let buf = e.data(&data);
|
||||||
|
let img = run(e, "fxn_fractal", &p, &b.img, None, Some(&buf));
|
||||||
|
Some(GBuf { img, ..b })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Median family (noise.rs, noise2.rs)
|
||||||
|
|
||||||
|
/// noise::median_image (of the straight colour with alpha 1 when `straight`).
|
||||||
|
fn median_image(e: &mut Enc, img: &GpuImage, r: usize, straight: bool) -> GpuImage {
|
||||||
|
let q = run(e, "fxn_quant", &with_u0([straight as u32, 0, 0, 0]), img, None, None);
|
||||||
|
run(e, "fxn_median", &with_u0([r as u32, 0, 0, 0]), &q, None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn median(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let r = (ctx.params.f("radius") * b.scale).round().max(0.0) as usize;
|
||||||
|
if r == 0 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
if r > MEDIAN_MAX_R {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let (mode, on_alpha, straight) = match id {
|
||||||
|
"ec.noise.medianlegacy" => {
|
||||||
|
let on = ctx.params.b("operateOnAlphaChannel");
|
||||||
|
(if on { 2 } else { 3 }, on, !on)
|
||||||
|
}
|
||||||
|
"ec.noise.dustscratches" => (1, ctx.params.b("operateOnAlpha"), false),
|
||||||
|
_ => (0, ctx.params.b("operateOnAlpha"), false),
|
||||||
|
};
|
||||||
|
let m = median_image(e, &b.img, r, straight);
|
||||||
|
let mut p = with_u0([mode, on_alpha as u32, 0, 0]);
|
||||||
|
if mode == 1 {
|
||||||
|
p.f[0][0] = ctx.params.f("threshold") as f32 / 255.0;
|
||||||
|
}
|
||||||
|
let img = run(e, "fxn_median_out", &p, &b.img, Some(&m), None);
|
||||||
|
Some(GBuf { img, ..b })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Remove Grain (noise.rs)
|
||||||
|
|
||||||
|
fn remove_grain(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let pr = ctx.params;
|
||||||
|
let amt = pr.f("noiseReductionSettings/noiseReduction") as f32;
|
||||||
|
let um_amount = pr.f("unsharpMask/amount") as f32 / 100.0;
|
||||||
|
let view = pr.get("viewingMode").map(Value::as_enum).unwrap_or(3);
|
||||||
|
// Noise Samples / Blending Matte views and Temporal Filtering render on the CPU.
|
||||||
|
if view == 1 || view == 2 || pr.b("temporalFiltering/enabled") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if amt <= 0.0 && um_amount <= 0.0 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
// The grain level measured in the sample boxes (CPU; only the smoothing uses it).
|
||||||
|
let level = if amt > 0.0 {
|
||||||
|
let img = e.download(&b.img)?;
|
||||||
|
effectcraft_effects::remove_grain_level(ctx, &Buf { img, offset: b.offset, scale: b.scale })
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
let passes = pr.f("noiseReductionSettings/passes").round().clamp(1.0, 8.0) as usize;
|
||||||
|
let multichannel = pr.e("noiseReductionSettings/mode") == 0;
|
||||||
|
let texture = (pr.f("fineTuning/texture") as f32).clamp(0.0, 1.0);
|
||||||
|
let r = (2.0 * b.scale).round().max(1.0) as usize;
|
||||||
|
let eps = (0.02 * amt * level).powi(2);
|
||||||
|
let orig = b.img.clone();
|
||||||
|
let mut ch = orig.clone();
|
||||||
|
if amt > 0.0 {
|
||||||
|
for _ in 0..passes {
|
||||||
|
let g = run(e, "fxn_gf_guide", &with_u0([!multichannel as u32, 0, 0, 0]), &ch, None, None);
|
||||||
|
ch = guided_filter(e, &g, &ch, r, eps);
|
||||||
|
}
|
||||||
|
if texture > 0.0 {
|
||||||
|
let mut p = Params::default();
|
||||||
|
p.f[0][0] = texture;
|
||||||
|
ch = run(e, "fxn_rg_texture", &p, &ch, Some(&orig), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if um_amount > 0.0 {
|
||||||
|
let radius = (pr.f("unsharpMask/radius") * b.scale).max(0.1);
|
||||||
|
let low = gaussian_blur(e, &ch, radius, radius, true);
|
||||||
|
let mut p = Params::default();
|
||||||
|
p.f[0] = [um_amount, pr.f("unsharpMask/threshold") as f32 / 255.0, 0.0, 0.0];
|
||||||
|
ch = run(e, "fxn_rg_unsharp", &p, &ch, Some(&low), None);
|
||||||
|
}
|
||||||
|
let mut p = Params::default();
|
||||||
|
if view == 0 {
|
||||||
|
let c = b.to_px(pr.v2("previewRegion/center"));
|
||||||
|
let (hw, hh) = (pr.f("previewRegion/width") * b.scale * 0.5, pr.f("previewRegion/height") * b.scale * 0.5);
|
||||||
|
let col = pr.color("previewRegion/boxColor");
|
||||||
|
p.u[0] = [1, pr.b("previewRegion/showBox") as u32, 0, 0];
|
||||||
|
p.f[0] = [(c.0 - hw) as f32, (c.0 + hw) as f32, (c.1 - hh) as f32, (c.1 + hh) as f32];
|
||||||
|
p.f[1] = [col[0], col[1], col[2], 1.0];
|
||||||
|
}
|
||||||
|
let img = run(e, "fxn_rg_out", &p, &ch, Some(&orig), None);
|
||||||
|
Some(GBuf { img, ..b })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One guided-filter arithmetic pass (`fxn_gf`), `d` = an image read through `data`.
|
||||||
|
fn gf(e: &mut Enc, mode: u32, s: &GpuImage, a: &GpuImage, d: Option<&GpuImage>, eps: f32) -> GpuImage {
|
||||||
|
let mut p = with_u0([mode, 0, 0, 0]);
|
||||||
|
p.f[0][0] = eps;
|
||||||
|
let rows = d.map(|d| e.image_rows(d));
|
||||||
|
if let Some((_, row)) = &rows {
|
||||||
|
p.u[0][1] = *row;
|
||||||
|
}
|
||||||
|
run(e, "fxn_gf", &p, s, Some(a), rows.as_ref().map(|r| &r.0))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// util::guided_filter on every channel at once (guide `g`, input `p`).
|
||||||
|
fn guided_filter(e: &mut Enc, g: &GpuImage, p: &GpuImage, r: usize, eps: f32) -> GpuImage {
|
||||||
|
let bx = |e: &mut Enc, img: &GpuImage| box_passes(e, img, &[r], &[r], true);
|
||||||
|
let mean_g = bx(e, g);
|
||||||
|
let mean_p = bx(e, p);
|
||||||
|
let gp = gf(e, 5, g, p, None, 0.0);
|
||||||
|
let gg = gf(e, 6, g, g, None, 0.0);
|
||||||
|
let corr_gp = bx(e, &gp);
|
||||||
|
let corr_gg = bx(e, &gg);
|
||||||
|
let cov = gf(e, 0, &corr_gp, &mean_p, Some(&mean_g), 0.0);
|
||||||
|
let var = gf(e, 1, &corr_gg, &mean_g, None, 0.0);
|
||||||
|
let a = gf(e, 2, &cov, &var, None, eps);
|
||||||
|
let bb = gf(e, 3, &a, &mean_p, Some(&mean_g), 0.0);
|
||||||
|
let ma = bx(e, &a);
|
||||||
|
let mb = bx(e, &bb);
|
||||||
|
gf(e, 4, &ma, &mb, Some(g), 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Noise Alpha / Noise HLS (noise.rs)
|
||||||
|
|
||||||
|
/// noise::phased_cycle's two seeds and smoothed fraction for `phase`.
|
||||||
|
fn phase_terms(seed: u32, phase: f32, cycle: i64) -> (u32, u32, f32) {
|
||||||
|
let k = phase.floor();
|
||||||
|
let t = phase - k;
|
||||||
|
let k = k as i64;
|
||||||
|
let wrap = |k: i64| if cycle > 0 { k.rem_euclid(cycle) } else { k } as u32;
|
||||||
|
(seed.wrapping_add(wrap(k).wrapping_mul(0x632b)), seed.wrapping_add(wrap(k + 1).wrapping_mul(0x632b)), t * t * (3.0 - 2.0 * t))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn noise_alpha(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let k = ctx.params.f("amount") as f32 / 100.0;
|
||||||
|
if k <= 0.0 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let kind = ctx.params.e("noise");
|
||||||
|
let seed = (ctx.params.f("randomSeed") as i64 as u32) ^ ctx.seed.wrapping_mul(0x2f1);
|
||||||
|
let phase = ctx.params.f("noisePhase") as f32 / 360.0 + if kind >= 2 { ctx.time as f32 } else { 0.0 };
|
||||||
|
let cycle = if ctx.params.b("noiseOptions/cycleNoise") { ctx.params.f("noiseOptions/cycle").round().max(1.0) as i64 } else { 0 };
|
||||||
|
let ph = if kind >= 2 { phase } else { phase.floor() };
|
||||||
|
let (sa, sb, t) = phase_terms(seed, ph, cycle);
|
||||||
|
let mut p = with_u0([kind % 2, ctx.params.e("originalAlpha"), ctx.params.e("overflow"), sa]);
|
||||||
|
p.u[1][0] = sb;
|
||||||
|
p.f[0] = [k, t, 0.0, 0.0];
|
||||||
|
let img = run(e, "fxn_noise_alpha", &p, &b.img, None, None);
|
||||||
|
Some(GBuf { img, ..b })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn noise_hls(e: &mut Enc, ctx: &EffectCtx, b: GBuf, phase: f32) -> Option<GBuf> {
|
||||||
|
let hue = ctx.params.f("hue") as f32 / 100.0;
|
||||||
|
let light = ctx.params.f("lightness") as f32 / 100.0;
|
||||||
|
let satv = ctx.params.f("saturation") as f32 / 100.0;
|
||||||
|
if hue == 0.0 && light == 0.0 && satv == 0.0 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let gs = (ctx.params.f("grainSize") * b.scale).max(0.1) as f32;
|
||||||
|
let seed = ctx.seed.wrapping_mul(0x51f3);
|
||||||
|
let mut seeds = [0u32; 6];
|
||||||
|
let mut t = 0.0;
|
||||||
|
for k in 0..3u32 {
|
||||||
|
let (sa, sb, tt) = phase_terms(seed.wrapping_add(k * 7919), phase, 0);
|
||||||
|
seeds[2 * k as usize] = sa;
|
||||||
|
seeds[2 * k as usize + 1] = sb;
|
||||||
|
t = tt;
|
||||||
|
}
|
||||||
|
let mut p = with_u0([ctx.params.e("noise"), seed, 0, 0]);
|
||||||
|
p.u[1] = [seeds[0], seeds[1], seeds[2], seeds[3]];
|
||||||
|
p.u[2] = [seeds[4], seeds[5], 0, 0];
|
||||||
|
p.f[0] = [hue, light, satv, gs];
|
||||||
|
p.f[1] = [phase, t, 0.0, 0.0];
|
||||||
|
let img = run(e, "fxn_noise_hls", &p, &b.img, None, None);
|
||||||
|
Some(GBuf { img, ..b })
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- blurs (blur2.rs, misc.rs)
|
||||||
|
|
||||||
|
/// blur2::edge_preserving (+ Bilateral's grey output with `grey`).
|
||||||
|
fn edge_preserving(e: &mut Enc, img: &GpuImage, r: f64, range: f32, hard: bool, grey: bool) -> GpuImage {
|
||||||
|
let ri = r.ceil() as u32;
|
||||||
|
let stride = ((r / 6.0).ceil() as u32).max(1);
|
||||||
|
let sig_s2 = 2.0 * (r / 2.0).max(0.5).powi(2);
|
||||||
|
let sig_r2 = 2.0 * range.max(1e-3).powi(2);
|
||||||
|
let d2max = (r * r + 0.5).floor() as u32;
|
||||||
|
let mut p = with_u0([ri, stride, hard as u32, d2max]);
|
||||||
|
p.u[1][0] = grey as u32;
|
||||||
|
p.f[0] = [sig_s2 as f32, sig_r2, range, 0.0];
|
||||||
|
run(e, "fxn_edge", &p, img, None, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bilateral(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let r = ctx.params.f("radius") * b.scale;
|
||||||
|
if r < 0.5 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let range = ctx.params.f("threshold") as f32 / 100.0;
|
||||||
|
let img = edge_preserving(e, &b.img, r, range, false, !ctx.params.b("colorize"));
|
||||||
|
Some(GBuf { img, ..b })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn smart_blur(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let r = ctx.params.f("radius") * b.scale;
|
||||||
|
let thr = ctx.params.f("threshold") as f32 / 100.0;
|
||||||
|
let mode = ctx.params.e("mode");
|
||||||
|
let blurred = if r >= 0.5 { edge_preserving(e, &b.img, r, thr, true, false) } else { b.img.clone() };
|
||||||
|
if mode == 0 {
|
||||||
|
return Some(GBuf { img: blurred, ..b });
|
||||||
|
}
|
||||||
|
let mut p = with_u0([mode, 0, 0, 0]);
|
||||||
|
p.f[0][0] = thr.max(0.01) * 0.5;
|
||||||
|
let img = run(e, "fxn_smart_edges", &p, &blurred, None, None);
|
||||||
|
Some(GBuf { img, ..b })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// misc::unsharp_core (Sharpen, Unsharp Mask).
|
||||||
|
fn unsharp(e: &mut Enc, b: GBuf, amount: f32, sigma: f64, threshold: f32) -> Option<GBuf> {
|
||||||
|
let blurred = gaussian_blur(e, &b.img, sigma, sigma, true);
|
||||||
|
let mut p = Params::default();
|
||||||
|
p.f[0] = [amount, threshold, 0.0, 0.0];
|
||||||
|
let img = run(e, "fxn_unsharp", &p, &b.img, Some(&blurred), None);
|
||||||
|
Some(GBuf { img, ..b })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dims_xy(dim: u32) -> (f64, f64) {
|
||||||
|
match dim {
|
||||||
|
1 => (1.0, 0.0),
|
||||||
|
2 => (0.0, 1.0),
|
||||||
|
_ => (1.0, 1.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pick(e: &mut Enc, acc: &GpuImage, from: &GpuImage, mask: [bool; 4], alpha: bool) -> GpuImage {
|
||||||
|
let mut p = with_u0([alpha as u32, 0, 0, 0]);
|
||||||
|
p.u[1] = mask.map(|m| m as u32);
|
||||||
|
run(e, "fxn_pick", &p, acc, Some(from), None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn channel_blur(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||||
|
let sig = ["redBlurriness", "greenBlurriness", "blueBlurriness", "alphaBlurriness"].map(|id| ctx.params.f(id).max(0.0) * 0.5 * b.scale);
|
||||||
|
if sig.iter().all(|&s| s < 0.05) {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let (kx, ky) = dims_xy(ctx.params.e("dimensions"));
|
||||||
|
let repeat = ctx.params.b("repeatEdge") || ctx.adjustment;
|
||||||
|
let maxs = sig.iter().cloned().fold(0.0, f64::max);
|
||||||
|
if !repeat {
|
||||||
|
b.pad(e, (maxs * 3.0).ceil() as u32 + 1)?;
|
||||||
|
}
|
||||||
|
// n: each colour channel blurred by its own amount; a: the alpha blurred by the same amount
|
||||||
|
// (its divisor), alpha = the Alpha Blurriness result.
|
||||||
|
let mut n = b.img.clone();
|
||||||
|
let mut a = pick(e, &b.img, &b.img, [true; 4], true);
|
||||||
|
let mut done: Vec<u64> = Vec::new();
|
||||||
|
for &s in &sig {
|
||||||
|
if s < 0.05 || done.contains(&s.to_bits()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
done.push(s.to_bits());
|
||||||
|
let bl = gaussian_blur(e, &b.img, s * kx, s * ky, true);
|
||||||
|
let on = |c: usize| sig[c] >= 0.05 && sig[c].to_bits() == s.to_bits();
|
||||||
|
n = pick(e, &n, &bl, [on(0), on(1), on(2), false], false);
|
||||||
|
a = pick(e, &a, &bl, [on(0), on(1), on(2), on(3)], true);
|
||||||
|
}
|
||||||
|
b.img = run(e, "fxn_cblur_out", &Params::default(), &n, Some(&a), None);
|
||||||
|
Some(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compound_blur(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||||
|
let max_sigma = ctx.params.f("maximumBlur").max(0.0) * 0.5 * b.scale;
|
||||||
|
if max_sigma < 0.05 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let invert = ctx.params.b("invertBlur");
|
||||||
|
if !ctx.adjustment {
|
||||||
|
b.pad(e, (max_sigma * 3.0).ceil() as u32 + 1)?;
|
||||||
|
}
|
||||||
|
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||||
|
// The blur map: the Blur Layer fitted to the buffer (util::fit_layer), else the layer itself.
|
||||||
|
let map = match ctx.layer_param("blurLayer", true) {
|
||||||
|
Some(o) => {
|
||||||
|
if o.buf.img.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let stretch = ctx.params.b("stretchMapToFit");
|
||||||
|
let (ls, os) = (ctx.layer_size, o.size);
|
||||||
|
let (sx, sy) = if stretch && ls[0] > 0.0 && ls[1] > 0.0 { (os[0] / ls[0], os[1] / ls[1]) } else { (1.0, 1.0) };
|
||||||
|
let (dx, dy) = if stretch { (0.0, 0.0) } else { ((os[0] - ls[0]) * 0.5, (os[1] - ls[1]) * 0.5) };
|
||||||
|
let inv = 1.0 / b.scale.max(1e-9);
|
||||||
|
let col = |x: usize| (((x as f64 + 0.5 - b.offset[0]) * inv) * sx + dx) * o.buf.scale + o.buf.offset[0];
|
||||||
|
let row = |y: usize| (((y as f64 + 0.5 - b.offset[1]) * inv) * sy + dy) * o.buf.scale + o.buf.offset[1];
|
||||||
|
let data: Vec<f32> = (0..w).map(|x| col(x) as f32).chain((0..h).map(|y| row(y) as f32)).collect();
|
||||||
|
let layer = e.g.upload_image(&o.buf.img)?;
|
||||||
|
let buf = e.data(&data);
|
||||||
|
let mut p = with_u0([1, invert as u32, 0, 0]);
|
||||||
|
p.u[1][0] = w as u32;
|
||||||
|
let out = e.image(b.img.width, b.img.height);
|
||||||
|
e.pixels("fxn_cb_map", &p, &layer, None, &out, Some(&buf));
|
||||||
|
out
|
||||||
|
}
|
||||||
|
None => run(e, "fxn_cb_map", &with_u0([0, invert as u32, 0, 0]), &b.img, None, None),
|
||||||
|
};
|
||||||
|
const N: usize = 5;
|
||||||
|
let levels: Vec<GpuImage> = (0..N)
|
||||||
|
.map(|k| {
|
||||||
|
if k == 0 {
|
||||||
|
b.img.clone()
|
||||||
|
} else {
|
||||||
|
let s = max_sigma * k as f64 / (N - 1) as f64;
|
||||||
|
gaussian_blur(e, &b.img, s, s, ctx.adjustment)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let (rows, row) = e.image_rows(&map);
|
||||||
|
let out = e.image(b.img.width, b.img.height);
|
||||||
|
for k in 0..N - 1 {
|
||||||
|
e.pixels("fxn_cb_level", &with_u0([k as u32, row, 0, 0]), &levels[k], Some(&levels[k + 1]), &out, Some(&rows));
|
||||||
|
}
|
||||||
|
b.img = out;
|
||||||
|
Some(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Minimax (channel.rs)
|
||||||
|
|
||||||
|
/// util::morph_plane per channel (`modes`: 0 min, 1 max, 2 keep) with radii (`rx`, `ry`).
|
||||||
|
fn morph(e: &mut Enc, img: &GpuImage, rx: usize, ry: usize, modes: [u32; 4]) -> GpuImage {
|
||||||
|
let mut cur = img.clone();
|
||||||
|
for (vertical, r) in [(0, rx), (1, ry)] {
|
||||||
|
if r == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let mut p = with_u0([r as u32, vertical, 0, 0]);
|
||||||
|
p.u[1] = modes;
|
||||||
|
cur = run(e, "fxn_morph", &p, &cur, None, None);
|
||||||
|
}
|
||||||
|
cur
|
||||||
|
}
|
||||||
|
|
||||||
|
fn minimax(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||||
|
let r = (ctx.params.f("radius") * b.scale).round().max(0.0) as usize;
|
||||||
|
if r == 0 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let op = ctx.params.e("operation");
|
||||||
|
let dont_shrink = ctx.params.b("dontShrinkEdges");
|
||||||
|
let (rx, ry) = match ctx.params.e("direction") {
|
||||||
|
1 => (r, 0),
|
||||||
|
2 => (0, r),
|
||||||
|
_ => (r, r),
|
||||||
|
};
|
||||||
|
let sel: [bool; 4] = match ctx.params.e("channel") {
|
||||||
|
1 => [false, false, false, true],
|
||||||
|
2 => [true, true, true, true],
|
||||||
|
3 => [true, false, false, false],
|
||||||
|
4 => [false, true, false, false],
|
||||||
|
5 => [false, false, true, false],
|
||||||
|
_ => [true, true, true, false],
|
||||||
|
};
|
||||||
|
if sel[3] && op != 0 && !ctx.adjustment {
|
||||||
|
b.pad(e, rx.max(ry) as u32)?;
|
||||||
|
}
|
||||||
|
let (w, h) = (b.img.width, b.img.height);
|
||||||
|
let mut planes = run(e, "fxn_straight", &Params::default(), &b.img, None, None);
|
||||||
|
// Beyond the layer edge is empty (0) unless Don't Shrink Edges repeats the edge pixels.
|
||||||
|
if !dont_shrink {
|
||||||
|
let (pw, ph) = (w + 2 * rx as u32, h + 2 * ry as u32);
|
||||||
|
if !e.g.fits(pw, ph) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let padded = e.image(pw, ph);
|
||||||
|
e.copy_into(&planes, &padded, rx as u32, ry as u32);
|
||||||
|
planes = padded;
|
||||||
|
}
|
||||||
|
let modes = |max: bool| sel.map(|s| if s { max as u32 } else { 2 });
|
||||||
|
let stages: &[bool] = match op {
|
||||||
|
0 => &[false],
|
||||||
|
1 => &[true],
|
||||||
|
2 => &[false, true],
|
||||||
|
_ => &[true, false],
|
||||||
|
};
|
||||||
|
for &max in stages {
|
||||||
|
planes = morph(e, &planes, rx, ry, modes(max));
|
||||||
|
}
|
||||||
|
if !dont_shrink {
|
||||||
|
let out = e.image(w, h);
|
||||||
|
e.pixels("fxn_crop", &with_u0([rx as u32, ry as u32, 0, 0]), &planes, None, &out, None);
|
||||||
|
planes = out;
|
||||||
|
}
|
||||||
|
b.img = run(e, "fxn_premul", &Params::default(), &planes, None, None);
|
||||||
|
Some(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Echo / Posterize Time (time_fx.rs)
|
||||||
|
|
||||||
|
fn echo(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let n = ctx.params.f("numberOfEchoes").round().clamp(0.0, 255.0) as usize;
|
||||||
|
let dt = ctx.params.f("echoTime");
|
||||||
|
let start = ctx.params.f("startingIntensity").clamp(0.0, 1.0) as f32;
|
||||||
|
let decay = ctx.params.f("decay").clamp(0.0, 1.0) as f32;
|
||||||
|
let op = ctx.params.e("echoOperator");
|
||||||
|
let times: Vec<f64> = (0..=n).map(|i| ctx.time + i as f64 * dt).collect();
|
||||||
|
// Without the effect host (or the frames) the layer passes through, as on the CPU.
|
||||||
|
let Some((grid, imgs)) = effectcraft_effects::time_frames(ctx, ×) else { return Some(b) };
|
||||||
|
let (w, h) = (grid.img.width, grid.img.height);
|
||||||
|
let mut acc = [e.image(w, h), e.image(w, h)];
|
||||||
|
for (i, img) in imgs.iter().enumerate() {
|
||||||
|
let frame = e.g.upload_image(img)?;
|
||||||
|
let mut p = with_u0([op, (i == 0) as u32, 0, 0]);
|
||||||
|
p.f[0][0] = start * decay.powi(i as i32);
|
||||||
|
let [a, o] = &acc;
|
||||||
|
e.pixels("fxn_echo", &p, a, Some(&frame), o, None);
|
||||||
|
acc.swap(0, 1);
|
||||||
|
}
|
||||||
|
let mut p = with_u0([op, 0, 0, 0]);
|
||||||
|
p.f[0][0] = (n + 1) as f32;
|
||||||
|
let img = run(e, "fxn_echo_fin", &p, &acc[0], None, None);
|
||||||
|
Some(GBuf { img, offset: grid.offset, scale: grid.scale })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn posterize_time(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
if ctx.env.host.is_none() {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let tt = effectcraft_effects::posterized_time(ctx.time, ctx.params.f("frameRate"));
|
||||||
|
match effectcraft_effects::time_frames(ctx, &[tt]) {
|
||||||
|
Some((grid, mut imgs)) => {
|
||||||
|
let img = e.g.upload_image(&imgs.pop().unwrap_or_default())?;
|
||||||
|
Some(GBuf { img, offset: grid.offset, scale: grid.scale })
|
||||||
|
}
|
||||||
|
None => Some(b),
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,381 @@
|
||||||
|
//! GPU effects, tonal and colour correction family (kernels in `shaders/fx_tone.wgsl`, every
|
||||||
|
//! entry point prefixed `fxt_`): one per-pixel pass each (`fxt_point`) with the CPU effect's
|
||||||
|
//! exact maths.
|
||||||
|
//!
|
||||||
|
//! Levels (Individual Controls), Gamma/Pedestal/Gain, Photo Filter, Change Color, Change to
|
||||||
|
//! Color, Leave Color, Broadcast Colors, Color Balance (HLS), Video Limiter, PS Arbitrary Map,
|
||||||
|
//! CC Toner, CC Color Offset and CC Kernel, plus Hue/Saturation's colour ranges and Levels'
|
||||||
|
//! per-channel controls (the parts the base kernels in `kernels.wgsl` leave to the CPU). Auto
|
||||||
|
//! Levels / Contrast / Color and Equalize measure the frame's histograms on the CPU (the image
|
||||||
|
//! is read back for that) and apply the correction on the GPU.
|
||||||
|
|
||||||
|
use effectcraft_color::rgb_to_hsl;
|
||||||
|
use effectcraft_effects::EffectCtx;
|
||||||
|
|
||||||
|
use crate::context::{Enc, Params};
|
||||||
|
use crate::effects::{GBuf, gaussian_blur};
|
||||||
|
|
||||||
|
/// Compute entry points in `fx_tone.wgsl`.
|
||||||
|
pub(crate) const KERNELS: &[&str] = &["fxt_point"];
|
||||||
|
|
||||||
|
/// Effect ids implemented here.
|
||||||
|
pub(crate) const IDS: &[&str] = &[
|
||||||
|
"ec.color.levelsic",
|
||||||
|
"ec.color.gammapedestalgain",
|
||||||
|
"ec.color.photofilter",
|
||||||
|
"ec.color.changecolor",
|
||||||
|
"ec.color.changetocolor",
|
||||||
|
"ec.color.leavecolor",
|
||||||
|
"ec.color.broadcast",
|
||||||
|
"ec.color.colorbalancehls",
|
||||||
|
"ec.color.videolimiter",
|
||||||
|
"ec.color.psarbitrarymap",
|
||||||
|
"ec.color.cctoner",
|
||||||
|
"ec.color.cccoloroffset",
|
||||||
|
"ec.color.cckernel",
|
||||||
|
"ec.color.autolevels",
|
||||||
|
"ec.color.autocontrast",
|
||||||
|
"ec.color.autocolor",
|
||||||
|
"ec.color.equalize",
|
||||||
|
"ec.color.shadowhighlight",
|
||||||
|
"ec.color.cccolorneutralizer",
|
||||||
|
"ec.color.colorstabilizer",
|
||||||
|
];
|
||||||
|
|
||||||
|
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
|
||||||
|
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
match id {
|
||||||
|
"ec.color.shadowhighlight" => shadow_highlight(e, ctx, b),
|
||||||
|
_ => point(e, id, ctx, b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Invert in the HLS and YIQ channels (the base `pointwise` kernel does RGB and Alpha).
|
||||||
|
pub(crate) fn invert(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let mut p = op(18);
|
||||||
|
p.u[0][1] = ctx.params.e("channel");
|
||||||
|
p.f[0][0] = 1.0 - ctx.params.f("blend") as f32 / 100.0;
|
||||||
|
run(e, &p, b, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shadow/Highlight: the amounts (Auto Amounts) and the Black / White Clip ranges are measured
|
||||||
|
/// on the CPU; the luminance bases are blurred and the image adjusted on the GPU.
|
||||||
|
fn shadow_highlight(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let pr = ctx.params;
|
||||||
|
let src = e.download(&b.img)?;
|
||||||
|
let (s_amt, h_amt) = effectcraft_effects::shadow_highlight_amounts(ctx, &src);
|
||||||
|
let (bc, wc) = (pr.f("moreOptions/blackClip").clamp(0.0, 49.0) / 100.0, pr.f("moreOptions/whiteClip").clamp(0.0, 49.0) / 100.0);
|
||||||
|
let clip0 = (bc > 0.0 || wc > 0.0).then(|| effectcraft_effects::luma_clip_points(&src, bc, wc));
|
||||||
|
let s_r = pr.f("moreOptions/shadowRadius").max(0.0) * b.scale / 2.0;
|
||||||
|
let h_r = pr.f("moreOptions/highlightRadius").max(0.0) * b.scale / 2.0;
|
||||||
|
let (w, h) = (b.img.width, b.img.height);
|
||||||
|
let luma = e.image(w, h);
|
||||||
|
e.pixels("fxt_point", &op(19), &b.img, None, &luma, None);
|
||||||
|
let base_s = gaussian_blur(e, &luma, s_r, s_r, true);
|
||||||
|
let base_h = if (h_r - s_r).abs() < 1e-9 { base_s.clone() } else { gaussian_blur(e, &luma, h_r, h_r, true) };
|
||||||
|
let bases = e.image(w, h);
|
||||||
|
e.pixels("fxt_point", &op(21), &base_s, Some(&base_h), &bases, None);
|
||||||
|
let mut p = op(20);
|
||||||
|
p.f[0] =
|
||||||
|
[s_amt, h_amt, (pr.f("moreOptions/shadowTonalWidth") as f32 / 100.0).max(0.01), (pr.f("moreOptions/highlightTonalWidth") as f32 / 100.0).max(0.01)];
|
||||||
|
p.f[1] = [
|
||||||
|
pr.f("moreOptions/colorCorrection") as f32 / 100.0,
|
||||||
|
pr.f("moreOptions/midtoneContrast") as f32 / 100.0,
|
||||||
|
(pr.f("blend") / 100.0).clamp(0.0, 1.0) as f32,
|
||||||
|
0.0,
|
||||||
|
];
|
||||||
|
let out = e.image(w, h);
|
||||||
|
e.pixels("fxt_point", &p, &b.img, Some(&bases), &out, None);
|
||||||
|
let Some((lo0, hi0)) = clip0 else { return Some(GBuf { img: out, ..b }) };
|
||||||
|
let adjusted = e.download(&out)?;
|
||||||
|
let (lo1, hi1) = effectcraft_effects::luma_clip_points(&adjusted, bc, wc);
|
||||||
|
if (lo0, hi0) == (lo1, hi1) || hi1 - lo1 <= 1e-3 {
|
||||||
|
return Some(GBuf { img: out, ..b });
|
||||||
|
}
|
||||||
|
let mut p = op(22);
|
||||||
|
p.f[0] = [lo0, lo1, (hi0 - lo0) / (hi1 - lo1), 0.0];
|
||||||
|
run(e, &p, GBuf { img: out, ..b }, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn rgb(c: [f32; 4]) -> [f32; 4] {
|
||||||
|
[c[0], c[1], c[2], 0.0]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hue/Saturation with colour ranges (Channel Control); Master and Colorize alone run in the
|
||||||
|
/// base `pointwise` kernel.
|
||||||
|
pub(crate) fn hue_saturation(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let pr = ctx.params;
|
||||||
|
let f = |id: String, d: f64| pr.get(&id).map(|v| v.as_f64()).unwrap_or(d) as f32;
|
||||||
|
let mut data = vec![];
|
||||||
|
for (r, _, c) in effectcraft_effects::HUESAT_RANGES {
|
||||||
|
let range = [
|
||||||
|
f(format!("{r}RangeStart"), c - 15.0),
|
||||||
|
f(format!("{r}RangeEnd"), c + 15.0),
|
||||||
|
f(format!("{r}StartFalloff"), 30.0).max(0.0),
|
||||||
|
f(format!("{r}EndFalloff"), 30.0).max(0.0),
|
||||||
|
f(format!("{r}Hue"), 0.0) / 360.0,
|
||||||
|
f(format!("{r}Saturation"), 0.0) / 100.0,
|
||||||
|
f(format!("{r}Lightness"), 0.0) / 100.0,
|
||||||
|
];
|
||||||
|
if range[4] != 0.0 || range[5] != 0.0 || range[6] != 0.0 {
|
||||||
|
data.extend(range);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let g = |k: &str| pr.f(k) as f32;
|
||||||
|
let mut p = Params::default();
|
||||||
|
p.u[0] = [14, pr.b("colorize") as u32, (data.len() / 7) as u32, 0];
|
||||||
|
p.f[0] = [g("hue") / 360.0, g("saturation") / 100.0, g("lightness") / 100.0, 0.0];
|
||||||
|
p.f[1] = [g("colorizeHue") / 360.0, g("colorizeSaturation") / 100.0, g("colorizeLightness") / 100.0, 0.0];
|
||||||
|
run(e, &p, b, Some(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Levels with Red / Green / Blue / Alpha controls (the RGB controls alone run in the base
|
||||||
|
/// `pointwise` kernel).
|
||||||
|
pub(crate) fn levels(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let pr = ctx.params;
|
||||||
|
let (cb, cw) = effectcraft_effects::levels_clip(ctx);
|
||||||
|
let mut data = vec![
|
||||||
|
pr.f("inBlack") as f32,
|
||||||
|
pr.f("inWhite") as f32,
|
||||||
|
pr.f("gamma").max(0.01) as f32,
|
||||||
|
pr.f("outBlack") as f32,
|
||||||
|
pr.f("outWhite") as f32,
|
||||||
|
cb as u32 as f32,
|
||||||
|
cw as u32 as f32,
|
||||||
|
];
|
||||||
|
for (v, (cb, cw)) in effectcraft_effects::levels_channel_settings(ctx) {
|
||||||
|
data.extend(v);
|
||||||
|
data.extend([cb as u32 as f32, cw as u32 as f32]);
|
||||||
|
}
|
||||||
|
run(e, &op(15), b, Some(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn op(code: u32) -> Params {
|
||||||
|
let mut p = Params::default();
|
||||||
|
p.u[0][0] = code;
|
||||||
|
p
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(e: &mut Enc, p: &Params, b: GBuf, data: Option<Vec<f32>>) -> Option<GBuf> {
|
||||||
|
let buf = data.map(|d| e.data(&d));
|
||||||
|
let out = e.image(b.img.width, b.img.height);
|
||||||
|
e.pixels("fxt_point", p, &b.img, None, &out, buf.as_ref());
|
||||||
|
Some(GBuf { img: out, ..b })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// color3::parse_map.
|
||||||
|
fn parse_map(s: &str) -> Vec<f32> {
|
||||||
|
let vals: Vec<f32> = s.split(|ch: char| ch == ',' || ch.is_whitespace()).filter(|t| !t.is_empty()).filter_map(|t| t.parse::<f32>().ok()).collect();
|
||||||
|
if vals.len() < 2 {
|
||||||
|
return (0..256).map(|i| i as f32 / 255.0).collect();
|
||||||
|
}
|
||||||
|
let max = vals.iter().cloned().fold(0.0f32, f32::max);
|
||||||
|
let k = if max > 1.0 { 1.0 / 255.0 } else { 1.0 };
|
||||||
|
vals.iter().map(|v| (v * k).clamp(0.0, 1.0)).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||||
|
let pr = ctx.params;
|
||||||
|
let f = |k: &str| pr.f(k) as f32;
|
||||||
|
let mut p = Params::default();
|
||||||
|
let mut data = None;
|
||||||
|
match id {
|
||||||
|
"ec.color.levelsic" => {
|
||||||
|
const IDS: [[&str; 5]; 5] = [
|
||||||
|
["rgb/rgbInBlack", "rgb/rgbInWhite", "rgb/rgbGamma", "rgb/rgbOutBlack", "rgb/rgbOutWhite"],
|
||||||
|
["red/redInBlack", "red/redInWhite", "red/redGamma", "red/redOutBlack", "red/redOutWhite"],
|
||||||
|
["green/greenInBlack", "green/greenInWhite", "green/greenGamma", "green/greenOutBlack", "green/greenOutWhite"],
|
||||||
|
["blue/blueInBlack", "blue/blueInWhite", "blue/blueGamma", "blue/blueOutBlack", "blue/blueOutWhite"],
|
||||||
|
["alpha/alphaInBlack", "alpha/alphaInWhite", "alpha/alphaGamma", "alpha/alphaOutBlack", "alpha/alphaOutWhite"],
|
||||||
|
];
|
||||||
|
let lv: Vec<[f32; 5]> = IDS.iter().map(|ids| ids.map(f)).collect();
|
||||||
|
let ident = |l: &[f32; 5]| *l == [0.0, 1.0, 1.0, 0.0, 1.0];
|
||||||
|
if lv.iter().all(ident) {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let (cb, cw) = effectcraft_effects::levels_clip(ctx);
|
||||||
|
let mut d = vec![];
|
||||||
|
for l in &lv {
|
||||||
|
d.extend(l);
|
||||||
|
d.push(ident(l) as u32 as f32);
|
||||||
|
}
|
||||||
|
p.u[0] = [1, cb as u32, cw as u32, 0];
|
||||||
|
data = Some(d);
|
||||||
|
}
|
||||||
|
"ec.color.gammapedestalgain" => {
|
||||||
|
let ch = |n: &str| [f(&format!("{n}Gamma")).max(0.01), f(&format!("{n}Pedestal")), f(&format!("{n}Gain")), 0.0];
|
||||||
|
p.u[0][0] = 2;
|
||||||
|
p.f[0] = ch("red");
|
||||||
|
p.f[1] = ch("green");
|
||||||
|
p.f[2] = ch("blue");
|
||||||
|
p.f[3][0] = f("blackStretch").max(0.0);
|
||||||
|
}
|
||||||
|
"ec.color.photofilter" => {
|
||||||
|
let fi = pr.e("filter");
|
||||||
|
let c = match effectcraft_effects::PHOTO_FILTERS.get(fi as usize) {
|
||||||
|
Some((_, rgb)) if fi != effectcraft_effects::PHOTO_FILTER_CUSTOM => rgb.map(|v| v as f32 / 255.0),
|
||||||
|
_ => {
|
||||||
|
let c = pr.color("color");
|
||||||
|
[c[0], c[1], c[2]]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
p.u[0] = [3, pr.b("preserveLuminosity") as u32, 0, 0];
|
||||||
|
p.f[0] = [c[0], c[1], c[2], f("density") / 100.0];
|
||||||
|
}
|
||||||
|
"ec.color.changetocolor" => {
|
||||||
|
let from = pr.color("from");
|
||||||
|
let to = pr.color("to");
|
||||||
|
let (fh, fs, fl) = rgb_to_hsl(from[0], from[1], from[2]);
|
||||||
|
let (th, ts, tl) = rgb_to_hsl(to[0], to[1], to[2]);
|
||||||
|
let change = pr.e("change");
|
||||||
|
let flags = matches!(change, 1 | 3) as u32
|
||||||
|
| (matches!(change, 2 | 3) as u32) << 1
|
||||||
|
| ((pr.e("changeBy") == 1) as u32) << 2
|
||||||
|
| (pr.b("viewCorrectionMatte") as u32) << 3;
|
||||||
|
p.u[0] = [4, flags, 0, 0];
|
||||||
|
p.f[0] = [fh, fs, fl, f("softness") / 100.0];
|
||||||
|
p.f[1] = [th, ts, tl, 0.0];
|
||||||
|
p.f[2] = [f("toleranceGroup/hue") / 100.0, f("toleranceGroup/lightness") / 100.0, f("toleranceGroup/saturation") / 100.0, 0.0];
|
||||||
|
}
|
||||||
|
"ec.color.leavecolor" => {
|
||||||
|
let amt = f("amount") / 100.0;
|
||||||
|
if amt <= 0.0 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let k = pr.color("color");
|
||||||
|
p.u[0] = [5, (pr.e("matchColors") == 1) as u32, 0, 0];
|
||||||
|
p.f[0] = [k[0], k[1], k[2], rgb_to_hsl(k[0], k[1], k[2]).0];
|
||||||
|
p.f[1] = [amt, f("tolerance") / 100.0, f("edgeSoftness") / 100.0, 0.0];
|
||||||
|
}
|
||||||
|
"ec.color.changecolor" => {
|
||||||
|
let view_mask = pr.e("view") == 1;
|
||||||
|
let (dh, dl, ds) = (f("hueTransform") / 360.0, f("lightnessTransform") / 100.0, f("saturationTransform") / 100.0);
|
||||||
|
if !view_mask && dh == 0.0 && dl == 0.0 && ds == 0.0 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let k = pr.color("colorToChange");
|
||||||
|
p.u[0] = [6, pr.e("matchColors"), view_mask as u32 | (pr.b("invertColorCorrectionMask") as u32) << 1, 0];
|
||||||
|
p.f[0] = [k[0], k[1], k[2], rgb_to_hsl(k[0], k[1], k[2]).0];
|
||||||
|
p.f[1] = [dh, dl, ds, 0.0];
|
||||||
|
p.f[2] = [f("matchingTolerance") / 100.0, f("matchingSoftness") / 100.0, 0.0, 0.0];
|
||||||
|
}
|
||||||
|
"ec.color.broadcast" => {
|
||||||
|
let pal = pr.e("locale") == 1;
|
||||||
|
let (setup, span) = if pal { (0.0, 100.0) } else { (7.5, 92.5) };
|
||||||
|
p.u[0] = [7, pal as u32, pr.e("howToMakeSafe"), 0];
|
||||||
|
p.f[0][0] = (f("maxSignal") - setup) / span;
|
||||||
|
}
|
||||||
|
"ec.color.colorbalancehls" => {
|
||||||
|
let (dh, dl, ds) = (f("hue") / 360.0, f("lightness") / 100.0, f("saturation") / 100.0);
|
||||||
|
if dh == 0.0 && dl == 0.0 && ds == 0.0 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
p.u[0][0] = 8;
|
||||||
|
p.f[0] = [dh, dl, ds, 0.0];
|
||||||
|
}
|
||||||
|
"ec.color.cctoner" => {
|
||||||
|
let g = |id: &str| rgb(pr.color(id));
|
||||||
|
let stops = match pr.e("tones") {
|
||||||
|
0 => vec![g("shadows"), g("highlights")],
|
||||||
|
2 => vec![g("shadows"), g("darktones"), g("midtones"), g("brights"), g("highlights")],
|
||||||
|
_ => vec![g("shadows"), g("midtones"), g("highlights")],
|
||||||
|
};
|
||||||
|
p.u[0] = [9, stops.len() as u32 - 1, 0, 0];
|
||||||
|
p.f[0][0] = (f("blend") / 100.0).clamp(0.0, 1.0);
|
||||||
|
for (i, s) in stops.iter().enumerate() {
|
||||||
|
p.f[1 + i] = *s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"ec.color.cccoloroffset" => {
|
||||||
|
let offs = ["redPhase", "greenPhase", "bluePhase"].map(|id| (pr.f(id) / 360.0).rem_euclid(1.0) as f32);
|
||||||
|
if offs.iter().all(|o| *o == 0.0) {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
p.u[0] = [10, pr.e("overflow"), 0, 0];
|
||||||
|
p.f[0] = [offs[0], offs[1], offs[2], 0.0];
|
||||||
|
}
|
||||||
|
"ec.color.cckernel" => {
|
||||||
|
let k = ["k1", "k2", "k3", "k4", "k5", "k6", "k7", "k8", "k9"].map(f);
|
||||||
|
let (scale, offset) = (f("scale"), f("offset"));
|
||||||
|
let identity = k.iter().enumerate().all(|(i, v)| if i == 4 { *v == 1.0 } else { *v == 0.0 });
|
||||||
|
if identity && scale == 1.0 && offset == 0.0 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
p.u[0] = [11, pr.b("absolute") as u32, 0, 0];
|
||||||
|
p.f[0] = [k[0], k[1], k[2], k[3]];
|
||||||
|
p.f[1] = [k[4], k[5], k[6], k[7]];
|
||||||
|
p.f[2] = [k[8], scale, offset, 0.0];
|
||||||
|
}
|
||||||
|
"ec.color.psarbitrarymap" => {
|
||||||
|
let map = parse_map(pr.s("map"));
|
||||||
|
p.u[0] = [12, pr.b("applyPhaseMapToAlpha") as u32, map.len() as u32, 0];
|
||||||
|
p.f[0][0] = (pr.f("phase") * (map.len() - 1) as f64 / 255.0) as f32;
|
||||||
|
data = Some(map);
|
||||||
|
}
|
||||||
|
"ec.color.videolimiter" => {
|
||||||
|
const CLIP_LEVELS: [f32; 5] = [0.9, 0.95, 1.0, 1.05, 1.09];
|
||||||
|
let level = CLIP_LEVELS[(pr.e("clipLevel") as usize).min(4)];
|
||||||
|
let comp = [0.0, 0.03, 0.05, 0.1, 0.2][(pr.e("compressionBeforeClipping") as usize).min(4)];
|
||||||
|
p.u[0] = [13, pr.e("clipMethod"), pr.b("gamutWarning") as u32, 0];
|
||||||
|
p.f[0] = [level, level * (1.0 - comp), 0.0, 0.0];
|
||||||
|
p.f[1] = rgb(pr.color("gamutWarningColor"));
|
||||||
|
}
|
||||||
|
"ec.color.colorstabilizer" => {
|
||||||
|
// The sample means are measured on the CPU (the frame is read back).
|
||||||
|
if ctx.env.host.is_none() {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let img = e.download(&b.img)?;
|
||||||
|
let cpu = effectcraft_effects::Buf { img, offset: b.offset, scale: b.scale };
|
||||||
|
let Some(maps) = effectcraft_effects::color_stabilizer_maps(ctx, &cpu) else { return Some(b) };
|
||||||
|
let mut d = vec![];
|
||||||
|
for m in &maps {
|
||||||
|
d.push(m.len() as f32);
|
||||||
|
for i in 0..5 {
|
||||||
|
let (x, y) = m.get(i).copied().unwrap_or((0.0, 0.0));
|
||||||
|
d.extend([x as f32, y as f32]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.u[0][0] = 24;
|
||||||
|
data = Some(d);
|
||||||
|
}
|
||||||
|
"ec.color.cccolorneutralizer" => {
|
||||||
|
let pairs = [("shadowsUnbalance", "shadowsBalance"), ("midtonesUnbalance", "midtonesBalance"), ("highlightsUnbalance", "highlightsBalance")];
|
||||||
|
for (i, (u, bal)) in pairs.iter().enumerate() {
|
||||||
|
let (u, v) = (pr.color(u), pr.color(bal));
|
||||||
|
p.f[i] = [v[0] - u[0], v[1] - u[1], v[2] - u[2], 0.0];
|
||||||
|
}
|
||||||
|
p.u[0][0] = 23;
|
||||||
|
p.f[3] = [f("pinning") / 100.0, f("blendWOriginal") / 100.0, f("contrast") / 100.0, 0.0];
|
||||||
|
p.f[4] = [f("darks") / 100.0, f("brights") / 100.0, 0.0, 0.0];
|
||||||
|
}
|
||||||
|
"ec.color.autolevels" | "ec.color.autocontrast" | "ec.color.autocolor" | "ec.color.equalize" => {
|
||||||
|
// The histograms are measured on the CPU.
|
||||||
|
let img = e.download(&b.img)?;
|
||||||
|
if id == "ec.color.equalize" {
|
||||||
|
let amt = (pr.f("amount") / 100.0).clamp(0.0, 1.0) as f32;
|
||||||
|
if amt <= 0.0 {
|
||||||
|
return Some(b);
|
||||||
|
}
|
||||||
|
let style = pr.e("style");
|
||||||
|
let tables = effectcraft_effects::equalize_tables(&img, style);
|
||||||
|
p.u[0] = [17, style, tables.first().map_or(0, |t| t.len()) as u32, 0];
|
||||||
|
p.f[0][0] = amt;
|
||||||
|
data = Some(tables.concat());
|
||||||
|
} else {
|
||||||
|
let kind = match id {
|
||||||
|
"ec.color.autolevels" => 0,
|
||||||
|
"ec.color.autocontrast" => 1,
|
||||||
|
_ => 2,
|
||||||
|
};
|
||||||
|
let (ranges, gam) = effectcraft_effects::auto_correct_settings(ctx, &img, kind);
|
||||||
|
p.u[0][0] = 16;
|
||||||
|
p.f[0] = [ranges[0].0, ranges[1].0, ranges[2].0, 0.0];
|
||||||
|
p.f[1] = [ranges[0].1, ranges[1].1, ranges[2].1, 0.0];
|
||||||
|
p.f[2] = [gam[0], gam[1], gam[2], (pr.f("blend") / 100.0).clamp(0.0, 1.0) as f32];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
}
|
||||||
|
run(e, &p, b, data)
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,10 @@
|
||||||
//!
|
//!
|
||||||
//! GPU effects ([`effectcraft_effects::GPU_EFFECTS`]) run as compute kernels with the CPU
|
//! GPU effects ([`effectcraft_effects::GPU_EFFECTS`]) run as compute kernels with the CPU
|
||||||
//! effect's exact steps (padding, box-blur radii, parameter conversions); chains of them are
|
//! effect's exact steps (padding, box-blur radii, parameter conversions); chains of them are
|
||||||
//! uploaded and read back once.
|
//! uploaded and read back once. Each family lives in its own module with its own WGSL file
|
||||||
|
//! (`fx_color`, `fx_distort`, `fx_generate`, `fx_key`, `fx_noise`, `fx_stylize`, `fx_tone`);
|
||||||
|
//! settings a kernel cannot match fall back to the CPU (`catalog::gpu_supported`, or `None`
|
||||||
|
//! from the family's `apply`).
|
||||||
//!
|
//!
|
||||||
//! GPU particles (`particles`): the stepped particle effects hand their simulation to
|
//! GPU particles (`particles`): the stepped particle effects hand their simulation to
|
||||||
//! [`effectcraft_effects::psim::ParticleSim`], implemented here with one invocation per particle
|
//! [`effectcraft_effects::psim::ParticleSim`], implemented here with one invocation per particle
|
||||||
|
|
@ -35,6 +38,10 @@ mod effects;
|
||||||
mod fx_color;
|
mod fx_color;
|
||||||
mod fx_distort;
|
mod fx_distort;
|
||||||
mod fx_generate;
|
mod fx_generate;
|
||||||
|
mod fx_key;
|
||||||
|
mod fx_noise;
|
||||||
|
mod fx_stylize;
|
||||||
|
mod fx_tone;
|
||||||
mod ops;
|
mod ops;
|
||||||
mod particles;
|
mod particles;
|
||||||
mod walk;
|
mod walk;
|
||||||
|
|
@ -174,4 +181,12 @@ mod tests_fx_distort;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests_fx_generate;
|
mod tests_fx_generate;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
mod tests_fx_key;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests_fx_noise;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests_fx_stylize;
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests_fx_tone;
|
||||||
|
#[cfg(test)]
|
||||||
mod tests_particles;
|
mod tests_particles;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,867 @@
|
||||||
|
// GPU effects (keying, matte and channel family): see src/fx_key.rs. Each entry point mirrors
|
||||||
|
// the CPU effect in effectcraft-effects operation for operation. Every name here is prefixed
|
||||||
|
// `fxk_` (the family files share one module).
|
||||||
|
|
||||||
|
// util::unpremul: (straight colour, alpha); colour 0 when alpha ≤ 1e-6.
|
||||||
|
fn fxk_straight(px: vec4<f32>) -> vec3<f32> {
|
||||||
|
if (px.w > 1e-6) {
|
||||||
|
return px.xyz / px.w;
|
||||||
|
}
|
||||||
|
return vec3<f32>(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fxk_premul(c: vec3<f32>, a: f32) -> vec4<f32> {
|
||||||
|
return vec4<f32>(c * a, a);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pixel `p` of the third image, copied into `data` as RGBA f32 rows of P.u[3].x pixels.
|
||||||
|
fn fxk_rows(p: vec2<i32>) -> vec4<f32> {
|
||||||
|
let i = (u32(p.y) * P.u[3].x + u32(p.x)) * 4u;
|
||||||
|
return vec4<f32>(data[i], data[i + 1u], data[i + 2u], data[i + 3u]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// keying::to_lab.
|
||||||
|
fn fxk_cbrt(t: f32) -> f32 {
|
||||||
|
return sign(t) * pow(abs(t), 1.0 / 3.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fxk_lab_f(t: f32) -> f32 {
|
||||||
|
if (t > 0.008856) {
|
||||||
|
return fxk_cbrt(t);
|
||||||
|
}
|
||||||
|
return 7.787 * t + 16.0 / 116.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// keying::range_components.
|
||||||
|
fn fxk_range_components(space: u32, c: vec3<f32>) -> vec3<f32> {
|
||||||
|
if (space == 1u) {
|
||||||
|
let y = 0.299 * c.x + 0.587 * c.y + 0.114 * c.z;
|
||||||
|
let u = -0.14713 * c.x - 0.28886 * c.y + 0.436 * c.z;
|
||||||
|
let v = 0.615 * c.x - 0.51499 * c.y - 0.10001 * c.z;
|
||||||
|
return vec3<f32>(y * 255.0, u * 255.0 + 128.0, v * 255.0 + 128.0);
|
||||||
|
}
|
||||||
|
if (space == 2u) {
|
||||||
|
return c * 255.0;
|
||||||
|
}
|
||||||
|
let cl = clamp(c, vec3<f32>(0.0), vec3<f32>(1.0));
|
||||||
|
let l = vec3<f32>(srgb_to_linear(cl.x), srgb_to_linear(cl.y), srgb_to_linear(cl.z));
|
||||||
|
let x = (0.4124 * l.x + 0.3576 * l.y + 0.1805 * l.z) / 0.95047;
|
||||||
|
let yy = 0.2126 * l.x + 0.7152 * l.y + 0.0722 * l.z;
|
||||||
|
let z = (0.0193 * l.x + 0.1192 * l.y + 0.9505 * l.z) / 1.08883;
|
||||||
|
let fx = fxk_lab_f(x);
|
||||||
|
let fy = fxk_lab_f(yy);
|
||||||
|
let fz = fxk_lab_f(z);
|
||||||
|
return vec3<f32>((116.0 * fy - 16.0) * 2.55, 500.0 * (fx - fy) + 128.0, 200.0 * (fy - fz) + 128.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// keying::matte_levels.
|
||||||
|
fn fxk_matte_levels(v: f32, in_b: f32, in_w: f32, gamma: f32, out_b: f32, out_w: f32) -> f32 {
|
||||||
|
var t = clamp((v - in_b / 255.0) / max((in_w - in_b) / 255.0, 1e-4), 0.0, 1.0);
|
||||||
|
t = powz(t, 1.0 / max(gamma, 0.01));
|
||||||
|
return clamp(out_b / 255.0 + t * (out_w - out_b) / 255.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// keying::secondary.
|
||||||
|
fn fxk_secondary(c: vec3<f32>, o1: u32, o2: u32, bal: f32) -> f32 {
|
||||||
|
var hi = c[o2];
|
||||||
|
var lo = c[o1];
|
||||||
|
if (c[o1] > c[o2]) {
|
||||||
|
hi = c[o1];
|
||||||
|
lo = c[o2];
|
||||||
|
}
|
||||||
|
return bal * hi + (1.0 - bal) * lo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// channel::arithmetic's 8-bit quantisation.
|
||||||
|
fn fxk_q(v: f32) -> u32 {
|
||||||
|
return u32(round_away(clamp(v, 0.0, 1.0) * 255.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// channel2::extract.
|
||||||
|
fn fxk_extract(px: vec4<f32>, ch: u32, inv: bool) -> vec4<f32> {
|
||||||
|
let c = fxk_straight(px);
|
||||||
|
let a = max(px.w, 0.0);
|
||||||
|
var g: f32;
|
||||||
|
switch ch {
|
||||||
|
case 0u: {
|
||||||
|
if (inv) {
|
||||||
|
return fxk_premul(1.0 - c, a);
|
||||||
|
}
|
||||||
|
return px;
|
||||||
|
}
|
||||||
|
case 1u: { g = luminance(c) * a; }
|
||||||
|
case 2u: { g = c.x * a; }
|
||||||
|
case 3u: { g = c.y * a; }
|
||||||
|
case 4u: { g = c.z * a; }
|
||||||
|
default: { g = a; }
|
||||||
|
}
|
||||||
|
if (inv) {
|
||||||
|
g = 1.0 - g;
|
||||||
|
}
|
||||||
|
return vec4<f32>(g, g, g, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// channel2::arith.
|
||||||
|
fn fxk_arith(op: u32, a: f32, b: f32) -> f32 {
|
||||||
|
switch op {
|
||||||
|
case 0u: { return b; }
|
||||||
|
case 1u: { return a + b; }
|
||||||
|
case 2u: { return a - b; }
|
||||||
|
case 3u: { return a * b; }
|
||||||
|
case 4u: { return abs(a - b); }
|
||||||
|
case 5u: { return f32(fxk_q(a) & fxk_q(b)) / 255.0; }
|
||||||
|
case 6u: { return f32(fxk_q(a) | fxk_q(b)) / 255.0; }
|
||||||
|
case 7u: { return f32(fxk_q(a) ^ fxk_q(b)) / 255.0; }
|
||||||
|
case 8u, 11u: { return max(a, b); }
|
||||||
|
case 9u, 10u: { return min(a, b); }
|
||||||
|
case 12u: { return a + b - a * b; }
|
||||||
|
case 13u: {
|
||||||
|
if (a < 0.5) {
|
||||||
|
return 2.0 * a * b;
|
||||||
|
}
|
||||||
|
return 1.0 - 2.0 * (1.0 - a) * (1.0 - b);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (b < 0.5) {
|
||||||
|
return 2.0 * a * b;
|
||||||
|
}
|
||||||
|
return 1.0 - 2.0 * (1.0 - a) * (1.0 - b);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// channel2::overflow.
|
||||||
|
fn fxk_overflow(op: u32, v: f32, how: u32) -> f32 {
|
||||||
|
if (how == 0u) {
|
||||||
|
return clamp(v, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
if (how == 1u) {
|
||||||
|
let m = 1.0001;
|
||||||
|
let r = v - m * floor(v / m);
|
||||||
|
return min(select(r, 0.0, r >= m || r < 0.0), 1.0);
|
||||||
|
}
|
||||||
|
if (op == 1u) {
|
||||||
|
return v * 0.5;
|
||||||
|
}
|
||||||
|
if (op == 2u) {
|
||||||
|
return (v + 1.0) * 0.5;
|
||||||
|
}
|
||||||
|
return clamp(v, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-pixel keying / matte / channel operations. u[0].x = op (see src/fx_key.rs for each
|
||||||
|
// effect's parameter layout):
|
||||||
|
// 1 Color Key matte 2 Luma Key matte 3 Color Range matte 4 Extract matte
|
||||||
|
// 5 apply a matte (aux.x; u[0].y = 0 final, 2 matte view, 3 Difference Matte's matte view,
|
||||||
|
// 4 Difference Matte's output)
|
||||||
|
// 6 Difference Matte matte (src and aux blurred)
|
||||||
|
// 7 Spill Suppressor 8 Advanced Spill Suppressor (aux = 1×1 excess sums when u[1].w)
|
||||||
|
// 9 Color Difference Key 10 Unmult 11 route channels (u[1] = sources)
|
||||||
|
// 12 Remove Color Matting 13 Arithmetic 14 Solid Composite 15 Set Matte
|
||||||
|
// 16 Channel Combiner 17 Blend 18 Calculations 19 Compound Arithmetic
|
||||||
|
// 20 Set Channels: channel u[0].y of the value image from aux; 21 Set Channels: premultiply
|
||||||
|
// 22 Screen Key matte 23 Screen Key output 24 alpha plane 25 Key Cleaner output
|
||||||
|
// 26 Matte Choker ramp 27 util::set_alpha (aux.x = alpha, data = fill)
|
||||||
|
// 28 matte view (alpha as opaque grey)
|
||||||
|
// Refine Soft / Hard Matte: 29 guide pack (luminance, alpha, products); 30 guided-filter
|
||||||
|
// coefficients from the box means (f[0].x = eps); 31 refined alpha (src = boxed coefficients,
|
||||||
|
// aux = pack, data = edge band); 32 edge band (src.y − aux.y > 1e-3); 33 choke / contrast /
|
||||||
|
// invert; 34 View Edge Region; 35 decontamination (aux.x = refined alpha, data = colour estimate)
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxk_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let dims = out_dims();
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (p.x >= dims.x || p.y >= dims.y) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let px = textureLoad(src, p, 0);
|
||||||
|
let c = fxk_straight(px);
|
||||||
|
let a = max(px.w, 0.0);
|
||||||
|
let u0 = P.u[0];
|
||||||
|
let u1 = P.u[1];
|
||||||
|
let f0 = P.f[0];
|
||||||
|
var o = px;
|
||||||
|
switch u0.x {
|
||||||
|
case 1u: {
|
||||||
|
let k = f0.xyz;
|
||||||
|
let d = max(max(abs(c.x - k.x), abs(c.y - k.y)), abs(c.z - k.z));
|
||||||
|
o = vec4<f32>(select(1.0, 0.0, d <= f0.w));
|
||||||
|
}
|
||||||
|
case 2u: {
|
||||||
|
let l = luminance(c);
|
||||||
|
let thr = f0.x;
|
||||||
|
let tol = f0.y;
|
||||||
|
var m: f32;
|
||||||
|
switch u0.y {
|
||||||
|
case 0u: {
|
||||||
|
if (tol > 0.0) {
|
||||||
|
m = clamp((thr + tol - l) / tol, 0.0, 1.0);
|
||||||
|
} else {
|
||||||
|
m = select(1.0, 0.0, l > thr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 2u: { m = select(1.0, 0.0, abs(l - thr) <= tol); }
|
||||||
|
case 3u: { m = select(0.0, 1.0, abs(l - thr) <= tol); }
|
||||||
|
default: {
|
||||||
|
if (tol > 0.0) {
|
||||||
|
m = clamp((l - (thr - tol)) / tol, 0.0, 1.0);
|
||||||
|
} else {
|
||||||
|
m = select(1.0, 0.0, l < thr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o = vec4<f32>(m);
|
||||||
|
}
|
||||||
|
case 3u: {
|
||||||
|
let v = fxk_range_components(u0.y, c);
|
||||||
|
var out_d = 0.0;
|
||||||
|
for (var i = 0u; i < 3u; i++) {
|
||||||
|
out_d = max(max(out_d, P.f[0][i] - v[i]), v[i] - P.f[1][i]);
|
||||||
|
}
|
||||||
|
let fuzz = P.f[2].x;
|
||||||
|
var m = 1.0;
|
||||||
|
if (out_d <= 0.0) {
|
||||||
|
m = 0.0;
|
||||||
|
} else if (fuzz > 0.0) {
|
||||||
|
m = clamp(out_d / fuzz, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
o = vec4<f32>(m);
|
||||||
|
}
|
||||||
|
case 4u: {
|
||||||
|
var v: f32;
|
||||||
|
switch u0.y {
|
||||||
|
case 1u: { v = c.x; }
|
||||||
|
case 2u: { v = c.y; }
|
||||||
|
case 3u: { v = c.z; }
|
||||||
|
case 4u: { v = a; }
|
||||||
|
default: { v = luminance(c); }
|
||||||
|
}
|
||||||
|
v = 255.0 * v;
|
||||||
|
let bp = f0.x;
|
||||||
|
let wp = f0.y;
|
||||||
|
let bs = f0.z;
|
||||||
|
let ws = f0.w;
|
||||||
|
var k = 1.0;
|
||||||
|
if (v < bp) {
|
||||||
|
k = select(0.0, clamp(1.0 - (bp - v) / bs, 0.0, 1.0), bs > 0.0);
|
||||||
|
} else if (v > wp) {
|
||||||
|
k = select(0.0, clamp(1.0 - (v - wp) / ws, 0.0, 1.0), ws > 0.0);
|
||||||
|
}
|
||||||
|
if (u0.z != 0u) {
|
||||||
|
k = 1.0 - k;
|
||||||
|
}
|
||||||
|
o = vec4<f32>(k);
|
||||||
|
}
|
||||||
|
case 5u: {
|
||||||
|
let m = textureLoad(aux, p, 0).x;
|
||||||
|
switch u0.y {
|
||||||
|
case 2u: {
|
||||||
|
let k = clamp(m, 0.0, 1.0);
|
||||||
|
let v = px.w * k;
|
||||||
|
o = vec4<f32>(v, v, v, 1.0);
|
||||||
|
}
|
||||||
|
case 3u: {
|
||||||
|
let v = m * px.w;
|
||||||
|
o = vec4<f32>(v, v, v, px.w);
|
||||||
|
}
|
||||||
|
case 4u: { o = px * m; }
|
||||||
|
default: { o = px * clamp(m, 0.0, 1.0); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 6u: {
|
||||||
|
let q = textureLoad(aux, p, 0);
|
||||||
|
let oc = fxk_straight(q);
|
||||||
|
let oa = max(q.w, 0.0);
|
||||||
|
let d = max(max(max(abs(c.x - oc.x), abs(c.y - oc.y)), abs(c.z - oc.z)), abs(a - oa));
|
||||||
|
var m: f32;
|
||||||
|
if (d <= f0.x) {
|
||||||
|
m = 0.0;
|
||||||
|
} else if (f0.y <= 1e-6) {
|
||||||
|
m = 1.0;
|
||||||
|
} else {
|
||||||
|
m = clamp((d - f0.x) / f0.y, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
o = vec4<f32>(m);
|
||||||
|
}
|
||||||
|
case 7u: {
|
||||||
|
if (px.w <= 0.0) {
|
||||||
|
textureStore(out, p, px);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var s = px.xyz / px.w;
|
||||||
|
if (u0.y == 1u) {
|
||||||
|
let g = (s.x + s.y + s.z) / 3.0;
|
||||||
|
let dir = f0.xyz;
|
||||||
|
let proj = (s.x - g) * dir.x + (s.y - g) * dir.y + (s.z - g) * dir.z;
|
||||||
|
if (proj > 0.0) {
|
||||||
|
s = s - proj * dir * f0.w;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let spill = max(s[u1.x] - max(s[u1.y], s[u1.z]), 0.0);
|
||||||
|
s[u1.x] = s[u1.x] - spill * f0.w;
|
||||||
|
}
|
||||||
|
o = vec4<f32>(s * px.w, px.w);
|
||||||
|
}
|
||||||
|
case 8u: {
|
||||||
|
if (px.w <= 0.0) {
|
||||||
|
textureStore(out, p, px);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var pi = u1.x;
|
||||||
|
var o1 = u1.y;
|
||||||
|
var o2 = u1.z;
|
||||||
|
if (u1.w != 0u) {
|
||||||
|
let ex = textureLoad(aux, vec2<i32>(0, 0), 0);
|
||||||
|
if (ex.y >= ex.x && ex.y >= ex.z) {
|
||||||
|
pi = 1u;
|
||||||
|
o1 = 0u;
|
||||||
|
o2 = 2u;
|
||||||
|
} else if (ex.z >= ex.x) {
|
||||||
|
pi = 2u;
|
||||||
|
o1 = 0u;
|
||||||
|
o2 = 1u;
|
||||||
|
} else {
|
||||||
|
pi = 0u;
|
||||||
|
o1 = 1u;
|
||||||
|
o2 = 2u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var s = px.xyz / px.w;
|
||||||
|
let amt = f0.x;
|
||||||
|
let spill_range = f0.y;
|
||||||
|
let luma = f0.z;
|
||||||
|
let tol = f0.w;
|
||||||
|
let desat = P.f[1].x;
|
||||||
|
let neutral = P.f[1].y;
|
||||||
|
let key_hue = P.f[1].z;
|
||||||
|
var w = 1.0;
|
||||||
|
if (tol < 1.0) {
|
||||||
|
let h = rgb_to_hsl(s).x;
|
||||||
|
let d0 = abs(h - key_hue);
|
||||||
|
let d = min(d0, 1.0 - d0);
|
||||||
|
w = clamp(1.0 - d / max(tol * 0.5, 1e-4), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
let mx = max(s[o1], s[o2]);
|
||||||
|
let avg = 0.5 * (s[o1] + s[o2]);
|
||||||
|
let lim = mx + (avg - mx) * spill_range;
|
||||||
|
let spill = max(s[pi] - lim, 0.0) * amt * w;
|
||||||
|
s[pi] = s[pi] - spill;
|
||||||
|
let lw = vec3<f32>(0.2126, 0.7152, 0.0722);
|
||||||
|
let back = spill * lw[pi] * luma + spill * neutral / 3.0;
|
||||||
|
s = s + back;
|
||||||
|
if (desat > 0.0 && spill > 0.0) {
|
||||||
|
let l = luminance(s);
|
||||||
|
let k = min(desat * min(spill * 4.0, 1.0), 1.0);
|
||||||
|
s = s + (vec3<f32>(l) - s) * k;
|
||||||
|
}
|
||||||
|
o = vec4<f32>(s * px.w, px.w);
|
||||||
|
}
|
||||||
|
case 9u: {
|
||||||
|
var cc = c;
|
||||||
|
if (u0.z != 0u) {
|
||||||
|
let cl = clamp(cc, vec3<f32>(0.0), vec3<f32>(1.0));
|
||||||
|
cc = vec3<f32>(srgb_to_linear(cl.x), srgb_to_linear(cl.y), srgb_to_linear(cl.z));
|
||||||
|
}
|
||||||
|
let pa = P.f[1];
|
||||||
|
let pb = P.f[3];
|
||||||
|
let ua = 1.0 - clamp((cc[u1.x] - cc[u1.y]) / f0.x, 0.0, 1.0);
|
||||||
|
let ub = 1.0 - clamp((cc[u1.x] - cc[u1.z]) / f0.y, 0.0, 1.0);
|
||||||
|
let ca = fxk_matte_levels(ua, pa.x, pa.y, pa.z, pa.w, P.f[2].x);
|
||||||
|
let cb = fxk_matte_levels(ub, pb.x, pb.y, pb.z, pb.w, P.f[2].y);
|
||||||
|
let um = max(ua, ub);
|
||||||
|
let cm = fxk_matte_levels(max(ca, cb), f0.z, f0.w, P.f[2].z, 0.0, 255.0);
|
||||||
|
var view = u0.y;
|
||||||
|
if (view == 8u) {
|
||||||
|
let right = p.x * 2 >= dims.x;
|
||||||
|
let bottom = p.y * 2 >= dims.y;
|
||||||
|
if (!right && !bottom) {
|
||||||
|
view = 2u;
|
||||||
|
} else if (right && !bottom) {
|
||||||
|
view = 4u;
|
||||||
|
} else if (!right && bottom) {
|
||||||
|
view = 6u;
|
||||||
|
} else {
|
||||||
|
view = 7u;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var g = -1.0;
|
||||||
|
switch view {
|
||||||
|
case 1u: { g = ua; }
|
||||||
|
case 2u: { g = ca; }
|
||||||
|
case 3u: { g = ub; }
|
||||||
|
case 4u: { g = cb; }
|
||||||
|
case 5u: { g = um; }
|
||||||
|
case 6u: { g = cm; }
|
||||||
|
default: {}
|
||||||
|
}
|
||||||
|
if (g >= 0.0) {
|
||||||
|
let m = g * a;
|
||||||
|
o = vec4<f32>(m, m, m, 1.0);
|
||||||
|
} else {
|
||||||
|
o = px * cm;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 10u: {
|
||||||
|
let white = u0.y != 0u;
|
||||||
|
var d: f32;
|
||||||
|
var start: f32;
|
||||||
|
if (white) {
|
||||||
|
d = 1.0 - min(min(c.x, c.y), c.z);
|
||||||
|
start = 1.0 - f0.x;
|
||||||
|
} else {
|
||||||
|
d = max(max(c.x, c.y), c.z);
|
||||||
|
start = f0.x;
|
||||||
|
}
|
||||||
|
d = clamp(d, 0.0, 1.0);
|
||||||
|
let width = f0.y * (1.0 - start);
|
||||||
|
var m: f32;
|
||||||
|
if (width > 1e-6) {
|
||||||
|
m = clamp((d - start) / width, 0.0, 1.0);
|
||||||
|
} else {
|
||||||
|
m = select(0.0, 1.0, d > start);
|
||||||
|
}
|
||||||
|
if (m <= 1e-6) {
|
||||||
|
o = vec4<f32>(0.0);
|
||||||
|
} else {
|
||||||
|
let bg = select(0.0, 1.0, white);
|
||||||
|
var oc = c;
|
||||||
|
if (u0.z != 0u) {
|
||||||
|
oc = (c - bg * (1.0 - m)) / m;
|
||||||
|
}
|
||||||
|
if (u0.w != 0u) {
|
||||||
|
oc = clamp(oc, vec3<f32>(0.0), vec3<f32>(1.0));
|
||||||
|
}
|
||||||
|
o = fxk_premul(oc, a * m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 11u: {
|
||||||
|
let v = vec4<f32>(dst_pick(u1.x, c, a), dst_pick(u1.y, c, a), dst_pick(u1.z, c, a), dst_pick(u1.w, c, a));
|
||||||
|
o = fxk_premul(v.xyz, clamp(v.w, 0.0, 1.0));
|
||||||
|
}
|
||||||
|
case 12u: {
|
||||||
|
if (px.w <= 1e-6) {
|
||||||
|
o = vec4<f32>(0.0);
|
||||||
|
} else {
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
let s = px[i] / px.w;
|
||||||
|
var v = max(s - f0[i] * (1.0 - px.w), 0.0);
|
||||||
|
if (u0.y != 0u) {
|
||||||
|
v = min(v, px.w);
|
||||||
|
}
|
||||||
|
o[i] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 13u: {
|
||||||
|
if (px.w <= 0.0) {
|
||||||
|
textureStore(out, p, px);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let s = px.xyz / px.w;
|
||||||
|
var r = vec3<f32>(0.0);
|
||||||
|
for (var i = 0u; i < 3u; i++) {
|
||||||
|
let v = s[i];
|
||||||
|
let kv = f0[i];
|
||||||
|
let iv = fxk_q(v);
|
||||||
|
let ik = u32(round_away(kv * 255.0));
|
||||||
|
var x: f32;
|
||||||
|
switch u0.y {
|
||||||
|
case 0u: { x = f32(iv & ik) / 255.0; }
|
||||||
|
case 1u: { x = f32(iv | ik) / 255.0; }
|
||||||
|
case 2u: { x = f32(iv ^ ik) / 255.0; }
|
||||||
|
case 3u: { x = v + kv; }
|
||||||
|
case 4u: { x = v - kv; }
|
||||||
|
case 5u: { x = abs(v - kv); }
|
||||||
|
case 6u: { x = min(v, kv); }
|
||||||
|
case 7u: { x = max(v, kv); }
|
||||||
|
case 8u: { x = select(v, 0.0, v > kv); }
|
||||||
|
case 9u: { x = select(v, 0.0, v < kv); }
|
||||||
|
case 10u: { x = select(0.0, 1.0, v >= kv); }
|
||||||
|
case 11u: { x = v * kv; }
|
||||||
|
default: { x = 1.0 - (1.0 - v) * (1.0 - kv); }
|
||||||
|
}
|
||||||
|
if (u0.z != 0u) {
|
||||||
|
r[i] = clamp(x, 0.0, 1.0);
|
||||||
|
} else {
|
||||||
|
r[i] = max(x, 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o = vec4<f32>(r * px.w, px.w);
|
||||||
|
}
|
||||||
|
case 14u: {
|
||||||
|
var b = blend_pixel(u0.y, f0, px * P.f[1].x, 0.5);
|
||||||
|
b.w = clamp(b.w, 0.0, 1.0);
|
||||||
|
o = vec4<f32>(max(b.xyz, vec3<f32>(0.0)), b.w);
|
||||||
|
}
|
||||||
|
case 15u: {
|
||||||
|
var mc = c;
|
||||||
|
var ma = a;
|
||||||
|
if ((u0.z & 8u) == 0u) {
|
||||||
|
let q = textureLoad(aux, p, 0);
|
||||||
|
mc = fxk_straight(q);
|
||||||
|
ma = max(q.w, 0.0);
|
||||||
|
}
|
||||||
|
var m = dst_pick(u0.y, mc, ma);
|
||||||
|
if ((u0.z & 4u) != 0u) {
|
||||||
|
m = m * ma;
|
||||||
|
}
|
||||||
|
m = clamp(m, 0.0, 1.0);
|
||||||
|
let inv = (u0.z & 1u) != 0u;
|
||||||
|
if (inv) {
|
||||||
|
m = 1.0 - m;
|
||||||
|
}
|
||||||
|
let own_m = (u0.z & 8u) != 0u;
|
||||||
|
let is_alpha = (u0.z & 16u) != 0u;
|
||||||
|
var na = m;
|
||||||
|
if ((u0.z & 2u) != 0u && (!own_m || !is_alpha || inv)) {
|
||||||
|
na = a * m;
|
||||||
|
}
|
||||||
|
o = fxk_premul(c, clamp(na, 0.0, 1.0));
|
||||||
|
}
|
||||||
|
case 16u: {
|
||||||
|
let ch_from = u0.y;
|
||||||
|
let ch_to = u0.z;
|
||||||
|
let inv = (u0.w & 1u) != 0u;
|
||||||
|
let solid = (u0.w & 2u) != 0u;
|
||||||
|
let second = (u0.w & 4u) != 0u;
|
||||||
|
var sc = c;
|
||||||
|
var sa = a;
|
||||||
|
if (second) {
|
||||||
|
let q = textureLoad(aux, p, 0);
|
||||||
|
sc = fxk_straight(q);
|
||||||
|
sa = max(q.w, 0.0);
|
||||||
|
}
|
||||||
|
if (sa <= 0.0 && ch_from != 7u && !second) {
|
||||||
|
if (solid) {
|
||||||
|
o = fxk_premul(c, 1.0);
|
||||||
|
}
|
||||||
|
textureStore(out, p, o);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var na = a;
|
||||||
|
var r: vec3<f32>;
|
||||||
|
if (ch_from <= 3u) {
|
||||||
|
switch ch_from {
|
||||||
|
case 0u: {
|
||||||
|
let hsl = rgb_to_hsl(sc);
|
||||||
|
r = vec3<f32>(hsl.x, hsl.z, hsl.y);
|
||||||
|
}
|
||||||
|
case 1u: { r = hsl_to_rgb(sc.x, sc.z, sc.y); }
|
||||||
|
case 2u: {
|
||||||
|
let y = 0.299 * sc.x + 0.587 * sc.y + 0.114 * sc.z;
|
||||||
|
r = vec3<f32>(y, 0.492 * (sc.z - y) + 0.5, 0.877 * (sc.x - y) + 0.5);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
let y = sc.x;
|
||||||
|
let uu = sc.y - 0.5;
|
||||||
|
let vv = sc.z - 0.5;
|
||||||
|
let rr = y + vv / 0.877;
|
||||||
|
let bb = y + uu / 0.492;
|
||||||
|
let gg = (y - 0.299 * rr - 0.114 * bb) / 0.587;
|
||||||
|
r = vec3<f32>(rr, gg, bb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (inv) {
|
||||||
|
r = 1.0 - r;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var v: f32;
|
||||||
|
switch ch_from {
|
||||||
|
case 4u: { v = sc.x; }
|
||||||
|
case 5u: { v = sc.y; }
|
||||||
|
case 6u: { v = sc.z; }
|
||||||
|
case 7u: { v = sa; }
|
||||||
|
case 8u: { v = rgb_to_hsl(sc).z; }
|
||||||
|
case 9u: { v = rgb_to_hsl(sc).x; }
|
||||||
|
case 10u: { v = rgb_to_hsl(sc).y; }
|
||||||
|
case 11u: { v = luminance(sc); }
|
||||||
|
case 12u: { v = max(max(sc.x, sc.y), sc.z); }
|
||||||
|
default: { v = min(min(sc.x, sc.y), sc.z); }
|
||||||
|
}
|
||||||
|
if (inv) {
|
||||||
|
v = 1.0 - v;
|
||||||
|
}
|
||||||
|
switch ch_to {
|
||||||
|
case 0u: { r = vec3<f32>(v, 0.0, 0.0); }
|
||||||
|
case 1u: { r = vec3<f32>(0.0, v, 0.0); }
|
||||||
|
case 2u: { r = vec3<f32>(0.0, 0.0, v); }
|
||||||
|
case 3u: {
|
||||||
|
na = clamp(v, 0.0, 1.0);
|
||||||
|
r = vec3<f32>(1.0);
|
||||||
|
}
|
||||||
|
case 5u: { r = hsl_to_rgb(v, 1.0, 0.5); }
|
||||||
|
case 6u: { r = hsl_to_rgb(0.0, clamp(v, 0.0, 1.0), 0.5); }
|
||||||
|
case 8u: {
|
||||||
|
let hsl = rgb_to_hsl(c);
|
||||||
|
r = hsl_to_rgb(hsl.x, clamp(hsl.y * v, 0.0, 1.0), hsl.z);
|
||||||
|
}
|
||||||
|
default: { r = vec3<f32>(v); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (solid) {
|
||||||
|
na = 1.0;
|
||||||
|
}
|
||||||
|
o = fxk_premul(max(r, vec3<f32>(0.0)), na);
|
||||||
|
}
|
||||||
|
case 17u: {
|
||||||
|
let q = textureLoad(aux, p, 0);
|
||||||
|
var bl: vec4<f32>;
|
||||||
|
switch u0.y {
|
||||||
|
case 0u: { bl = q; }
|
||||||
|
case 1u, 2u: {
|
||||||
|
let oa = q.w;
|
||||||
|
if (px.w <= 0.0 || oa <= 0.0) {
|
||||||
|
bl = px;
|
||||||
|
} else {
|
||||||
|
let hsl = rgb_to_hsl(c);
|
||||||
|
let ohsl = rgb_to_hsl(fxk_straight(q));
|
||||||
|
var h2 = ohsl.x;
|
||||||
|
var s2 = ohsl.y;
|
||||||
|
if (u0.y == 2u) {
|
||||||
|
if (hsl.y > 1e-4) {
|
||||||
|
s2 = hsl.y;
|
||||||
|
} else {
|
||||||
|
h2 = 0.0;
|
||||||
|
s2 = 0.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let rgb = hsl_to_rgb(h2, s2, hsl.z);
|
||||||
|
bl = px + (fxk_premul(rgb, a) - px) * oa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 3u: { bl = vec4<f32>(min(px.xyz, q.xyz), px.w); }
|
||||||
|
default: { bl = max(px, q); }
|
||||||
|
}
|
||||||
|
o = bl + (px - bl) * f0.x;
|
||||||
|
}
|
||||||
|
case 18u: {
|
||||||
|
let flags = u1.x;
|
||||||
|
var r = fxk_extract(px, u0.y, (flags & 1u) != 0u);
|
||||||
|
if ((flags & 8u) != 0u) {
|
||||||
|
let s = fxk_extract(textureLoad(aux, p, 0), u0.z, (flags & 2u) != 0u) * f0.x;
|
||||||
|
r = blend_pixel(u0.w, r, s, 0.5);
|
||||||
|
}
|
||||||
|
if ((flags & 4u) != 0u && r.w > 1e-6) {
|
||||||
|
r = fxk_premul(fxk_straight(r), px.w);
|
||||||
|
}
|
||||||
|
r.w = clamp(r.w, 0.0, 1.0);
|
||||||
|
o = r;
|
||||||
|
}
|
||||||
|
case 19u: {
|
||||||
|
let q = textureLoad(aux, p, 0);
|
||||||
|
let oc = fxk_straight(q);
|
||||||
|
let oa = max(q.w, 0.0);
|
||||||
|
var rc = c;
|
||||||
|
var ra = a;
|
||||||
|
if (u0.z != 2u) {
|
||||||
|
for (var i = 0u; i < 3u; i++) {
|
||||||
|
rc[i] = fxk_overflow(u0.y, fxk_arith(u0.y, c[i], oc[i]), u0.w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (u0.z != 0u) {
|
||||||
|
ra = fxk_overflow(u0.y, fxk_arith(u0.y, a, oa), u0.w);
|
||||||
|
}
|
||||||
|
let res = fxk_premul(rc, ra);
|
||||||
|
o = res + (px - res) * f0.x;
|
||||||
|
}
|
||||||
|
case 20u: {
|
||||||
|
let q = textureLoad(aux, p, 0);
|
||||||
|
o = px;
|
||||||
|
o[u0.y] = dst_pick(u0.z, fxk_straight(q), max(q.w, 0.0));
|
||||||
|
}
|
||||||
|
case 21u: {
|
||||||
|
o = fxk_premul(px.xyz, clamp(px.w, 0.0, 1.0));
|
||||||
|
}
|
||||||
|
case 22u: {
|
||||||
|
let d = c[u1.x] - fxk_secondary(c, u1.y, u1.z, f0.z);
|
||||||
|
let t = clamp(d / f0.x * f0.y, 0.0, 1.0);
|
||||||
|
o = vec4<f32>(clamp((1.0 - t - f0.w) / max(P.f[1].x - f0.w, 1e-3), 0.0, 1.0));
|
||||||
|
}
|
||||||
|
case 23u: {
|
||||||
|
let k = clamp(textureLoad(aux, p, 0).x, 0.0, 1.0);
|
||||||
|
let alpha = a * k;
|
||||||
|
if (u0.y == 1u) {
|
||||||
|
o = vec4<f32>(alpha, alpha, alpha, 1.0);
|
||||||
|
} else if (u0.y == 2u) {
|
||||||
|
var v = 0.5;
|
||||||
|
if (alpha <= 1e-3) {
|
||||||
|
v = 0.0;
|
||||||
|
} else if (alpha >= 0.999) {
|
||||||
|
v = 1.0;
|
||||||
|
}
|
||||||
|
o = vec4<f32>(v, v, v, 1.0);
|
||||||
|
} else {
|
||||||
|
let screen = f0.xyz;
|
||||||
|
var fg = c;
|
||||||
|
if (!(u0.z != 0u || k <= 1e-4)) {
|
||||||
|
let s = 1.0 - k;
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
fg[i] = min(max(c[i] - s * screen[i], 0.0) / k, max(c[i], 1.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let spill = max(fg[u1.x] - fxk_secondary(fg, u1.y, u1.z, f0.w), 0.0);
|
||||||
|
fg[u1.x] = fg[u1.x] - spill * P.f[1].x;
|
||||||
|
o = fxk_premul(fg, alpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 24u: {
|
||||||
|
o = vec4<f32>(px.w);
|
||||||
|
}
|
||||||
|
case 25u: {
|
||||||
|
var na = textureLoad(aux, p, 0).x;
|
||||||
|
if (u0.y != 0u) {
|
||||||
|
na = clamp((na - 0.5) * 1.5 + 0.5, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
var cc = c;
|
||||||
|
let a0 = a;
|
||||||
|
if (P.u[3].y != 0u && a0 > 1e-4 && a0 < 0.999) {
|
||||||
|
let q = fxk_rows(p);
|
||||||
|
if (q.w > 1e-6) {
|
||||||
|
let ea = q.w;
|
||||||
|
if (ea > 1e-4) {
|
||||||
|
let t = f0.y * (1.0 - a0);
|
||||||
|
cc = cc + (q.xyz / ea - cc) * t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
na = clamp((na - 0.5) * f0.x + 0.5, 0.0, 1.0);
|
||||||
|
if (a0 <= 1e-6) {
|
||||||
|
na = 0.0;
|
||||||
|
}
|
||||||
|
o = fxk_premul(cc, na);
|
||||||
|
}
|
||||||
|
case 26u: {
|
||||||
|
o = vec4<f32>(clamp((px.x - f0.x) / f0.y + 0.5, 0.0, 1.0));
|
||||||
|
}
|
||||||
|
case 27u: {
|
||||||
|
let na = clamp(textureLoad(aux, p, 0).x, 0.0, 1.0);
|
||||||
|
var cc = c;
|
||||||
|
if (px.w <= 1e-4) {
|
||||||
|
cc = fxk_straight(fxk_rows(p));
|
||||||
|
}
|
||||||
|
o = fxk_premul(cc, na);
|
||||||
|
}
|
||||||
|
case 29u: {
|
||||||
|
let g = luminance(px.xyz);
|
||||||
|
o = vec4<f32>(g, px.w, g * px.w, g * g);
|
||||||
|
}
|
||||||
|
case 30u: {
|
||||||
|
let var_g = px.w - px.x * px.x;
|
||||||
|
let cov = px.z - px.x * px.y;
|
||||||
|
let ca = cov / (var_g + f0.x);
|
||||||
|
o = vec4<f32>(ca, px.y - ca * px.x, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
case 31u: {
|
||||||
|
let q = textureLoad(aux, p, 0);
|
||||||
|
if (fxk_rows(p).x != 0.0) {
|
||||||
|
o = vec4<f32>(clamp(px.x * q.x + px.y, 0.0, 1.0));
|
||||||
|
} else {
|
||||||
|
o = vec4<f32>(q.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 32u: {
|
||||||
|
o = vec4<f32>(select(0.0, 1.0, px.y - textureLoad(aux, p, 0).y > 1e-3));
|
||||||
|
}
|
||||||
|
case 33u: {
|
||||||
|
var v = px.x;
|
||||||
|
let choke = f0.x;
|
||||||
|
if (choke > 0.0) {
|
||||||
|
v = (v - choke) / (1.0 - choke);
|
||||||
|
} else if (choke < 0.0) {
|
||||||
|
v = v / (1.0 + choke);
|
||||||
|
}
|
||||||
|
if (abs(f0.y - 1.0) > 1e-6) {
|
||||||
|
v = (v - 0.5) * f0.y + 0.5;
|
||||||
|
}
|
||||||
|
v = clamp(v, 0.0, 1.0);
|
||||||
|
if (u0.y != 0u) {
|
||||||
|
v = 1.0 - v;
|
||||||
|
}
|
||||||
|
o = vec4<f32>(v);
|
||||||
|
}
|
||||||
|
case 34u: {
|
||||||
|
let v = px.x;
|
||||||
|
if (textureLoad(aux, p, 0).x != 0.0) {
|
||||||
|
o = vec4<f32>(0.5 + 0.5 * v, 0.5 * v, 0.5 * v, 1.0);
|
||||||
|
} else {
|
||||||
|
o = vec4<f32>(v, v, v, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 35u: {
|
||||||
|
let n = textureLoad(aux, p, 0).x;
|
||||||
|
let a0 = px.w;
|
||||||
|
var t = 0.0;
|
||||||
|
if (a0 > 1e-4 && a0 < 0.999) {
|
||||||
|
t = 1.0 - a0;
|
||||||
|
} else if (u0.w != 0u && n > 1e-4 && n < 0.999) {
|
||||||
|
t = 1.0 - n;
|
||||||
|
}
|
||||||
|
let strength = f0.x * t;
|
||||||
|
if (u0.y != 0u) {
|
||||||
|
let v = select(0.0, strength, u0.z != 0u);
|
||||||
|
o = vec4<f32>(v, v, v, 1.0);
|
||||||
|
} else if (P.u[3].y != 0u && strength > 0.0 && a0 > 1e-4) {
|
||||||
|
let q = fxk_rows(p);
|
||||||
|
if (q.w > 1e-4) {
|
||||||
|
let cc = c + (q.xyz / q.w - c) * strength;
|
||||||
|
o = fxk_premul(cc, a0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 28u: {
|
||||||
|
let v = clamp(px.w, 0.0, 1.0);
|
||||||
|
o = vec4<f32>(v, v, v, 1.0);
|
||||||
|
}
|
||||||
|
default: {}
|
||||||
|
}
|
||||||
|
textureStore(out, p, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
// util::fit_layer: bilinear sample of the other layer (src) at the buffer coordinates in data
|
||||||
|
// (one per column, then one per row).
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxk_fit(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let dims = out_dims();
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (p.x >= dims.x || p.y >= dims.y) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let x = data[u32(p.x)];
|
||||||
|
let y = data[u32(dims.x) + u32(p.y)];
|
||||||
|
textureStore(out, p, sample_bilinear(src, x, y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sum of 16×16 blocks; u[0].x = first pass: sum Advanced Spill Suppressor's per-channel excess
|
||||||
|
// of each primary over the other two (straight colour × alpha), else sum the values.
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxk_reduce(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let dims = out_dims();
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (p.x >= dims.x || p.y >= dims.y) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let sd = vec2<i32>(textureDimensions(src));
|
||||||
|
var acc = vec4<f32>(0.0);
|
||||||
|
for (var j = 0; j < 16; j++) {
|
||||||
|
for (var i = 0; i < 16; i++) {
|
||||||
|
let q = vec2<i32>(p.x * 16 + i, p.y * 16 + j);
|
||||||
|
if (q.x >= sd.x || q.y >= sd.y) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let v = textureLoad(src, q, 0);
|
||||||
|
if (P.u[0].x != 0u) {
|
||||||
|
let c = fxk_straight(v);
|
||||||
|
let a = max(v.w, 0.0);
|
||||||
|
acc += vec4<f32>(
|
||||||
|
max(c.x - max(c.y, c.z), 0.0) * a,
|
||||||
|
max(c.y - max(c.x, c.z), 0.0) * a,
|
||||||
|
max(c.z - max(c.x, c.y), 0.0) * a,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
acc += v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textureStore(out, p, acc);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,825 @@
|
||||||
|
// GPU effects (noise, blur and time family): see src/fx_noise.rs. Each entry point mirrors the CPU effect in
|
||||||
|
// effectcraft-effects operation for operation. Every name here is prefixed `fxn_` (the
|
||||||
|
// family files share one module).
|
||||||
|
|
||||||
|
const FXN_MAX: f32 = 3.40282347e38;
|
||||||
|
|
||||||
|
fn fxn_pixel(gid: vec3<u32>) -> vec2<i32> {
|
||||||
|
let dims = out_dims();
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (p.x >= dims.x || p.y >= dims.y) {
|
||||||
|
return vec2<i32>(-1);
|
||||||
|
}
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
|
||||||
|
// a / b rounded like the CPU's IEEE division (GPU division may be approximate): one
|
||||||
|
// residual correction with fma. Used where a threshold follows the quotient.
|
||||||
|
fn fxn_div4(a: vec4<f32>, b: vec4<f32>) -> vec4<f32> {
|
||||||
|
let q = a / b;
|
||||||
|
let r = fma(-q, b, a);
|
||||||
|
return fma(r, 1.0 / b, q);
|
||||||
|
}
|
||||||
|
|
||||||
|
// util::unpremul: (straight colour, alpha).
|
||||||
|
fn fxn_unpremul(p: vec4<f32>) -> vec4<f32> {
|
||||||
|
if (p.w > 1e-6) {
|
||||||
|
return vec4<f32>(p.xyz / p.w, p.w);
|
||||||
|
}
|
||||||
|
return vec4<f32>(0.0, 0.0, 0.0, max(p.w, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Median family (noise::median_image)
|
||||||
|
|
||||||
|
// Quantise to the median's 512 levels (0..511). u[0].x = 1: straight colour with alpha 1
|
||||||
|
// (Median (Legacy) colour only).
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_quant(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var v = textureLoad(src, p, 0);
|
||||||
|
if (P.u[0].x == 1u) {
|
||||||
|
v = vec4<f32>(fxn_unpremul(v).xyz, 1.0);
|
||||||
|
}
|
||||||
|
let q = clamp(v, vec4<f32>(0.0), vec4<f32>(1.0)) * 511.0;
|
||||||
|
textureStore(out, p, vec4<f32>(round_away(q.x), round_away(q.y), round_away(q.z), round_away(q.w)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-channel median of the quantised levels over a (2r+1)² window (edges repeated): the
|
||||||
|
// smallest level m with more than half the window at or below m. u[0].x = r.
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_median(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let r = i32(P.u[0].x);
|
||||||
|
let n = (2 * r + 1) * (2 * r + 1);
|
||||||
|
let half = f32(n / 2);
|
||||||
|
var lo = vec4<f32>(0.0);
|
||||||
|
var hi = vec4<f32>(511.0);
|
||||||
|
for (var it = 0; it < 9; it++) {
|
||||||
|
let mid = floor((lo + hi) * 0.5);
|
||||||
|
var cnt = vec4<f32>(0.0);
|
||||||
|
for (var dy = -r; dy <= r; dy++) {
|
||||||
|
for (var dx = -r; dx <= r; dx++) {
|
||||||
|
let q = tex_get_clamped(src, p.x + dx, p.y + dy);
|
||||||
|
cnt += select(vec4<f32>(0.0), vec4<f32>(1.0), q <= mid);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let above = cnt > vec4<f32>(half);
|
||||||
|
hi = select(hi, mid, above);
|
||||||
|
lo = select(mid + 1.0, lo, above);
|
||||||
|
}
|
||||||
|
textureStore(out, p, fxn_div4(lo, vec4<f32>(511.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// noise::median_px.
|
||||||
|
fn fxn_median_px(o: vec4<f32>, m: vec4<f32>, on_alpha: bool) -> vec4<f32> {
|
||||||
|
if (on_alpha) {
|
||||||
|
let a = clamp(m.w, 0.0, 1.0);
|
||||||
|
return vec4<f32>(min(m.xyz, vec3<f32>(a)), a);
|
||||||
|
}
|
||||||
|
if (m.w <= 1e-4) {
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
return vec4<f32>(min(fxn_div4(m, vec4<f32>(m.w)).xyz, vec3<f32>(1.0)) * o.w, o.w);
|
||||||
|
}
|
||||||
|
|
||||||
|
// src = the layer, aux = the median. u[0] = (mode, operate on alpha): 0 Median, 1 Dust &
|
||||||
|
// Scratches (f[0].x = threshold), 2 Median (Legacy) with alpha, 3 Median (Legacy) colour only.
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_median_out(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let o = textureLoad(src, p, 0);
|
||||||
|
let m = textureLoad(aux, p, 0);
|
||||||
|
let on_alpha = P.u[0].y != 0u;
|
||||||
|
var r = o;
|
||||||
|
switch P.u[0].x {
|
||||||
|
case 0u: {
|
||||||
|
r = fxn_median_px(o, m, on_alpha);
|
||||||
|
}
|
||||||
|
case 1u: {
|
||||||
|
let cand = fxn_median_px(o, m, on_alpha);
|
||||||
|
let d = abs(cand - o);
|
||||||
|
if (max(max(d.x, d.y), max(d.z, d.w)) > P.f[0].x) {
|
||||||
|
r = cand;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 2u: {
|
||||||
|
r = m;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
r = vec4<f32>(m.xyz * o.w, o.w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textureStore(out, p, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Bilateral / Smart Blur (blur2.rs)
|
||||||
|
|
||||||
|
// blur2::edge_preserving. u[0] = (ceil r, stride, hard, largest d²); u[1].x = grey (Bilateral
|
||||||
|
// with Colorize off); f[0] = (2σs², 2σr², range).
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_edge(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ri = i32(P.u[0].x);
|
||||||
|
let stride = i32(P.u[0].y);
|
||||||
|
let hard = P.u[0].z != 0u;
|
||||||
|
let d2max = i32(P.u[0].w);
|
||||||
|
let sig_s2 = P.f[0].x;
|
||||||
|
let sig_r2 = P.f[0].y;
|
||||||
|
let range = P.f[0].z;
|
||||||
|
let c = textureLoad(src, p, 0);
|
||||||
|
let cl = luminance(c.xyz);
|
||||||
|
var acc = vec4<f32>(0.0);
|
||||||
|
var wsum = 0.0;
|
||||||
|
for (var dy = -ri; dy <= ri; dy += stride) {
|
||||||
|
for (var dx = -ri; dx <= ri; dx += stride) {
|
||||||
|
let d2 = dx * dx + dy * dy;
|
||||||
|
if (d2 > d2max) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let q = tex_get_clamped(src, p.x + dx, p.y + dy);
|
||||||
|
var w = 0.0;
|
||||||
|
if (hard) {
|
||||||
|
w = select(0.0, 1.0, abs(luminance(q.xyz) - cl) <= range);
|
||||||
|
} else {
|
||||||
|
let d = q - c;
|
||||||
|
let dc = d.x * d.x + d.y * d.y + d.z * d.z + d.w * d.w;
|
||||||
|
w = exp(-f32(d2) / sig_s2 - dc / sig_r2);
|
||||||
|
}
|
||||||
|
if (w > 0.0) {
|
||||||
|
acc += q * w;
|
||||||
|
wsum += w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var o = c;
|
||||||
|
if (wsum > 0.0) {
|
||||||
|
o = acc / wsum;
|
||||||
|
}
|
||||||
|
if (P.u[1].x != 0u && o.w > 0.0) {
|
||||||
|
let l = luminance(o.xyz / o.w);
|
||||||
|
o = vec4<f32>(vec3<f32>(l * o.w), o.w);
|
||||||
|
}
|
||||||
|
textureStore(out, p, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Smart Blur's Edge Only / Overlay Edge (blur2::sobel on the blurred layer's premultiplied
|
||||||
|
// luminance). u[0].x = mode; f[0].x = edge threshold.
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_smart_edges(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var l: array<f32, 9>;
|
||||||
|
for (var j = 0; j < 3; j++) {
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
l[j * 3 + i] = luminance(tex_get_clamped(src, p.x + i - 1, p.y + j - 1).xyz);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let gx = l[2] + 2.0 * l[5] + l[8] - l[0] - 2.0 * l[3] - l[6];
|
||||||
|
let gy = l[6] + 2.0 * l[7] + l[8] - l[0] - 2.0 * l[1] - l[2];
|
||||||
|
let e = sqrt(gx * gx + gy * gy) * 0.25;
|
||||||
|
let px = textureLoad(src, p, 0);
|
||||||
|
let on = e > P.f[0].x;
|
||||||
|
var o = px;
|
||||||
|
if (P.u[0].x == 1u) {
|
||||||
|
o = vec4<f32>(vec3<f32>(select(0.0, 1.0, on) * px.w), px.w);
|
||||||
|
} else if (on) {
|
||||||
|
o = vec4<f32>(vec3<f32>(px.w), px.w);
|
||||||
|
}
|
||||||
|
textureStore(out, p, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Sharpen / Unsharp Mask (misc::unsharp_core)
|
||||||
|
|
||||||
|
// src = the layer, aux = its blur. f[0] = (amount, threshold in 0..255).
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_unsharp(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var o = textureLoad(src, p, 0);
|
||||||
|
let bl = textureLoad(aux, p, 0);
|
||||||
|
for (var c = 0; c < 3; c++) {
|
||||||
|
let d = o[c] - bl[c];
|
||||||
|
if (abs(d) * 255.0 >= P.f[0].y) {
|
||||||
|
o[c] = max(o[c] + d * P.f[0].x, 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textureStore(out, p, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- planes
|
||||||
|
|
||||||
|
// Channel select: channel c of `src` becomes aux[c] (u[0].x = 0) or aux's alpha (1) where
|
||||||
|
// u[1][c] != 0.
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_pick(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var o = textureLoad(src, p, 0);
|
||||||
|
let a = textureLoad(aux, p, 0);
|
||||||
|
for (var c = 0; c < 4; c++) {
|
||||||
|
if (P.u[1][c] != 0u) {
|
||||||
|
o[c] = select(a[c], a.w, P.u[0].x == 1u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textureStore(out, p, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Channel Blur's result: src = blurred premultiplied channels, aux = the alpha blurred like
|
||||||
|
// each channel (alpha = the Alpha Blurriness result).
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_cblur_out(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let v = textureLoad(src, p, 0);
|
||||||
|
let ba = textureLoad(aux, p, 0);
|
||||||
|
var s = vec3<f32>(0.0);
|
||||||
|
for (var c = 0; c < 3; c++) {
|
||||||
|
if (ba[c] > 1e-6) {
|
||||||
|
s[c] = v[c] / ba[c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let a = clamp(ba.w, 0.0, 1.0);
|
||||||
|
textureStore(out, p, vec4<f32>(s * a, a));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Straight colour planes with the raw alpha (Minimax).
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_straight(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let v = textureLoad(src, p, 0);
|
||||||
|
textureStore(out, p, vec4<f32>(fxn_unpremul(v).xyz, v.w));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Premultiply straight planes by the clamped alpha.
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_premul(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let v = textureLoad(src, p, 0);
|
||||||
|
let a = clamp(v.w, 0.0, 1.0);
|
||||||
|
textureStore(out, p, vec4<f32>(v.xyz * a, a));
|
||||||
|
}
|
||||||
|
|
||||||
|
// util::morph_plane along one axis (edges repeated). u[0] = (r, vertical); u[1] = per-channel
|
||||||
|
// operation (0 min, 1 max, 2 keep).
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_morph(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let r = i32(P.u[0].x);
|
||||||
|
let vertical = P.u[0].y != 0u;
|
||||||
|
let centre = textureLoad(src, p, 0);
|
||||||
|
var lo = centre;
|
||||||
|
var hi = centre;
|
||||||
|
for (var k = -r; k <= r; k++) {
|
||||||
|
var v: vec4<f32>;
|
||||||
|
if (vertical) {
|
||||||
|
v = tex_get_clamped(src, p.x, p.y + k);
|
||||||
|
} else {
|
||||||
|
v = tex_get_clamped(src, p.x + k, p.y);
|
||||||
|
}
|
||||||
|
lo = min(lo, v);
|
||||||
|
hi = max(hi, v);
|
||||||
|
}
|
||||||
|
var o = centre;
|
||||||
|
for (var c = 0; c < 4; c++) {
|
||||||
|
if (P.u[1][c] == 0u) {
|
||||||
|
o[c] = lo[c];
|
||||||
|
} else if (P.u[1][c] == 1u) {
|
||||||
|
o[c] = hi[c];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textureStore(out, p, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crop: out(p) = src(p + u[0].xy).
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_crop(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
textureStore(out, p, textureLoad(src, p + vec2<i32>(P.u[0].xy), 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Compound Blur (blur2::compound_blur)
|
||||||
|
|
||||||
|
// The blur map (straight luminance × alpha, inverted with u[0].y) in every channel. u[0].x = 1:
|
||||||
|
// `src` is the Blur Layer sampled at the per-column / per-row coordinates in `data` (u[1].x =
|
||||||
|
// width); 0: `src` is the layer itself.
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_cb_map(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var v: vec4<f32>;
|
||||||
|
if (P.u[0].x == 1u) {
|
||||||
|
let w = P.u[1].x;
|
||||||
|
v = sample_bilinear(src, data[u32(p.x)], data[w + u32(p.y)]);
|
||||||
|
} else {
|
||||||
|
v = textureLoad(src, p, 0);
|
||||||
|
}
|
||||||
|
let s = fxn_unpremul(v);
|
||||||
|
var l = luminance(s.xyz) * s.w;
|
||||||
|
if (P.u[0].y != 0u) {
|
||||||
|
l = 1.0 - l;
|
||||||
|
}
|
||||||
|
textureStore(out, p, vec4<f32>(l));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writes the pixels whose blur level falls between levels k (src) and k + 1 (aux). The map is
|
||||||
|
// `data` (RGBA f32 rows of u[0].y pixels); u[0].x = k.
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_cb_level(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let m = data[(u32(p.y) * P.u[0].y + u32(p.x)) * 4u];
|
||||||
|
let t = clamp(m, 0.0, 1.0) * 4.0;
|
||||||
|
let k = min(u32(floor(t)), 3u);
|
||||||
|
if (k != P.u[0].x) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let f = t - f32(k);
|
||||||
|
let a = textureLoad(src, p, 0);
|
||||||
|
let c = textureLoad(aux, p, 0);
|
||||||
|
textureStore(out, p, a + (c - a) * f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Echo (time_fx::echo)
|
||||||
|
|
||||||
|
// acc (src) ⊕ frame (aux) × f[0].x. u[0] = (operator, first frame).
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_echo(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let op = P.u[0].x;
|
||||||
|
var acc = textureLoad(src, p, 0);
|
||||||
|
if (P.u[0].y != 0u) {
|
||||||
|
acc = select(vec4<f32>(0.0), vec4<f32>(FXN_MAX), op == 2u);
|
||||||
|
}
|
||||||
|
let k = P.f[0].x;
|
||||||
|
let q = textureLoad(aux, p, 0) * k;
|
||||||
|
switch op {
|
||||||
|
case 1u: {
|
||||||
|
acc = max(acc, q);
|
||||||
|
}
|
||||||
|
case 2u: {
|
||||||
|
acc = min(acc, q);
|
||||||
|
}
|
||||||
|
case 3u: {
|
||||||
|
acc = acc + q - acc * q;
|
||||||
|
}
|
||||||
|
case 4u: {
|
||||||
|
acc += q * (1.0 - acc.w);
|
||||||
|
}
|
||||||
|
case 5u: {
|
||||||
|
acc = q + acc * (1.0 - q.w);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
acc += q;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textureStore(out, p, acc);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Echo's finish. u[0].x = operator; f[0].x = number of frames.
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_echo_fin(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let op = P.u[0].x;
|
||||||
|
var acc = textureLoad(src, p, 0);
|
||||||
|
if (op == 6u) {
|
||||||
|
acc = acc / P.f[0].x;
|
||||||
|
}
|
||||||
|
if (op == 2u && acc.x == FXN_MAX) {
|
||||||
|
acc = vec4<f32>(0.0);
|
||||||
|
}
|
||||||
|
textureStore(out, p, vec4<f32>(max(acc.xyz, vec3<f32>(0.0)), clamp(acc.w, 0.0, 1.0)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Fractal / Turbulent Noise (noise3.rs)
|
||||||
|
|
||||||
|
fn fxn_catmull(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {
|
||||||
|
return 0.5 * (2.0 * p1 + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t * t + (3.0 * p1 - p0 - 3.0 * p2 + p3) * t * t * t);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fxn_slice(ix: i32, iy: i32, iz: i32, tx: f32, ty: f32, seed: u32, nt: u32) -> f32 {
|
||||||
|
if (nt == 0u) {
|
||||||
|
return lattice(ix, iy, iz, seed);
|
||||||
|
}
|
||||||
|
if (nt == 1u) {
|
||||||
|
let a = lattice(ix, iy, iz, seed);
|
||||||
|
let b = lattice(ix + 1, iy, iz, seed);
|
||||||
|
let c = lattice(ix, iy + 1, iz, seed);
|
||||||
|
let d = lattice(ix + 1, iy + 1, iz, seed);
|
||||||
|
let top = a + (b - a) * tx;
|
||||||
|
let bot = c + (d - c) * tx;
|
||||||
|
return top + (bot - top) * ty;
|
||||||
|
}
|
||||||
|
var rows: array<f32, 4>;
|
||||||
|
for (var j = 0; j < 4; j++) {
|
||||||
|
let y = iy + j - 1;
|
||||||
|
rows[j] = fxn_catmull(lattice(ix - 1, y, iz, seed), lattice(ix, y, iz, seed), lattice(ix + 1, y, iz, seed), lattice(ix + 2, y, iz, seed), tx);
|
||||||
|
}
|
||||||
|
return fxn_catmull(rows[0], rows[1], rows[2], rows[3], ty);
|
||||||
|
}
|
||||||
|
|
||||||
|
// noise3::typed_noise.
|
||||||
|
fn fxn_typed_noise(x: f32, y: f32, z: f32, seed: u32, nt: u32) -> f32 {
|
||||||
|
if (nt == 2u) {
|
||||||
|
return value_noise(x, y, z, seed);
|
||||||
|
}
|
||||||
|
let x0 = floor(x);
|
||||||
|
let y0 = floor(y);
|
||||||
|
let z0 = floor(z);
|
||||||
|
let tx = x - x0;
|
||||||
|
let ty = y - y0;
|
||||||
|
let fz = fade(z - z0);
|
||||||
|
let a = fxn_slice(i32(x0), i32(y0), i32(z0), tx, ty, seed, nt);
|
||||||
|
let b = fxn_slice(i32(x0), i32(y0), i32(z0) + 1, tx, ty, seed, nt);
|
||||||
|
return a + (b - a) * fz;
|
||||||
|
}
|
||||||
|
|
||||||
|
// noise3::shape.
|
||||||
|
fn fxn_shape(kind: u32, n: f32) -> f32 {
|
||||||
|
let s = n * 2.0 - 1.0;
|
||||||
|
switch kind {
|
||||||
|
case 1u: {
|
||||||
|
return 1.0 - s * s;
|
||||||
|
}
|
||||||
|
case 2u: {
|
||||||
|
return 1.0 - abs(s);
|
||||||
|
}
|
||||||
|
case 3u: {
|
||||||
|
let t = 1.0 - abs(s);
|
||||||
|
return t * t;
|
||||||
|
}
|
||||||
|
case 8u: {
|
||||||
|
let t = clamp(abs(s) / 0.12, 0.0, 1.0);
|
||||||
|
return 1.0 - t * t;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fractal::layer. `o` = octave data base index.
|
||||||
|
fn fxn_layer(u: f32, v: f32, o: u32, z: f32, seed: u32) -> f32 {
|
||||||
|
let nt = P.u[0].y;
|
||||||
|
if (P.u[1].y != 0u) {
|
||||||
|
let t = P.f[2].x;
|
||||||
|
let c = P.f[2].y;
|
||||||
|
let w = P.f[2].z;
|
||||||
|
let a = fxn_typed_noise(u, v, t + z, seed, nt);
|
||||||
|
let b = fxn_typed_noise(u, v, t - c + z, seed, nt);
|
||||||
|
return a * (1.0 - w) + b * w;
|
||||||
|
}
|
||||||
|
return fxn_typed_noise(u, v, data[o + 7u] + z, seed, nt);
|
||||||
|
}
|
||||||
|
|
||||||
|
// u[0] = (kind, noise type, octaves, seed); u[1] = (sub rotation, cycle, invert, overflow);
|
||||||
|
// u[2].x = blend mode (BlendMode::ALL index, 255 = None); f[0] = (sin, cos, size x, size y);
|
||||||
|
// f[1] = (contrast, brightness, opacity, blend with original); f[2] = (cycle t, cycle, t / cycle,
|
||||||
|
// norm); data = 10 floats per octave (noise3::FractalGpu).
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_fractal(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let kind = P.u[0].x;
|
||||||
|
let n_oct = P.u[0].z;
|
||||||
|
let sr = P.f[0].x;
|
||||||
|
let cr = P.f[0].y;
|
||||||
|
let x = f32(p.x) + 0.5;
|
||||||
|
let y = f32(p.y) + 0.5;
|
||||||
|
var sum = 0.0;
|
||||||
|
var best = 0.0;
|
||||||
|
for (var oi = 0u; oi < n_oct; oi++) {
|
||||||
|
let o = oi * 10u;
|
||||||
|
let dx = x - data[o];
|
||||||
|
let dy = y - data[o + 1u];
|
||||||
|
var u = (dx * cr + dy * sr) / P.f[0].z;
|
||||||
|
var v = (-dx * sr + dy * cr) / P.f[0].w;
|
||||||
|
if (P.u[1].x != 0u) {
|
||||||
|
let s = data[o + 2u];
|
||||||
|
let c = data[o + 3u];
|
||||||
|
let u2 = u * c + v * s;
|
||||||
|
v = -u * s + v * c;
|
||||||
|
u = u2;
|
||||||
|
}
|
||||||
|
let f = data[o + 4u];
|
||||||
|
u = u * f;
|
||||||
|
v = v * f;
|
||||||
|
let z = data[o + 8u];
|
||||||
|
let seed = P.u[0].w + oi;
|
||||||
|
if (kind >= 4u && kind <= 6u) {
|
||||||
|
let k = data[o + 9u];
|
||||||
|
let a = fxn_layer(u * 0.5, v * 0.5, o, z + 3.1, seed ^ 0x55u) - 0.5;
|
||||||
|
let bb = fxn_layer(u * 0.5, v * 0.5, o, z + 5.7, seed ^ 0xaau) - 0.5;
|
||||||
|
if (kind == 5u) {
|
||||||
|
let s = sin(a * 3.0);
|
||||||
|
let c = cos(a * 3.0);
|
||||||
|
let u2 = u * c - v * s;
|
||||||
|
v = u * s + v * c;
|
||||||
|
u = u2;
|
||||||
|
} else {
|
||||||
|
u += a * 1.5 * k;
|
||||||
|
v += bb * 1.5 * k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let n = fxn_shape(kind, fxn_layer(u, v, o, z, seed));
|
||||||
|
let amp = data[o + 5u];
|
||||||
|
if (kind == 7u) {
|
||||||
|
best = max(best, n * amp);
|
||||||
|
}
|
||||||
|
sum += n * amp * data[o + 6u];
|
||||||
|
}
|
||||||
|
var raw = sum / max(P.f[2].w, 1e-6);
|
||||||
|
if (kind == 7u) {
|
||||||
|
raw = best;
|
||||||
|
}
|
||||||
|
var val = (raw - 0.5) * P.f[1].x + 0.5 + P.f[1].y;
|
||||||
|
let ov = P.u[1].w;
|
||||||
|
if (ov == 0u) {
|
||||||
|
val = clamp(val, 0.0, 1.0);
|
||||||
|
} else if (ov == 1u) {
|
||||||
|
val = clamp(0.5 + 0.5 * tanh(2.0 * (val - 0.5)) / tanh(1.0), 0.0, 1.0);
|
||||||
|
} else if (ov == 2u) {
|
||||||
|
let t = val - 2.0 * floor(val / 2.0);
|
||||||
|
val = select(t, 2.0 - t, t > 1.0);
|
||||||
|
}
|
||||||
|
if (P.u[1].z != 0u) {
|
||||||
|
val = 1.0 - val;
|
||||||
|
}
|
||||||
|
let op = P.f[1].z;
|
||||||
|
let s = vec4<f32>(vec3<f32>(val * op), op);
|
||||||
|
let orig = textureLoad(src, p, 0);
|
||||||
|
var r = s;
|
||||||
|
if (P.u[2].x != 255u) {
|
||||||
|
r = blend_pixel(P.u[2].x, orig, s, 0.5);
|
||||||
|
}
|
||||||
|
let bo = P.f[1].w;
|
||||||
|
textureStore(out, p, orig * bo + r * (1.0 - bo));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Noise Alpha / Noise HLS (noise.rs)
|
||||||
|
|
||||||
|
// noise::phased_cycle with the phase's two seeds and smoothed fraction resolved on the CPU.
|
||||||
|
fn fxn_phased(x: u32, y: u32, sa: u32, sb: u32, t: f32) -> f32 {
|
||||||
|
let a = hash_noise(x, y, sa);
|
||||||
|
let b = hash_noise(x, y, sb);
|
||||||
|
return a + (b - a) * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// u[0] = (squared, original alpha mode, overflow, seed a); u[1].x = seed b; f[0] = (amount, t).
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_noise_alpha(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let k = P.f[0].x;
|
||||||
|
var n = fxn_phased(u32(p.x), u32(p.y), P.u[0].w, P.u[1].x, P.f[0].y);
|
||||||
|
if (P.u[0].x != 0u) {
|
||||||
|
n = n * n;
|
||||||
|
}
|
||||||
|
let s = fxn_unpremul(textureLoad(src, p, 0));
|
||||||
|
let a = s.w;
|
||||||
|
var na: f32;
|
||||||
|
switch P.u[0].y {
|
||||||
|
case 0u: {
|
||||||
|
na = a + n * k;
|
||||||
|
}
|
||||||
|
case 1u: {
|
||||||
|
na = select(0.0, a + n * k, a > 0.0);
|
||||||
|
}
|
||||||
|
case 2u: {
|
||||||
|
na = a * (1.0 + (2.0 * n - 1.0) * k);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
na = a + (2.0 * n - 1.0) * k * 4.0 * a * (1.0 - a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (P.u[0].z == 1u) {
|
||||||
|
let t = na - 2.0 * floor(na / 2.0);
|
||||||
|
na = select(t, 2.0 - t, t > 1.0);
|
||||||
|
} else if (P.u[0].z == 2u && !(na >= 0.0 && na <= 1.0)) {
|
||||||
|
na = na - floor(na);
|
||||||
|
}
|
||||||
|
na = clamp(na, 0.0, 1.0);
|
||||||
|
textureStore(out, p, vec4<f32>(s.xyz * na, na));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Noise HLS: u[0] = (noise kind, seed); u[1], u[2].xy = per channel (hue, lightness,
|
||||||
|
// saturation) the phase's two seeds; f[0] = (hue, lightness, saturation, grain size); f[1].x =
|
||||||
|
// phase (Grain), f[1].y = smoothed fraction.
|
||||||
|
fn fxn_hls_seed(i: u32) -> u32 {
|
||||||
|
if (i < 4u) {
|
||||||
|
return P.u[1][i];
|
||||||
|
}
|
||||||
|
return P.u[2][i - 4u];
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fxn_hls_n(x: u32, y: u32, k: u32) -> f32 {
|
||||||
|
let s = P.u[0].y + k * 7919u;
|
||||||
|
switch P.u[0].x {
|
||||||
|
case 2u: {
|
||||||
|
let gs = P.f[0].w;
|
||||||
|
return clamp((value_noise(f32(x) / gs, f32(y) / gs, P.f[1].x, s) - 0.5) * 3.0, -1.0, 1.0);
|
||||||
|
}
|
||||||
|
case 1u: {
|
||||||
|
let v = fxn_phased(x, y, fxn_hls_seed(2u * k), fxn_hls_seed(2u * k + 1u), P.f[1].y) * 2.0 - 1.0;
|
||||||
|
return v * abs(v);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return fxn_phased(x, y, fxn_hls_seed(2u * k), fxn_hls_seed(2u * k + 1u), P.f[1].y) * 2.0 - 1.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_noise_hls(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let px = textureLoad(src, p, 0);
|
||||||
|
let s0 = fxn_unpremul(px);
|
||||||
|
let a = s0.w;
|
||||||
|
if (a <= 0.0) {
|
||||||
|
textureStore(out, p, px);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let x = u32(p.x);
|
||||||
|
let y = u32(p.y);
|
||||||
|
var hsl = rgb_to_hsl(s0.xyz);
|
||||||
|
if (P.f[0].x != 0.0) {
|
||||||
|
hsl.x = fract_euclid(hsl.x + fxn_hls_n(x, y, 0u) * P.f[0].x * 0.5);
|
||||||
|
}
|
||||||
|
if (P.f[0].y != 0.0) {
|
||||||
|
hsl.z = clamp(hsl.z + fxn_hls_n(x, y, 1u) * P.f[0].y, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
if (P.f[0].z != 0.0) {
|
||||||
|
hsl.y = clamp(hsl.y + fxn_hls_n(x, y, 2u) * P.f[0].z, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
let c = hsl_to_rgb(hsl.x, hsl.y, hsl.z);
|
||||||
|
textureStore(out, p, vec4<f32>(c * a, a));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Remove Grain (noise::remove_grain)
|
||||||
|
|
||||||
|
// The guide (util::guided_filter's g): the luminance of the premultiplied channels in every
|
||||||
|
// channel (Multichannel), or the channels themselves (u[0].x, Single Channel).
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_gf_guide(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let v = textureLoad(src, p, 0);
|
||||||
|
if (P.u[0].x != 0u) {
|
||||||
|
textureStore(out, p, v);
|
||||||
|
} else {
|
||||||
|
textureStore(out, p, vec4<f32>(luminance(v.xyz)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guided filter arithmetic, S = src, A = aux, D = `data` (RGBA f32 rows of u[0].y pixels):
|
||||||
|
// u[0].x = 0: S − A·D, 1: S − A·A, 2: S / (A + f[0].x), 3: A − S·D, 4: S·D + A, 5: S·A, 6: S·S.
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_gf(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let s = textureLoad(src, p, 0);
|
||||||
|
let a = textureLoad(aux, p, 0);
|
||||||
|
var d = vec4<f32>(0.0);
|
||||||
|
let mode = P.u[0].x;
|
||||||
|
if (mode == 0u || mode == 3u || mode == 4u) {
|
||||||
|
let i = (u32(p.y) * P.u[0].y + u32(p.x)) * 4u;
|
||||||
|
d = vec4<f32>(data[i], data[i + 1u], data[i + 2u], data[i + 3u]);
|
||||||
|
}
|
||||||
|
var o: vec4<f32>;
|
||||||
|
switch mode {
|
||||||
|
case 0u: {
|
||||||
|
o = s - a * d;
|
||||||
|
}
|
||||||
|
case 1u: {
|
||||||
|
o = s - a * a;
|
||||||
|
}
|
||||||
|
case 2u: {
|
||||||
|
o = s / (a + P.f[0].x);
|
||||||
|
}
|
||||||
|
case 3u: {
|
||||||
|
o = a - s * d;
|
||||||
|
}
|
||||||
|
case 4u: {
|
||||||
|
o = s * d + a;
|
||||||
|
}
|
||||||
|
case 5u: {
|
||||||
|
o = s * a;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
o = s * s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textureStore(out, p, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Texture: f + (o − f) × f[0].x on colour (src = filtered, aux = original).
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_rg_texture(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let f = textureLoad(src, p, 0);
|
||||||
|
let o = textureLoad(aux, p, 0);
|
||||||
|
textureStore(out, p, vec4<f32>(f.xyz + (o.xyz - f.xyz) * P.f[0].x, f.w));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsharp Mask on the planes: src = channels, aux = their blur; f[0] = (amount, threshold).
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_rg_unsharp(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var v = textureLoad(src, p, 0);
|
||||||
|
let l = textureLoad(aux, p, 0);
|
||||||
|
for (var c = 0; c < 3; c++) {
|
||||||
|
if (abs(v[c] - l[c]) > P.f[0].y) {
|
||||||
|
v[c] = v[c] + (v[c] - l[c]) * P.f[0].x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textureStore(out, p, v);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove Grain's result: colour clamped to 0..alpha (alpha from aux = the original); with
|
||||||
|
// u[0].x the Preview viewing mode (noise::preview_compose): the original (aux) outside the
|
||||||
|
// region f[0] = (x0, x1, y0, y1), the box outline in f[1] where u[0].y.
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxn_rg_out(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = fxn_pixel(gid);
|
||||||
|
if (p.x < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let o = textureLoad(aux, p, 0);
|
||||||
|
let a = o.w;
|
||||||
|
var v = vec4<f32>(clamp(textureLoad(src, p, 0).xyz, vec3<f32>(0.0), vec3<f32>(max(a, 0.0))), a);
|
||||||
|
if (P.u[0].x != 0u) {
|
||||||
|
let x = f32(p.x) + 0.5;
|
||||||
|
let y = f32(p.y) + 0.5;
|
||||||
|
let r = P.f[0];
|
||||||
|
let inside = x >= r.x && x < r.y && y >= r.z && y < r.w;
|
||||||
|
if (!inside) {
|
||||||
|
v = o;
|
||||||
|
}
|
||||||
|
if (P.u[0].y != 0u) {
|
||||||
|
let nx = abs(x - r.x) < 0.75 || abs(x - r.y) < 0.75;
|
||||||
|
let ny = abs(y - r.z) < 0.75 || abs(y - r.w) < 0.75;
|
||||||
|
if ((nx && y >= r.z - 0.75 && y < r.w + 0.75) || (ny && x >= r.x - 0.75 && x < r.y + 0.75)) {
|
||||||
|
v = vec4<f32>(P.f[1].xyz, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textureStore(out, p, v);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,925 @@
|
||||||
|
// GPU effects (stylize and distort family): see src/fx_stylize.rs. Each entry point mirrors the CPU effect in
|
||||||
|
// effectcraft-effects operation for operation. Every name here is prefixed `fxs_` (the
|
||||||
|
// family files share one module). Coordinates are pixel centres (x + 0.5, y + 0.5) in buffer
|
||||||
|
// pixels, as in util::remap. Per-column / per-row tables in `data` hold coordinates the CPU
|
||||||
|
// computes in f64 (anything with a floor or a wrap), so tile and wrap boundaries match exactly.
|
||||||
|
|
||||||
|
const FXS_TAU: f32 = 6.2831855;
|
||||||
|
const FXS_PI: f32 = 3.1415927;
|
||||||
|
const FXS_HALF_PI: f32 = 1.5707964;
|
||||||
|
|
||||||
|
fn fxs_inside(p: vec2<i32>) -> bool {
|
||||||
|
let d = out_dims();
|
||||||
|
return p.x < d.x && p.y < d.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fxs_centre(p: vec2<i32>) -> vec2<f32> {
|
||||||
|
return vec2<f32>(f32(p.x) + 0.5, f32(p.y) + 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
// util::unpremul: straight colour (zero below alpha 1e-6) and alpha (≥ 0).
|
||||||
|
fn fxs_unpremul(px: vec4<f32>) -> vec4<f32> {
|
||||||
|
let a = px.w;
|
||||||
|
if (a > 1e-6) {
|
||||||
|
return vec4<f32>(fxs_qdiv(px.xyz, a), a);
|
||||||
|
}
|
||||||
|
return vec4<f32>(0.0, 0.0, 0.0, max(a, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// util::smoothstep.
|
||||||
|
fn fxs_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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// x / a rounded like the CPU's IEEE division (GPU division may be off by an ulp, which flips
|
||||||
|
// thresholds on 8 / 16 bpc colours that sit exactly on them): one residual correction.
|
||||||
|
fn fxs_qdiv(x: vec3<f32>, a: f32) -> vec3<f32> {
|
||||||
|
let q = x / a;
|
||||||
|
let r = fma(-q, vec3<f32>(a), x);
|
||||||
|
return q + r / a;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fxs_rem2(i: i32) -> i32 {
|
||||||
|
return ((i % 2) + 2) % 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- per-pixel colour effects
|
||||||
|
// u[0].x = mode:
|
||||||
|
// 1 Posterize (misc::posterize): f[0].x = levels − 1
|
||||||
|
// 2 Threshold (misc::threshold): f[0].x = level
|
||||||
|
// 3 CC Threshold: u[0].yz = (channel, invert); f[0].xy = (threshold, blend)
|
||||||
|
// 4 CC Threshold RGB: u[0].yzw = invert R, G, B; f[0] = (thresholds, blend)
|
||||||
|
// 5 Strobe Light on colour: u[0].y = operator; f[0] = (strobe colour, blend)
|
||||||
|
// 6 Strobe Light transparent (Image::scale_alpha): f[0].x = factor
|
||||||
|
// 7 CC Vignette: f[0] = (cx, cy, distance, amount); f[1].x = pin highlights
|
||||||
|
|
||||||
|
fn fxs_bin(v: f32, t: f32, inv: bool) -> f32 {
|
||||||
|
return select(0.0, 1.0, (v >= t) != inv);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fxs_strobe_op(op: u32, v: f32, k: f32) -> f32 {
|
||||||
|
switch op {
|
||||||
|
case 1u: { return v + k; }
|
||||||
|
case 2u: { return max(v - k, 0.0); }
|
||||||
|
case 3u: { return v * k; }
|
||||||
|
case 4u: { return abs(v - k); }
|
||||||
|
case 5u: { return 1.0 - (1.0 - v) * (1.0 - k); }
|
||||||
|
case 6u: { return max(v, k); }
|
||||||
|
case 7u: { return min(v, k); }
|
||||||
|
default: { return k; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let px = textureLoad(src, p, 0);
|
||||||
|
let mode = P.u[0].x;
|
||||||
|
let a = px.w;
|
||||||
|
if (mode == 6u) {
|
||||||
|
textureStore(out, p, px * P.f[0].x);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (a <= 0.0) {
|
||||||
|
textureStore(out, p, px);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
switch mode {
|
||||||
|
case 1u: {
|
||||||
|
let lv = P.f[0].x;
|
||||||
|
let c = clamp(fxs_qdiv(px.xyz, a), vec3<f32>(0.0), vec3<f32>(1.0)) * lv;
|
||||||
|
let o = vec3<f32>(round_away(c.x), round_away(c.y), round_away(c.z)) / lv;
|
||||||
|
textureStore(out, p, vec4<f32>(o * a, a));
|
||||||
|
}
|
||||||
|
case 2u: {
|
||||||
|
let v = select(0.0, 1.0, luminance(fxs_qdiv(px.xyz, a)) >= P.f[0].x);
|
||||||
|
textureStore(out, p, vec4<f32>(vec3<f32>(v) * a, a));
|
||||||
|
}
|
||||||
|
case 3u: {
|
||||||
|
let u = fxs_unpremul(px);
|
||||||
|
let c = u.xyz;
|
||||||
|
let t = P.f[0].x;
|
||||||
|
let blend = P.f[0].y;
|
||||||
|
let inv = P.u[0].z != 0u;
|
||||||
|
var o = c;
|
||||||
|
var na = u.w;
|
||||||
|
switch P.u[0].y {
|
||||||
|
case 1u: {
|
||||||
|
o = vec3<f32>(fxs_bin(c.x, t, inv), fxs_bin(c.y, t, inv), fxs_bin(c.z, t, inv));
|
||||||
|
}
|
||||||
|
case 2u: {
|
||||||
|
o = vec3<f32>(fxs_bin(rgb_to_hsl(c).y, t, inv));
|
||||||
|
}
|
||||||
|
case 3u: {
|
||||||
|
na = fxs_bin(u.w, t, inv);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
o = vec3<f32>(fxs_bin(luminance(c), t, inv));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o = o + (c - o) * blend;
|
||||||
|
na = na + (u.w - na) * blend;
|
||||||
|
textureStore(out, p, vec4<f32>(o * na, na));
|
||||||
|
}
|
||||||
|
case 4u: {
|
||||||
|
let c = fxs_qdiv(px.xyz, a);
|
||||||
|
let t = P.f[0].xyz;
|
||||||
|
let o = vec3<f32>(fxs_bin(c.x, t.x, P.u[0].y != 0u), fxs_bin(c.y, t.y, P.u[0].z != 0u), fxs_bin(c.z, t.z, P.u[0].w != 0u));
|
||||||
|
let r = o + (c - o) * P.f[0].w;
|
||||||
|
textureStore(out, p, vec4<f32>(r * a, a));
|
||||||
|
}
|
||||||
|
case 5u: {
|
||||||
|
let c = px.xyz / a;
|
||||||
|
let k = P.f[0].xyz;
|
||||||
|
let op = P.u[0].y;
|
||||||
|
let o = vec3<f32>(fxs_strobe_op(op, c.x, k.x), fxs_strobe_op(op, c.y, k.y), fxs_strobe_op(op, c.z, k.z));
|
||||||
|
let r = o + (c - o) * P.f[0].w;
|
||||||
|
textureStore(out, p, vec4<f32>(r * a, a));
|
||||||
|
}
|
||||||
|
case 7u: {
|
||||||
|
let u = fxs_unpremul(px);
|
||||||
|
let q = fxs_centre(p);
|
||||||
|
let dx = q.x - P.f[0].x;
|
||||||
|
let dy = q.y - P.f[0].y;
|
||||||
|
let r = sqrt(dx * dx + dy * dy) / P.f[0].z;
|
||||||
|
let f0 = 1.0 / (1.0 + r * r);
|
||||||
|
let f = f0 * f0;
|
||||||
|
var k = 1.0 - P.f[0].w * (1.0 - f);
|
||||||
|
let pin = P.f[1].x;
|
||||||
|
if (pin > 0.0) {
|
||||||
|
let l = clamp(luminance(u.xyz), 0.0, 1.0);
|
||||||
|
k += (1.0 - k) * pin * l;
|
||||||
|
}
|
||||||
|
textureStore(out, p, vec4<f32>(max(u.xyz * k, vec3<f32>(0.0)) * u.w, u.w));
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
textureStore(out, p, px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- inverse-mapped warps
|
||||||
|
// u[0].x = mode (all sample bilinear, transparent outside, unless noted):
|
||||||
|
// 1 Mirror (distort::mirror): f[0] = (cx, cy, nx, ny)
|
||||||
|
// 2 Spherize (distort::spherize): f[0] = (cx, cy, radius)
|
||||||
|
// 3 Polar Coordinates (distort::polar, edges repeated): u[0].y = to polar;
|
||||||
|
// f[0] = (w, h, cx, cy); f[1] = (rmax, interpolation)
|
||||||
|
// 4 CC Slant (distort3::cc_slant): f[0] = (floor x, floor y, 1 / height, slant); f[1].x = x scale
|
||||||
|
// 5 CC Smear (distort3::cc_smear): f[0] = (from x, from y, dx, dy); f[1] = (len², reach, radius)
|
||||||
|
// 6 CC Split (distort3::split_impl): f[0] = (ax, ay, ux, uy); f[1] = (len, split 1, split 2, reach)
|
||||||
|
// 7 Optics Compensation (distort2::optics_compensation): u[0].y = reverse;
|
||||||
|
// f[0] = (cx, cy, fit, f); f[1] = (reference radius, half FOV)
|
||||||
|
// 8 CC Kaleida (stylize2::kaleida, edges repeated): u[0].y = mirror;
|
||||||
|
// f[0] = (cx, cy, size, rotation); f[1].x = segment angle
|
||||||
|
// 9 Liquify (distort4::liquify): u[0].xy = (_, mesh nx, mesh ny); f[0] = (cell, mesh offset x, y,
|
||||||
|
// percentage); f[1] = (scale, buffer offset x, y); data = mesh offsets (x, y) per node
|
||||||
|
// 10 Twirl (Legacy) (distort3::twirl_legacy): f[0] = (cx, cy, radius, angle)
|
||||||
|
// 11 CC Ripple Pulse (distort3::cc_ripple_pulse): u[0].y = render bump; f[0] = (cx, cy, front,
|
||||||
|
// speed); f[1] = (time span, wavelength, height)
|
||||||
|
// 12 CC Power Pin, full perspective (distort3::cc_power_pin): f[2..5] = inverse of the
|
||||||
|
// square → quad matrix (rows); f[5] = source rect in layer pixels (x0, y0, w, h);
|
||||||
|
// f[6] = (scale, buffer offset x, y)
|
||||||
|
|
||||||
|
fn fxs_mesh_d(i: u32, j: u32) -> vec2<f32> {
|
||||||
|
let k = (j * P.u[0].y + i) * 2u;
|
||||||
|
return vec2<f32>(data[k], data[k + 1u]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// distort3::cc_ripple_pulse's wave.
|
||||||
|
fn fxs_pulse(r: f32) -> f32 {
|
||||||
|
let front = P.f[0].z;
|
||||||
|
let speed = P.f[0].w;
|
||||||
|
let span = P.f[1].x;
|
||||||
|
let age = (front - r) / max(speed, 1e-9);
|
||||||
|
if (age < 0.0 || age > span) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
let fade = 1.0 - age / span;
|
||||||
|
return sin(FXS_TAU * (r - front) / P.f[1].y) * fade;
|
||||||
|
}
|
||||||
|
|
||||||
|
// distort4::Mesh::at.
|
||||||
|
fn fxs_mesh_at(x: f32, y: f32) -> vec2<f32> {
|
||||||
|
let cell = P.f[0].x;
|
||||||
|
let nx = P.u[0].y;
|
||||||
|
let ny = P.u[0].z;
|
||||||
|
let fx = x / cell;
|
||||||
|
let fy = y / cell;
|
||||||
|
if (fx < 0.0 || fy < 0.0 || fx > f32(nx - 1u) || fy > f32(ny - 1u)) {
|
||||||
|
return vec2<f32>(0.0);
|
||||||
|
}
|
||||||
|
let x0 = min(u32(floor(fx)), nx - 2u);
|
||||||
|
let y0 = min(u32(floor(fy)), ny - 2u);
|
||||||
|
let x1 = min(x0 + 1u, nx - 1u);
|
||||||
|
let y1 = min(y0 + 1u, ny - 1u);
|
||||||
|
let tx = fx - f32(x0);
|
||||||
|
let ty = fy - f32(y0);
|
||||||
|
let a = fxs_mesh_d(x0, y0);
|
||||||
|
let b = fxs_mesh_d(x1, y0);
|
||||||
|
let c = fxs_mesh_d(x0, y1);
|
||||||
|
let d = fxs_mesh_d(x1, y1);
|
||||||
|
let top = a + (b - a) * tx;
|
||||||
|
let bot = c + (d - c) * tx;
|
||||||
|
return top + (bot - top) * ty;
|
||||||
|
}
|
||||||
|
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_warp(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let q = fxs_centre(p);
|
||||||
|
let x = q.x;
|
||||||
|
let y = q.y;
|
||||||
|
var s = q;
|
||||||
|
var clamped = false;
|
||||||
|
switch P.u[0].x {
|
||||||
|
case 1u: {
|
||||||
|
let n = P.f[0].zw;
|
||||||
|
let d = (x - P.f[0].x) * n.x + (y - P.f[0].y) * n.y;
|
||||||
|
if (d > 0.0) {
|
||||||
|
s = vec2<f32>(x - 2.0 * d * n.x, y - 2.0 * d * n.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 2u: {
|
||||||
|
let c = P.f[0].xy;
|
||||||
|
let r = P.f[0].z;
|
||||||
|
let dx = x - c.x;
|
||||||
|
let dy = y - c.y;
|
||||||
|
let d = sqrt(dx * dx + dy * dy);
|
||||||
|
if (d < r && d != 0.0) {
|
||||||
|
let nd = d / r;
|
||||||
|
let k = (asin(nd) / FXS_HALF_PI) / nd;
|
||||||
|
s = vec2<f32>(c.x + dx * k, c.y + dy * k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 3u: {
|
||||||
|
clamped = true;
|
||||||
|
let w = P.f[0].x;
|
||||||
|
let h = P.f[0].y;
|
||||||
|
let cx = P.f[0].z;
|
||||||
|
let cy = P.f[0].w;
|
||||||
|
let rmax = P.f[1].x;
|
||||||
|
var pp: vec2<f32>;
|
||||||
|
if (P.u[0].y != 0u) {
|
||||||
|
let dx = x - cx;
|
||||||
|
let dy = y - cy;
|
||||||
|
let a0 = atan2(dx, -dy);
|
||||||
|
let a = a0 - floor(a0 / FXS_TAU) * FXS_TAU;
|
||||||
|
let r = sqrt(dx * dx + dy * dy) / rmax;
|
||||||
|
pp = vec2<f32>(a / FXS_TAU * w, h - r * h);
|
||||||
|
} else {
|
||||||
|
let a = x / w * FXS_TAU;
|
||||||
|
let r = (h - y) / h * rmax;
|
||||||
|
pp = vec2<f32>(cx + r * sin(a), cy - r * cos(a));
|
||||||
|
}
|
||||||
|
let amt = P.f[1].y;
|
||||||
|
s = vec2<f32>(x + (pp.x - x) * amt, y + (pp.y - y) * amt);
|
||||||
|
}
|
||||||
|
case 4u: {
|
||||||
|
let fl = P.f[0].xy;
|
||||||
|
s = vec2<f32>(fl.x + (x - fl.x - (fl.y - y) * P.f[0].w) / P.f[1].x, fl.y + (y - fl.y) * P.f[0].z);
|
||||||
|
}
|
||||||
|
case 5u: {
|
||||||
|
let fr = P.f[0].xy;
|
||||||
|
let dx = P.f[0].z;
|
||||||
|
let dy = P.f[0].w;
|
||||||
|
let t = clamp(((x - fr.x) * dx + (y - fr.y) * dy) / P.f[1].x, 0.0, 1.0);
|
||||||
|
let px = fr.x + dx * t;
|
||||||
|
let py = fr.y + dy * t;
|
||||||
|
let d = sqrt((x - px) * (x - px) + (y - py) * (y - py));
|
||||||
|
let w0 = max(1.0 - d / P.f[1].z, 0.0);
|
||||||
|
let w = w0 * w0 * (3.0 - 2.0 * w0);
|
||||||
|
let k = P.f[1].y * w * t;
|
||||||
|
s = vec2<f32>(x - dx * k, y - dy * k);
|
||||||
|
}
|
||||||
|
case 6u: {
|
||||||
|
let a = P.f[0].xy;
|
||||||
|
let ux = P.f[0].z;
|
||||||
|
let uy = P.f[0].w;
|
||||||
|
let nx = -uy;
|
||||||
|
let ny = ux;
|
||||||
|
let len = P.f[1].x;
|
||||||
|
let reach = P.f[1].w;
|
||||||
|
let rx = x - a.x;
|
||||||
|
let ry = y - a.y;
|
||||||
|
let t = (rx * ux + ry * uy) / len;
|
||||||
|
let sd = rx * nx + ry * ny;
|
||||||
|
if (t >= 0.0 && t <= 1.0) {
|
||||||
|
let amt = select(P.f[1].z, P.f[1].y, sd >= 0.0);
|
||||||
|
let g = amt * reach * sin(FXS_PI * t);
|
||||||
|
let ad = abs(sd);
|
||||||
|
if (ad < g) {
|
||||||
|
textureStore(out, p, vec4<f32>(0.0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let push = g * max(1.0 - (ad - g) / reach, 0.0);
|
||||||
|
let ns = select(-1.0, 1.0, sd >= 0.0) * (ad - push);
|
||||||
|
s = vec2<f32>(x + nx * (ns - sd), y + ny * (ns - sd));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 7u: {
|
||||||
|
let c = P.f[0].xy;
|
||||||
|
let fit = P.f[0].z;
|
||||||
|
let f = P.f[0].w;
|
||||||
|
let r_ref = P.f[1].x;
|
||||||
|
let half = P.f[1].y;
|
||||||
|
let dx = x - c.x;
|
||||||
|
let dy = y - c.y;
|
||||||
|
let r = sqrt(dx * dx + dy * dy) * fit;
|
||||||
|
if (r >= 1e-9) {
|
||||||
|
var rs: f32;
|
||||||
|
if (P.u[0].y != 0u) {
|
||||||
|
rs = atan(r / f) / half * r_ref;
|
||||||
|
} else {
|
||||||
|
let th = r / r_ref * half;
|
||||||
|
if (th >= FXS_HALF_PI - 1e-4) {
|
||||||
|
textureStore(out, p, vec4<f32>(0.0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
rs = f * tan(th);
|
||||||
|
}
|
||||||
|
let k = rs / r * fit;
|
||||||
|
s = vec2<f32>(c.x + dx * k, c.y + dy * k);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 8u: {
|
||||||
|
clamped = true;
|
||||||
|
let c = P.f[0].xy;
|
||||||
|
let rot = P.f[0].w;
|
||||||
|
let seg = P.f[1].x;
|
||||||
|
let dx = x - c.x;
|
||||||
|
let dy = y - c.y;
|
||||||
|
let r = sqrt(dx * dx + dy * dy) / P.f[0].z;
|
||||||
|
let a0 = atan2(dy, dx) - rot;
|
||||||
|
var a = a0 - floor(a0 / seg) * seg;
|
||||||
|
if (P.u[0].y != 0u && a > seg * 0.5) {
|
||||||
|
a = seg - a;
|
||||||
|
}
|
||||||
|
s = vec2<f32>(c.x + r * cos(a + rot), c.y + r * sin(a + rot));
|
||||||
|
}
|
||||||
|
case 9u: {
|
||||||
|
let scale = P.f[1].x;
|
||||||
|
let o = P.f[1].yz;
|
||||||
|
let lx = (x - o.x) / scale;
|
||||||
|
let ly = (y - o.y) / scale;
|
||||||
|
let d = fxs_mesh_at(lx - P.f[0].y, ly - P.f[0].z);
|
||||||
|
if (d.x == 0.0 && d.y == 0.0) {
|
||||||
|
textureStore(out, p, textureLoad(src, p, 0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let pct = P.f[0].w;
|
||||||
|
s = vec2<f32>((lx + d.x * pct) * scale + o.x, (ly + d.y * pct) * scale + o.y);
|
||||||
|
}
|
||||||
|
case 10u: {
|
||||||
|
let c = P.f[0].xy;
|
||||||
|
let r = P.f[0].z;
|
||||||
|
let dx = x - c.x;
|
||||||
|
let dy = y - c.y;
|
||||||
|
let d = sqrt(dx * dx + dy * dy);
|
||||||
|
if (d < r) {
|
||||||
|
let a = -P.f[0].w * (1.0 - d / r);
|
||||||
|
let sn = sin(a);
|
||||||
|
let co = cos(a);
|
||||||
|
s = vec2<f32>(c.x + dx * co - dy * sn, c.y + dx * sn + dy * co);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 11u: {
|
||||||
|
let c = P.f[0].xy;
|
||||||
|
let dx = x - c.x;
|
||||||
|
let dy = y - c.y;
|
||||||
|
let r = sqrt(dx * dx + dy * dy);
|
||||||
|
let height = P.f[1].z;
|
||||||
|
let d = fxs_pulse(r) * height;
|
||||||
|
var u = vec2<f32>(0.0);
|
||||||
|
if (r > 1e-9) {
|
||||||
|
u = vec2<f32>(dx / r, dy / r);
|
||||||
|
}
|
||||||
|
var o = sample_bilinear(src, x - u.x * d, y - u.y * d);
|
||||||
|
if (P.u[0].y != 0u && d != 0.0) {
|
||||||
|
let slope = (fxs_pulse(r + 0.5) - fxs_pulse(r - 0.5)) * height;
|
||||||
|
let k = clamp(1.0 - slope * 0.5, 0.0, 2.0);
|
||||||
|
o = vec4<f32>(o.xyz * k, o.w);
|
||||||
|
}
|
||||||
|
textureStore(out, p, o);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
case 12u: {
|
||||||
|
let r0 = P.f[2];
|
||||||
|
let r1 = P.f[3];
|
||||||
|
let r2 = P.f[4];
|
||||||
|
var u = r0.x * x + r0.y * y + r0.z;
|
||||||
|
var v = r1.x * x + r1.y * y + r1.z;
|
||||||
|
let w = r2.x * x + r2.y * y + r2.z;
|
||||||
|
if (w != 1.0 && w != 0.0) {
|
||||||
|
u = u / w;
|
||||||
|
v = v / w;
|
||||||
|
}
|
||||||
|
if (u < -1e-9 || u > 1.0 + 1e-9 || v < -1e-9 || v > 1.0 + 1e-9) {
|
||||||
|
textureStore(out, p, vec4<f32>(0.0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let rc = P.f[5];
|
||||||
|
let lx = rc.x + u * rc.z;
|
||||||
|
let ly = rc.y + v * rc.w;
|
||||||
|
let o = P.f[6];
|
||||||
|
s = vec2<f32>(lx * o.x + o.y, ly * o.x + o.z);
|
||||||
|
}
|
||||||
|
default: {}
|
||||||
|
}
|
||||||
|
if (clamped) {
|
||||||
|
textureStore(out, p, sample_bilinear_clamped(src, s.x, s.y));
|
||||||
|
} else {
|
||||||
|
textureStore(out, p, sample_bilinear(src, s.x, s.y));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- CC Flo Motion (distort4::flo_motion)
|
||||||
|
|
||||||
|
// One knot's pull at layer point q: f[k] = (knot x, knot y, amount, _).
|
||||||
|
fn fxs_flo_knot(q: vec2<f32>, k: vec4<f32>) -> vec2<f32> {
|
||||||
|
if (k.z == 0.0) {
|
||||||
|
return q;
|
||||||
|
}
|
||||||
|
let d = q - k.xy;
|
||||||
|
let r = sqrt(d.x * d.x + d.y * d.y) / P.f[2].y;
|
||||||
|
let w = powz(1.0 - min(r, 1.0), P.f[2].x);
|
||||||
|
let s = max(1.0 - clamp(k.z, -4.0, 0.95) * w, 0.02);
|
||||||
|
return k.xy + d * s;
|
||||||
|
}
|
||||||
|
|
||||||
|
// u[0] = (supersampling, tile edges); f[0], f[1] = knots; f[2] = (falloff, radius, layer w,
|
||||||
|
// layer h); f[3] = (scale, buffer offset x, y)
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_flomotion(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let ss = P.u[0].x;
|
||||||
|
let scale = P.f[3].x;
|
||||||
|
let off = P.f[3].yz;
|
||||||
|
let lw = P.f[2].z;
|
||||||
|
let lh = P.f[2].w;
|
||||||
|
var acc = vec4<f32>(0.0);
|
||||||
|
for (var sy = 0u; sy < ss; sy++) {
|
||||||
|
for (var sx = 0u; sx < ss; sx++) {
|
||||||
|
let px = f32(p.x) + (f32(sx) + 0.5) / f32(ss);
|
||||||
|
let py = f32(p.y) + (f32(sy) + 0.5) / f32(ss);
|
||||||
|
var q = vec2<f32>((px - off.x) / scale, (py - off.y) / scale);
|
||||||
|
q = fxs_flo_knot(q, P.f[0]);
|
||||||
|
q = fxs_flo_knot(q, P.f[1]);
|
||||||
|
if (P.u[0].y != 0u) {
|
||||||
|
q = vec2<f32>(q.x - floor(q.x / lw) * lw, q.y - floor(q.y / lh) * lh);
|
||||||
|
}
|
||||||
|
acc += sample_bilinear(src, q.x * scale + off.x, q.y * scale + off.y);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textureStore(out, p, acc / f32(ss * ss));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- separable table warps
|
||||||
|
// The source point is (data[x], data[w + y]) (computed in f64 per column and per row), then
|
||||||
|
// mixed with the original: out = sample + (orig − sample) · blend (Offset, CC Tiler).
|
||||||
|
// u[0] = (buffer width, repeat edges); f[0].x = blend
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_table(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let w = P.u[0].x;
|
||||||
|
let sx = data[u32(p.x)];
|
||||||
|
let sy = data[w + u32(p.y)];
|
||||||
|
var s: vec4<f32>;
|
||||||
|
if (P.u[0].y != 0u) {
|
||||||
|
s = sample_bilinear_clamped(src, sx, sy);
|
||||||
|
} else {
|
||||||
|
s = sample_bilinear(src, sx, sy);
|
||||||
|
}
|
||||||
|
let o = textureLoad(src, p, 0);
|
||||||
|
textureStore(out, p, s + (o - s) * P.f[0].x);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CC Griddler (distort2::cc_griddler). data: per column (tile centre x, local x), per row
|
||||||
|
// (tile centre y, local y). u[0] = (buffer width, cut tiles); f[0] = (cos, sin, h scale, v scale);
|
||||||
|
// f[1].x = cut half size (0.45 · tile)
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_griddler(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let w = P.u[0].x;
|
||||||
|
let x = u32(p.x);
|
||||||
|
let y = u32(p.y);
|
||||||
|
let tcx = data[2u * x];
|
||||||
|
let lx = data[2u * x + 1u];
|
||||||
|
let tcy = data[2u * w + 2u * y];
|
||||||
|
let ly = data[2u * w + 2u * y + 1u];
|
||||||
|
if (P.u[0].y != 0u && max(abs(lx), abs(ly)) > P.f[1].x) {
|
||||||
|
textureStore(out, p, vec4<f32>(0.0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cr = P.f[0].x;
|
||||||
|
let sr = P.f[0].y;
|
||||||
|
let rx = lx * cr + ly * sr;
|
||||||
|
let ry = -lx * sr + ly * cr;
|
||||||
|
textureStore(out, p, sample_bilinear(src, tcx + rx / P.f[0].z, tcy + ry / P.f[0].w));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Motion Tile (stylize2::motion_tile) into a new output size. data: per output column (tile
|
||||||
|
// index, fraction) of tx, then per row of ty. u[0] = (output width, horizontal phase shift,
|
||||||
|
// mirror edges); f[0] = (phase, layer w · scale, layer h · scale); f[1].xy = source buffer offset
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_motiontile(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let w = P.u[0].x;
|
||||||
|
let x = u32(p.x);
|
||||||
|
let y = u32(p.y);
|
||||||
|
let ixc = data[2u * x];
|
||||||
|
let fxc = data[2u * x + 1u];
|
||||||
|
let iyr = data[2u * w + 2u * y];
|
||||||
|
let fyr = data[2u * w + 2u * y + 1u];
|
||||||
|
let phase = P.f[0].x;
|
||||||
|
var i = ixc;
|
||||||
|
var fx = fxc;
|
||||||
|
var j = iyr;
|
||||||
|
var fy = fyr;
|
||||||
|
if (P.u[0].y != 0u) {
|
||||||
|
let t = fxc + phase * iyr;
|
||||||
|
i = ixc + floor(t);
|
||||||
|
fx = t - floor(t);
|
||||||
|
} else {
|
||||||
|
let t = fyr + phase * ixc;
|
||||||
|
j = iyr + floor(t);
|
||||||
|
fy = t - floor(t);
|
||||||
|
}
|
||||||
|
if (P.u[0].z != 0u) {
|
||||||
|
if (fxs_rem2(i32(i)) == 1) {
|
||||||
|
fx = 1.0 - fx;
|
||||||
|
}
|
||||||
|
if (fxs_rem2(i32(j)) == 1) {
|
||||||
|
fy = 1.0 - fy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
textureStore(out, p, sample_bilinear(src, fx * P.f[0].y + P.f[1].x, fy * P.f[0].z + P.f[1].y));
|
||||||
|
}
|
||||||
|
|
||||||
|
// CC RepeTile (stylize2::repetile) into a new output size. data: per output column (tile index,
|
||||||
|
// position in the tile), then per row. u[0] = (output width, tiling); f[0] = layer rect (x0, y0, w, h)
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_repetile(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let w = P.u[0].x;
|
||||||
|
let x = u32(p.x);
|
||||||
|
let y = u32(p.y);
|
||||||
|
let ix = i32(data[2u * x]);
|
||||||
|
var fx = data[2u * x + 1u];
|
||||||
|
let iy = i32(data[2u * w + 2u * y]);
|
||||||
|
var fy = data[2u * w + 2u * y + 1u];
|
||||||
|
let odd = fxs_rem2(ix + iy) == 1;
|
||||||
|
var flip_x = false;
|
||||||
|
var flip_y = false;
|
||||||
|
switch P.u[0].y {
|
||||||
|
case 1u: {
|
||||||
|
flip_x = fxs_rem2(ix) == 1;
|
||||||
|
flip_y = fxs_rem2(iy) == 1;
|
||||||
|
}
|
||||||
|
case 2u: {
|
||||||
|
flip_x = odd;
|
||||||
|
}
|
||||||
|
case 3u: {
|
||||||
|
flip_y = odd;
|
||||||
|
}
|
||||||
|
case 4u: {
|
||||||
|
flip_x = odd;
|
||||||
|
flip_y = odd;
|
||||||
|
}
|
||||||
|
default: {}
|
||||||
|
}
|
||||||
|
let r = P.f[0];
|
||||||
|
if (flip_x) {
|
||||||
|
fx = r.z - fx;
|
||||||
|
}
|
||||||
|
if (flip_y) {
|
||||||
|
fy = r.w - fy;
|
||||||
|
}
|
||||||
|
textureStore(out, p, sample_bilinear(src, r.x + fx, r.y + fy));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Magnify (distort2::magnify)
|
||||||
|
// data: per column (dx, sx, floor(sx) + 0.5), then per row (dy, sy, floor(sy) + 0.5).
|
||||||
|
// u[0] = (buffer width, square, scaling, blend: 0 none, 1 normal, 2 blend mode);
|
||||||
|
// u[1] = (blend mode id, seed); f[0] = (size, feather, opacity, magnification); f[1].x = size − feather
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_magnify(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let bw = P.u[0].x;
|
||||||
|
let x = u32(p.x);
|
||||||
|
let y = u32(p.y);
|
||||||
|
let dx = data[3u * x];
|
||||||
|
let sx = data[3u * x + 1u];
|
||||||
|
let nx = data[3u * x + 2u];
|
||||||
|
let dy = data[3u * bw + 3u * y];
|
||||||
|
let sy = data[3u * bw + 3u * y + 1u];
|
||||||
|
let ny = data[3u * bw + 3u * y + 2u];
|
||||||
|
var d: f32;
|
||||||
|
if (P.u[0].y != 0u) {
|
||||||
|
d = max(abs(dx), abs(dy));
|
||||||
|
} else {
|
||||||
|
d = sqrt(dx * dx + dy * dy);
|
||||||
|
}
|
||||||
|
let size = P.f[0].x;
|
||||||
|
let feather = P.f[0].y;
|
||||||
|
var w: f32;
|
||||||
|
if (feather > 0.01) {
|
||||||
|
w = 1.0 - fxs_smoothstep(P.f[1].x, size, d);
|
||||||
|
} else {
|
||||||
|
w = clamp(size - d + 0.5, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
w = w * P.f[0].z;
|
||||||
|
let px = textureLoad(src, p, 0);
|
||||||
|
let blend = P.u[0].w;
|
||||||
|
if (w <= 0.0) {
|
||||||
|
if (blend == 0u) {
|
||||||
|
textureStore(out, p, vec4<f32>(0.0));
|
||||||
|
} else {
|
||||||
|
textureStore(out, p, px);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var m: vec4<f32>;
|
||||||
|
switch P.u[0].z {
|
||||||
|
case 1u: {
|
||||||
|
m = sample_bicubic(src, sx, sy);
|
||||||
|
}
|
||||||
|
case 2u: {
|
||||||
|
let seed = P.u[1].y;
|
||||||
|
let k = max(1.0 - 1.0 / P.f[0].w, 0.0);
|
||||||
|
let j1 = (hash_noise(x, y ^ (1u << 20u), seed) - 0.5) * k;
|
||||||
|
let j2 = (hash_noise(x, y ^ (2u << 20u), seed) - 0.5) * k;
|
||||||
|
m = sample_bilinear(src, nx + j1, ny + j2);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
m = sample_bilinear(src, nx, ny);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (blend == 0u) {
|
||||||
|
textureStore(out, p, m * w);
|
||||||
|
} else if (blend == 1u) {
|
||||||
|
textureStore(out, p, px + (m - px) * w);
|
||||||
|
} else {
|
||||||
|
textureStore(out, p, blend_pixel(P.u[1].x, px, m * w, 0.5));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Scatter (stylize2::scatter)
|
||||||
|
// u[0] = (grain, seed); f[0].x = amount
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_scatter(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let grain = P.u[0].x;
|
||||||
|
let seed = P.u[0].y;
|
||||||
|
let amt = P.f[0].x;
|
||||||
|
let x = u32(p.x);
|
||||||
|
let y = u32(p.y);
|
||||||
|
var dx = 0.0;
|
||||||
|
var dy = 0.0;
|
||||||
|
if (grain != 2u) {
|
||||||
|
dx = (hash_noise(x, y, seed) - 0.5) * 2.0 * amt;
|
||||||
|
}
|
||||||
|
if (grain != 1u) {
|
||||||
|
dy = (hash_noise(x, y, seed ^ 0x5bd1u) - 0.5) * 2.0 * amt;
|
||||||
|
}
|
||||||
|
textureStore(out, p, tex_get(src, i32(round_away(f32(p.x) + dx)), i32(round_away(f32(p.y) + dy))));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Brush Strokes (stylize2::brush_strokes)
|
||||||
|
// data: cell column per x (w), cell row per y (h), then per cell (dx, dy, length, jitter, samples,
|
||||||
|
// painted). u[0] = (buffer width, cell columns, paint surface, buffer height); f[0].x = blend
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_brush(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let w = P.u[0].x;
|
||||||
|
let h = P.u[0].w;
|
||||||
|
let cx = u32(data[u32(p.x)]);
|
||||||
|
let cy = u32(data[w + u32(p.y)]);
|
||||||
|
let base = w + h + (cy * P.u[0].y + cx) * 6u;
|
||||||
|
let dx = data[base];
|
||||||
|
let dy = data[base + 1u];
|
||||||
|
let l = data[base + 2u];
|
||||||
|
let jit = data[base + 3u];
|
||||||
|
let n = u32(data[base + 4u]);
|
||||||
|
let m = data[base + 5u];
|
||||||
|
let q = fxs_centre(p);
|
||||||
|
let bx = q.x - dy * jit;
|
||||||
|
let by = q.y + dx * jit;
|
||||||
|
var acc = vec4<f32>(0.0);
|
||||||
|
for (var i = 0u; i < n; i++) {
|
||||||
|
var t = 0.0;
|
||||||
|
if (n != 1u) {
|
||||||
|
t = (f32(i) / f32(n - 1u) - 0.5) * l;
|
||||||
|
}
|
||||||
|
acc += sample_bilinear_clamped(src, bx + dx * t, by + dy * t);
|
||||||
|
}
|
||||||
|
let stroke = acc / f32(n);
|
||||||
|
let orig = textureLoad(src, p, 0);
|
||||||
|
let surface = P.u[0].z;
|
||||||
|
var base_px = orig;
|
||||||
|
if (surface == 1u) {
|
||||||
|
base_px = vec4<f32>(0.0);
|
||||||
|
} else if (surface == 2u) {
|
||||||
|
base_px = vec4<f32>(1.0);
|
||||||
|
} else if (surface == 3u) {
|
||||||
|
base_px = vec4<f32>(0.0, 0.0, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
var painted: vec4<f32>;
|
||||||
|
if (surface == 2u || surface == 3u) {
|
||||||
|
let k = 1.0 - stroke.w * m;
|
||||||
|
painted = stroke * m + base_px * k;
|
||||||
|
} else {
|
||||||
|
painted = base_px + (stroke - base_px) * m;
|
||||||
|
}
|
||||||
|
textureStore(out, p, painted + (orig - painted) * P.f[0].x);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Roughen Edges (stylize2::roughen_edges)
|
||||||
|
|
||||||
|
// noise::fbm.
|
||||||
|
fn fxs_fbm(u: f32, v: f32, z: f32, seed: u32, octaves0: f32) -> f32 {
|
||||||
|
let octaves = clamp(octaves0, 1.0, 20.0);
|
||||||
|
let n = u32(ceil(octaves));
|
||||||
|
let frac = octaves - floor(octaves);
|
||||||
|
var sum = 0.0;
|
||||||
|
var norm = 0.0;
|
||||||
|
var amp = 1.0;
|
||||||
|
var f = 1.0;
|
||||||
|
for (var o = 0u; o < n; o++) {
|
||||||
|
var w = 1.0;
|
||||||
|
if (o + 1u == n && frac > 0.0) {
|
||||||
|
w = frac;
|
||||||
|
}
|
||||||
|
sum += value_noise(u * f, v * f, z + f32(o) * 7.31, seed + o) * amp * w;
|
||||||
|
norm += amp * w;
|
||||||
|
amp *= 0.5;
|
||||||
|
f *= 2.0;
|
||||||
|
}
|
||||||
|
return sum / max(norm, 1e-6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// aux = the (blurred) alpha in .w. u[0] = (seed, cycle evolution, coloured);
|
||||||
|
// f[0] = (offset x, offset y, scale · stretch x, scale · stretch y); f[1] = (frequency, evolution,
|
||||||
|
// cycle, cycle weight); f[2] = (octaves, influence, influence multiplier, sharpness);
|
||||||
|
// f[3] = (edge colour, sharpness multiplier)
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_roughen(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let px = textureLoad(src, p, 0);
|
||||||
|
if (px.w <= 0.0) {
|
||||||
|
textureStore(out, p, px);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dv = textureLoad(aux, p, 0).w;
|
||||||
|
let fm = P.f[1].x;
|
||||||
|
let nx = (f32(p.x) + 0.5 - P.f[0].x) / P.f[0].z * fm;
|
||||||
|
let ny = (f32(p.y) + 0.5 - P.f[0].y) / P.f[0].w * fm;
|
||||||
|
let seed = P.u[0].x;
|
||||||
|
let evo = P.f[1].y;
|
||||||
|
let oct = P.f[2].x;
|
||||||
|
var n = fxs_fbm(nx, ny, evo, seed, oct);
|
||||||
|
if (P.u[0].y != 0u) {
|
||||||
|
n += (fxs_fbm(nx, ny, evo - P.f[1].z, seed, oct) - n) * P.f[1].w;
|
||||||
|
}
|
||||||
|
let infl = P.f[2].y;
|
||||||
|
let s = dv - 0.5 + (n - 0.5) * infl * P.f[2].z * 0.5;
|
||||||
|
let m = clamp(s * P.f[2].w * P.f[3].w * 4.0 + 0.5, 0.0, 1.0);
|
||||||
|
let u = fxs_unpremul(px);
|
||||||
|
var c = u.xyz;
|
||||||
|
if (P.u[0].z != 0u) {
|
||||||
|
let band = 1.0 - fxs_smoothstep(0.5, 0.9, dv + (n - 0.5) * infl * 0.5);
|
||||||
|
c = c + (P.f[3].xyz - c) * band;
|
||||||
|
}
|
||||||
|
let a = u.w * m;
|
||||||
|
textureStore(out, p, vec4<f32>(c * a, a));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Texturize (stylize2::texturize)
|
||||||
|
|
||||||
|
// The texture layer placed into the buffer (transition::place_layer) as luminance
|
||||||
|
// (Plane::luma). aux = the texture layer's buffer; data: per column x, then per row y in its
|
||||||
|
// pixels. u[0].x = buffer width
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_tex_luma(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let w = P.u[0].x;
|
||||||
|
let t = sample_bilinear(aux, data[u32(p.x)], data[w + u32(p.y)]);
|
||||||
|
let l = luminance(fxs_unpremul(t).xyz);
|
||||||
|
textureStore(out, p, vec4<f32>(l));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Relief shading. aux = the blurred texture luminance; f[0] = (light x, light y, contrast)
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_texturize(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
let shade = max(1.0 - P.f[0].z * 4.0 * (gx * P.f[0].x + gy * P.f[0].y), 0.0);
|
||||||
|
let px = textureLoad(src, p, 0);
|
||||||
|
textureStore(out, p, vec4<f32>(px.xyz * shade, px.w));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Glow (misc::glow) extras
|
||||||
|
|
||||||
|
// Bright pass with the Arbitrary Map: as `glow_bright` (same f[0] / u[0] layout, see
|
||||||
|
// kernels.wgsl) with the red, green and blue curves at data offsets f[1].xyz (−1 = identity).
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_glow_map(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let px = textureLoad(src, p, 0);
|
||||||
|
let a = px.w;
|
||||||
|
if (a <= 0.0) {
|
||||||
|
textureStore(out, p, vec4<f32>(0.0));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let thr = P.f[0].x;
|
||||||
|
var l = luminance(px.xyz / a);
|
||||||
|
if (P.u[0].y != 0u) {
|
||||||
|
l = a;
|
||||||
|
}
|
||||||
|
let k = clamp((l - thr) / max(1.0 - thr, 1e-3), 0.0, 1.0);
|
||||||
|
let t = glow_ab_t(l);
|
||||||
|
let c = vec3<f32>(curve_eval(P.f[1].x, t), curve_eval(P.f[1].y, t), curve_eval(P.f[1].z, t));
|
||||||
|
textureStore(out, p, vec4<f32>(c * k * a, k * a));
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Top with another Glow Operation: blend_pixel(mode, original, glow · intensity, 0).
|
||||||
|
// aux = the blurred glow; u[0].x = blend mode id; f[0].x = intensity
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_glow_op(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let g = textureLoad(aux, p, 0) * P.f[0].x;
|
||||||
|
var o = blend_pixel(P.u[0].x, textureLoad(src, p, 0), g, 0.0);
|
||||||
|
o.w = clamp(o.w, 0.0, 1.0);
|
||||||
|
textureStore(out, p, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- accumulation (Transform motion blur)
|
||||||
|
|
||||||
|
// out = src + aux
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_add(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
textureStore(out, p, textureLoad(src, p, 0) + textureLoad(aux, p, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// out = src / f[0].x
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxs_div(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (!fxs_inside(p)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
textureStore(out, p, textureLoad(src, p, 0) / P.f[0].x);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,729 @@
|
||||||
|
// GPU effects (tonal and colour correction family): see src/fx_tone.rs. Each operation mirrors
|
||||||
|
// the CPU effect in effectcraft-effects operation for operation. Every name here is prefixed
|
||||||
|
// `fxt_` (the family files share one module).
|
||||||
|
|
||||||
|
fn fxt_straight(px: vec4<f32>) -> vec3<f32> {
|
||||||
|
if (px.w > 1e-6) {
|
||||||
|
return px.xyz / px.w;
|
||||||
|
}
|
||||||
|
return vec3<f32>(0.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fxt_mix(a: vec3<f32>, b: vec3<f32>, t: f32) -> vec3<f32> {
|
||||||
|
return a + (b - a) * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
// f32::rem_euclid(360.0).
|
||||||
|
fn fxt_rem360(x: f32) -> f32 {
|
||||||
|
let r = x - 360.0 * floor(x / 360.0);
|
||||||
|
return select(r, 0.0, r >= 360.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Levels (Individual Controls) channel `i` (data: 5 × (ib, iw, g, ob, ow, identity)).
|
||||||
|
fn fxt_lv(i: u32, v: f32, clip_b: bool, clip_w: bool) -> f32 {
|
||||||
|
let o = i * 6u;
|
||||||
|
if (data[o + 5u] != 0.0) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
var t = (v - data[o]) / max(data[o + 1u] - data[o], 1e-6);
|
||||||
|
if (clip_b) {
|
||||||
|
t = max(t, 0.0);
|
||||||
|
}
|
||||||
|
if (clip_w) {
|
||||||
|
t = min(t, 1.0);
|
||||||
|
}
|
||||||
|
return data[o + 3u] + (data[o + 4u] - data[o + 3u]) * powz(max(t, 0.0), 1.0 / max(data[o + 2u], 0.01));
|
||||||
|
}
|
||||||
|
|
||||||
|
// color_fx::level (data offset o: ib, iw, g, ob, ow, clip black, clip white).
|
||||||
|
fn fxt_level(o: u32, v: f32) -> f32 {
|
||||||
|
var t = (v - data[o]) / max(data[o + 1u] - data[o], 1e-6);
|
||||||
|
if (data[o + 5u] != 0.0) {
|
||||||
|
t = max(t, 0.0);
|
||||||
|
}
|
||||||
|
if (data[o + 6u] != 0.0) {
|
||||||
|
t = min(t, 1.0);
|
||||||
|
}
|
||||||
|
t = powz(max(t, 0.0), 1.0 / data[o + 2u]);
|
||||||
|
return data[o + 3u] + (data[o + 4u] - data[o + 3u]) * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fxt_level_ident(o: u32) -> bool {
|
||||||
|
return data[o] == 0.0 && data[o + 1u] == 1.0 && data[o + 2u] == 1.0 && data[o + 3u] == 0.0 && data[o + 4u] == 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fxt_adjust_sat(s: f32, sat: f32) -> f32 {
|
||||||
|
if (sat >= 0.0) {
|
||||||
|
return s + (1.0 - s) * sat * min(s, 1.0);
|
||||||
|
}
|
||||||
|
return s * (1.0 + sat);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fxt_adjust_light(l: f32, light: f32) -> f32 {
|
||||||
|
if (light >= 0.0) {
|
||||||
|
return l + (1.0 - l) * light;
|
||||||
|
}
|
||||||
|
return l * (1.0 + light);
|
||||||
|
}
|
||||||
|
|
||||||
|
// HueRange::weight for range `r` (data: start, end, fall start, fall end, hue, sat, light).
|
||||||
|
fn fxt_range_weight(r: u32, h: f32) -> f32 {
|
||||||
|
let o = r * 7u;
|
||||||
|
let width = fxt_rem360(data[o + 1u] - data[o]);
|
||||||
|
let d = fxt_rem360(h - data[o]);
|
||||||
|
if (d <= width) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
let after = d - width;
|
||||||
|
let before = 360.0 - d;
|
||||||
|
var wa = 0.0;
|
||||||
|
if (data[o + 3u] > 0.0) {
|
||||||
|
wa = 1.0 - after / data[o + 3u];
|
||||||
|
}
|
||||||
|
var wb = 0.0;
|
||||||
|
if (data[o + 2u] > 0.0) {
|
||||||
|
wb = 1.0 - before / data[o + 2u];
|
||||||
|
}
|
||||||
|
return clamp(max(wa, wb), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// color3::colour_match.
|
||||||
|
fn fxt_colour_match(c: vec3<f32>, key: vec3<f32>, key_hue: f32, mode: u32, tol: f32, soft: f32) -> f32 {
|
||||||
|
var d: f32;
|
||||||
|
if (mode == 0u) {
|
||||||
|
d = sqrt(dot(c - key, c - key)) / sqrt(3.0);
|
||||||
|
} else if (mode == 1u) {
|
||||||
|
let hs = rgb_to_hsl(c);
|
||||||
|
let dh0 = abs(hs.x - key_hue);
|
||||||
|
let dh = min(dh0, 1.0 - dh0) * 2.0;
|
||||||
|
d = select(dh, 1.0, hs.y < 0.02);
|
||||||
|
} else {
|
||||||
|
let sa = max(c.x + c.y + c.z, 1e-6);
|
||||||
|
let sk = max(key.x + key.y + key.z, 1e-6);
|
||||||
|
let a = vec2<f32>(c.x / sa, c.y / sa);
|
||||||
|
let k = vec2<f32>(key.x / sk, key.y / sk);
|
||||||
|
d = sqrt(dot(a - k, a - k)) * 1.5;
|
||||||
|
}
|
||||||
|
if (d <= tol) {
|
||||||
|
return 1.0;
|
||||||
|
}
|
||||||
|
if (soft <= 1e-6) {
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
return clamp(1.0 - (d - tol) / soft, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// color3::knee.
|
||||||
|
fn fxt_knee(v: f32, start: f32, limit: f32) -> f32 {
|
||||||
|
if (v <= start || limit <= start) {
|
||||||
|
return min(v, limit);
|
||||||
|
}
|
||||||
|
let r = limit - start;
|
||||||
|
return start + r * (1.0 - exp(-(v - start) / r));
|
||||||
|
}
|
||||||
|
|
||||||
|
// PS Arbitrary Map lookup (map in data, n entries; f[0].x = phase offset in entries).
|
||||||
|
fn fxt_lookup(v: f32, n: u32) -> f32 {
|
||||||
|
let nf = f32(n);
|
||||||
|
var x = clamp(v, 0.0, 1.0) * (nf - 1.0) + P.f[0].x;
|
||||||
|
x = x - nf * floor(x / nf);
|
||||||
|
let fl = floor(x);
|
||||||
|
let i0 = u32(fl) % n;
|
||||||
|
let i1 = select(i0, i0 + 1u, i0 + 1u < n);
|
||||||
|
let t = x - fl;
|
||||||
|
return data[i0] + (data[i1] - data[i0]) * t;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fxt_eq_look(table: u32, n: u32, v: f32) -> f32 {
|
||||||
|
let i = min(u32(round_away(clamp(v, 0.0, 1.0) * f32(n - 1u))), n - 1u);
|
||||||
|
return data[table * n + i];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-pixel colour corrections. u[0].x = op (layouts in src/fx_tone.rs):
|
||||||
|
// 1 Levels (Individual Controls) 2 Gamma/Pedestal/Gain 3 Photo Filter 4 Change to Color
|
||||||
|
// 5 Leave Color 6 Change Color 7 Broadcast Colors 8 Color Balance (HLS) 9 CC Toner
|
||||||
|
// 10 CC Color Offset 11 CC Kernel 12 PS Arbitrary Map 13 Video Limiter
|
||||||
|
// 14 Hue/Saturation with colour ranges 15 Levels with channel controls
|
||||||
|
// 16 Auto Levels / Contrast / Color apply 17 Equalize apply 18 Invert (HLS / YIQ channels)
|
||||||
|
// 19 luminance plane 20 Shadow/Highlight (aux = (shadow base, highlight base))
|
||||||
|
// 21 pack src.x, aux.x 22 Shadow/Highlight clip remap 23 CC Color Neutralizer
|
||||||
|
// 24 Color Stabilizer (data: per channel n, then 5 × (x, y))
|
||||||
|
@compute @workgroup_size(16, 16)
|
||||||
|
fn fxt_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||||
|
let dims = out_dims();
|
||||||
|
let p = vec2<i32>(gid.xy);
|
||||||
|
if (p.x >= dims.x || p.y >= dims.y) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let px = textureLoad(src, p, 0);
|
||||||
|
let a = px.w;
|
||||||
|
let u0 = P.u[0];
|
||||||
|
let f0 = P.f[0];
|
||||||
|
// Image::map_straight: transparent pixels stay as they are.
|
||||||
|
let straight_skip = a <= 0.0;
|
||||||
|
let c = fxt_straight(px);
|
||||||
|
var o = px;
|
||||||
|
switch u0.x {
|
||||||
|
case 1u: {
|
||||||
|
let cb = u0.y != 0u;
|
||||||
|
let cw = u0.z != 0u;
|
||||||
|
// color2::map_ca.
|
||||||
|
if (a <= 0.0 && px.x == 0.0 && px.y == 0.0 && px.z == 0.0) {
|
||||||
|
let na = clamp(fxt_lv(4u, 0.0, cb, cw), 0.0, 1.0);
|
||||||
|
if (na > 0.0) {
|
||||||
|
o = vec4<f32>(0.0, 0.0, 0.0, clamp(na, 0.0, 1.0));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let aa = max(a, 0.0);
|
||||||
|
var nc: vec3<f32>;
|
||||||
|
for (var i = 0u; i < 3u; i++) {
|
||||||
|
nc[i] = fxt_lv(i + 1u, fxt_lv(0u, c[i], cb, cw), cb, cw);
|
||||||
|
}
|
||||||
|
let na = clamp(clamp(fxt_lv(4u, aa, cb, cw), 0.0, 1.0), 0.0, 1.0);
|
||||||
|
o = vec4<f32>(nc * na, na);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 2u: {
|
||||||
|
if (straight_skip) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let stretch = P.f[3].x;
|
||||||
|
var r: vec3<f32>;
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
var v = c[i];
|
||||||
|
if (stretch > 0.0 && v > 0.0 && v < 1.0) {
|
||||||
|
let q = 1.0 - v;
|
||||||
|
v = v + stretch * 0.25 * v * (q * q * q * q);
|
||||||
|
}
|
||||||
|
let g = P.f[i];
|
||||||
|
r[i] = max(g.y + (g.z - g.y) * powz(max(v, 0.0), g.x), 0.0);
|
||||||
|
}
|
||||||
|
o = vec4<f32>(r * a, a);
|
||||||
|
}
|
||||||
|
case 3u: {
|
||||||
|
if (straight_skip) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var r = fxt_mix(c, c * f0.xyz, f0.w);
|
||||||
|
if (u0.y != 0u) {
|
||||||
|
let l0 = luminance(c);
|
||||||
|
let l1 = max(luminance(r), 1e-6);
|
||||||
|
r = r * l0 / l1;
|
||||||
|
}
|
||||||
|
o = vec4<f32>(r * a, a);
|
||||||
|
}
|
||||||
|
case 4u: {
|
||||||
|
if (straight_skip) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let fr = f0.xyz;
|
||||||
|
let tc = P.f[1].xyz;
|
||||||
|
let tol = P.f[2].xyz;
|
||||||
|
let soft = f0.w;
|
||||||
|
let hsl = rgb_to_hsl(c);
|
||||||
|
let d = vec3<f32>(abs(fract_euclid(hsl.x - fr.x + 0.5) - 0.5) * 2.0, abs(hsl.z - fr.z), abs(hsl.y - fr.y));
|
||||||
|
var k = 1.0;
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
let over = d[i] - tol[i];
|
||||||
|
if (over > 0.0) {
|
||||||
|
k = min(k, select(clamp(1.0 - over / soft, 0.0, 1.0), 0.0, soft <= 1e-6));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let ch_l = (u0.y & 1u) != 0u;
|
||||||
|
let ch_s = (u0.y & 2u) != 0u;
|
||||||
|
var r = c;
|
||||||
|
if ((u0.y & 8u) != 0u) {
|
||||||
|
r = vec3<f32>(k);
|
||||||
|
} else if (k > 0.0) {
|
||||||
|
var nh: f32;
|
||||||
|
var ns = hsl.y;
|
||||||
|
var nl = hsl.z;
|
||||||
|
if ((u0.y & 4u) != 0u) {
|
||||||
|
let dh = (fract_euclid(tc.x - fr.x + 0.5) - 0.5) * k;
|
||||||
|
nh = fract_euclid(hsl.x + dh);
|
||||||
|
if (ch_s) {
|
||||||
|
ns = hsl.y + (tc.y - fr.y) * k;
|
||||||
|
}
|
||||||
|
if (ch_l) {
|
||||||
|
nl = hsl.z + (tc.z - fr.z) * k;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let dh = (fract_euclid(tc.x - hsl.x + 0.5) - 0.5) * k;
|
||||||
|
nh = fract_euclid(hsl.x + dh);
|
||||||
|
if (ch_s) {
|
||||||
|
ns = hsl.y + (tc.y - hsl.y) * k;
|
||||||
|
}
|
||||||
|
if (ch_l) {
|
||||||
|
nl = hsl.z + (tc.z - hsl.z) * k;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
r = hsl_to_rgb(nh, clamp(ns, 0.0, 1.0), clamp(nl, 0.0, 1.0));
|
||||||
|
}
|
||||||
|
o = vec4<f32>(r * a, a);
|
||||||
|
}
|
||||||
|
case 5u: {
|
||||||
|
if (straight_skip) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let keep = f0.xyz;
|
||||||
|
let amt = P.f[1].x;
|
||||||
|
let tol = P.f[1].y;
|
||||||
|
let soft = P.f[1].z;
|
||||||
|
var d: f32;
|
||||||
|
if (u0.y != 0u) {
|
||||||
|
let hsl = rgb_to_hsl(c);
|
||||||
|
d = select(abs(fract_euclid(hsl.x - f0.w + 0.5) - 0.5) * 2.0, 1.0, hsl.y < 0.02);
|
||||||
|
} else {
|
||||||
|
d = sqrt(dot(c - keep, c - keep)) / sqrt(3.0);
|
||||||
|
}
|
||||||
|
var keepk: f32;
|
||||||
|
if (tol >= 1.0 || d <= tol) {
|
||||||
|
keepk = 1.0;
|
||||||
|
} else if (soft <= 1e-6) {
|
||||||
|
keepk = 0.0;
|
||||||
|
} else {
|
||||||
|
keepk = clamp(1.0 - (d - tol) / soft, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
let g = luminance(c);
|
||||||
|
let r = fxt_mix(c, vec3<f32>(g), amt * (1.0 - keepk));
|
||||||
|
o = vec4<f32>(r * a, a);
|
||||||
|
}
|
||||||
|
case 6u: {
|
||||||
|
if (a <= 0.0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var m = fxt_colour_match(c, f0.xyz, f0.w, u0.y, P.f[2].x, P.f[2].y);
|
||||||
|
if ((u0.z & 2u) != 0u) {
|
||||||
|
m = 1.0 - m;
|
||||||
|
}
|
||||||
|
var r = c;
|
||||||
|
if ((u0.z & 1u) != 0u) {
|
||||||
|
r = vec3<f32>(m);
|
||||||
|
} else if (m > 0.0) {
|
||||||
|
let hsl = rgb_to_hsl(c);
|
||||||
|
let dh = P.f[1].x;
|
||||||
|
let dl = P.f[1].y;
|
||||||
|
let ds = P.f[1].z;
|
||||||
|
let h2 = fract_euclid(hsl.x + dh);
|
||||||
|
let s2 = clamp(hsl.y + ds * select(hsl.y, 1.0 - hsl.y, ds > 0.0), 0.0, 1.0);
|
||||||
|
let l2 = clamp(hsl.z + dl * select(hsl.z, 1.0 - hsl.z, dl > 0.0), 0.0, 1.0);
|
||||||
|
r = fxt_mix(c, hsl_to_rgb(h2, s2, l2), m);
|
||||||
|
}
|
||||||
|
o = vec4<f32>(r * a, a);
|
||||||
|
}
|
||||||
|
case 7u: {
|
||||||
|
if (a <= 0.0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let pal = u0.y != 0u;
|
||||||
|
let y = 0.299 * c.x + 0.587 * c.y + 0.114 * c.z;
|
||||||
|
var ch: f32;
|
||||||
|
if (pal) {
|
||||||
|
let uu = 0.492 * (c.z - y);
|
||||||
|
let vv = 0.877 * (c.x - y);
|
||||||
|
ch = sqrt(uu * uu + vv * vv);
|
||||||
|
} else {
|
||||||
|
let ii = 0.596 * c.x - 0.274 * c.y - 0.322 * c.z;
|
||||||
|
let qq = 0.211 * c.x - 0.523 * c.y + 0.312 * c.z;
|
||||||
|
ch = sqrt(ii * ii + qq * qq);
|
||||||
|
}
|
||||||
|
let max_norm = f0.x;
|
||||||
|
let unsafe_px = y + ch > max_norm + 1e-5;
|
||||||
|
switch u0.z {
|
||||||
|
case 2u: {
|
||||||
|
if (unsafe_px) {
|
||||||
|
o = vec4<f32>(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 3u: {
|
||||||
|
if (!unsafe_px) {
|
||||||
|
o = vec4<f32>(0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 1u: {
|
||||||
|
if (unsafe_px) {
|
||||||
|
let target_v = max(max_norm - y, 0.0);
|
||||||
|
let s = select(1.0, target_v / ch, ch > 1e-6);
|
||||||
|
var r = y + (c - y) * s;
|
||||||
|
if (y > max_norm) {
|
||||||
|
r = r * max_norm / y;
|
||||||
|
}
|
||||||
|
o = vec4<f32>(r * a, a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
if (unsafe_px) {
|
||||||
|
let k = max_norm / (y + ch);
|
||||||
|
o = vec4<f32>(c * k * a, a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 8u: {
|
||||||
|
if (straight_skip) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let hsl = rgb_to_hsl(c);
|
||||||
|
let dh = f0.x;
|
||||||
|
let dl = f0.y;
|
||||||
|
let ds = f0.z;
|
||||||
|
var l = hsl.z;
|
||||||
|
if (dl > 0.0) {
|
||||||
|
l = l + (1.0 - l) * dl;
|
||||||
|
} else {
|
||||||
|
l = l + l * dl;
|
||||||
|
}
|
||||||
|
var s = hsl.y;
|
||||||
|
if (ds > 0.0) {
|
||||||
|
s = s + (1.0 - s) * ds;
|
||||||
|
} else {
|
||||||
|
s = s + s * ds;
|
||||||
|
}
|
||||||
|
let r = hsl_to_rgb(hsl.x + dh, clamp(s, 0.0, 1.0), clamp(l, 0.0, 1.0));
|
||||||
|
o = vec4<f32>(r * a, a);
|
||||||
|
}
|
||||||
|
case 9u: {
|
||||||
|
if (straight_skip) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let n = u0.y;
|
||||||
|
let t = clamp(luminance(c), 0.0, 1.0) * f32(n);
|
||||||
|
let k = min(u32(floor(t)), n - 1u);
|
||||||
|
let tone = fxt_mix(P.f[1u + k].xyz, P.f[2u + k].xyz, t - f32(k));
|
||||||
|
let r = fxt_mix(tone, c, f0.x);
|
||||||
|
o = vec4<f32>(r * a, a);
|
||||||
|
}
|
||||||
|
case 10u: {
|
||||||
|
if (straight_skip) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var r = c;
|
||||||
|
for (var k = 0; k < 3; k++) {
|
||||||
|
if (f0[k] == 0.0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let x = c[k] + f0[k];
|
||||||
|
if (x <= 1.0) {
|
||||||
|
r[k] = x;
|
||||||
|
} else if (u0.y == 1u) {
|
||||||
|
r[k] = max(2.0 - x, 0.0);
|
||||||
|
} else if (u0.y == 2u) {
|
||||||
|
r[k] = select(1.0, 0.0, x - 1.0 < 0.5);
|
||||||
|
} else {
|
||||||
|
r[k] = fract_euclid(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
o = vec4<f32>(r * a, a);
|
||||||
|
}
|
||||||
|
case 11u: {
|
||||||
|
if (a <= 0.0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var acc = vec3<f32>(0.0);
|
||||||
|
for (var j = 0; j < 3; j++) {
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
let idx = j * 3 + i;
|
||||||
|
let w = P.f[idx / 4][idx % 4];
|
||||||
|
if (w == 0.0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let q = tex_get_clamped(src, p.x + i - 1, p.y + j - 1);
|
||||||
|
acc += fxt_straight(q) * w;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var v = acc * P.f[2].y + P.f[2].z;
|
||||||
|
if (u0.y != 0u) {
|
||||||
|
v = abs(v);
|
||||||
|
}
|
||||||
|
o = vec4<f32>(max(v, vec3<f32>(0.0)) * a, a);
|
||||||
|
}
|
||||||
|
case 12u: {
|
||||||
|
let n = u0.z;
|
||||||
|
let aa = max(a, 0.0);
|
||||||
|
let r = vec3<f32>(fxt_lookup(c.x, n), fxt_lookup(c.y, n), fxt_lookup(c.z, n));
|
||||||
|
var na = aa;
|
||||||
|
if (u0.y != 0u) {
|
||||||
|
na = fxt_lookup(aa, n);
|
||||||
|
}
|
||||||
|
o = vec4<f32>(r * na, na);
|
||||||
|
}
|
||||||
|
case 13u: {
|
||||||
|
if (a <= 0.0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let level = f0.x;
|
||||||
|
let start = f0.y;
|
||||||
|
let y = luminance(c);
|
||||||
|
var r = c;
|
||||||
|
if (u0.y != 1u) {
|
||||||
|
let y2 = fxt_knee(max(y, 0.0), start, level);
|
||||||
|
r = r - y + y2;
|
||||||
|
}
|
||||||
|
if (u0.y != 0u) {
|
||||||
|
let yl = luminance(r);
|
||||||
|
var t = 1.0;
|
||||||
|
for (var i = 0; i < 3; i++) {
|
||||||
|
let v = r[i];
|
||||||
|
if (v > level && v - yl > 1e-6) {
|
||||||
|
t = min(t, (level - yl) / (v - yl));
|
||||||
|
}
|
||||||
|
if (v < 0.0 && yl - v > 1e-6) {
|
||||||
|
t = min(t, yl / (yl - v));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t = clamp(t, 0.0, 1.0);
|
||||||
|
r = yl + (r - yl) * t;
|
||||||
|
}
|
||||||
|
r = clamp(r, vec3<f32>(0.0), vec3<f32>(level));
|
||||||
|
if (u0.z != 0u && any(abs(c - r) > vec3<f32>(1e-4))) {
|
||||||
|
r = P.f[1].xyz;
|
||||||
|
}
|
||||||
|
o = vec4<f32>(r * a, a);
|
||||||
|
}
|
||||||
|
case 14u: {
|
||||||
|
if (straight_skip) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let hsl = rgb_to_hsl(c);
|
||||||
|
var h = hsl.x;
|
||||||
|
var s = hsl.y;
|
||||||
|
var l = hsl.z;
|
||||||
|
if (u0.y != 0u) {
|
||||||
|
let cl = P.f[1].z;
|
||||||
|
h = P.f[1].x;
|
||||||
|
s = P.f[1].y;
|
||||||
|
l = clamp(l + cl * select(l, 1.0 - l, cl > 0.0), 0.0, 1.0);
|
||||||
|
} else {
|
||||||
|
var s2 = fxt_adjust_sat(s, f0.y);
|
||||||
|
var l2 = fxt_adjust_light(l, f0.z);
|
||||||
|
var h2 = h + f0.x;
|
||||||
|
for (var r = 0u; r < u0.z; r++) {
|
||||||
|
let w = fxt_range_weight(r, hsl.x * 360.0) * min(hsl.y * 20.0, 1.0);
|
||||||
|
if (w > 0.0) {
|
||||||
|
let ro = r * 7u;
|
||||||
|
h2 += data[ro + 4u] * w;
|
||||||
|
s2 = fxt_adjust_sat(clamp(s2, 0.0, 1.0), data[ro + 5u] * w);
|
||||||
|
l2 = fxt_adjust_light(l2, data[ro + 6u] * w);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h = fract_euclid(h2);
|
||||||
|
s = clamp(s2, 0.0, 1.0);
|
||||||
|
l = l2;
|
||||||
|
}
|
||||||
|
o = vec4<f32>(hsl_to_rgb(h, s, l) * a, a);
|
||||||
|
}
|
||||||
|
case 15u: {
|
||||||
|
var r = c;
|
||||||
|
if (!straight_skip) {
|
||||||
|
for (var i = 0u; i < 3u; i++) {
|
||||||
|
r[i] = fxt_level(0u, c[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The master pass writes premultiplied values; the channel pass reads them back.
|
||||||
|
var q = px;
|
||||||
|
if (!straight_skip) {
|
||||||
|
q = vec4<f32>(r * a, a);
|
||||||
|
}
|
||||||
|
let qc = fxt_straight(q);
|
||||||
|
let qa = max(q.w, 0.0);
|
||||||
|
var any_ch = false;
|
||||||
|
for (var k = 0u; k < 4u; k++) {
|
||||||
|
if (!fxt_level_ident(7u + 7u * k)) {
|
||||||
|
any_ch = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!any_ch) {
|
||||||
|
o = q;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var oc = qc;
|
||||||
|
for (var i = 0u; i < 3u; i++) {
|
||||||
|
let off = 7u + 7u * i;
|
||||||
|
if (!fxt_level_ident(off)) {
|
||||||
|
oc[i] = fxt_level(off, qc[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var na = qa;
|
||||||
|
if (!fxt_level_ident(28u)) {
|
||||||
|
na = clamp(fxt_level(28u, qa), 0.0, 1.0);
|
||||||
|
}
|
||||||
|
o = vec4<f32>(oc * na, na);
|
||||||
|
}
|
||||||
|
case 16u: {
|
||||||
|
if (a <= 0.0 && px.x == 0.0 && px.y == 0.0 && px.z == 0.0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let lo = f0.xyz;
|
||||||
|
let hi = P.f[1].xyz;
|
||||||
|
let g = P.f[2];
|
||||||
|
var r: vec3<f32>;
|
||||||
|
for (var k = 0; k < 3; k++) {
|
||||||
|
let st = clamp((c[k] - lo[k]) / (hi[k] - lo[k]), 0.0, 1.0);
|
||||||
|
r[k] = powz(st, g[k]);
|
||||||
|
}
|
||||||
|
r = fxt_mix(r, c, g.w);
|
||||||
|
let na = clamp(max(a, 0.0), 0.0, 1.0);
|
||||||
|
o = vec4<f32>(r * na, na);
|
||||||
|
}
|
||||||
|
case 17u: {
|
||||||
|
if (a <= 0.0 && px.x == 0.0 && px.y == 0.0 && px.z == 0.0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let n = u0.z;
|
||||||
|
let amt = f0.x;
|
||||||
|
var r: vec3<f32>;
|
||||||
|
if (u0.y == 1u) {
|
||||||
|
let l = luminance(c);
|
||||||
|
let nl = fxt_eq_look(0u, n, l);
|
||||||
|
if (l > 1e-5) {
|
||||||
|
r = c * nl / l;
|
||||||
|
} else {
|
||||||
|
r = vec3<f32>(nl);
|
||||||
|
}
|
||||||
|
} else if (u0.y == 2u) {
|
||||||
|
r = vec3<f32>(fxt_eq_look(0u, n, c.x), fxt_eq_look(0u, n, c.y), fxt_eq_look(0u, n, c.z));
|
||||||
|
} else {
|
||||||
|
r = vec3<f32>(fxt_eq_look(0u, n, c.x), fxt_eq_look(1u, n, c.y), fxt_eq_look(2u, n, c.z));
|
||||||
|
}
|
||||||
|
r = fxt_mix(c, r, amt);
|
||||||
|
let na = clamp(max(a, 0.0), 0.0, 1.0);
|
||||||
|
o = vec4<f32>(r * na, na);
|
||||||
|
}
|
||||||
|
case 18u: {
|
||||||
|
if (a <= 0.0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let ch = u0.y;
|
||||||
|
var r: vec3<f32>;
|
||||||
|
if (ch <= 7u) {
|
||||||
|
var hsl = rgb_to_hsl(c);
|
||||||
|
if (ch == 4u || ch == 5u) {
|
||||||
|
hsl.x = 1.0 - hsl.x;
|
||||||
|
}
|
||||||
|
if (ch == 4u || ch == 6u) {
|
||||||
|
hsl.z = 1.0 - hsl.z;
|
||||||
|
}
|
||||||
|
if (ch == 4u || ch == 7u) {
|
||||||
|
hsl.y = 1.0 - hsl.y;
|
||||||
|
}
|
||||||
|
r = hsl_to_rgb(hsl.x, hsl.y, hsl.z);
|
||||||
|
} else {
|
||||||
|
var q = vec3<f32>(
|
||||||
|
0.299 * c.x + 0.587 * c.y + 0.114 * c.z,
|
||||||
|
0.596 * c.x - 0.274 * c.y - 0.322 * c.z,
|
||||||
|
0.211 * c.x - 0.523 * c.y + 0.312 * c.z,
|
||||||
|
);
|
||||||
|
if (ch == 8u || ch == 9u) {
|
||||||
|
q.x = 1.0 - q.x;
|
||||||
|
}
|
||||||
|
if (ch == 8u || ch == 10u) {
|
||||||
|
q.y = -q.y;
|
||||||
|
}
|
||||||
|
if (ch == 8u || ch == 11u) {
|
||||||
|
q.z = -q.z;
|
||||||
|
}
|
||||||
|
r = max(
|
||||||
|
vec3<f32>(q.x + 0.956 * q.y + 0.621 * q.z, q.x - 0.272 * q.y - 0.647 * q.z, q.x - 1.106 * q.y + 1.703 * q.z),
|
||||||
|
vec3<f32>(0.0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
o = vec4<f32>((c + (r - c) * f0.x) * a, a);
|
||||||
|
}
|
||||||
|
case 19u: {
|
||||||
|
o = vec4<f32>(luminance(c));
|
||||||
|
}
|
||||||
|
case 20u: {
|
||||||
|
if (a <= 0.0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let bases = textureLoad(aux, p, 0);
|
||||||
|
let s_amt = f0.x;
|
||||||
|
let h_amt = f0.y;
|
||||||
|
let ws0 = clamp(1.0 - bases.x / f0.z, 0.0, 1.0);
|
||||||
|
let wh0 = clamp((bases.y - (1.0 - f0.w)) / f0.w, 0.0, 1.0);
|
||||||
|
let ws = ws0 * ws0;
|
||||||
|
let wh = wh0 * wh0;
|
||||||
|
let gain = (1.0 + s_amt * ws * 2.0) * (1.0 - h_amt * wh * 0.5);
|
||||||
|
let l = luminance(c);
|
||||||
|
let nl = l * gain;
|
||||||
|
let sat = pow(max(gain, 1e-3), P.f[1].x);
|
||||||
|
var r = nl + (c - l) * sat;
|
||||||
|
let mc = P.f[1].y;
|
||||||
|
if (mc != 0.0) {
|
||||||
|
let shift = (0.5 + (nl - 0.5) * (1.0 + mc)) - nl;
|
||||||
|
r = r + shift;
|
||||||
|
}
|
||||||
|
r = fxt_mix(max(r, vec3<f32>(0.0)), c, P.f[1].z);
|
||||||
|
o = vec4<f32>(r * a, a);
|
||||||
|
}
|
||||||
|
case 21u: {
|
||||||
|
o = vec4<f32>(px.x, textureLoad(aux, p, 0).x, 0.0, 0.0);
|
||||||
|
}
|
||||||
|
case 22u: {
|
||||||
|
if (a <= 0.0 && px.x == 0.0 && px.y == 0.0 && px.z == 0.0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let r = max(f0.x + (c - f0.y) * f0.z, vec3<f32>(0.0));
|
||||||
|
let na = clamp(max(a, 0.0), 0.0, 1.0);
|
||||||
|
o = vec4<f32>(r * na, na);
|
||||||
|
}
|
||||||
|
case 23u: {
|
||||||
|
if (a <= 0.0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let l = clamp(luminance(c), 0.0, 1.0);
|
||||||
|
let ws = 1.0 - fxt_smoothstep(0.0, 0.5, l);
|
||||||
|
let wh = fxt_smoothstep(0.5, 1.0, l);
|
||||||
|
let wm = 1.0 - ws - wh;
|
||||||
|
let pin = P.f[3].x;
|
||||||
|
let keep = 1.0 - pin * clamp(1.0 - 4.0 * l * (1.0 - l), 0.0, 1.0);
|
||||||
|
let contrast = P.f[3].z;
|
||||||
|
let darks = P.f[4].x;
|
||||||
|
let brights = P.f[4].y;
|
||||||
|
var r: vec3<f32>;
|
||||||
|
for (var k = 0; k < 3; k++) {
|
||||||
|
let d = P.f[0][k] * ws + P.f[1][k] * wm + P.f[2][k] * wh;
|
||||||
|
var v = c[k] + d * keep;
|
||||||
|
v = 0.5 + (v - 0.5) * (1.0 + contrast);
|
||||||
|
v += darks * 0.25 * (1.0 - l) * (1.0 - l) + brights * 0.25 * l * l;
|
||||||
|
r[k] = max(v, 0.0);
|
||||||
|
}
|
||||||
|
r = fxt_mix(r, c, P.f[3].y);
|
||||||
|
o = vec4<f32>(r * a, a);
|
||||||
|
}
|
||||||
|
case 24u: {
|
||||||
|
if (a <= 0.0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
var r: vec3<f32>;
|
||||||
|
for (var k = 0u; k < 3u; k++) {
|
||||||
|
let base = k * 11u;
|
||||||
|
let n = u32(data[base]);
|
||||||
|
let v = c[k];
|
||||||
|
// slice::partition_point(x < v), clamped to 1..n-1.
|
||||||
|
var i = 0u;
|
||||||
|
while (i < n && data[base + 1u + 2u * i] < v) {
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
i = clamp(i, 1u, n - 1u);
|
||||||
|
let ax = data[base + 1u + 2u * (i - 1u)];
|
||||||
|
let ay = data[base + 2u + 2u * (i - 1u)];
|
||||||
|
let cx = data[base + 1u + 2u * i];
|
||||||
|
let cy = data[base + 2u + 2u * i];
|
||||||
|
r[k] = ay + (cy - ay) * (v - ax) / max(cx - ax, 1e-9);
|
||||||
|
}
|
||||||
|
o = vec4<f32>(r * a, a);
|
||||||
|
}
|
||||||
|
default: {}
|
||||||
|
}
|
||||||
|
textureStore(out, p, o);
|
||||||
|
}
|
||||||
|
|
||||||
|
// util::smoothstep.
|
||||||
|
fn fxt_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);
|
||||||
|
}
|
||||||
|
|
@ -511,7 +511,7 @@ fn effect_chains_and_mixed_stacks() {
|
||||||
let mut l = s.footage(80, 50);
|
let mut l = s.footage(80, 50);
|
||||||
s.effect(&mut l, "ec.color.exposure", &[("master/exposure", n(0.7))]);
|
s.effect(&mut l, "ec.color.exposure", &[("master/exposure", n(0.7))]);
|
||||||
s.effect(&mut l, "ec.blur.gaussian", &[("blurriness", n(5.0))]);
|
s.effect(&mut l, "ec.blur.gaussian", &[("blurriness", n(5.0))]);
|
||||||
s.effect(&mut l, "ec.stylize.posterize", &[]);
|
s.effect(&mut l, "ec.stylize.coloremboss", &[]);
|
||||||
s.effect(&mut l, "ec.stylize.glow", &[]);
|
s.effect(&mut l, "ec.stylize.glow", &[]);
|
||||||
s.effect(&mut l, "ec.channel.invert", &[("blend", n(50.0))]);
|
s.effect(&mut l, "ec.channel.invert", &[("blend", n(50.0))]);
|
||||||
s.push(l);
|
s.push(l);
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ fn gpu_adjustment_chains_match_the_cpu() {
|
||||||
#[test]
|
#[test]
|
||||||
fn mixed_adjustment_chains_fall_back_per_effect() {
|
fn mixed_adjustment_chains_fall_back_per_effect() {
|
||||||
let mut stack = gpu_stack();
|
let mut stack = gpu_stack();
|
||||||
stack.insert(1, ("ec.stylize.posterize", vec![]));
|
stack.insert(1, ("ec.stylize.coloremboss", vec![]));
|
||||||
stack.push(("ec.stylize.glow", vec![("threshold", n(30.0))]));
|
stack.push(("ec.stylize.glow", vec![("threshold", n(30.0))]));
|
||||||
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||||
let s = adjustment_scene(depth, &stack, true);
|
let s = adjustment_scene(depth, &stack, true);
|
||||||
|
|
@ -93,7 +93,7 @@ impl FxTarget for Probe {
|
||||||
fn adjustment_stacks_split_into_gpu_runs_and_cpu_steps() {
|
fn adjustment_stacks_split_into_gpu_runs_and_cpu_steps() {
|
||||||
let Some(g) = gpu() else { return };
|
let Some(g) = gpu() else { return };
|
||||||
let mut stack = gpu_stack();
|
let mut stack = gpu_stack();
|
||||||
stack.insert(1, ("ec.stylize.posterize", vec![]));
|
stack.insert(1, ("ec.stylize.coloremboss", vec![]));
|
||||||
let s = adjustment_scene(BitDepth::Bpc32, &stack, false);
|
let s = adjustment_scene(BitDepth::Bpc32, &stack, false);
|
||||||
let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() });
|
let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() });
|
||||||
r.accel = Some(g);
|
r.accel = Some(g);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,230 @@
|
||||||
|
//! GPU keying, matte and channel family effects 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 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 off() -> Value {
|
||||||
|
Value::Bool(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// [`effect_case`] for hard thresholds (8-bit quantisation, bitwise operators): the composited
|
||||||
|
/// renders may flip a few pixels sitting on the threshold after the layer transform's float
|
||||||
|
/// rounding, so up to 0.1 % of them may differ by any amount.
|
||||||
|
fn discontinuous_case(id: &str, vals: &[(&str, Value)]) {
|
||||||
|
crate::tests::effect_direct(id, vals, false);
|
||||||
|
crate::tests::effect_direct(id, vals, true);
|
||||||
|
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);
|
||||||
|
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), 0.001);
|
||||||
|
check(&format!("{id} {vals:?} {depth:?} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.001);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn colour_luma_range_extract_keys() {
|
||||||
|
effect_case("ec.key.colorkey", &[("keyColor", c(0.7, 0.6, 0.5)), ("colorTolerance", n(60.0))]);
|
||||||
|
effect_case("ec.key.colorkey", &[("keyColor", c(0.5, 0.6, 0.5)), ("colorTolerance", n(70.0)), ("edgeThin", n(1.5)), ("edgeFeather", n(4.0))]);
|
||||||
|
effect_case("ec.key.colorkey", &[("keyColor", c(0.5, 0.6, 0.5)), ("colorTolerance", n(70.0)), ("edgeThin", n(-2.0))]);
|
||||||
|
effect_case("ec.key.luma", &[("threshold", n(120.0)), ("tolerance", n(40.0))]);
|
||||||
|
effect_case("ec.key.luma", &[("keyType", e(0)), ("threshold", n(140.0)), ("tolerance", n(30.0)), ("edgeFeather", n(3.0))]);
|
||||||
|
effect_case("ec.key.luma", &[("keyType", e(2)), ("threshold", n(128.0)), ("tolerance", n(25.0)), ("edgeThin", n(1.0))]);
|
||||||
|
effect_case("ec.key.luma", &[("keyType", e(3)), ("threshold", n(100.0)), ("tolerance", n(50.0))]);
|
||||||
|
effect_case("ec.key.colorrange", &[("fuzziness", n(20.0))]);
|
||||||
|
effect_case("ec.key.colorrange", &[("colorSpace", e(1)), ("minL", n(60.0)), ("maxA", n(150.0)), ("minB", n(90.0)), ("fuzziness", n(15.0))]);
|
||||||
|
effect_case("ec.key.colorrange", &[("colorSpace", e(2)), ("minL", n(80.0)), ("maxA", n(180.0)), ("minB", n(40.0)), ("fuzziness", n(30.0))]);
|
||||||
|
effect_case("ec.key.extract", &[("blackPoint", n(60.0)), ("whitePoint", n(200.0)), ("blackSoftness", n(30.0)), ("whiteSoftness", n(20.0))]);
|
||||||
|
discontinuous_case("ec.key.extract", &[("channel", e(1)), ("blackPoint", n(90.0)), ("invert", on())]);
|
||||||
|
effect_case("ec.key.extract", &[("channel", e(4)), ("whitePoint", n(200.0)), ("whiteSoftness", n(40.0))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn difference_colour_difference_and_screen_keys() {
|
||||||
|
effect_case("ec.key.differencematte", &[]);
|
||||||
|
effect_case("ec.key.differencematte", &[("view", e(2)), ("matchingTolerance", n(0.0)), ("blurBeforeDifference", n(3.0))]);
|
||||||
|
effect_case("ec.key.colordifference", &[("keyColor", c(0.2, 0.3, 0.8))]);
|
||||||
|
effect_case(
|
||||||
|
"ec.key.colordifference",
|
||||||
|
&[
|
||||||
|
("keyColor", c(0.3, 0.8, 0.2)),
|
||||||
|
("colorMatchingAccuracy", e(1)),
|
||||||
|
("partialAInBlack", n(20.0)),
|
||||||
|
("partialAGamma", n(1.4)),
|
||||||
|
("partialBOutWhite", n(200.0)),
|
||||||
|
("blackLevel", n(10.0)),
|
||||||
|
("gamma", n(0.8)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
for view in [1, 2, 3, 4, 5, 6, 8] {
|
||||||
|
effect_case("ec.key.colordifference", &[("keyColor", c(0.2, 0.3, 0.8)), ("view", e(view)), ("partialBInWhite", n(180.0))]);
|
||||||
|
}
|
||||||
|
effect_case("ec.key.screen", &[("screenColor", c(0.4, 0.75, 0.3))]);
|
||||||
|
effect_case(
|
||||||
|
"ec.key.screen",
|
||||||
|
&[
|
||||||
|
("screenColor", c(0.3, 0.4, 0.8)),
|
||||||
|
("screenGain", n(130.0)),
|
||||||
|
("screenBalance", n(30.0)),
|
||||||
|
("despill", n(60.0)),
|
||||||
|
("clipBlack", n(10.0)),
|
||||||
|
("clipWhite", n(85.0)),
|
||||||
|
("screenPreblur", n(2.0)),
|
||||||
|
("screenShrinkGrow", n(-1.5)),
|
||||||
|
("screenSoftness", n(3.0)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
effect_case("ec.key.screen", &[("screenColor", c(0.8, 0.4, 0.3)), ("screenShrinkGrow", n(2.0)), ("edgeColor", e(1))]);
|
||||||
|
for view in [1, 2] {
|
||||||
|
effect_case("ec.key.screen", &[("screenColor", c(0.4, 0.75, 0.3)), ("view", e(view))]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spill_unmult_and_key_cleaner() {
|
||||||
|
effect_case("ec.key.spill", &[("colorToSuppress", c(0.2, 0.9, 0.1))]);
|
||||||
|
effect_case("ec.key.spill", &[("colorToSuppress", c(0.9, 0.8, 0.1)), ("colorAccuracy", e(1)), ("suppression", n(150.0))]);
|
||||||
|
effect_case("ec.key.advancedspill", &[]);
|
||||||
|
effect_case("ec.key.advancedspill", &[("suppression", n(80.0)), ("ultraSettings/spillRange", n(20.0)), ("ultraSettings/lumaCorrection", n(50.0))]);
|
||||||
|
effect_case(
|
||||||
|
"ec.key.advancedspill",
|
||||||
|
&[
|
||||||
|
("method", e(1)),
|
||||||
|
("ultraSettings/keyColor", c(0.9, 0.2, 0.3)),
|
||||||
|
("ultraSettings/tolerance", n(40.0)),
|
||||||
|
("ultraSettings/desaturate", n(50.0)),
|
||||||
|
("ultraSettings/spillColorCorrection", n(30.0)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
effect_case("ec.key.unmult", &[]);
|
||||||
|
effect_case("ec.key.unmult", &[("backgroundColor", e(1)), ("whiteLevel", n(90.0)), ("softness", n(40.0)), ("clipHdrResults", off())]);
|
||||||
|
effect_case("ec.key.unmult", &[("blackLevel", n(30.0)), ("softness", n(0.0)), ("removeColorMatting", off())]);
|
||||||
|
effect_case("ec.key.keycleaner", &[]);
|
||||||
|
effect_case("ec.key.keycleaner", &[("additionalEdgeRadius", n(6.0)), ("reduceChatter", on()), ("alphaContrast", n(250.0)), ("strength", n(60.0))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn chokers() {
|
||||||
|
effect_case("ec.matte.simplechoker", &[("chokeMatte", n(1.5))]);
|
||||||
|
effect_case("ec.matte.simplechoker", &[("chokeMatte", n(-2.5))]);
|
||||||
|
effect_case("ec.matte.simplechoker", &[("chokeMatte", n(-1.0)), ("view", e(1))]);
|
||||||
|
effect_case("ec.matte.mattechoker", &[]);
|
||||||
|
effect_case(
|
||||||
|
"ec.matte.mattechoker",
|
||||||
|
&[
|
||||||
|
("geometricSoftness1", n(2.0)),
|
||||||
|
("choke1", n(-40.0)),
|
||||||
|
("geometricSoftness2", n(3.0)),
|
||||||
|
("choke2", n(30.0)),
|
||||||
|
("grayLevelSoftness2", n(40.0)),
|
||||||
|
("iterations", n(2.0)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn refine_mattes() {
|
||||||
|
effect_case("ec.matte.refinesoft", &[]);
|
||||||
|
effect_case("ec.matte.refinesoft", &[("radius", n(4.0)), ("smooth", n(40.0)), ("feather", n(30.0)), ("contrast", n(20.0)), ("shiftEdge", n(-25.0))]);
|
||||||
|
effect_case("ec.matte.refinesoft", &[("calculateEdgeDetails", off()), ("smooth", n(50.0)), ("shiftEdge", n(30.0))]);
|
||||||
|
effect_case("ec.matte.refinesoft", &[("radius", n(3.0)), ("viewEdgeRegion", on())]);
|
||||||
|
effect_case("ec.matte.refinesoft", &[("decontamination/viewDecontaminationMap", on()), ("decontamination/extendWhereSmoothed", off())]);
|
||||||
|
effect_case("ec.matte.refinesoft", &[("decontamination/increaseDecontaminationRadius", n(4.0)), ("decontamination/decontaminationAmount", n(60.0))]);
|
||||||
|
effect_case("ec.matte.refinehard", &[]);
|
||||||
|
effect_case("ec.matte.refinehard", &[("choke", n(20.0)), ("feather", n(20.0)), ("decontaminate", off())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn channel_effects() {
|
||||||
|
effect_case("ec.channel.setchannels", &[("setRedTo", e(2)), ("setGreenTo", e(4)), ("setBlueTo", e(5)), ("setAlphaTo", e(6))]);
|
||||||
|
effect_case("ec.channel.shiftchannels", &[("takeRedFrom", e(3)), ("takeGreenFrom", e(7)), ("takeAlphaFrom", e(4))]);
|
||||||
|
effect_case("ec.channel.removecolormatting", &[("backgroundColor", c(0.3, 0.5, 0.7))]);
|
||||||
|
effect_case("ec.channel.removecolormatting", &[("backgroundColor", c(0.8, 0.2, 0.1)), ("clipHdr", off())]);
|
||||||
|
for op in [0, 2, 3, 4, 8, 10, 12] {
|
||||||
|
discontinuous_case("ec.channel.arithmetic", &[("operator", e(op)), ("redValue", n(120.0)), ("greenValue", n(60.0)), ("blueValue", n(200.0))]);
|
||||||
|
}
|
||||||
|
effect_case("ec.channel.arithmetic", &[("operator", e(3)), ("redValue", n(200.0)), ("clip", off())]);
|
||||||
|
for mode in [0, 2, 4, 9, 11] {
|
||||||
|
effect_case("ec.channel.solidcomposite", &[("blendingMode", e(mode)), ("color", c(0.8, 0.3, 0.2)), ("opacity", n(70.0)), ("sourceOpacity", n(80.0))]);
|
||||||
|
}
|
||||||
|
effect_case("ec.channel.setmatte", &[("takeMatteFrom", e(4))]);
|
||||||
|
effect_case("ec.channel.setmatte", &[("takeMatteFrom", e(3)), ("invertMatte", on())]);
|
||||||
|
effect_case("ec.channel.setmatte", &[("takeMatteFrom", e(1)), ("compositeMatteWithOriginal", off()), ("premultiplyMatteLayer", off())]);
|
||||||
|
for (from, to) in [(0, 0), (1, 0), (2, 0), (3, 0), (8, 4), (9, 5), (10, 6), (11, 3), (12, 8), (13, 7), (7, 1)] {
|
||||||
|
effect_case("ec.channel.combiner", &[("from", e(from)), ("to", e(to))]);
|
||||||
|
}
|
||||||
|
effect_case("ec.channel.combiner", &[("from", e(4)), ("to", e(2)), ("invert", on()), ("solidAlpha", on())]);
|
||||||
|
effect_case("ec.channel.blend", &[("mode", e(1)), ("blendWithOriginal", n(30.0))]);
|
||||||
|
effect_case("ec.channel.calculations", &[("inputChannel", e(1))]);
|
||||||
|
effect_case("ec.channel.calculations", &[("inputChannel", e(0)), ("invertInput", on())]);
|
||||||
|
effect_case("ec.channel.compoundarithmetic", &[("operator", e(3)), ("operateOnChannels", e(1))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The channel and keying effects that read another layer, composited.
|
||||||
|
#[test]
|
||||||
|
fn layer_inputs() {
|
||||||
|
let cases: Vec<(&str, Vec<(&str, Value)>)> = vec![
|
||||||
|
("ec.key.differencematte", vec![("matchingTolerance", n(10.0)), ("matchingSoftness", n(20.0))]),
|
||||||
|
("ec.key.differencematte", vec![("ifLayerSizesDiffer", e(1)), ("blurBeforeDifference", n(2.0)), ("view", e(2))]),
|
||||||
|
("ec.channel.setmatte", vec![("takeMatteFrom", e(4)), ("stretchMatteToFit", off())]),
|
||||||
|
("ec.channel.setmatte", vec![("takeMatteFrom", e(3))]),
|
||||||
|
("ec.channel.setchannels", vec![("setGreenTo", e(0)), ("setAlphaTo", e(4))]),
|
||||||
|
("ec.channel.combiner", vec![("useSecondLayer", on()), ("from", e(11)), ("to", e(7))]),
|
||||||
|
("ec.channel.blend", vec![("mode", e(0)), ("blendWithOriginal", n(40.0))]),
|
||||||
|
("ec.channel.blend", vec![("mode", e(2))]),
|
||||||
|
("ec.channel.blend", vec![("mode", e(3)), ("ifLayerSizesDiffer", e(1))]),
|
||||||
|
("ec.channel.blend", vec![("mode", e(4))]),
|
||||||
|
("ec.channel.calculations", vec![("secondLayerChannel", e(2)), ("blendingMode", e(2)), ("secondLayerOpacity", n(70.0))]),
|
||||||
|
("ec.channel.calculations", vec![("inputChannel", e(4)), ("blendingMode", e(10)), ("preserveTransparency", on()), ("invertSecondLayer", on())]),
|
||||||
|
("ec.channel.compoundarithmetic", vec![("operator", e(1)), ("overflowBehavior", e(2))]),
|
||||||
|
("ec.channel.compoundarithmetic", vec![("operator", e(4)), ("operateOnChannels", e(1)), ("blendWithOriginal", n(25.0))]),
|
||||||
|
("ec.channel.compoundarithmetic", vec![("operator", e(13)), ("operateOnChannels", e(2))]),
|
||||||
|
];
|
||||||
|
let layer_param = |id: &str| match id {
|
||||||
|
"ec.key.differencematte" => vec!["differenceLayer"],
|
||||||
|
"ec.channel.setmatte" => vec!["takeMatteFromLayer"],
|
||||||
|
"ec.channel.setchannels" => vec!["sourceLayer2", "sourceLayer4"],
|
||||||
|
"ec.channel.combiner" => vec!["sourceLayer"],
|
||||||
|
"ec.channel.blend" => vec!["blendWithLayer"],
|
||||||
|
"ec.channel.calculations" => vec!["secondLayer"],
|
||||||
|
_ => vec!["secondSource"],
|
||||||
|
};
|
||||||
|
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||||
|
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 mut l = s.footage(70, 44);
|
||||||
|
let mut vals = vals.clone();
|
||||||
|
for p in layer_param(id) {
|
||||||
|
vals.push((p, Value::Layer(Some(oid.0))));
|
||||||
|
}
|
||||||
|
s.effect(&mut l, id, &vals);
|
||||||
|
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||||
|
s.push(l);
|
||||||
|
let label = format!("{id} {vals:?} {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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
//! GPU noise, blur and time family effects 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_color::Label;
|
||||||
|
use effectcraft_keyframe::{Keyframe, Value};
|
||||||
|
use effectcraft_project::build;
|
||||||
|
use effectcraft_project::{BitDepth, Comp, ItemKind, LayerSource};
|
||||||
|
use effectcraft_render::RenderOpts;
|
||||||
|
use effectcraft_time::{FrameRate, 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 medians() {
|
||||||
|
effect_case("ec.noise.median", &[("radius", n(2.0))]);
|
||||||
|
effect_case("ec.noise.median", &[("radius", n(5.0)), ("operateOnAlpha", on())]);
|
||||||
|
effect_case("ec.noise.medianlegacy", &[("radius", n(3.0))]);
|
||||||
|
effect_case("ec.noise.medianlegacy", &[("radius", n(2.0)), ("operateOnAlphaChannel", on())]);
|
||||||
|
effect_case("ec.noise.dustscratches", &[("radius", n(3.0)), ("threshold", n(10.0))]);
|
||||||
|
effect_case("ec.noise.dustscratches", &[("radius", n(2.0)), ("threshold", n(0.0)), ("operateOnAlpha", on())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn edge_preserving_blurs() {
|
||||||
|
effect_case("ec.blur.bilateral", &[]);
|
||||||
|
effect_case("ec.blur.bilateral", &[("radius", n(9.0)), ("threshold", n(30.0)), ("colorize", Value::Bool(false))]);
|
||||||
|
effect_case("ec.blur.bilateral", &[("radius", n(14.0)), ("threshold", n(5.0))]);
|
||||||
|
effect_case("ec.blur.smart", &[]);
|
||||||
|
effect_case("ec.blur.smart", &[("radius", n(6.0)), ("threshold", n(15.0)), ("mode", e(1))]);
|
||||||
|
effect_case("ec.blur.smart", &[("radius", n(4.0)), ("threshold", n(40.0)), ("mode", e(2))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sharpen_and_unsharp_mask() {
|
||||||
|
effect_case("ec.blur.sharpen", &[("amount", n(60.0))]);
|
||||||
|
effect_case("ec.blur.sharpen", &[("amount", n(250.0))]);
|
||||||
|
effect_case("ec.blur.unsharp", &[("amount", n(120.0)), ("radius", n(4.0))]);
|
||||||
|
effect_case("ec.blur.unsharp", &[("amount", n(80.0)), ("radius", n(2.5)), ("threshold", n(12.0))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn channel_and_compound_blur() {
|
||||||
|
effect_case("ec.blur.channel", &[("redBlurriness", n(8.0)), ("blueBlurriness", n(3.0))]);
|
||||||
|
effect_case("ec.blur.channel", &[("greenBlurriness", n(6.0)), ("alphaBlurriness", n(6.0)), ("repeatEdge", on())]);
|
||||||
|
effect_case("ec.blur.channel", &[("redBlurriness", n(5.0)), ("alphaBlurriness", n(10.0)), ("dimensions", e(1))]);
|
||||||
|
effect_case("ec.blur.compound", &[]);
|
||||||
|
effect_case("ec.blur.compound", &[("maximumBlur", n(12.0)), ("invertBlur", on())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compound Blur reading another layer (stretched and centred), composited.
|
||||||
|
#[test]
|
||||||
|
fn compound_blur_layer() {
|
||||||
|
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||||
|
for stretch in [true, false] {
|
||||||
|
let mut s = Scene::new(depth);
|
||||||
|
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
||||||
|
s.push(bg);
|
||||||
|
let mut map = s.footage(50, 36);
|
||||||
|
map.switches.video = false;
|
||||||
|
let mid = s.push(map);
|
||||||
|
let mut l = s.footage(70, 44);
|
||||||
|
s.effect(
|
||||||
|
&mut l,
|
||||||
|
"ec.blur.compound",
|
||||||
|
&[("blurLayer", Value::Layer(Some(mid.0))), ("maximumBlur", n(10.0)), ("stretchMapToFit", Value::Bool(stretch))],
|
||||||
|
);
|
||||||
|
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||||
|
s.push(l);
|
||||||
|
let label = format!("compound blur layer {stretch} {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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn minimax() {
|
||||||
|
effect_case("ec.channel.minimax", &[("radius", n(3.0))]);
|
||||||
|
effect_case("ec.channel.minimax", &[("radius", n(2.0)), ("operation", e(0)), ("channel", e(2))]);
|
||||||
|
effect_case("ec.channel.minimax", &[("radius", n(4.0)), ("operation", e(2)), ("channel", e(1)), ("direction", e(1))]);
|
||||||
|
effect_case("ec.channel.minimax", &[("radius", n(3.0)), ("operation", e(3)), ("channel", e(4)), ("dontShrinkEdges", on()), ("direction", e(2))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn turbulent_noise() {
|
||||||
|
effect_case("ec.noise.turbulent", &[]);
|
||||||
|
effect_case(
|
||||||
|
"ec.noise.turbulent",
|
||||||
|
&[("fractalType", e(3)), ("noiseType", e(3)), ("evolution", n(120.0)), ("evolutionOptions/turbulenceFactor", n(1.5)), ("blendingMode", e(5))],
|
||||||
|
);
|
||||||
|
effect_case(
|
||||||
|
"ec.noise.turbulent",
|
||||||
|
&[("fractalType", e(5)), ("noiseType", e(1)), ("complexity", n(3.5)), ("transform/scale", n(40.0)), ("overflow", e(1)), ("blendingMode", e(0))],
|
||||||
|
);
|
||||||
|
effect_case("ec.noise.turbulent", &[("fractalType", e(7)), ("noiseType", e(0)), ("transform/perspectiveOffset", on()), ("opacity", n(70.0))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn fractal_noise_other_modes() {
|
||||||
|
effect_case("ec.noise.fractal", &[("noiseType", e(3)), ("fractalType", e(4)), ("blendingMode", e(3))]);
|
||||||
|
effect_case(
|
||||||
|
"ec.noise.fractal",
|
||||||
|
&[
|
||||||
|
("fractalType", e(6)),
|
||||||
|
("evolutionOptions/cycleEvolution", on()),
|
||||||
|
("evolutionOptions/cycle", n(2.0)),
|
||||||
|
("evolution", n(500.0)),
|
||||||
|
("subSettings/subRotation", n(25.0)),
|
||||||
|
("subSettings/subOffset", pt(4.0, -3.0)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
effect_case("ec.noise.fractal", &[("fractalType", e(8)), ("noiseType", e(1)), ("blendingMode", e(12)), ("contrast", n(160.0)), ("invert", on())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn noise_alpha_and_hls() {
|
||||||
|
effect_case("ec.noise.noisealpha", &[("amount", n(40.0))]);
|
||||||
|
effect_case("ec.noise.noisealpha", &[("amount", n(70.0)), ("noise", e(3)), ("originalAlpha", e(2)), ("overflow", e(1)), ("noisePhase", n(100.0))]);
|
||||||
|
effect_case(
|
||||||
|
"ec.noise.noisealpha",
|
||||||
|
&[("amount", n(60.0)), ("noise", e(2)), ("originalAlpha", e(3)), ("overflow", e(2)), ("noiseOptions/cycleNoise", on()), ("randomSeed", n(9.0))],
|
||||||
|
);
|
||||||
|
effect_case("ec.noise.noisehls", &[("hue", n(30.0)), ("lightness", n(20.0)), ("saturation", n(40.0))]);
|
||||||
|
effect_case("ec.noise.noisehls", &[("noise", e(1)), ("lightness", n(50.0)), ("noisePhase", n(200.0))]);
|
||||||
|
effect_case("ec.noise.noisehls", &[("noise", e(2)), ("hue", n(60.0)), ("saturation", n(25.0)), ("grainSize", n(3.0))]);
|
||||||
|
effect_case("ec.noise.noisehlsauto", &[("noise", e(0)), ("hue", n(20.0)), ("lightness", n(30.0)), ("noiseAnimationSpeed", n(7.0))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remove_grain() {
|
||||||
|
effect_case("ec.noise.removegrain", &[]);
|
||||||
|
effect_case(
|
||||||
|
"ec.noise.removegrain",
|
||||||
|
&[
|
||||||
|
("noiseReductionSettings/noiseReduction", n(2.5)),
|
||||||
|
("noiseReductionSettings/passes", n(2.0)),
|
||||||
|
("noiseReductionSettings/mode", e(1)),
|
||||||
|
("fineTuning/texture", n(0.3)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
effect_case("ec.noise.removegrain", &[("unsharpMask/amount", n(80.0)), ("unsharpMask/radius", n(2.0)), ("unsharpMask/threshold", n(4.0))]);
|
||||||
|
effect_case("ec.noise.removegrain", &[("viewingMode", e(0)), ("previewRegion/showBox", on()), ("previewRegion/boxColor", c(1.0, 0.2, 0.1))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Echo and Posterize Time fetch other frames through the effect host: a layer whose rotation
|
||||||
|
/// is keyframed, at a nonzero time (the direct cases have no host and pass the layer through).
|
||||||
|
#[test]
|
||||||
|
fn echo_and_posterize_time() {
|
||||||
|
effect_case("ec.time.echo", &[]);
|
||||||
|
effect_case("ec.time.posterizetime", &[]);
|
||||||
|
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||||
|
for (id, vals) in [
|
||||||
|
("ec.time.echo", vec![("numberOfEchoes", n(3.0)), ("decay", n(0.7)), ("echoTime", n(-0.1))]),
|
||||||
|
("ec.time.echo", vec![("numberOfEchoes", n(4.0)), ("echoOperator", e(2)), ("echoTime", n(0.05))]),
|
||||||
|
("ec.time.echo", vec![("numberOfEchoes", n(2.0)), ("echoOperator", e(4)), ("startingIntensity", n(0.8))]),
|
||||||
|
("ec.time.echo", vec![("numberOfEchoes", n(5.0)), ("echoOperator", e(6))]),
|
||||||
|
("ec.time.posterizetime", vec![("frameRate", n(4.0))]),
|
||||||
|
] {
|
||||||
|
let mut s = Scene::new(depth);
|
||||||
|
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
||||||
|
s.push(bg);
|
||||||
|
// A precomp whose content moves, so the frames differ.
|
||||||
|
let inner = Comp::new(60, 40, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||||
|
let inner_id = s.p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.into()));
|
||||||
|
let mut a = s.footage(30, 24);
|
||||||
|
let pos = a.props.prop_mut("transform/position").unwrap();
|
||||||
|
pos.keys = vec![Keyframe::new(Tick::ZERO, crate::tests::v3(10.0, 12.0)), Keyframe::new(Tick::from_seconds_f64(1.0), crate::tests::v3(50.0, 30.0))];
|
||||||
|
let rot = a.props.prop_mut("transform/rotation").unwrap();
|
||||||
|
rot.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(0.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(90.0))];
|
||||||
|
s.p.comp_mut(inner_id).unwrap().layers = vec![a];
|
||||||
|
let mut l = build::layer(&mut s.p, &s.comp, "Inner", LayerSource::Comp { item: inner_id }, (60, 40), None);
|
||||||
|
s.effect(&mut l, id, &vals);
|
||||||
|
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||||
|
s.push(l);
|
||||||
|
let t = Tick::from_seconds_f64(0.4);
|
||||||
|
let label = format!("{id} {vals:?} {depth:?}");
|
||||||
|
check(&label, compare_at(&s, opts(), t), 0.0);
|
||||||
|
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, t), 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,223 @@
|
||||||
|
//! GPU stylize and distort family effects 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::{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 posterize_thresholds_strobe_vignette() {
|
||||||
|
effect_case("ec.stylize.posterize", &[("level", n(4.0))]);
|
||||||
|
effect_case("ec.stylize.posterize", &[("level", n(9.0))]);
|
||||||
|
effect_case("ec.stylize.threshold", &[("level", n(100.0))]);
|
||||||
|
effect_case("ec.stylize.threshold", &[("level", n(181.0))]);
|
||||||
|
effect_case("ec.stylize.ccthreshold", &[]);
|
||||||
|
effect_case("ec.stylize.ccthreshold", &[("channel", e(1)), ("invert", on()), ("threshold", n(90.0))]);
|
||||||
|
effect_case("ec.stylize.ccthreshold", &[("channel", e(2)), ("blend", n(30.0))]);
|
||||||
|
effect_case("ec.stylize.ccthreshold", &[("channel", e(3)), ("threshold", n(60.0))]);
|
||||||
|
effect_case(
|
||||||
|
"ec.stylize.ccthresholdrgb",
|
||||||
|
&[("redThreshold", n(70.0)), ("greenThreshold", n(150.0)), ("blueThreshold", n(110.0)), ("invertGreen", on()), ("blend", n(25.0))],
|
||||||
|
);
|
||||||
|
effect_case(
|
||||||
|
"ec.stylize.strobe",
|
||||||
|
&[("strobeDuration", n(0.5)), ("strobeOperator", e(1)), ("strobeColor", c(0.2, 0.5, 0.9)), ("blendWithOriginal", n(30.0))],
|
||||||
|
);
|
||||||
|
effect_case("ec.stylize.strobe", &[("strobeDuration", n(0.5)), ("strobeOperator", e(4)), ("strobeColor", c(0.6, 0.1, 0.3))]);
|
||||||
|
effect_case("ec.stylize.strobe", &[("strobeDuration", n(0.5)), ("strobe", e(1)), ("blendWithOriginal", n(40.0))]);
|
||||||
|
effect_case("ec.stylize.strobe", &[("strobeDuration", n(0.1)), ("strobePeriod", n(0.2)), ("randomStrobeProbability", n(50.0))]);
|
||||||
|
effect_case("ec.stylize.ccvignette", &[("amount", n(80.0))]);
|
||||||
|
effect_case("ec.stylize.ccvignette", &[("amount", n(-60.0)), ("pinHighlights", n(50.0)), ("angleOfView", n(120.0)), ("center", pt(20.0, 15.0))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scatter_brush_strokes() {
|
||||||
|
effect_case("ec.stylize.scatter", &[("amount", n(5.0))]);
|
||||||
|
effect_case("ec.stylize.scatter", &[("amount", n(3.0)), ("grain", e(1))]);
|
||||||
|
effect_case("ec.stylize.scatter", &[("amount", n(7.0)), ("grain", e(2)), ("randomizeEveryFrame", on())]);
|
||||||
|
effect_case("ec.stylize.brushstrokes", &[]);
|
||||||
|
effect_case(
|
||||||
|
"ec.stylize.brushstrokes",
|
||||||
|
&[("strokeAngle", n(40.0)), ("brushSize", n(3.0)), ("strokeLength", n(10.0)), ("paintSurface", e(2)), ("blendWithOriginal", n(20.0))],
|
||||||
|
);
|
||||||
|
effect_case("ec.stylize.brushstrokes", &[("paintSurface", e(1)), ("strokeDensity", n(0.6)), ("strokeRandomness", n(2.0)), ("brushSize", n(1.3))]);
|
||||||
|
effect_case("ec.stylize.brushstrokes", &[("paintSurface", e(3)), ("strokeLength", n(25.0))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roughen_edges() {
|
||||||
|
effect_case("ec.stylize.roughenedges", &[]);
|
||||||
|
effect_case("ec.stylize.roughenedges", &[("edgeType", e(1)), ("border", n(4.0)), ("edgeColor", c(0.1, 0.6, 0.9))]);
|
||||||
|
effect_case(
|
||||||
|
"ec.stylize.roughenedges",
|
||||||
|
&[
|
||||||
|
("edgeType", e(5)),
|
||||||
|
("evolutionOptions/cycleEvolution", on()),
|
||||||
|
("evolutionOptions/cycle", n(2.0)),
|
||||||
|
("evolution", n(500.0)),
|
||||||
|
("stretch", n(2.0)),
|
||||||
|
("complexity", n(3.0)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
effect_case("ec.stylize.roughenedges", &[("edgeType", e(7)), ("border", n(0.0)), ("scale", n(60.0)), ("evolutionOptions/randomSeed", n(9.0))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tiles_kaleida_repetile() {
|
||||||
|
effect_case("ec.stylize.motiontile", &[("tileWidth", n(50.0)), ("tileHeight", n(40.0))]);
|
||||||
|
effect_case(
|
||||||
|
"ec.stylize.motiontile",
|
||||||
|
&[("outputWidth", n(150.0)), ("outputHeight", n(130.0)), ("mirrorEdges", on()), ("phase", n(70.0)), ("tileWidth", n(45.0))],
|
||||||
|
);
|
||||||
|
effect_case(
|
||||||
|
"ec.stylize.motiontile",
|
||||||
|
&[("horizontalPhaseShift", on()), ("phase", n(33.0)), ("tileCenter", pt(30.0, 17.0)), ("tileHeight", n(55.0)), ("outputWidth", n(80.0))],
|
||||||
|
);
|
||||||
|
effect_case("ec.stylize.cckaleida", &[]);
|
||||||
|
effect_case("ec.stylize.cckaleida", &[("mirroring", e(2)), ("rotation", n(20.0)), ("size", n(150.0))]);
|
||||||
|
effect_case("ec.stylize.ccrepetile", &[("expandRight", n(10.0)), ("expandUp", n(4.0))]);
|
||||||
|
effect_case("ec.stylize.ccrepetile", &[("expandLeft", n(12.0)), ("expandDown", n(9.0)), ("tiling", e(1))]);
|
||||||
|
effect_case("ec.stylize.ccrepetile", &[("expandLeft", n(30.0)), ("expandRight", n(30.0)), ("expandUp", n(20.0)), ("tiling", e(4))]);
|
||||||
|
effect_case("ec.distort.cctiler", &[]);
|
||||||
|
effect_case("ec.distort.cctiler", &[("scale", n(70.0)), ("blendWithOriginal", n(30.0)), ("center", pt(30.0, 18.0))]);
|
||||||
|
effect_case("ec.distort.ccgriddler", &[("rotation", n(20.0))]);
|
||||||
|
effect_case("ec.distort.ccgriddler", &[("cutTiles", on()), ("horizontalScale", n(120.0)), ("verticalScale", n(80.0)), ("tileSize", n(17.0))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mirror_offset_polar_spherize_corner_pin() {
|
||||||
|
effect_case("ec.distort.mirror", &[("angle", n(30.0))]);
|
||||||
|
effect_case("ec.distort.mirror", &[("angle", n(200.0)), ("center", pt(27.0, 19.0))]);
|
||||||
|
effect_case("ec.distort.offset", &[("shift", pt(10.0, 5.0))]);
|
||||||
|
effect_case("ec.distort.offset", &[("shift", pt(50.3, 20.7)), ("blend", n(40.0))]);
|
||||||
|
effect_case("ec.distort.polar", &[("interpolation", n(100.0))]);
|
||||||
|
effect_case("ec.distort.polar", &[("interpolation", n(60.0)), ("conversion", e(1))]);
|
||||||
|
effect_case("ec.distort.spherize", &[("radius", n(20.0))]);
|
||||||
|
effect_case("ec.distort.spherize", &[("radius", n(35.0)), ("center", pt(30.0, 25.0))]);
|
||||||
|
effect_case("ec.distort.cornerpin", &[("ul", pt(5.0, 3.0)), ("ur", pt(60.0, 8.0)), ("lr", pt(66.0, 40.0)), ("ll", pt(2.0, 44.0))]);
|
||||||
|
effect_case("ec.distort.cornerpin", &[("ul", pt(-8.0, 6.0)), ("ur", pt(75.0, -4.0)), ("lr", pt(50.0, 30.0)), ("ll", pt(12.0, 50.0))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn optics_magnify() {
|
||||||
|
effect_case("ec.distort.opticscompensation", &[("fieldOfView", n(60.0))]);
|
||||||
|
effect_case("ec.distort.opticscompensation", &[("fieldOfView", n(90.0)), ("reverseLensDistortion", on()), ("fovOrientation", e(2))]);
|
||||||
|
effect_case(
|
||||||
|
"ec.distort.opticscompensation",
|
||||||
|
&[("fieldOfView", n(110.0)), ("reverseLensDistortion", on()), ("resize", e(1)), ("viewCenter", pt(30.0, 20.0))],
|
||||||
|
);
|
||||||
|
effect_case("ec.distort.opticscompensation", &[("fieldOfView", n(120.0)), ("optimalPixels", on()), ("fovOrientation", e(1))]);
|
||||||
|
effect_case("ec.distort.magnify", &[("size", n(15.0))]);
|
||||||
|
effect_case(
|
||||||
|
"ec.distort.magnify",
|
||||||
|
&[("shape", e(1)), ("feather", n(6.0)), ("size", n(18.0)), ("scaling", e(1)), ("blendingMode", e(3)), ("magnification", n(270.0))],
|
||||||
|
);
|
||||||
|
effect_case("ec.distort.magnify", &[("scaling", e(2)), ("blendingMode", e(0)), ("size", n(20.0)), ("opacity", n(70.0))]);
|
||||||
|
effect_case("ec.distort.magnify", &[("size", n(25.0)), ("center", pt(60.0, 10.0)), ("resizeLayer", on()), ("blendingMode", e(5))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cc_slant_smear_split() {
|
||||||
|
effect_case("ec.distort.ccslant", &[("slant", n(30.0))]);
|
||||||
|
effect_case("ec.distort.ccslant", &[("slant", n(-20.0)), ("stretching", on()), ("height", n(120.0))]);
|
||||||
|
effect_case("ec.distort.ccsmear", &[]);
|
||||||
|
effect_case("ec.distort.ccsmear", &[("reach", n(-40.0)), ("radius", n(20.0)), ("to", pt(40.0, 10.0))]);
|
||||||
|
effect_case("ec.distort.ccsplit", &[]);
|
||||||
|
effect_case("ec.distort.ccsplit2", &[("split1", n(30.0)), ("split2", n(80.0)), ("pointB", pt(60.0, 30.0))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn twirl_legacy_ripple_pulse_power_pin() {
|
||||||
|
effect_case("ec.distort.twirllegacy", &[("angle", n(90.0))]);
|
||||||
|
effect_case("ec.distort.twirllegacy", &[("angle", n(-250.0)), ("radius", n(80.0)), ("center", pt(30.0, 18.0))]);
|
||||||
|
effect_case("ec.distort.ccripplepulse", &[("pulseLevel", n(80.0)), ("timeSpan", n(0.5))]);
|
||||||
|
effect_case("ec.distort.ccripplepulse", &[("pulseLevel", n(-60.0)), ("amplitude", n(150.0)), ("renderBump", on()), ("center", pt(25.0, 20.0))]);
|
||||||
|
effect_case("ec.distort.ccpowerpin", &[("topLeft", pt(5.0, 3.0)), ("bottomRight", pt(62.0, 38.0))]);
|
||||||
|
effect_case("ec.distort.ccpowerpin", &[("topRight", pt(80.0, -6.0)), ("bottomLeft", pt(-4.0, 50.0)), ("expandLeft", n(20.0)), ("expandBottom", n(-10.0))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flo_motion() {
|
||||||
|
effect_case("ec.distort.ccflomotion", &[]);
|
||||||
|
effect_case("ec.distort.ccflomotion", &[("antialiasing", e(2)), ("tileEdges", on()), ("amount1", n(-150.0)), ("falloff", n(0.7))]);
|
||||||
|
effect_case("ec.distort.ccflomotion", &[("antialiasing", e(0)), ("amount2", n(0.0)), ("knot1", pt(20.0, 30.0))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn liquify_render() {
|
||||||
|
let mesh = "warp 20 50 0 0 0 10,10 20,15 30,20\ntwirlClockwise 24 80 0 0 0 40,25 42,27";
|
||||||
|
effect_case("ec.distort.liquify", &[("distortionMesh", Value::Str(mesh.into()))]);
|
||||||
|
effect_case(
|
||||||
|
"ec.distort.liquify",
|
||||||
|
&[("distortionMesh", Value::Str(mesh.into())), ("distortionPercentage", n(60.0)), ("distortionMeshOffset", pt(3.0, -2.0))],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn glow_operations_and_arbitrary_map() {
|
||||||
|
effect_case("ec.stylize.glow", &[("glowOperation", e(2)), ("threshold", n(30.0)), ("radius", n(8.0))]);
|
||||||
|
effect_case("ec.stylize.glow", &[("glowOperation", e(3)), ("colors", e(1)), ("intensity", n(2.0))]);
|
||||||
|
effect_case("ec.stylize.glow", &[("colors", e(2)), ("arbitraryMap", Value::Str("0,0 0.5,0.8 1,1|0,0.2 1,0.6|".into())), ("threshold", n(20.0))]);
|
||||||
|
effect_case("ec.stylize.glow", &[("colors", e(2)), ("arbitraryMap", Value::Str("0,1 1,0||0,0 1,1".into())), ("operation", e(1)), ("glowOperation", e(4))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Texturize with a texture layer (Tile / Center / Stretch), composited.
|
||||||
|
#[test]
|
||||||
|
fn texturize_layer() {
|
||||||
|
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||||
|
for placement in 0..3 {
|
||||||
|
let mut s = Scene::new(depth);
|
||||||
|
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
||||||
|
s.push(bg);
|
||||||
|
let mut tex = s.footage(50, 36);
|
||||||
|
tex.switches.video = false;
|
||||||
|
let tid = s.push(tex);
|
||||||
|
let mut l = s.footage(70, 44);
|
||||||
|
s.effect(
|
||||||
|
&mut l,
|
||||||
|
"ec.stylize.texturize",
|
||||||
|
&[("textureLayer", Value::Layer(Some(tid.0))), ("texturePlacement", e(placement)), ("textureContrast", n(1.5)), ("lightDirection", n(60.0))],
|
||||||
|
);
|
||||||
|
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||||
|
s.push(l);
|
||||||
|
let label = format!("texturize {placement} {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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transform's own shutter angle: the effect's rotation and position animate.
|
||||||
|
#[test]
|
||||||
|
fn transform_motion_blur() {
|
||||||
|
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);
|
||||||
|
s.effect(&mut l, "ec.distort.transform", &[("useCompositionShutterAngle", Value::Bool(false)), ("shutterAngle", n(180.0))]);
|
||||||
|
let rot = l.props.prop_mut("effects/#1/rotation").expect("transform rotation");
|
||||||
|
rot.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(0.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(120.0))];
|
||||||
|
let pos = l.props.prop_mut("effects/#1/position").expect("transform position");
|
||||||
|
pos.keys = vec![Keyframe::new(Tick::ZERO, pt(35.0, 22.0)), Keyframe::new(Tick::from_seconds_f64(1.0), pt(95.0, 40.0))];
|
||||||
|
s.push(l);
|
||||||
|
let label = format!("transform motion blur {depth:?}");
|
||||||
|
check(&label, compare_at(&s, opts(), Tick::from_seconds_f64(0.3)), 0.0);
|
||||||
|
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::from_seconds_f64(0.3)), 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,185 @@
|
||||||
|
//! GPU colour-correction family effects 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_color::Label;
|
||||||
|
use effectcraft_keyframe::{Keyframe, Value};
|
||||||
|
use effectcraft_project::{BitDepth, Comp, ItemKind, LayerSource, build};
|
||||||
|
use effectcraft_render::RenderOpts;
|
||||||
|
use effectcraft_time::{FrameRate, 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 off() -> Value {
|
||||||
|
Value::Bool(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn levels_and_gamma() {
|
||||||
|
effect_case(
|
||||||
|
"ec.color.levelsic",
|
||||||
|
&[("rgb/rgbInBlack", n(0.1)), ("rgb/rgbGamma", n(1.3)), ("red/redOutWhite", n(0.8)), ("blue/blueInWhite", n(0.7)), ("alpha/alphaGamma", n(0.7))],
|
||||||
|
);
|
||||||
|
effect_case("ec.color.levelsic", &[("green/greenInBlack", n(0.2)), ("green/greenInWhite", n(0.6)), ("clipToOutputWhite", e(0))]);
|
||||||
|
effect_case("ec.color.levelsic", &[("alpha/alphaOutBlack", n(0.3))]);
|
||||||
|
effect_case("ec.color.levels", &[("redInBlack", n(0.1)), ("greenGamma", n(1.5)), ("blueOutWhite", n(0.7)), ("inWhite", n(0.9))]);
|
||||||
|
effect_case("ec.color.levels", &[("alphaInWhite", n(0.6)), ("redInWhite", n(0.5)), ("redClipToOutputWhite", e(0))]);
|
||||||
|
effect_case("ec.color.gammapedestalgain", &[("redGamma", n(1.4)), ("greenPedestal", n(0.1)), ("blueGain", n(0.8))]);
|
||||||
|
effect_case("ec.color.gammapedestalgain", &[("blackStretch", n(2.0)), ("redGain", n(1.2))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hue_saturation_ranges() {
|
||||||
|
effect_case("ec.color.huesaturation", &[("redsHue", n(40.0)), ("bluesSaturation", n(-60.0)), ("greensLightness", n(30.0))]);
|
||||||
|
effect_case(
|
||||||
|
"ec.color.huesaturation",
|
||||||
|
&[
|
||||||
|
("hue", n(20.0)),
|
||||||
|
("saturation", n(15.0)),
|
||||||
|
("yellowsSaturation", n(50.0)),
|
||||||
|
("yellowsRangeStart", n(30.0)),
|
||||||
|
("yellowsStartFalloff", n(8.0)),
|
||||||
|
("cyansLightness", n(-40.0)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn colour_replacement() {
|
||||||
|
effect_case("ec.color.photofilter", &[]);
|
||||||
|
effect_case(
|
||||||
|
"ec.color.photofilter",
|
||||||
|
&[("filter", e(effectcraft_effects::PHOTO_FILTER_CUSTOM)), ("color", c(0.2, 0.7, 0.4)), ("density", n(60.0)), ("preserveLuminosity", off())],
|
||||||
|
);
|
||||||
|
effect_case("ec.color.changetocolor", &[("from", c(0.8, 0.4, 0.3)), ("to", c(0.2, 0.5, 0.9)), ("toleranceGroup/hue", n(15.0))]);
|
||||||
|
effect_case(
|
||||||
|
"ec.color.changetocolor",
|
||||||
|
&[("from", c(0.6, 0.6, 0.3)), ("to", c(0.9, 0.2, 0.6)), ("change", e(3)), ("changeBy", e(1)), ("toleranceGroup/hue", n(25.0)), ("softness", n(30.0))],
|
||||||
|
);
|
||||||
|
effect_case("ec.color.changetocolor", &[("from", c(0.6, 0.6, 0.3)), ("change", e(2)), ("toleranceGroup/hue", n(25.0)), ("viewCorrectionMatte", on())]);
|
||||||
|
effect_case("ec.color.leavecolor", &[("amount", n(80.0)), ("color", c(0.7, 0.5, 0.4)), ("tolerance", n(10.0)), ("edgeSoftness", n(20.0))]);
|
||||||
|
effect_case(
|
||||||
|
"ec.color.leavecolor",
|
||||||
|
&[("amount", n(100.0)), ("color", c(0.8, 0.4, 0.3)), ("matchColors", e(1)), ("tolerance", n(5.0)), ("edgeSoftness", n(30.0))],
|
||||||
|
);
|
||||||
|
effect_case("ec.color.changecolor", &[("hueTransform", n(90.0)), ("colorToChange", c(0.7, 0.5, 0.4)), ("matchingSoftness", n(20.0))]);
|
||||||
|
effect_case(
|
||||||
|
"ec.color.changecolor",
|
||||||
|
&[
|
||||||
|
("lightnessTransform", n(-30.0)),
|
||||||
|
("saturationTransform", n(40.0)),
|
||||||
|
("matchColors", e(0)),
|
||||||
|
("matchingTolerance", n(20.0)),
|
||||||
|
("matchingSoftness", n(15.0)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
effect_case("ec.color.changecolor", &[("view", e(1)), ("matchColors", e(2)), ("matchingSoftness", n(25.0)), ("invertColorCorrectionMask", on())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn broadcast_balance_limiter() {
|
||||||
|
effect_case("ec.color.broadcast", &[("maxSignal", n(95.0))]);
|
||||||
|
effect_case("ec.color.broadcast", &[("locale", e(1)), ("howToMakeSafe", e(1)), ("maxSignal", n(90.0))]);
|
||||||
|
effect_case("ec.color.colorbalancehls", &[("hue", n(40.0)), ("lightness", n(-20.0)), ("saturation", n(30.0))]);
|
||||||
|
effect_case("ec.color.colorbalancehls", &[("lightness", n(25.0)), ("saturation", n(-50.0))]);
|
||||||
|
effect_case("ec.color.videolimiter", &[("clipLevel", e(0))]);
|
||||||
|
effect_case("ec.color.videolimiter", &[("clipLevel", e(1)), ("clipMethod", e(1)), ("compressionBeforeClipping", e(4))]);
|
||||||
|
effect_case("ec.color.videolimiter", &[("clipLevel", e(0)), ("clipMethod", e(0)), ("gamutWarning", on())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cc_colour_and_arbitrary_map() {
|
||||||
|
effect_case("ec.color.cctoner", &[]);
|
||||||
|
effect_case("ec.color.cctoner", &[("tones", e(2)), ("blend", n(30.0))]);
|
||||||
|
effect_case("ec.color.cctoner", &[("tones", e(0)), ("shadows", c(0.1, 0.0, 0.3)), ("highlights", c(1.0, 0.9, 0.6))]);
|
||||||
|
effect_case("ec.color.cccoloroffset", &[("redPhase", n(90.0)), ("bluePhase", n(200.0))]);
|
||||||
|
effect_case("ec.color.cccoloroffset", &[("greenPhase", n(120.0)), ("overflow", e(1))]);
|
||||||
|
effect_case("ec.color.cckernel", &[("k1", n(-1.0)), ("k5", n(5.0)), ("k9", n(-1.0)), ("scale", n(0.5)), ("offset", n(0.05))]);
|
||||||
|
effect_case("ec.color.cckernel", &[("k2", n(1.0)), ("k5", n(0.0)), ("k8", n(-1.0)), ("absolute", on())]);
|
||||||
|
let map: String = (0..256).map(|i| format!("{}", ((i as f64 / 255.0).powf(0.6) * 255.0).round())).collect::<Vec<_>>().join(",");
|
||||||
|
effect_case("ec.color.psarbitrarymap", &[("map", Value::Str(map.clone()))]);
|
||||||
|
effect_case("ec.color.psarbitrarymap", &[("map", Value::Str(map)), ("phase", n(40.0)), ("applyPhaseMapToAlpha", on())]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn automatic_corrections() {
|
||||||
|
effect_case("ec.color.autolevels", &[]);
|
||||||
|
effect_case("ec.color.autolevels", &[("blackClip", n(2.0)), ("whiteClip", n(3.0)), ("blend", n(20.0))]);
|
||||||
|
effect_case("ec.color.autocontrast", &[("blackClip", n(1.0))]);
|
||||||
|
effect_case("ec.color.autocolor", &[]);
|
||||||
|
effect_case("ec.color.autocolor", &[("snapNeutralMidtones", on()), ("whiteClip", n(2.0))]);
|
||||||
|
effect_case("ec.color.equalize", &[]);
|
||||||
|
effect_case("ec.color.equalize", &[("style", e(1)), ("amount", n(70.0))]);
|
||||||
|
effect_case("ec.color.equalize", &[("style", e(2))]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shadow_highlight_neutralizer_invert() {
|
||||||
|
effect_case("ec.color.shadowhighlight", &[]);
|
||||||
|
effect_case(
|
||||||
|
"ec.color.shadowhighlight",
|
||||||
|
&[
|
||||||
|
("autoAmounts", off()),
|
||||||
|
("shadowAmount", n(70.0)),
|
||||||
|
("highlightAmount", n(40.0)),
|
||||||
|
("moreOptions/shadowRadius", n(8.0)),
|
||||||
|
("moreOptions/highlightRadius", n(15.0)),
|
||||||
|
("moreOptions/midtoneContrast", n(30.0)),
|
||||||
|
("moreOptions/blackClip", n(2.0)),
|
||||||
|
("blend", n(20.0)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
effect_case("ec.color.shadowhighlight", &[("moreOptions/blackClip", n(0.0)), ("moreOptions/whiteClip", n(0.0)), ("moreOptions/colorCorrection", n(-40.0))]);
|
||||||
|
effect_case(
|
||||||
|
"ec.color.cccolorneutralizer",
|
||||||
|
&[("shadowsBalance", c(0.1, 0.0, 0.05)), ("midtonesUnbalance", c(0.6, 0.5, 0.4)), ("pinning", n(40.0)), ("contrast", n(20.0)), ("darks", n(-30.0))],
|
||||||
|
);
|
||||||
|
effect_case("ec.color.cccolorneutralizer", &[("highlightsBalance", c(0.9, 1.0, 0.8)), ("brights", n(40.0)), ("blendWOriginal", n(25.0))]);
|
||||||
|
for ch in 4..=11 {
|
||||||
|
effect_case("ec.channel.invert", &[("channel", e(ch)), ("blend", n(20.0))]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Color Stabilizer reads its reference frame through the effect host: a precomp whose content
|
||||||
|
/// moves, at a nonzero time (the direct cases have no host and pass the layer through).
|
||||||
|
#[test]
|
||||||
|
fn color_stabilizer() {
|
||||||
|
effect_case("ec.color.colorstabilizer", &[]);
|
||||||
|
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
||||||
|
for mode in [0, 1, 2] {
|
||||||
|
let mut s = Scene::new(depth);
|
||||||
|
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
||||||
|
s.push(bg);
|
||||||
|
let inner = Comp::new(60, 40, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||||
|
let inner_id = s.p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.into()));
|
||||||
|
let mut a = s.footage(50, 34);
|
||||||
|
let pos = a.props.prop_mut("transform/position").unwrap();
|
||||||
|
pos.keys = vec![Keyframe::new(Tick::ZERO, crate::tests::v3(22.0, 18.0)), Keyframe::new(Tick::from_seconds_f64(1.0), crate::tests::v3(38.0, 24.0))];
|
||||||
|
let op = a.props.prop_mut("transform/opacity").unwrap();
|
||||||
|
op.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(100.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(40.0))];
|
||||||
|
s.p.comp_mut(inner_id).unwrap().layers = vec![a];
|
||||||
|
let mut l = build::layer(&mut s.p, &s.comp, "Inner", LayerSource::Comp { item: inner_id }, (60, 40), None);
|
||||||
|
let vals = [
|
||||||
|
("stabilize", e(mode)),
|
||||||
|
("blackPoint", Value::Vec2([18.0, 20.0])),
|
||||||
|
("midPoint", Value::Vec2([30.0, 18.0])),
|
||||||
|
("whitePoint", Value::Vec2([42.0, 22.0])),
|
||||||
|
("sampleSize", n(4.0)),
|
||||||
|
];
|
||||||
|
s.effect(&mut l, "ec.color.colorstabilizer", &vals);
|
||||||
|
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||||
|
s.push(l);
|
||||||
|
let t = Tick::from_seconds_f64(0.5);
|
||||||
|
let label = format!("color stabilizer {mode} {depth:?}");
|
||||||
|
check(&label, compare_at(&s, opts(), t), 0.0);
|
||||||
|
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, t), 0.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -174,10 +174,15 @@ against the caster planes (Shadow Diffusion, Light Transmission) and blending. A
|
||||||
run their effect stacks on the GPU-resident comp (`Renderer::run_effects_on` with an `FxTarget`):
|
run their effect stacks on the GPU-resident comp (`Renderer::run_effects_on` with an `FxTarget`):
|
||||||
runs of GPU effects stay on the device and only non-GPU effects read back and upload. Advanced 3D
|
runs of GPU effects stay on the device and only non-GPU effects read back and upload. Advanced 3D
|
||||||
compositing and wireframes still run on the CPU between GPU steps (read back, draw, upload). GPU
|
compositing and wireframes still run on the CPU between GPU steps (read back, draw, upload). GPU
|
||||||
effects (`effects::GPU_EFFECTS`, 58 of them: blurs, colour, keying incl. Key Light, distortion,
|
effects (`effects::GPU_EFFECTS`, 152 of them: blurs, colour correction, keying incl. Key Light,
|
||||||
transitions, generators, noise and grain; see [effects.md](effects.md)) repeat the CPU effect's
|
mattes, channel, stylize, distortion, transitions, generators, noise, grain and time; see
|
||||||
steps (padding, box radii, parameters, hashes) as kernels; consecutive GPU effects run as one
|
[effects.md](effects.md)) repeat the CPU effect's steps (padding, box radii, parameters, hashes)
|
||||||
chain with one upload and one readback. Tests render scenes on both paths and compare them
|
as kernels, in one module per family (`gpu::fx_*` with `shaders/fx_*.wgsl`); consecutive GPU
|
||||||
|
effects run as one chain with one upload and one readback. Statistics that need the whole frame
|
||||||
|
(Auto Levels / Contrast / Color, Equalize, Shadow/Highlight, Color Stabilizer, Remove Grain's
|
||||||
|
noise level) are measured on the CPU from one readback and applied on the GPU; effects reading
|
||||||
|
other frames (Echo, Posterize Time) upload the frames the host renders. Settings a kernel cannot
|
||||||
|
match render on the CPU (`effects::catalog::gpu_supported`). Tests render scenes on both paths and compare them
|
||||||
(≤ 1/255 at 8 bpc, ≤ 1e-3 at 32 bpc); they skip without an adapter. The desktop viewer builds the
|
(≤ 1/255 at 8 bpc, ≤ 1e-3 at 32 bpc); they skip without an adapter. The desktop viewer builds the
|
||||||
`Gpu` on egui-wgpu's device and shows frames from GPU textures without reading them back
|
`Gpu` on egui-wgpu's device and shows frames from GPU textures without reading them back
|
||||||
(`ui-egui::frames`); headless renders, the CLI (unless `--gpu`) and CI use the CPU. On the web
|
(`ui-egui::frames`); headless renders, the CLI (unless `--gpu`) and CI use the CPU. On the web
|
||||||
|
|
|
||||||
188
docs/effects.md
188
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.
|
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**: 59 effects also run on the GPU compositor with identical results.
|
- **GPU**: 152 effects also run on the GPU compositor with identical results.
|
||||||
- **32**: 306 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
- **32**: 306 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||||
- **Status**: 306 implemented in full, 0 partial (what is missing is listed).
|
- **Status**: 306 implemented in full, 0 partial (what is missing is listed).
|
||||||
|
|
||||||
|
|
@ -46,85 +46,85 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||||
|
|
||||||
| Effect | Id | Params | GPU | 32 | Status |
|
| Effect | Id | Params | GPU | 32 | Status |
|
||||||
|---|---|---:|:-:|:-:|---|
|
|---|---|---:|:-:|:-:|---|
|
||||||
| Bilateral Blur | `ec.blur.bilateral` | 3 | | 32 | Implemented |
|
| Bilateral Blur | `ec.blur.bilateral` | 3 | GPU | 32 | Implemented |
|
||||||
| CC Cross Blur | `ec.blur.cccross` | 3 | | 32 | Implemented |
|
| CC Cross Blur | `ec.blur.cccross` | 3 | | 32 | Implemented |
|
||||||
| CC Radial Blur | `ec.blur.ccradial` | 4 | | 32 | Implemented |
|
| CC Radial Blur | `ec.blur.ccradial` | 4 | | 32 | Implemented |
|
||||||
| CC Radial Fast Blur | `ec.blur.ccradialfast` | 3 | 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 | | 32 | Implemented |
|
||||||
| Camera Lens Blur | `ec.blur.cameralens` | 16 | GPU | 32 | Implemented |
|
| Camera Lens Blur | `ec.blur.cameralens` | 16 | GPU | 32 | Implemented |
|
||||||
| Camera-Shake Deblur | `ec.blur.camerashakedeblur` | 8 | | 32 | Implemented |
|
| Camera-Shake Deblur | `ec.blur.camerashakedeblur` | 8 | | 32 | Implemented |
|
||||||
| Channel Blur | `ec.blur.channel` | 6 | | 32 | Implemented |
|
| Channel Blur | `ec.blur.channel` | 6 | GPU | 32 | Implemented |
|
||||||
| Compound Blur | `ec.blur.compound` | 4 | | 32 | Implemented |
|
| Compound Blur | `ec.blur.compound` | 4 | GPU | 32 | Implemented |
|
||||||
| Directional Blur | `ec.blur.directional` | 2 | GPU | 32 | Implemented |
|
| Directional Blur | `ec.blur.directional` | 2 | GPU | 32 | Implemented |
|
||||||
| Fast Box Blur | `ec.blur.fastbox` | 4 | GPU | 32 | Implemented |
|
| Fast Box Blur | `ec.blur.fastbox` | 4 | GPU | 32 | Implemented |
|
||||||
| Gaussian Blur | `ec.blur.gaussian` | 3 | GPU | 32 | Implemented |
|
| Gaussian Blur | `ec.blur.gaussian` | 3 | GPU | 32 | Implemented |
|
||||||
| Radial Blur | `ec.blur.radial` | 4 | GPU | 32 | Implemented |
|
| Radial Blur | `ec.blur.radial` | 4 | GPU | 32 | Implemented |
|
||||||
| Sharpen | `ec.blur.sharpen` | 1 | | 32 | Implemented |
|
| Sharpen | `ec.blur.sharpen` | 1 | GPU | 32 | Implemented |
|
||||||
| Smart Blur | `ec.blur.smart` | 3 | | 32 | Implemented |
|
| Smart Blur | `ec.blur.smart` | 3 | GPU | 32 | Implemented |
|
||||||
| Unsharp Mask | `ec.blur.unsharp` | 3 | | 32 | Implemented |
|
| Unsharp Mask | `ec.blur.unsharp` | 3 | GPU | 32 | Implemented |
|
||||||
|
|
||||||
## Channel
|
## Channel
|
||||||
|
|
||||||
| Effect | Id | Params | GPU | 32 | Status |
|
| Effect | Id | Params | GPU | 32 | Status |
|
||||||
|---|---|---:|:-:|:-:|---|
|
|---|---|---:|:-:|:-:|---|
|
||||||
| Arithmetic | `ec.channel.arithmetic` | 5 | | 32 | Implemented |
|
| Arithmetic | `ec.channel.arithmetic` | 5 | GPU | 32 | Implemented |
|
||||||
| Blend | `ec.channel.blend` | 4 | | 32 | Implemented |
|
| Blend | `ec.channel.blend` | 4 | GPU | 32 | Implemented |
|
||||||
| CC Composite | `ec.channel.cccomposite` | 3 | | 32 | Implemented |
|
| CC Composite | `ec.channel.cccomposite` | 3 | | 32 | Implemented |
|
||||||
| Calculations | `ec.channel.calculations` | 9 | | 32 | Implemented |
|
| Calculations | `ec.channel.calculations` | 9 | GPU | 32 | Implemented |
|
||||||
| Channel Combiner | `ec.channel.combiner` | 6 | | 32 | Implemented |
|
| Channel Combiner | `ec.channel.combiner` | 6 | GPU | 32 | Implemented |
|
||||||
| Compound Arithmetic | `ec.channel.compoundarithmetic` | 6 | | 32 | Implemented |
|
| Compound Arithmetic | `ec.channel.compoundarithmetic` | 6 | GPU | 32 | Implemented |
|
||||||
| Invert | `ec.channel.invert` | 2 | GPU | 32 | Implemented |
|
| Invert | `ec.channel.invert` | 2 | GPU | 32 | Implemented |
|
||||||
| Minimax | `ec.channel.minimax` | 5 | | 32 | Implemented |
|
| Minimax | `ec.channel.minimax` | 5 | GPU | 32 | Implemented |
|
||||||
| Remove Color Matting | `ec.channel.removecolormatting` | 2 | | 32 | Implemented |
|
| Remove Color Matting | `ec.channel.removecolormatting` | 2 | GPU | 32 | Implemented |
|
||||||
| Set Channels | `ec.channel.setchannels` | 9 | | 32 | Implemented |
|
| Set Channels | `ec.channel.setchannels` | 9 | GPU | 32 | Implemented |
|
||||||
| Set Matte | `ec.channel.setmatte` | 6 | | 32 | Implemented |
|
| Set Matte | `ec.channel.setmatte` | 6 | GPU | 32 | Implemented |
|
||||||
| Shift Channels | `ec.channel.shiftchannels` | 4 | | 32 | Implemented |
|
| Shift Channels | `ec.channel.shiftchannels` | 4 | GPU | 32 | Implemented |
|
||||||
| Solid Composite | `ec.channel.solidcomposite` | 4 | | 32 | Implemented |
|
| Solid Composite | `ec.channel.solidcomposite` | 4 | GPU | 32 | Implemented |
|
||||||
|
|
||||||
## Color Correction
|
## Color Correction
|
||||||
|
|
||||||
| Effect | Id | Params | GPU | 32 | Status |
|
| Effect | Id | Params | GPU | 32 | Status |
|
||||||
|---|---|---:|:-:|:-:|---|
|
|---|---|---:|:-:|:-:|---|
|
||||||
| Auto Color | `ec.color.autocolor` | 6 | | 32 | Implemented |
|
| Auto Color | `ec.color.autocolor` | 6 | GPU | 32 | Implemented |
|
||||||
| Auto Contrast | `ec.color.autocontrast` | 5 | | 32 | Implemented |
|
| Auto Contrast | `ec.color.autocontrast` | 5 | GPU | 32 | Implemented |
|
||||||
| Auto Levels | `ec.color.autolevels` | 5 | | 32 | Implemented |
|
| Auto Levels | `ec.color.autolevels` | 5 | GPU | 32 | Implemented |
|
||||||
| Black & White | `ec.color.blackwhite` | 8 | GPU | 32 | Implemented |
|
| Black & White | `ec.color.blackwhite` | 8 | GPU | 32 | Implemented |
|
||||||
| Brightness & Contrast | `ec.color.brightnesscontrast` | 3 | GPU | 32 | Implemented |
|
| Brightness & Contrast | `ec.color.brightnesscontrast` | 3 | GPU | 32 | Implemented |
|
||||||
| Broadcast Colors | `ec.color.broadcast` | 3 | | 32 | Implemented |
|
| Broadcast Colors | `ec.color.broadcast` | 3 | GPU | 32 | Implemented |
|
||||||
| CC Color Neutralizer | `ec.color.cccolorneutralizer` | 11 | | 32 | Implemented |
|
| CC Color Neutralizer | `ec.color.cccolorneutralizer` | 11 | GPU | 32 | Implemented |
|
||||||
| CC Color Offset | `ec.color.cccoloroffset` | 4 | | 32 | Implemented |
|
| CC Color Offset | `ec.color.cccoloroffset` | 4 | GPU | 32 | Implemented |
|
||||||
| CC Kernel | `ec.color.cckernel` | 12 | | 32 | Implemented |
|
| CC Kernel | `ec.color.cckernel` | 12 | GPU | 32 | Implemented |
|
||||||
| CC Toner | `ec.color.cctoner` | 7 | | 32 | Implemented |
|
| CC Toner | `ec.color.cctoner` | 7 | GPU | 32 | Implemented |
|
||||||
| Change Color | `ec.color.changecolor` | 9 | | 32 | Implemented |
|
| Change Color | `ec.color.changecolor` | 9 | GPU | 32 | Implemented |
|
||||||
| Change to Color | `ec.color.changetocolor` | 9 | | 32 | Implemented |
|
| Change to Color | `ec.color.changetocolor` | 9 | GPU | 32 | Implemented |
|
||||||
| Channel Mixer | `ec.color.channelmixer` | 13 | GPU | 32 | Implemented |
|
| Channel Mixer | `ec.color.channelmixer` | 13 | GPU | 32 | Implemented |
|
||||||
| Color Balance | `ec.color.colorbalance` | 10 | GPU | 32 | Implemented |
|
| Color Balance | `ec.color.colorbalance` | 10 | GPU | 32 | Implemented |
|
||||||
| Color Balance (HLS) | `ec.color.colorbalancehls` | 3 | | 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 | | 32 | Implemented |
|
||||||
| Color Stabilizer | `ec.color.colorstabilizer` | 6 | | 32 | Implemented |
|
| Color Stabilizer | `ec.color.colorstabilizer` | 6 | GPU | 32 | Implemented |
|
||||||
| Colorama | `ec.color.colorama` | 20 | GPU | 32 | Implemented |
|
| Colorama | `ec.color.colorama` | 20 | GPU | 32 | Implemented |
|
||||||
| Curves | `ec.color.curves` | 0 | GPU | 32 | Implemented |
|
| Curves | `ec.color.curves` | 0 | GPU | 32 | Implemented |
|
||||||
| Equalize | `ec.color.equalize` | 2 | | 32 | Implemented |
|
| Equalize | `ec.color.equalize` | 2 | GPU | 32 | Implemented |
|
||||||
| Exposure | `ec.color.exposure` | 14 | GPU | 32 | Implemented |
|
| Exposure | `ec.color.exposure` | 14 | GPU | 32 | Implemented |
|
||||||
| Gamma/Pedestal/Gain | `ec.color.gammapedestalgain` | 10 | | 32 | Implemented |
|
| Gamma/Pedestal/Gain | `ec.color.gammapedestalgain` | 10 | GPU | 32 | Implemented |
|
||||||
| Hue/Saturation | `ec.color.huesaturation` | 50 | GPU | 32 | Implemented |
|
| Hue/Saturation | `ec.color.huesaturation` | 50 | GPU | 32 | Implemented |
|
||||||
| Leave Color | `ec.color.leavecolor` | 5 | | 32 | Implemented |
|
| Leave Color | `ec.color.leavecolor` | 5 | GPU | 32 | Implemented |
|
||||||
| Levels | `ec.color.levels` | 36 | GPU | 32 | Implemented |
|
| Levels | `ec.color.levels` | 36 | GPU | 32 | Implemented |
|
||||||
| Levels (Individual Controls) | `ec.color.levelsic` | 27 | | 32 | Implemented |
|
| Levels (Individual Controls) | `ec.color.levelsic` | 27 | GPU | 32 | Implemented |
|
||||||
| Lumetri Color | `ec.color.lumetri` | 66 | GPU | 32 | Implemented |
|
| Lumetri Color | `ec.color.lumetri` | 66 | GPU | 32 | Implemented |
|
||||||
| OCIO CDL Transform | `ec.color.ociocdl` | 12 | | 32 | Implemented |
|
| OCIO CDL Transform | `ec.color.ociocdl` | 12 | | 32 | Implemented |
|
||||||
| OCIO Color Space Transform | `ec.color.ociocolorspace` | 7 | | 32 | Implemented |
|
| OCIO Color Space Transform | `ec.color.ociocolorspace` | 7 | | 32 | Implemented |
|
||||||
| OCIO Display Transform | `ec.color.ociodisplay` | 9 | | 32 | Implemented |
|
| OCIO Display Transform | `ec.color.ociodisplay` | 9 | | 32 | Implemented |
|
||||||
| OCIO File Transform | `ec.color.ociofile` | 4 | | 32 | Implemented |
|
| OCIO File Transform | `ec.color.ociofile` | 4 | | 32 | Implemented |
|
||||||
| OCIO Look Transform | `ec.color.ociolook` | 6 | | 32 | Implemented |
|
| OCIO Look Transform | `ec.color.ociolook` | 6 | | 32 | Implemented |
|
||||||
| PS Arbitrary Map | `ec.color.psarbitrarymap` | 2 | | 32 | Implemented |
|
| PS Arbitrary Map | `ec.color.psarbitrarymap` | 2 | GPU | 32 | Implemented |
|
||||||
| Photo Filter | `ec.color.photofilter` | 4 | | 32 | Implemented |
|
| Photo Filter | `ec.color.photofilter` | 4 | GPU | 32 | Implemented |
|
||||||
| Selective Color | `ec.color.selectivecolor` | 38 | GPU | 32 | Implemented |
|
| Selective Color | `ec.color.selectivecolor` | 38 | GPU | 32 | Implemented |
|
||||||
| Shadow/Highlight | `ec.color.shadowhighlight` | 14 | | 32 | Implemented |
|
| Shadow/Highlight | `ec.color.shadowhighlight` | 14 | GPU | 32 | Implemented |
|
||||||
| Tint | `ec.color.tint` | 3 | GPU | 32 | Implemented |
|
| Tint | `ec.color.tint` | 3 | GPU | 32 | Implemented |
|
||||||
| Tritone | `ec.color.tritone` | 4 | GPU | 32 | Implemented |
|
| Tritone | `ec.color.tritone` | 4 | GPU | 32 | Implemented |
|
||||||
| Vibrance | `ec.color.vibrance` | 2 | GPU | 32 | Implemented |
|
| Vibrance | `ec.color.vibrance` | 2 | GPU | 32 | Implemented |
|
||||||
| Video Limiter | `ec.color.videolimiter` | 5 | | 32 | Implemented |
|
| Video Limiter | `ec.color.videolimiter` | 5 | GPU | 32 | Implemented |
|
||||||
|
|
||||||
## Distort
|
## Distort
|
||||||
|
|
||||||
|
|
@ -135,37 +135,37 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||||
| CC Bend It | `ec.distort.ccbendit` | 4 | | 32 | Implemented |
|
| CC Bend It | `ec.distort.ccbendit` | 4 | | 32 | Implemented |
|
||||||
| CC Bender | `ec.distort.ccbender` | 5 | | 32 | Implemented |
|
| CC Bender | `ec.distort.ccbender` | 5 | | 32 | Implemented |
|
||||||
| CC Blobbylize | `ec.distort.ccblobbylize` | 15 | | 32 | Implemented |
|
| CC Blobbylize | `ec.distort.ccblobbylize` | 15 | | 32 | Implemented |
|
||||||
| CC Flo Motion | `ec.distort.ccflomotion` | 7 | | 32 | Implemented |
|
| CC Flo Motion | `ec.distort.ccflomotion` | 7 | GPU | 32 | Implemented |
|
||||||
| CC Griddler | `ec.distort.ccgriddler` | 5 | | 32 | Implemented |
|
| CC Griddler | `ec.distort.ccgriddler` | 5 | GPU | 32 | Implemented |
|
||||||
| CC Lens | `ec.distort.cclens` | 3 | GPU | 32 | Implemented |
|
| CC Lens | `ec.distort.cclens` | 3 | GPU | 32 | Implemented |
|
||||||
| CC Page Turn | `ec.distort.ccpageturn` | 7 | | 32 | Implemented |
|
| CC Page Turn | `ec.distort.ccpageturn` | 7 | | 32 | Implemented |
|
||||||
| CC Power Pin | `ec.distort.ccpowerpin` | 10 | | 32 | Implemented |
|
| CC Power Pin | `ec.distort.ccpowerpin` | 10 | GPU | 32 | Implemented |
|
||||||
| CC Ripple Pulse | `ec.distort.ccripplepulse` | 5 | | 32 | Implemented |
|
| CC Ripple Pulse | `ec.distort.ccripplepulse` | 5 | GPU | 32 | Implemented |
|
||||||
| CC Slant | `ec.distort.ccslant` | 4 | | 32 | Implemented |
|
| CC Slant | `ec.distort.ccslant` | 4 | GPU | 32 | Implemented |
|
||||||
| CC Smear | `ec.distort.ccsmear` | 4 | | 32 | Implemented |
|
| CC Smear | `ec.distort.ccsmear` | 4 | GPU | 32 | Implemented |
|
||||||
| CC Split | `ec.distort.ccsplit` | 3 | | 32 | Implemented |
|
| CC Split | `ec.distort.ccsplit` | 3 | GPU | 32 | Implemented |
|
||||||
| CC Split 2 | `ec.distort.ccsplit2` | 4 | | 32 | Implemented |
|
| CC Split 2 | `ec.distort.ccsplit2` | 4 | GPU | 32 | Implemented |
|
||||||
| CC Tiler | `ec.distort.cctiler` | 3 | | 32 | Implemented |
|
| CC Tiler | `ec.distort.cctiler` | 3 | GPU | 32 | Implemented |
|
||||||
| Corner Pin | `ec.distort.cornerpin` | 4 | | 32 | Implemented |
|
| Corner Pin | `ec.distort.cornerpin` | 4 | GPU | 32 | Implemented |
|
||||||
| Detail-preserving Upscale | `ec.distort.upscale` | 4 | | 32 | Implemented |
|
| Detail-preserving Upscale | `ec.distort.upscale` | 4 | | 32 | Implemented |
|
||||||
| Displacement Map | `ec.distort.displacementmap` | 8 | GPU | 32 | Implemented |
|
| Displacement Map | `ec.distort.displacementmap` | 8 | GPU | 32 | Implemented |
|
||||||
| Liquify | `ec.distort.liquify` | 12 | | 32 | Implemented |
|
| Liquify | `ec.distort.liquify` | 12 | GPU | 32 | Implemented |
|
||||||
| Magnify | `ec.distort.magnify` | 10 | | 32 | Implemented |
|
| Magnify | `ec.distort.magnify` | 10 | GPU | 32 | Implemented |
|
||||||
| Mesh Warp | `ec.distort.meshwarp` | 3 | GPU | 32 | Implemented |
|
| Mesh Warp | `ec.distort.meshwarp` | 3 | GPU | 32 | Implemented |
|
||||||
| Mirror | `ec.distort.mirror` | 2 | | 32 | Implemented |
|
| Mirror | `ec.distort.mirror` | 2 | GPU | 32 | Implemented |
|
||||||
| Offset | `ec.distort.offset` | 2 | | 32 | Implemented |
|
| Offset | `ec.distort.offset` | 2 | GPU | 32 | Implemented |
|
||||||
| Optics Compensation | `ec.distort.opticscompensation` | 6 | | 32 | Implemented |
|
| Optics Compensation | `ec.distort.opticscompensation` | 6 | GPU | 32 | Implemented |
|
||||||
| Polar Coordinates | `ec.distort.polar` | 2 | | 32 | Implemented |
|
| Polar Coordinates | `ec.distort.polar` | 2 | GPU | 32 | Implemented |
|
||||||
| Puppet | `ec.distort.puppet` | 1 | | 32 | Implemented |
|
| Puppet | `ec.distort.puppet` | 1 | | 32 | Implemented |
|
||||||
| Reshape | `ec.distort.reshape` | 6 | | 32 | Implemented |
|
| Reshape | `ec.distort.reshape` | 6 | | 32 | Implemented |
|
||||||
| Ripple | `ec.distort.ripple` | 7 | GPU | 32 | Implemented |
|
| Ripple | `ec.distort.ripple` | 7 | GPU | 32 | Implemented |
|
||||||
| Rolling Shutter Repair | `ec.distort.rollingshutterrepair` | 5 | | 32 | Implemented |
|
| Rolling Shutter Repair | `ec.distort.rollingshutterrepair` | 5 | | 32 | Implemented |
|
||||||
| Smear | `ec.distort.smear` | 8 | | 32 | Implemented |
|
| Smear | `ec.distort.smear` | 8 | | 32 | Implemented |
|
||||||
| Spherize | `ec.distort.spherize` | 2 | | 32 | Implemented |
|
| Spherize | `ec.distort.spherize` | 2 | GPU | 32 | Implemented |
|
||||||
| Transform | `ec.distort.transform` | 12 | GPU | 32 | Implemented |
|
| Transform | `ec.distort.transform` | 12 | GPU | 32 | Implemented |
|
||||||
| Turbulent Displace | `ec.distort.turbulentdisplace` | 11 | GPU | 32 | Implemented |
|
| Turbulent Displace | `ec.distort.turbulentdisplace` | 11 | GPU | 32 | Implemented |
|
||||||
| Twirl | `ec.distort.twirl` | 3 | GPU | 32 | Implemented |
|
| Twirl | `ec.distort.twirl` | 3 | GPU | 32 | Implemented |
|
||||||
| Twirl (Legacy) | `ec.distort.twirllegacy` | 3 | | 32 | Implemented |
|
| Twirl (Legacy) | `ec.distort.twirllegacy` | 3 | GPU | 32 | Implemented |
|
||||||
| Warp | `ec.distort.warp` | 5 | | 32 | Implemented |
|
| Warp | `ec.distort.warp` | 5 | | 32 | Implemented |
|
||||||
| Warp Stabilizer | `ec.distort.warpstabilizer` | 20 | | 32 | Implemented |
|
| Warp Stabilizer | `ec.distort.warpstabilizer` | 20 | | 32 | Implemented |
|
||||||
| Wave Warp | `ec.distort.wavewarp` | 7 | GPU | 32 | Implemented |
|
| Wave Warp | `ec.distort.wavewarp` | 7 | GPU | 32 | Implemented |
|
||||||
|
|
@ -236,29 +236,29 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||||
|
|
||||||
| Effect | Id | Params | GPU | 32 | Status |
|
| Effect | Id | Params | GPU | 32 | Status |
|
||||||
|---|---|---:|:-:|:-:|---|
|
|---|---|---:|:-:|:-:|---|
|
||||||
| Advanced Spill Suppressor | `ec.key.advancedspill` | 8 | | 32 | Implemented |
|
| 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 | | 32 | Implemented |
|
||||||
| Color Difference Key | `ec.key.colordifference` | 16 | | 32 | Implemented |
|
| Color Difference Key | `ec.key.colordifference` | 16 | GPU | 32 | Implemented |
|
||||||
| Color Range | `ec.key.colorrange` | 8 | | 32 | Implemented |
|
| Color Range | `ec.key.colorrange` | 8 | GPU | 32 | Implemented |
|
||||||
| Difference Matte | `ec.key.differencematte` | 6 | | 32 | Implemented |
|
| Difference Matte | `ec.key.differencematte` | 6 | GPU | 32 | Implemented |
|
||||||
| Extract | `ec.key.extract` | 6 | | 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 | | 32 | Implemented |
|
||||||
| Key Cleaner | `ec.key.keycleaner` | 4 | | 32 | Implemented |
|
| Key Cleaner | `ec.key.keycleaner` | 4 | GPU | 32 | Implemented |
|
||||||
| Key Light | `ec.keying.keylight` | 56 | GPU | 32 | Implemented |
|
| Key Light | `ec.keying.keylight` | 56 | GPU | 32 | Implemented |
|
||||||
| Linear Color Key | `ec.key.linearcolor` | 6 | GPU | 32 | Implemented |
|
| Linear Color Key | `ec.key.linearcolor` | 6 | GPU | 32 | Implemented |
|
||||||
| Screen Key | `ec.key.screen` | 11 | | 32 | Implemented |
|
| Screen Key | `ec.key.screen` | 11 | GPU | 32 | Implemented |
|
||||||
| Unmult | `ec.key.unmult` | 6 | | 32 | Implemented |
|
| Unmult | `ec.key.unmult` | 6 | GPU | 32 | Implemented |
|
||||||
|
|
||||||
## Matte
|
## Matte
|
||||||
|
|
||||||
| Effect | Id | Params | GPU | 32 | Status |
|
| Effect | Id | Params | GPU | 32 | Status |
|
||||||
|---|---|---:|:-:|:-:|---|
|
|---|---|---:|:-:|:-:|---|
|
||||||
| Matte Choker | `ec.matte.mattechoker` | 7 | | 32 | Implemented |
|
| Matte Choker | `ec.matte.mattechoker` | 7 | GPU | 32 | Implemented |
|
||||||
| Mocha shape | `ec.obsolete.mochashape` | 6 | | 32 | Implemented |
|
| Mocha shape | `ec.obsolete.mochashape` | 6 | | 32 | Implemented |
|
||||||
| Refine Hard Matte | `ec.matte.refinehard` | 16 | | 32 | Implemented |
|
| Refine Hard Matte | `ec.matte.refinehard` | 16 | GPU | 32 | Implemented |
|
||||||
| Refine Soft Matte | `ec.matte.refinesoft` | 18 | | 32 | Implemented |
|
| Refine Soft Matte | `ec.matte.refinesoft` | 18 | GPU | 32 | Implemented |
|
||||||
| Roto Brush & Refine Edge | `ec.matte.rotobrush` | 24 | | 32 | Implemented |
|
| Roto Brush & Refine Edge | `ec.matte.rotobrush` | 24 | | 32 | Implemented |
|
||||||
| Simple Choker | `ec.matte.simplechoker` | 2 | | 32 | Implemented |
|
| Simple Choker | `ec.matte.simplechoker` | 2 | GPU | 32 | Implemented |
|
||||||
|
|
||||||
## Noise & Grain
|
## Noise & Grain
|
||||||
|
|
||||||
|
|
@ -266,17 +266,17 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||||
|---|---|---:|:-:|:-:|---|
|
|---|---|---:|:-:|:-:|---|
|
||||||
| Add Grain | `ec.noise.addgrain` | 32 | GPU | 32 | Implemented |
|
| Add Grain | `ec.noise.addgrain` | 32 | GPU | 32 | Implemented |
|
||||||
| Curl Noise | `ec.noise.curlnoise` | 10 | | 32 | Implemented |
|
| Curl Noise | `ec.noise.curlnoise` | 10 | | 32 | Implemented |
|
||||||
| Dust & Scratches | `ec.noise.dustscratches` | 3 | | 32 | Implemented |
|
| Dust & Scratches | `ec.noise.dustscratches` | 3 | GPU | 32 | Implemented |
|
||||||
| Fractal Noise | `ec.noise.fractal` | 26 | GPU | 32 | Implemented |
|
| Fractal Noise | `ec.noise.fractal` | 26 | GPU | 32 | Implemented |
|
||||||
| Match Grain | `ec.noise.matchgrain` | 37 | | 32 | Implemented |
|
| Match Grain | `ec.noise.matchgrain` | 37 | | 32 | Implemented |
|
||||||
| Median | `ec.noise.median` | 2 | | 32 | Implemented |
|
| Median | `ec.noise.median` | 2 | GPU | 32 | Implemented |
|
||||||
| Median (Legacy) | `ec.noise.medianlegacy` | 2 | | 32 | Implemented |
|
| Median (Legacy) | `ec.noise.medianlegacy` | 2 | GPU | 32 | Implemented |
|
||||||
| Noise | `ec.noise.noise` | 3 | GPU | 32 | Implemented |
|
| Noise | `ec.noise.noise` | 3 | GPU | 32 | Implemented |
|
||||||
| Noise Alpha | `ec.noise.noisealpha` | 8 | | 32 | Implemented |
|
| Noise Alpha | `ec.noise.noisealpha` | 8 | GPU | 32 | Implemented |
|
||||||
| Noise HLS | `ec.noise.noisehls` | 6 | | 32 | Implemented |
|
| Noise HLS | `ec.noise.noisehls` | 6 | GPU | 32 | Implemented |
|
||||||
| Noise HLS Auto | `ec.noise.noisehlsauto` | 6 | | 32 | Implemented |
|
| Noise HLS Auto | `ec.noise.noisehlsauto` | 6 | GPU | 32 | Implemented |
|
||||||
| Remove Grain | `ec.noise.removegrain` | 20 | | 32 | Implemented |
|
| Remove Grain | `ec.noise.removegrain` | 20 | GPU | 32 | Implemented |
|
||||||
| Turbulent Noise | `ec.noise.turbulent` | 22 | | 32 | Implemented |
|
| Turbulent Noise | `ec.noise.turbulent` | 22 | GPU | 32 | Implemented |
|
||||||
|
|
||||||
## Obsolete
|
## Obsolete
|
||||||
|
|
||||||
|
|
@ -284,13 +284,13 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||||
|---|---|---:|:-:|:-:|---|
|
|---|---|---:|:-:|:-:|---|
|
||||||
| Basic 3D | `ec.obsolete.basic3d` | 5 | | 32 | Implemented |
|
| Basic 3D | `ec.obsolete.basic3d` | 5 | | 32 | Implemented |
|
||||||
| Basic Text | `ec.obsolete.basictext` | 11 | | 32 | Implemented |
|
| Basic Text | `ec.obsolete.basictext` | 11 | | 32 | Implemented |
|
||||||
| Color Key | `ec.key.colorkey` | 4 | | 32 | Implemented |
|
| Color Key | `ec.key.colorkey` | 4 | GPU | 32 | Implemented |
|
||||||
| Gaussian Blur (Legacy) | `ec.obsolete.gaussianlegacy` | 2 | | 32 | Implemented |
|
| Gaussian Blur (Legacy) | `ec.obsolete.gaussianlegacy` | 2 | | 32 | Implemented |
|
||||||
| Lightning | `ec.obsolete.lightning` | 25 | | 32 | Implemented |
|
| Lightning | `ec.obsolete.lightning` | 25 | | 32 | Implemented |
|
||||||
| Luma Key | `ec.key.luma` | 5 | | 32 | Implemented |
|
| Luma Key | `ec.key.luma` | 5 | GPU | 32 | Implemented |
|
||||||
| Path Text | `ec.obsolete.pathtext` | 32 | | 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 | | 32 | Implemented |
|
||||||
| Spill Suppressor | `ec.key.spill` | 3 | | 32 | Implemented |
|
| Spill Suppressor | `ec.key.spill` | 3 | GPU | 32 | Implemented |
|
||||||
|
|
||||||
## Paint
|
## Paint
|
||||||
|
|
||||||
|
|
@ -340,31 +340,31 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||||
|
|
||||||
| Effect | Id | Params | GPU | 32 | Status |
|
| Effect | Id | Params | GPU | 32 | Status |
|
||||||
|---|---|---:|:-:|:-:|---|
|
|---|---|---:|:-:|:-:|---|
|
||||||
| Brush Strokes | `ec.stylize.brushstrokes` | 7 | | 32 | Implemented |
|
| Brush Strokes | `ec.stylize.brushstrokes` | 7 | GPU | 32 | Implemented |
|
||||||
| CC Block Load | `ec.stylize.ccblockload` | 5 | | 32 | Implemented |
|
| CC Block Load | `ec.stylize.ccblockload` | 5 | | 32 | Implemented |
|
||||||
| CC Burn Film | `ec.stylize.ccburnfilm` | 3 | | 32 | Implemented |
|
| CC Burn Film | `ec.stylize.ccburnfilm` | 3 | | 32 | Implemented |
|
||||||
| CC Glass | `ec.stylize.ccglass` | 17 | | 32 | Implemented |
|
| CC Glass | `ec.stylize.ccglass` | 17 | | 32 | Implemented |
|
||||||
| CC HexTile | `ec.stylize.cchextile` | 5 | | 32 | Implemented |
|
| CC HexTile | `ec.stylize.cchextile` | 5 | | 32 | Implemented |
|
||||||
| CC Kaleida | `ec.stylize.cckaleida` | 4 | | 32 | Implemented |
|
| CC Kaleida | `ec.stylize.cckaleida` | 4 | GPU | 32 | Implemented |
|
||||||
| CC Mr. Smoothie | `ec.stylize.ccmrsmoothie` | 7 | | 32 | Implemented |
|
| CC Mr. Smoothie | `ec.stylize.ccmrsmoothie` | 7 | | 32 | Implemented |
|
||||||
| CC Plastic | `ec.stylize.ccplastic` | 18 | | 32 | Implemented |
|
| CC Plastic | `ec.stylize.ccplastic` | 18 | | 32 | Implemented |
|
||||||
| CC RepeTile | `ec.stylize.ccrepetile` | 5 | | 32 | Implemented |
|
| CC RepeTile | `ec.stylize.ccrepetile` | 5 | GPU | 32 | Implemented |
|
||||||
| CC Threshold | `ec.stylize.ccthreshold` | 4 | | 32 | Implemented |
|
| CC Threshold | `ec.stylize.ccthreshold` | 4 | GPU | 32 | Implemented |
|
||||||
| CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | | 32 | Implemented |
|
| CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | GPU | 32 | Implemented |
|
||||||
| CC Vignette | `ec.stylize.ccvignette` | 4 | | 32 | Implemented |
|
| CC Vignette | `ec.stylize.ccvignette` | 4 | GPU | 32 | Implemented |
|
||||||
| Cartoon | `ec.stylize.cartoon` | 12 | | 32 | Implemented |
|
| Cartoon | `ec.stylize.cartoon` | 12 | | 32 | Implemented |
|
||||||
| Color Emboss | `ec.stylize.coloremboss` | 4 | | 32 | Implemented |
|
| Color Emboss | `ec.stylize.coloremboss` | 4 | | 32 | Implemented |
|
||||||
| Emboss | `ec.stylize.emboss` | 4 | GPU | 32 | Implemented |
|
| Emboss | `ec.stylize.emboss` | 4 | GPU | 32 | Implemented |
|
||||||
| Find Edges | `ec.stylize.findedges` | 2 | GPU | 32 | Implemented |
|
| Find Edges | `ec.stylize.findedges` | 2 | GPU | 32 | Implemented |
|
||||||
| Glow | `ec.stylize.glow` | 14 | GPU | 32 | Implemented |
|
| Glow | `ec.stylize.glow` | 14 | GPU | 32 | Implemented |
|
||||||
| Mosaic | `ec.stylize.mosaic` | 3 | GPU | 32 | Implemented |
|
| Mosaic | `ec.stylize.mosaic` | 3 | GPU | 32 | Implemented |
|
||||||
| Motion Tile | `ec.stylize.motiontile` | 8 | | 32 | Implemented |
|
| Motion Tile | `ec.stylize.motiontile` | 8 | GPU | 32 | Implemented |
|
||||||
| Posterize | `ec.stylize.posterize` | 1 | | 32 | Implemented |
|
| Posterize | `ec.stylize.posterize` | 1 | GPU | 32 | Implemented |
|
||||||
| Roughen Edges | `ec.stylize.roughenedges` | 13 | | 32 | Implemented |
|
| Roughen Edges | `ec.stylize.roughenedges` | 13 | GPU | 32 | Implemented |
|
||||||
| Scatter | `ec.stylize.scatter` | 3 | | 32 | Implemented |
|
| Scatter | `ec.stylize.scatter` | 3 | GPU | 32 | Implemented |
|
||||||
| Strobe Light | `ec.stylize.strobe` | 7 | | 32 | Implemented |
|
| Strobe Light | `ec.stylize.strobe` | 7 | GPU | 32 | Implemented |
|
||||||
| Texturize | `ec.stylize.texturize` | 4 | | 32 | Implemented |
|
| Texturize | `ec.stylize.texturize` | 4 | GPU | 32 | Implemented |
|
||||||
| Threshold | `ec.stylize.threshold` | 1 | | 32 | Implemented |
|
| Threshold | `ec.stylize.threshold` | 1 | GPU | 32 | Implemented |
|
||||||
|
|
||||||
## Text
|
## Text
|
||||||
|
|
||||||
|
|
@ -379,9 +379,9 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||||
|---|---|---:|:-:|:-:|---|
|
|---|---|---:|:-:|:-:|---|
|
||||||
| CC Force Motion Blur | `ec.time.ccforcemotionblur` | 4 | | 32 | Implemented |
|
| CC Force Motion Blur | `ec.time.ccforcemotionblur` | 4 | | 32 | Implemented |
|
||||||
| CC Wide Time | `ec.time.ccwidetime` | 3 | | 32 | Implemented |
|
| CC Wide Time | `ec.time.ccwidetime` | 3 | | 32 | Implemented |
|
||||||
| Echo | `ec.time.echo` | 5 | | 32 | Implemented |
|
| Echo | `ec.time.echo` | 5 | GPU | 32 | Implemented |
|
||||||
| Pixel Motion Blur | `ec.time.pixelmotionblur` | 4 | | 32 | Implemented |
|
| Pixel Motion Blur | `ec.time.pixelmotionblur` | 4 | | 32 | Implemented |
|
||||||
| Posterize Time | `ec.time.posterizetime` | 1 | | 32 | Implemented |
|
| Posterize Time | `ec.time.posterizetime` | 1 | GPU | 32 | Implemented |
|
||||||
| Time Difference | `ec.time.timedifference` | 5 | | 32 | Implemented |
|
| Time Difference | `ec.time.timedifference` | 5 | | 32 | Implemented |
|
||||||
| Time Displacement | `ec.time.timedisplacement` | 4 | | 32 | Implemented |
|
| Time Displacement | `ec.time.timedisplacement` | 4 | | 32 | Implemented |
|
||||||
| Timewarp | `ec.time.timewarp` | 28 | | 32 | Implemented |
|
| Timewarp | `ec.time.timewarp` | 28 | | 32 | Implemented |
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ How close EffectCraft is to After Effects 2026, feature by feature, and how much
|
||||||
| Unweighted | ≈ 94% (half-credit) / 98% (fractions) |
|
| Unweighted | ≈ 94% (half-credit) / 98% (fractions) |
|
||||||
| P0 / P1 / P2 | ≈ 96% / 92% / 85% (half-credit); 99% / 97% / 96% (fractions) |
|
| P0 / P1 / P2 | ≈ 96% / 92% / 85% (half-credit); 99% / 97% / 96% (fractions) |
|
||||||
| Features done / partial / missing | 80 / 12 / 0 of 92 |
|
| Features done / partial / missing | 80 / 12 / 0 of 92 |
|
||||||
| **Effects** | **298 of 298** After Effects 2026 effects exist; 42 still simplified ([effects.md](effects.md)); 58 run on the GPU |
|
| **Effects** | **298 of 298** After Effects 2026 effects exist; 42 still simplified ([effects.md](effects.md)); 152 run on the GPU |
|
||||||
| Disabled menu entries left | 0 (the stub list is empty) |
|
| Disabled menu entries left | 0 (the stub list is empty) |
|
||||||
| Remaining work | ≈ 19–22 agent-hours at the pace measured so far (≈ 58 on the conservative audit scale) |
|
| Remaining work | ≈ 19–22 agent-hours at the pace measured so far (≈ 58 on the conservative audit scale) |
|
||||||
| **Wall-clock estimate** | **≈ 4–5 hours** with five agents in parallel; ≈ 3 hours for 100% of P0 + P1 |
|
| **Wall-clock estimate** | **≈ 4–5 hours** with five agents in parallel; ≈ 3 hours for 100% of P0 + P1 |
|
||||||
|
|
@ -136,7 +136,7 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
|
||||||
| Text | ≈ 95% | 0.4 | no extruded strokes, the Variable Font Axes animator changes outlines but not advances (M13.6: variable font axes in the character style — `layer.setText variations`, the Character panel's Variable Font Axes fields — shape with HVAR / gvar advances and draw at that design-space position; OpenType features — stylistic sets, discretionary ligatures, contextual / stylistic alternates, swash, titling, ordinals, fractions, figure styles, true small caps / all small caps and superior / inferior glyphs with faux fallback — per character with the Character panel's OpenType popup and `text.fontFeatures` landed in M13.2; vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) |
|
| Text | ≈ 95% | 0.4 | no extruded strokes, the Variable Font Axes animator changes outlines but not advances (M13.6: variable font axes in the character style — `layer.setText variations`, the Character panel's Variable Font Axes fields — shape with HVAR / gvar advances and draw at that design-space position; OpenType features — stylistic sets, discretionary ligatures, contextual / stylistic alternates, swash, titling, ordinals, fractions, figure styles, true small caps / all small caps and superior / inferior glyphs with faux fallback — per character with the Character panel's OpenType popup and `text.fontFeatures` landed in M13.2; vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) |
|
||||||
| Web | 85% | 1.0 | viewer frames and Roto Brush propagation still on the page's thread; GPU effects in the browser (browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2) |
|
| Web | 85% | 1.0 | viewer frames and Roto Brush propagation still on the page's thread; GPU effects in the browser (browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2) |
|
||||||
| 3D | 88% | 5.5 | multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6 |
|
| 3D | 88% | 5.5 | multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6 |
|
||||||
| Effects | ≈ 80% | 4.0 | GPU versions of the remaining effects (58 run on the GPU since M12.7) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (152 run on the GPU since M13.9, EFF-5: keying, mattes, channel, colour correction, stylize, distort, noise, blurs and time effects joined the 58 of M12.7; Warp, Bezier Warp, Reshape, Smear, Cartoon, CC Page Turn and others still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
|
||||||
| Interface | 74% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
|
| Interface | 74% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
|
||||||
| Project | ≈ 68% | 5.5 | auto-save, folder moves, OCIO displays beyond the built-in tone map (Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3) |
|
| Project | ≈ 68% | 5.5 | auto-save, folder moves, OCIO displays beyond the built-in tone map (Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3) |
|
||||||
| Masks & roto | 74% | 5.0 | Roto Brush's learned (3.0) segmentation model (variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) |
|
| Masks & roto | 74% | 5.0 | Roto Brush's learned (3.0) segmentation model (variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) |
|
||||||
|
|
@ -247,7 +247,7 @@ Browser in the web app.
|
||||||
2. Effect Controls widgets: angle dial, point crosshair, eyedropper, curves and levels editors.
|
2. Effect Controls widgets: angle dial, point crosshair, eyedropper, curves and levels editors.
|
||||||
3. ~~Viewer basics: snapping, rulers, channel view, snapshots, exposure, a drawable region of interest.~~ (M0.13)
|
3. ~~Viewer basics: snapping, rulers, channel view, snapshots, exposure, a drawable region of interest.~~ (M0.13)
|
||||||
4. ~~A GPU (wgpu) compositor, then GPU effects~~ (M12.2: 2D compositing and 16 GPU effects; M12.7:
|
4. ~~A GPU (wgpu) compositor, then GPU effects~~ (M12.2: 2D compositing and 16 GPU effects; M12.7:
|
||||||
Classic 3D runs, adjustment layers, 58 GPU effects and GPU particles).
|
Classic 3D runs, adjustment layers, 58 GPU effects and GPU particles; M13.9: 152 GPU effects).
|
||||||
5. ~~Pen tool for shape paths, shape vertex editing, free transform~~ (M6.5); the Layer viewer.
|
5. ~~Pen tool for shape paths, shape vertex editing, free transform~~ (M6.5); the Layer viewer.
|
||||||
6. ~~Motion-path handles in the viewer; graph editor transform box and snapping.~~ (M5.8)
|
6. ~~Motion-path handles in the viewer; graph editor transform box and snapping.~~ (M5.8)
|
||||||
7. Point tracking and stabilization (in progress).
|
7. Point tracking and stabilization (in progress).
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue