//! [`Camera`]: perspective projection plus view/projection matrix helpers. //! //! A camera holds only projection parameters; its *position* is a //! [`Transform`] supplied at render time (so a camera can be an entity in the //! scene). The view matrix is the inverse of that world transform. use serde::{Deserialize, Serialize}; use crate::layer::{Layer, LayerMask}; use crate::math::{Mat4, Transform}; /// A perspective camera. /// /// Stage 4 ships perspective projection only; orthographic and other /// projections can be added later without changing the renderer interface. /// /// A `Camera` is also a **reflected, addable component**: place one on an /// entity and it becomes the scene's viewpoint, dual-editable from the editor /// and scripts like any other component. (The runtime gathering of camera /// entities into the render path is wired in a later stage; today the editor /// drives its own viewport camera.) #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, crate::reflect::Reflect)] pub struct Camera { /// Vertical field of view, in radians. pub fov_y: f32, /// Near clip plane distance (> 0). pub z_near: f32, /// Far clip plane distance (> `z_near`). pub z_far: f32, /// The layers this camera renders. An entity is drawn only if its /// [`Layer`](crate::layer::Layer) membership intersects this mask. Defaults /// to [`LayerMask::ALL`] (sees everything) — e.g. a minimap or first-person /// view-model camera narrows it. The host applies it when gathering objects. pub visibility: LayerMask, } impl Default for Camera { /// A 60° vertical FOV camera with a 0.1–1000 unit depth range that sees all /// layers. fn default() -> Self { Self { fov_y: 60_f32.to_radians(), z_near: 0.1, z_far: 1000.0, visibility: LayerMask::ALL, } } } impl Camera { /// Creates a perspective camera from a vertical FOV (radians) and clip range, /// seeing all layers. pub fn perspective(fov_y: f32, z_near: f32, z_far: f32) -> Self { Self { fov_y, z_near, z_far, visibility: LayerMask::ALL, } } /// Sets the layer-visibility mask (builder style). pub fn with_visibility(mut self, visibility: LayerMask) -> Self { self.visibility = visibility; self } /// Whether this camera renders an entity with the given layer membership. pub fn sees(&self, layer: Layer) -> bool { layer.matches(self.visibility) } /// The projection matrix for a viewport of the given `aspect` (width / /// height). Uses a reversed-Z-free, `0..1` NDC depth range (wgpu/Vulkan/ /// DX/Metal convention). pub fn projection_matrix(&self, aspect: f32) -> Mat4 { Mat4::perspective_rh( self.fov_y, aspect.max(f32::EPSILON), self.z_near, self.z_far, ) } /// The view matrix for a camera placed at `view_transform` — i.e. the /// inverse of the camera's world transform. pub fn view_matrix(view_transform: &Transform) -> Mat4 { view_transform.to_matrix().inverse() } /// The combined view-projection matrix: `projection * view`. pub fn view_projection(&self, aspect: f32, view_transform: &Transform) -> Mat4 { self.projection_matrix(aspect) * Self::view_matrix(view_transform) } } #[cfg(test)] mod tests { use super::*; use crate::math::Vec3; #[test] fn visibility_filters_by_layer() { // Default camera sees every layer. let cam = Camera::default(); assert!(cam.sees(Layer::on(7))); // A camera restricted to the "UI" layer (3) only sees layer-3 entities. let ui_cam = Camera::default().with_visibility(LayerMask::layer(3)); assert!(ui_cam.sees(Layer::on(3))); assert!(!ui_cam.sees(Layer::on(0))); assert!(!ui_cam.sees(Layer::default())); // default layer 0 } #[test] fn projection_is_finite_and_depth_mapped() { let cam = Camera::default(); let proj = cam.projection_matrix(16.0 / 9.0); assert!(proj.is_finite()); // A point on the near plane maps to NDC z ~ 0, the far plane to ~ 1. let near = proj.project_point3(Vec3::new(0.0, 0.0, -cam.z_near)); let far = proj.project_point3(Vec3::new(0.0, 0.0, -cam.z_far)); assert!(near.z.abs() < 1e-3, "near z = {}", near.z); assert!((far.z - 1.0).abs() < 1e-3, "far z = {}", far.z); } #[test] fn view_matrix_moves_world_into_camera_space() { // Camera at +Z looking at the origin: the origin should sit straight // ahead, down the camera's -Z axis. let cam_tf = Transform::looking_at(Vec3::new(0.0, 0.0, 5.0), Vec3::ZERO, Vec3::Y); let view = Camera::view_matrix(&cam_tf); let origin_in_view = view.project_point3(Vec3::ZERO); assert!((origin_in_view.x).abs() < 1e-5); assert!((origin_in_view.y).abs() < 1e-5); assert!( (origin_in_view.z + 5.0).abs() < 1e-4, "z = {}", origin_in_view.z ); } }