1use super::profile_numeric::{finite_clamp, lerp, sanitize_bands, unit};
8use super::{BackdropStyle, DisplayTransform, LightingEnvironment};
9use molgfx_core::SelectionHandle;
10use molgfx_math::Vec3;
11use serde::{Deserialize, Serialize};
12
13#[path = "profile_resolve.rs"]
14mod resolve;
15
16#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize)]
18pub enum FocusTarget {
19 #[default]
21 CameraTarget,
22 Distance(f32),
24 WorldPoint(Vec3),
26 Selection(SelectionHandle),
28}
29
30impl FocusTarget {
31 fn sanitize(self) -> Self {
32 match self {
33 Self::Distance(distance) if distance.is_finite() && distance > 0.0 => self,
34 Self::WorldPoint(point) if point.is_finite() => self,
35 Self::Selection(_) | Self::CameraTarget => self,
36 Self::Distance(_) | Self::WorldPoint(_) => Self::CameraTarget,
37 }
38 }
39}
40
41#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize)]
44pub struct IllustrationStyle {
45 pub silhouette_strength: f32,
47 pub cavity_strength: f32,
49 pub depth_cue_strength: f32,
51 #[serde(default)]
53 pub posterize_levels: f32,
54 #[serde(default)]
57 pub motion_persistence: f32,
58 #[serde(default)]
60 pub outline_width: f32,
61}
62
63#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
65pub struct DepthOfField {
66 pub focal_length_mm: f32,
68 pub f_number: f32,
70 pub sensor_width_mm: f32,
72 pub max_blur_pixels: f32,
74 pub blade_count: u8,
76 pub focus: FocusTarget,
78}
79
80impl DepthOfField {
81 #[must_use]
83 pub const fn cinematic() -> Self {
84 Self {
85 focal_length_mm: 50.0,
86 f_number: 8.0,
93 sensor_width_mm: 36.0,
94 max_blur_pixels: 14.0,
95 blade_count: 7,
96 focus: FocusTarget::CameraTarget,
97 }
98 }
99
100 fn sanitize(self) -> Self {
101 Self {
102 focal_length_mm: finite_clamp(self.focal_length_mm, 1.0, 300.0, 50.0),
103 f_number: finite_clamp(self.f_number, 0.7, 64.0, 4.0),
104 sensor_width_mm: finite_clamp(self.sensor_width_mm, 1.0, 100.0, 36.0),
105 max_blur_pixels: finite_clamp(self.max_blur_pixels, 0.0, 64.0, 0.0),
106 blade_count: self.blade_count.clamp(3, 12),
107 focus: self.focus.sanitize(),
108 }
109 }
110
111 fn blend(self, other: Self, weight: f32) -> Self {
112 let weight = unit(weight);
113 Self {
114 focal_length_mm: lerp(self.focal_length_mm, other.focal_length_mm, weight),
115 f_number: lerp(self.f_number, other.f_number, weight),
116 sensor_width_mm: lerp(self.sensor_width_mm, other.sensor_width_mm, weight),
117 max_blur_pixels: lerp(self.max_blur_pixels, other.max_blur_pixels, weight),
118 blade_count: if weight >= 0.5 {
119 other.blade_count
120 } else {
121 self.blade_count
122 },
123 focus: if weight >= 0.5 {
124 other.focus
125 } else {
126 self.focus
127 },
128 }
129 .sanitize()
130 }
131
132 pub(crate) fn packed(self, focus_distance: f32) -> [f32; 4] {
133 let settings = self.sanitize();
134 [
135 focus_distance.max(1.0e-3),
136 settings.focal_length_mm / (settings.f_number * settings.sensor_width_mm),
137 settings.max_blur_pixels,
138 f32::from(settings.blade_count),
139 ]
140 }
141}
142
143#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
145pub struct MotionBlur {
146 pub shutter: f32,
148 pub max_blur_pixels: f32,
150}
151
152impl MotionBlur {
153 #[must_use]
155 pub const fn cinematic() -> Self {
156 Self {
157 shutter: 0.55,
158 max_blur_pixels: 18.0,
159 }
160 }
161
162 fn sanitize(self) -> Self {
163 Self {
164 shutter: unit(self.shutter),
165 max_blur_pixels: finite_clamp(self.max_blur_pixels, 0.0, 96.0, 18.0),
166 }
167 }
168
169 fn blend(self, other: Self, weight: f32) -> Self {
170 let weight = unit(weight);
171 Self {
172 shutter: lerp(self.shutter, other.shutter, weight),
173 max_blur_pixels: lerp(self.max_blur_pixels, other.max_blur_pixels, weight),
174 }
175 .sanitize()
176 }
177
178 pub(crate) fn packed(self) -> [f32; 4] {
179 let value = self.sanitize();
180 [value.shutter, value.max_blur_pixels, 0.0, 0.0]
181 }
182}
183
184#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
186pub struct BloomStyle {
187 pub threshold: f32,
189 pub intensity: f32,
191 pub radius: f32,
193}
194
195impl BloomStyle {
196 #[must_use]
198 pub const fn cinematic() -> Self {
199 Self {
200 threshold: 1.7,
201 intensity: 0.26,
202 radius: 2.0,
203 }
204 }
205
206 fn sanitize(self) -> Self {
207 Self {
208 threshold: finite_clamp(self.threshold, 0.0, 64.0, 1.05),
209 intensity: unit(self.intensity),
210 radius: finite_clamp(self.radius, 0.0, 8.0, 2.0),
211 }
212 }
213
214 fn blend(self, other: Self, weight: f32) -> Self {
215 let weight = unit(weight);
216 Self {
217 threshold: lerp(self.threshold, other.threshold, weight),
218 intensity: lerp(self.intensity, other.intensity, weight),
219 radius: lerp(self.radius, other.radius, weight),
220 }
221 .sanitize()
222 }
223
224 pub(crate) fn packed(self) -> [f32; 4] {
225 let style = self.sanitize();
226 [style.threshold, style.intensity, style.radius, 0.0]
227 }
228}
229
230impl IllustrationStyle {
231 #[must_use]
233 pub const fn publication() -> Self {
234 Self {
235 silhouette_strength: 0.65,
236 cavity_strength: 0.35,
237 depth_cue_strength: 0.15,
238 posterize_levels: 0.0,
239 motion_persistence: 0.0,
240 outline_width: 0.0,
241 }
242 }
243
244 fn sanitize(self) -> Self {
245 Self {
246 silhouette_strength: unit(self.silhouette_strength),
247 cavity_strength: unit(self.cavity_strength),
248 depth_cue_strength: unit(self.depth_cue_strength),
249 posterize_levels: sanitize_bands(self.posterize_levels),
250 motion_persistence: unit(self.motion_persistence),
251 outline_width: finite_clamp(self.outline_width, 0.0, 8.0, 0.0),
252 }
253 }
254
255 fn blend(self, other: Self, weight: f32) -> Self {
256 let weight = unit(weight);
257 Self {
258 silhouette_strength: lerp(self.silhouette_strength, other.silhouette_strength, weight),
259 cavity_strength: lerp(self.cavity_strength, other.cavity_strength, weight),
260 depth_cue_strength: lerp(self.depth_cue_strength, other.depth_cue_strength, weight),
261 posterize_levels: lerp(self.posterize_levels, other.posterize_levels, weight),
262 motion_persistence: lerp(self.motion_persistence, other.motion_persistence, weight),
263 outline_width: lerp(self.outline_width, other.outline_width, weight),
264 }
265 }
266
267 pub(crate) fn packed(self, focus_distance: f32) -> [f32; 4] {
268 let style = self.sanitize();
269 [
270 style.silhouette_strength,
271 style.cavity_strength,
272 style.depth_cue_strength,
273 if focus_distance.is_finite() {
274 focus_distance.max(1.0e-3)
275 } else {
276 1.0
277 },
278 ]
279 }
280
281 pub(crate) fn npr_packed(self) -> [f32; 4] {
284 let s = self.sanitize();
285 [
286 s.posterize_levels,
287 s.motion_persistence,
288 s.outline_width,
289 0.0,
290 ]
291 }
292}
293
294#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
299#[non_exhaustive]
300pub enum PresentationEffect {
301 Illustration(IllustrationStyle),
303 DepthOfField(DepthOfField),
305 MotionBlur(MotionBlur),
307 Backdrop(BackdropStyle),
309 Lighting(LightingEnvironment),
311 Display(DisplayTransform),
313 Bloom(BloomStyle),
315}
316
317#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
319pub struct EffectLayer {
320 pub priority: i16,
323 pub weight: f32,
325 pub effect: PresentationEffect,
327}
328
329impl EffectLayer {
330 #[must_use]
332 pub const fn new(effect: PresentationEffect) -> Self {
333 Self {
334 priority: 0,
335 weight: 1.0,
336 effect,
337 }
338 }
339
340 #[must_use]
342 pub const fn with_weight(mut self, weight: f32) -> Self {
343 self.weight = weight;
344 self
345 }
346
347 #[must_use]
349 pub const fn with_priority(mut self, priority: i16) -> Self {
350 self.priority = priority;
351 self
352 }
353}
354
355#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
357pub struct RenderProfile {
358 layers: Vec<EffectLayer>,
359}
360
361impl RenderProfile {
362 #[must_use]
365 pub const fn inspection() -> Self {
366 Self { layers: Vec::new() }
367 }
368
369 #[must_use]
371 pub fn illustrative() -> Self {
372 Self::inspection().with_effect(PresentationEffect::Illustration(
373 IllustrationStyle::publication(),
374 ))
375 }
376
377 #[must_use]
381 pub fn cinematic() -> Self {
382 Self::inspection()
383 .with_effect(PresentationEffect::Illustration(IllustrationStyle {
384 silhouette_strength: 0.5,
385 cavity_strength: 0.4,
386 depth_cue_strength: 0.28,
387 posterize_levels: 0.0,
388 motion_persistence: 0.0,
389 outline_width: 0.0,
390 }))
391 .with_effect(PresentationEffect::Lighting(
392 LightingEnvironment::documentary(),
393 ))
394 .with_effect(PresentationEffect::Display(DisplayTransform::cinematic()))
395 .with_effect(PresentationEffect::Bloom(BloomStyle::cinematic()))
396 .with_effect(PresentationEffect::DepthOfField(DepthOfField::cinematic()))
397 .with_effect(PresentationEffect::MotionBlur(MotionBlur::cinematic()))
398 }
399
400 #[must_use]
402 pub fn with_effect(self, effect: PresentationEffect) -> Self {
403 self.with_layer(EffectLayer::new(effect))
404 }
405
406 #[must_use]
408 pub fn with_layer(mut self, layer: EffectLayer) -> Self {
409 self.layers.push(layer);
410 self
411 }
412
413 #[must_use]
415 pub fn layers(&self) -> &[EffectLayer] {
416 &self.layers
417 }
418}
419
420#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize)]
422pub struct ResolvedRenderPlan {
423 illustration: IllustrationStyle,
424 depth_of_field: Option<DepthOfField>,
425 motion_blur: Option<MotionBlur>,
426 bloom: Option<BloomStyle>,
427 backdrop: BackdropStyle,
428 lighting: LightingEnvironment,
429 display: DisplayTransform,
430}
431
432impl ResolvedRenderPlan {
433 #[must_use]
435 pub const fn illustration(&self) -> IllustrationStyle {
436 self.illustration
437 }
438
439 #[must_use]
441 pub const fn depth_of_field(&self) -> Option<DepthOfField> {
442 self.depth_of_field
443 }
444
445 #[must_use]
447 pub const fn motion_blur(&self) -> Option<MotionBlur> {
448 self.motion_blur
449 }
450
451 #[must_use]
453 pub const fn backdrop(&self) -> BackdropStyle {
454 self.backdrop
455 }
456
457 #[must_use]
459 pub const fn lighting(&self) -> LightingEnvironment {
460 self.lighting
461 }
462
463 #[must_use]
465 pub const fn display(&self) -> DisplayTransform {
466 self.display
467 }
468
469 #[must_use]
471 pub const fn bloom(&self) -> Option<BloomStyle> {
472 self.bloom
473 }
474
475 pub(crate) fn packed_presentation(self, has_translucency: bool) -> [[f32; 4]; 6] {
476 super::backdrop::pack(
477 self.backdrop,
478 self.display,
479 has_translucency,
480 match self.bloom {
481 Some(style) => style.packed(),
482 None => [0.0; 4],
483 },
484 )
485 }
486
487 pub(crate) fn packed_lighting(self) -> [[f32; 4]; 8] {
488 self.lighting.packed()
489 }
490
491 pub(crate) fn optics(self, focus_distance: f32) -> [f32; 4] {
492 match self.depth_of_field {
493 Some(settings) => settings.packed(focus_distance),
494 None => [focus_distance.max(1.0e-3), 0.0, 0.0, 0.0],
495 }
496 }
497}
498#[cfg(test)]
499#[path = "profile_tests.rs"]
500mod tests;