Skip to content

Commit

Permalink
Add orbiting camera mode
Browse files Browse the repository at this point in the history
Rotates the camera around a target point.
  • Loading branch information
jdahlstrom committed Dec 29, 2024
1 parent 91bac87 commit 414b8d1
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 5 deletions.
2 changes: 1 addition & 1 deletion core/src/math/mat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -569,7 +569,7 @@ fn orient(new_y: Vec3, new_z: Vec3) -> Mat4x4<RealToReal<3>> {
let new_x = new_y.cross(&new_z);
assert!(
!new_x.len_sqr().approx_eq(&0.0),
"{new_y:?} × {new_z:?} too close to zero vector"
"{new_y:?} × {new_z:?} non-finite or too close to zero vector"
);
Mat4x4::from_basis(new_x, new_y, new_z)
}
Expand Down
20 changes: 17 additions & 3 deletions core/src/math/vec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,18 @@ impl<Sp, const N: usize> Vector<[f32; N], Sp> {
/// Panics in dev mode if `self` is a zero vector.
#[inline]
#[must_use]
pub fn normalize(&self) -> Self {
pub fn normalize(&self) -> Self
where
Self: Debug,
{
use super::float::f32;
#[cfg(feature = "std")]
use super::float::RecipSqrt;

let len_sqr = self.len_sqr();
debug_assert_ne!(len_sqr, 0.0, "cannot normalize a zero-length vector");
assert!(
len_sqr.is_finite() && !len_sqr.approx_eq(&0.0),
"cannot normalize a near-zero or non-finite vector: {self:?}"
);
*self * f32::recip_sqrt(len_sqr)
}

Expand All @@ -163,10 +168,19 @@ impl<Sp, const N: usize> Vector<[f32; N], Sp> {
// TODO f32 and f64 have inherent clamp methods because they're not Ord.
// A generic clamp for Sc: Ord would conflict with this one. There is
// currently no clean way to support both floats and impl Ord types.
// However, VecXi and VecXu should have their own inherent impls.
#[must_use]
pub fn clamp(&self, min: &Self, max: &Self) -> Self {
array::from_fn(|i| self[i].clamp(min[i], max[i])).into()
}

/// Returns `true` if every component of `self` is finite,
/// `false` otherwise.
///
/// See [`f32::is_finite()`].
pub fn is_finite(&self) -> bool {
self.0.iter().all(|c| c.is_finite())
}
}

impl<Sc, Sp, const N: usize> Vector<[Sc; N], Sp>
Expand Down
87 changes: 86 additions & 1 deletion core/src/render/cam.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ use crate::math::{
use crate::util::{rect::Rect, Dims};

#[cfg(feature = "fp")]
use crate::math::{orient_z, pt3, spherical, translate, turns, Angle, Vec3};
use crate::math::{
orient_z, pt3, rotate_x, rotate_y, spherical, translate, turns, Angle, Vec3,
};

use super::{
clip::ClipVec, Context, FragmentShader, NdcToScreen, RealToProj, Target,
Expand Down Expand Up @@ -57,6 +59,17 @@ pub type ViewToWorld = RealToReal<3, View, World>;
fn az_alt(az: Angle, alt: Angle) -> SphericalVec {
spherical(1.0, az, alt)
}
/// Orbiting camera mode.
///
/// This mode can rotate the camera around a fixed point, centered on the
/// point, and change the camera's distance to the target point.
#[derive(Copy, Clone, Debug)]
pub struct Orbit {
/// The camera's target point in **world** space.
pub target: Point3<World>,
/// The camera's direction in **world** space.
pub dir: SphericalVec,
}

//
// Inherent impls
Expand Down Expand Up @@ -188,6 +201,7 @@ impl FirstPerson {
}

/// Translates the camera by a relative offset in *view* space.
// TODO Explain that up/down is actually in world space (dir of gravity)
pub fn translate(&mut self, delta: Vec3<View>) {
// Zero azimuth means parallel to the x-axis
let fwd = az_alt(self.heading.az(), turns(0.0)).to_cart();
Expand All @@ -199,6 +213,50 @@ impl FirstPerson {
}
}

#[cfg(feature = "fp")]
impl Orbit {
/// Adds the azimuth and altitude given to the camera's current direction.
pub fn rotate(&mut self, az_delta: Angle, alt_delta: Angle) {
self.rotate_to(self.dir.az() + az_delta, self.dir.alt() + alt_delta);
}

/// Rotates the camera to the **world**-space azimuth and altitude given.
pub fn rotate_to(&mut self, az: Angle, alt: Angle) {
self.dir = spherical(
self.dir.r(),
az.wrap(turns(-0.5), turns(0.5)),
alt.clamp(turns(-0.25), turns(0.25)),
);
}

/// Translates the camera's target point in **world** space.
pub fn translate(&mut self, delta: Vec3<World>) {
self.target += delta;
}

/// Moves the camera towards or away from the target.
///
/// Multiplies the current camera distance by `factor`. The distance is
/// clamped to zero. Note that if the distance becomes zero, you cannot use
/// this method to make it nonzero again!
///
/// To set an absolute zoom distance, use [`zoom_to`][Self::zoom_to].
///
/// # Panics
/// If debug assertions are enabled, panics if `factor < 0`.
pub fn zoom(&mut self, factor: f32) {
debug_assert!(factor >= 0.0, "zoom factor cannot be negative");
self.zoom_to(self.dir.r() * factor);
}
/// Moves the camera to the given distance from the target.
///
/// The distance is clamped to 0.0.
pub fn zoom_to(&mut self, r: f32) {
debug_assert!(r >= 0.0, "camera distance cannot be negative");
self.dir[0] = r.max(0.0);
}
}

//
// Local trait impls
//
Expand All @@ -219,6 +277,23 @@ impl Mode for FirstPerson {
}
}

#[cfg(feature = "fp")]
impl Mode for Orbit {
fn world_to_view(&self) -> Mat4x4<WorldToView> {
// TODO Figure out how to do this with orient
//let fwd = self.dir.to_cart().normalize();
//let o = orient_z(fwd, Vec3::X - 0.1 * Vec3::Z);

// TODO Work out how and whether this is the correct inverse
// of the view-to-world transform
translate(self.target.to_vec().to()) // to world-space target
.then(&rotate_y(self.dir.az())) // to world-space az
.then(&rotate_x(self.dir.alt())) // to world-space alt
.then(&translate(self.dir.r() * Vec3::Z)) // view space
.to()
}
}

impl Mode for Mat4x4<WorldToView> {
fn world_to_view(&self) -> Mat4x4<WorldToView> {
*self
Expand All @@ -237,6 +312,16 @@ impl Default for FirstPerson {
}
}

#[cfg(feature = "fp")]
impl Default for Orbit {
fn default() -> Self {
Self {
target: Point3::default(),
dir: az_alt(turns(0.0), turns(0.0)),
}
}
}

#[cfg(test)]
mod tests {
// use super::*;
Expand Down

0 comments on commit 414b8d1

Please sign in to comment.