1use crate::{Color, Point, Rect, Transform, ValidationError};
4
5const BOKEH_TAPS: [u32; 2] = [8, 64];
7
8const BOKEH_TAP_DENSITY: f64 = 1.4;
11
12#[derive(Clone, Copy, Debug, Default, PartialEq)]
14pub enum ApertureShape {
15 #[default]
17 Disc,
18 Polygon {
20 sides: u8,
22 rotation: f64,
24 },
25}
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum HeightFrom {
30 Luma,
32 Alpha,
34}
35
36#[derive(Clone, Debug, PartialEq)]
38pub enum Optics {
39 LensFlare {
41 center: Point,
43 ghosts: u8,
45 threshold: f64,
47 intensity: f64,
49 tint: Color,
51 },
52 Lens {
54 distortion: f64,
56 chromatic: f64,
58 auto_fit: bool,
60 center: Option<Point>,
62 },
63 LensVignette {
65 angle_of_view: f64,
67 strength: f64,
69 roundness: f64,
71 },
72 Bokeh {
75 radius: f64,
77 shape: ApertureShape,
79 highlight_boost: f64,
81 },
82 RadialBlur {
84 center: Point,
86 strength: f64,
88 samples: u8,
90 decay: f64,
92 },
93 Anamorphic {
96 threshold: f64,
98 length: f64,
100 stretch: f64,
102 tint: Color,
104 intensity: f64,
106 },
107 Relight {
109 height_from: HeightFrom,
111 light_dir: [f64; 3],
113 color: Color,
115 ambient: f64,
117 specular: f64,
119 },
120}
121
122impl Optics {
123 pub fn validate(&self) -> Result<(), ValidationError> {
125 let invalid = || {
126 ValidationError::invalid(
127 "optics",
128 "parameters",
129 "Require finite bounded optical parameters",
130 )
131 };
132 let valid = match self {
133 Self::LensFlare {
134 center,
135 ghosts,
136 threshold,
137 intensity,
138 ..
139 } => {
140 center.x.is_finite()
141 && center.y.is_finite()
142 && (1..=8).contains(ghosts)
143 && threshold.is_finite()
144 && *threshold >= 0.0
145 && intensity.is_finite()
146 && *intensity >= 0.0
147 }
148 Self::Lens {
149 distortion,
150 chromatic,
151 center,
152 ..
153 } => {
154 distortion.is_finite()
155 && (-0.3..=1.0).contains(distortion)
156 && chromatic.is_finite()
157 && (-0.1..=0.1).contains(chromatic)
158 && center.is_none_or(|point| point.x.is_finite() && point.y.is_finite())
159 }
160 Self::LensVignette {
161 angle_of_view,
162 strength,
163 roundness,
164 } => {
165 angle_of_view.is_finite()
166 && *angle_of_view > 0.0
167 && *angle_of_view < 170.0
168 && (0.0..=1.0).contains(strength)
169 && (0.0..=1.0).contains(roundness)
170 }
171 Self::Bokeh {
172 radius,
173 shape,
174 highlight_boost,
175 } => {
176 radius.is_finite()
177 && *radius >= 0.0
178 && (radius * 3.0).is_finite()
179 && highlight_boost.is_finite()
180 && *highlight_boost >= 0.0
181 && match shape {
182 ApertureShape::Disc => true,
183 ApertureShape::Polygon { sides, rotation } => {
184 (3..=16).contains(sides) && rotation.is_finite()
185 }
186 }
187 }
188 Self::RadialBlur {
189 center,
190 strength,
191 samples,
192 decay,
193 } => {
194 center.x.is_finite()
195 && center.y.is_finite()
196 && (0.0..=1.0).contains(strength)
197 && (0.0..=1.0).contains(decay)
198 && (1..=64).contains(samples)
199 }
200 Self::Anamorphic {
201 threshold,
202 length,
203 stretch,
204 intensity,
205 ..
206 } => {
207 threshold.is_finite()
208 && *threshold >= 0.0
209 && length.is_finite()
210 && *length >= 0.0
211 && (length * 3.0).is_finite()
212 && (1.0..=64.0).contains(stretch)
213 && intensity.is_finite()
214 && *intensity >= 0.0
215 }
216 Self::Relight {
217 light_dir,
218 ambient,
219 specular,
220 ..
221 } => {
222 light_dir.iter().all(|value| value.is_finite())
223 && light_dir
224 .iter()
225 .map(|value| value * value)
226 .sum::<f64>()
227 .is_finite()
228 && light_dir.iter().map(|value| value * value).sum::<f64>() > 0.0
229 && ambient.is_finite()
230 && *ambient >= 0.0
231 && specular.is_finite()
232 && *specular >= 0.0
233 }
234 };
235 if valid { Ok(()) } else { Err(invalid()) }
236 }
237
238 pub fn device(&self, transform: Transform, scale: f64) -> Self {
240 match self {
241 Self::LensFlare {
242 center,
243 ghosts,
244 threshold,
245 intensity,
246 tint,
247 } => Self::LensFlare {
248 center: transform.apply(*center),
249 ghosts: *ghosts,
250 threshold: *threshold,
251 intensity: *intensity,
252 tint: *tint,
253 },
254 Self::Lens {
255 distortion,
256 chromatic,
257 auto_fit,
258 center,
259 } => Self::Lens {
260 distortion: *distortion,
261 chromatic: *chromatic,
262 auto_fit: *auto_fit,
263 center: center.map(|center| transform.apply(center)),
264 },
265 Self::Bokeh {
266 radius,
267 shape,
268 highlight_boost,
269 } => Self::Bokeh {
270 radius: radius * scale,
271 shape: *shape,
272 highlight_boost: *highlight_boost,
273 },
274 Self::RadialBlur {
275 center,
276 strength,
277 samples,
278 decay,
279 } => Self::RadialBlur {
280 center: transform.apply(*center),
281 strength: *strength,
282 samples: *samples,
283 decay: *decay,
284 },
285 Self::Anamorphic {
286 threshold,
287 length,
288 stretch,
289 tint,
290 intensity,
291 } => Self::Anamorphic {
292 threshold: *threshold,
293 length: length * scale,
294 stretch: *stretch,
295 tint: *tint,
296 intensity: *intensity,
297 },
298 _ => self.clone(),
299 }
300 }
301
302 pub fn streak_sigmas(&self) -> Option<[f64; 2]> {
304 match self {
305 Self::Anamorphic {
306 length, stretch, ..
307 } => Some([*length, *length / *stretch]),
308 _ => None,
309 }
310 }
311
312 pub fn bounds(&self, source: Rect) -> Rect {
314 match self {
315 Self::Bokeh { radius, .. } => {
316 let radius = radius.ceil();
317 Rect::new(
318 source.left - radius,
319 source.top - radius,
320 source.right + radius,
321 source.bottom + radius,
322 )
323 }
324 Self::Anamorphic { .. } => {
325 let [x, y] = self.streak_sigmas().expect("anamorphic sigmas");
326 crate::FilterOperation::BlurXY {
327 sigma_x: x,
328 sigma_y: y,
329 }
330 .bounds(source, source)
331 }
332 _ => source,
333 }
334 }
335
336 pub fn bokeh_taps(radius: f64) -> u32 {
339 let [minimum, maximum] = BOKEH_TAPS;
340 let wanted = (BOKEH_TAP_DENSITY * radius * radius).ceil();
341 if wanted.is_finite() {
342 (wanted as u32).clamp(minimum, maximum)
343 } else {
344 maximum
345 }
346 }
347
348 pub fn parameters(&self, bounds: Rect) -> ([f64; 32], [[f32; 2]; 64]) {
351 let mut values = [0.0; 32];
352 let mut kernel = [[0.0; 2]; 64];
353 match self {
354 Self::LensFlare {
355 center,
356 ghosts,
357 threshold,
358 intensity,
359 tint,
360 } => {
361 let tint = tint.to_linear_premultiplied();
362 values[..10].copy_from_slice(&[
363 5.0,
364 center.x,
365 center.y,
366 f64::from(*ghosts),
367 *threshold,
368 *intensity,
369 tint[0],
370 tint[1],
371 tint[2],
372 libm::hypot(bounds.width(), bounds.height()).max(1.0) * 0.5,
373 ]);
374 values[10] = tint[3];
375 }
376 Self::Lens {
377 distortion,
378 chromatic,
379 auto_fit,
380 center,
381 } => {
382 let center = center.unwrap_or(bounds.center());
383 let radius = [
384 Point::new(bounds.left, bounds.top),
385 Point::new(bounds.right, bounds.top),
386 Point::new(bounds.left, bounds.bottom),
387 Point::new(bounds.right, bounds.bottom),
388 ]
389 .map(|point| libm::hypot(point.x - center.x, point.y - center.y))
390 .into_iter()
391 .fold(0.0, f64::max);
392 values[..7].copy_from_slice(&[
393 0.0,
394 *distortion,
395 *chromatic,
396 f64::from(*auto_fit),
397 center.x,
398 center.y,
399 radius.max(1e-6),
400 ]);
401 }
402 Self::LensVignette {
403 angle_of_view,
404 strength,
405 roundness,
406 } => {
407 let center = bounds.center();
408 values[..8].copy_from_slice(&[
409 1.0,
410 libm::tan(angle_of_view.to_radians() * 0.5),
411 *strength,
412 *roundness,
413 center.x,
414 center.y,
415 (bounds.width() * 0.5).max(1e-6),
416 (bounds.height() * 0.5).max(1e-6),
417 ]);
418 }
419 Self::Bokeh {
420 radius,
421 shape,
422 highlight_boost,
423 } => {
424 let taps = Self::bokeh_taps(*radius);
425 values[..4].copy_from_slice(&[2.0, *radius, *highlight_boost, f64::from(taps)]);
426 for (index, tap) in kernel.iter_mut().take(taps as usize).enumerate() {
427 let angle = index as f64 * 2.399963229728653;
428 let mut distance = libm::sqrt((index as f64 + 0.5) / f64::from(taps)) * radius;
429 if let ApertureShape::Polygon { sides, rotation } = shape {
430 let sector = std::f64::consts::TAU / f64::from(*sides);
431 distance *= libm::cos(std::f64::consts::PI / f64::from(*sides))
432 / libm::cos(
433 (angle - rotation + sector * 0.5).rem_euclid(sector) - sector * 0.5,
434 );
435 }
436 *tap = [
437 (libm::cos(angle) * distance) as f32,
438 (libm::sin(angle) * distance) as f32,
439 ];
440 }
441 }
442 Self::RadialBlur {
443 center,
444 strength,
445 samples,
446 decay,
447 } => {
448 values[..6].copy_from_slice(&[
449 3.0,
450 center.x,
451 center.y,
452 *strength,
453 f64::from(*samples),
454 *decay,
455 ]);
456 }
457 Self::Anamorphic { .. } => values[0] = 6.0,
458 Self::Relight {
459 height_from,
460 light_dir,
461 color,
462 ambient,
463 specular,
464 } => {
465 let length = libm::sqrt(light_dir.iter().map(|value| value * value).sum());
466 let color = color.to_linear_premultiplied();
467 values[..10].copy_from_slice(&[
468 4.0,
469 f64::from(*height_from == HeightFrom::Alpha),
470 light_dir[0] / length,
471 light_dir[1] / length,
472 light_dir[2] / length,
473 color[0],
474 color[1],
475 color[2],
476 *ambient,
477 *specular,
478 ]);
479 }
480 }
481 (values, kernel)
482 }
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 #[test]
490 fn bokeh_taps_scale_with_radius_and_fill_only_the_used_offsets() {
491 assert_eq!(Optics::bokeh_taps(0.0), 8);
492 assert_eq!(Optics::bokeh_taps(3.0), 13);
493 assert_eq!(Optics::bokeh_taps(6.0), 51);
494 assert_eq!(Optics::bokeh_taps(16.0), 64);
495 assert_eq!(Optics::bokeh_taps(f64::MAX), 64);
496 for (radius, shape) in [
497 (2.5, ApertureShape::Disc),
498 (
499 12.0,
500 ApertureShape::Polygon {
501 sides: 6,
502 rotation: 0.3,
503 },
504 ),
505 ] {
506 let optics = Optics::Bokeh {
507 radius,
508 shape,
509 highlight_boost: 1.0,
510 };
511 let (values, kernel) = optics.parameters(Rect::new(0.0, 0.0, 64.0, 64.0));
512 let taps = Optics::bokeh_taps(radius) as usize;
513 assert_eq!(values[3], taps as f64);
514 for (index, tap) in kernel.iter().enumerate() {
515 let distance = f64::from(tap[0]).hypot(f64::from(tap[1]));
516 if index < taps {
517 assert!(distance > 0.0 && distance <= radius + 1e-4, "{index}");
518 } else {
519 assert_eq!(*tap, [0.0; 2]);
520 }
521 }
522 let reach = kernel[..taps]
524 .iter()
525 .map(|tap| f64::from(tap[0]).hypot(f64::from(tap[1])))
526 .fold(0.0, f64::max);
527 assert!(reach > radius * 0.85, "{reach}");
528 }
529 }
530}