use std;
use math_utils as math;
use math_utils::{approx, num, vek};
use crate::graphics;
const PERSPECTIVE_INITIAL_FOVY : math::Deg <f32> = math::Deg (90.0);
const PERSPECTIVE_NEAR_PLANE : f32 = 0.1;
const PERSPECTIVE_FAR_PLANE : f32 = 1000.0;
const ORTHOGRAPHIC_PIXEL_SCALE : f32 = 64.0;
const ORTHOGRAPHIC_NEAR_PLANE : f32 = 0.0;
const ORTHOGRAPHIC_FAR_PLANE : f32 = 1000.0;
#[derive(Clone, Debug, PartialEq)]
pub struct Camera3d {
pose : math::Pose3 <f32>,
orientation : math::Rotation3 <f32>,
transform_mat_world_to_view : math::Matrix4 <f32>,
projection3d : Projection3d
}
#[derive(Clone, Debug, PartialEq)]
pub struct Projection3d {
viewport_width : u16,
viewport_height : u16,
inner : Projection3dInner
}
#[derive(Clone, Debug, PartialEq)]
pub enum Projection3dInner {
Perspective {
perspective_fov : PerspectiveFov <f32>,
mat : math::Matrix4 <f32>
},
Orthographic {
zoom : f32,
ortho : vek::FrustumPlanes <f32>,
mat : math::Matrix4 <f32>
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct PerspectiveFov <S> {
pub fovy : math::Rad <S>,
pub aspect : S,
pub near : S,
pub far : S
}
#[inline]
pub fn aspect_ratio (viewport_width : u16, viewport_height : u16) -> f32 {
viewport_width as f32 / viewport_height as f32
}
pub fn transform_mat_world_to_view (
view_position : &math::Point3 <f32>,
view_orientation : &math::Rotation3 <f32>
) -> math::Matrix4 <f32> {
let eye = view_position;
let target = *view_position + view_orientation.cols.y;
let up = view_orientation.cols.z;
math::Matrix4::<f32>::look_at_rh (eye.0, target.0, up)
}
fn compute_ortho (viewport_width : u16, viewport_height : u16, zoom : f32)
-> vek::FrustumPlanes <f32>
{
let half_scaled_width = (0.5 * (viewport_width as f32 / zoom))
/ ORTHOGRAPHIC_PIXEL_SCALE;
let half_scaled_height = (0.5 * (viewport_height as f32 / zoom))
/ ORTHOGRAPHIC_PIXEL_SCALE;
vek::FrustumPlanes {
left: -half_scaled_width,
right: half_scaled_width,
bottom: -half_scaled_height,
top: half_scaled_height,
near: ORTHOGRAPHIC_NEAR_PLANE,
far: ORTHOGRAPHIC_FAR_PLANE
}
}
impl Camera3d {
#[inline]
pub fn new (viewport_width : u16, viewport_height : u16) -> Self {
Self::with_pose (
viewport_width, viewport_height, Default::default())
}
pub fn with_pose (
viewport_width : u16,
viewport_height : u16,
pose : math::Pose3 <f32>
) -> Self {
let orientation = pose.angles.into();
let transform_mat_world_to_view =
transform_mat_world_to_view (&pose.position, &orientation);
let projection3d = Projection3d::perspective (
viewport_width, viewport_height, PERSPECTIVE_INITIAL_FOVY.into());
Camera3d {
pose,
orientation,
transform_mat_world_to_view,
projection3d
}
}
pub const fn position (&self) -> math::Point3 <f32> {
self.pose.position
}
pub const fn yaw (&self) -> math::Rad <f32> {
self.pose.angles.yaw.angle()
}
pub const fn pitch (&self) -> math::Rad <f32> {
self.pose.angles.pitch.angle()
}
pub const fn roll (&self) -> math::Rad <f32> {
self.pose.angles.roll.angle()
}
pub const fn orientation (&self) -> math::Rotation3 <f32> {
self.orientation
}
pub const fn transform_mat_world_to_view (&self) -> math::Matrix4 <f32> {
self.transform_mat_world_to_view
}
pub const fn projection (&self) -> &Projection3d {
&self.projection3d
}
#[inline]
pub fn set_viewport_dimensions (&mut self,
viewport_width : u16, viewport_height : u16
) {
self.projection3d.set_viewport_dimensions (
viewport_width, viewport_height);
}
pub fn set_position (&mut self, position : math::Point3 <f32>) {
if self.pose.position != position {
self.pose.position = position;
self.compute_transform();
}
}
pub fn set_orientation (&mut self, orientation : math::Rotation3 <f32>) {
if self.orientation != orientation {
self.orientation = orientation;
self.compute_angles();
}
}
pub fn scale_fovy_or_zoom (&mut self, scale : f32) {
self.projection3d.scale_fovy_or_zoom (scale)
}
pub fn rotate (&mut self,
dyaw : math::Rad <f32>, dpitch : math::Rad <f32>, droll : math::Rad <f32>
) {
use num::Zero;
self.pose.angles.yaw += dyaw;
self.pose.angles.pitch += dpitch;
self.pose.angles.roll += droll;
if !dyaw.is_zero() || !dpitch.is_zero() || !droll.is_zero() {
self.compute_orientation();
}
}
pub fn move_local_xy (&mut self, dx : f32, dy : f32, dz : f32) {
if dx != 0.0 || dy != 0.0 || dz != 0.0 {
let xy_basis = math::Matrix3::rotation_z (self.pose.angles.yaw.angle().0);
self.pose.position +=
(dx * xy_basis.cols.x) + (dy * xy_basis.cols.y) + (dz * xy_basis.cols.z);
self.compute_transform();
}
}
pub fn look_at (&mut self, target : math::Point3 <f32>) {
let orientation =
math::Rotation3::look_at ((target - self.pose.position).into());
self.set_orientation (orientation);
}
#[inline]
pub fn view_mats (&self) -> ([[f32; 4]; 4], [[f32; 4]; 4]) {
( self.transform_mat_world_to_view.into_col_arrays(),
self.projection3d.as_matrix().into_col_arrays()
)
}
pub fn to_orthographic (&mut self, zoom : f32) {
self.projection3d.to_orthographic (zoom)
}
pub fn to_perspective (&mut self, fovy : math::Rad <f32>) {
self.projection3d.to_perspective (fovy)
}
#[inline]
fn compute_transform (&mut self) {
self.transform_mat_world_to_view =
transform_mat_world_to_view (&self.pose.position, &self.orientation);
}
#[inline]
fn compute_orientation (&mut self) {
self.orientation = self.pose.angles.into();
self.compute_transform();
}
#[inline]
fn compute_angles (&mut self) {
self.pose.angles = self.orientation.into();
self.compute_transform();
}
}
impl Projection3d {
pub fn perspective (
viewport_width : u16, viewport_height : u16, fovy : math::Rad <f32>
) -> Self {
let inner
= Projection3dInner::perspective (viewport_width, viewport_height, fovy);
Projection3d { viewport_width, viewport_height, inner }
}
pub fn orthographic (viewport_width : u16, viewport_height : u16, zoom : f32)
-> Self
{
let inner
= Projection3dInner::orthographic (viewport_width, viewport_height, zoom);
Projection3d { viewport_width, viewport_height, inner }
}
pub const fn viewport_width (&self) -> u16 {
self.viewport_width
}
pub const fn viewport_height (&self) -> u16 {
self.viewport_height
}
#[inline]
pub const fn as_matrix (&self) -> &math::Matrix4 <f32> {
self.inner.as_matrix()
}
pub const fn is_orthographic (&self) -> bool {
match self.inner {
Projection3dInner::Orthographic {..} => true,
Projection3dInner::Perspective {..} => false
}
}
pub const fn is_perspective (&self) -> bool {
match self.inner {
Projection3dInner::Orthographic {..} => false,
Projection3dInner::Perspective {..} => true
}
}
pub fn to_orthographic (&mut self, zoom : f32) {
match self.inner {
ref mut inner@Projection3dInner::Orthographic { .. } => {
inner.set_orthographic_zoom (
self.viewport_width, self.viewport_height, zoom);
}
ref mut inner@Projection3dInner::Perspective { .. } => {
*inner = Projection3dInner::orthographic (
self.viewport_width, self.viewport_height, 1.0);
}
}
}
pub fn to_perspective (&mut self, fovy : math::Rad <f32>) {
match self.inner {
ref mut inner@Projection3dInner::Orthographic { .. } => {
*inner = Projection3dInner::perspective (
self.viewport_width, self.viewport_height, fovy);
}
ref mut inner@Projection3dInner::Perspective { .. } => {
inner.set_perspective_fovy (fovy);
}
}
}
pub fn set_viewport_dimensions (&mut self,
viewport_width : u16, viewport_height : u16
) {
self.viewport_width = viewport_width;
self.viewport_height = viewport_height;
self.inner.update_viewport_dimensions (viewport_width, viewport_height);
}
pub fn scale_fovy_or_zoom (&mut self, scale : f32) {
assert!(0.0 < scale);
if scale != 1.0 {
use approx::AbsDiffEq;
match self.inner {
Projection3dInner::Perspective {
ref mut perspective_fov, ref mut mat
} => {
let max_fovy = math::Rad (
std::f32::consts::PI - f32::default_epsilon()
);
let min_fovy = math::Rad (f32::default_epsilon());
perspective_fov.fovy *= scale;
debug_assert!(math::Rad (0.0) <= perspective_fov.fovy);
if max_fovy < perspective_fov.fovy {
perspective_fov.fovy = max_fovy;
} else if perspective_fov.fovy < min_fovy {
perspective_fov.fovy = min_fovy;
}
*mat = graphics::projection_mat_perspective (perspective_fov);
}
Projection3dInner::Orthographic {
ref mut zoom, ref mut ortho, ref mut mat
} => {
*zoom *= scale;
debug_assert!(0.0 <= *zoom);
if *zoom < f32::default_epsilon() {
*zoom = f32::default_epsilon();
}
*ortho = compute_ortho (
self.viewport_width, self.viewport_height, *zoom);
*mat = graphics::projection_mat_orthographic (ortho);
}
}
}
}
}
impl Projection3dInner {
fn perspective (
viewport_width : u16, viewport_height : u16, fovy : math::Rad <f32>
) -> Self {
use approx::AbsDiffEq;
assert!(0 < viewport_width);
assert!(0 < viewport_height);
assert!(0.0 < fovy.0);
assert!(fovy.0 <= std::f32::consts::PI - f32::default_epsilon());
let perspective_fov = PerspectiveFov {
fovy,
aspect: aspect_ratio (viewport_width, viewport_height),
near: PERSPECTIVE_NEAR_PLANE,
far: PERSPECTIVE_FAR_PLANE
};
let mat = graphics::projection_mat_perspective (&perspective_fov);
Projection3dInner::Perspective { perspective_fov, mat }
}
fn orthographic (viewport_width : u16, viewport_height : u16, zoom : f32)
-> Self
{
assert!(0 < viewport_width);
assert!(0 < viewport_height);
assert!(0.0 < zoom);
let ortho = compute_ortho (viewport_width, viewport_height, zoom);
let mat = graphics::projection_mat_orthographic (&ortho);
Projection3dInner::Orthographic { zoom, ortho, mat }
}
const fn as_matrix (&self) -> &math::Matrix4 <f32> {
match self {
Projection3dInner::Perspective { mat, .. } |
Projection3dInner::Orthographic { mat, .. } => mat
}
}
fn update_viewport_dimensions (&mut self,
viewport_width : u16, viewport_height : u16
) {
assert!(0 < viewport_width);
assert!(0 < viewport_height);
match self {
Projection3dInner::Perspective { perspective_fov, mat } => {
perspective_fov.aspect = aspect_ratio (viewport_width, viewport_height);
*mat = graphics::projection_mat_perspective (perspective_fov);
}
Projection3dInner::Orthographic { zoom, ortho, mat } => {
*ortho = compute_ortho (viewport_width, viewport_height, *zoom);
*mat = graphics::projection_mat_orthographic (ortho);
}
}
}
fn set_perspective_fovy (&mut self, new_fovy : math::Rad <f32>) {
use approx::AbsDiffEq;
let max_fovy = math::Rad (std::f32::consts::PI - f32::default_epsilon());
assert!(f32::default_epsilon() < new_fovy.0);
assert!(new_fovy <= max_fovy);
match *self {
Projection3dInner::Perspective {
ref mut perspective_fov, ref mut mat
} => {
if perspective_fov.fovy != new_fovy {
perspective_fov.fovy = new_fovy;
*mat = graphics::projection_mat_perspective (perspective_fov);
}
}
Projection3dInner::Orthographic {..} =>
unreachable!("expected perspective projection")
}
}
fn set_orthographic_zoom (&mut self,
viewport_width : u16, viewport_height : u16, new_zoom : f32
) {
use approx::AbsDiffEq;
assert!(f32::default_epsilon() < new_zoom);
match *self {
Projection3dInner::Orthographic {
ref mut zoom, ref mut ortho, ref mut mat
} => {
if *zoom != new_zoom {
*zoom = new_zoom;
*ortho = compute_ortho (viewport_width, viewport_height, *zoom);
*mat = graphics::projection_mat_orthographic (ortho);
}
}
Projection3dInner::Perspective {..} =>
unreachable!("expected orthographic projection")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use math;
use approx;
#[test]
fn camera3d_look_at() {
use approx::AbsDiffEq;
let epsilon = 4.0 * f32::default_epsilon();
let mut camera = Camera3d::new (640, 480);
camera.look_at ([0.0, 1.0, 0.0].into());
approx::assert_relative_eq!(
camera.orientation().mat(),
math::Rotation3::identity().mat());
camera.look_at ([0.0, -1.0, 0.0].into());
approx::assert_relative_eq!(
camera.orientation().mat(),
math::Rotation3::from_angle_z (math::Turn (0.5).into()).mat(),
epsilon=epsilon);
}
}