use crate::{CoreError, Scene, Timeline};
use molgfx_math::{Camera, Mat3, Projection, Quat, Vec3};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
#[cfg(test)]
#[path = "camera_path_tests.rs"]
mod tests;
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
pub enum CameraEasing {
Linear,
#[default]
SmoothStep,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub struct CameraKeyframe {
time_seconds: f64,
camera: Camera,
}
impl CameraKeyframe {
pub fn new(time_seconds: f64, camera: Camera) -> Result<Self, CoreError> {
validate_camera(camera)?;
if !time_seconds.is_finite() {
return Err(invalid("camera keyframe time must be finite"));
}
Ok(Self {
time_seconds,
camera,
})
}
#[must_use]
pub const fn time_seconds(self) -> f64 {
self.time_seconds
}
#[must_use]
pub const fn camera(self) -> Camera {
self.camera
}
}
#[derive(Clone, Debug)]
pub struct CameraPath {
keyframes: Arc<[CameraKeyframe]>,
easing: CameraEasing,
}
impl CameraPath {
pub fn new(keyframes: Arc<[CameraKeyframe]>, easing: CameraEasing) -> Result<Self, CoreError> {
if keyframes.len() < 2
|| keyframes
.windows(2)
.any(|rows| rows[0].time_seconds >= rows[1].time_seconds)
{
return Err(invalid(
"camera path needs two strictly increasing keyframes",
));
}
if keyframes.windows(2).any(|rows| {
std::mem::discriminant(&rows[0].camera.projection)
!= std::mem::discriminant(&rows[1].camera.projection)
}) {
return Err(invalid("camera path projection models must match"));
}
Ok(Self { keyframes, easing })
}
#[must_use]
pub fn sample(&self, time_seconds: f64) -> Option<Camera> {
if !time_seconds.is_finite() {
return None;
}
let first = *self.keyframes.first()?;
let last = *self.keyframes.last()?;
if time_seconds <= first.time_seconds {
return Some(first.camera);
}
if time_seconds >= last.time_seconds {
return Some(last.camera);
}
let upper = self
.keyframes
.partition_point(|keyframe| keyframe.time_seconds <= time_seconds);
let start = *self.keyframes.get(upper.saturating_sub(1))?;
let end = *self.keyframes.get(upper)?;
let phase = num_traits::cast(
(time_seconds - start.time_seconds) / (end.time_seconds - start.time_seconds),
)?;
Some(interpolate(
start.camera,
end.camera,
self.easing.apply(phase),
))
}
#[must_use]
pub fn keyframes(&self) -> &[CameraKeyframe] {
&self.keyframes
}
#[must_use]
pub fn range(&self) -> [f64; 2] {
self.keyframes
.first()
.zip(self.keyframes.last())
.map_or([0.0; 2], |(first, last)| {
[first.time_seconds, last.time_seconds]
})
}
}
impl CameraEasing {
fn apply(self, value: f32) -> f32 {
let value = value.clamp(0.0, 1.0);
match self {
Self::Linear => value,
Self::SmoothStep => value * value * (3.0 - 2.0 * value),
}
}
}
#[derive(Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct CameraBookmark {
label: Box<str>,
time_seconds: f64,
camera: Camera,
}
impl CameraBookmark {
pub fn new(
label: impl Into<Box<str>>,
time_seconds: f64,
camera: Camera,
) -> Result<Self, CoreError> {
let label = label.into();
if label.trim().is_empty() || !time_seconds.is_finite() {
return Err(invalid("camera bookmark label and time must be valid"));
}
validate_camera(camera)?;
Ok(Self {
label,
time_seconds,
camera,
})
}
pub fn restore(
&self,
timeline: &mut Timeline,
scene: &mut Scene,
camera: &mut Camera,
) -> Result<(), CoreError> {
timeline.apply(scene, self.time_seconds)?;
*camera = self.camera;
Ok(())
}
#[must_use]
pub fn label(&self) -> &str {
&self.label
}
#[must_use]
pub const fn time_seconds(&self) -> f64 {
self.time_seconds
}
#[must_use]
pub const fn camera(&self) -> Camera {
self.camera
}
pub fn to_json(&self) -> Result<String, CoreError> {
serde_json::to_string(self).map_err(|error| CoreError::InvalidSceneDescription {
summary: error.to_string(),
})
}
pub fn from_json(source: &str) -> Result<Self, CoreError> {
let value: Self =
serde_json::from_str(source).map_err(|error| CoreError::InvalidSceneDescription {
summary: error.to_string(),
})?;
Self::new(value.label, value.time_seconds, value.camera)
}
}
fn interpolate(start: Camera, end: Camera, phase: f32) -> Camera {
let rotation = orientation(start)
.slerp(orientation(end), phase)
.normalize();
let target = start.target.lerp(end.target, phase);
let distance = start.focus_distance() + (end.focus_distance() - start.focus_distance()) * phase;
let forward = rotation * -Vec3::Z;
Camera {
eye: target - forward * distance,
target,
up: rotation * Vec3::Y,
projection: interpolate_projection(start.projection, end.projection, phase),
}
}
fn orientation(camera: Camera) -> Quat {
let forward = (camera.target - camera.eye).normalize();
let right = forward.cross(camera.up).normalize();
let up = right.cross(forward).normalize();
Quat::from_mat3(&Mat3::from_cols(right, up, -forward)).normalize()
}
fn interpolate_projection(start: Projection, end: Projection, phase: f32) -> Projection {
let blend = |left: f32, right: f32| left + (right - left) * phase;
match (start, end) {
(
Projection::Perspective {
fov_y: start_fov,
aspect: start_aspect,
near: start_near,
far: start_far,
},
Projection::Perspective {
fov_y: end_fov,
aspect: end_aspect,
near: end_near,
far: end_far,
},
) => Projection::Perspective {
fov_y: blend(start_fov, end_fov),
aspect: blend(start_aspect, end_aspect),
near: blend(start_near, end_near),
far: blend(start_far, end_far),
},
(
Projection::Orthographic {
height: start_height,
aspect: start_aspect,
near: start_near,
far: start_far,
},
Projection::Orthographic {
height: end_height,
aspect: end_aspect,
near: end_near,
far: end_far,
},
) => Projection::Orthographic {
height: blend(start_height, end_height),
aspect: blend(start_aspect, end_aspect),
near: blend(start_near, end_near),
far: blend(start_far, end_far),
},
_ => start,
}
}
fn validate_camera(camera: Camera) -> Result<(), CoreError> {
let forward = camera.target - camera.eye;
if !camera.eye.is_finite()
|| !camera.target.is_finite()
|| !camera.up.is_finite()
|| forward.length_squared() <= f32::EPSILON
|| camera.up.length_squared() <= f32::EPSILON
|| forward.cross(camera.up).length_squared() <= f32::EPSILON
|| !valid_projection(camera.projection)
{
return Err(invalid("camera path contains a malformed camera"));
}
Ok(())
}
fn valid_projection(projection: Projection) -> bool {
match projection {
Projection::Perspective {
fov_y,
aspect,
near,
far,
} => {
[fov_y, aspect, near, far].into_iter().all(f32::is_finite)
&& fov_y > 0.0
&& fov_y < std::f32::consts::PI
&& aspect > 0.0
&& near > 0.0
&& near < far
}
Projection::Orthographic {
height,
aspect,
near,
far,
} => {
[height, aspect, near, far].into_iter().all(f32::is_finite)
&& height > 0.0
&& aspect > 0.0
&& near > 0.0
&& near < far
}
}
}
const fn invalid(reason: &'static str) -> CoreError {
CoreError::InvalidTimeline { reason }
}