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>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
//! [`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
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user