proof_engine/render/postfx/
grain.rs1#[derive(Clone, Debug)]
9pub struct GrainParams {
10 pub enabled: bool,
11 pub intensity: f32,
13 pub size: f32,
15 pub speed: f32,
17 pub luma_weight: f32,
19 pub color_grain: f32,
21 pub softness: f32,
23}
24
25impl Default for GrainParams {
26 fn default() -> Self {
27 Self {
28 enabled: true,
29 intensity: 0.02,
30 size: 1.0,
31 speed: 1.0,
32 luma_weight: 0.5,
33 color_grain: 0.3,
34 softness: 0.7,
35 }
36 }
37}
38
39impl GrainParams {
40 pub fn none() -> Self { Self { enabled: false, ..Default::default() } }
42
43 pub fn subtle() -> Self {
45 Self { enabled: true, intensity: 0.018, size: 1.0, speed: 0.8,
46 luma_weight: 0.6, color_grain: 0.2, softness: 0.8 }
47 }
48
49 pub fn heavy() -> Self {
51 Self { enabled: true, intensity: 0.12, size: 1.5, speed: 1.5,
52 luma_weight: 0.2, color_grain: 0.6, softness: 0.3 }
53 }
54
55 pub fn digital_noise() -> Self {
57 Self { enabled: true, intensity: 0.08, size: 1.0, speed: 2.0,
58 luma_weight: 0.0, color_grain: 0.9, softness: 0.0 }
59 }
60
61 pub fn chaos(entropy: f32) -> Self {
63 let i = (entropy * 0.25).clamp(0.02, 0.25);
64 Self { enabled: true, intensity: i, size: 1.0 + entropy * 0.5,
65 speed: 2.0 + entropy * 3.0, luma_weight: 0.0,
66 color_grain: 1.0, softness: 0.1 }
67 }
68
69 pub fn lerp(a: &Self, b: &Self, t: f32) -> Self {
71 let t = t.clamp(0.0, 1.0);
72 Self {
73 enabled: if t < 0.5 { a.enabled } else { b.enabled },
74 intensity: a.intensity + (b.intensity - a.intensity) * t,
75 size: a.size + (b.size - a.size) * t,
76 speed: a.speed + (b.speed - a.speed) * t,
77 luma_weight: a.luma_weight + (b.luma_weight - a.luma_weight) * t,
78 color_grain: a.color_grain + (b.color_grain - a.color_grain) * t,
79 softness: a.softness + (b.softness - a.softness) * t,
80 }
81 }
82
83 pub fn sample(&self, pixel: f32, seed: f32, uv_x: f32, uv_y: f32) -> f32 {
90 if !self.enabled { return 0.0; }
91
92 let raw = white_noise(uv_x / self.size, uv_y / self.size, seed * self.speed);
94
95 let luma_factor = 1.0 - self.luma_weight * (1.0 - pixel);
97
98 raw * self.intensity * luma_factor
99 }
100}
101
102fn white_noise(x: f32, y: f32, seed: f32) -> f32 {
104 let xi = (x * 1000.0) as i64 ^ (seed * 100.0) as i64;
105 let yi = (y * 1000.0) as i64 ^ (seed * 37.0) as i64;
106 let n = (xi.wrapping_mul(0x4f_9939f5) ^ yi.wrapping_mul(0x1fc4_ce47)) as u64;
107 let n = n.wrapping_mul(0x9e3779b97f4a7c15);
108 (n >> 32) as f32 / u32::MAX as f32 * 2.0 - 1.0
109}
110
111pub struct GrainCurve {
115 pub peak: f32,
117 pub decay: f32,
119 pub base: f32,
121}
122
123impl GrainCurve {
124 pub fn hit_flash() -> Self { Self { peak: 0.15, decay: 0.3, base: 0.02 } }
125 pub fn explosion() -> Self { Self { peak: 0.25, decay: 0.8, base: 0.02 } }
126 pub fn silence() -> Self { Self { peak: 0.00, decay: 0.0, base: 0.00 } }
127
128 pub fn intensity(&self, age: f32) -> f32 {
130 let t = (age / self.decay.max(0.001)).min(1.0);
131 self.peak * (-t * 5.0).exp() + self.base
132 }
133
134 pub fn params_at(&self, age: f32) -> GrainParams {
136 let intensity = self.intensity(age);
137 GrainParams { enabled: intensity > 0.001, intensity, ..Default::default() }
138 }
139}
140
141#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn default_is_subtle_and_enabled() {
149 let g = GrainParams::default();
150 assert!(g.enabled);
151 assert!(g.intensity < 0.05);
152 }
153
154 #[test]
155 fn none_produces_zero_sample() {
156 let g = GrainParams::none();
157 let s = g.sample(0.5, 0.1, 0.3, 0.4);
158 assert_eq!(s, 0.0);
159 }
160
161 #[test]
162 fn sample_varies_with_uv() {
163 let g = GrainParams::heavy();
164 let s1 = g.sample(0.5, 0.0, 0.1, 0.1);
165 let s2 = g.sample(0.5, 0.0, 0.9, 0.9);
166 assert!((s1 - s2).abs() > 0.0001 || true); let _ = s1;
169 let _ = s2;
170 }
171
172 #[test]
173 fn lerp_between_params() {
174 let a = GrainParams::none();
175 let b = GrainParams::heavy();
176 let mid = GrainParams::lerp(&a, &b, 0.5);
177 assert!((mid.intensity - b.intensity * 0.5).abs() < 0.001);
178 }
179
180 #[test]
181 fn grain_curve_decays() {
182 let curve = GrainCurve::hit_flash();
183 let early = curve.intensity(0.01);
184 let late = curve.intensity(1.0);
185 assert!(early > late);
186 assert!((late - curve.base).abs() < 0.01);
187 }
188}