1#[derive(Debug, Clone, Copy, PartialEq)]
5pub struct LookParams {
6 pub exposure: f32,
8 pub warmth: f32,
9 pub tint: f32,
10 pub contrast: f32,
11 pub gamma: f32,
12 pub lift: f32,
13 pub gain: f32,
14 pub saturation: f32,
15}
16
17impl Default for LookParams {
18 fn default() -> Self {
19 Self {
20 exposure: 0.0,
21 warmth: 0.0,
22 tint: 0.0,
23 contrast: 0.0,
24 gamma: 1.0,
25 lift: 0.0,
26 gain: 1.0,
27 saturation: 1.0,
28 }
29 }
30}
31
32impl LookParams {
33 pub fn is_identity(&self) -> bool {
34 *self == Self::default()
35 }
36
37 pub fn params(&self) -> Vec<(String, f64)> {
39 [
40 ("exposure", self.exposure),
41 ("warmth", self.warmth),
42 ("tint", self.tint),
43 ("contrast", self.contrast),
44 ("gamma", self.gamma),
45 ("lift", self.lift),
46 ("gain", self.gain),
47 ("saturation", self.saturation),
48 ]
49 .into_iter()
50 .map(|(k, v)| (k.to_owned(), f64::from(v)))
51 .collect()
52 }
53
54 #[allow(clippy::many_single_char_names)] pub fn apply(&self, rgb: [f32; 3]) -> [f32; 3] {
57 use std::f32::consts::PI;
58 let mut v = rgb.map(|c| c.clamp(0.0, 1.0));
59 if self.exposure != 0.0 {
60 let k = self.exposure.exp2();
61 v = v.map(|c| (c.powf(2.4) * k).powf(1.0 / 2.4).clamp(0.0, 1.0));
62 }
63 let l = f32::midpoint(v[0].max(v[1]).max(v[2]), v[0].min(v[1]).min(v[2]));
64 let (a, b, k) = (4.0, 0.333, 0.7);
65 let ws = ((b - l) * a + 0.5).clamp(0.0, 1.0) * k;
66 let wm =
67 ((l - b) * a + 0.5).clamp(0.0, 1.0) * ((1.0 - l - b) * a + 0.5).clamp(0.0, 1.0) * k;
68 let wh = ((l + b - 1.0) * a + 0.5).clamp(0.0, 1.0) * k;
69 let (w, t) = (self.warmth, self.tint);
70 let shift = [
71 (ws * 0.4 + wm + wh * 0.6) * w,
72 -wm * t,
73 -(ws * 0.4 + wm + wh * 0.6) * w,
74 ];
75 let y = std::array::from_fn::<f32, 3, _>(|c| {
76 let x = (v[c] + shift[c])
77 .clamp(0.0, 1.0)
78 .powf(1.0 / self.gamma.max(0.01));
79 let x = x - self.contrast / PI * 0.5 * (2.0 * PI * x).sin();
80 (self.lift + (self.gain - self.lift) * x).clamp(0.0, 1.0)
81 });
82 let luma = 0.2126 * y[0] + 0.7152 * y[1] + 0.0722 * y[2];
83 y.map(|c| (luma + (c - luma) * self.saturation).clamp(0.0, 1.0))
84 }
85}
86
87pub fn look() -> &'static str {
94 r"//!PARAM exposure
95//!TYPE float
960.0
97//!PARAM warmth
98//!TYPE float
990.0
100//!PARAM tint
101//!TYPE float
1020.0
103//!PARAM contrast
104//!TYPE float
1050.0
106//!PARAM gamma
107//!TYPE float
108//!MINIMUM 0.01
1091.0
110//!PARAM lift
111//!TYPE float
1120.0
113//!PARAM gain
114//!TYPE float
1151.0
116//!PARAM saturation
117//!TYPE float
1181.0
119//!HOOK PREOUTPUT
120//!BIND HOOKED
121//!DESC ffcine look (exposure, colorbalance, curves, saturation)
122vec4 hook() {
123 vec4 c = HOOKED_texOff(vec2(0.0));
124 vec3 v = clamp(c.rgb, 0.0, 1.0);
125 if (exposure != 0.0)
126 v = clamp(pow(pow(v, vec3(2.4)) * exp2(exposure), vec3(1.0 / 2.4)), 0.0, 1.0);
127 float l = 0.5 * (max(v.r, max(v.g, v.b)) + min(v.r, min(v.g, v.b)));
128 const float a = 4.0, b = 0.333, k = 0.7;
129 float ws = clamp((b - l) * a + 0.5, 0.0, 1.0) * k;
130 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;
131 float wh = clamp((l + b - 1.0) * a + 0.5, 0.0, 1.0) * k;
132 v += ws * vec3(0.4 * warmth, 0.0, -0.4 * warmth);
133 v += wm * vec3(warmth, -tint, -warmth);
134 v += wh * vec3(0.6 * warmth, 0.0, -0.6 * warmth);
135 v = clamp(v, 0.0, 1.0);
136 vec3 y = pow(v, vec3(1.0 / gamma));
137 y -= contrast / 3.14159265 * 0.5 * sin(6.28318531 * y);
138 y = clamp(lift + (gain - lift) * y, 0.0, 1.0);
139 float luma = dot(y, vec3(0.2126, 0.7152, 0.0722));
140 c.rgb = clamp(mix(vec3(luma), y, saturation), 0.0, 1.0);
141 return c;
142}
143"
144}
145
146pub fn unsharp(amount: f32) -> Option<String> {
153 if amount == 0.0 {
154 return None;
155 }
156 Some(format!(
157 r"//!HOOK PREOUTPUT
158//!BIND HOOKED
159//!DESC unsharp 5x5 luma amount={amount}
160vec4 hook() {{
161 const vec3 k = vec3(0.299, 0.587, 0.114);
162 const float w[5] = float[5](1.0, 4.0, 6.0, 4.0, 1.0);
163 vec4 c = HOOKED_texOff(vec2(0.0));
164 float b = 0.0;
165 for (int j = -2; j <= 2; j++)
166 for (int i = -2; i <= 2; i++)
167 b += w[i + 2] * w[j + 2] * dot(HOOKED_texOff(vec2(float(i), float(j))).rgb, k);
168 c.rgb += {amount:.4} * (dot(c.rgb, k) - b / 256.0);
169 return c;
170}}
171"
172 ))
173}
174
175pub fn finish() -> &'static str {
182 r"//!PARAM darken
183//!TYPE float
1841.0
185//!PARAM vignette
186//!TYPE float
1870.0
188//!PARAM lift
189//!TYPE float
1900.0
191//!PARAM fade
192//!TYPE float
1931.0
194//!PARAM fade_r
195//!TYPE float
1960.0
197//!PARAM fade_g
198//!TYPE float
1990.0
200//!PARAM fade_b
201//!TYPE float
2020.0
203//!HOOK PREOUTPUT
204//!BIND HOOKED
205//!DESC ffcine finish (darken, vignette, flash, fade)
206vec4 hook() {
207 vec4 c = HOOKED_texOff(vec2(0.0));
208 vec3 v = c.rgb * darken;
209 if (vignette > 0.0) {
210 vec2 h = 0.5 * HOOKED_size;
211 float d = length(HOOKED_pos * HOOKED_size - h) / length(h);
212 float k = d > 1.0 ? 0.0 : cos(vignette * d);
213 v *= k * k * k * k;
214 }
215 v = lift + (1.0 - lift) * v;
216 c.rgb = mix(vec3(fade_r, fade_g, fade_b), v, fade);
217 return c;
218}
219"
220}
221
222pub fn alpha() -> &'static str {
226 r"//!PARAM opacity
227//!TYPE float
2281.0
229//!PARAM radius
230//!TYPE float
2310.0
232//!HOOK OUTPUT
233//!BIND HOOKED
234//!DESC ffcine layer alpha (opacity, rounded corners)
235vec4 hook() {
236 vec4 c = HOOKED_texOff(vec2(0.0));
237 vec2 h = 0.5 * HOOKED_size;
238 vec2 p = HOOKED_pos * HOOKED_size - h;
239 float r = clamp(radius, 0.0, min(h.x, h.y));
240 vec2 q = abs(p) - h + vec2(r);
241 float d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
242 float a = clamp(0.5 - d, 0.0, 1.0) * clamp(opacity, 0.0, 1.0);
243 c.a *= a;
244 return c;
245}
246"
247}
248
249pub fn layer_alpha() -> &'static str {
257 r"//!PARAM opacity
258//!TYPE float
2591.0
260//!PARAM ky
261//!TYPE float
262//!MINIMUM 0.001
2631.0
264//!PARAM radius
265//!TYPE float
2660.0
267//!PARAM shape
268//!TYPE float
2690.0
270//!PARAM mask_x
271//!TYPE float
2720.0
273//!PARAM mask_y
274//!TYPE float
2750.0
276//!PARAM mask_w
277//!TYPE float
2781.0
279//!PARAM mask_h
280//!TYPE float
2811.0
282//!PARAM feather
283//!TYPE float
2840.0
285//!PARAM invert
286//!TYPE float
2870.0
288//!HOOK OUTPUT
289//!BIND HOOKED
290//!DESC ffcine comp layer alpha (opacity, rounded corners, mask)
291vec4 hook() {
292 vec4 c = HOOKED_texOff(vec2(0.0));
293 // In horizontal texture pixels.
294 vec2 size = HOOKED_size / vec2(1.0, ky);
295 vec2 h = 0.5 * size;
296 vec2 p = HOOKED_pos * size - h;
297 float r = clamp(radius, 0.0, min(h.x, h.y));
298 vec2 q = abs(p) - h + vec2(r);
299 float d = length(max(q, 0.0)) + min(max(q.x, q.y), 0.0) - r;
300 float a = clamp(0.5 - d, 0.0, 1.0) * clamp(opacity, 0.0, 1.0);
301 if (shape > 0.5) {
302 vec2 mh = max(0.5 * vec2(mask_w, mask_h) * size, vec2(1e-3));
303 vec2 mp = HOOKED_pos * size - vec2(mask_x, mask_y) * size - mh;
304 float md;
305 if (shape < 1.5) {
306 vec2 mq = abs(mp) - mh;
307 md = length(max(mq, 0.0)) + min(max(mq.x, mq.y), 0.0);
308 } else {
309 // The implicit (|p/h| - 1) over its gradient: px from the edge near it, so the
310 // soft edge is `feather` px wide all round, not only on the short axis.
311 float l = length(mp / mh);
312 vec2 g = mp / (mh * mh);
313 md = l < 1e-4 ? -min(mh.x, mh.y) : (l - 1.0) * l / max(length(g), 1e-9);
314 }
315 float m = clamp(0.5 - md / max(feather, 1.0), 0.0, 1.0);
316 a *= invert > 0.5 ? 1.0 - m : m;
317 }
318 c.a *= a;
319 return c;
320}
321"
322}
323
324pub fn solid() -> &'static str {
327 r"//!PARAM solid_r
328//!TYPE float
3290.0
330//!PARAM solid_g
331//!TYPE float
3320.0
333//!PARAM solid_b
334//!TYPE float
3350.0
336//!HOOK PREOUTPUT
337//!BIND HOOKED
338//!DESC ffcine solid
339vec4 hook() {
340 return vec4(clamp(vec3(solid_r, solid_g, solid_b), 0.0, 1.0), 1.0);
341}
342"
343}
344
345pub fn motion_blur() -> &'static str {
348 r"//!PARAM blur
349//!TYPE float
3500.0
351//!PARAM dir_x
352//!TYPE float
3531.0
354//!PARAM dir_y
355//!TYPE float
3560.0
357//!HOOK PREOUTPUT
358//!BIND HOOKED
359//!DESC ffcine motion blur
360vec4 hook() {
361 if (blur < 0.5)
362 return HOOKED_texOff(vec2(0.0));
363 vec4 s = vec4(0.0);
364 for (int i = 0; i < 32; i++) {
365 float u = (float(i) / 31.0 - 0.5) * blur;
366 s += HOOKED_texOff(vec2(dir_x, dir_y) * u);
367 }
368 return s / 32.0;
369}
370"
371}
372
373pub fn background() -> &'static str {
377 r"//!PARAM bg_r
378//!TYPE float
3790.0
380//!PARAM bg_g
381//!TYPE float
3820.0
383//!PARAM bg_b
384//!TYPE float
3850.0
386//!PARAM vignette
387//!TYPE float
3880.0
389//!HOOK PREOUTPUT
390//!BIND HOOKED
391//!DESC ffcine background
392vec4 hook() {
393 vec2 u = HOOKED_pos - 0.5;
394 float k = 1.0 - vignette * (pow(1.5 * u.x, 2.0) + pow(1.8 * u.y, 2.0));
395 return vec4(clamp(vec3(bg_r, bg_g, bg_b) * k, 0.0, 1.0), 1.0);
396}
397"
398}
399
400#[cfg(test)]
401mod tests {
402 use super::LookParams;
403
404 #[test]
405 fn look_on_the_cpu() {
406 let id = LookParams::default();
407 for v in [[0.0, 0.5, 1.0], [0.2, 0.3, 0.9]] {
408 let o = id.apply(v);
409 assert!((0..3).all(|c| (o[c] - v[c]).abs() < 1e-6), "{o:?}");
410 }
411 let up = LookParams {
413 exposure: 1.0,
414 ..id
415 }
416 .apply([0.4; 3]);
417 assert!((up[0].powf(2.4) - 2.0 * 0.4f32.powf(2.4)).abs() < 1e-5);
418 let warm = LookParams { warmth: 0.1, ..id }.apply([0.5; 3]);
420 assert!(warm[0] > 0.5 && warm[2] < 0.5);
421 let grey = LookParams {
422 saturation: 0.0,
423 ..id
424 }
425 .apply([0.8, 0.2, 0.1]);
426 assert!((grey[0] - grey[2]).abs() < 1e-6);
427 }
428
429 #[test]
430 fn unsharp_hook() {
431 assert!(super::unsharp(0.0).is_none());
432 let s = super::unsharp(0.6).unwrap();
433 assert!(s.starts_with("//!HOOK PREOUTPUT"));
434 assert!(s.contains("c.rgb += 0.6000 *"));
435 }
436}