mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-28 12:07:48 -06:00
M13.9: gpu colour corrections (fx_tone)
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, CC Kernel, Auto Levels / Contrast / Color and Equalize (histograms measured on the CPU, applied on the GPU) on the GPU; Hue/Saturation colour ranges and Levels' channel controls no longer fall back to the CPU. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8ba936f87b
commit
7a96fab302
@@ -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).
|
||||
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 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, &b.img);
|
||||
let h = smoothed_histograms(ctx, img);
|
||||
let ranges: [(f32, f32); 3] = if kind == 1 {
|
||||
let mut combined = vec![0.0; BINS];
|
||||
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];
|
||||
if kind == 2 && ctx.params.b("snapNeutralMidtones") {
|
||||
let mut sums = [0.0f64; 4];
|
||||
for px in &b.img.data {
|
||||
for px in &img.data {
|
||||
let (c, a) = unpremul(*px);
|
||||
if a > 1e-4 {
|
||||
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| {
|
||||
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
|
||||
(ranges, gam)
|
||||
}
|
||||
|
||||
fn auto_levels(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
@@ -397,24 +404,12 @@ fn cdf(h: &[f64]) -> Vec<f32> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
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 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)];
|
||||
/// Equalize's lookup tables (`BINS` entries each) for `img`: the R, G and B cumulative
|
||||
/// histograms (Style RGB), the luminance one (Brightness) or the combined one (Photoshop Style).
|
||||
pub fn equalize_tables(img: &Image, style: u32) -> Vec<Vec<f32>> {
|
||||
let h = histograms(img);
|
||||
match style {
|
||||
1 => {
|
||||
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)
|
||||
});
|
||||
}
|
||||
1 => vec![cdf(&h[3])],
|
||||
2 => {
|
||||
let mut combined = vec![0.0; BINS];
|
||||
for k in 0..3 {
|
||||
@@ -422,12 +417,34 @@ fn equalize(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
combined[i] += h[k][i];
|
||||
}
|
||||
}
|
||||
let c = cdf(&combined);
|
||||
map_ca(&mut b.img, |col, a| (lerp3(col, col.map(|v| look(&c, v)), amt), a));
|
||||
vec![cdf(&combined)]
|
||||
}
|
||||
_ => 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(&cs[k], col[k])), amt), a));
|
||||
map_ca(&mut b.img, |col, a| (lerp3(col, [0, 1, 2].map(|k| look(&t[k], col[k])), amt), a));
|
||||
}
|
||||
}
|
||||
b
|
||||
|
||||
@@ -67,9 +67,10 @@ use std::sync::OnceLock;
|
||||
|
||||
pub use card3d::{CompLight, CompScene};
|
||||
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};
|
||||
pub use distort::transform_shutter;
|
||||
pub use distort2::parse_mesh;
|
||||
use effectcraft_keyframe::Value;
|
||||
@@ -831,6 +832,24 @@ pub const GPU_EFFECTS: &[&str] = &[
|
||||
"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",
|
||||
// effectcraft-gpu fx_stylize (stylize, distort)
|
||||
//
|
||||
// effectcraft-gpu fx_noise (noise, blur, time)
|
||||
|
||||
@@ -154,6 +154,7 @@ impl GpuContext {
|
||||
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();
|
||||
let module =
|
||||
@@ -210,6 +211,7 @@ impl GpuContext {
|
||||
.chain(crate::fx_key::KERNELS)
|
||||
.chain(crate::fx_stylize::KERNELS)
|
||||
.chain(crate::fx_noise::KERNELS)
|
||||
.chain(crate::fx_tone::KERNELS)
|
||||
.map(|e| (e, &layout))
|
||||
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
|
||||
.map(|(e, layout)| {
|
||||
|
||||
@@ -117,6 +117,8 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
"ec.perspective.dropshadow" => drop_shadow(e, ctx, b),
|
||||
"ec.distort.transform" => transform(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),
|
||||
_ 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_distort::IDS.contains(&id) => crate::fx_distort::apply(e, id, ctx, b),
|
||||
@@ -124,6 +126,7 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
_ 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),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
//! 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;
|
||||
|
||||
/// 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",
|
||||
];
|
||||
|
||||
/// 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> {
|
||||
point(e, id, ctx, b)
|
||||
}
|
||||
|
||||
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.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)
|
||||
}
|
||||
@@ -37,6 +37,7 @@ mod fx_generate;
|
||||
mod fx_key;
|
||||
mod fx_noise;
|
||||
mod fx_stylize;
|
||||
mod fx_tone;
|
||||
mod ops;
|
||||
mod particles;
|
||||
mod walk;
|
||||
@@ -182,4 +183,6 @@ mod tests_fx_noise;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_stylize;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_tone;
|
||||
#[cfg(test)]
|
||||
mod tests_particles;
|
||||
|
||||
@@ -0,0 +1,592 @@
|
||||
// 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
|
||||
@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);
|
||||
}
|
||||
default: {}
|
||||
}
|
||||
textureStore(out, p, o);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
//! 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_keyframe::Value;
|
||||
|
||||
use crate::tests::{c, effect_case, n};
|
||||
|
||||
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))]);
|
||||
}
|
||||
Reference in New Issue
Block a user