proof_engine/render/postfx/
chromatic.rs1#[derive(Clone, Debug)]
11pub struct ChromaticParams {
12 pub enabled: bool,
13 pub red_offset: f32,
15 pub blue_offset: f32,
17 pub green_offset: f32,
19 pub radial_scale: bool,
21 pub tangential: f32,
23 pub spectrum_spread: f32,
25 pub barrel_distortion: f32,
27}
28
29impl Default for ChromaticParams {
30 fn default() -> Self {
31 Self {
32 enabled: true,
33 red_offset: 0.002,
34 blue_offset: 0.003,
35 green_offset: 0.0,
36 radial_scale: true,
37 tangential: 0.0,
38 spectrum_spread: 0.0,
39 barrel_distortion: 0.0,
40 }
41 }
42}
43
44impl ChromaticParams {
45 pub fn none() -> Self { Self { enabled: false, ..Default::default() } }
47
48 pub fn subtle() -> Self {
50 Self {
51 enabled: true,
52 red_offset: 0.001, blue_offset: 0.0015, green_offset: 0.0,
53 radial_scale: true, tangential: 0.0, spectrum_spread: 0.0,
54 barrel_distortion: 0.0,
55 }
56 }
57
58 pub fn cheap_lens() -> Self {
60 Self {
61 enabled: true,
62 red_offset: 0.006, blue_offset: 0.008, green_offset: 0.0,
63 radial_scale: true, tangential: 0.15, spectrum_spread: 0.3,
64 barrel_distortion: 0.04,
65 }
66 }
67
68 pub fn glitch() -> Self {
70 Self {
71 enabled: true,
72 red_offset: 0.015, blue_offset: 0.012, green_offset: 0.005,
73 radial_scale: false, tangential: 0.5, spectrum_spread: 0.7,
74 barrel_distortion: 0.0,
75 }
76 }
77
78 pub fn chaos_rift(entropy: f32) -> Self {
80 let s = entropy.clamp(0.0, 1.0);
81 Self {
82 enabled: s > 0.01,
83 red_offset: 0.002 + s * 0.02,
84 blue_offset: 0.003 + s * 0.025,
85 green_offset: s * 0.005,
86 radial_scale: true,
87 tangential: s * 0.4,
88 spectrum_spread: s * 0.6,
89 barrel_distortion: s * 0.08,
90 }
91 }
92
93 pub fn trauma_shake(trauma: f32) -> Self {
95 let t = (trauma * trauma).clamp(0.0, 1.0); Self {
97 enabled: t > 0.01,
98 red_offset: t * 0.012,
99 blue_offset: t * 0.015,
100 green_offset: 0.0,
101 radial_scale: true,
102 tangential: 0.0,
103 spectrum_spread: t * 0.2,
104 barrel_distortion: 0.0,
105 }
106 }
107
108 pub fn lerp(a: &Self, b: &Self, t: f32) -> Self {
110 let t = t.clamp(0.0, 1.0);
111 Self {
112 enabled: if t < 0.5 { a.enabled } else { b.enabled },
113 red_offset: lerp_f32(a.red_offset, b.red_offset, t),
114 blue_offset: lerp_f32(a.blue_offset, b.blue_offset, t),
115 green_offset: lerp_f32(a.green_offset, b.green_offset, t),
116 radial_scale: a.radial_scale,
117 tangential: lerp_f32(a.tangential, b.tangential, t),
118 spectrum_spread: lerp_f32(a.spectrum_spread, b.spectrum_spread, t),
119 barrel_distortion: lerp_f32(a.barrel_distortion, b.barrel_distortion, t),
120 }
121 }
122
123 pub fn channel_uvs(&self, uv: [f32; 2]) -> ([f32; 2], [f32; 2], [f32; 2]) {
129 if !self.enabled {
130 return (uv, uv, uv);
131 }
132
133 let cx = uv[0] - 0.5;
134 let cy = uv[1] - 0.5;
135 let radial_dist = (cx * cx + cy * cy).sqrt();
136
137 let (rx, ry) = if radial_dist > 0.0001 {
139 (cx / radial_dist, cy / radial_dist)
140 } else {
141 (0.0, 0.0)
142 };
143
144 let (tx, ty) = (-ry, rx);
146
147 let scale = if self.radial_scale { radial_dist * 2.0 } else { 1.0 };
149
150 let barrel_r = self.barrel_distortion;
152 let barrel_factor = |u: f32, v: f32| -> [f32; 2] {
153 let dx = u - 0.5;
154 let dy = v - 0.5;
155 let r2 = dx * dx + dy * dy;
156 let bd = 1.0 + barrel_r * r2;
157 [0.5 + dx * bd, 0.5 + dy * bd]
158 };
159
160 let offset_uv = |channel_offset: f32| -> [f32; 2] {
161 let radial_component = channel_offset * scale;
162 let tang_component = channel_offset * self.tangential * scale;
163 let ou = cx + (rx * radial_component + tx * tang_component);
164 let ov = cy + (ry * radial_component + ty * tang_component);
165 let base = [0.5 + ou, 0.5 + ov];
166 barrel_factor(base[0], base[1])
167 };
168
169 (
170 offset_uv( self.red_offset),
171 offset_uv( self.green_offset),
172 offset_uv(-self.blue_offset), )
174 }
175}
176
177fn lerp_f32(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t }
178
179#[cfg(test)]
182mod tests {
183 use super::*;
184
185 #[test]
186 fn disabled_returns_same_uv() {
187 let params = ChromaticParams::none();
188 let uv = [0.3, 0.7];
189 let (r, g, b) = params.channel_uvs(uv);
190 assert_eq!(r, uv);
191 assert_eq!(g, uv);
192 assert_eq!(b, uv);
193 }
194
195 #[test]
196 fn enabled_offsets_channels_differently() {
197 let params = ChromaticParams::cheap_lens();
198 let uv = [0.8, 0.5]; let (r, g, b) = params.channel_uvs(uv);
200 assert!((r[0] - b[0]).abs() > 0.001, "R and B should differ at r={:?} b={:?}", r, b);
202 }
203
204 #[test]
205 fn center_pixel_has_zero_offset() {
206 let params = ChromaticParams::cheap_lens();
207 let uv = [0.5, 0.5]; let (r, g, b) = params.channel_uvs(uv);
209 assert!((r[0] - 0.5).abs() < 0.001);
211 assert!((b[0] - 0.5).abs() < 0.001);
212 let _ = g;
213 }
214
215 #[test]
216 fn chaos_rift_scales_with_entropy() {
217 let low = ChromaticParams::chaos_rift(0.1);
218 let high = ChromaticParams::chaos_rift(0.9);
219 assert!(high.red_offset > low.red_offset);
220 }
221
222 #[test]
223 fn trauma_quadratic_at_zero_is_disabled() {
224 let params = ChromaticParams::trauma_shake(0.0);
225 assert!(!params.enabled);
226 }
227}