9eead719b0
Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
143 lines
5.1 KiB
Rust
143 lines
5.1 KiB
Rust
//! [`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
|
||
);
|
||
}
|
||
}
|