// Copyright (C) 2019 Oscar Shrimpton // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) // any later version. // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for // more details. // You should have received a copy of the GNU General Public License along // with this program. If not, see . //! Things related to converting 3D world space to 2D screen space use std::iter::once; use std::f32::consts::PI; use hal::prelude::*; use hal::buffer::Usage; use na::{look_at_lh, perspective_lh_zo, radians}; use core::mem::ManuallyDrop; use crate::error; use crate::types::*; use super::buffer::{StagedBuffer, ModifiableBuffer}; use stockton_types::{Vector3, Matrix4}; use na::{Mat4, Vec4}; /// 90 degrees in radians const R90: f32 = PI / 2.0; /// 180 degrees in radians const R180: f32 = PI; fn euler_to_direction(euler: &Vector3) -> Vector3 { let pitch = euler.x; let yaw = euler.y; let _roll = euler.z; // TODO: Support camera roll Vector3::new( yaw.sin() * pitch.cos(), pitch.sin(), yaw.cos() * pitch.cos() ) } pub struct CameraSettings { /// Position of the camera (world units) pub position: Vector3, /// Rotation of the camera (euler angles in radians) pub rotation: Vector3, /// The up direction (normalized) pub up: Vector3, /// FOV (radians) pub fov: f32, /// Near clipping plane (world units) pub near: f32, /// Far clipping plane (world units) pub far: f32, } /// Holds settings related to the projection of world space to screen space /// Also holds maths for generating important matrices pub struct WorkingCamera<'a> { /// Settings for the camera settings: CameraSettings, /// Aspect ratio as a fraction aspect_ratio: f32, /// Layout of the descriptor set to pass to the shader pub descriptor_set_layout: ManuallyDrop, /// Buffer of memory used for passing data to shaders // TODO: Does this need to be staged? buffer: ManuallyDrop>, // TODO: Share descriptor pool with textures? descriptor_pool: ManuallyDrop, descriptor_set: DescriptorSet, /// If true, buffer needs updated is_dirty: bool } impl<'a> WorkingCamera<'a> { pub fn defaults(aspect_ratio: f32, device: &mut Device, adapter: &Adapter, command_queue: &mut CommandQueue, command_pool: &mut CommandPool) -> Result, error::CreationError> { WorkingCamera::with_settings(CameraSettings { position: Vector3::new(0.0, 0.0, 0.0), rotation: Vector3::new(0.0, R90, 0.0), up: Vector3::new(0.0, 1.0, 0.0), fov: f32::to_radians(90.0), near: 0.1, far: 1024.0, }, aspect_ratio, device, adapter, command_queue, command_pool) } /// Return a camera with default settings // TODO pub fn with_settings(settings: CameraSettings, aspect_ratio: f32, device: &mut Device, adapter: &Adapter, command_queue: &mut CommandQueue, command_pool: &mut CommandPool) -> Result, error::CreationError> { let descriptor_type = { use hal::pso::{DescriptorType, BufferDescriptorType, BufferDescriptorFormat}; DescriptorType::Buffer { ty: BufferDescriptorType::Uniform, format: BufferDescriptorFormat::Structured { dynamic_offset: false } } }; // Create set layout let descriptor_set_layout = unsafe { use hal::pso::{DescriptorSetLayoutBinding, ShaderStageFlags}; device.create_descriptor_set_layout( &[ DescriptorSetLayoutBinding { binding: 0, ty: descriptor_type, count: 1, stage_flags: ShaderStageFlags::VERTEX, immutable_samplers: false } ], &[], ) }.map_err(|_| error::CreationError::OutOfMemoryError)?; // Create pool and allocate set let (descriptor_pool, descriptor_set) = unsafe { use hal::pso::{DescriptorRangeDesc, DescriptorPoolCreateFlags}; let mut pool = device.create_descriptor_pool( 1, &[ DescriptorRangeDesc { ty: descriptor_type, count: 1 } ], DescriptorPoolCreateFlags::empty() ).map_err(|_| error::CreationError::OutOfMemoryError)?; let set = pool.allocate_set(&descriptor_set_layout).map_err(|_| error::CreationError::OutOfMemoryError)?; (pool, set) }; // Create buffer for descriptor let mut buffer = StagedBuffer::new(device, adapter, Usage::UNIFORM, 1)?; // Bind our buffer to our descriptor set unsafe { use hal::pso::{Descriptor, DescriptorSetWrite}; use hal::buffer::SubRange; device.write_descriptor_sets(once( DescriptorSetWrite { set: &descriptor_set, binding: 0, array_offset: 0, descriptors: once( Descriptor::Buffer(buffer.commit(device, command_queue, command_pool), SubRange::WHOLE) ) } )); } Ok(WorkingCamera { aspect_ratio, settings, descriptor_set_layout: ManuallyDrop::new(descriptor_set_layout), buffer: ManuallyDrop::new(buffer), descriptor_pool: ManuallyDrop::new(descriptor_pool), descriptor_set: descriptor_set, is_dirty: true }) } /// Returns a matrix that transforms from world space to screen space pub fn vp_matrix(&self) -> Matrix4 { // Get look direction from euler angles let direction = euler_to_direction(&self.settings.rotation); // Converts world space to camera space let view_matrix = look_at_lh( &self.settings.position, &(direction + &self.settings.position), &self.settings.up ); // Converts camera space to screen space let projection_matrix = { let mut temp = perspective_lh_zo( self.aspect_ratio, self.settings.fov, self.settings.near, self.settings.far ); // Vulkan's co-ord system is different from OpenGLs temp[(1, 1)] *= -1.0; temp }; // Chain them together into a single matrix projection_matrix * view_matrix } /// Update the aspect ratio pub fn update_aspect_ratio(&mut self, new: f32) { self.aspect_ratio = new; self.is_dirty = true; } /// Apply rotation of the camera /// `euler` should be euler angles in degrees pub fn rotate(&mut self, euler: Vector3) { // TODO self.settings.rotation += euler; // Clamp -pi/2 < pitch < pi/2 if self.settings.rotation.x > R90 { self.settings.rotation.x = R90; } else if self.settings.rotation.x < -R90 { self.settings.rotation.x = -R90; } // -pi < yaw <= pi if self.settings.rotation.y <= -R180 { self.settings.rotation.y = R180 - self.settings.rotation.y % -R180; } else if self.settings.rotation.y > 180.0 { self.settings.rotation.y = -R180 + self.settings.rotation.y % R180; } self.is_dirty = true; } /// Move the camera by `delta`, relative to the camera's rotation pub fn move_camera_relative(&mut self, delta: Vector3) { let rot_matrix = Mat4::from_euler_angles( -self.settings.rotation.x, self.settings.rotation.y, self.settings.rotation.z ); let new = rot_matrix * Vec4::new(delta.x, delta.y, delta.z, 1.0); self.settings.position.x += new.x; self.settings.position.y += new.y; self.settings.position.z += new.z; self.is_dirty = true; } /// Ensures the VP matrix on the GPU is up-to-date pub fn commit<'b>(&'b mut self, device: &Device, command_queue: &mut CommandQueue, command_pool: &mut CommandPool) -> &'b DescriptorSet { // Update buffer if needed if self.is_dirty { self.buffer[0] = self.vp_matrix(); self.buffer.commit(device, command_queue, command_pool); self.is_dirty = false; } // Return the descriptor set for matrices &self.descriptor_set } /// This should be called before dropping pub fn deactivate(mut self, device: &mut Device) -> () { unsafe { use core::ptr::read; ManuallyDrop::into_inner(read(&self.buffer)).deactivate(device); self.descriptor_pool.reset(); device.destroy_descriptor_pool(ManuallyDrop::into_inner(read(&self.descriptor_pool))); device.destroy_descriptor_set_layout(ManuallyDrop::into_inner(read(&self.descriptor_set_layout))); } } pub fn camera_pos(&self) -> Vector3 { self.settings.position } }