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, intensity: 0.0, ..Default::default() } }
43
44 pub fn subtle() -> Self {
46 Self { enabled: true, intensity: 0.018, size: 1.0, speed: 0.8,
47 luma_weight: 0.6, color_grain: 0.2, softness: 0.8 }
48 }
49
50 pub fn heavy() -> Self {
52 Self { enabled: true, intensity: 0.12, size: 1.5, speed: 1.5,
53 luma_weight: 0.2, color_grain: 0.6, softness: 0.3 }
54 }
55
56 pub fn digital_noise() -> Self {
58 Self { enabled: true, intensity: 0.08, size: 1.0, speed: 2.0,
59 luma_weight: 0.0, color_grain: 0.9, softness: 0.0 }
60 }
61
62 pub fn chaos(entropy: f32) -> Self {
64 let i = (entropy * 0.25).clamp(0.02, 0.25);
65 Self { enabled: true, intensity: i, size: 1.0 + entropy * 0.5,
66 speed: 2.0 + entropy * 3.0, luma_weight: 0.0,
67 color_grain: 1.0, softness: 0.1 }
68 }
69
70 pub fn lerp(a: &Self, b: &Self, t: f32) -> Self {
72 let t = t.clamp(0.0, 1.0);
73 Self {
74 enabled: if t < 0.5 { a.enabled } else { b.enabled },
75 intensity: a.intensity + (b.intensity - a.intensity) * t,
76 size: a.size + (b.size - a.size) * t,
77 speed: a.speed + (b.speed - a.speed) * t,
78 luma_weight: a.luma_weight + (b.luma_weight - a.luma_weight) * t,
79 color_grain: a.color_grain + (b.color_grain - a.color_grain) * t,
80 softness: a.softness + (b.softness - a.softness) * t,
81 }
82 }
83
84 pub fn sample(&self, pixel: f32, seed: f32, uv_x: f32, uv_y: f32) -> f32 {
91 if !self.enabled { return 0.0; }
92
93 let raw = white_noise(uv_x / self.size, uv_y / self.size, seed * self.speed);
95
96 let luma_factor = 1.0 - self.luma_weight * (1.0 - pixel);
98
99 raw * self.intensity * luma_factor
100 }
101}
102
103fn white_noise(x: f32, y: f32, seed: f32) -> f32 {
105 let xi = (x * 1000.0) as i64 ^ (seed * 100.0) as i64;
106 let yi = (y * 1000.0) as i64 ^ (seed * 37.0) as i64;
107 let n = (xi.wrapping_mul(0x4f_9939f5) ^ yi.wrapping_mul(0x1fc4_ce47)) as u64;
108 let n = n.wrapping_mul(0x9e3779b97f4a7c15);
109 (n >> 32) as f32 / u32::MAX as f32 * 2.0 - 1.0
110}
111
112pub struct GrainCurve {
116 pub peak: f32,
118 pub decay: f32,
120 pub base: f32,
122}
123
124impl GrainCurve {
125 pub fn hit_flash() -> Self { Self { peak: 0.15, decay: 0.3, base: 0.02 } }
126 pub fn explosion() -> Self { Self { peak: 0.25, decay: 0.8, base: 0.02 } }
127 pub fn silence() -> Self { Self { peak: 0.00, decay: 0.0, base: 0.00 } }
128
129 pub fn intensity(&self, age: f32) -> f32 {
131 let t = (age / self.decay.max(0.001)).min(1.0);
132 self.peak * (-t * 5.0).exp() + self.base
133 }
134
135 pub fn params_at(&self, age: f32) -> GrainParams {
137 let intensity = self.intensity(age);
138 GrainParams { enabled: intensity > 0.001, intensity, ..Default::default() }
139 }
140}
141
142#[cfg(test)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn default_is_subtle_and_enabled() {
150 let g = GrainParams::default();
151 assert!(g.enabled);
152 assert!(g.intensity < 0.05);
153 }
154
155 #[test]
156 fn none_produces_zero_sample() {
157 let g = GrainParams::none();
158 let s = g.sample(0.5, 0.1, 0.3, 0.4);
159 assert_eq!(s, 0.0);
160 }
161
162 #[test]
163 fn sample_varies_with_uv() {
164 let g = GrainParams::heavy();
165 let s1 = g.sample(0.5, 0.0, 0.1, 0.1);
166 let s2 = g.sample(0.5, 0.0, 0.9, 0.9);
167 assert!((s1 - s2).abs() > 0.0001 || true); let _ = s1;
170 let _ = s2;
171 }
172
173 #[test]
174 fn lerp_between_params() {
175 let a = GrainParams::none();
176 let b = GrainParams::heavy();
177 let mid = GrainParams::lerp(&a, &b, 0.5);
178 assert!((mid.intensity - b.intensity * 0.5).abs() < 0.001);
179 }
180
181 #[test]
182 fn grain_curve_decays() {
183 let curve = GrainCurve::hit_flash();
184 let early = curve.intensity(0.01);
185 let late = curve.intensity(1.0);
186 assert!(early > late);
187 assert!((late - curve.base).abs() < 0.01);
188 }
189}