1use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
9#[serde(default, deny_unknown_fields)]
10pub struct Grain {
11 pub strength: f32,
13 pub size: f32,
15 pub chroma: f32,
17}
18
19impl Default for Grain {
20 fn default() -> Self {
21 Self {
22 strength: 3.0,
23 size: 1.5,
24 chroma: 0.0,
25 }
26 }
27}
28
29fn noise_glsl(size: f32) -> String {
32 format!(
33 r"
34uint ffc_hash(uvec3 v) {{
35 v = v * 1664525u + 1013904223u;
36 v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
37 v ^= v >> 16u;
38 v.x += v.y * v.z; v.y += v.z * v.x; v.z += v.x * v.y;
39 return v.x ^ v.y ^ v.z;
40}}
41vec3 ffc_gauss(ivec2 c, uint seed) {{
42 vec3 g = vec3(0.0);
43 for (uint i = 0u; i < 4u; i++) {{
44 uint h = ffc_hash(uvec3(uvec2(c), seed * 4u + i));
45 g += vec3(h & 1023u, (h >> 10) & 1023u, (h >> 20) & 1023u) / 1023.0;
46 }}
47 return (g - 2.0) * 1.7320508;
48}}
49vec3 ffc_grain(vec2 pos, uint seed) {{
50 vec2 p = pos / {size:.4} - 0.5;
51 ivec2 i = ivec2(floor(p));
52 vec2 f = fract(p);
53 f = f * f * (3.0 - 2.0 * f);
54 vec3 a = mix(ffc_gauss(i, seed), ffc_gauss(i + ivec2(1, 0), seed), f.x);
55 vec3 b = mix(ffc_gauss(i + ivec2(0, 1), seed), ffc_gauss(i + ivec2(1, 1), seed), f.x);
56 // Interpolation lowers the variance; put it back so `strength` stays the sigma.
57 return mix(a, b, f.y) * {gain:.4};
58}}
59",
60 size = size.max(1.0),
61 gain = if size > 1.0 { 1.6 } else { 1.0 },
62 )
63}
64
65pub fn hook(g: &Grain) -> Option<String> {
68 if g.strength <= 0.0 {
69 return None;
70 }
71 Some(format!(
72 r"//!HOOK OUTPUT
73//!BIND HOOKED
74//!DESC ffcine grain strength={s} size={z} chroma={c}
75{noise}
76vec4 hook() {{
77 vec4 c = HOOKED_texOff(vec2(0.0));
78 float y = dot(c.rgb, vec3(0.2126, 0.7152, 0.0722));
79 // Strongest in the midtones, as on film; never zero, so shadows don't look plastic.
80 float resp = mix(0.35, 1.0, 4.0 * y * (1.0 - y));
81 vec3 n = ffc_grain(HOOKED_pos * HOOKED_size, uint(frame)) * ({s:.4} / 255.0) * resp;
82 c.rgb += n.r + {c:.4} * vec3(n.g, -0.5 * (n.g + n.b), n.b);
83 return c;
84}}
85",
86 s = g.strength,
87 z = g.size,
88 c = g.chroma,
89 noise = noise_glsl(g.size),
90 ))
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn hook_off_and_on() {
99 assert!(
100 hook(&Grain {
101 strength: 0.0,
102 ..Grain::default()
103 })
104 .is_none()
105 );
106 let h = hook(&Grain::default()).unwrap();
107 assert!(h.starts_with("//!HOOK OUTPUT"));
108 assert!(h.contains("uint(frame)"));
109 }
110}