codecraft 0.2.0

A minimalist 3D game engine built on parts of Bevy (ECS, color) with wgpu and winit: OpenPBR materials, clustered lighting, a yakui-drawn UI, audio and gamepad haptics; its binary maps any folder, and the symbols of its Rust files, as a 3D wall of boxes
Documentation
//! Which parts of the frame the world is drawn into, and from where: a view is
//! a rectangle plus a camera, and the renderer draws the world once per view.
//!
//! ```no_run
//! # use codecraft::{Camera, Views};
//! # fn demo(one: Camera, two: Camera) {
//! // Two players at one screen, with the map they share over the join.
//! let views = Views::split(one, two, 1920.0, 1080.0).with_minimap(Camera::default());
//! # }
//! ```
use glam::Vec3;

use crate::ecs::Resource;
use crate::sceneobjects::cameras::Camera;
use crate::ui::Rect;

const MINIMAP_HEIGHT: f32 = 0.25;

const MINIMAP_MARGIN: f32 = 0.02;

/// One view onto the world: where it lands on the frame, and the camera that draws it.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct View {
    /// In pixels, from the top left of the frame.
    pub rect: Rect,
    pub camera: Camera,
    /// Whether this view is one of the tiles that cover the frame (what a resize divides up) or an overlay laid over them.
    pub tiles: bool,
}

impl View {
    /// A view that is one of the tiles covering the frame.
    pub fn new(rect: Rect, camera: Camera) -> Self {
        Self {
            rect,
            camera,
            tiles: true,
        }
    }

    /// A view drawn over the tiles, keeping what they left around it.
    pub fn overlay(rect: Rect, camera: Camera) -> Self {
        Self {
            rect,
            camera,
            tiles: false,
        }
    }

    /// How wide against how tall, for the projection.
    pub fn aspect(&self) -> f32 {
        self.rect.width / self.rect.height.max(1.0)
    }

    /// Where a point in the world lands on the frame in pixels through this view's camera.
    /// `None` when behind the camera, past the far plane, or outside this view's rectangle.
    pub fn project(&self, world: Vec3) -> Option<(f32, f32)> {
        let (x, y) = self.project_unclipped(world)?;
        self.rect.contains(x, y).then_some((x, y))
    }

    /// [`View::project`] without the rectangle check: where a point in front of the camera
    /// lands even when that is off the edge, so things half in view can still be placed.
    pub fn project_unclipped(&self, world: Vec3) -> Option<(f32, f32)> {
        let clip = self.camera.view_proj(self.aspect()) * world.extend(1.0);
        // Perspective: behind is w <= 0. Orthographic: w is 1 everywhere, behind is z < 0.
        if clip.w <= 0.0 || clip.z < 0.0 {
            return None;
        }
        let ndc = clip.truncate() / clip.w;
        if ndc.z > 1.0 {
            return None;
        }
        let x = self.rect.x + (ndc.x + 1.0) * 0.5 * self.rect.width;
        let y = self.rect.y + (1.0 - ndc.y) * 0.5 * self.rect.height;
        Some((x, y))
    }

    /// Whether a screen point is within `margin` pixels of this view.
    pub fn is_near(&self, x: f32, y: f32, margin: f32) -> bool {
        x >= self.rect.x - margin
            && x <= self.rect.x + self.rect.width + margin
            && y >= self.rect.y - margin
            && y <= self.rect.y + self.rect.height + margin
    }
}

/// Every view the frame is drawn from, in the order they are drawn. Never empty.
#[derive(Resource, Clone, Debug, PartialEq)]
pub struct Views(Vec<View>);

impl Default for Views {
    fn default() -> Self {
        Self::one(Camera::default())
    }
}

impl Views {
    /// The whole frame, from one camera; the rect is filled in by the renderer.
    pub fn one(camera: Camera) -> Self {
        Self(vec![View::new(Rect::new(0.0, 0.0, 0.0, 0.0), camera)])
    }

    /// Two players at one screen, side by side.
    pub fn split(left: Camera, right: Camera, width: f32, height: f32) -> Self {
        let half = width * 0.5;
        Self(vec![
            View::new(Rect::new(0.0, 0.0, half, height), left),
            View::new(Rect::new(half, 0.0, width - half, height), right),
        ])
    }

    /// Adds a shared map laid over the bottom centre of the frame.
    pub fn with_minimap(mut self, camera: Camera) -> Self {
        let frame = self.frame();
        let side = frame.height * MINIMAP_HEIGHT;
        let margin = frame.height * MINIMAP_MARGIN;
        self.0.push(View::overlay(
            Rect::new(
                frame.center_x() - side * 0.5,
                frame.height - side - margin,
                side,
                side,
            ),
            camera,
        ));
        self
    }

