proof_engine/render/postfx/
motion_blur.rs1use glam::Vec2;
13
14#[derive(Clone, Debug)]
16pub struct MotionBlurParams {
17 pub enabled: bool,
18 pub samples: u8,
20 pub scale: f32,
22 pub max_length: f32,
24 pub falloff: f32,
26 pub camera_blur: bool,
28 pub camera_blur_scale: f32,
30 pub quality: BlurQuality,
32 pub temporal: f32,
34}
35
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub enum BlurQuality {
39 Low,
41 Medium,
43 High,
45 Ultra,
47}
48
49impl BlurQuality {
50 pub fn sample_count(&self) -> u8 {
51 match self {
52 BlurQuality::Low => 4,
53 BlurQuality::Medium => 8,
54 BlurQuality::High => 14,
55 BlurQuality::Ultra => 28,
56 }
57 }
58}
59
60impl Default for MotionBlurParams {
61 fn default() -> Self {
62 Self {
63 enabled: true,
64 samples: 8,
65 scale: 0.3,
66 max_length: 0.05,
67 falloff: 0.7,
68 camera_blur: false,
69 camera_blur_scale: 0.5,
70 quality: BlurQuality::Medium,
71 temporal: 0.0,
72 }
73 }
74}
75
76impl MotionBlurParams {
77 pub fn none() -> Self { Self { enabled: false, ..Default::default() } }
79
80 pub fn cinematic() -> Self {
82 Self {
83 enabled: true,
84 samples: 16,
85 scale: 0.5,
86 max_length: 0.08,
87 falloff: 0.8,
88 camera_blur: true,
89 camera_blur_scale: 0.8,
90 quality: BlurQuality::High,
91 temporal: 0.1,
92 }
93 }
94
95 pub fn game() -> Self {
97 Self {
98 enabled: true,
99 samples: 6,
100 scale: 0.25,
101 max_length: 0.04,
102 falloff: 0.6,
103 camera_blur: false,
104 camera_blur_scale: 0.0,
105 quality: BlurQuality::Low,
106 temporal: 0.0,
107 }
108 }
109
110 pub fn chaos_warp(intensity: f32) -> Self {
112 let i = intensity.clamp(0.0, 1.0);
113 Self {
114 enabled: i > 0.05,
115 samples: (4.0 + i * 20.0) as u8,
116 scale: i * 1.5,
117 max_length: i * 0.2,
118 falloff: 0.3,
119 camera_blur: true,
120 camera_blur_scale: i * 2.0,
121 quality: BlurQuality::Medium,
122 temporal: i * 0.5,
123 }
124 }
125
126 pub fn lerp(a: &Self, b: &Self, t: f32) -> Self {
128 let t = t.clamp(0.0, 1.0);
129 Self {
130 enabled: if t < 0.5 { a.enabled } else { b.enabled },
131 samples: if t < 0.5 { a.samples } else { b.samples },
132 scale: lerp_f32(a.scale, b.scale, t),
133 max_length: lerp_f32(a.max_length, b.max_length, t),
134 falloff: lerp_f32(a.falloff, b.falloff, t),
135 camera_blur: if t < 0.5 { a.camera_blur } else { b.camera_blur },
136 camera_blur_scale: lerp_f32(a.camera_blur_scale, b.camera_blur_scale, t),
137 quality: if t < 0.5 { a.quality } else { b.quality },
138 temporal: lerp_f32(a.temporal, b.temporal, t),
139 }
140 }
141}
142
143pub struct VelocityBuffer {
150 pub width: u32,
151 pub height: u32,
152 velocities: Vec<Vec2>,
153}
154
155impl VelocityBuffer {
156 pub fn new(width: u32, height: u32) -> Self {
157 Self {
158 width, height,
159 velocities: vec![Vec2::ZERO; (width * height) as usize],
160 }
161 }
162
163 pub fn clear(&mut self) {
164 self.velocities.iter_mut().for_each(|v| *v = Vec2::ZERO);
165 }
166
167 pub fn set(&mut self, x: u32, y: u32, velocity: Vec2) {
168 if x < self.width && y < self.height {
169 self.velocities[(y * self.width + x) as usize] = velocity;
170 }
171 }
172
173 pub fn get(&self, x: u32, y: u32) -> Vec2 {
174 if x < self.width && y < self.height {
175 self.velocities[(y * self.width + x) as usize]
176 } else {
177 Vec2::ZERO
178 }
179 }
180
181 pub fn add(&mut self, x: u32, y: u32, velocity: Vec2) {
183 if x < self.width && y < self.height {
184 self.velocities[(y * self.width + x) as usize] += velocity;
185 }
186 }
187
188 pub fn splat(&mut self, pos_px: Vec2, vel_uv: Vec2, radius_px: f32) {
192 let r = radius_px.ceil() as i32;
193 let cx = pos_px.x as i32;
194 let cy = pos_px.y as i32;
195 for dy in -r..=r {
196 for dx in -r..=r {
197 let x = cx + dx;
198 let y = cy + dy;
199 if x >= 0 && y >= 0 {
200 let dist = ((dx * dx + dy * dy) as f32).sqrt();
201 if dist <= radius_px {
202 let weight = 1.0 - dist / (radius_px + 1.0);
203 self.add(x as u32, y as u32, vel_uv * weight);
204 }
205 }
206 }
207 }
208 }
209
210 pub fn clamp(&mut self, max_length: f32) {
212 for v in &mut self.velocities {
213 let len = v.length();
214 if len > max_length {
215 *v = *v / len * max_length;
216 }
217 }
218 }
219
220 pub fn as_f32_slice(&self) -> &[f32] {
222 unsafe {
223 std::slice::from_raw_parts(
224 self.velocities.as_ptr() as *const f32,
225 self.velocities.len() * 2,
226 )
227 }
228 }
229
230 pub fn pixel_count(&self) -> usize { self.velocities.len() }
231}
232
233pub fn sample_weight(i: usize, n: usize, falloff: f32) -> f32 {
240 if n <= 1 { return 1.0; }
241 let t = i as f32 / (n - 1) as f32;
242 let linear = 1.0 - t;
243 let exp = (-t * 3.0 * falloff).exp();
245 linear * (1.0 - falloff) + exp * falloff
246}
247
248pub fn blur_sample_uvs(velocity_uv: Vec2, n: usize, scale: f32, max_length: f32) -> Vec<Vec2> {
253 let vel = velocity_uv * scale;
254 let vel_len = vel.length();
255 let clamped_vel = if vel_len > max_length && vel_len > 0.0001 {
256 vel / vel_len * max_length
257 } else {
258 vel
259 };
260
261 (0..n).map(|i| {
262 let t = if n <= 1 { 0.0 } else { i as f32 / (n - 1) as f32 };
263 clamped_vel * t
264 }).collect()
265}
266
267fn lerp_f32(a: f32, b: f32, t: f32) -> f32 { a + (b - a) * t }
268
269#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn velocity_buffer_set_get() {
277 let mut buf = VelocityBuffer::new(64, 64);
278 buf.set(10, 20, Vec2::new(0.01, -0.02));
279 let v = buf.get(10, 20);
280 assert!((v.x - 0.01).abs() < 1e-6);
281 assert!((v.y + 0.02).abs() < 1e-6);
282 }
283
284 #[test]
285 fn velocity_buffer_clear() {
286 let mut buf = VelocityBuffer::new(8, 8);
287 buf.set(3, 3, Vec2::new(1.0, 1.0));
288 buf.clear();
289 assert_eq!(buf.get(3, 3), Vec2::ZERO);
290 }
291
292 #[test]
293 fn sample_weight_first_is_highest() {
294 let w0 = sample_weight(0, 8, 0.7);
295 let w7 = sample_weight(7, 8, 0.7);
296 assert!(w0 > w7, "Closest sample should have highest weight");
297 }
298
299 #[test]
300 fn blur_uvs_starts_at_zero() {
301 let vel = Vec2::new(0.1, 0.0);
302 let uvs = blur_sample_uvs(vel, 4, 1.0, 0.5);
303 assert_eq!(uvs.len(), 4);
304 assert!((uvs[0].x).abs() < 1e-6, "First sample should be at origin");
305 }
306
307 #[test]
308 fn blur_uvs_clamped() {
309 let vel = Vec2::new(10.0, 0.0);
310 let uvs = blur_sample_uvs(vel, 4, 1.0, 0.05);
311 for uv in &uvs {
312 assert!(uv.length() <= 0.051, "UV should be clamped: {:?}", uv);
313 }
314 }
315
316 #[test]
317 fn velocity_buffer_f32_slice_length() {
318 let buf = VelocityBuffer::new(16, 16);
319 assert_eq!(buf.as_f32_slice().len(), 16 * 16 * 2);
320 }
321
322 #[test]
323 fn splat_writes_neighborhood() {
324 let mut buf = VelocityBuffer::new(64, 64);
325 buf.splat(Vec2::new(32.0, 32.0), Vec2::new(0.01, 0.0), 3.0);
326 let center = buf.get(32, 32);
328 assert!(center.x > 0.0);
329 let far = buf.get(0, 0);
331 assert_eq!(far, Vec2::ZERO);
332 }
333
334 #[test]
335 fn quality_sample_counts() {
336 assert!(BlurQuality::Ultra.sample_count() > BlurQuality::High.sample_count());
337 assert!(BlurQuality::High.sample_count() > BlurQuality::Low.sample_count());
338 }
339}