//! # Camera //! //! Map points from the IFS coordinate system to pixel coordinates. This is a lossy transformation. use bytemuck::{Pod, Zeroable}; use glam::{Affine2, IVec2, UVec2, Vec2, vec2}; use libm::powf; /// Settings used to map IFS coordinates to pixel coordinates. /// /// The camera is itself an affine transformation, capable of zoom, rotation, and translation /// of the IFS coordinates before rendering to the final image. #[derive(Copy, Clone, Pod, Zeroable)] #[repr(C)] pub struct Camera { dimensions: UVec2, transform: Affine2, } impl Camera { /// Construct a new camera for translating IFS coordinates to pixel coordinates. /// /// While the camera is implemented as a single affine transformation, it's helpful /// to express the transform steps individually. /// /// # Arguments /// /// * `dimensions` - Width and height of the output image (in pixels). /// * `center` - Location of the origin in IFS coordinates. Positive `x` shifts the image /// left, and positive `y` position shifts the image up. /// * `rotate` - Rotation angle (in radians) of IFS coordinates. Rotation is applied after the /// `center` translation, so it is about the new origin. /// * `zoom` - Zoom factor applied to IFS coordinates. IFS coordinates are scaled by /// `pow(2, zoom)`, so a zoom factor of 0 is the identity. /// * `scale` - Pixels per unit of IFS coordinates. This parameter is usually chosen such /// that the largest dimension will cover the range `[-2, 2]`, but values higher or lower /// can be used as a secondary zoom. pub fn new(dimensions: UVec2, center: Vec2, rotate: f32, zoom: Vec2, scale: Vec2) -> Camera { let ifs_center_transform = Affine2::from_translation(-center); let zoom_transform = Affine2::from_scale(vec2(powf(2.0, zoom.x), powf(2.0, zoom.y))); let scale_transform = Affine2::from_scale(scale); let rotate_transform = Affine2::from_angle(rotate); let image_center_transform = Affine2::from_translation((dimensions / 2).as_vec2()); let transform = image_center_transform * rotate_transform * scale_transform * zoom_transform * ifs_center_transform; Camera { dimensions, transform, } } /// Map a point from IFS coordinates to pixel coordinates. /// /// ``` /// # use glam::{vec2, ivec2, uvec2, Vec2}; /// # use crate::enkou_shaders::camera::Camera; /// // Output image is 600x600 pixels, centered at the origin, no rotation, no zoom, /// // and scaled such that it covers the range [-2, 2]. /// // Use the origin as the IFS coordinate, so the pixel coordinate is the center of the image /// let camera = Camera::new( /// uvec2(600, 600), /// Vec2::ZERO, /// 0.0, /// Vec2::ZERO, /// vec2(150.0, 150.0) /// ); /// assert_eq!(camera.transform_point(vec2(0.0, 0.0)), ivec2(300, 300)); /// ``` pub fn transform_point(&self, point: Vec2) -> IVec2 { self.transform.transform_point2(point).as_ivec2() } /// Map a point from IFS coordinates to pixel coordinates (like [`transform_point`](Camera::transform_point)), /// and check that the result is within the provided image dimensions. pub fn transform_point_to_image(&self, point: Vec2) -> Option { let pixel_coordinates = self.transform_point(point); if pixel_coordinates.x < 0 || pixel_coordinates.y < 0 || (pixel_coordinates.x as u32) >= self.dimensions.x || (pixel_coordinates.y as u32) >= self.dimensions.y { None } else { Some(pixel_coordinates.as_uvec2()) } } } /// Shader entry point for running the camera transformation over a list of IFS coordinates pub mod entry { use crate::camera::Camera; use glam::{Vec4, Vec4Swizzles}; use spirv_std::glam::IVec2; use spirv_std::spirv; /// Transform IFS coordinates to pixel coordinates #[spirv(compute(entry_point_name = "main_camera", threads(1)))] pub fn main_camera( #[spirv(storage_buffer, descriptor_set = 0, binding = 0)] camera: &Camera, #[spirv(storage_buffer, descriptor_set = 0, binding = 1)] coordinates_ifs: &[Vec4], #[spirv(storage_buffer, descriptor_set = 1, binding = 0)] coordinates_pixel: &mut [IVec2], ) { for i in 0..coordinates_ifs.len() { coordinates_pixel[i] = camera.transform_point(coordinates_ifs[i].xy()) } } } #[cfg(test)] mod test { use crate::camera::Camera; use glam::{Affine2, Vec2, ivec2, uvec2, vec2}; use libm::powf; #[test] pub fn manual_camera() { let starting_point = vec2(1.0, 1.0); // Move the origin; points move right and up by one unit, giving us (2.0, 2.0) let center = vec2(-1.0, -1.0); let point = starting_point - center; // Rotate about the new origin; points move counter-clockwise, giving us (-2.0, 2.0) let rotate = 90.0f32.to_radians(); let point = Affine2::from_angle(rotate).transform_point2(point); // Zoom in by a factor of 1; points will be twice as far from the origin, // giving us (-4.0, 4.0) let zoom = vec2(1.0, 1.0); let point = point * vec2(powf(2.0, zoom.x), powf(2.0, zoom.y)); // Apply scaling; scale 100 in a 1000 x 1000 image is an effective range // of [-5, 5] in IFS coordinates. // After scaling, the point is (-400.0, 400.0) let scale = vec2(100.0, 100.0); let point = point * scale; // Move the origin from (0, 0) to image center, // giving us (100.0, 900.0) let dimensions = uvec2(1000, 1000); let point = point.as_ivec2() + dimensions.as_ivec2() / 2; // Check that the camera implementation ends up at the same point let camera = Camera::new(dimensions, center, rotate, zoom, scale); // The camera is implemented by composing affine transforms, // which ends up with a slightly different result because of rounding. let error = camera.transform_point(starting_point) - point; assert!(error.x.abs() <= 1); assert!(error.y.abs() <= 1); } #[test] pub fn point_outside_camera() { // Scale 250 for an image 1000 x 1000 gives an effective range of [-2, 2] let camera = Camera::new( uvec2(1000, 1000), Vec2::ZERO, 0.0, Vec2::ZERO, vec2(250.0, 250.0), ); // Converting a point outside the effective range is legal, but outside the image bounds assert_eq!(camera.transform_point(vec2(3.0, 3.0)), ivec2(1250, 1250)); } #[test] pub fn point_outside_camera_negative() { // Scale 250 for an image 1000 x 1000 gives an effective range of [-2, 2] let camera = Camera::new( uvec2(1000, 1000), Vec2::ZERO, 0.0, Vec2::ZERO, vec2(250.0, 250.0), ); // Converting a point outside the effective range is legal, but outside the image bounds assert_eq!(camera.transform_point(vec2(-3.0, -3.0)), ivec2(-250, -250)); } #[test] pub fn aspect_ratio() { // Scale 100 for an image 1600 x 900 gives an effective X range of [-8, 8], // and effective Y range of [-4.5, 4.5] let camera = Camera::new( uvec2(1600, 900), Vec2::ZERO, 0.0, Vec2::ZERO, vec2(100.0, 100.0), ); // This point is inside the image width, but outside its height assert_eq!(camera.transform_point(vec2(6.0, 6.0)), ivec2(1400, 1050)); } }