proof_engine/render/postfx/
distortion.rs1use glam::Vec2;
11
12#[derive(Clone, Debug)]
14pub struct DistortionParams {
15 pub enabled: bool,
16 pub scale: f32,
18 pub chromatic_split: f32,
20 pub max_offset: f32,
22 pub time_scale: f32,
24 pub edge_fade: bool,
26 pub edge_fade_width: f32,
28}
29
30impl Default for DistortionParams {
31 fn default() -> Self {
32 Self {
33 enabled: true,
34 scale: 1.0,
35 chromatic_split: 0.2,
36 max_offset: 0.05,
37 time_scale: 1.0,
38 edge_fade: true,
39 edge_fade_width: 0.05,
40 }
41 }
42}
43
44impl DistortionParams {
45 pub fn none() -> Self { Self { enabled: false, ..Default::default() } }
47
48 pub fn heat_shimmer() -> Self {
50 Self { enabled: true, scale: 0.3, chromatic_split: 0.1,
51 max_offset: 0.008, time_scale: 1.5, edge_fade: true,
52 edge_fade_width: 0.1 }
53 }
54
55 pub fn gravity_lens() -> Self {
57 Self { enabled: true, scale: 2.0, chromatic_split: 0.5,
58 max_offset: 0.08, time_scale: 0.3, edge_fade: true,
59 edge_fade_width: 0.05 }
60 }
61
62 pub fn chaos_rift(entropy: f32) -> Self {
64 let e = entropy.clamp(0.0, 1.0);
65 Self { enabled: e > 0.01, scale: e * 3.0, chromatic_split: e * 0.8,
66 max_offset: e * 0.12, time_scale: 1.0 + e * 4.0, edge_fade: false,
67 edge_fade_width: 0.0 }
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 scale: lerp_f32(a.scale, b.scale, t),
76 chromatic_split: lerp_f32(a.chromatic_split, b.chromatic_split, t),
77 max_offset: lerp_f32(a.max_offset, b.max_offset, t),
78 time_scale: lerp_f32(a.time_scale, b.time_scale, t),
79 edge_fade: a.edge_fade,
80 edge_fade_width: lerp_f32(a.edge_fade_width, b.edge_fade_width, t),
81 }
82 }
83}
84
85pub struct DistortionMap {
92 pub width: u32,
93 pub height: u32,
94 offsets: Vec<Vec2>,
96}
97
98impl DistortionMap {
99 pub fn new(width: u32, height: u32) -> Self {
100 Self {
101 width,
102 height,
103 offsets: vec![Vec2::ZERO; (width * height) as usize],
104 }
105 }
106
107 pub fn clear(&mut self) {
108 self.offsets.iter_mut().for_each(|o| *o = Vec2::ZERO);
109 }
110
111 pub fn set(&mut self, x: u32, y: u32, offset: Vec2) {
112 if x < self.width && y < self.height {
113 self.offsets[(y * self.width + x) as usize] = offset;
114 }
115 }
116
117 pub fn get(&self, x: u32, y: u32) -> Vec2 {
118 if x < self.width && y < self.height {
119 self.offsets[(y * self.width + x) as usize]
120 } else {
121 Vec2::ZERO
122 }
123 }
124
125 pub fn add(&mut self, x: u32, y: u32, offset: Vec2) {
126 if x < self.width && y < self.height {
127 self.offsets[(y * self.width + x) as usize] += offset;
128 }
129 }
130
131 pub fn as_f32_slice(&self) -> Vec<f32> {
133 self.offsets.iter().flat_map(|o| [o.x, o.y, 0.0, 0.0]).collect()
134 }
135
136 pub fn add_gravity_lens(&mut self, center_uv: Vec2, strength: f32, radius_uv: f32) {
141 for y in 0..self.height {
142 for x in 0..self.width {
143 let uv = Vec2::new(
144 x as f32 / self.width as f32,
145 y as f32 / self.height as f32,
146 );
147 let delta = center_uv - uv;
148 let dist = delta.length();
149 if dist < radius_uv && dist > 0.0001 {
150 let factor = (1.0 - dist / radius_uv).powi(2);
151 let pull = delta / dist * strength * factor;
152 self.add(x, y, pull);
153 }
154 }
155 }
156 }
157
158 pub fn add_heat_shimmer(
160 &mut self,
161 region_min: Vec2,
162 region_max: Vec2,
163 strength: f32,
164 time: f32,
165 ) {
166 for y in 0..self.height {
167 for x in 0..self.width {
168 let uv = Vec2::new(
169 x as f32 / self.width as f32,
170 y as f32 / self.height as f32,
171 );
172 if uv.x < region_min.x || uv.x > region_max.x
173 || uv.y < region_min.y || uv.y > region_max.y {
174 continue;
175 }
176 let phase_x = (uv.y * 20.0 + time * 3.0).sin();
178 let phase_y = (uv.x * 15.0 + time * 2.3).cos();
179 let offset = Vec2::new(phase_x, phase_y) * strength;
180 self.add(x, y, offset);
181 }
182 }
183 }
184
185 pub fn clamp_offsets(&mut self, max_magnitude: f32) {
187 for o in &mut self.offsets {
188 let len = o.length();
189 if len > max_magnitude {
190 *o = *o / len * max_magnitude;
191 }
192 }
193 }
194
195 pub fn pixel_count(&self) -> usize { self.offsets.len() }
196}
197
198fn lerp_f32(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t }
199
200#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn new_map_is_all_zero() {
208 let map = DistortionMap::new(16, 16);
209 assert_eq!(map.get(0, 0), Vec2::ZERO);
210 assert_eq!(map.get(8, 8), Vec2::ZERO);
211 }
212
213 #[test]
214 fn set_and_get() {
215 let mut map = DistortionMap::new(16, 16);
216 map.set(3, 5, Vec2::new(0.01, -0.02));
217 let v = map.get(3, 5);
218 assert!((v.x - 0.01).abs() < 1e-6);
219 assert!((v.y + 0.02).abs() < 1e-6);
220 }
221
222 #[test]
223 fn clear_resets_all() {
224 let mut map = DistortionMap::new(8, 8);
225 map.set(2, 2, Vec2::new(1.0, 1.0));
226 map.clear();
227 assert_eq!(map.get(2, 2), Vec2::ZERO);
228 }
229
230 #[test]
231 fn gravity_lens_creates_offsets() {
232 let mut map = DistortionMap::new(64, 64);
233 map.add_gravity_lens(Vec2::new(0.5, 0.5), 0.05, 0.3);
234 let edge = map.get(0, 0);
237 let near_center = map.get(32, 32);
238 assert!(edge.length() > near_center.length());
239 }
240
241 #[test]
242 fn clamp_limits_magnitude() {
243 let mut map = DistortionMap::new(4, 4);
244 map.set(0, 0, Vec2::new(10.0, 10.0));
245 map.clamp_offsets(0.1);
246 assert!(map.get(0, 0).length() <= 0.11);
247 }
248
249 #[test]
250 fn as_f32_slice_length() {
251 let map = DistortionMap::new(8, 8);
252 let v = map.as_f32_slice();
253 assert_eq!(v.len(), 8 * 8 * 4); }
255
256 #[test]
257 fn chaos_rift_scales() {
258 let low = DistortionParams::chaos_rift(0.1);
259 let high = DistortionParams::chaos_rift(0.9);
260 assert!(high.scale > low.scale);
261 }
262}