molgfx_math/camera/projection.rs
1//! Projection construction with reversed [0, 1] clip depth.
2//!
3//! Reversed depth (near plane at 1.0, far at 0.0) is applied here, once, at
4//! matrix construction; nothing downstream reasons about the clip convention
5//! again. Near and far are fit to the scene's bounding sphere rather than
6//! fixed, so depth precision follows the structure on screen.
7
8use crate::aabb::BoundingSphere;
9use crate::{Mat4, Vec3};
10use serde::{Deserialize, Serialize};
11
12#[cfg(test)]
13#[path = "projection_tests.rs"]
14mod tests;
15
16/// The closest the fitted near plane may come to the camera, in Ångström.
17/// Keeps the projection finite when the camera sits inside the scene bound.
18const MIN_NEAR: f32 = 0.01;
19
20/// How a camera maps view space to clip space.
21///
22/// Orthographic projection is offered as a first-class choice because it is
23/// often the physically correct one: parallel lines stay parallel and
24/// distances compare across the image.
25#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
26pub enum Projection {
27 /// Perspective projection with a vertical field of view in radians.
28 Perspective {
29 /// Vertical field of view, radians.
30 fov_y: f32,
31 /// Width over height.
32 aspect: f32,
33 /// Near plane distance, Ångström; maps to clip depth 1.0.
34 near: f32,
35 /// Far plane distance, Ångström; maps to clip depth 0.0.
36 far: f32,
37 },
38 /// Orthographic projection with an explicit vertical extent.
39 Orthographic {
40 /// Full visible height, Ångström.
41 height: f32,
42 /// Width over height.
43 aspect: f32,
44 /// Near plane distance, Ångström; maps to clip depth 1.0.
45 near: f32,
46 /// Far plane distance, Ångström; maps to clip depth 0.0.
47 far: f32,
48 },
49}
50
51impl Projection {
52 /// The clip-from-view matrix with reversed [0, 1] depth.
53 #[must_use]
54 pub fn matrix(&self) -> Mat4 {
55 match *self {
56 Self::Perspective {
57 fov_y,
58 aspect,
59 near,
60 far,
61 } => {
62 // The right-handed [0, 1] projection maps near→0, far→1;
63 // swapping the plane arguments reverses the depth range.
64 glam::camera::rh::proj::directx::perspective(fov_y, aspect, far, near).into()
65 }
66 Self::Orthographic {
67 height,
68 aspect,
69 near,
70 far,
71 } => {
72 let half_h = height * 0.5;
73 let half_w = half_h * aspect;
74 glam::camera::rh::proj::directx::orthographic(
75 -half_w, half_w, -half_h, half_h, far, near,
76 )
77 .into()
78 }
79 }
80 }
81
82 /// Refits the near and far planes to a world-space bounding sphere seen
83 /// from `eye` looking toward the sphere. Called on camera change so the
84 /// depth range always brackets the scene tightly.
85 pub fn fit_near_far(&mut self, eye: Vec3, bound: &BoundingSphere) {
86 let distance = eye.distance(bound.center);
87 let pad = bound.radius.max(1.0) * 0.01;
88 let new_far = distance + bound.radius + pad;
89 let new_near = (distance - bound.radius - pad).max(MIN_NEAR);
90 match self {
91 Self::Perspective { near, far, .. } | Self::Orthographic { near, far, .. } => {
92 *near = new_near;
93 *far = new_far;
94 }
95 }
96 }
97
98 /// Width over height.
99 #[must_use]
100 pub fn aspect(&self) -> f32 {
101 match *self {
102 Self::Perspective { aspect, .. } | Self::Orthographic { aspect, .. } => aspect,
103 }
104 }
105
106 /// Updates the aspect ratio, preserving everything else.
107 pub fn set_aspect(&mut self, new_aspect: f32) {
108 match self {
109 Self::Perspective { aspect, .. } | Self::Orthographic { aspect, .. } => {
110 *aspect = new_aspect;
111 }
112 }
113 }
114}