    fn frame(&self) -> Rect {
        self.0.iter().filter(|view| view.tiles).fold(
            Rect::new(0.0, 0.0, 0.0, 0.0),
            |frame, view| {
                Rect::new(
                    0.0,
                    0.0,
                    frame.width.max(view.rect.x + view.rect.width),
                    frame.height.max(view.rect.y + view.rect.height),
                )
            },
        )
    }

    pub fn iter(&self) -> impl Iterator<Item = &View> {
        self.0.iter()
    }

    pub fn len(&self) -> usize {
        self.0.len()
    }

    pub fn is_empty(&self) -> bool {
        self.0.is_empty()
    }

    /// The camera a click or a ray should be read against: the first view's.
    pub fn camera(&self) -> Camera {
        self.0
            .first()
            .map(|view| view.camera)
            .unwrap_or_else(Camera::default)
    }

    /// Points the frame's one view from `camera`, and says whether it did; with several views it leaves them alone.
    pub fn set_camera(&mut self, camera: Camera) -> bool {
        match self.0.as_mut_slice() {
            [only] => {
                only.camera = camera;
                true
            }
            _ => false,
        }
    }

    /// Which view a point on the frame is inside, latest (topmost) first.
    pub fn at(&self, x: f32, y: f32) -> Option<&View> {
        self.0.iter().rev().find(|view| view.rect.contains(x, y))
    }

    /// Fills in any view that has no size yet and rescales the rest to the frame; called by the renderer every frame.
    pub fn fit(&mut self, width: f32, height: f32) {
        let frame = self.frame();
        let (was_width, was_height) = (frame.width, frame.height);
        for view in &mut self.0 {
            if was_width <= 0.0 || was_height <= 0.0 {
                view.rect = Rect::new(0.0, 0.0, width, height);
                continue;
            }
            let (sx, sy) = (width / was_width, height / was_height);
            view.rect = Rect::new(
                view.rect.x * sx,
                view.rect.y * sy,
                view.rect.width * sx,
                view.rect.height * sy,
            );
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn camera(x: f32) -> Camera {
        Camera::looking_at(glam::Vec3::new(x, 0.0, 0.0), glam::Vec3::ZERO)
    }

    #[test]
    fn one_view_fills_the_frame() {
        let mut views = Views::one(camera(0.0));
        views.fit(1920.0, 1080.0);

        assert_eq!(views.len(), 1);
        let view = views.iter().next().unwrap();
        assert_eq!(view.rect, Rect::new(0.0, 0.0, 1920.0, 1080.0));
        assert!(view.tiles, "it is the frame");
    }

    #[test]
    fn a_split_screen_covers_the_frame_exactly() {
        let mut views = Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0);
        views.fit(1920.0, 1080.0);
        let halves: Vec<Rect> = views.iter().map(|view| view.rect).collect();

        assert_eq!(halves[0], Rect::new(0.0, 0.0, 960.0, 1080.0));
        assert_eq!(halves[1], Rect::new(960.0, 0.0, 960.0, 1080.0));
        assert_eq!(
            halves[0].x + halves[0].width,
            halves[1].x,
            "they have to meet exactly",
        );
        assert_eq!(halves[1].x + halves[1].width, 1920.0, "and reach the edge");
    }

    #[test]
    fn an_odd_width_still_covers_every_pixel() {
        let mut views = Views::split(camera(-1.0), camera(1.0), 1921.0, 1080.0);
        views.fit(1921.0, 1080.0);
        let halves: Vec<Rect> = views.iter().map(|view| view.rect).collect();

        assert_eq!(halves[0].x + halves[0].width, halves[1].x);
        assert_eq!(halves[1].x + halves[1].width, 1921.0);
    }

    #[test]
    fn each_half_knows_it_is_half_as_wide() {
        let mut views = Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0);
        views.fit(1920.0, 1080.0);

        for view in views.iter() {
            assert!(
                (view.aspect() - 960.0 / 1080.0).abs() < 1e-5,
                "a half-width view is not the frame's shape: {}",
                view.aspect(),
            );
        }
    }

    #[test]
    fn the_minimap_sits_over_the_join_at_the_bottom() {
        let mut views =
            Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0).with_minimap(camera(0.0));
        views.fit(1920.0, 1080.0);

