#[derive(Debug, Clone, Copy, PartialEq)]
pub struct LookParams {
pub exposure: f32,
pub warmth: f32,
pub tint: f32,
pub contrast: f32,
pub gamma: f32,
pub lift: f32,
pub gain: f32,
pub saturation: f32,
}
impl Default for LookParams {
fn default() -> Self {
Self {
exposure: 0.0,
warmth: 0.0,
tint: 0.0,
contrast: 0.0,
gamma: 1.0,
lift: 0.0,
gain: 1.0,
saturation: 1.0,
}
}
}
impl LookParams {
pub fn is_identity(&self) -> bool {
*self == Self::default()
}
pub fn params(&self) -> Vec<(String, f64)> {
[
("exposure", self.exposure),
("warmth", self.warmth),
("tint", self.tint),
("contrast", self.contrast),
("gamma", self.gamma),
("lift", self.lift),
("gain", self.gain),
("saturation", self.saturation),
]
.into_iter()
.map(|(k, v)| (k.to_owned(), f64::from(v)))
.collect()
}
#[allow(clippy::many_single_char_names)] pub fn apply(&self, rgb: [f32; 3]) -> [f32; 3] {
use std::f32::consts::PI;
let mut v = rgb.map(|c| c.clamp(0.0, 1.0));
if self.exposure != 0.0 {
let k = self.exposure.exp2();
v = v.map(|c| (c.powf(2.4) * k).powf(1.0 / 2.4).clamp(0.0, 1.0));
}
let l = f32::midpoint(v[0].max(v[1]).max(v[2]), v[0].min(v[1]).min(v[2]));
let (a, b, k) = (4.0, 0.333, 0.7);
let ws = ((b - l) * a + 0.5).clamp(0.0, 1.0) * k;
let wm =
((l - b) * a + 0.5).clamp(0.0, 1.0) * ((1.0 - l - b) * a + 0.5).clamp(0.0, 1.0) * k;
let wh = ((l + b - 1.0) * a + 0.5).clamp(0.0, 1.0) * k;
let (w, t) = (self.warmth, self.tint);
let shift = [
(ws * 0.4 + wm + wh * 0.6) * w,
-wm * t,
-(ws * 0.4 + wm + wh * 0.6) * w,
];
let y = std::array::from_fn::<f32, 3, _>(|c| {
let x = (v[c] + shift[c])
.clamp(0.0, 1.0)
.powf(1.0 / self.gamma.max(0.01));
let x = x - self.contrast / PI * 0.5 * (2.0 * PI * x).sin();
(self.lift + (self.gain - self.lift) * x).clamp(0.0, 1.0)
});
let luma = 0.2126 * y[0] + 0.7152 * y[1] + 0.0722 * y[2];
y.map(|c| (luma + (c - luma) * self.saturation).clamp(0.0, 1.0))
}
}
pub fn look() -> &'static str {
r"//!PARAM exposure
//!TYPE float
0.0
//!PARAM warmth
//!TYPE float
0.0
//!PARAM tint
//!TYPE float
0.0
//!PARAM contrast
//!TYPE float
0.0
//!PARAM gamma
//!TYPE float
//!MINIMUM 0.01
1.0
//!PARAM lift
//!TYPE float
0.0
//!PARAM gain
//!TYPE float
1.0
//!PARAM saturation
//!TYPE float
1.0
//!HOOK PREOUTPUT
//!BIND HOOKED
//!DESC ffcine look (exposure, colorbalance, curves, saturation)
vec4 hook() {
vec4 c = HOOKED_texOff(vec2(0.0));
vec3 v = clamp(c.rgb, 0.0, 1.0);
if (exposure != 0.0)
v = clamp(pow(pow(v, vec3(2.4)) * exp2(exposure), vec3(1.0 / 2.4)), 0.0, 1.0);
float l = 0.5 * (max(v.r, max(v.g, v.b)) + min(v.r, min(v.g, v.b)));
const float a = 4.0, b = 0.333, k = 0.7;
float ws = clamp((b - l) * a + 0.5, 0.0, 1.0) * k;
float wm = clamp((l - b) * a + 0.5, 0.0, 1.0) * clamp((1.0 - l - b) * a + 0.5, 0.0, 1.0) * k;
float wh = clamp((l + b - 1.0) * a + 0.5, 0.0, 1.0) * k;
v += ws * vec3(0.4 * warmth, 0.0, -0.4 * warmth);
v += wm * vec3(warmth, -tint, -warmth);
v += wh * vec3(0.6 * warmth, 0.0, -0.6 * warmth);
v = clamp(v, 0.0, 1.0);
vec3 y = pow(v, vec3(1.0 / gamma));
y -= contrast / 3.14159265 * 0.5 * sin(6.28318531 * y);
y = clamp(lift + (gain - lift) * y, 0.0, 1.0);
float luma = dot(y, vec3(0.2126, 0.7152, 0.0722));
c.rgb = clamp(mix(vec3(luma), y, saturation), 0.0, 1.0);
return c;
}
"
}
pub fn unsharp(amount: f32) -> Option<String> {
if amount == 0.0 {
return None;
}
Some(format!(
r"//!HOOK PREOUTPUT
//!BIND HOOKED
//!DESC unsharp 5x5 luma amount={amount}
vec4 hook() {{
const vec3 k = vec3(0.299, 0.587, 0.114);
const float w[5] = float[5](1.0, 4.0, 6.0, 4.0, 1.0);
vec4 c = HOOKED_texOff(vec2(0.0));
float b = 0.0;
for (int j = -2; j <= 2; j++)
for (int i = -2; i <= 2; i++)
b += w[i + 2] * w[j + 2] * dot(HOOKED_texOff(vec2(float(i), float(j))).rgb, k);
c.rgb += {amount:.4} * (dot(c.rgb, k) - b / 256.0);
return c;
}}
"
))
}
pub fn finish() -> &'static str {
r"//!PARAM darken
//!TYPE float
1.0
//!PARAM vignette
//!TYPE float
0.0
//!PARAM lift
//!TYPE float
0.0
//!PARAM fade
//!TYPE float
1.0
//!PARAM fade_r
//!TYPE float
0.0
//!PARAM fade_g
//!TYPE float
0.0
//!PARAM fade_b
//!TYPE float
0.0
//!HOOK PREOUTPUT
//!BIND HOOKED
//!DESC ffcine finish (darken, vignette, flash, fade)
vec4 hook() {
vec4 c = HOOKED_texOff(vec2(0.0));
vec3 v = c.rgb * darken;
if (vignette > 0.0) {
vec2 h = 0.5 * HOOKED_size;
float d = length(HOOKED_pos * HOOKED_size - h) / length(h);
float k = d > 1.0 ? 0.0 : cos(vignette * d);
v *= k * k * k * k;
}
v = lift + (1.0 - lift) * v;
c.rgb = mix(vec3(fade_r, fade_g, fade_b), v, fade);
return c;
}
"
}
pub fn alpha() -> &'static str {
r"//!PARAM opacity
//!TYPE float
1.0
//!PARAM radius
//!TYPE float
0.0
//!HOOK OUTPUT
//!BIND HOOKED
//!DESC ffcine layer alpha (opacity, rounded corners)
vec4 hook() {
vec4 c = HOOKED_texOff(vec2(0.0));
vec2 h = 0.5 * HOOKED_size;
vec2 p = HOOKED_pos * HOOKED_size - h;
float r = clamp(radius, 0.0, min(h.x, h.y));
vec2 q = abs(p) - h + vec2(r);
float d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
float a = clamp(0.5 - d, 0.0, 1.0) * clamp(opacity, 0.0, 1.0);
c.a *= a;
return c;
}
"
}
pub fn layer_alpha() -> &'static str {
r"//!PARAM opacity
//!TYPE float
1.0
//!PARAM ky
//!TYPE float
//!MINIMUM 0.001
1.0
//!PARAM radius
//!TYPE float
0.0
//!PARAM shape
//!TYPE float
0.0
//!PARAM mask_x
//!TYPE float
0.0
//!PARAM mask_y
//!TYPE float
0.0
//!PARAM mask_w
//!TYPE float
1.0
//!PARAM mask_h
//!TYPE float
1.0
//!PARAM feather
//!TYPE float
0.0
//!PARAM invert
//!TYPE float
0.0
//!HOOK OUTPUT
//!BIND HOOKED
//!DESC ffcine comp layer alpha (opacity, rounded corners, mask)
vec4 hook() {
vec4 c = HOOKED_texOff(vec2(0.0));
// In horizontal texture pixels.
vec2 size = HOOKED_size / vec2(1.0, ky);
vec2 h = 0.5 * size;
vec2 p = HOOKED_pos * size - h;
float r = clamp(radius, 0.0, min(h.x, h.y));
vec2 q = abs(p) - h + vec2(r);
float d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
float a = clamp(0.5 - d, 0.0, 1.0) * clamp(opacity, 0.0, 1.0);
if (shape > 0.5) {
vec2 mh = max(0.5 * vec2(mask_w, mask_h) * size, vec2(1e-3));
vec2 mp = HOOKED_pos * size - vec2(mask_x, mask_y) * size - mh;
float md;
if (shape < 1.5) {
vec2 mq = abs(mp) - mh;
md = length(max(mq, 0.0)) + min(max(mq.x, mq.y), 0.0);
} else {
// The implicit (|p/h| - 1) over its gradient: px from the edge near it, so the
// soft edge is `feather` px wide all round, not only on the short axis.
float l = length(mp / mh);
vec2 g = mp / (mh * mh);
md = l < 1e-4 ? -min(mh.x, mh.y) : (l - 1.0) * l / max(length(g), 1e-9);
}
float m = clamp(0.5 - md / max(feather, 1.0), 0.0, 1.0);
a *= invert > 0.5 ? 1.0 - m : m;
}
c.a *= a;
return c;
}
"
}
pub fn solid() -> &'static str {
r"//!PARAM solid_r
//!TYPE float
0.0
//!PARAM solid_g
//!TYPE float
0.0
//!PARAM solid_b
//!TYPE float
0.0
//!HOOK PREOUTPUT
//!BIND HOOKED
//!DESC ffcine solid
vec4 hook() {
return vec4(clamp(vec3(solid_r, solid_g, solid_b), 0.0, 1.0), 1.0);
}
"
}
pub fn motion_blur() -> &'static str {
r"//!PARAM blur
//!TYPE float
0.0
//!PARAM dir_x
//!TYPE float
1.0
//!PARAM dir_y
//!TYPE float
0.0
//!HOOK PREOUTPUT
//!BIND HOOKED
//!DESC ffcine motion blur
vec4 hook() {
if (blur < 0.5)
return HOOKED_texOff(vec2(0.0));
vec4 s = vec4(0.0);
for (int i = 0; i < 32; i++) {
float u = (float(i) / 31.0 - 0.5) * blur;
s += HOOKED_texOff(vec2(dir_x, dir_y) * u);
}
return s / 32.0;
}
"
}
pub fn background() -> &'static str {
r"//!PARAM bg_r
//!TYPE float
0.0
//!PARAM bg_g
//!TYPE float
0.0
//!PARAM bg_b
//!TYPE float
0.0
//!PARAM vignette
//!TYPE float
0.0
//!HOOK PREOUTPUT
//!BIND HOOKED
//!DESC ffcine background
vec4 hook() {
vec2 u = HOOKED_pos - 0.5;
float k = 1.0 - vignette * (pow(1.5 * u.x, 2.0) + pow(1.8 * u.y, 2.0));
return vec4(clamp(vec3(bg_r, bg_g, bg_b) * k, 0.0, 1.0), 1.0);
}
"
}
#[cfg(test)]
mod tests {
use super::LookParams;
#[test]
fn look_on_the_cpu() {
let id = LookParams::default();
for v in [[0.0, 0.5, 1.0], [0.2, 0.3, 0.9]] {
let o = id.apply(v);
assert!((0..3).all(|c| (o[c] - v[c]).abs() < 1e-6), "{o:?}");
}
let up = LookParams {
exposure: 1.0,
..id
}
.apply([0.4; 3]);
assert!((up[0].powf(2.4) - 2.0 * 0.4f32.powf(2.4)).abs() < 1e-5);
let warm = LookParams { warmth: 0.1, ..id }.apply([0.5; 3]);
assert!(warm[0] > 0.5 && warm[2] < 0.5);
let grey = LookParams {
saturation: 0.0,
..id
}
.apply([0.8, 0.2, 0.1]);
assert!((grey[0] - grey[2]).abs() < 1e-6);
}
#[test]
fn unsharp_hook() {
assert!(super::unsharp(0.0).is_none());
let s = super::unsharp(0.6).unwrap();
assert!(s.starts_with("//!HOOK PREOUTPUT"));
assert!(s.contains("c.rgb += 0.6000 *"));
}
}