Skip to main content

molgfx_render/engine/
optics.rs

1//! Camera-space optical presentation settings.
2
3use super::profile_numeric::{finite_clamp, lerp, unit};
4use molgfx_core::SelectionHandle;
5use molgfx_math::Vec3;
6use serde::{Deserialize, Serialize};
7
8/// A physical or scene-tracked focal plane for thin-lens presentation.
9#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize)]
10pub enum FocusTarget {
11    /// Follow the camera's look-at target.
12    #[default]
13    CameraTarget,
14    /// Use an explicit positive view-space distance in Ångström.
15    Distance(f32),
16    /// Track one world-space point as the camera moves.
17    WorldPoint(Vec3),
18    /// Track the centroid of a caller-authored molecular selection.
19    Selection(SelectionHandle),
20}
21
22impl FocusTarget {
23    pub(super) fn sanitize(self) -> Self {
24        match self {
25            Self::Distance(distance) if distance.is_finite() && distance > 0.0 => self,
26            Self::WorldPoint(point) if point.is_finite() => self,
27            Self::Selection(_) | Self::CameraTarget => self,
28            Self::Distance(_) | Self::WorldPoint(_) => Self::CameraTarget,
29        }
30    }
31}
32
33/// Thin-lens depth-of-field settings for cinematic presentation.
34#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
35pub struct DepthOfField {
36    /// Lens focal length, millimetres.
37    pub focal_length_mm: f32,
38    /// Aperture f-number.
39    pub f_number: f32,
40    /// Horizontal sensor extent, millimetres.
41    pub sensor_width_mm: f32,
42    /// Maximum circle-of-confusion radius, physical pixels.
43    pub max_blur_pixels: f32,
44    /// Aperture blade count, clamped to `[3, 12]`.
45    pub blade_count: u8,
46    /// Scene target from which the focal plane is resolved.
47    pub focus: FocusTarget,
48}
49
50impl DepthOfField {
51    /// A restrained full-frame macro-lens recipe for molecular cinematics.
52    #[must_use]
53    pub const fn cinematic() -> Self {
54        Self {
55            focal_length_mm: 50.0,
56            // Stopped well down on purpose. A molecule is a deep subject: at a
57            // wide aperture its own front and back fall outside the focal
58            // range and the whole specimen goes soft, which reads as a
59            // low-resolution image rather than a photographic one. This keeps
60            // the subject crisp and spends the blur on what is genuinely far
61            // from the focal plane.
62            f_number: 8.0,
63            sensor_width_mm: 36.0,
64            max_blur_pixels: 14.0,
65            blade_count: 7,
66            focus: FocusTarget::CameraTarget,
67        }
68    }
69
70    pub(super) fn sanitize(self) -> Self {
71        Self {
72            focal_length_mm: finite_clamp(self.focal_length_mm, 1.0, 300.0, 50.0),
73            f_number: finite_clamp(self.f_number, 0.7, 64.0, 4.0),
74            sensor_width_mm: finite_clamp(self.sensor_width_mm, 1.0, 100.0, 36.0),
75            max_blur_pixels: finite_clamp(self.max_blur_pixels, 0.0, 64.0, 0.0),
76            blade_count: self.blade_count.clamp(3, 12),
77            focus: self.focus.sanitize(),
78        }
79    }
80
81    pub(super) fn blend(self, other: Self, weight: f32) -> Self {
82        let weight = unit(weight);
83        Self {
84            focal_length_mm: lerp(self.focal_length_mm, other.focal_length_mm, weight),
85            f_number: lerp(self.f_number, other.f_number, weight),
86            sensor_width_mm: lerp(self.sensor_width_mm, other.sensor_width_mm, weight),
87            max_blur_pixels: lerp(self.max_blur_pixels, other.max_blur_pixels, weight),
88            blade_count: if weight >= 0.5 {
89                other.blade_count
90            } else {
91                self.blade_count
92            },
93            focus: if weight >= 0.5 {
94                other.focus
95            } else {
96                self.focus
97            },
98        }
99        .sanitize()
100    }
101
102    pub(crate) fn packed(self, focus_distance: f32) -> [f32; 4] {
103        let settings = self.sanitize();
104        [
105            focus_distance.max(1.0e-3),
106            settings.focal_length_mm / (settings.f_number * settings.sensor_width_mm),
107            settings.max_blur_pixels,
108            f32::from(settings.blade_count),
109        ]
110    }
111}
112
113/// Camera-shutter motion blur driven by the renderer's true object motion.
114#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
115pub struct MotionBlur {
116    /// Fraction of the frame interval exposed by the virtual shutter.
117    pub shutter: f32,
118    /// Maximum blur length in physical pixels.
119    pub max_blur_pixels: f32,
120}
121
122impl MotionBlur {
123    /// A restrained cinematic shutter that preserves molecular legibility.
124    #[must_use]
125    pub const fn cinematic() -> Self {
126        Self {
127            shutter: 0.55,
128            max_blur_pixels: 18.0,
129        }
130    }
131
132    pub(super) fn sanitize(self) -> Self {
133        Self {
134            shutter: unit(self.shutter),
135            max_blur_pixels: finite_clamp(self.max_blur_pixels, 0.0, 96.0, 18.0),
136        }
137    }
138
139    pub(super) fn blend(self, other: Self, weight: f32) -> Self {
140        let weight = unit(weight);
141        Self {
142            shutter: lerp(self.shutter, other.shutter, weight),
143            max_blur_pixels: lerp(self.max_blur_pixels, other.max_blur_pixels, weight),
144        }
145        .sanitize()
146    }
147
148    pub(crate) fn packed(self) -> [f32; 4] {
149        let value = self.sanitize();
150        [value.shutter, value.max_blur_pixels, 0.0, 0.0]
151    }
152}