use amethyst_assets::PrefabData;
use amethyst_core::{
ecs::{Component, DenseVecStorage, Entity, Join, ReadExpect, System, WriteStorage},
Axis2,
};
use amethyst_derive::PrefabData;
use amethyst_error::Error;
use amethyst_rendy::camera::{Camera, Orthographic};
use amethyst_window::ScreenDimensions;
use derive_new::new;
use serde::{Deserialize, Serialize};
#[cfg(feature = "profiler")]
use thread_profiler::profile_scope;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Copy)]
pub struct CameraOrthoWorldCoordinates {
pub left: f32,
pub right: f32,
pub bottom: f32,
pub top: f32,
}
impl CameraOrthoWorldCoordinates {
pub fn normalized() -> CameraOrthoWorldCoordinates {
CameraOrthoWorldCoordinates {
left: 0.0,
right: 1.0,
bottom: 0.0,
top: 1.0,
}
}
pub fn aspect_ratio(&self) -> f32 {
self.width() / self.height()
}
pub fn width(&self) -> f32 {
self.right - self.left
}
pub fn height(&self) -> f32 {
(self.top - self.bottom).abs()
}
}
impl Default for CameraOrthoWorldCoordinates {
fn default() -> Self {
Self::normalized()
}
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, PrefabData, new)]
#[prefab(Component)]
pub struct CameraOrtho {
pub mode: CameraNormalizeMode,
pub world_coordinates: CameraOrthoWorldCoordinates,
#[new(default)]
aspect_ratio_cache: f32,
}
impl CameraOrtho {
pub fn normalized(mode: CameraNormalizeMode) -> CameraOrtho {
CameraOrtho {
mode,
world_coordinates: Default::default(),
aspect_ratio_cache: 0.0,
}
}
pub fn camera_offsets(&self, window_aspect_ratio: f32) -> (f32, f32, f32, f32) {
self.mode
.camera_offsets(window_aspect_ratio, &self.world_coordinates)
}
}
impl Component for CameraOrtho {
type Storage = DenseVecStorage<Self>;
}
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq)]
pub enum CameraNormalizeMode {
Lossy {
stretch_direction: Axis2,
},
Contain,
}
impl CameraNormalizeMode {
fn camera_offsets(
self,
window_aspect_ratio: f32,
desired_coordinates: &CameraOrthoWorldCoordinates,
) -> (f32, f32, f32, f32) {
match self {
CameraNormalizeMode::Lossy {
ref stretch_direction,
} => match stretch_direction {
Axis2::X => CameraNormalizeMode::lossy_x(window_aspect_ratio, desired_coordinates),
Axis2::Y => CameraNormalizeMode::lossy_y(window_aspect_ratio, desired_coordinates),
},
CameraNormalizeMode::Contain => {
let desired_aspect_ratio = desired_coordinates.aspect_ratio();
if window_aspect_ratio > desired_aspect_ratio {
CameraNormalizeMode::lossy_x(window_aspect_ratio, desired_coordinates)
} else {
CameraNormalizeMode::lossy_y(window_aspect_ratio, desired_coordinates)
}
}
}
}
fn lossy_x(
window_aspect_ratio: f32,
desired_coordinates: &CameraOrthoWorldCoordinates,
) -> (f32, f32, f32, f32) {
let offset = (window_aspect_ratio * desired_coordinates.height()
- desired_coordinates.width())
/ 2.0;
(
desired_coordinates.left - offset,
desired_coordinates.right + offset,
desired_coordinates.bottom,
desired_coordinates.top,
)
}
fn lossy_y(
window_aspect_ratio: f32,
desired_coordinates: &CameraOrthoWorldCoordinates,
) -> (f32, f32, f32, f32) {
let sign = if desired_coordinates.bottom > desired_coordinates.top {
-1.0
} else {
1.0
};
let offset = (desired_coordinates.width() / window_aspect_ratio
- desired_coordinates.height())
/ 2.0
* sign;
(
desired_coordinates.left,
desired_coordinates.right,
desired_coordinates.bottom - offset,
desired_coordinates.top + offset,
)
}
}
impl Default for CameraNormalizeMode {
fn default() -> Self {
CameraNormalizeMode::Contain
}
}
#[derive(Default, Debug)]
pub struct CameraOrthoSystem;
impl<'a> System<'a> for CameraOrthoSystem {
type SystemData = (
ReadExpect<'a, ScreenDimensions>,
WriteStorage<'a, Camera>,
WriteStorage<'a, CameraOrtho>,
);
#[allow(clippy::float_cmp)]
fn run(&mut self, (dimensions, mut cameras, mut ortho_cameras): Self::SystemData) {
#[cfg(feature = "profiler")]
profile_scope!("camera_ortho_system");
let aspect = dimensions.aspect_ratio();
for (camera, mut ortho_camera) in (&mut cameras, &mut ortho_cameras).join() {
if aspect != ortho_camera.aspect_ratio_cache {
ortho_camera.aspect_ratio_cache = aspect;
let offsets = ortho_camera.camera_offsets(aspect);
let (near, far) = if let Some(prev) = camera.projection().as_orthographic() {
(prev.near(), prev.far())
} else {
continue;
};
camera.set_projection(
Orthographic::new(offsets.0, offsets.1, offsets.2, offsets.3, near, far).into(),
);
}
}
}
}
#[cfg(test)]
mod test {
use crate::ortho_camera::{CameraNormalizeMode, CameraOrtho, CameraOrthoWorldCoordinates};
use super::Axis2;
#[test]
fn normal_camera_large_lossy_horizontal() {
let aspect = 2.0 / 1.0;
let cam = CameraOrtho::normalized(CameraNormalizeMode::Lossy {
stretch_direction: Axis2::X,
});
assert_eq!((-0.5, 1.5, 0.0, 1.0), cam.camera_offsets(aspect));
}
#[test]
fn normal_camera_large_lossy_vertical() {
let aspect = 2.0 / 1.0;
let cam = CameraOrtho::normalized(CameraNormalizeMode::Lossy {
stretch_direction: Axis2::Y,
});
assert_eq!((0.0, 1.0, 0.25, 0.75), cam.camera_offsets(aspect));
}
#[test]
fn normal_camera_high_lossy_horizontal() {
let aspect = 1.0 / 2.0;
let cam = CameraOrtho::normalized(CameraNormalizeMode::Lossy {
stretch_direction: Axis2::X,
});
assert_eq!((0.25, 0.75, 0.0, 1.0), cam.camera_offsets(aspect));
}
#[test]
fn normal_camera_high_lossy_vertical() {
let aspect = 1.0 / 2.0;
let cam = CameraOrtho::normalized(CameraNormalizeMode::Lossy {
stretch_direction: Axis2::Y,
});
assert_eq!((0.0, 1.0, -0.5, 1.5), cam.camera_offsets(aspect));
}
#[test]
fn normal_camera_square_lossy_horizontal() {
let aspect = 1.0;
let cam = CameraOrtho::normalized(CameraNormalizeMode::Lossy {
stretch_direction: Axis2::X,
});
assert_eq!((0.0, 1.0, 0.0, 1.0), cam.camera_offsets(aspect));
}
#[test]
fn normal_camera_square_lossy_vertical() {
let aspect = 1.0;
let cam = CameraOrtho::normalized(CameraNormalizeMode::Lossy {
stretch_direction: Axis2::Y,
});
assert_eq!((0.0, 1.0, 0.0, 1.0), cam.camera_offsets(aspect));
}
#[test]
fn normal_camera_large_contain() {
let aspect = 2.0 / 1.0;
let cam = CameraOrtho::normalized(CameraNormalizeMode::Contain);
assert_eq!((-0.5, 1.5, 0.0, 1.0), cam.camera_offsets(aspect));
}
#[test]
fn normal_camera_high_contain() {
let aspect = 1.0 / 2.0;
let cam = CameraOrtho::normalized(CameraNormalizeMode::Contain);
assert_eq!((0.0, 1.0, -0.5, 1.5), cam.camera_offsets(aspect));
}
#[test]
fn normal_camera_square_contain() {
let aspect = 1.0;
let cam = CameraOrtho::normalized(CameraNormalizeMode::Contain);
assert_eq!((0.0, 1.0, 0.0, 1.0), cam.camera_offsets(aspect));
}
#[test]
fn custom_camera_large_contain() {
let aspect = 2.0 / 1.0;
let camera_ortho_world_coordinates = CameraOrthoWorldCoordinates {
left: 0.,
right: 800.,
bottom: 0.,
top: 600.,
};
let cam = CameraOrtho::new(CameraNormalizeMode::Contain, camera_ortho_world_coordinates);
assert_eq!((-200.0, 1000.0, 0.0, 600.0), cam.camera_offsets(aspect));
}
#[test]
fn flipped_y_lossy_vertical() {
let aspect = 1.0 / 2.0;
let cam = CameraOrtho {
mode: CameraNormalizeMode::Contain,
world_coordinates: CameraOrthoWorldCoordinates {
left: 0.0,
right: 1.0,
top: 0.0,
bottom: 1.0,
},
aspect_ratio_cache: 0.0,
};
assert_eq!((0.0, 1.0, 1.5, -0.5), cam.camera_offsets(aspect));
}
#[test]
fn camera_square_contain() {
let aspect = 1.0;
let cam = CameraOrtho {
mode: CameraNormalizeMode::Contain,
world_coordinates: CameraOrthoWorldCoordinates {
left: 0.0,
right: 2.0,
top: 2.0,
bottom: 0.0,
},
aspect_ratio_cache: 0.0,
};
assert_eq!((0.0, 2.0, 0.0, 2.0), cam.camera_offsets(aspect));
}
#[test]
fn camera_large_contain() {
let aspect = 2.0 / 1.0;
let cam = CameraOrtho {
mode: CameraNormalizeMode::Contain,
world_coordinates: CameraOrthoWorldCoordinates {
left: 0.0,
right: 2.0,
top: 2.0,
bottom: 0.0,
},
aspect_ratio_cache: 0.0,
};
assert_eq!((-1.0, 3.0, 0.0, 2.0), cam.camera_offsets(aspect));
}
#[test]
fn camera_high_contain() {
let aspect = 1.0 / 2.0;
let cam = CameraOrtho {
mode: CameraNormalizeMode::Contain,
world_coordinates: CameraOrthoWorldCoordinates {
left: 0.0,
right: 2.0,
top: 2.0,
bottom: 0.0,
},
aspect_ratio_cache: 0.0,
};
assert_eq!((0.0, 2.0, -1.0, 3.0), cam.camera_offsets(aspect));
}
}