use core::ops::Range;
#[cfg(feature = "fp")]
use crate::math::{
Angle, Vec3, orient_z, rotate_x, rotate_y, spherical, translate, turns,
};
use crate::math::{
Apply, Lerp, Mat4x4, Point3, SphericalVec, Vary, mat::RealToReal,
orthographic, perspective, pt2, viewport,
};
use crate::util::{Dims, rect::Rect};
use super::{
Clip, Context, NdcToScreen, RealToProj, Render, Shader, Target, View,
ViewToProj, World, WorldToView,
};
pub trait Transform {
fn world_to_view(&self) -> Mat4x4<WorldToView>;
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum Fov {
FocalRatio(f32),
Equiv35mm(f32),
#[cfg(feature = "fp")]
Horizontal(Angle),
#[cfg(feature = "fp")]
Vertical(Angle),
#[cfg(feature = "fp")]
Diagonal(Angle),
}
#[derive(Copy, Clone, Debug, Default)]
pub struct Camera<Tf> {
pub transform: Tf,
pub dims: Dims,
pub project: Mat4x4<ViewToProj>,
pub viewport: Mat4x4<NdcToScreen>,
}
#[derive(Copy, Clone, Debug)]
pub struct FirstPerson {
pub pos: Point3<World>,
pub heading: SphericalVec<World>,
}
pub type ViewToWorld = RealToReal<3, View, World>;
#[cfg(feature = "fp")]
fn az_alt<B>(az: Angle, alt: Angle) -> SphericalVec<B> {
spherical(1.0, az, alt)
}
#[derive(Copy, Clone, Debug)]
pub struct Orbit {
pub target: Point3<World>,
pub dir: SphericalVec<World>,
}
impl Fov {
pub fn focal_ratio(self, aspect_ratio: f32) -> f32 {
use Fov::*;
#[cfg(feature = "fp")]
fn ratio(a: Angle) -> f32 {
1.0 / (a / 2.0).tan()
}
match self {
FocalRatio(r) => r,
Equiv35mm(mm) => mm / (36.0 / 2.0),
#[cfg(feature = "fp")]
Horizontal(a) => ratio(a),
#[cfg(feature = "fp")]
Vertical(a) => ratio(a) / aspect_ratio,
#[cfg(feature = "fp")]
Diagonal(a) => {
use crate::math::float::f32;
let diag = f32::sqrt(1.0 + 1.0 / aspect_ratio / aspect_ratio);
ratio(a) * diag
}
}
}
}
impl Camera<()> {
pub fn new(dims: Dims) -> Self {
Self {
dims,
viewport: viewport(pt2(0, 0)..pt2(dims.0, dims.1)),
..Self::default()
}
}
pub fn transform<T: Transform>(self, tf: T) -> Camera<T> {
let Self { dims, project, viewport, .. } = self;
Camera {
transform: tf,
dims,
project,
viewport,
}
}
}
impl<T> Camera<T> {
pub fn viewport(self, bounds: impl Into<Rect<u32>>) -> Self {
let (w, h) = self.dims;
let Rect {
left: Some(l),
top: Some(t),
right: Some(r),
bottom: Some(b),
} = bounds.into().intersect(&(0..w, 0..h).into())
else {
unreachable!("bounded ∩ bounded should be bounded")
};
Self {
dims: (r.abs_diff(l), b.abs_diff(t)),
viewport: viewport(pt2(l, t)..pt2(r, b)),
..self
}
}
pub fn perspective(mut self, fov: Fov, near_far: Range<f32>) -> Self {
let aspect = self.dims.0 as f32 / self.dims.1 as f32;
self.project = perspective(fov.focal_ratio(aspect), aspect, near_far);
self
}
pub fn orthographic(mut self, bounds: Range<Point3>) -> Self {
self.project = orthographic(bounds.start, bounds.end);
self
}
}
impl<T: Transform> Camera<T> {
pub fn world_to_project(&self) -> Mat4x4<RealToProj<World>> {
self.transform.world_to_view().then(&self.project)
}
pub fn render<B, Prim, Vtx: Clone, Var: Lerp + Vary, Uni: Copy, Shd>(
&self,
prims: impl AsRef<[Prim]>,
verts: impl AsRef<[Vtx]>,
to_world: &Mat4x4<RealToReal<3, B, World>>,
shader: &Shd,
uniform: Uni,
target: &mut impl Target,
ctx: &Context,
) where
Prim: Render<Var> + Clone,
[<Prim>::Clip]: Clip<Item = Prim::Clip>,
Shd: for<'a> Shader<Vtx, Var, (&'a Mat4x4<RealToProj<B>>, Uni)>,
{
let tf = to_world.then(&self.world_to_project());
super::render(
prims.as_ref(),
verts.as_ref(),
shader,
(&tf, uniform),
self.viewport,
target,
ctx,
);
}
}
#[cfg(feature = "fp")]
impl FirstPerson {
pub fn new() -> Self {
Self {
pos: Point3::origin(),
heading: az_alt(turns(0.0), turns(0.0)),
}
}
pub fn look_at(&mut self, pt: Point3<World>) {
let head = (pt - self.pos).to_spherical();
self.rotate_to(head.az(), head.alt());
}
pub fn rotate(&mut self, delta_az: Angle, delta_alt: Angle) {
let head = self.heading;
self.rotate_to(head.az() + delta_az, head.alt() + delta_alt);
}
pub fn rotate_to(&mut self, az: Angle, alt: Angle) {
self.heading = az_alt(
az.wrap(turns(-0.5), turns(0.5)),
alt.clamp(turns(-0.25), turns(0.25)),
);
}
pub fn translate(&mut self, delta: Vec3<View>) {
let fwd = az_alt(self.heading.az(), turns(0.0)).to_cart();
let up = Vec3::Y;
let right = up.cross(&fwd);
let to_world = Mat4x4::from_linear(right, up, fwd);
self.pos += to_world.apply(&delta);
}
}
#[cfg(feature = "fp")]
impl Orbit {
pub fn rotate(&mut self, az_delta: Angle, alt_delta: Angle) {
self.rotate_to(self.dir.az() + az_delta, self.dir.alt() + alt_delta);
}
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)),
);
}
pub fn translate(&mut self, delta: Vec3<World>) {
self.target += delta;
}
pub fn zoom(&mut self, factor: f32) {
assert!(factor >= 0.0, "zoom factor cannot be negative");
self.zoom_to(self.dir.r() * factor);
}
pub fn zoom_to(&mut self, r: f32) {
assert!(r >= 0.0, "camera distance cannot be negative");
self.dir[0] = r.max(0.0);
}
}
#[cfg(feature = "fp")]
impl Transform for FirstPerson {
fn world_to_view(&self) -> Mat4x4<WorldToView> {
let &Self { pos, heading, .. } = self;
let fwd_move = az_alt(heading.az(), turns(0.0)).to_cart();
let fwd = heading.to_cart();
let right = Vec3::Y.cross(&fwd_move);
let transl = translate(-pos.to_vec().to());
let orient = orient_z(fwd.to(), right).transpose();
transl.then(&orient).to()
}
}
#[cfg(feature = "fp")]
impl Transform for Orbit {
fn world_to_view(&self) -> Mat4x4<WorldToView> {
translate(self.target.to_vec().to()) .then(&rotate_y(self.dir.az())) .then(&rotate_x(self.dir.alt())) .then(&translate(self.dir.r() * Vec3::Z)) .to()
}
}
impl Transform for Mat4x4<WorldToView> {
fn world_to_view(&self) -> Mat4x4<WorldToView> {
*self
}
}
#[cfg(feature = "fp")]
impl Default for FirstPerson {
fn default() -> Self {
Self::new()
}
}
#[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::*;
use Fov::*;
#[test]
fn camera_tests_here() {
}
#[test]
fn fov_focal_ratio() {
assert_eq!(FocalRatio(2.345).focal_ratio(1.0), 2.345);
assert_eq!(FocalRatio(2.345).focal_ratio(2.0), 2.345);
assert_eq!(Equiv35mm(18.0).focal_ratio(1.0), 1.0);
assert_eq!(Equiv35mm(36.0).focal_ratio(1.5), 2.0);
}
#[cfg(feature = "fp")]
#[test]
fn angle_of_view_focal_ratio_with_unit_aspect_ratio() {
use crate::math::degs;
use core::f32::consts::SQRT_2;
const SQRT_3: f32 = 1.7320509;
assert_eq!(Horizontal(degs(60.0)).focal_ratio(1.0), SQRT_3);
assert_eq!(Vertical(degs(60.0)).focal_ratio(1.0), SQRT_3);
assert_eq!(Diagonal(degs(60.0)).focal_ratio(1.0), SQRT_3 * SQRT_2);
}
#[cfg(feature = "fp")]
#[test]
fn angle_of_view_focal_ratio_with_other_aspect_ratio() {
use crate::math::degs;
const SQRT_3: f32 = 1.7320509;
assert_eq!(Horizontal(degs(60.0)).focal_ratio(SQRT_3), SQRT_3);
assert_eq!(Vertical(degs(60.0)).focal_ratio(SQRT_3), 1.0);
assert_eq!(Diagonal(degs(60.0)).focal_ratio(SQRT_3), 2.0);
}
}