use super::profile_numeric::{finite_clamp, lerp, sanitize_bands, unit};
use super::{BackdropStyle, DisplayTransform, LightingEnvironment};
use molgfx_core::SelectionHandle;
use molgfx_math::Vec3;
use serde::{Deserialize, Serialize};
#[path = "profile_resolve.rs"]
mod resolve;
#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize)]
pub enum FocusTarget {
#[default]
CameraTarget,
Distance(f32),
WorldPoint(Vec3),
Selection(SelectionHandle),
}
impl FocusTarget {
fn sanitize(self) -> Self {
match self {
Self::Distance(distance) if distance.is_finite() && distance > 0.0 => self,
Self::WorldPoint(point) if point.is_finite() => self,
Self::Selection(_) | Self::CameraTarget => self,
Self::Distance(_) | Self::WorldPoint(_) => Self::CameraTarget,
}
}
}
#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct IllustrationStyle {
pub silhouette_strength: f32,
pub cavity_strength: f32,
pub depth_cue_strength: f32,
#[serde(default)]
pub posterize_levels: f32,
#[serde(default)]
pub motion_persistence: f32,
#[serde(default)]
pub outline_width: f32,
}
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
pub struct DepthOfField {
pub focal_length_mm: f32,
pub f_number: f32,
pub sensor_width_mm: f32,
pub max_blur_pixels: f32,
pub blade_count: u8,
pub focus: FocusTarget,
}
impl DepthOfField {
#[must_use]
pub const fn cinematic() -> Self {
Self {
focal_length_mm: 50.0,
f_number: 8.0,
sensor_width_mm: 36.0,
max_blur_pixels: 14.0,
blade_count: 7,
focus: FocusTarget::CameraTarget,
}
}
fn sanitize(self) -> Self {
Self {
focal_length_mm: finite_clamp(self.focal_length_mm, 1.0, 300.0, 50.0),
f_number: finite_clamp(self.f_number, 0.7, 64.0, 4.0),
sensor_width_mm: finite_clamp(self.sensor_width_mm, 1.0, 100.0, 36.0),
max_blur_pixels: finite_clamp(self.max_blur_pixels, 0.0, 64.0, 0.0),
blade_count: self.blade_count.clamp(3, 12),
focus: self.focus.sanitize(),
}
}
fn blend(self, other: Self, weight: f32) -> Self {
let weight = unit(weight);
Self {
focal_length_mm: lerp(self.focal_length_mm, other.focal_length_mm, weight),
f_number: lerp(self.f_number, other.f_number, weight),
sensor_width_mm: lerp(self.sensor_width_mm, other.sensor_width_mm, weight),
max_blur_pixels: lerp(self.max_blur_pixels, other.max_blur_pixels, weight),
blade_count: if weight >= 0.5 {
other.blade_count
} else {
self.blade_count
},
focus: if weight >= 0.5 {
other.focus
} else {
self.focus
},
}
.sanitize()
}
pub(crate) fn packed(self, focus_distance: f32) -> [f32; 4] {
let settings = self.sanitize();
[
focus_distance.max(1.0e-3),
settings.focal_length_mm / (settings.f_number * settings.sensor_width_mm),
settings.max_blur_pixels,
f32::from(settings.blade_count),
]
}
}
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
pub struct MotionBlur {
pub shutter: f32,
pub max_blur_pixels: f32,
}
impl MotionBlur {
#[must_use]
pub const fn cinematic() -> Self {
Self {
shutter: 0.55,
max_blur_pixels: 18.0,
}
}
fn sanitize(self) -> Self {
Self {
shutter: unit(self.shutter),
max_blur_pixels: finite_clamp(self.max_blur_pixels, 0.0, 96.0, 18.0),
}
}
fn blend(self, other: Self, weight: f32) -> Self {
let weight = unit(weight);
Self {
shutter: lerp(self.shutter, other.shutter, weight),
max_blur_pixels: lerp(self.max_blur_pixels, other.max_blur_pixels, weight),
}
.sanitize()
}
pub(crate) fn packed(self) -> [f32; 4] {
let value = self.sanitize();
[value.shutter, value.max_blur_pixels, 0.0, 0.0]
}
}
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
pub struct BloomStyle {
pub threshold: f32,
pub intensity: f32,
pub radius: f32,
}
impl BloomStyle {
#[must_use]
pub const fn cinematic() -> Self {
Self {
threshold: 1.7,
intensity: 0.26,
radius: 2.0,
}
}
fn sanitize(self) -> Self {
Self {
threshold: finite_clamp(self.threshold, 0.0, 64.0, 1.05),
intensity: unit(self.intensity),
radius: finite_clamp(self.radius, 0.0, 8.0, 2.0),
}
}
fn blend(self, other: Self, weight: f32) -> Self {
let weight = unit(weight);
Self {
threshold: lerp(self.threshold, other.threshold, weight),
intensity: lerp(self.intensity, other.intensity, weight),
radius: lerp(self.radius, other.radius, weight),
}
.sanitize()
}
pub(crate) fn packed(self) -> [f32; 4] {
let style = self.sanitize();
[style.threshold, style.intensity, style.radius, 0.0]
}
}
impl IllustrationStyle {
#[must_use]
pub const fn publication() -> Self {
Self {
silhouette_strength: 0.65,
cavity_strength: 0.35,
depth_cue_strength: 0.15,
posterize_levels: 0.0,
motion_persistence: 0.0,
outline_width: 0.0,
}
}
fn sanitize(self) -> Self {
Self {
silhouette_strength: unit(self.silhouette_strength),
cavity_strength: unit(self.cavity_strength),
depth_cue_strength: unit(self.depth_cue_strength),
posterize_levels: sanitize_bands(self.posterize_levels),
motion_persistence: unit(self.motion_persistence),
outline_width: finite_clamp(self.outline_width, 0.0, 8.0, 0.0),
}
}
fn blend(self, other: Self, weight: f32) -> Self {
let weight = unit(weight);
Self {
silhouette_strength: lerp(self.silhouette_strength, other.silhouette_strength, weight),
cavity_strength: lerp(self.cavity_strength, other.cavity_strength, weight),
depth_cue_strength: lerp(self.depth_cue_strength, other.depth_cue_strength, weight),
posterize_levels: lerp(self.posterize_levels, other.posterize_levels, weight),
motion_persistence: lerp(self.motion_persistence, other.motion_persistence, weight),
outline_width: lerp(self.outline_width, other.outline_width, weight),
}
}
pub(crate) fn packed(self, focus_distance: f32) -> [f32; 4] {
let style = self.sanitize();
[
style.silhouette_strength,
style.cavity_strength,
style.depth_cue_strength,
if focus_distance.is_finite() {
focus_distance.max(1.0e-3)
} else {
1.0
},
]
}
pub(crate) fn npr_packed(self) -> [f32; 4] {
let s = self.sanitize();
[
s.posterize_levels,
s.motion_persistence,
s.outline_width,
0.0,
]
}
}
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub enum PresentationEffect {
Illustration(IllustrationStyle),
DepthOfField(DepthOfField),
MotionBlur(MotionBlur),
Backdrop(BackdropStyle),
Lighting(LightingEnvironment),
Display(DisplayTransform),
Bloom(BloomStyle),
}
#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
pub struct EffectLayer {
pub priority: i16,
pub weight: f32,
pub effect: PresentationEffect,
}
impl EffectLayer {
#[must_use]
pub const fn new(effect: PresentationEffect) -> Self {
Self {
priority: 0,
weight: 1.0,
effect,
}
}
#[must_use]
pub const fn with_weight(mut self, weight: f32) -> Self {
self.weight = weight;
self
}
#[must_use]
pub const fn with_priority(mut self, priority: i16) -> Self {
self.priority = priority;
self
}
}
#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct RenderProfile {
layers: Vec<EffectLayer>,
}
impl RenderProfile {
#[must_use]
pub const fn inspection() -> Self {
Self { layers: Vec::new() }
}
#[must_use]
pub fn illustrative() -> Self {
Self::inspection().with_effect(PresentationEffect::Illustration(
IllustrationStyle::publication(),
))
}
#[must_use]
pub fn cinematic() -> Self {
Self::inspection()
.with_effect(PresentationEffect::Illustration(IllustrationStyle {
silhouette_strength: 0.5,
cavity_strength: 0.4,
depth_cue_strength: 0.28,
posterize_levels: 0.0,
motion_persistence: 0.0,
outline_width: 0.0,
}))
.with_effect(PresentationEffect::Lighting(
LightingEnvironment::documentary(),
))
.with_effect(PresentationEffect::Display(DisplayTransform::cinematic()))
.with_effect(PresentationEffect::Bloom(BloomStyle::cinematic()))
.with_effect(PresentationEffect::DepthOfField(DepthOfField::cinematic()))
.with_effect(PresentationEffect::MotionBlur(MotionBlur::cinematic()))
}
#[must_use]
pub fn with_effect(self, effect: PresentationEffect) -> Self {
self.with_layer(EffectLayer::new(effect))
}
#[must_use]
pub fn with_layer(mut self, layer: EffectLayer) -> Self {
self.layers.push(layer);
self
}
#[must_use]
pub fn layers(&self) -> &[EffectLayer] {
&self.layers
}
}
#[derive(Clone, Copy, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct ResolvedRenderPlan {
illustration: IllustrationStyle,
depth_of_field: Option<DepthOfField>,
motion_blur: Option<MotionBlur>,
bloom: Option<BloomStyle>,
backdrop: BackdropStyle,
lighting: LightingEnvironment,
display: DisplayTransform,
}
impl ResolvedRenderPlan {
#[must_use]
pub const fn illustration(&self) -> IllustrationStyle {
self.illustration
}
#[must_use]
pub const fn depth_of_field(&self) -> Option<DepthOfField> {
self.depth_of_field
}
#[must_use]
pub const fn motion_blur(&self) -> Option<MotionBlur> {
self.motion_blur
}
#[must_use]
pub const fn backdrop(&self) -> BackdropStyle {
self.backdrop
}
#[must_use]
pub const fn lighting(&self) -> LightingEnvironment {
self.lighting
}
#[must_use]
pub const fn display(&self) -> DisplayTransform {
self.display
}
#[must_use]
pub const fn bloom(&self) -> Option<BloomStyle> {
self.bloom
}
pub(crate) fn packed_presentation(self, has_translucency: bool) -> [[f32; 4]; 6] {
super::backdrop::pack(
self.backdrop,
self.display,
has_translucency,
match self.bloom {
Some(style) => style.packed(),
None => [0.0; 4],
},
)
}
pub(crate) fn packed_lighting(self) -> [[f32; 4]; 8] {
self.lighting.packed()
}
pub(crate) fn optics(self, focus_distance: f32) -> [f32; 4] {
match self.depth_of_field {
Some(settings) => settings.packed(focus_distance),
None => [focus_distance.max(1.0e-3), 0.0, 0.0, 0.0],
}
}
}
#[cfg(test)]
#[path = "profile_tests.rs"]
mod tests;