        let map = views.iter().last().unwrap();
        assert!(!map.tiles, "it is laid over the two, not cut into them");
        assert!(
            (map.rect.center_x() - 960.0).abs() < 1e-4,
            "centred on the join: {:?}",
            map.rect,
        );
        assert!(
            map.rect.y + map.rect.height < 1080.0,
            "floating off the bottom edge rather than glued to it",
        );
        assert_eq!(map.rect.width, map.rect.height, "square, like the ground");
    }

    #[test]
    fn the_minimap_overlaps_both_players() {
        let mut views =
            Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0).with_minimap(camera(0.0));
        views.fit(1920.0, 1080.0);

        let rects: Vec<Rect> = views.iter().map(|view| view.rect).collect();
        let (left, right, map) = (rects[0], rects[1], rects[2]);
        assert!(map.x < left.x + left.width, "it reaches into the left half");
        assert!(map.x + map.width > right.x, "and into the right one");
    }

    #[test]
    fn a_resize_keeps_the_layout() {
        let mut views =
            Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0).with_minimap(camera(0.0));
        views.fit(1920.0, 1080.0);
        views.fit(1280.0, 720.0);

        let rects: Vec<Rect> = views.iter().map(|view| view.rect).collect();
        assert_eq!(rects[0], Rect::new(0.0, 0.0, 640.0, 720.0));
        assert_eq!(rects[1], Rect::new(640.0, 0.0, 640.0, 720.0));
        assert!(
            (rects[2].center_x() - 640.0).abs() < 1e-4,
            "the map stays over the join: {:?}",
            rects[2],
        );
    }

    #[test]
    fn a_point_belongs_to_the_view_drawn_last() {
        let mut views =
            Views::split(camera(-1.0), camera(1.0), 1920.0, 1080.0).with_minimap(camera(0.0));
        views.fit(1920.0, 1080.0);
        let map = views.iter().last().unwrap().rect;

        let over_map = views.at(map.center_x(), map.center_y()).unwrap().rect;
        assert_eq!(over_map, map, "the map wins where it covers the players");

        let left = views.at(100.0, 100.0).unwrap().rect;
        assert_eq!(left.x, 0.0, "and away from it the player view answers");
        assert!(views.at(-1.0, 100.0).is_none(), "off the frame is nobody's");
    }

    /// Two players looking at the origin, one from -X and one from +Z, on a 1920x1080 frame.
    fn facing() -> (View, View) {
        let from_z = Camera::looking_at(Vec3::new(0.0, 0.0, 10.0), Vec3::ZERO);
        let mut views = Views::split(camera(-10.0), from_z, 1920.0, 1080.0);
        views.fit(1920.0, 1080.0);
        let mut views = views.iter().copied();
        (views.next().unwrap(), views.next().unwrap())
    }

    #[test]
    fn what_a_camera_looks_at_lands_in_the_middle_of_its_own_view() {
        let (left, right) = facing();
        let (x, y) = left.project(Vec3::ZERO).unwrap();
        assert!(
            (x - 480.0).abs() < 1e-2 && (y - 540.0).abs() < 1e-2,
            "({x}, {y})"
        );
        let (x, y) = right.project(Vec3::ZERO).unwrap();
        assert!(
            (x - 1440.0).abs() < 1e-2 && (y - 540.0).abs() < 1e-2,
            "({x}, {y})"
        );
    }

    #[test]
    fn up_in_the_world_is_up_the_screen() {
        let (left, _) = facing();
        let (x, y) = left.project(Vec3::new(0.0, 1.0, 0.0)).unwrap();
        assert!(y < 540.0, "above the target came out at y {y}");
        assert!((x - 480.0).abs() < 1e-2, "and dead ahead: x {x}");
    }

    #[test]
    fn a_point_behind_the_camera_is_nowhere_on_screen() {
        let (left, _) = facing();
        assert_eq!(left.project(Vec3::new(-20.0, 0.0, 0.0)), None);
        assert_eq!(left.project(Vec3::new(-10.0, 0.0, 0.0)), None);
    }

    #[test]
    fn a_point_past_the_far_plane_is_nowhere_on_screen_either() {
        let (left, _) = facing();
        let eye = left.camera.eye.x;
        let just_short = Vec3::new(eye + left.camera.far * 0.99, 0.0, 0.0);
        let just_past = Vec3::new(eye + left.camera.far * 1.01, 0.0, 0.0);
        assert!(left.project(just_short).is_some());
        assert_eq!(left.project(just_past), None);
    }

    #[test]
    fn a_point_in_the_other_players_view_is_none_through_this_one() {
        let (left, right) = facing();
        let ahead_of_right = Vec3::new(0.0, 0.0, 5.0);
        let (x, y) = right.project(ahead_of_right).unwrap();
        assert!(
            (x - 1440.0).abs() < 1e-2 && (y - 540.0).abs() < 1e-2,
            "({x}, {y})"
        );
        assert_eq!(left.project(ahead_of_right), None);
    }

    #[test]
    fn the_edge_a_projection_falls_off_is_the_views_own() {
        let (left, _) = facing();
        // Half the visible width at ten units out: the default lens is 45 degrees tall.
        let half_width = (45f32.to_radians() * 0.5).tan() * left.aspect() * 10.0;
        let inside = Vec3::new(0.0, 0.0, half_width * 0.99);
        let outside = Vec3::new(0.0, 0.0, half_width * 1.01);
        let (x, _) = left.project(inside).unwrap();
        assert!(x > 940.0 && x <= 960.0, "{x}");
        assert_eq!(left.project(outside), None);
    }
}