Files
Oxide/engine/src/render/camera.rs
T
Homer Simpson 9eead719b0 Import Oxide engine (Stages 0–10) under MIT license
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>
2026-07-05 20:41:02 +02:00

143 lines
5.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! [`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.11000 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
);
}
}