diff --git a/Cargo.toml b/Cargo.toml index c450fdb0..84a6c390 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,17 +33,25 @@ resolver = "2" [workspace.package] edition = "2024" -version = "0.4.0-pre3" +version = "0.4.0-pre4" authors = ["Johannes 'Sharlin' Dahlström "] license = "MIT OR Apache-2.0" repository = "https://github.com/jdahlstrom/retrofire" keywords = ["graphics", "gamedev", "demoscene", "retrocomputing", "rendering"] categories = ["graphics", "game-development", "no-std"] +[workspace.lints] +clippy.manual_range_contains = "allow" +clippy.collapsible_if = "allow" + +[features] +default = ["std"] +std = ["retrofire-core/std", "retrofire-geom/std"] + [dependencies] -retrofire-core = { version = "0.4.0-pre3", path = "core" } -retrofire-front = { version = "0.4.0-pre3", path = "front" } -retrofire-geom = { version = "0.4.0-pre3", path = "geom" } +retrofire-core = { version = "0.4.0-pre4", path = "core" } +retrofire-front = { version = "0.4.0-pre4", path = "front" } +retrofire-geom = { version = "0.4.0-pre4", path = "geom" } [profile.release] opt-level = 2 diff --git a/README.md b/README.md index a0e6e881..20aee6c3 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,8 @@ *Note: This document is best viewed on an 80-column VGA terminal.* -Retrofire is a software 3D rendering library +Retrofire is a software 3D rendering library focusing on performance, +correctness, and pedagogical value. The Retrofire project began as a shamelessly nostalgic effort to explore the state of graphics programming as it was in the mid-to-late 90s in @@ -75,24 +76,24 @@ for custom allocators is planned in order to make `alloc` optional as well. ## Planned -- Material support -- Basic scene graph -- Hierarchical transforms - -- Mipmapping and mipmap generation -- More procedural generation -- Basic animation and sequencing -- Particle simulations -- Support for more file types +* Material support +* Basic scene graph +* Hierarchical transforms +* Mipmapping and mipmap generation +* More procedural generation +* Basic animation and sequencing +* Particle simulations +* Support for more file types # Organization Retrofire is split into several packages: -* core: math, renderer, utilities; no-std compatible -* geom: geometric shapes, mesh builders, model loading -* front: frontends for writing simple graphical applications -* demos: binaries showcasing retrofire features +* retrofire: a metapackage that just re-exports core, geom, and front +* retrofire-core: math, renderer, utilities; no-std compatible +* retrofire-geom: geometric shapes, mesh builders, model loading +* retrofire-front: frontends for writing simple graphical applications +* retrofire-demos: binaries showcasing retrofire features. # Dependencies @@ -104,13 +105,25 @@ functions, the package is not fully functional unless either the `std`, `libm`, or `mm` feature is enabled. Activating `std` additionally enables APIs that do I/O. -The `front` package depends on either `sdl2`, `minifb`, or `wasm-bindgen` -and `web-sys`, depending on enabled features. +The `retrofire-front` package depends on either `sdl2`, `minifb`, or +`wasm-bindgen` and `web-sys`, depending on enabled features. + +The `retrofire-geom` package has no external dependencies. It only requires +`alloc`; activating the optional feature `std` enables APIs that do I/O. + +The `retrofire-demos` package depends on `retrofire`. + +# Screenshots + +The classic Stanford bunny. +![The classic Stanford bunny 3D model.](docs/bunny.jpg) -The `geom` package has no external dependencies. It only requires `alloc`; -activating the optional feature `std` enables APIs that do I/O. +A first-person mouse-and-keyboard scene with many "Rust crates" strewn on a +checkered floor. +![Many wooden crates on a plane, each with the Rust logo](docs/crates.jpg) -The `retrofire-demos` package depends on `retrofire-front`. +Ten thousand spherical particles positioned randomly in a sphere. +![Ten thousand spherical particles in random positions.](docs/sprites.jpg) # License diff --git a/core/Cargo.toml b/core/Cargo.toml index 94e3a19e..8925a066 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -34,3 +34,6 @@ fp = [] [dependencies] libm = { version = "0.2", optional = true } micromath = { version = "2.1", optional = true } + +[lints] +workspace = true diff --git a/core/README.md b/core/README.md index e4fcd4af..3f1f379b 100644 --- a/core/README.md +++ b/core/README.md @@ -12,12 +12,14 @@ # Retrofire-core -Core functionality of the `retrofire` project. +Core functionality of the [`retrofire`][1] project. Includes a math library with strongly typed points, vectors, matrices, colors, and angles; basic geometry primitives; a software 3D renderer with customizable shaders; with more to come. +[1]: https://crates.io/crates/retrofire + ## Crate features * `std`: diff --git a/core/examples/hello_tri.rs b/core/examples/hello_tri.rs new file mode 100644 index 00000000..1aa88264 --- /dev/null +++ b/core/examples/hello_tri.rs @@ -0,0 +1,58 @@ +use retrofire_core::geom::tri; +use retrofire_core::{prelude::*, util::*}; + +fn main() { + let verts = [ + vertex(pt3(-1.0, 1.0, 0.0), rgb(1.0, 0.0, 0.0)), + vertex(pt3(1.0, 1.0, 0.0), rgb(0.0, 0.8, 0.0)), + vertex(pt3(0.0, -1.0, 0.0), rgb(0.4, 0.4, 1.0)), + ]; + + #[cfg(feature = "fp")] + let shader = shader::new( + |v: Vertex3, mvp: &Mat4x4| { + // Transform vertex position from model to projection space + // Interpolate vertex colors in linear color space + vertex(mvp.apply(&v.pos), v.attrib.to_linear()) + }, + |frag: Frag>| frag.var.to_srgb().to_color4(), + ); + #[cfg(not(feature = "fp"))] + let shader = shader::new( + |v: Vertex3, mvp: &Mat4x4| { + // Transform vertex position from model to projection space + // Interpolate vertex colors in normal sRGB color space + vertex(mvp.apply(&v.pos), v.attrib) + }, + |frag: Frag>| frag.var.to_color4(), + ); + + let dims @ (w, h) = (640, 480); + let modelview = translate3(0.0, 0.0, 2.0).to(); + let project = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); + let viewport = viewport(pt2(0, h)..pt2(w, 0)); + + let mut framebuf = Buf2::::new(dims); + + render( + [tri(0, 1, 2)], + verts, + &shader, + &modelview.then(&project), + viewport, + &mut framebuf, + &Context::default(), + ); + + let center_pixel = framebuf[[w / 2, h / 2]]; + + if cfg!(feature = "fp") { + assert_eq!(center_pixel, rgba(150, 128, 186, 255)); + } else { + assert_eq!(center_pixel, rgba(114, 102, 127, 255)); + } + #[cfg(feature = "std")] + { + pnm::save_ppm("triangle.ppm", framebuf).unwrap(); + } +} diff --git a/core/src/geom.rs b/core/src/geom.rs index 6ad5c285..915e12bf 100644 --- a/core/src/geom.rs +++ b/core/src/geom.rs @@ -1,10 +1,11 @@ //! Basic geometric primitives. use alloc::vec::Vec; +use core::fmt::Debug; use crate::math::{ - Affine, Lerp, Linear, Parametric, Point2, Point3, Vec2, Vec3, Vector, - space::Real, + Affine, Lerp, Linear, Mat4x4, Parametric, Point2, Point3, Vec2, Vec3, + Vector, mat::RealToReal, space::Real, vec2, vec3, }; use crate::render::Model; @@ -30,11 +31,14 @@ pub type Vertex3 = Vertex, A>; #[repr(transparent)] pub struct Tri(pub [V; 3]); -/// Plane, defined by normal vector and offset from the origin +/// Plane, defined by the four parameters of the plane equation. #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(transparent)] pub struct Plane(pub(crate) V); +/// Plane embedded in 3D space, splitting the space into two half-spaces. +pub type Plane3 = Plane>>; + /// A ray, or a half line, composed of an initial point and a direction vector. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Ray(pub T, pub T::Diff); @@ -46,6 +50,13 @@ pub struct Ray(pub T, pub T::Diff); #[derive(Clone, Debug, Eq, PartialEq)] pub struct Polyline(pub Vec); +/// A closed curve composed of a chain of line segments. +/// +/// The polygon is represented as a list of points, or vertices, with each pair +/// of consecutive vertices, as well as the first and last vertex, sharing an edge. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Polygon(pub Vec); + /// A line segment between two vertices. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct Edge(pub T, pub T); @@ -56,22 +67,482 @@ pub type Normal3 = Vec3; /// A surface normal in 2D. pub type Normal2 = Vec2; +/// Polygon winding order. +/// +/// The triangle *ABC* below has clockwise winding, while +/// the triangle *DEF* has counter-clockwise winding. +/// +/// ```text +/// B F +/// / \ / \ +/// / \ / \ +/// / \ / \ +/// A-------C D-------E +/// Cw Ccw +/// ``` +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum Winding { + /// Clockwise winding. + Cw, + /// Counter-clockwise winding. + #[default] + Ccw, +} + +/// Creates a `Vertex` with the give position and attribute values. +#[inline] pub const fn vertex(pos: P, attrib: A) -> Vertex { Vertex { pos, attrib } } -impl Plane>> { +/// Creates a `Tri` with the given vertices. +#[inline] +pub const fn tri(a: V, b: V, c: V) -> Tri { + Tri([a, b, c]) +} + +// +// Inherent impls +// + +impl Tri { + /// Given a triangle ABC, returns the edges [AB, BC, CA]. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{Tri, Edge}; + /// use retrofire_core::math::{Point2, pt2}; + /// + /// let pts: [Point2; _] = [pt2(-1.0, 0.0), pt2(2.0, 0.0), pt2(1.0, 2.0)]; + /// let tri = Tri(pts); + /// + /// let [e0, e1, e2] = tri.edges(); + /// assert_eq!(e0, Edge(&pts[0], &pts[1])); + /// assert_eq!(e1, Edge(&pts[1], &pts[2])); + /// assert_eq!(e2, Edge(&pts[2], &pts[0])); + /// + /// ``` + #[inline] + pub fn edges(&self) -> [Edge<&V>; 3] { + let [a, b, c] = &self.0; + [Edge(a, b), Edge(b, c), Edge(c, a)] + } +} + +impl Tri> { + /// Given a triangle ABC, returns the vectors [AB, AC]. + #[inline] + pub fn tangents(&self) -> [P::Diff; 2] { + let [a, b, c] = &self.0; + [b.pos.sub(&a.pos), c.pos.sub(&a.pos)] + } +} + +impl Tri> { + /// Returns the winding order of `self`. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{Tri, vertex, Winding}; + /// use retrofire_core::math::pt2; + /// + /// let mut tri = Tri([ + /// vertex(pt2::<_, ()>(0.0, 0.0), ()), + /// vertex(pt2(0.0, 3.0), ()), + /// vertex(pt2(4.0, 0.0), ()), + /// ]); + /// assert_eq!(tri.winding(), Winding::Cw); + /// + /// tri.0.swap(1, 2); + /// assert_eq!(tri.winding(), Winding::Ccw); + /// ``` + pub fn winding(&self) -> Winding { + let [t, u] = self.tangents(); + if t.perp_dot(u) < 0.0 { + Winding::Cw + } else { + Winding::Ccw + } + } + + /// Returns the signed area of `self`. + /// + /// The area is positive *iff* `self` is wound counter-clockwise. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{Tri, vertex}; + /// use retrofire_core::math::pt2; + /// + /// let tri = Tri([ + /// vertex(pt2::<_, ()>(0.0, 0.0), ()), + /// vertex(pt2(0.0, 3.0), ()), + /// vertex(pt2(4.0, 0.0), ()), + /// ]); + /// assert_eq!(tri.signed_area(), -6.0); + /// ``` + pub fn signed_area(&self) -> f32 { + let [t, u] = self.tangents(); + t.perp_dot(u) / 2.0 + } + + /// Returns the (positive) area of `self`. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{vertex, Tri}; + /// use retrofire_core::math::pt2; + /// + /// let tri = Tri([ + /// vertex(pt2::<_, ()>(0.0, 0.0), ()), + /// vertex(pt2(0.0, 3.0), ()), + /// vertex(pt2(4.0, 0.0), ()), + /// ]); + /// assert_eq!(tri.area(), 6.0); + /// ``` + pub fn area(&self) -> f32 { + self.signed_area().abs() + } +} + +impl Tri> { + /// Returns the normal vector of `self`. + /// + /// The result is normalized to unit length. + /// + /// # Examples + /// ``` + /// use core::f32::consts::SQRT_2; + /// use retrofire_core::geom::{Tri, vertex}; + /// use retrofire_core::math::{pt3, vec3}; + /// + /// // Triangle lying in a 45° angle + /// let tri = Tri([ + /// vertex(pt3::<_, ()>(0.0, 0.0, 0.0), ()), + /// vertex(pt3(0.0, 3.0, 3.0), ()), + /// vertex(pt3(4.0, 0.0,0.0), ()), + /// ]); + /// assert_eq!(tri.normal(), vec3(0.0, SQRT_2 / 2.0, -SQRT_2 / 2.0)); + /// ``` + pub fn normal(&self) -> Normal3 { + let [t, u] = self.tangents(); + // TODO normal with basis + t.cross(&u).normalize().to() + } + + /// Returns the plane that `self` lies on. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{Tri, Plane3, vertex}; + /// use retrofire_core::math::{pt3, Vec3}; + /// + /// let tri = Tri([ + /// vertex(pt3::(0.0, 0.0, 2.0), ()), + /// vertex(pt3(1.0, 0.0, 2.0), ()), + /// vertex(pt3(0.0, 1.0, 2.0), ()) + /// ]); + /// assert_eq!(tri.plane().normal(), Vec3::Z); + /// assert_eq!(tri.plane().offset(), 2.0); + /// ``` + pub fn plane(&self) -> Plane3 { + let [a, b, c] = self.0.each_ref().map(|v| v.pos); + Plane::from_points(a, b, c) + } + + /// Returns the winding order of `self`, as projected to the XY plane. + // TODO is this 3D version meaningful/useful enough? + pub fn winding(&self) -> Winding { + // TODO better way to xyz->xy... + let [u, v] = self.tangents(); + let ([ux, uy, _], [vx, vy, _]) = (u.0, v.0); + let z = vec2::<_, ()>(ux, uy).perp_dot(vec2(vx, vy)); + if z < 0.0 { Winding::Cw } else { Winding::Ccw } + } + + /// Returns the area of `self`. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{tri, vertex}; + /// use retrofire_core::math::pt3; + /// + /// let tri = tri( + /// vertex(pt3::<_, ()>(0.0, 0.0, 0.0), ()), + /// vertex(pt3(4.0, 0.0, 0.0), ()), + /// vertex(pt3(0.0, 3.0, 0.0), ()), + /// ); + /// assert_eq!(tri.area(), 6.0); + /// ``` + #[cfg(feature = "fp")] + pub fn area(&self) -> f32 { + let [t, u] = self.tangents(); + t.cross(&u).len() / 2.0 + } +} + +impl Plane3 { /// The x = 0 coordinate plane. - pub const YZ: Self = Self(Vector::new([1.0, 0.0, 0.0, 0.0])); + pub const YZ: Self = Self::new(1.0, 0.0, 0.0, 0.0); /// The y = 0 coordinate plane. - pub const XZ: Self = Self(Vector::new([0.0, 1.0, 1.0, 0.0])); + pub const XZ: Self = Self::new(0.0, 1.0, 0.0, 0.0); /// The z = 0 coordinate plane. - pub const XY: Self = Self(Vector::new([0.0, 0.0, 1.0, 0.0])); + pub const XY: Self = Self::new(0.0, 0.0, 1.0, 0.0); + + /// Creates a new plane with the given coefficients. + /// + // TODO not normalized because const + // The coefficients are normalized to + // + // (a', b', c', d') = (a, b, c, d) / |(a, b, c)|. + /// + /// The returned plane satisfies the plane equation + /// + /// *ax* + *by* + *cz* = *d*, + /// + /// or equivalently + /// + /// *ax* + *by* + *cz* - *d* = 0. + /// + /// Note the sign of the *d* coefficient. + /// + /// The coefficients (a, b, c) make up a vector normal to the plane, + /// and d is proportional to the plane's distance to the origin. + /// If (a, b, c) is a unit vector, then d is exactly the offset of the + /// plane from the origin in the direction of the normal. + /// + /// # Examples + /// ``` + /// use retrofire_core::{geom::Plane3, math::Vec3}; + /// + /// let p = ::new(1.0, 0.0, 0.0, -2.0); + /// assert_eq!(p.normal(), Vec3::X); + /// assert_eq!(p.offset(), -2.0); + /// + /// ``` + #[inline] + pub const fn new(a: f32, b: f32, c: f32, d: f32) -> Self { + Self(Vector::new([a, b, c, -d])) + } + + /// Creates a plane given three points on the plane. + /// + /// # Panics + /// If the points are collinear or nearly so. + /// + /// # Examples + /// ``` + /// use retrofire_core::{geom::Plane3, math::{pt3, vec3}}; + /// + /// let p = ::from_points( + /// pt3(0.0, 0.0, 2.0), + /// pt3(1.0, 0.0, 2.0), + /// pt3(0.0, 1.0, 2.0), + /// ); + /// assert_eq!(p.normal(), vec3(0.0, 0.0, 1.0)); + /// assert_eq!(p.offset(), 2.0); + /// + /// ``` + pub fn from_points(a: Point3, b: Point3, c: Point3) -> Self { + let n = (b - a).cross(&(c - a)).to(); + Self::from_point_and_normal(a, n) + } + + /// Creates a plane given a point on the plane and a normal. + /// + /// `n` does not have to be normalized. + /// + /// # Panics + /// If `n` is non-finite or nearly zero-length. + /// + /// # Examples + /// ``` + /// use retrofire_core::{geom::Plane3, math::{Vec3, pt3, vec3}}; + /// + /// let p = ::from_point_and_normal(pt3(1.0, 2.0, 3.0), Vec3::Z); + /// assert_eq!(p.normal(), Vec3::Z); + /// assert_eq!(p.offset(), 3.0); + /// + /// ``` + pub fn from_point_and_normal(pt: Point3, n: Normal3) -> Self { + let n = n.normalize(); + // For example, if pt = (0, 1, 0) and n = (0, 1, 0), d has to be 1 + // to satisfy the plane equation n_x + n_y + n_z = d + let d = pt.to_vec().dot(&n.to()); + Plane::new(n.x(), n.y(), n.z(), d) + } + + /// Returns the normal vector of `self`. + /// + /// The normal returned is unit length. + /// + /// # Examples + /// ``` + /// use retrofire_core::{geom::Plane3, math::Vec3}; + /// + /// assert_eq!(::XY.normal(), Vec3::Z); + /// assert_eq!(::YZ.normal(), Vec3::X); + #[inline] + pub fn normal(&self) -> Normal3 { + self.abc().normalize().to() + } + + /// Returns the signed distance of `self` from the origin. + /// + /// This distance is negative if the origin is [*outside*][Self::is_inside] + /// the plane and positive if the origin is *inside* the plane. + /// + /// # Examples + /// ``` + /// use retrofire_core::{geom::Plane3, math::{Vec3, pt3}}; + /// + /// assert_eq!(::new(0.0, 1.0, 0.0, 3.0).offset(), 3.0); + /// assert_eq!(::new(0.0, 2.0, 0.0, 6.0).offset(), 3.0); + /// assert_eq!(::new(0.0, -1.0, 0.0, -3.0).offset(), -3.0); + /// ``` + #[inline] + pub fn offset(&self) -> f32 { + // plane dist from origin is origin dist from plane, negated + -self.signed_dist(Point3::origin()) + } + + /// Returns the perpendicular projection of a point on `self`. + /// + /// In other words, returns *P'*, the point on the plane closest to *P*. + /// + /// ```text + /// ^ P + /// / · + /// / · + /// / · · · · · P' + /// / · + /// / · + /// O------------------> + /// ``` + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::Plane3; + /// use retrofire_core::math::{Point3, pt3}; + /// + /// let pt: Point3 = pt3(1.0, 2.0, -3.0); + /// + /// assert_eq!(::XZ.project(pt), pt3(1.0, 0.0, -3.0)); + /// assert_eq!(::XY.project(pt), pt3(1.0, 2.0, 0.0)); + /// + /// assert_eq!(::new(0.0, 0.0, 1.0, 2.0).project(pt), pt3(1.0, 2.0, 2.0)); + /// assert_eq!(::new(0.0, 0.0, 2.0, 2.0).project(pt), pt3(1.0, 2.0, 1.0)); + /// ``` + pub fn project(&self, pt: Point3) -> Point3 { + // t = -(plane dot orig) / (plane dot dir) + // In this case dir is parallel to plane normal + + let dir = self.abc().to(); + + // TODO add to_homog()/to_real() methods + let pt_hom = [pt.x(), pt.y(), pt.z(), 1.0].into(); + + // Use homogeneous pt to get self · pt = ax + by + cz + d + // Could also just add d manually to ax + by + cz + let plane_dot_orig = self.0.dot(&pt_hom); + + // Vector, so w = 0, so dir_hom · dir_hom = dir · dir + let plane_dot_dir = dir.len_sqr(); // = dir · dir + + let t = -plane_dot_orig / plane_dot_dir; + + pt + t * dir + } + + /// Returns the signed distance of a point to `self`. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::Plane3; + /// use retrofire_core::math::{Point3, pt3, Vec3}; + /// + /// let pt: Point3 = pt3(1.0, 2.0, -3.0); + /// + /// assert_eq!(::XZ.signed_dist(pt), 2.0); + /// assert_eq!(::XY.signed_dist(pt), -3.0); + /// + /// let p = ::new(-1.0, 0.0, 0.0, 2.0); + /// assert_eq!(p.signed_dist(pt), -3.0); + /// ``` + #[inline] + pub fn signed_dist(&self, pt: Point3) -> f32 { + use crate::math::float::*; + let len_sqr = self.abc().len_sqr(); + // TODO use to_homog once committed + let pt = [pt.x(), pt.y(), pt.z(), 1.0].into(); + self.0.dot(&pt) * f32::recip_sqrt(len_sqr) + } + + /// Returns whether a point is in the half-space that the normal of `self` + /// points away from. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::Plane3; + /// use retrofire_core::math::{Point3, pt3}; + /// + /// let pt: Point3 = pt3(1.0, 2.0, -3.0); + /// + /// assert!(!::XZ.is_inside(pt)); + /// assert!(::XY.is_inside(pt)); + /// ``` + // TODO "plane.is_inside(point)" reads wrong + #[cfg(feature = "fp")] + #[inline] + pub fn is_inside(&self, pt: Point3) -> bool { + self.signed_dist(pt) <= 0.0 + } + + /// Returns an orthonormal affine basis on `self`. + /// + /// The y-axis of the basis is the normal vector; the x- and z-axes are + /// two arbitrary orthogonal unit vectors tangent to the plane. The origin + /// point is the point on the plane closest to the origin. + /// + /// # Examples + /// ``` + /// use retrofire_core::assert_approx_eq; + /// use retrofire_core::geom::Plane3; + /// use retrofire_core::math::{Point3, pt3, vec3, Apply}; + /// + /// let p = ::from_point_and_normal(pt3(0.0,1.0,0.0), vec3(0.0,1.0,1.0)); + /// let m = p.basis::<()>(); + /// + /// assert_approx_eq!(m.apply(&Point3::origin()), pt3(0.0, 0.5, 0.5)); + /// ``` + pub fn basis(&self) -> Mat4x4> { + let up = self.abc(); + + let right: Vec3 = + if up.x().abs() < up.y().abs() && up.x().abs() < up.z().abs() { + Vec3::X + } else { + Vec3::Z + }; + let fwd = right.cross(&up).normalize(); + let right = up.normalize().cross(&fwd); + + let origin = self.offset() * up; + + Mat4x4::from_affine(right, up, fwd, origin.to_pt()) + } + + /// Helper that returns the plane normal non-normalized. + fn abc(&self) -> Vec3 { + let [a, b, c, _] = self.0.0; + vec3(a, b, c) + } } impl Polyline { + /// Creates a new polyline from an iterator of vertex points. pub fn new(verts: impl IntoIterator) -> Self { Self(verts.into_iter().collect()) } @@ -80,30 +551,65 @@ impl Polyline { /// /// # Examples /// ``` - /// use retrofire_core::{ - /// geom::{Polyline, Edge}, - /// math::{pt2, Point2} - /// }; + /// use retrofire_core::geom::{Polyline, Edge}; + /// use retrofire_core::math::{pt2, Point2}; /// - /// let points = [pt2(0.0, 0.0), pt2(1.0, 1.0), pt2(2.0, 1.0)]; + /// let pts: [Point2; _] = [pt2(0.0, 0.0), pt2(1.0, 1.0), pt2(2.0, 1.0)]; /// - /// let pl = Polyline::::new(points); - /// let mut edges = pl.edges(); + /// let pline = Polyline::new(pts); + /// let mut edges = pline.edges(); /// - /// assert_eq!(edges.next(), Some(Edge(points[0], points[1]))); - /// assert_eq!(edges.next(), Some(Edge(points[1], points[2]))); + /// assert_eq!(edges.next(), Some(Edge(&pts[0], &pts[1]))); + /// assert_eq!(edges.next(), Some(Edge(&pts[1], &pts[2]))); /// assert_eq!(edges.next(), None); /// ``` - pub fn edges(&self) -> impl Iterator> + '_ - where - T: Clone, - { + pub fn edges(&self) -> impl Iterator> + '_ { + self.0.windows(2).map(|e| Edge(&e[0], &e[1])) + } +} + +impl Polygon { + pub fn new(verts: impl IntoIterator) -> Self { + Self(verts.into_iter().collect()) + } + + /// Returns an iterator over the edges of `self`. + /// + /// Given a polygon ABC...XYZ, returns the edges AB, BC, ..., XY, YZ, ZA. + /// If `self` has zero or one vertices, returns an empty iterator. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{Polygon, Edge}; + /// use retrofire_core::math::{Point2, pt2}; + /// + /// let pts: [Point2; _] = [pt2(0.0, 0.0), pt2(1.0, 1.0), pt2(2.0, 1.0)]; + /// + /// let poly = Polygon::new(pts); + /// let mut edges = poly.edges(); + /// + /// assert_eq!(edges.next(), Some(Edge(&pts[0], &pts[1]))); + /// assert_eq!(edges.next(), Some(Edge(&pts[1], &pts[2]))); + /// assert_eq!(edges.next(), Some(Edge(&pts[2], &pts[0]))); + /// assert_eq!(edges.next(), None); + /// ``` + pub fn edges(&self) -> impl Iterator> + '_ { + let last_first = if let [f, .., l] = &self.0[..] { + Some(Edge(l, f)) + } else { + None + }; self.0 .windows(2) - .map(|e| Edge(e[0].clone(), e[1].clone())) + .map(|e| Edge(&e[0], &e[1])) + .chain(last_first) } } +// +// Local trait impls +// + impl Parametric for Ray where T: Affine>, @@ -114,22 +620,22 @@ where } impl Parametric for Polyline { - /// Returns the point on `self` at `t`. + /// Returns the point on `self` at *t*. /// - /// If the number of vertices in `self` is `n`, the vertex at index - /// `k` < `n` corresponds to `t` = `k` / (`n` - 1). Intermediate values - /// of `t` are linearly interpolated between the two closest vertices. - /// Values `t` < 0 and `t` > 1 are clamped to 0 and 1 respectively. + /// If the number of vertices in `self` is *n* > 1, the vertex at index + /// *k* < *n* corresponds to `t` = *k* / (*n* - 1). Intermediate values + /// of *t* are linearly interpolated between the two closest vertices. + /// Values *t* < 0 and *t* > 1 are clamped to 0 and 1 respectively. + /// A polyline with a single vertex returns the value of that vertex + /// for any value of *t*. /// /// # Panics /// If `self` has no vertices. /// /// # Examples /// ``` - /// use retrofire_core::{ - /// geom::{Polyline, Edge}, - /// math::{pt2, Point2, Parametric} - /// }; + /// use retrofire_core::geom::{Polyline, Edge}; + /// use retrofire_core::math::{pt2, Point2, Parametric}; /// /// let pl = Polyline::( /// vec![pt2(0.0, 0.0), pt2(1.0, 2.0), pt2(2.0, 1.0)]); @@ -147,19 +653,18 @@ impl Parametric for Polyline { /// assert_eq!(pl.eval(7.68), pl.eval(1.0)); /// ``` fn eval(&self, t: f32) -> T { - assert!(self.0.len() > 0, "cannot eval an empty polyline"); + let pts = &self.0; + assert!(!pts.is_empty(), "cannot eval an empty polyline"); - let max = self.0.len() as f32 - 1.0; - let i = 0.0.lerp(&max, t.clamp(0.0, 1.0)); + let max = pts.len() - 1; + let i = t.clamp(0.0, 1.0) * max as f32; let t_rem = i % 1.0; let i = i as usize; - if i == max as usize { - self.0[i].clone() + if i == max { + pts[i].clone() } else { - let p0 = &self.0[i]; - let p1 = &self.0[i + 1]; - p0.lerp(p1, t_rem) + pts[i].lerp(&pts[i + 1], t_rem) } } } @@ -176,10 +681,167 @@ impl Lerp for Vertex { #[cfg(test)] mod tests { + use crate::assert_approx_eq; + use crate::math::*; use alloc::vec; use super::*; - use crate::math::Parametric; + + type Pt = Point<[f32; N], Real>; + + fn tri( + a: Pt, + b: Pt, + c: Pt, + ) -> Tri, ()>> { + Tri([a, b, c].map(|p| vertex(p, ()))) + } + + #[test] + fn triangle_winding_2_cw() { + let tri = tri(pt2(-1.0, 0.0), pt2(0.0, 1.0), pt2(1.0, -1.0)); + assert_eq!(tri.winding(), Winding::Cw); + } + #[test] + fn triangle_winding_2_ccw() { + let tri = tri(pt2(-2.0, 0.0), pt2(1.0, 0.0), pt2(0.0, 1.0)); + assert_eq!(tri.winding(), Winding::Ccw); + } + #[test] + fn triangle_winding_3_cw() { + let tri = + tri(pt3(-1.0, 0.0, 0.0), pt3(0.0, 1.0, 1.0), pt3(1.0, -1.0, 0.0)); + assert_eq!(tri.winding(), Winding::Cw); + } + #[test] + fn triangle_winding_3_ccw() { + let tri = + tri(pt3(-1.0, 0.0, 0.0), pt3(1.0, 0.0, 0.0), pt3(0.0, 1.0, -1.0)); + assert_eq!(tri.winding(), Winding::Ccw); + } + + #[test] + fn triangle_area_2() { + let tri = tri(pt2(-1.0, 0.0), pt2(2.0, 0.0), pt2(2.0, 1.0)); + assert_eq!(tri.area(), 1.5); + } + #[cfg(feature = "fp")] + #[test] + fn triangle_area_3() { + // base = 3, height = 2 + let tri = tri( + pt3(-1.0, 0.0, -1.0), + pt3(2.0, 0.0, -1.0), + pt3(0.0, 0.0, 1.0), + ); + assert_approx_eq!(tri.area(), 3.0); + } + + #[test] + fn triangle_plane() { + let tri = tri( + pt3(-1.0, -2.0, -1.0), + pt3(2.0, -2.0, -1.0), + pt3(0.0, -2.0, 1.0), + ); + assert_approx_eq!(tri.plane().0, Plane3::new(0.0, -1.0, 0.0, 2.0).0); + } + + #[test] + fn plane_from_points() { + let p = ::from_points( + pt3(1.0, 0.0, 0.0), + pt3(0.0, 1.0, 0.0), + pt3(0.0, 0.0, 1.0), + ); + + assert_approx_eq!(p.normal(), vec3(1.0, 1.0, 1.0).normalize()); + assert_approx_eq!(p.offset(), f32::sqrt(1.0 / 3.0)); + } + #[test] + #[should_panic] + fn plane_from_collinear_points_panics() { + ::from_points( + pt3(1.0, 2.0, 3.0), + pt3(-2.0, -4.0, -6.0), + pt3(0.5, 1.0, 1.5), + ); + } + #[test] + #[should_panic] + fn plane_from_zero_normal_panics() { + ::from_point_and_normal( + pt3(1.0, 2.0, 3.0), + vec3(0.0, 0.0, 0.0), + ); + } + #[test] + fn plane_from_point_and_normal() { + let p = ::from_point_and_normal( + pt3(1.0, 2.0, -3.0), + vec3(0.0, 0.0, 12.3), + ); + assert_approx_eq!(p.normal(), vec3(0.0, 0.0, 1.0)); + assert_approx_eq!(p.offset(), -3.0); + } + #[cfg(feature = "fp")] + #[test] + fn plane_is_point_inside_xz() { + let p = ::from_point_and_normal(pt3(1.0, 2.0, 3.0), Vec3::Y); + + // Inside + assert!(p.is_inside(pt3(0.0, 0.0, 0.0))); + // Coincident=inside + assert!(p.is_inside(pt3(0.0, 2.0, 0.0))); + assert!(p.is_inside(pt3(1.0, 2.0, 3.0))); + // Outside + assert!(!p.is_inside(pt3(0.0, 3.0, 0.0))); + assert!(!p.is_inside(pt3(1.0, 3.0, 3.0))); + } + #[cfg(feature = "fp")] + #[test] + fn plane_is_point_inside_neg_xz() { + let p = ::from_point_and_normal(pt3(1.0, 2.0, 3.0), -Vec3::Y); + + // Outside + assert!(!p.is_inside(pt3(0.0, 0.0, 0.0))); + // Coincident=inside + assert!(p.is_inside(pt3(0.0, 2.0, 0.0))); + assert!(p.is_inside(pt3(1.0, 2.0, 3.0))); + // Inside + assert!(p.is_inside(pt3(0.0, 3.0, 0.0))); + assert!(p.is_inside(pt3(1.0, 3.0, 3.0))); + } + #[cfg(feature = "fp")] + #[test] + fn plane_is_point_inside_diagonal() { + let p = ::from_point_and_normal(pt3(0.0, 1.0, 0.0), splat(1.0)); + + // Inside + assert!(p.is_inside(pt3(0.0, 0.0, 0.0))); + assert!(p.is_inside(pt3(-1.0, 1.0, -1.0))); + // Coincident=inside + assert!(p.is_inside(pt3(0.0, 1.0, 0.0))); + // Outside + assert!(!p.is_inside(pt3(0.0, 2.0, 0.0))); + assert!(!p.is_inside(pt3(1.0, 1.0, 1.0))); + assert!(!p.is_inside(pt3(1.0, 0.0, 1.0))); + } + + #[test] + fn plane_project_point() { + let p = ::from_point_and_normal(pt3(0.0, 2.0, 0.0), Vec3::Y); + + // Outside + assert_approx_eq!(p.project(pt3(5.0, 10.0, -3.0)), pt3(5.0, 2.0, -3.0)); + // Coincident + assert_approx_eq!(p.project(pt3(5.0, 2.0, -3.0)), pt3(5.0, 2.0, -3.0)); + // Inside + assert_approx_eq!( + p.project(pt3(5.0, -10.0, -3.0)), + pt3(5.0, 2.0, -3.0) + ); + } #[test] fn polyline_eval_f32() { diff --git a/core/src/geom/mesh.rs b/core/src/geom/mesh.rs index a301a405..daae554b 100644 --- a/core/src/geom/mesh.rs +++ b/core/src/geom/mesh.rs @@ -7,11 +7,11 @@ use core::{ }; use crate::{ - math::{Linear, Mat4x4, Point3, mat::RealToReal}, + math::{Apply, Linear, Mat4x4, Point3, mat::RealToReal}, render::Model, }; -use super::{Normal3, Tri, Vertex3, vertex}; +use super::{Normal3, Tri, Vertex3, tri, vertex}; /// A triangle mesh. /// @@ -47,7 +47,7 @@ impl Mesh { /// /// # Examples /// ``` - /// use retrofire_core::geom::{Tri, Mesh, vertex}; + /// use retrofire_core::geom::{Mesh, tri, vertex}; /// use retrofire_core::math::pt3; /// /// let verts = [ @@ -60,10 +60,10 @@ impl Mesh { /// /// let faces = [ /// // Indices point to the verts array - /// Tri([0, 1, 2]), - /// Tri([0, 1, 3]), - /// Tri([0, 2, 3]), - /// Tri([1, 2, 3]) + /// tri(0, 1, 2), + /// tri(0, 1, 3), + /// tri(0, 2, 3), + /// tri(1, 2, 3) /// ]; /// /// // Create a mesh with a tetrahedral shape @@ -88,6 +88,14 @@ impl Mesh { Self { faces, verts } } + /// Returns an iterator over the faces of `self`, mapping the vertex indices + /// to references to the corresponding vertices. + pub fn faces(&self) -> impl Iterator>> { + self.faces + .iter() + .map(|Tri(vs)| Tri(vs.map(|i| &self.verts[i]))) + } + /// Returns a mesh with the faces and vertices of both `self` and `other`. pub fn merge(mut self, Self { faces, verts }: Self) -> Self { let n = self.verts.len(); @@ -121,7 +129,7 @@ impl Builder { /// as long as all indices are valid when the [`build`][Builder::build] /// method is called. pub fn push_face(&mut self, a: usize, b: usize, c: usize) { - self.mesh.faces.push(Tri([a, b, c])); + self.mesh.faces.push(tri(a, b, c)); } /// Appends all the faces yielded by the given iterator. @@ -160,25 +168,23 @@ impl Builder { } } -impl Builder<()> { +impl Builder { /// Applies the given transform to the position of each vertex. /// /// This is an eager operation, that is, only vertices *currently* /// added to the builder are transformed. - pub fn transform( - self, - tf: &Mat4x4>, - ) -> Builder<()> { - let mesh = Mesh { - faces: self.mesh.faces, - verts: self - .mesh - .verts - .into_iter() - .map(|v| vertex(tf.apply_pt(&v.pos), v.attrib)) - .collect(), - }; - mesh.into_builder() + pub fn transform(self, tf: &Mat4x4>) -> Self { + self.warp(|v| vertex(tf.apply(&v.pos), v.attrib)) + } + + /// Applies an arbitrary mapping to each vertex. + /// + /// This method can be used for various nonlinear transformations such as + /// twisting or dilation. This is an eager operation, that is, only vertices + /// *currently* added to the builder are transformed. + pub fn warp(mut self, f: impl FnMut(Vertex3) -> Vertex3) -> Self { + self.mesh.verts = self.mesh.verts.into_iter().map(f).collect(); + self } /// Computes a vertex normal for each vertex as an area-weighted average @@ -275,7 +281,7 @@ mod tests { #[should_panic] fn mesh_new_panics_if_vertex_index_oob() { let _: Mesh<()> = Mesh::new( - [Tri([0, 1, 2]), Tri([1, 2, 3])], + [tri(0, 1, 2), tri(1, 2, 3)], [ vertex(pt3(0.0, 0.0, 0.0), ()), vertex(pt3(1.0, 1.0, 1.0), ()), diff --git a/core/src/lib.rs b/core/src/lib.rs index c1d93f61..5ef734b1 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -34,6 +34,12 @@ //! via the [micromath](https://crates.io/crates/micromath) crate. //! //! All features are disabled by default. +//! +//! # Example +//! +//! ``` +#![doc = include_str!("../examples/hello_tri.rs")] +//! ``` #![no_std] @@ -52,7 +58,9 @@ pub mod util; /// Prelude module exporting many frequently used items. pub mod prelude { pub use crate::{ - geom::{Mesh, Normal2, Normal3, Tri, Vertex, Vertex2, Vertex3, vertex}, + geom::{ + Mesh, Normal2, Normal3, Tri, Vertex, Vertex2, Vertex3, tri, vertex, + }, math::*, render::{raster::Frag, *}, util::buf::{AsMutSlice2, AsSlice2, Buf2, MutSlice2, Slice2}, diff --git a/core/src/math.rs b/core/src/math.rs index 94d89a74..eb8211f4 100644 --- a/core/src/math.rs +++ b/core/src/math.rs @@ -24,8 +24,8 @@ pub use { approx::ApproxEq, color::{Color, Color3, Color3f, Color4, Color4f, rgb, rgba}, mat::{ - Mat3x3, Mat4x4, Matrix, orthographic, perspective, scale, scale3, - translate, translate3, viewport, + Apply, Mat2x2, Mat3x3, Mat4x4, Matrix, orthographic, perspective, + scale, scale3, translate, translate3, viewport, }, param::Parametric, point::{Point, Point2, Point2u, Point3, pt2, pt3}, @@ -40,6 +40,26 @@ pub use { mat::{orient_y, orient_z, rotate, rotate_x, rotate_y, rotate_z, rotate2}, }; +/// Implements an operator trait in terms of an op-assign trait. +macro_rules! impl_op { + ($trait:ident :: $method:ident, $self:ident, $rhs:ty, $op:tt) => { + impl_op!($trait::$method, $self, $rhs, $op, bound=Linear); + }; + ($trait:ident :: $method:ident, $self:ident, $rhs:ty, $op:tt, bound=$bnd:path) => { + impl $trait<$rhs> for $self + where + Self: $bnd, + { + type Output = Self; + /// TODO + #[inline] + fn $method(mut self, rhs: $rhs) -> Self { + self $op rhs; self + } + } + }; +} + pub mod angle; pub mod approx; pub mod color; @@ -64,11 +84,27 @@ pub trait Lerp: Sized { /// ``` /// /// This method does not panic if `t < 0.0` or `t > 1.0`, or if `t` - /// is a `NaN`, but the return value in those cases is unspecified. + /// is `NaN`, but the return value in those cases is unspecified. /// Individual implementations may offer stronger guarantees. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::Lerp; + /// + /// assert_eq!(f32::lerp(&1.0, &5.0, 0.25), 2.0); + /// ``` fn lerp(&self, other: &Self, t: f32) -> Self; /// Returns the (unweighted) average of `self` and `other`. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{Lerp, pt2, Point2}; + /// + /// let a: Point2 = pt2(-1.0, 2.0); + /// let b = pt2(3.0, -2.0); + /// assert_eq!(a.midpoint(&b), pt2(1.0, 0.0)); + /// ``` fn midpoint(&self, other: &Self) -> Self { self.lerp(other, 0.5) } @@ -76,12 +112,35 @@ pub trait Lerp: Sized { /// Linearly interpolates between two values. /// -/// For more information, see [`Lerp::lerp`]. +/// For examples and more information, see [`Lerp::lerp`]. #[inline] pub fn lerp(t: f32, from: T, to: T) -> T { from.lerp(&to, t) } +/// Returns the relative position of `t` between `min` and `max`. +/// +/// That is, returns 0 when `t` = `min`, 1 when `t` = `max`, and linearly +/// interpolates in between. +/// +/// The result is unspecified if any of the parameters is non-finite, or if +/// `min` = `max`. +/// +/// # Examples +/// ``` +/// use retrofire_core::math::inv_lerp; +/// +/// // Two is one fourth of the way from one to five +/// assert_eq!(inv_lerp(2.0, 1.0, 5.0), 0.25); +/// +/// // Zero is halfway between -2 and 2 +/// assert_eq!(inv_lerp(0.0, -2.0, 2.0), 0.5); +/// ``` +#[inline] +pub fn inv_lerp(t: f32, min: f32, max: f32) -> f32 { + (t - min) / (max - min) +} + impl Lerp for T where T: Affine>, @@ -99,7 +158,7 @@ where /// ``` /// /// If `t < 0.0` or `t > 1.0`, returns the appropriate extrapolated value. - /// If `t` is a NaN, the result is unspecified. + /// If `t` is NaN, the result is unspecified. /// /// # Examples /// ``` @@ -129,6 +188,6 @@ impl Lerp for () { impl Lerp for (U, V) { fn lerp(&self, (u, v): &Self, t: f32) -> Self { - (self.0.lerp(&u, t), self.1.lerp(&v, t)) + (self.0.lerp(u, t), self.1.lerp(v, t)) } } diff --git a/core/src/math/angle.rs b/core/src/math/angle.rs index 55547740..3dc2a1d0 100644 --- a/core/src/math/angle.rs +++ b/core/src/math/angle.rs @@ -27,18 +27,18 @@ pub struct Angle(f32); /// Tag type for a polar coordinate space #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] -pub struct Polar; +pub struct Polar(PhantomData); /// Tag type for a spherical coordinate space. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub struct Spherical(PhantomData); /// A polar coordinate vector, with radius and azimuth components. -pub type PolarVec = Vector<[f32; 2], Polar>; +pub type PolarVec = Vector<[f32; 2], Polar>; /// A spherical coordinate vector, with radius, azimuth, and altitude /// (elevation) components. -pub type SphericalVec = Vector<[f32; 3], Spherical>; +pub type SphericalVec = Vector<[f32; 3], Spherical>; // // Free fns and consts @@ -115,7 +115,7 @@ pub fn atan2(y: f32, x: f32) -> Angle { } /// Returns a polar coordinate vector with azimuth `az` and radius `r`. -pub const fn polar(r: f32, az: Angle) -> PolarVec { +pub const fn polar(r: f32, az: Angle) -> PolarVec { Vector::new([r, az.to_rads()]) } @@ -264,7 +264,7 @@ impl Angle { } } -impl PolarVec { +impl PolarVec { /// Returns the radial component of `self`. #[inline] pub fn r(&self) -> f32 { @@ -278,8 +278,8 @@ impl PolarVec { /// Returns `self` converted to the equivalent Cartesian 2-vector. /// - /// Let the components of self be `(r, az)`. Then the `x` component of the - /// result equals `r * cos(az)`, and the `y` component equals `r * sin(az)`. + /// Let the components of `self` be `(r, az)`. Then the result `(x, y)` + /// equals `(r * cos(az), r * sin(az))`. /// /// ```text /// +y @@ -296,13 +296,15 @@ impl PolarVec { /// use retrofire_core::assert_approx_eq; /// use retrofire_core::math::{vec2, polar, degs}; /// + /// let vec2 = vec2::; + /// /// assert_approx_eq!(polar(2.0, degs(0.0)).to_cart(), vec2(2.0, 0.0)); /// assert_approx_eq!(polar(3.0, degs(90.0)).to_cart(), vec2(0.0, 3.0)); /// assert_approx_eq!(polar(4.0, degs(-180.0)).to_cart(), vec2(-4.0, 0.0)); /// /// ``` #[cfg(feature = "fp")] - pub fn to_cart(&self) -> Vec2 { + pub fn to_cart(&self) -> Vec2 { let (y, x) = self.az().sin_cos(); vec2(x, y) * self.r() } @@ -343,7 +345,7 @@ impl SphericalVec { } #[cfg(feature = "fp")] -impl Vec2 { +impl Vec2 { /// Returns `self` converted into the equivalent polar coordinate vector. /// /// The `r` component of the result equals `self.len()`. @@ -364,6 +366,8 @@ impl Vec2 { /// use retrofire_core::assert_approx_eq; /// use retrofire_core::math::{vec2, degs}; /// + /// let vec2 = vec2::; + /// /// // A non-negative x and zero y maps to zero azimuth /// assert_eq!(vec2(0.0, 0.0).to_polar().az(), degs(0.0)); /// assert_eq!(vec2(1.0, 0.0).to_polar().az(), degs(0.0)); @@ -377,7 +381,7 @@ impl Vec2 { /// // A negative x and zero y maps to straight angle azimuth /// assert_approx_eq!(vec2(-1.0, 0.0).to_polar().az(), degs(180.0)); /// ``` - pub fn to_polar(&self) -> PolarVec { + pub fn to_polar(&self) -> PolarVec { let r = self.len(); let az = atan2(self.y(), self.x()); polar(r, az) @@ -473,6 +477,17 @@ impl ZDiv for Angle {} // Foreign trait impls // +impl Default for SphericalVec { + fn default() -> Self { + Self::new([1.0, 0.0, 0.0]) + } +} +impl Default for PolarVec { + fn default() -> Self { + Self::new([1.0, 0.0]) + } +} + impl Display for Angle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let (val, unit) = if f.alternate() { @@ -532,21 +547,21 @@ impl Rem for Angle { } #[cfg(feature = "fp")] -impl From for Vec2 { +impl From> for Vec2 { /// Converts a polar vector into the equivalent Cartesian vector. /// /// See [PolarVec::to_cart] for more information. - fn from(p: PolarVec) -> Self { + fn from(p: PolarVec) -> Self { p.to_cart() } } #[cfg(feature = "fp")] -impl From for PolarVec { +impl From> for PolarVec { /// Converts a Cartesian 2-vector into the equivalent polar vector. /// /// See [Vec2::to_polar] for more information. - fn from(v: Vec2) -> Self { + fn from(v: Vec2) -> Self { v.to_polar() } } @@ -572,13 +587,13 @@ impl From> for SphericalVec { } #[cfg(test)] -#[allow(unused)] +#[allow(unused, nonstandard_style)] mod tests { use core::f32::consts::{PI, TAU}; use crate::{ assert_approx_eq, - math::{Lerp, Vary}, + math::{self, Lerp, Vary, Vec2, Vec3}, }; use super::*; @@ -695,6 +710,9 @@ mod tests { assert_approx_eq!(i.next(), None); } + const vec2: fn(f32, f32) -> Vec2 = math::vec2; + const vec3: fn(f32, f32, f32) -> Vec3 = math::vec3; + #[cfg(feature = "fp")] #[test] fn polar_to_cartesian_zero_r() { @@ -773,7 +791,6 @@ mod tests { #[cfg(feature = "fp")] #[test] fn cartesian_to_spherical_zero_alt() { - let vec3 = vec3::; assert_approx_eq!( vec3(0.0, 0.0, 0.0).to_spherical(), spherical(0.0, degs(0.0), degs(0.0)) @@ -796,7 +813,6 @@ mod tests { #[test] fn cartesian_to_spherical() { use core::f32::consts::SQRT_2; - let vec3 = vec3::; assert_approx_eq!( vec3(SQRT_3, 0.0, 1.0).to_spherical(), spherical(2.0, degs(30.0), degs(0.0)) diff --git a/core/src/math/approx.rs b/core/src/math/approx.rs index 093d8b2c..3fb4dd73 100644 --- a/core/src/math/approx.rs +++ b/core/src/math/approx.rs @@ -36,9 +36,8 @@ pub trait ApproxEq { impl ApproxEq for f32 { fn approx_eq_eps(&self, other: &Self, rel_eps: &Self) -> bool { - use super::float::f32; - let diff = f32::abs(self - other); - diff <= *rel_eps * f32::abs(*self).max(1.0) + let diff = (self - other).abs(); + diff <= *rel_eps * self.abs().max(1.0) } fn relative_epsilon() -> Self { diff --git a/core/src/math/color.rs b/core/src/math/color.rs index 5098905f..d2ec147e 100644 --- a/core/src/math/color.rs +++ b/core/src/math/color.rs @@ -1,13 +1,15 @@ //! Colors and color spaces. +use core::ops::{ + Add, AddAssign, Div, DivAssign, Index, Mul, MulAssign, Neg, Sub, SubAssign, +}; use core::{ array, - fmt::{self, Debug, Formatter}, + fmt::{self, Debug, Display, Formatter}, marker::PhantomData, - ops::Index, }; -use crate::math::{Affine, Linear, Vector, float::f32, vary::ZDiv}; +use super::{Affine, Linear, Vector, vary::ZDiv}; // // Types @@ -19,7 +21,7 @@ use crate::math::{Affine, Linear, Vector, float::f32, vary::ZDiv}; /// /// # Type parameters /// * `Repr`: the representation of the components of `Self`. -/// Color components are also called *channels*. +/// Color components are also called *channels*. /// * `Space`: the color space that `Self` is an element of. #[repr(transparent)] #[derive(Copy, Clone, Default, Eq, PartialEq)] @@ -106,10 +108,25 @@ impl Color3 { rgba(r, g, b, 0xFF) } + pub fn to_color3f(self) -> Color3f { + self.0.map(|c| c as f32 / 255.0).into() + } + /// Returns the HSL color equivalent to `self`. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::color::{hsl, rgb}; + /// + /// let red = rgb(1.0, 0.0, 0.0); + /// assert_eq!(red.to_hsl(), hsl(0.0, 1.0, 0.5)); + /// + /// let light_blue = rgb(0.5, 0.5, 1.0); + /// assert_eq!(light_blue.to_hsl(), hsl(2.0/3.0, 1.0, 0.75)); + /// ``` pub fn to_hsl(self) -> Color3 { // Fixed point multiplier - const M: i32 = 256; + const _1: i32 = 256; let [r, g, b] = self.0.map(i32::from); @@ -120,18 +137,18 @@ impl Color3 { let h = if d == 0 { 0 } else if max == r { - (((g - b) * M) / d).rem_euclid(6 * M) + (((g - b) * _1) / d).rem_euclid(6 * _1) } else if max == g { - ((b - r) * M) / d + (2 * M) + ((b - r) * _1) / d + (2 * _1) } else { - ((r - g) * M) / d + (4 * M) + ((r - g) * _1) / d + (4 * _1) }; let h = h / 6; let l = (max + min + 1) / 2; let s = if l == 0 || l == 255 { 0 } else { - (d * M) / (M - (2 * l - M).abs()) + (d * _1) / (_1 - (2 * l - _1).abs()) }; [h, s, l].map(|c| c.clamp(0, 255) as u8).into() @@ -146,6 +163,10 @@ impl Color4 { rgb(r, g, b) } + pub fn to_color4f(self) -> Color4f { + self.0.map(|c| c as f32 / 255.0).into() + } + /// Returns the HSLA color equivalent to `self`. pub fn to_hsla(self) -> Color4 { let [r, g, b, _] = self.0; @@ -195,10 +216,22 @@ impl Color3f { #[cfg(feature = "fp")] #[inline] pub fn to_linear(self) -> Color3f { + use super::float::f32; self.0.map(|c| f32::powf(c, GAMMA)).into() } /// Returns the HSL color equivalent to `self`. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::color::{hsl, rgb}; + /// + /// let red = rgb(0xFF, 0, 0); + /// assert_eq!(red.to_hsl(), hsl(0, 0xFF, 0x80)); + /// + /// let light_blue = rgb(0x80, 0x80, 0xFF); + /// assert_eq!(light_blue.to_hsl(), hsl(0xAA, 0xFE, 0xC0)); + /// ``` pub fn to_hsl(self) -> Color3f { let [r, g, b] = self.0; @@ -209,7 +242,7 @@ impl Color3f { let h = if d == 0.0 { 0.0 } else if max == r { - f32::rem_euclid((g - b) / d, 6.0) + super::float::f32::rem_euclid((g - b) / d, 6.0) } else if max == g { (b - r) / d + 2.0 } else { @@ -220,7 +253,7 @@ impl Color3f { let s = if l == 0.0 || l == 1.0 { 0.0 } else { - d / (1.0 - f32::abs(2.0 * l - 1.0)) + d / (1.0 - (2.0 * l - 1.0).abs()) }; for ch in [h, s, l] { @@ -266,9 +299,9 @@ impl Color4f { impl Color3f { /// Returns `self` gamma-converted to sRGB space. /// - /// Linear interpolation, used to compute eg. gradients and blending, + /// Linear interpolation, used to compute e.g. gradients and blending, /// is just an approximation if carried out in a nonlinear, gamma-corrected - /// color space such as the standard sRGB space. For visually optimal + /// color space such as the standard sRGB space. For visually correct /// results, sRGB input colors should be [converted to linear space][1] /// before interpolation, and right before writing to the output. /// Conversion, however, incurs a small performance penalty. @@ -277,39 +310,42 @@ impl Color3f { #[cfg(feature = "fp")] #[inline] pub fn to_srgb(self) -> Color3f { + use super::float::f32; self.0.map(|c| f32::powf(c, INV_GAMMA)).into() } } impl Color3 { /// Returns the RGB color equivalent to `self`. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::color::{hsl, rgb}; + /// + /// let red = hsl(0, 0xFF, 0x80); + /// assert_eq!(red.to_rgb(), rgb(0xFF, 0, 0)); + /// + /// let light_blue = hsl(0xAB, 0xFF, 0xC0); + /// assert_eq!(light_blue.to_rgb(), rgb(0x80, 0x80, 0xFF)); + /// ``` pub fn to_rgb(self) -> Color3 { // Fixed-point multiplier - const M: i32 = 256; + const _1: i32 = 256; let [h, s, l] = self.0.map(i32::from); - let h = h * 6; - - let c = (M - (2 * l - M).abs()) * s; - let x = c * (M - (h % (2 * M) - M).abs()); - let m = M * l - c / 2; - - let c = c / M; - let x = x / M / M; - let m = m / M; - - let rgb = match h / M { - 0 => [c, x, 0], - 1 => [x, c, 0], - 2 => [0, c, x], - 3 => [0, x, c], - 4 => [x, 0, c], - 5 => [c, 0, x], - _ => unreachable!(), - }; + let h = 6 * h; + + let c = (_1 - (2 * l - _1).abs()) * s; + let x = c * (_1 - (h % (2 * _1) - _1).abs()); + let m = _1 * l - c / 2; + + // Normalize + let [c, x, m] = [c / _1, x / _1 / _1, m / _1]; + + let rgb = hcx_to_rgb(h / _1, c, x, 0); rgb.map(|ch| { let ch = ch + m; - debug_assert!(0 <= ch && ch < 256, "channel oob: {:?}", ch); + debug_assert!(0 <= ch && ch < _1, "channel oob: {:?}", ch); ch as u8 }) .into() @@ -318,33 +354,47 @@ impl Color3 { impl Color3f { /// Returns the RGB color equivalent to `self`. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::color::{hsl, rgb}; + /// + /// let red = hsl(0.0f32, 1.0, 0.5); + /// assert_eq!(red.to_rgb(), rgb(1.0, 0.0, 0.0)); + /// + /// let light_blue = hsl(2.0 / 3.0, 1.0, 0.75); + /// assert_eq!(light_blue.to_rgb(), rgb(0.5, 0.5, 1.0)); + /// ``` pub fn to_rgb(self) -> Color3f { let [h, s, l] = self.0; - let h = h * 6.0; - - let c = (1.0 - f32::abs(2.0 * l - 1.0)) * s; - let x = c * (1.0 - f32::abs(h % 2.0 - 1.0)); - let m = 1.0 * l - c / 2.0; - - let rgb = match (h - 0.5) as i32 { - 0 => [c, x, 0.0], - 1 => [x, c, 0.0], - 2 => [0.0, c, x], - 3 => [0.0, x, c], - 4 => [x, 0.0, c], - 5 => [c, 0.0, x], - _ => unreachable!("h={h}"), - }; + let h = 6.0 * h; + + let c = (1.0 - (2.0 * l - 1.0).abs()) * s; + let x = c * (1.0 - (h % 2.0 - 1.0).abs()); + let m = l - c / 2.0; + let rgb = hcx_to_rgb(h as i32, c, x, 0.0); rgb.map(|ch| { let ch = ch + m; - debug_assert!(0.0 <= ch && ch <= 1.0, "channel oob: {ch:?}"); + debug_assert!(-1e6 <= ch && ch <= 1.0 + 1e6, "channel oob: {ch:?}"); ch }) .into() } } +fn hcx_to_rgb(h: i32, c: T, x: T, z: T) -> [T; 3] { + match h { + 0 => [c, x, z], + 1 => [x, c, z], + 2 => [z, c, x], + 3 => [z, x, c], + 4 => [x, z, c], + 5 | 6 => [c, z, x], + _ => unreachable!("h = {h}, c = {c}, x = {z}"), + } +} + impl Color4 { /// Returns `self` as HSL, discarding the alpha channel. pub fn to_hsl(self) -> Color3 { @@ -525,12 +575,102 @@ impl From for Color { } } +// +// Arithmetic trait impls +// + +// +// Arithmetic traits +// + +/// The color += color operator. +impl AddAssign for Color +where + Self: Affine, +{ + #[inline] + fn add_assign(&mut self, rhs: D) { + *self = Affine::add(&*self, &rhs); + } +} + +/// The color -= color operator. +impl SubAssign for Color +where + D: Linear, + Self: Affine, +{ + #[inline] + fn sub_assign(&mut self, rhs: D) { + *self += rhs.neg(); + } +} + +// The color *= scalar operator. +impl MulAssign for Color +where + Self: Linear, +{ + #[inline] + fn mul_assign(&mut self, rhs: Sc) { + *self = Linear::mul(&*self, rhs); + } +} + +// The color /= scalar operator. +impl DivAssign for Color +where + Self: Linear, +{ + #[inline] + fn div_assign(&mut self, rhs: f32) { + use crate::math::ApproxEq; + debug_assert!(!rhs.approx_eq(&0.0)); + *self = Linear::mul(&*self, rhs.recip()); + } +} + +/// The color negation operator. +impl Neg for Color +where + Self: Linear, +{ + type Output = Self; + + #[inline] + fn neg(self) -> Self { + ::neg(&self) + } +} + +/// The scalar * color operator. +impl Mul> for as Linear>::Scalar +where + Color: Linear, +{ + type Output = Color; + + #[inline] + fn mul(self, rhs: Color) -> Color { + rhs * self + } +} + +// The color + color operator. +impl_op!(Add::add, Color, ::Diff, +=, bound=Affine); +// The color - color operator. +impl_op!(Sub::sub, Color, ::Diff, -=, bound=Affine); +// The color * scalar operator. +impl_op!(Mul::mul, Color, ::Scalar, *=); +// The color / scalar operator. +impl_op!(Div::div, Color, f32, /=, bound=Linear); + #[cfg(test)] mod tests { use super::*; #[test] - fn color_components() { + fn rgb_components() { assert_eq!(rgb(0xFF, 0, 0).r(), 0xFF); assert_eq!(rgb(0, 0xFF, 0).g(), 0xFF); assert_eq!(rgb(0, 0, 0xFF).b(), 0xFF); @@ -541,6 +681,40 @@ mod tests { assert_eq!(rgba(0, 0, 0, 0xFF).a(), 0xFF); } + #[test] + fn hsl_components() { + assert_eq!(hsl(0xFF, 0, 0).h(), 0xFF); + assert_eq!(hsl(0, 0xFF, 0).s(), 0xFF); + assert_eq!(hsl(0, 0, 0xFF).l(), 0xFF); + + assert_eq!(hsla(0xFF, 0, 0, 0).h(), 0xFF); + assert_eq!(hsla(0, 0xFF, 0, 0).s(), 0xFF); + assert_eq!(hsla(0, 0, 0xFF, 0).l(), 0xFF); + assert_eq!(hsla(0, 0, 0, 0xFF).a(), 0xFF); + } + + #[test] + fn rgb_f32_ops() { + let lhs = rgb(0.5, 0.625, 0.75); + let rhs = rgb(0.125, 0.25, 0.375); + + assert_eq!(lhs + rhs, rgb(0.625, 0.875, 1.125)); + assert_eq!(lhs - rhs, rgb(0.375, 0.375, 0.375)); + assert_eq!(lhs * 0.5, rgb(0.25, 0.3125, 0.375)); + assert_eq!(0.5 * lhs, rgb(0.25, 0.3125, 0.375)); + assert_eq!(lhs / 2.0, rgb(0.25, 0.3125, 0.375)); + assert_eq!(-lhs, rgb(-0.5, -0.625, -0.75)); + } + + #[test] + fn rgb_u8_ops() { + let lhs = rgb(0x77, 0x88, 0x99); + let rhs = [0x11_i32, 0x33, 0x55].into(); + + assert_eq!(lhs + rhs, rgb(0x88, 0xBB, 0xEE)); + assert_eq!(lhs - rhs, rgb(0x66, 0x55, 0x44)); + } + #[test] fn rgb_to_hsl() { let cases = [ diff --git a/core/src/math/float.rs b/core/src/math/float.rs index 96d5a255..73a56ed7 100644 --- a/core/src/math/float.rs +++ b/core/src/math/float.rs @@ -8,7 +8,6 @@ #[cfg(feature = "libm")] pub mod libm { - pub use libm::fabsf as abs; pub use libm::floorf as floor; pub use libm::powf; @@ -36,10 +35,6 @@ pub mod libm { pub mod mm { use micromath::F32Ext as mm; - #[inline] - pub fn abs(x: f32) -> f32 { - mm::abs(x) - } #[inline] pub fn floor(x: f32) -> f32 { mm::floor(x) @@ -98,11 +93,6 @@ pub mod mm { } pub mod fallback { - /// Returns the absolute value of `x`. - #[inline] - pub fn abs(x: f32) -> f32 { - f32::from_bits(x.to_bits() & !0x8000_0000) - } /// Returns the largest integer less than or equal to `x`. #[inline] pub fn floor(x: f32) -> f32 { @@ -194,10 +184,6 @@ mod tests { assert_eq!(f32::floor(0.0), 0.0); assert_eq!(f32::floor(-1.23), -2.0); - assert_eq!(f32::abs(1.23), 1.23); - assert_eq!(f32::abs(0.0), 0.0); - assert_eq!(f32::abs(-1.23), 1.23); - assert_approx_eq!(f32::rem_euclid(1.23, 4.0), 1.23); assert_approx_eq!(f32::rem_euclid(4.0, 4.0), 0.0); assert_approx_eq!(f32::rem_euclid(5.67, 4.0), 1.67); diff --git a/core/src/math/mat.rs b/core/src/math/mat.rs index ee323674..8ce22bab 100644 --- a/core/src/math/mat.rs +++ b/core/src/math/mat.rs @@ -1,11 +1,11 @@ #![allow(clippy::needless_range_loop)] -//! Matrices and linear transforms. +//! Matrices and linear and affine transforms. //! //! TODO Docs use core::{ - array::{self, from_fn}, + array, fmt::{self, Debug, Formatter}, marker::PhantomData as Pd, ops::Range, @@ -14,10 +14,11 @@ use core::{ use crate::render::{NdcToScreen, ViewToProj}; use super::{ + approx::ApproxEq, float::f32, - point::{Point2, Point2u, Point3}, + point::{Point2, Point2u, Point3, pt2}, space::{Linear, Proj3, Real}, - vec::{ProjVec3, Vec2, Vec3, Vector}, + vec::{ProjVec3, Vec2, Vec3, Vector, vec2, vec3}, }; /// A linear transform from one space (or basis) to another. @@ -33,6 +34,7 @@ pub trait LinearMap { } /// Composition of two `LinearMap`s, `Self` ∘ `Inner`. +/// /// If `Self` maps from `B` to `C`, and `Inner` maps from `A` to `B`, /// `Self::Result` maps from `A` to `C`. pub trait Compose: LinearMap { @@ -40,6 +42,16 @@ pub trait Compose: LinearMap { type Result: LinearMap; } +/// Trait for applying a transform to a type. +pub trait Apply { + /// The transform codomain type. + type Output; + + /// Applies this transform to a value. + #[must_use] + fn apply(&self, t: &T) -> Self::Output; +} + /// A change of basis in real vector space of dimension `DIM`. #[derive(Copy, Clone, Default, Eq, PartialEq)] pub struct RealToReal( @@ -55,6 +67,8 @@ pub struct RealToProj(Pd); #[derive(Copy, Eq, PartialEq)] pub struct Matrix(pub Repr, Pd); +/// Type alias for a 2x2 float matrix. +pub type Mat2x2 = Matrix<[[f32; 2]; 2], Map>; /// Type alias for a 3x3 float matrix. pub type Mat3x3 = Matrix<[[f32; 3]; 3], Map>; /// Type alias for a 4x4 float matrix. @@ -64,9 +78,29 @@ pub type Mat4x4 = Matrix<[[f32; 4]; 4], Map>; // Inherent impls // +/// Slight syntactic sugar for creating [`Matrix`] instances. +/// +/// # Examples +/// ``` +/// use retrofire_core::{mat, math::Mat3x3}; +/// +/// let m: Mat3x3 = mat![ +/// 0.0, 2.0, 0.0; +/// 1.0, 0.0, 0.0; +/// 0.0, 0.0, 3.0; +/// ]; +/// assert_eq!(m.0, [ +/// [0.0, 2.0, 0.0], +/// [1.0, 0.0, 0.0], +/// [0.0, 0.0, 3.0] +/// ]); +/// ``` +#[macro_export] macro_rules! mat { - ($($i:expr),+; $($j:expr),+; $($k:expr),+; $($($l:expr),+)? $(;)?) => { - Matrix([[$($i),+], [$($j),+], [$($k),+], $([$($l),+])?], Pd) + ( $( $( $elem:expr ),+ );+ $(;)? ) => { + $crate::math::mat::Matrix::new([ + $([$($elem),+]),+ + ]) }; } @@ -128,6 +162,19 @@ impl impl Matrix<[[f32; N]; N], Map> { /// Returns the `N`×`N` identity matrix. + /// + /// An identity matrix is a square matrix with ones on the main diagonal + /// and zeroes everywhere else: + /// ```text + /// ⎛ 1 0 ⋯ 0 ⎞ + /// I = ⎜ 0 1 ⎟ + /// ⎜ ⋮ ⋱ 0 ⎟ + /// ⎝ 0 0 1 ⎠ + /// ``` + /// It is the neutral element of matrix multiplication: + /// **A · I** = **I · A** = **A**, as well as matrix-vector + /// multiplication: **I·v** = **v**. + pub const fn identity() -> Self { let mut els = [[0.0; N]; N]; let mut i = 0; @@ -146,19 +193,31 @@ impl Matrix<[[f32; N]; N], Map> { } impl Mat4x4 { - /// Constructs a matrix from a set of basis vectors. + /// Constructs a matrix from a linear basis. /// - /// The vector do not need to be linearly independent. - pub const fn from_basis( + /// The basis does not have to be orthonormal. + pub const fn from_linear( i: Vec3, j: Vec3, k: Vec3, ) -> Mat4x4> { - let (i, j, k) = (i.0, j.0, k.0); + Self::from_affine(i, j, k, Point3::origin()) + } + + /// Constructs a matrix from an affine basis, or frame. + /// + /// The basis does not have to be orthonormal. + pub const fn from_affine( + i: Vec3, + j: Vec3, + k: Vec3, + o: Point3, + ) -> Mat4x4> { + let (o, i, j, k) = (o.0, i.0, j.0, k.0); mat![ - i[0], j[0], k[0], 0.0; - i[1], j[1], k[1], 0.0; - i[2], j[2], k[2], 0.0; + i[0], j[0], k[0], o[0]; + i[1], j[1], k[1], o[1]; + i[2], j[2], k[2], o[2]; 0.0, 0.0, 0.0, 1.0 ] } @@ -175,7 +234,7 @@ where /// the resulting transformation is equivalent to first applying `other` /// and then `self`. More succinctly, /// ```text - /// (𝗠 ∘ 𝗡)𝘃 = 𝗠(𝗡𝘃) + /// (𝗠 ∘ 𝗡) 𝘃 = 𝗠(𝗡 𝘃) /// ``` /// for some matrices 𝗠 and 𝗡 and a vector 𝘃. #[must_use] @@ -208,72 +267,194 @@ where } } -impl Mat3x3> { - /// Maps the real 2-vector 𝘃 from basis `Src` to basis `Dst`. +impl Mat2x2> { + /// Returns the determinant of `self`. /// - /// Computes the matrix–vector multiplication 𝝡𝘃 where 𝘃 is interpreted as - /// a column vector with an implicit 𝘃2 component with value 1: + /// # Examples + /// ``` + /// use retrofire_core::math::{Mat2x2, mat::RealToReal}; /// - /// ```text - /// / M00 · · \ / v0 \ / v0' \ - /// Mv = | · · · | | v1 | = | v1' | - /// \ · · M22 / \ 1 / \ 1 / + /// let double: Mat2x2> = [[2.0, 0.0], [0.0, 2.0]].into(); + /// assert_eq!(double.determinant(), 4.0); + /// + /// let singular: Mat2x2> = [[1.0, 0.0], [2.0, 0.0]].into(); + /// assert_eq!(singular.determinant(), 0.0); + /// ``` + pub const fn determinant(&self) -> f32 { + let [[a, b], [c, d]] = self.0; + a * d - b * c + } + + /// Returns the [inverse][Self::inverse] of `self`, or `None` if `self` + /// is not invertible. + /// + /// A matrix is invertible if and only if its [determinant][Self::determinant] + /// is nonzero. A non-invertible matrix is also called singular. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{Mat2x2, mat::RealToReal}; + /// + /// let rotate_90: Mat2x2> = [[0.0, -1.0], [1.0, 0.0]].into(); + /// let rotate_neg_90 = rotate_90.checked_inverse(); + /// + /// assert_eq!(rotate_neg_90, Some([[0.0, 1.0], [-1.0, 0.0]].into())); + /// + /// let singular: Mat2x2> = [[1.0, 0.0], [2.0, 0.0]].into(); + /// assert_eq!(singular.checked_inverse(), None); /// ``` #[must_use] - pub fn apply(&self, v: &Vec2) -> Vec2 { - let v = [v.x(), v.y(), 1.0].into(); // TODO w=0.0 - array::from_fn(|i| self.row_vec(i).dot(&v)).into() + pub const fn checked_inverse( + &self, + ) -> Option>> { + let det = self.determinant(); + // No approx_eq in const :/ + if det.abs() < 1e-6 { + return None; + } + let r_det = 1.0 / det; + let [[a, b], [c, d]] = self.0; + Some(mat![ + r_det * d, r_det * -b; + r_det * -c, r_det * a + ]) } - // TODO Add trait to overload apply or similar + /// Returns the inverse of `self`, if it exists. + /// + /// A matrix is invertible if and only if its [determinant][Self::determinant] + /// is nonzero. A non-invertible matrix is also called singular. + /// + /// # Panics + /// If `self` has no inverse. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{Mat2x2, mat::RealToReal, vec2}; + /// + /// let rotate_90: Mat2x2> = [[0.0, -1.0], [1.0, 0.0]].into(); + /// let rotate_neg_90 = rotate_90.inverse(); + /// + /// assert_eq!(rotate_neg_90.0, [[0.0, 1.0], [-1.0, 0.0]]); + /// assert_eq!(rotate_90.then(&rotate_neg_90), Mat2x2::identity()) + /// ``` + /// ```should_panic + /// # use retrofire_core::math::{Mat2x2, mat::RealToReal}; + /// + /// // This matrix has no inverse + /// let singular: Mat2x2> = [[1.0, 0.0], [2.0, 0.0]].into(); + /// + /// // This will panic + /// let _ = singular.inverse(); + /// ``` #[must_use] - pub fn apply_pt(&self, p: &Point2) -> Point2 { - let p = [p.x(), p.y(), 1.0].into(); - array::from_fn(|i| self.row_vec(i).dot(&p)).into() + pub const fn inverse(&self) -> Mat2x2> { + self.checked_inverse() + .expect("matrix cannot be singular or near-singular") } } -impl Mat4x4> { - /// Maps the real 3-vector 𝘃 from basis `Src` to basis `Dst`. +impl Mat3x3> { + /// Returns the determinant of `self`. + pub const fn determinant(&self) -> f32 { + let [a, b, c] = self.0[0]; + + // assert!(g == 0.0 && h == 0.0 && i == 1.0); + // TODO If affine (as should be), reduces to: + // a * e - b * d + + a * self.cofactor(0, 0) + + b * self.cofactor(0, 1) + + c * self.cofactor(0, 2) + } + + /// Returns the cofactor of the element at the given row and column. /// - /// Computes the matrix–vector multiplication 𝝡𝘃 where 𝘃 is interpreted as - /// a column vector with an implicit 𝘃3 component with value 1: + /// Cofactors are used to compute the inverse of a matrix. A cofactor is + /// calculated as follows: /// - /// ```text - /// / M00 · · · \ / v0 \ / v0' \ - /// Mv = | · · · · | | v1 | = | v1' | - /// | · · · · | | v2 | | v2' | - /// \ · · · M33 / \ 1 / \ 1 / + /// 1. Remove the given row and column from `self` to get a 2x2 submatrix; + /// 2. Compute its determinant; + /// 3. If exactly one of `row` and `col` is even, multiply by -1. + /// + /// # Examples /// ``` - #[must_use] - pub fn apply(&self, v: &Vec3) -> Vec3 { - let v = [v.x(), v.y(), v.z(), 1.0].into(); // TODO w=0.0 - array::from_fn(|i| self.row_vec(i).dot(&v)).into() + /// use retrofire_core::{mat, math::Mat3x3, math::mat::RealToReal}; + /// + /// let mat: Mat3x3> = mat![ + /// 1.0, 2.0, 3.0; + /// 4.0, 5.0, 6.0; + /// 7.0, 8.0, 9.0 + /// ]; + /// // Remove row 0 and col 1, giving [[4.0, 6.0], [7.0, 9.0]]. + /// // The determinant of this submatrix is 4.0 * 7.0 - 6.0 * 9.0. + /// // Multiply by -1 because row is even and col is odd. + /// assert_eq!(mat.cofactor(0, 1), 6.0 * 7.0 - 4.0 * 9.0); + /// ``` + #[inline] + pub const fn cofactor(&self, row: usize, col: usize) -> f32 { + // This automatically takes care of the negation + let r1 = (row + 1) % 3; + let r2 = (row + 2) % 3; + let c1 = (col + 1) % 3; + let c2 = (col + 2) % 3; + self.0[r1][c1] * self.0[r2][c2] - self.0[r1][c2] * self.0[r2][c1] } - // TODO Add trait to overload apply or similar + /// Returns the inverse of `self`, or `None` if `self` is singular. #[must_use] - pub fn apply_pt(&self, p: &Point3) -> Point3 { - let p = [p.x(), p.y(), p.z(), 1.0].into(); - array::from_fn(|i| self.row_vec(i).dot(&p)).into() + pub fn checked_inverse(&self) -> Option>> { + let det = self.determinant(); + if det.abs() < 1e-6 { + return None; + } + + // Compute cofactors + let c_a = self.cofactor(0, 0); // = e + let c_b = self.cofactor(0, 1); // = d + let c_c = self.cofactor(0, 2); // = 0 + let c_d = self.cofactor(1, 0); // = b + let c_e = self.cofactor(1, 1); // = a + let c_f = self.cofactor(1, 2); // = 0 + let c_g = self.cofactor(2, 0); // = b * f - c * e + let c_h = self.cofactor(2, 1); // = a * f - c * d + let c_i = self.cofactor(2, 2); // = a * e - b * d + + let r_det = 1.0 / det; + // Inverse is transpose of cofactor matrix, divided by determinant + let abc = r_det * vec3(c_a, c_d, c_g); + let def = r_det * vec3(c_b, c_e, c_h); + let ghi = r_det * vec3(c_c, c_f, c_i); + + Some(Mat3x3::from_rows(abc, def, ghi)) + } + + pub fn inverse(&self) -> Mat3x3> { + self.checked_inverse() + .expect("matrix cannot be singular or near-singular") } + const fn from_rows(i: Vec3, j: Vec3, k: Vec3) -> Self { + Self::new([i.0, j.0, k.0]) + } +} + +impl Mat4x4> { /// Returns the determinant of `self`. /// /// Given a matrix M, /// ```text - /// / a b c d \ - /// M = | e f g h | - /// | i j k l | - /// \ m n o p / + /// ⎛ a b c d ⎞ + /// M = ⎜ e f g h ⎟ + /// ⎜ i j k l ⎟ + /// ⎝ m n o p ⎠ /// ``` - /// its determinant can be computed by recursively computing - /// the determinants of sub-matrices on rows 1.. and multiplying - /// them by the elements on row 0: + /// its determinant can be computed by recursively computing the determinants + /// of sub-matrices on rows 1..4 and multiplying them by the elements on row 0: /// ```text - /// | f g h | | e g h | - /// det(M) = a · | j k l | - b · | i k l | + - ··· - /// | n o p | | m o p | + /// ⎜ f g h ⎜ ⎜ e g h ⎜ + /// det(M) = a · ⎜ j k l ⎜ - b · ⎜ i k l ⎜ + - ··· + /// ⎜ n o p ⎜ ⎜ m o p ⎜ /// ``` pub fn determinant(&self) -> f32 { let [[a, b, c, d], r, s, t] = self.0; @@ -288,7 +469,7 @@ impl Mat4x4> { /// Returns the inverse matrix of `self`. /// /// The inverse 𝝡-1 of matrix 𝝡 is a matrix that, when - /// composed with 𝝡, results in the identity matrix: + /// composed with 𝝡, results in the [identity](Self::identity) matrix: /// /// 𝝡 ∘ 𝝡-1 = 𝝡-1 ∘ 𝝡 = 𝐈 /// @@ -304,17 +485,16 @@ impl Mat4x4> { /// suffer from imprecision or numerical instability in certain cases. /// /// # Panics - /// If `self` is singular or near-singular: - /// * Panics in debug mode. - /// * Does not panic in release mode, but the result may be inaccurate - /// or contain `Inf`s or `NaN`s. + /// If debug assertions are enabled, panics if `self` is singular or near-singular. + /// If not enabled, the return value is unspecified and may contain non-finite + /// values (infinities and NaNs). #[must_use] pub fn inverse(&self) -> Mat4x4> { use super::float::f32; if cfg!(debug_assertions) { let det = self.determinant(); assert!( - f32::abs(det) > f32::EPSILON, + !det.approx_eq(&0.0), "a singular, near-singular, or non-finite matrix does not \ have a well-defined inverse (determinant = {det})" ); @@ -349,9 +529,9 @@ impl Mat4x4> { for idx in 0..4 { let pivot = (idx..4) .max_by(|&r1, &r2| { - let v1 = f32::abs(this.0[r1][idx]); - let v2 = f32::abs(this.0[r2][idx]); - v1.partial_cmp(&v2).unwrap() + let v1 = this.0[r1][idx].abs(); + let v2 = this.0[r2][idx].abs(); + v1.total_cmp(&v2) }) .unwrap(); @@ -388,25 +568,6 @@ impl Mat4x4> { } } -impl Mat4x4> { - /// Maps the real 3-vector 𝘃 from basis 𝖡 to the projective 4-space. - /// - /// Computes the matrix–vector multiplication 𝝡𝘃 where 𝘃 is interpreted as - /// a column vector with an implicit 𝘃3 component with value 1: - /// - /// ```text - /// / M00 · · \ / v0 \ / v0' \ - /// Mv = | · | | v1 | = | v1' | - /// | · | | v2 | | v2' | - /// \ · · M33 / \ 1 / \ v3' / - /// ``` - #[must_use] - pub fn apply(&self, p: &Point3) -> ProjVec3 { - let v = Vector::new([p.x(), p.y(), p.z(), 1.0]); - from_fn(|i| self.row_vec(i).dot(&v)).into() - } -} - // // Local trait impls // @@ -437,6 +598,194 @@ impl LinearMap for () { type Dest = (); } +impl ApproxEq for Matrix +where + Repr: ApproxEq, +{ + fn approx_eq_eps(&self, other: &Self, rel_eps: &E) -> bool { + self.0.approx_eq_eps(&other.0, rel_eps) + } + + fn relative_epsilon() -> E { + Repr::relative_epsilon() + } +} + +// Apply trait impls + +impl Apply> for Mat2x2> { + type Output = Vec2; + + /// Maps a real 2-vector from basis `Src` to basis `Dst`. + /// + /// Computes the matrix–vector multiplication **MV** where **v** is + /// interpreted as a column vector: + /// + /// ```text + /// Mv = ⎛ M00 M01 ⎞ ⎛ v0 ⎞ = ⎛ v0' ⎞ + /// ⎝ M10 M11 ⎠ ⎝ v1 ⎠ ⎝ v1' ⎠ + /// ``` + fn apply(&self, v: &Vec2) -> Vec2 { + vec2(self.row_vec(0).dot(v), self.row_vec(1).dot(v)) + } +} + +impl Apply> for Mat2x2> { + type Output = Point2; + + /// Maps a real 2-vector from basis `Src` to basis `Dst`. + /// + /// Computes the matrix–point multiplication **M***p* where *p* is + /// interpreted as a column vector: + /// + /// ```text + /// Mp = ⎛ M00 M01 ⎞ ⎛ v0 ⎞ = ⎛ v0' ⎞ + /// ⎝ M10 M11 ⎠ ⎝ v1 ⎠ ⎝ v1' ⎠ + /// ``` + fn apply(&self, pt: &Point2) -> Point2 { + self.apply(&pt.to_vec()).to_pt() + } +} + +impl Apply> for Mat3x3> { + type Output = Vec2; + + /// Maps a real 2-vector from basis `Src` to basis `Dst`. + /// + /// Computes the matrix–vector multiplication 𝝡𝘃 where 𝘃 is interpreted as + /// a column vector with an implicit 𝘃2 component with value 0: + /// + /// ```text + /// ⎛ M00 · · ⎞ ⎛ v0 ⎞ ⎛ v0' ⎞ + /// Mv = ⎜ · · · ⎟ ⎜ v1 ⎟ = ⎜ v1' ⎟ + /// ⎝ · · M22 ⎠ ⎝ 0 ⎠ ⎝ 0 ⎠ + /// ``` + fn apply(&self, v: &Vec2) -> Vec2 { + // TODO can't use vec3, as space has to be Real<2> to match row_vec + let v = Vector::new([v.x(), v.y(), 0.0]); + vec2(self.row_vec(0).dot(&v), self.row_vec(1).dot(&v)) + } +} + +impl Apply> for Mat3x3> { + type Output = Point2; + + /// Maps a real 2-point from basis `Src` to basis `Dst`. + /// + /// Computes the affine matrix–point multiplication 𝝡*p* where *p* is interpreted + /// as a column vector with an implicit *p*2 component with value 1: + /// + /// ```text + /// ⎛ M00 · · ⎞ ⎛ p0 ⎞ ⎛ p0' ⎞ + /// Mp = ⎜ · · · ⎟ ⎜ p1 ⎟ = ⎜ p1' ⎟ + /// ⎝ · · M22 ⎠ ⎝ 1 ⎠ ⎝ 1 ⎠ + /// ``` + fn apply(&self, p: &Point2) -> Point2 { + let v = Vector::new([p.x(), p.y(), 1.0]); + pt2(self.row_vec(0).dot(&v), self.row_vec(1).dot(&v)) + } +} + +impl Apply> for Mat3x3> { + type Output = Vec3; + + /// Maps a real 3-vector from basis `Src` to basis `Dst`. + /// + /// Computes the matrix–vector multiplication **Mv** where **v** is + /// interpreted as a column vector: + /// + /// ```text + /// ⎛ M00 · · ⎞ ⎛ v0 ⎞ ⎛ v0' ⎞ + /// Mv = ⎜ · · · ⎟ ⎜ v1 ⎟ = ⎜ v1' ⎟ + /// ⎝ · · M22 ⎠ ⎝ v2 ⎠ ⎝ v2' ⎠ + /// ``` + fn apply(&self, v: &Vec3) -> Vec3 { + vec3( + self.row_vec(0).dot(v), + self.row_vec(1).dot(v), + self.row_vec(2).dot(v), + ) + } +} + +impl Apply> for Mat3x3> { + type Output = Point3; + + /// Maps a real 3-point from basis `Src` to basis `Dst`. + /// + /// Computes the linear matrix–point multiplication **M***p* where *p* is + /// interpreted as a column vector: + /// + /// ```text + /// ⎛ M00 · · ⎞ ⎛ p0 ⎞ ⎛ p0' ⎞ + /// Mp = ⎜ · · · ⎟ ⎜ p1 ⎟ = ⎜ p1' ⎟ + /// ⎝ · · M22 ⎠ ⎝ p2 ⎠ ⎝ p2' ⎠ + /// ``` + fn apply(&self, p: &Point3) -> Point3 { + self.apply(&p.to_vec()).to_pt() + } +} + +impl Apply> for Mat4x4> { + type Output = Vec3; + + /// Maps a real 3-vector from basis `Src` to basis `Dst`. + /// + /// Computes the matrix–vector multiplication **Mv** where **v** is interpreted + /// as a column vector with an implicit **v**3 component with value 0: + /// + /// ```text + /// ⎛ M00 · · · ⎞ ⎛ v0 ⎞ ⎛ v0' ⎞ + /// Mv = ⎜ · · · · ⎟ ⎜ v1 ⎟ = ⎜ v1' ⎟ + /// ⎜ · · · · ⎟ ⎜ v2 ⎟ ⎜ v2' ⎟ + /// ⎝ · · · M33 ⎠ ⎝ 0 ⎠ ⎝ 0 ⎠ + /// ``` + fn apply(&self, v: &Vec3) -> Vec3 { + let v = [v.x(), v.y(), v.z(), 0.0].into(); + array::from_fn(|i| self.row_vec(i).dot(&v)).into() + } +} + +impl Apply> for Mat4x4> { + type Output = Point3; + + /// Maps a real 3-point from basis `Src` to basis `Dst`. + /// + /// Computes the affine matrix–point multiplication 𝝡*p* where *p* is interpreted + /// as a column vector with an implicit *p*3 component with value 1: + /// + /// ```text + /// ⎛ M00 · · · ⎞ ⎛ p0 ⎞ ⎛ p0' ⎞ + /// Mp = ⎜ · · · · ⎟ ⎜ p1 ⎟ = ⎜ p1' ⎟ + /// ⎜ · · · · ⎟ ⎜ p2 ⎟ ⎜ p2' ⎟ + /// ⎝ · · · M33 ⎠ ⎝ 1 ⎠ ⎝ 1 ⎠ + /// ``` + fn apply(&self, p: &Point3) -> Point3 { + let p = [p.x(), p.y(), p.z(), 1.0].into(); + array::from_fn(|i| self.row_vec(i).dot(&p)).into() + } +} + +impl Apply> for Mat4x4> { + type Output = ProjVec3; + + /// Maps the real 3-point *p* from basis B to the projective 3-space. + /// + /// Computes the matrix–point multiplication **M***p* where *p* is interpreted + /// as a column vector with an implicit *p*3 component with value 1: + /// + /// ```text + /// ⎛ M00 · · ⎞ ⎛ p0 ⎞ ⎛ p0' ⎞ + /// Mp = ⎜ · ⎟ ⎜ p1 ⎟ = ⎜ p1' ⎟ + /// ⎜ · ⎟ ⎜ p2 ⎟ ⎜ p2' ⎟ + /// ⎝ · · M33 ⎠ ⎝ 1 ⎠ ⎝ p3' ⎠ + /// ``` + fn apply(&self, p: &Point3) -> ProjVec3 { + let v = Vector::new([p.x(), p.y(), p.z(), 1.0]); + array::from_fn(|i| self.row_vec(i).dot(&v)).into() + } +} + // // Foreign trait impls // @@ -460,7 +809,7 @@ impl Debug fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { writeln!(f, "Matrix<{:?}>[", Map::default())?; for i in 0..M { - writeln!(f, " {:6.2?}", self.0[i])?; + writeln!(f, " {:4?}", self.0[i])?; } write!(f, "]") } @@ -501,7 +850,7 @@ pub const fn scale(s: Vec3) -> Mat4x4> { } pub const fn scale3(x: f32, y: f32, z: f32) -> Mat4x4> { - mat! [ + mat![ x, 0.0, 0.0, 0.0; 0.0, y, 0.0, 0.0; 0.0, 0.0, z, 0.0; @@ -524,9 +873,9 @@ pub const fn translate3(x: f32, y: f32, z: f32) -> Mat4x4> { } #[cfg(feature = "fp")] -use super::{Angle, ApproxEq}; +use super::Angle; -/// Returns a matrix applying a rotation such that the original y axis +/// Returns a matrix applying a rotation such that the original y-axis /// is now parallel with `new_y` and the new z axis is orthogonal to /// both `x` and `new_y`. /// @@ -541,7 +890,7 @@ pub fn orient_y(new_y: Vec3, x: Vec3) -> Mat4x4> { orient(new_y, x.cross(&new_y).normalize()) } /// Returns a matrix applying a rotation such that the original z axis -/// is now parallel with `new_z` and the new y axis is orthogonal to +/// is now parallel with `new_z` and the new y-axis is orthogonal to /// both `new_z` and `x`. /// /// Returns an orthogonal basis. If `new_z` and `x` are unit vectors, @@ -571,12 +920,12 @@ fn orient(new_y: Vec3, new_z: Vec3) -> Mat4x4> { !new_x.len_sqr().approx_eq(&0.0), "{new_y:?} × {new_z:?} non-finite or too close to zero vector" ); - Mat4x4::from_basis(new_x, new_y, new_z) + Mat4x4::from_linear(new_x, new_y, new_z) } // TODO constify rotate_* functions once we have const trig functions -/// Returns a matrix applying a 3D rotation about the x axis. +/// Returns a matrix applying a 3D rotation about the x-axis. #[cfg(feature = "fp")] pub fn rotate_x(a: Angle) -> Mat4x4> { let (sin, cos) = a.sin_cos(); @@ -587,7 +936,7 @@ pub fn rotate_x(a: Angle) -> Mat4x4> { 0.0, 0.0, 0.0, 1.0; ] } -/// Returns a matrix applying a 3D rotation about the y axis. +/// Returns a matrix applying a 3D rotation about the y-axis. #[cfg(feature = "fp")] pub fn rotate_y(a: Angle) -> Mat4x4> { let (sin, cos) = a.sin_cos(); @@ -636,6 +985,7 @@ pub fn rotate(axis: Vec3, a: Angle) -> Mat4x4> { } let z_to_axis = orient_z(axis.normalize(), other); + // Inverse of orthogonal matrix is its transpose let axis_to_z = z_to_axis.transpose(); axis_to_z.then(&rotate_z(a)).then(&z_to_axis) } @@ -664,7 +1014,7 @@ pub fn perspective( assert!(focal_ratio > 0.0, "focal ratio must be positive"); assert!(aspect_ratio > 0.0, "aspect ratio must be positive"); assert!(near > 0.0, "near must be positive"); - assert!(!near_far.is_empty(), "far must be greater than near"); + assert!(far > near, "far must be greater than near"); let e00 = focal_ratio; let e11 = e00 * aspect_ratio; @@ -697,9 +1047,9 @@ pub fn orthographic(lbn: Point3, rtf: Point3) -> Mat4x4 { /// Creates a viewport transform matrix with the given pixel space bounds. /// -/// A viewport matrix is used to transform points from the NDC space to screen -/// space for rasterization. NDC coordinates (-1, -1, z) are mapped to -/// `bounds.start` and NDC coordinates (1, 1, z) to `bounds.end`. +/// A viewport matrix is used to transform points from the NDC space to +/// screen space for rasterization. NDC coordinates (-1, -1, z) are mapped +/// to `bounds.start` and NDC coordinates (1, 1, z) to `bounds.end`. pub fn viewport(bounds: Range) -> Mat4x4 { let s = bounds.start.map(|c| c as f32); let e = bounds.end.map(|c| c as f32); @@ -717,7 +1067,7 @@ pub fn viewport(bounds: Range) -> Mat4x4 { #[cfg(test)] mod tests { use crate::assert_approx_eq; - use crate::math::{pt2, pt3, vec2, vec3}; + use crate::math::pt3; #[cfg(feature = "fp")] use crate::math::degs; @@ -737,9 +1087,42 @@ mod tests { const Z: Vec3 = Vec3::Z; const O: Vec3 = Vec3::new([0.0; 3]); + mod mat2x2 { + use super::*; + + #[test] + fn determinant_of_identity_is_one() { + let id = Mat2x2::>::identity(); + assert_eq!(id.determinant(), 1.0); + } + #[test] + fn determinant_of_reflection_is_negative_one() { + let refl: Mat2x2> = [[0.0, 1.0], [1.0, 0.0]].into(); + assert_eq!(refl.determinant(), -1.0); + } + + #[test] + fn inverse_of_identity_is_identity() { + let id = Mat2x2::>::identity(); + assert_eq!(id.inverse(), id); + } + #[test] + fn inverse_of_inverse_is_original() { + let m: Mat2x2> = [[0.5, 1.5], [1.0, -0.5]].into(); + let m_inv: Mat2x2> = m.inverse(); + assert_approx_eq!(m_inv.inverse(), m); + } + #[test] + fn composition_of_inverse_is_identity() { + let m: Mat2x2> = [[0.5, 1.5], [1.0, -0.5]].into(); + let m_inv: Mat2x2> = m.inverse(); + assert_approx_eq!(m.compose(&m_inv), Mat2x2::identity()); + assert_approx_eq!(m.then(&m_inv), Mat2x2::identity()); + } + } + mod mat3x3 { use super::*; - use crate::math::pt2; const MAT: Mat3x3 = mat![ 0.0, 1.0, 2.0; @@ -755,25 +1138,28 @@ mod tests { #[test] fn composition() { - let t: Mat3x3> = mat![ + let tr: Mat3x3> = mat![ 1.0, 0.0, 2.0; 0.0, 1.0, -3.0; 0.0, 0.0, 1.0; ]; - let s: Mat3x3> = mat![ + let sc: Mat3x3> = mat![ -1.0, 0.0, 0.0; 0.0, 2.0, 0.0; 0.0, 0.0, 1.0; ]; - let ts = t.then(&s); - let st = s.then(&t); + let tr_sc = tr.then(&sc); + let sc_tr = sc.then(&tr); - assert_eq!(ts, s.compose(&t)); - assert_eq!(st, t.compose(&s)); + assert_eq!(tr_sc, sc.compose(&tr)); + assert_eq!(sc_tr, tr.compose(&sc)); + + assert_eq!(tr_sc.apply(&vec2(1.0, 2.0)), vec2(-1.0, 4.0)); + assert_eq!(sc_tr.apply(&vec2(1.0, 2.0)), vec2(-1.0, 4.0)); - assert_eq!(ts.apply(&vec2(0.0, 0.0)), vec2(-2.0, -6.0)); - assert_eq!(st.apply(&vec2(0.0, 0.0)), vec2(2.0, -3.0)); + assert_eq!(tr_sc.apply(&pt2(1.0, 2.0)), pt2(-3.0, -2.0)); + assert_eq!(sc_tr.apply(&pt2(1.0, 2.0)), pt2(1.0, 1.0)); } #[test] @@ -784,7 +1170,7 @@ mod tests { 0.0, 0.0, 1.0; ]; assert_eq!(m.apply(&vec2(1.0, 2.0)), vec2(2.0, -6.0)); - assert_eq!(m.apply_pt(&pt2(2.0, -1.0)), pt2(4.0, 3.0)); + assert_eq!(m.apply(&pt2(2.0, -1.0)), pt2(4.0, 3.0)); } #[test] @@ -794,8 +1180,51 @@ mod tests { 0.0, 1.0, -3.0; 0.0, 0.0, 1.0; ]; - assert_eq!(m.apply(&vec2(1.0, 2.0)), vec2(3.0, -1.0)); - assert_eq!(m.apply_pt(&pt2(2.0, -1.0)), pt2(4.0, -4.0)); + assert_eq!(m.apply(&vec2(1.0, 2.0)), vec2(1.0, 2.0)); + assert_eq!(m.apply(&pt2(2.0, -1.0)), pt2(4.0, -4.0)); + } + + #[test] + fn inverse_of_identity_is_identity() { + let i = Mat3x3::>::identity(); + assert_eq!(i.inverse(), i); + } + #[test] + fn inverse_of_scale_is_reciprocal_scale() { + let scale: Mat3x3> = mat![ + 2.0, 0.0, 0.0; + 0.0, -3.0, 0.0; + 0.0, 0.0, 4.0; + ]; + assert_eq!( + scale.inverse(), + mat![ + 1.0/2.0, 0.0, 0.0; + 0.0, -1.0/3.0, 0.0; + 0.0, 0.0, 1.0/4.0 + ] + ); + } + #[test] + fn matrix_composed_with_inverse_is_identity() { + let mat: Mat3x3> = mat![ + 1.0, -2.0, 2.0; + 3.0, 4.0, -3.0; + 0.0, 0.0, 1.0; + ]; + let composed = mat.compose(&mat.inverse()); + assert_approx_eq!(composed, Mat3x3::identity()); + } + + #[test] + fn singular_matrix_has_no_inverse() { + let singular: Mat3x3> = mat![ + 1.0, 2.0, 0.0; + 0.0, 0.0, 0.0; + 0.0, 0.0, 1.0; + ]; + + assert_approx_eq!(singular.checked_inverse(), None); } #[test] @@ -803,9 +1232,9 @@ mod tests { assert_eq!( alloc::format!("{MAT:?}"), r#"Matrix[ - [ 0.00, 1.00, 2.00] - [ 10.00, 11.00, 12.00] - [ 20.00, 21.00, 22.00] + [ 0.0, 1.0, 2.0] + [10.0, 11.0, 12.0] + [20.0, 21.0, 22.0] ]"# ); } @@ -813,7 +1242,6 @@ mod tests { mod mat4x4 { use super::*; - use crate::math::pt3; const MAT: Mat4x4 = mat![ 0.0, 1.0, 2.0, 3.0; @@ -839,8 +1267,9 @@ mod tests { assert_eq!(ts, s.compose(&t)); assert_eq!(st, t.compose(&s)); - assert_eq!(ts.apply(&O.to()), vec3::<_, Basis1>(3.0, 4.0, 3.0)); - assert_eq!(st.apply(&O.to()), vec3::<_, Basis2>(1.0, 2.0, 3.0)); + let o = ::origin(); + assert_eq!(ts.apply(&o.to()), pt3::<_, Basis1>(3.0, 4.0, 3.0)); + assert_eq!(st.apply(&o.to()), pt3::<_, Basis2>(1.0, 2.0, 3.0)); } #[test] @@ -851,7 +1280,7 @@ mod tests { assert_eq!(m.apply(&v), vec3(0.0, -8.0, -9.0)); let p = pt3(4.0, 0.0, -3.0); - assert_eq!(m.apply_pt(&p), pt3(4.0, 0.0, -9.0)); + assert_eq!(m.apply(&p), pt3(4.0, 0.0, -9.0)); } #[test] @@ -859,10 +1288,10 @@ mod tests { let m = translate3(1.0, 2.0, 3.0); let v = vec3(0.0, 5.0, -3.0); - assert_eq!(m.apply(&v), vec3(1.0, 7.0, 0.0)); + assert_eq!(m.apply(&v), vec3(0.0, 5.0, -3.0)); let p = pt3(3.0, 5.0, 0.0); - assert_eq!(m.apply_pt(&p), pt3(4.0, 7.0, 3.0)); + assert_eq!(m.apply(&p), pt3(4.0, 7.0, 3.0)); } #[cfg(feature = "fp")] @@ -874,7 +1303,7 @@ mod tests { assert_approx_eq!(m.apply(&Z), Y); assert_approx_eq!( - m.apply_pt(&pt3(0.0, -2.0, 0.0)), + m.apply(&pt3(0.0, -2.0, 0.0)), pt3(0.0, 0.0, 2.0) ); } @@ -888,7 +1317,7 @@ mod tests { assert_approx_eq!(m.apply(&X), Z); assert_approx_eq!( - m.apply_pt(&pt3(0.0, 0.0, -2.0)), + m.apply(&pt3(0.0, 0.0, -2.0)), pt3(2.0, 0.0, 0.0) ); } @@ -902,7 +1331,7 @@ mod tests { assert_approx_eq!(m.apply(&Y), X); assert_approx_eq!( - m.apply_pt(&(pt3(-2.0, 0.0, 0.0))), + m.apply(&(pt3(-2.0, 0.0, 0.0))), pt3(0.0, 2.0, 0.0) ); } @@ -941,7 +1370,7 @@ mod tests { #[test] fn from_basis() { - let m = Mat4x4::from_basis(Y, 2.0 * Z, -3.0 * X); + let m = Mat4x4::from_linear(Y, 2.0 * Z, -3.0 * X); assert_eq!(m.apply(&X), Y); assert_eq!(m.apply(&Y), 2.0 * Z); assert_eq!(m.apply(&Z), -3.0 * X); @@ -953,13 +1382,13 @@ mod tests { let m = orient_y(Y, X); assert_eq!(m.apply(&X), X); - assert_eq!(m.apply_pt(&X.to_pt()), X.to_pt()); + assert_eq!(m.apply(&X.to_pt()), X.to_pt()); assert_eq!(m.apply(&Y), Y); - assert_eq!(m.apply_pt(&Y.to_pt()), Y.to_pt()); + assert_eq!(m.apply(&Y.to_pt()), Y.to_pt()); assert_eq!(m.apply(&Z), Z); - assert_eq!(m.apply_pt(&Z.to_pt()), Z.to_pt()); + assert_eq!(m.apply(&Z.to_pt()), Z.to_pt()); } #[cfg(feature = "fp")] @@ -968,13 +1397,13 @@ mod tests { let m = orient_y(Z, X); assert_eq!(m.apply(&X), X); - assert_eq!(m.apply_pt(&X.to_pt()), X.to_pt()); + assert_eq!(m.apply(&X.to_pt()), X.to_pt()); assert_eq!(m.apply(&Y), Z); - assert_eq!(m.apply_pt(&Y.to_pt()), Z.to_pt()); + assert_eq!(m.apply(&Y.to_pt()), Z.to_pt()); assert_eq!(m.apply(&Z), -Y); - assert_eq!(m.apply_pt(&Z.to_pt()), (-Y).to_pt()); + assert_eq!(m.apply(&Z.to_pt()), (-Y).to_pt()); } #[cfg(feature = "fp")] @@ -983,13 +1412,13 @@ mod tests { let m = orient_z(Y, X); assert_eq!(m.apply(&X), X); - assert_eq!(m.apply_pt(&X.to_pt()), X.to_pt()); + assert_eq!(m.apply(&X.to_pt()), X.to_pt()); assert_eq!(m.apply(&Y), -Z); - assert_eq!(m.apply_pt(&Y.to_pt()), (-Z).to_pt()); + assert_eq!(m.apply(&Y.to_pt()), (-Z).to_pt()); assert_eq!(m.apply(&Z), Y); - assert_eq!(m.apply_pt(&Z.to_pt()), Y.to_pt()); + assert_eq!(m.apply(&Z.to_pt()), Y.to_pt()); } #[test] @@ -997,10 +1426,10 @@ mod tests { assert_eq!( alloc::format!("{MAT:?}"), r#"Matrix[ - [ 0.00, 1.00, 2.00, 3.00] - [ 10.00, 11.00, 12.00, 13.00] - [ 20.00, 21.00, 22.00, 23.00] - [ 30.00, 31.00, 32.00, 33.00] + [ 0.0, 1.0, 2.0, 3.0] + [10.0, 11.0, 12.0, 13.0] + [20.0, 21.0, 22.0, 23.0] + [30.0, 31.0, 32.0, 33.0] ]"# ); } @@ -1044,7 +1473,7 @@ mod tests { #[cfg(feature = "fp")] #[test] - fn mat_times_mat_inverse_is_identity() { + fn matrix_composed_with_inverse_is_identity() { let m = translate3(1.0e3, -2.0e2, 0.0) .then(&scale3(0.5, 100.0, 42.0)) .to::(); @@ -1104,7 +1533,7 @@ mod tests { fn viewport_maps_ndc_to_screen() { let m = viewport(pt2(20, 10)..pt2(620, 470)); - assert_eq!(m.apply_pt(&pt3(-1.0, -1.0, 0.2)), pt3(20.0, 10.0, 0.2)); - assert_eq!(m.apply_pt(&pt3(1.0, 1.0, 0.6)), pt3(620.0, 470.0, 0.6)); + assert_eq!(m.apply(&pt3(-1.0, -1.0, 0.2)), pt3(20.0, 10.0, 0.2)); + assert_eq!(m.apply(&pt3(1.0, 1.0, 0.6)), pt3(620.0, 470.0, 0.6)); } } diff --git a/core/src/math/point.rs b/core/src/math/point.rs index b816a5b9..def997b4 100644 --- a/core/src/math/point.rs +++ b/core/src/math/point.rs @@ -29,7 +29,7 @@ pub const fn pt3(x: Sc, y: Sc, z: Sc) -> Point<[Sc; 3], Real<3, B>> { impl Point { #[inline] - pub fn new(repr: R) -> Self { + pub const fn new(repr: R) -> Self { Self(repr, Pd) } @@ -88,6 +88,12 @@ impl Point<[Sc; N], Sp> { } impl Point<[f32; N], Real> { + /// Returns the canonical origin point (0, …, 0). + #[inline] + pub const fn origin() -> Self { + Self::new([0.0; N]) + } + /// Returns the Euclidean distance between `self` and another point. /// /// # Example @@ -103,6 +109,7 @@ impl Point<[f32; N], Real> { pub fn distance(&self, other: &Self) -> f32 { self.sub(other).len() } + /// Returns the square of the Euclidean distance between `self` and another /// point. /// @@ -279,6 +286,20 @@ impl From for Point { Self(repr, Pd) } } +/* +impl From> for HomVec3 { + fn from(p: Point3) -> Self { + let [x, y, z] = p.0; + [x, y, z, 1.0].into() + } +} + +impl From> for HomVec2 { + fn from(p: Point2) -> Self { + let [x, y] = p.0; + [x, y, 1.0].into() + } +}*/ impl, Sp> Index for Point { type Output = R::Output; diff --git a/core/src/math/rand.rs b/core/src/math/rand.rs index b6782199..143db680 100644 --- a/core/src/math/rand.rs +++ b/core/src/math/rand.rs @@ -2,7 +2,7 @@ use core::{array, fmt::Debug, ops::Range}; -use super::{Point, Point2, Point3, Vec2, Vec3, Vector}; +use super::{Color, Point, Point2, Point3, Vec2, Vec3, Vector}; // // Traits and types @@ -193,7 +193,7 @@ impl Iterator for Iter { /// /// This method never returns `None`. fn next(&mut self) -> Option { - Some(self.0.sample(&mut self.1)) + Some(self.0.sample(self.1)) } } @@ -217,7 +217,7 @@ impl Default for Xorshift64 { // Local trait impls // -/// Uniformly distributed integers. +/// Uniformly distributed signed integers. impl Distrib for Uniform { type Sample = i32; @@ -228,11 +228,11 @@ impl Distrib for Uniform { /// use retrofire_core::math::rand::*; /// let rng = &mut DefaultRng::default(); /// - /// // Simulate rolling a six-sided die - /// let mut iter = Uniform(1..7).samples(rng); - /// assert_eq!(iter.next(), Some(3)); - /// assert_eq!(iter.next(), Some(2)); + /// + /// let mut iter = Uniform(-5i32..6).samples(rng); + /// assert_eq!(iter.next(), Some(0)); /// assert_eq!(iter.next(), Some(4)); + /// assert_eq!(iter.next(), Some(5)); /// ``` fn sample(&self, rng: &mut DefaultRng) -> i32 { let bits = rng.next_bits() as i32; @@ -240,6 +240,58 @@ impl Distrib for Uniform { bits.rem_euclid(self.0.end - self.0.start) + self.0.start } } +/// Uniformly distributed unsigned integers. +impl Distrib for Uniform { + type Sample = u32; + + /// Returns a uniformly distributed `u32` in the range. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::rand::*; + /// let rng = &mut DefaultRng::from_seed(1234); + /// + /// // Simulate rolling a six-sided die + /// let mut rolls: Vec<_> = Uniform(1u32..7) + /// .samples(rng) + /// .take(6) + /// .collect(); + /// assert_eq!(rolls, [2, 4, 6, 6, 3, 1]); + /// ``` + fn sample(&self, rng: &mut DefaultRng) -> u32 { + let bits = rng.next_bits() as u32; + // TODO rem introduces slight bias + bits.rem_euclid(self.0.end - self.0.start) + self.0.start + } +} + +/// Uniformly distributed indices. +impl Distrib for Uniform { + type Sample = usize; + + /// Returns a uniformly distributed `usize` in the range. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::rand::*; + /// let rng = &mut DefaultRng::default(); + /// + /// // Randomly sample elements from a list (with replacement) + /// let beverages = ["water", "tea", "coffee", "Coke", "Red Bull"]; + /// let mut x: Vec<_> = Uniform(0..beverages.len()) + /// .samples(rng) + /// .take(3) + /// .map(|i| beverages[i]) + /// .collect(); + /// + /// assert_eq!(x, ["water", "tea", "Red Bull"]); + /// ``` + fn sample(&self, rng: &mut DefaultRng) -> usize { + let bits = rng.next_bits() as usize; + // TODO rem introduces slight bias + bits.rem_euclid(self.0.end - self.0.start) + self.0.start + } +} /// Uniformly distributed floats. impl Distrib for Uniform { @@ -329,6 +381,22 @@ where .into() } } +impl Distrib for Uniform> +where + Sc: Copy, + Sp: Clone, // TODO Color needs manual Clone etc impls like Vector + Uniform<[Sc; DIM]>: Distrib, +{ + type Sample = Point<[Sc; DIM], Sp>; + + /// Returns a point uniformly sampled from the rectangular volume + /// bounded by `self.0`. + fn sample(&self, rng: &mut DefaultRng) -> Self::Sample { + Uniform(self.0.start.0..self.0.end.0) + .sample(rng) + .into() + } +} #[cfg(feature = "fp")] impl Distrib for UnitCircle { @@ -491,7 +559,7 @@ mod tests { #[test] fn uniform_i32() { - let dist = Uniform(-123..456); + let dist = Uniform(-123i32..456); for r in dist.samples(&mut rng()).take(COUNT) { assert!(-123 <= r && r < 456); } diff --git a/core/src/math/space.rs b/core/src/math/space.rs index 10998b38..b7bfb8b5 100644 --- a/core/src/math/space.rs +++ b/core/src/math/space.rs @@ -3,6 +3,7 @@ //! TODO use core::fmt::{Debug, Formatter}; +use core::iter::zip; use core::marker::PhantomData; use crate::math::vary::{Iter, Vary, ZDiv}; @@ -29,6 +30,28 @@ pub trait Affine: Sized { /// /// `sub` is anti-commutative: `v.sub(w) == w.sub(v).neg()`. fn sub(&self, other: &Self) -> Self::Diff; + + /// Returns an affine combination of points. + /// + /// Given a list of weights (w1, ..., w*n*) and + /// points (P1, ..., P*n*), + /// returns + /// + /// w1 * P1 + ... + w*n* * P*n* + fn combine( + weights: &[S; N], + points: &[Self; N], + ) -> Self + where + Self: Clone, + Self::Diff: Linear, + { + const { assert!(N != 0) } + + let p0 = &points[0]; // ok, asserted N > 0 + zip(&weights[1..], &points[1..]) + .fold(p0.clone(), |res, (w, q)| res.add(&q.sub(p0).mul(*w))) + } } /// Trait for types representing elements of a linear space (vector space). @@ -81,6 +104,9 @@ pub struct Real(PhantomData); #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub struct Proj3; +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct Hom(PhantomData); + impl Affine for f32 { type Space = (); type Diff = Self; diff --git a/core/src/math/vec.rs b/core/src/math/vec.rs index 002da656..84703795 100644 --- a/core/src/math/vec.rs +++ b/core/src/math/vec.rs @@ -13,7 +13,6 @@ use core::{ use crate::math::{ Affine, ApproxEq, Linear, Point, - float::f32, space::{Proj3, Real}, vary::ZDiv, }; @@ -28,9 +27,9 @@ use crate::math::{ /// /// # Type parameters /// * `Repr`: Representation of the scalar components of the vector, -/// for example an array or a SIMD vector. +/// for example an array or a SIMD vector. /// * `Space`: The space that the vector is an element of. A tag type used to -/// prevent mixing up vectors of different spaces and bases. +/// prevent mixing up vectors of different spaces and bases. /// /// # Examples /// TODO examples @@ -44,6 +43,9 @@ pub type Vec3 = Vector<[f32; 3], Real<3, Basis>>; /// A `f32` 4-vector in the projective 3-space over ℝ, aka P3(ℝ). pub type ProjVec3 = Vector<[f32; 4], Proj3>; +//pub type HomVec2 = Vector<[f32; 3], Hom<2, B>>; +//pub type HomVec3 = Vector<[f32; 4], Hom<3, B>>; + /// A 2-vector with `i32` components. pub type Vec2i = Vector<[i32; 2], Real<2, Basis>>; /// A 3-vector with `i32` components. @@ -133,17 +135,15 @@ impl Vector<[f32; N], Sp> { /// Panics in dev mode if `self` is a zero vector. #[inline] #[must_use] - pub fn normalize(&self) -> Self - where - Self: Debug, - { + pub fn normalize(&self) -> Self { #[cfg(feature = "std")] use super::float::RecipSqrt; use super::float::f32; let len_sqr = self.len_sqr(); assert!( len_sqr.is_finite() && !len_sqr.approx_eq_eps(&0.0, &1e-12), - "cannot normalize a near-zero or non-finite vector: {self:?}" + "cannot normalize a near-zero or non-finite vector: {:?}", + self.0 ); *self * f32::recip_sqrt(len_sqr) } @@ -322,18 +322,52 @@ impl Vec2 { } /// Returns `self` rotated 90° counter-clockwise. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::Vec2; + /// + /// assert_eq!(::X.perp(), Vec2::Y); + /// assert_eq!(::Y.perp(), -Vec2::X); + /// ``` + #[inline] pub fn perp(self) -> Self { vec2(-self.y(), self.x()) } /// Returns the "perpendicular dot product" of `self` and `other`. /// - /// This operation is also called a "2D cross product". Like its 3D analog, - /// it satisfies the identity + /// This operation is also called the "2D cross product". Like its 3D analog, + /// it satisfies the following identity: /// /// **a** · **b** = |**a**| |**b**| sin *θ*, /// - /// where θ is the (signed) angle between **a** and **b**. + /// where *θ* is the (signed) angle between **a** and **b**. In particular, + /// the result is zero if **a** and **b** are parallel (or either is zero), + /// positive if the angle from **a** to **b** is positive, and negative if + /// the angle is negative: + /// + /// ```text + /// ^ b ^ a + /// / ^ b / ^ a + /// ^ a \ / \ + /// / \ / \ + /// O O O-----> b + /// + /// a⟂·b = 0 a⟂·b > 0 a⟂·b < 0 + /// ``` + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{vec2, Vec2}; + /// let v: Vec2 = vec2(2.0, 1.0); + /// + /// assert_eq!(v.perp_dot(3.0 * v), 0.0, "v and 3*v are parallel"); + /// assert_eq!(v.perp_dot(-v), 0.0, "v and -v are parallel"); + /// assert! (v.perp_dot(Vec2::X) < 0.0, "X is clockwise from v"); + /// assert! (v.perp_dot(Vec2::Y) > 0.0, "Y is counter-clockwise from v"); + /// ``` + #[inline] pub fn perp_dot(self, other: Self) -> f32 { self.perp().dot(&other) } @@ -506,8 +540,8 @@ impl ApproxEq // Foreign trait impls // -// Manual impls of Copy, Clone, Eq, and PartialEq to avoid -// superfluous where S: Trait bound +// Manual impls of Copy, Clone, Default, Eq, and PartialEq +// to avoid superfluous where S: Trait bound impl Copy for Vector {} @@ -517,7 +551,7 @@ impl Clone for Vector { } } -impl Default for Vector { +impl Default for Vector> { fn default() -> Self { Self(R::default(), Pd) } @@ -554,7 +588,20 @@ impl From for Vector<[Sc; DIM], Sp> { splat(scalar) } } - +/* +impl From> for HomVec2 { + fn from(v: Vec2) -> Self { + let [x, y] = v.0; + [x, y, 0.0].into() + } +} +impl From> for HomVec3 { + fn from(v: Vec3) -> Self { + let [x, y, z] = v.0; + [x, y, z, 0.0].into() + } +} +*/ impl Index for Vector where Self: Affine, @@ -604,26 +651,6 @@ where // Arithmetic traits // -/// Implements an operator trait in terms of an op-assign trait. -macro_rules! impl_op { - ($trait:ident :: $method:ident, $rhs:ty, $op:tt) => { - impl_op!($trait::$method, $rhs, $op, bound=Linear); - }; - ($trait:ident :: $method:ident, $rhs:ty, $op:tt, bound=$bnd:path) => { - impl $trait<$rhs> for Vector - where - Self: $bnd, - { - type Output = Self; - /// TODO docs for macro-generated operators - #[inline] - fn $method(mut self, rhs: $rhs) -> Self { - self $op rhs; self - } - } - }; -} - /// The vector += vector operator. impl AddAssign<::Diff> for Vector where @@ -634,8 +661,6 @@ where *self = Affine::add(&*self, &rhs); } } -// The vector + vector operator. -impl_op!(Add::add, ::Diff, +=, bound=Affine); /// The vector -= vector operator. impl SubAssign<::Diff> for Vector @@ -648,9 +673,6 @@ where } } -// The vector - vector operator. -impl_op!(Sub::sub, ::Diff, -=, bound=Affine); - // The vector *= scalar operator. impl MulAssign<::Scalar> for Vector where @@ -661,8 +683,6 @@ where *self = Linear::mul(&*self, rhs); } } -// The vector * scalar operator. -impl_op!(Mul::mul, ::Scalar, *=); // The vector /= scalar operator. impl DivAssign for Vector @@ -671,14 +691,11 @@ where { #[inline] fn div_assign(&mut self, rhs: f32) { - debug_assert!(f32::abs(rhs) > 1e-7, "divisor {rhs} < epsilon"); + debug_assert!(!rhs.approx_eq(&0.0), "divisor {rhs} < epsilon"); *self = Linear::mul(&*self, rhs.recip()); } } -// The vector / scalar operator. -impl_op!(Div::div, f32, /=, bound=Linear); - /// The vector negation operator. impl Neg for Vector where @@ -726,6 +743,15 @@ where } } +// The vector + vector operator. +impl_op!(Add::add, Vector, ::Diff, +=, bound=Affine); +// The vector - vector operator. +impl_op!(Sub::sub, Vector, ::Diff, -=, bound=Affine); +// The vector * scalar operator. +impl_op!(Mul::mul, Vector, ::Scalar, *=); +// The vector / scalar operator. +impl_op!(Div::div, Vector, f32, /=, bound=Linear); + // // Unit tests // diff --git a/core/src/render.rs b/core/src/render.rs index 6c39dba7..c71eb70a 100644 --- a/core/src/render.rs +++ b/core/src/render.rs @@ -15,20 +15,20 @@ use crate::math::{ vec::ProjVec3, }; -use { +use self::{ clip::{ClipVert, view_frustum}, ctx::DepthSort, raster::Scanline, }; -pub use { +pub use self::{ batch::Batch, cam::Camera, clip::Clip, ctx::Context, shader::{FragmentShader, VertexShader}, stats::Stats, - target::{Framebuf, Target}, + target::{Colorbuf, Framebuf, Target}, tex::{TexCoord, Texture, uv}, text::Text, }; diff --git a/core/src/render/batch.rs b/core/src/render/batch.rs index e714ed28..06e27eb0 100644 --- a/core/src/render/batch.rs +++ b/core/src/render/batch.rs @@ -8,18 +8,19 @@ use crate::{ math::{Mat4x4, Vary}, }; -use super::{Context, NdcToScreen, Shader, Target}; +use super::{Clip, Context, NdcToScreen, Render, Shader, Target}; /// A builder for rendering a chunk of geometry as a batch. /// /// Several values must be assigned before the [`render`][Batch::render] /// method can be called: -/// * [faces][Batch::faces]: A list of triangles, each a triplet of indices -/// into the list of vertices (TODO: handling oob) +/// * [primitives][Batch::primitives]: A list of primitives, each a tuple +/// of indices into the list of vertices (TODO: handling oob) /// * [vertices][Batch::vertices]: A list of vertices /// * [shader][Batch::shader]: The combined vertex and fragment shader used /// * [target][Batch::target]: The render target to render into -/// * [context][Batch::context]: The rendering context and settings used. (TODO: optional?) +/// * [context][Batch::context]: The rendering context and settings used. +/// (TODO: optional?) /// /// Additionally, setting the following values is optional: /// * [uniform][Batch::uniform]: The uniform value passed to the vertex shader @@ -29,8 +30,8 @@ use super::{Context, NdcToScreen, Shader, Target}; // using the same configuration, or several [instances] of the same geometry. // [instances]: https://en.wikipedia.org/wiki/Geometry_instancing #[derive(Clone, Debug, Default)] -pub struct Batch { - faces: Vec>, +pub struct Batch { + prims: Vec, verts: Vec, uniform: Uni, shader: Shd, @@ -46,21 +47,22 @@ macro_rules! update { }}; } -impl Batch<(), (), (), (), Context> { +impl Batch<(), (), (), (), (), Context> { pub fn new() -> Self { Self::default() } } -impl Batch { - /// Sets the faces to be rendered. +impl Batch { + /// Sets the primitives to be rendered. /// - /// The faces are copied into the batch. - pub fn faces(self, faces: impl AsRef<[Tri]>) -> Self { - Self { - faces: faces.as_ref().to_vec(), - ..self - } + /// The primitives are copied into the batch. + pub fn primitives( + self, + prims: impl AsRef<[P]>, + ) -> Batch { + let prims = prims.as_ref().to_vec(); + update!(prims; self verts uniform shader viewport target ctx) } /// Sets the vertices to be rendered. @@ -70,69 +72,78 @@ impl Batch { pub fn vertices( self, verts: impl AsRef<[V]>, - ) -> Batch { + ) -> Batch { let verts = verts.as_ref().to_vec(); - update!(verts; self faces uniform shader viewport target ctx) + update!(verts; self prims uniform shader viewport target ctx) } /// Clones faces and vertices from a mesh to this batch. pub fn mesh( self, mesh: &Mesh, - ) -> Batch, Uni, Shd, Tgt, Ctx> { - let faces = mesh.faces.clone(); + ) -> Batch, Vertex3, Uni, Shd, Tgt, Ctx> { + let prims = mesh.faces.clone(); let verts = mesh.verts.clone(); - update!(verts faces; self uniform shader viewport target ctx) + update!(verts prims; self uniform shader viewport target ctx) } /// Sets the uniform data to be passed to the vertex shaders. - pub fn uniform(self, uniform: U) -> Batch { - update!(uniform; self verts faces shader viewport target ctx) + pub fn uniform( + self, + uniform: U, + ) -> Batch { + update!(uniform; self verts prims shader viewport target ctx) } /// Sets the combined vertex and fragment shader. - pub fn shader>( + pub fn shader>( self, shader: S, - ) -> Batch { - update!(shader; self verts faces uniform viewport target ctx) + ) -> Batch { + update!(shader; self verts prims uniform viewport target ctx) } /// Sets the viewport matrix. pub fn viewport(self, viewport: Mat4x4) -> Self { - update!(viewport; self verts faces uniform shader target ctx) + update!(viewport; self verts prims uniform shader target ctx) } /// Sets the render target. // TODO what bound for T? - pub fn target(self, target: T) -> Batch { - update!(target; self verts faces uniform shader viewport ctx) + pub fn target(self, target: T) -> Batch { + update!(target; self verts prims uniform shader viewport ctx) } /// Sets the rendering context. - pub fn context(self, ctx: &Context) -> Batch { - update!(ctx; self verts faces uniform shader viewport target) + pub fn context( + self, + ctx: &Context, + ) -> Batch { + update!(ctx; self verts prims uniform shader viewport target) } } -impl Batch { +impl Batch { /// Renders this batch of geometry. #[rustfmt::skip] - pub fn render(&mut self) + pub fn render(&mut self) where + Var: Vary, + Prim: Render + Clone, Vtx: Clone, Uni: Copy, - Shd: Shader, + [::Clip]: Clip, + Shd: Shader, Tgt: Target, Ctx: Borrow { let Self { - faces, verts, shader, uniform, viewport, target, ctx, + prims, verts, shader, uniform, viewport, target, ctx, } = self; super::render( - faces, verts, shader, *uniform, *viewport, - *target, (*ctx).borrow(), + prims, verts, shader, *uniform, *viewport, *target, + (*ctx).borrow(), ); } } diff --git a/core/src/render/cam.rs b/core/src/render/cam.rs index f9f3f7b9..e87f4f1a 100644 --- a/core/src/render/cam.rs +++ b/core/src/render/cam.rs @@ -2,21 +2,19 @@ use core::ops::Range; -use crate::geom::Vertex; +#[cfg(feature = "fp")] use crate::math::{ - Lerp, Mat4x4, Point3, SphericalVec, Vary, mat::RealToReal, orthographic, - perspective, pt2, viewport, + Angle, Vec3, orient_z, rotate_x, rotate_y, spherical, translate, turns, }; -use crate::util::{Dims, rect::Rect}; - -#[cfg(feature = "fp")] use crate::math::{ - Angle, Vec3, orient_z, pt3, rotate_x, rotate_y, spherical, translate, turns, + Apply, Lerp, Mat4x4, Point3, SphericalVec, Vary, mat::RealToReal, + orthographic, perspective, pt2, viewport, }; +use crate::util::{Dims, rect::Rect}; use super::{ - Clip, Context, FragmentShader, NdcToScreen, RealToProj, Render, Target, - VertexShader, View, ViewToProj, World, WorldToView, clip::ClipVec, + Clip, Context, NdcToScreen, RealToProj, Render, Shader, Target, View, + ViewToProj, World, WorldToView, }; /// Trait for different modes of camera motion. @@ -82,6 +80,7 @@ pub struct FirstPerson { pub type ViewToWorld = RealToReal<3, View, World>; +/// Creates a unit `SphericalVec` from azimuth and altitude. #[cfg(feature = "fp")] fn az_alt(az: Angle, alt: Angle) -> SphericalVec { spherical(1.0, az, alt) @@ -217,11 +216,7 @@ impl Camera { ) where Prim: Render + Clone, [::Clip]: Clip, - Shd: for<'a> VertexShader< - Vtx, - (&'a Mat4x4>, Uni), - Output = Vertex, - > + FragmentShader, + Shd: for<'a> Shader>, Uni)>, { let tf = to_world.then(&self.world_to_project()); @@ -243,7 +238,7 @@ impl FirstPerson { /// and heading in the direction of the positive x-axis. pub fn new() -> Self { Self { - pos: pt3(0.0, 0.0, 0.0), + pos: Point3::origin(), heading: az_alt(turns(0.0), turns(0.0)), } } @@ -277,7 +272,7 @@ impl FirstPerson { let up = Vec3::Y; let right = up.cross(&fwd); - let to_world = Mat4x4::from_basis(right, up, fwd); + let to_world = Mat4x4::from_linear(right, up, fwd); self.pos += to_world.apply(&delta); } } diff --git a/core/src/render/clip.rs b/core/src/render/clip.rs index 50ada24a..be28523c 100644 --- a/core/src/render/clip.rs +++ b/core/src/render/clip.rs @@ -287,7 +287,7 @@ pub fn clip_simple_polygon<'a, A: Lerp + Clone>( debug_assert!(verts_out.is_empty()); for (p, i) in zip(planes, 0..) { - p.clip_simple_polygon(&verts_in, verts_out); + p.clip_simple_polygon(verts_in, verts_out); verts_in.clear(); if verts_out.is_empty() { // Nothing left to clip; the polygon was fully outside @@ -372,18 +372,18 @@ impl Clip for [Tri>] { // Clipping a triangle results in an n-gon, where n depends on // how many planes the triangle intersects. For example, here // clipping triangle ABC generated three new vertices, resulting - // in quad ARQP. Convert the quad into triangles PAR and PRQ - // (or ARQ and AQP, it is arbitrary): + // in quad APQR. Convert the quad into triangles PRA and PQR + // (or APQ and AQR, it is arbitrary): // // B // // / \ - // Q_____R___________ + // Q_____P___________ // / |..../..\ // |.../.....\ // / |./.........\ // |/............\ - // C _ _ _P_______________A + // C _ _ _R_______________A // | // | // diff --git a/core/src/render/ctx.rs b/core/src/render/ctx.rs index 5193cb7a..f3ed5fcb 100644 --- a/core/src/render/ctx.rs +++ b/core/src/render/ctx.rs @@ -10,41 +10,54 @@ use super::Stats; #[derive(Clone, Debug)] pub struct Context { /// The color with which to fill the color buffer to clear it, if any. + /// /// If rendered geometry always fills the entire frame, `color_clear` - /// can be set to `None`. + /// can be set to `None` to avoid redundant work. pub color_clear: Option, /// The value with which to fill the depth buffer to clear it, if any. pub depth_clear: Option, /// Whether to cull (discard) faces pointing either away from or towards - /// the camera. If all geometry drawn is "solid" meshes without holes, - /// backfaces can usually be culled because they are always occluded by - /// frontfaces and drawing them would be useless. + /// the camera. + /// + /// If all geometry drawn is "solid" meshes without holes, backfaces can + /// usually be culled because they are always occluded by front faces and + /// drawing them would be redundant. pub face_cull: Option, - /// Whether to sort visible faces by their depth. This is important when - /// rendering overlapping transparent faces, which *have* to be drawn - /// back-to-front to get correct results. On the other hand, rendering - /// nontransparent geometry in front-to-back order can improve performance - /// by reducing overdraw. + /// Whether to sort visible faces by their depth. + /// + /// If z-buffering or other hidden surface determination method is not used, + /// back-to-front depth sorting can be used to ensure correct rendering + /// unless there is intersecting or non-orderable geometry (this is the + /// so-called "painter's algorithm"). + /// + /// Overlapping transparent surfaces have to be drawn back-to-front to get + /// correct results. Rendering nontransparent geometry in front-to-back + /// order can improve performance by reducing overdraw. pub depth_sort: Option, - /// Whether to do depth testing and which predicate to use. If set to - /// `Some(Ordering::Less)`, a fragment passes the depth test *iff* - /// `new_z < old_z` (the default). If set to `None`, depth test is not - /// performed. This setting has no effect if the render target does not - /// support z-buffering. + /// Whether to do depth testing and which predicate to use. + /// + /// If set to `Some(Ordering::Less)`, a fragment passes the depth test + /// *iff* `new_z < old_z` (the default). If set to `None`, depth test + /// is not performed. This setting has no effect if the render target + /// does not support z-buffering. pub depth_test: Option, - /// Whether to write color values. If `false`, other fragment processing - /// is done but there is no color output. This setting has no effect if - /// the render target does not support color writes. + /// Whether to write color values. + /// + /// If `false`, other fragment processing is done but there is no color + /// output. This setting has no effect if the render target does not + /// support color writes. pub color_write: bool, - /// Whether to write depth values. If `false`, other fragment processing - /// is done but there is no depth output. This setting has no effect if - /// the render target does not support depth writes. + /// Whether to write depth values. + /// + /// If `false`, other fragment processing is done but there is no depth + /// output. This setting has no effect if the render target does not + /// support depth writes. pub depth_write: bool, /// Collecting rendering statistics. @@ -58,7 +71,7 @@ pub enum DepthSort { BackToFront, } -/// Whether to cull frontfaces or backfaces. +/// Whether to cull front faces or backfaces. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum FaceCull { Front, diff --git a/core/src/render/prim.rs b/core/src/render/prim.rs index e57c1c4c..f91d3e51 100644 --- a/core/src/render/prim.rs +++ b/core/src/render/prim.rs @@ -1,11 +1,13 @@ //! Render impls for primitives and related items. -use crate::geom::{Edge, Tri, Vertex}; -use crate::math::{Mat4x4, Vary, vary::ZDiv, vec3}; +use crate::geom::{Edge, Tri, Vertex, Winding}; +use crate::math::{Apply, Mat4x4, Vary, pt3, vary::ZDiv}; -use super::clip::ClipVert; -use super::raster::{Scanline, ScreenPt, line, tri_fill}; -use super::{NdcToScreen, Render}; +use super::{ + NdcToScreen, Render, + clip::ClipVert, + raster::{Scanline, ScreenPt, line, tri_fill}, +}; impl Render for Tri { type Clip = Tri>; @@ -20,10 +22,8 @@ impl Render for Tri { (a.pos.z() + b.pos.z() + c.pos.z()) / 3.0 } - fn is_backface(Tri(vs): &Self::Screen) -> bool { - let v = vs[1].pos - vs[0].pos; - let u = vs[2].pos - vs[0].pos; - v[0] * u[1] - v[1] * u[0] > 0.0 + fn is_backface(tri: &Self::Screen) -> bool { + tri.winding() == Winding::Cw } fn to_screen( @@ -71,11 +71,16 @@ pub fn to_screen( // of the original view-space depth. The interpolated reciprocal // is used in fragment processing for depth testing (larger values // are closer) and for perspective correction of the varyings. - // TODO z_div could be space-aware - let pos = vec3(x, y, 1.0).z_div(w); + // + // TODO Ad-hoc conversion from clip space vector to screen space point. + // This should be a typed conversion from projective to real space. + // Vec3 was used here, which only worked because apply used to use + // w=1 for vectors. Fixing it made viewport transform incorrect. + // The z-div concept and trait likely need clarification. + let pos = pt3(x, y, 1.0).z_div(w); Vertex { // Viewport transform - pos: tf.apply(&pos).to_pt(), + pos: tf.apply(&pos), // Perspective correction attrib: v.attrib.z_div(w), } diff --git a/core/src/render/raster.rs b/core/src/render/raster.rs index 4a2e582c..cba0cf36 100644 --- a/core/src/render/raster.rs +++ b/core/src/render/raster.rs @@ -87,7 +87,6 @@ impl Iterator for ScanlineIter { } let v0 = self.left.next()?; let x1 = self.right.next()?; - let y = self.y; // Find the next pixel centers to the right // @@ -104,14 +103,13 @@ impl Iterator for ScanlineIter { let vs = v0.vary(self.dv_dx.clone(), Some((x1 - x0) as u32)); + let y = self.y as usize; + let xs = x0 as usize..x1 as usize; + self.y += 1.0; self.n -= 1; - Some(Scanline { - y: y as usize, - xs: x0 as usize..x1 as usize, - vs, - }) + Some(Scanline { y, xs, vs }) } } @@ -123,32 +121,59 @@ impl Iterator for ScanlineIter { // TODO Guarantee subpixel precision pub fn line([mut v0, mut v1]: [Vertex; 2], mut scan_fn: F) where - V: Vary + Clone, + V: Vary, F: FnMut(Scanline), { - use crate::math::float::f32; - if v0.pos.y() > v1.pos.y() { swap(&mut v0, &mut v1); } - let d = v1.pos - v0.pos; - let max_d = if f32::abs(d.x()) > d.y() { 0 } else { 1 }; - - let (p0, p1) = (&mut v0.pos[max_d], &mut v1.pos[max_d]); - *p0 = f32::floor(*p0); - *p1 = f32::floor(*p1); - let n = f32::abs(*p1 - *p0); - - let y_vary = (v0.pos, v0.attrib) - // TODO Should vary to exclusive - .vary_to((v1.pos, v1.attrib), n as u32); - - y_vary.for_each(|(pos, var)| { - let [x, y, _] = pos.map(|x| x as usize).0; - let vs = (pos, var.clone()).vary_to((pos, var), 1); + let [dx, dy, _] = (v1.pos - v0.pos).0; - scan_fn(Scanline { y, xs: x..x + 1, vs }); - }) + if dx.abs() > dy { + // More wide than tall + if dx < 0.0 { + // Always draw from left to right + swap(&mut v0, &mut v1); + } + let x0 = round_up_to_half(v0.pos.x()); + let x1 = round_up_to_half(v1.pos.x()); + + let dy_dx = dy / dx; + // Adjust y0 to match the rounded x0 + let y0 = v0.pos.y() + dy_dx * (x0 - v0.pos.x()); + + let (xs, mut y) = (x0 as usize..x1 as usize, y0); + for x in xs { + let vs = (v0.pos, v0.attrib.clone()); + let vs = vs.clone().vary_to(vs, 1); // TODO a bit silly + scan_fn(Scanline { + y: y as usize, + xs: x..x + 1, + vs, + }); + y += dy_dx; + } + } else { + // More tall than wide + let y0 = round_up_to_half(v0.pos.y()); + let y1 = round_up_to_half(v1.pos.y()); + + let dx_dy = dx / dy; + // Adjust x0 to match the rounded y0 + let x0 = v0.pos.x() + dx_dy * (y0 - v0.pos.y()); + + let mut x = x0; + for y in y0 as usize..y1 as usize { + let vs = (v0.pos, v0.attrib.clone()); + let vs = vs.clone().vary_to(vs.clone(), 1); + scan_fn(Scanline { + y, + xs: x as usize..x as usize + 1, + vs, + }); + x += dx_dy; + } + } } /// Rasterizes a filled triangle defined by three vertices. @@ -276,15 +301,7 @@ pub fn scan( #[inline] fn round_up_to_half(x: f32) -> f32 { - #[cfg(feature = "fp")] - { - use crate::math::float::f32; - f32::floor(x + 0.5) + 0.5 - } - #[cfg(not(feature = "fp"))] - { - (x + 0.5) as i32 as f32 + 0.5 - } + crate::math::float::f32::floor(x + 0.5) + 0.5 } #[cfg(test)] @@ -373,7 +390,7 @@ mod tests { super::tri_fill(verts, |mut sl| { write!(s, "{:w$}", " ", w = sl.xs.start).ok(); - for c in sl.fragments().map(|f| ((10.0 * f.var) as u8)) { + for c in sl.fragments().map(|f| (10.0 * f.var) as u8) { write!(s, "{c}").ok(); } writeln!(s).ok(); diff --git a/core/src/render/scene.rs b/core/src/render/scene.rs index ac8d69d9..4625827a 100644 --- a/core/src/render/scene.rs +++ b/core/src/render/scene.rs @@ -1,10 +1,13 @@ -use crate::geom::vertex; -use crate::math::mat::RealToProj; -use crate::prelude::clip::{ClipVert, Status, view_frustum}; -use crate::{ - geom::Mesh, - math::{Mat4x4, Point3, pt3, splat}, - render::{Model, ModelToWorld}, +use crate::geom::{Mesh, vertex}; +use crate::math::{ + Mat4x4, Point3, + mat::{Apply, RealToProj}, + pt3, splat, +}; + +use super::{ + Model, ModelToWorld, + clip::{ClipVert, Status, view_frustum}, }; #[derive(Clone, Debug)] @@ -31,10 +34,7 @@ impl Obj { impl BBox { pub fn of(mesh: &Mesh) -> Self { - (&mesh.verts) - .into_iter() - .map(|v| &v.pos) - .collect() + mesh.verts.iter().map(|v| &v.pos).collect() } /// If needed, enlarges `self` so that a point is just contained. @@ -131,7 +131,7 @@ mod tests { #[test] fn bbox_default() { assert!(BBox::<()>::default().is_empty()); - assert!(!BBox::<()>::default().contains(&pt3(0.0, 0.0, 0.0))); + assert!(!BBox::<()>::default().contains(&Point3::origin())); } #[test] diff --git a/core/src/render/shader.rs b/core/src/render/shader.rs index dc6b639f..9c9708d4 100644 --- a/core/src/render/shader.rs +++ b/core/src/render/shader.rs @@ -27,7 +27,7 @@ use super::raster::Frag; /// # Type parameters /// * `In`: Type of the input vertex. /// * `Uni`: Type of custom "uniform" (non-vertex-specific) data, such as -/// transform matrices, passed to the shader. +/// transform matrices, passed to the shader. pub trait VertexShader { /// The type of the output vertex. type Output; diff --git a/core/src/render/target.rs b/core/src/render/target.rs index a5118530..a979058e 100644 --- a/core/src/render/target.rs +++ b/core/src/render/target.rs @@ -4,9 +4,9 @@ //! and possible auxiliary buffers. Special render targets can be used, //! for example, for visibility or occlusion computations. -use crate::math::{Color4, Vary}; +use crate::math::{Color3, Color4, Vary}; use crate::util::{ - buf::{AsMutSlice2, MutSlice2}, + buf::{AsMutSlice2, Buf2, MutSlice2}, pixfmt::IntoPixel, }; @@ -48,7 +48,7 @@ impl Colorbuf { } impl, F> AsMutSlice2 for Colorbuf { - fn as_mut_slice2(&mut self) -> MutSlice2 { + fn as_mut_slice2(&mut self) -> MutSlice2<'_, T> { self.buf.as_mut_slice2() } } @@ -60,43 +60,20 @@ where Color4: IntoPixel, { /// Rasterizes `scanline` into this framebuffer. - fn rasterize( + fn rasterize>( &mut self, - mut sl: Scanline, + sl: Scanline, fs: &Fs, ctx: &Context, - ) -> Throughput - where - V: Vary, - Fs: FragmentShader, - { - let x0 = sl.xs.start; - let x1 = sl.xs.end.max(x0); - let cbuf_span = &mut self.color_buf.as_mut_slice2()[sl.y][x0..x1]; - let zbuf_span = &mut self.depth_buf.as_mut_slice2()[sl.y][x0..x1]; - - let mut io = Throughput { i: x1 - x0, o: 0 }; - - sl.fragments() - .zip(cbuf_span) - .zip(zbuf_span) - .for_each(|((frag, curr_col), curr_z)| { - let new_z = frag.pos.z(); - - if ctx.depth_test(new_z, *curr_z) { - if let Some(new_col) = fs.shade_fragment(frag) { - if ctx.color_write { - io.o += 1; - // TODO Blending should happen here - *curr_col = new_col.into_pixel() - } - if ctx.depth_write { - *curr_z = new_z; - } - } - } - }); - io + ) -> Throughput { + rasterize_fb( + &mut self.color_buf, + &mut self.depth_buf, + sl, + fs, + Color4::into_pixel, + ctx, + ) } } @@ -107,31 +84,96 @@ where { /// Rasterizes `scanline` into this `u32` color buffer. /// Does no z-buffering. - fn rasterize( + fn rasterize>( &mut self, - mut sl: Scanline, + sl: Scanline, fs: &Fs, ctx: &Context, - ) -> Throughput - where - V: Vary, - Fs: FragmentShader, - { - let x0 = sl.xs.start; - let x1 = sl.xs.end.max(x0); - let mut io = Throughput { i: x1 - x0, o: 0 }; - let cbuf_span = &mut self.as_mut_slice2()[sl.y][x0..x1]; - - sl.fragments() - .zip(cbuf_span) - .for_each(|(frag, c)| { - if let Some(color) = fs.shade_fragment(frag) { - if ctx.color_write { - io.o += 1; - *c = color.into_pixel() - } - } - }); - io + ) -> Throughput { + rasterize(&mut self.buf, sl, fs, Color4::into_pixel, ctx) + } +} + +impl Target for Buf2 { + fn rasterize>( + &mut self, + sl: Scanline, + fs: &Fs, + ctx: &Context, + ) -> Throughput { + rasterize(self, sl, fs, |c| c, ctx) } } + +impl Target for Buf2 { + fn rasterize>( + &mut self, + sl: Scanline, + fs: &Fs, + ctx: &Context, + ) -> Throughput { + rasterize(self, sl, fs, |c| c.to_rgb(), ctx) + } +} + +pub fn rasterize( + buf: &mut impl AsMutSlice2, + mut sl: Scanline, + fs: &impl FragmentShader, + mut conv: impl FnMut(Color4) -> T, + ctx: &Context, +) -> Throughput { + let x0 = sl.xs.start; + let x1 = sl.xs.end.max(x0); + let mut io = Throughput { i: x1 - x0, o: 0 }; + let cbuf_span = &mut buf.as_mut_slice2()[sl.y][x0..x1]; + + sl.fragments() + .zip(cbuf_span) + .for_each(|(frag, curr_col)| { + if let Some(new_col) = fs.shade_fragment(frag) + && ctx.color_write + { + io.o += 1; + *curr_col = conv(new_col); + } + }); + io +} + +pub fn rasterize_fb( + cbuf: &mut impl AsMutSlice2, + zbuf: &mut impl AsMutSlice2, + mut sl: Scanline, + fs: &impl FragmentShader, + mut conv: impl FnMut(Color4) -> T, + ctx: &Context, +) -> Throughput { + let x0 = sl.xs.start; + let x1 = sl.xs.end.max(x0); + let cbuf_span = &mut cbuf.as_mut_slice2()[sl.y][x0..x1]; + let zbuf_span = &mut zbuf.as_mut_slice2()[sl.y][x0..x1]; + + let mut io = Throughput { i: x1 - x0, o: 0 }; + + sl.fragments() + .zip(cbuf_span) + .zip(zbuf_span) + .for_each(|((frag, curr_col), curr_z)| { + let new_z = frag.pos.z(); + + if ctx.depth_test(new_z, *curr_z) + && let Some(new_col) = fs.shade_fragment(frag) + { + if ctx.color_write { + io.o += 1; + // TODO Blending should happen here + *curr_col = conv(new_col); + } + if ctx.depth_write { + *curr_z = new_z; + } + } + }); + io +} diff --git a/core/src/render/tex.rs b/core/src/render/tex.rs index de184433..d78266ca 100644 --- a/core/src/render/tex.rs +++ b/core/src/render/tex.rs @@ -1,6 +1,7 @@ //! Textures and texture samplers. -use crate::math::{Point2u, Vec2, Vector, pt2, vec2}; +use crate::geom::Normal3; +use crate::math::{Point2u, Vec2, Vec3, Vector, pt2, splat, vec2}; use crate::util::{ Dims, buf::{AsSlice2, Buf2, Slice2}, @@ -16,23 +17,6 @@ pub struct Tex; /// of the actual dimensions of the texture. pub type TexCoord = Vec2; -impl TexCoord { - /// Returns the u (horizontal) component of `self`. - pub const fn u(&self) -> f32 { - self.0[0] - } - /// Returns the v (vertical) component of `self`. - pub const fn v(&self) -> f32 { - self.0[1] - } -} - -/// Returns a new texture coordinate with components `u` and `v`. -#[inline] -pub const fn uv(u: f32, v: f32) -> TexCoord { - Vector::new([u, v]) -} - /// A texture type. Can contain either owned or borrowed pixel data. /// /// Textures are used to render *texture mapped* geometry, by interpolating @@ -65,10 +49,78 @@ pub enum Layout { Grid { sub_dims: Dims }, } +/// Returns a new texture coordinate with components `u` and `v`. +#[inline] +pub const fn uv(u: f32, v: f32) -> TexCoord { + Vector::new([u, v]) +} + +/// Returns a texture coordinate in a cube map. +/// +/// A cube map texture is a composite of six subtextures in a 3x2 grid. +/// Each subtexture corresponds to one of the six cardinal directions: +/// right (+x), left (-x), top (+y), bottom (-y), front (+z), back (-z). +/// +/// The subtexture is chosen based on which component of `dir` has the greatest +/// absolute value. The texture coordinates within the subtexture are based on +/// the zy, xz, or xy components of `pos` such that the range [-1.0, 1.0] is +/// transformed to the range of uv values in the appropriate subtexture. +/// +/// ```text +/// u +/// 0 1/3 2/3 1 +/// v 0 +------+------+------+ +/// | | | | +/// | +x | +y | +z | +/// 1 | | | | +/// / +--zy--+--xz--+--xy--+ +/// 2 | | | | +/// | -x | -y | -z | +/// | | | | +/// 1 +------+------+------+ +/// +/// ``` +pub fn cube_map(pos: Vec3, dir: Normal3) -> TexCoord { + // -1.0..1.0 -> 0.0..1.0 + let [x, y, z] = (0.5 * pos + splat(0.5)) + .clamp(&splat(0.0), &splat(1.0)) + .0; + // TODO implement vec::abs + let [ax, ay, az] = dir.map(f32::abs).0; + + // TODO implement vec::argmax + let (max_i, mut u, mut v) = if az > ax && az > ay { + // xy plane + (2, x, y) + } else if ay > ax && ay > az { + // xz plane left-handed - mirror x + (1, 1.0 - x, z) + } else { + // zy plane left-handed - mirror z + (0, 1.0 - z, y) + }; + if dir[max_i] < 0.0 { + u = 1.0 - u; + v += 1.0; + } + uv((u + max_i as f32) / 3.0, v / 2.0) +} + // // Inherent impls // +impl TexCoord { + /// Returns the u (horizontal) component of `self`. + pub const fn u(&self) -> f32 { + self.0[0] + } + /// Returns the v (vertical) component of `self`. + pub const fn v(&self) -> f32 { + self.0[1] + } +} + impl Texture { /// Returns the width of `self` as `f32`. #[inline] @@ -302,7 +354,7 @@ impl SamplerOnce { mod tests { use alloc::vec; - use crate::math::{Color3, rgb}; + use crate::math::{Color3, Linear, rgb}; use crate::util::buf::Buf2; use super::*; @@ -358,4 +410,23 @@ mod tests { assert_eq!(s.sample(&tex, uv(0.0, 0.5)), rgb(0, 0, 0xFF)); assert_eq!(s.sample(&tex, uv(0.5, 0.5)), rgb(0xFF, 0xFF, 0)); } + + #[test] + fn cube_mapping() { + let zero = Vec3::zero(); + let tc = cube_map(zero, Vec3::X); + assert_eq!(tc, uv(1.0 / 6.0, 0.25)); + let tc = cube_map(zero, -Vec3::X); + assert_eq!(tc, uv(1.0 / 6.0, 0.75)); + + let tc = cube_map(zero, Vec3::Y); + assert_eq!(tc, uv(3.0 / 6.0, 0.25)); + let tc = cube_map(zero, -Vec3::Y); + assert_eq!(tc, uv(3.0 / 6.0, 0.75)); + + let tc = cube_map(zero, Vec3::Z); + assert_eq!(tc, uv(5.0 / 6.0, 0.25)); + let tc = cube_map(zero, -Vec3::Z); + assert_eq!(tc, uv(5.0 / 6.0, 0.75)); + } } diff --git a/core/src/render/text.rs b/core/src/render/text.rs index b549f596..e36aa116 100644 --- a/core/src/render/text.rs +++ b/core/src/render/text.rs @@ -2,7 +2,7 @@ use core::fmt; #[cfg(feature = "std")] use std::io; -use crate::geom::{Mesh, Tri, vertex}; +use crate::geom::{Mesh, tri, vertex}; use crate::math::{Color3, Point2, Vec2, pt2, vec2, vec3}; use crate::util::buf::Buf2; @@ -50,8 +50,9 @@ impl Text { /// Erases all text from `self`. pub fn clear(&mut self) { - self.cursor = pt2(0.0, 0.0); - self.geom = Mesh::default(); + self.cursor = Point2::origin(); + self.geom.faces.clear(); + self.geom.verts.clear(); } /// Samples the font at `uv`. @@ -84,8 +85,8 @@ impl Text { vertex(pos + vec3(0.0, glyph_h, 0.0), bl), vertex(pos + vec3(glyph_w, glyph_h, 0.0), br), ]); - geom.faces.push(Tri([l, l + 1, l + 3])); - geom.faces.push(Tri([l, l + 3, l + 2])); + geom.faces.push(tri(l, l + 1, l + 3)); + geom.faces.push(tri(l, l + 3, l + 2)); *cursor += vec2(glyph_w, 0.0); } diff --git a/core/src/util/buf.rs b/core/src/util/buf.rs index f3449220..2bf040cc 100644 --- a/core/src/util/buf.rs +++ b/core/src/util/buf.rs @@ -198,13 +198,22 @@ impl Buf2 { } /// Returns a view of the backing data of `self`. - pub fn data(&self) -> &[T] { + /*pub fn data(&self) -> &[T] { self.0.data() } /// Returns a mutable view of the backing data of `self`. pub fn data_mut(&mut self) -> &mut [T] { self.0.data_mut() + }*/ + + /// Reinterprets `self` as a buffer of different dimensions but same area. + /// + /// # Panics + /// If `nw` * `nh` != `cw` * `ch` for the new dimensions (`nw`, `nh`) + /// and current dimensions (`cw`, `ch`). + pub fn reshape(&mut self, dims: Dims) { + self.0.reshape(dims); } } @@ -317,20 +326,18 @@ impl Debug for Slice2<'_, T> { } impl Debug for MutSlice2<'_, T> { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - self.0.debug_fmt(f, "Slice2Mut") + self.0.debug_fmt(f, "MutSlice2") } } impl Deref for Buf2 { type Target = Inner>; - fn deref(&self) -> &Self::Target { &self.0 } } impl<'a, T> Deref for Slice2<'a, T> { type Target = Inner; - fn deref(&self) -> &Self::Target { &self.0 } @@ -482,6 +489,12 @@ pub mod inner { .field("stride", &self.stride) .finish() } + + pub(super) fn reshape(&mut self, dims: Dims) { + assert!(self.is_contiguous()); + assert_eq!(dims.0 * dims.1, self.dims.0 * self.dims.1); + self.dims = dims; + } } impl> Inner { @@ -508,7 +521,7 @@ pub mod inner { } /// Returns the data of `self` as a linear slice. - pub(super) fn data(&self) -> &[T] { + pub fn data(&self) -> &[T] { &self.data } @@ -563,7 +576,7 @@ pub mod inner { } /// Returns the data of `self` as a single mutable slice. - pub(super) fn data_mut(&mut self) -> &mut [T] { + pub fn data_mut(&mut self) -> &mut [T] { &mut self.data } diff --git a/core/src/util/pixfmt.rs b/core/src/util/pixfmt.rs index 559f3048..4990b076 100644 --- a/core/src/util/pixfmt.rs +++ b/core/src/util/pixfmt.rs @@ -107,6 +107,11 @@ impl IntoPixel<[u8; 4], Bgra8888> for Color4 { [b, g, r, a] } } +impl IntoPixel<[u8; 3], Rgb888> for Color4 { + fn into_pixel(self) -> [u8; 3] { + [self.r(), self.g(), self.b()] + } +} impl IntoPixel<[u8; 2], Rgba4444> for Color4 { fn into_pixel(self) -> [u8; 2] { let c: u16 = self.into_pixel_fmt(Rgba4444); diff --git a/core/src/util/pnm.rs b/core/src/util/pnm.rs index 47aa6d52..2852e567 100644 --- a/core/src/util/pnm.rs +++ b/core/src/util/pnm.rs @@ -193,6 +193,7 @@ pub fn load_pnm(path: impl AsRef) -> Result> { /// Returns [`pnm::Error`][Error] in case of an I/O error or invalid PNM image. #[cfg(feature = "std")] pub fn read_pnm(input: impl Read) -> Result> { + let input = BufReader::new(input); parse_pnm(input.bytes().map_while(io::Result::ok)) } @@ -302,12 +303,11 @@ where .write(&mut out)?; // Appease the borrow checker - let res = slice + slice .rows() .flatten() .map(|c| c.into_pixel()) - .try_for_each(|rgb| out.write_all(&rgb[..])); - res + .try_for_each(|rgb| out.write_all(&rgb[..])) } /// Parses a numeric value from `src`, skipping whitespace and comments. diff --git a/core/tests/rendering.rs b/core/tests/rendering.rs index c1a1db99..24fd3eab 100644 --- a/core/tests/rendering.rs +++ b/core/tests/rendering.rs @@ -1,19 +1,19 @@ #![allow(unused)] +use retrofire_core::prelude::*; + use retrofire_core::{ - math::color::Rgb, - prelude::*, - render::{target::Colorbuf, tex::SamplerClamp}, + render::tex::SamplerClamp, util::{self, pixfmt::Xrgb8888, pnm::parse_pnm}, }; const VERTS: [Vertex3; 4] = [ vertex(pt3(-1.0, -1.0, 0.0), uv(0.0, 0.0)), - vertex(pt3(-1.0, 1.0, 0.0), uv(0.0, 1.0)), - vertex(pt3(1.0, -1.0, 0.0), uv(1.0, 0.0)), + vertex(pt3(1.0, -1.0, 0.0), uv(0.0, 1.0)), + vertex(pt3(-1.0, 1.0, 0.0), uv(1.0, 0.0)), vertex(pt3(1.0, 1.0, 0.0), uv(1.0, 1.0)), ]; -const FACES: [Tri; 2] = [Tri([0, 1, 2]), Tri([3, 2, 1])]; +const FACES: [Tri; 2] = [tri(0, 1, 2), tri(3, 2, 1)]; #[test] fn textured_quad() { @@ -35,38 +35,25 @@ fn textured_quad() { let viewport = viewport(pt2(0, 0)..pt2(w, h)); let mvp = translate3(0.0, 0.0, 1.0).to().then(&project); - let mut color_buf = Colorbuf { - buf: Buf2::new((w, h)), - fmt: Xrgb8888, - }; - let ctx = Context::default(); - - render(FACES, VERTS, &shader, &mvp, viewport, &mut color_buf, &ctx); + let mut framebuf = Buf2::::new((w, h)); + let mut ctx = Context::default(); - let buf = color_buf.buf; - assert_eq!(buf[0][0], 0x00_00_00_FF); - assert_eq!(buf[255][0], 0x00_7F_00_00); - assert_eq!(buf[0][255], 0x00_7F_00_00); + render(FACES, VERTS, &shader, &mvp, viewport, &mut framebuf, &ctx); - let buf = Buf2::new_from( - (w, h), - buf.data().iter().map(|u| { - // Xrgb8888: 0x00_RR_GG_BB - let [_, rgb @ ..] = u.to_be_bytes(); - Color3::::from(rgb) - }), - ); + assert_eq!(framebuf[0][0], rgb(0, 0, 0xFF)); + assert_eq!(framebuf[255][0], rgb(0x7F, 0, 0)); + assert_eq!(framebuf[0][255], rgb(0x7F, 0, 0)); let comp = *include_bytes!("textured_quad.ppm"); let comp = parse_pnm(comp).expect("should be a valid ppm"); - assert_eq!(buf, comp); + assert_eq!(framebuf, comp); #[cfg(feature = "std")] { use util::pnm::*; // Uncomment to save generated image to compare visually - //save_ppm("tests/textured_quad_actual.ppm", &buf); + //save_ppm("tests/textured_quad_actual.ppm", &framebuf); // Uncomment to (re)generate the comparison image //save_ppm("tests/textured_quad.ppm", &buf).expect("should save image"); } diff --git a/demos/Cargo.toml b/demos/Cargo.toml index 19c06c97..e6dddb29 100644 --- a/demos/Cargo.toml +++ b/demos/Cargo.toml @@ -21,19 +21,25 @@ categories.workspace = true repository.workspace = true [dependencies] -re = { version = "0.4.0-pre3", path = "../core", package = "retrofire-core" } -re-front = { version = "0.4.0-pre3", path = "../front", package = "retrofire-front" } -re-geom = { version = "0.4.0-pre3", path = "../geom", package = "retrofire-geom", features = ["std"] } +re = { version = "0.4.0-pre4", path = "..", package = "retrofire" } +re-front = { version = "0.4.0-pre4", path = "../front", package = "retrofire-front" } minifb = { version = "0.27.0", optional = true } sdl2 = { version = "0.35.2", optional = true } +pancurses = { version = "0.17.0", optional = true } [features] default = ["minifb"] -# Enables the SDL2 frontend used by the crates demo minifb = ["dep:minifb", "re-front/minifb"] sdl2 = ["dep:sdl2", "re-front/sdl2"] [[bin]] name = "crates" required-features = ["sdl2"] + +[[bin]] +name = "curses" +required-features = ["pancurses"] + +[lints] +workspace = true diff --git a/demos/README.md b/demos/README.md index 631bbbe0..a60be49a 100644 --- a/demos/README.md +++ b/demos/README.md @@ -12,7 +12,9 @@ # Retrofire-demos -Simple demo programs showcasing Retrofire features. +Simple demo programs showcasing [`retrofire`][1] features. + +[1]: https://crates.io/crates/retrofire ## Demo binaries diff --git a/demos/assets/crate.ppm b/demos/assets/crate.ppm new file mode 100644 index 00000000..60025949 --- /dev/null +++ b/demos/assets/crate.ppm @@ -0,0 +1,5 @@ +P6 +128 +128 +255 +~xwxyzxxyuvyxwwvvwzwuwwwsrx{wvtttssrrqqqsopnsrrssssqlnpmllkppnorrnlsssqsussrsruvttywtsssurruwssttttvstsrsrtssqsvssrrstuyvsqqlts~{}j}jy|vw{}|w{|z|}~}{ywvxxxtwxvtrrqrsvuvywxzywssru{|xvx}~|{y|~~{v{z|{|~~||wuy{zyu}wp\q^}~|uy|zz{y^Z_ROUo}wwx{}xwy||z{zyzwxxzsuywzuuwtuwuvxxzwt|{tsrwtuwz}}~|{xvy}y}wz{zzzxx|zw||}~}}}}}~XUZWTYznlwyqnrf}nboxvuprspkjlnnmopmijpomttsstvrppvtvwuuxuvzxxuuxyvstpnlmpmkmkmompplhgjmidgigghfdffdjkglkffiiggffffhlpusponnmjinmsdXufZorx}|tnkr}slolgnmegghmpohhimnnrqpmomljiomptusu{zytuuuuswzvwvvxxuvxyxvt~zuuyz|ztuzyxxz||z|xy}|y}}zyyyxtpsvvzzutw|zwx|rrz{rz|qgc~|yzxwuw|}~}|}zyz{|zw{{{xy{~xz{uvzzwxvuvxy{{{{{ux\{vtkedlyzrolok~fjnkonosoortqrtuwurprsqnmnnrtqmkjpllmqnnmnpnnsrqnlttvttutrswywutz{{zzzyzww~~w{|{~{z{|yvvwwvwzwyyzz|qpuxwrrwzy_w[r}x{fVTYykbbfk{pgibiZQWSYwegfijjjjjmkffhnmlomfhjhkkfijijliiijkmmmfcijmmkookkljlkinkeeiffjijgdgc~b}bee~b~cdfhffhhligjghnnhejfhijfgn\RNTpaXgl~bcrq{axY~ocQNSqȴyvaQOUwi`bjix{rplm`XVTZqamrqqrolpjgi}c{a}c~deeffggkjgigegkkgflpmjigfigklffhijlmghjmnkfehijiii}c|b}cx^x^z_y_x^v\w]tZw\y_v\uZv\w]uZuZx^x]w]y^y^{az`v\u[u[sXpUnTtZtWrbRRQXg[TsVuYv[tyo{az[{laQNSrȴ{l|c}cflks~}qplimpnorpkklijopkimnkilkiljgnnlmkhggijihjhjmkigih~b~bhijihlnkkkjhkkjmonlkkjijikkilnjijkjeddgez^z_{`c{_tXtWsXqUrVvZqTrZx\wYv[toq|bz_qw{rkniknwtxlRqWz^zav]u]t[rYoVnVqXqYt[rYqXs[rYrZt\w_ybv_r[rYrYnUnVpXqYoVkRlS~jQlTqXqXsZqYqXpW~kR}jP}kRzhO{iP{iPygNxeMvcKubJwdMxeLxfNzgOxeL{hP~lS{hPyfM~lSoVlS{iPyeLzfN{hO}jQzgN|iPlTnUoVmUnVmTnUnUoVu\v_w^u]u]u]t\x_w^u[v^zay`x`za|dzbx_w^x_z`{bx`y`{bz^z`t\zwptgz_v{zpkifihroxH@6MB5UJiU?t_G|fLkQlQoSqUsVwZuXsWtXuXuXsVqSqTpSnRpSmQlQlQjMpUmRoTpUpTqUoTpTrWv[tYsZrXsYpWu[x]y^~cf{a|a{`w\z`|a|b~b~b|a}a|`{_|aw\pVpVlTzfOp]H[L;r~mw}cmRelsxvql{`|`kntw}cVGo^I{hSpZrZv]gjhnkimlkjjmnmqoposUJ<D<1w`opuswhTu`r]J?3~dijlt\A8,QF7okknplknhegdeki~d}b|cy_u]w_nXwePkZFYL;t}rt}cnT~dipv|tky_dnmuu}i]N{lXwckmszy{|~}xz{{}zyyz{yz~81'w`u[OAiprrraTDSG9ms\6/&j|aegff~ce|ax^{a{az`z`|b{ax^zax`u]oX|jSucNl[GWK;w|wt}cqV{`gmwwj{`eohou|}g[LwgSo[ycikmqqnuyuv|ywzwvyz~2,%             !!4/(!"""!"!!"""!""""!!!!!!!"61*~zv}lpfY{|sqgoTx]hju}xldgndsww|tk^~lv{}83-# # # # # # # # # # # # # # # # # " """""MF="""!""!!"!!!!!! !!!!!   2-'{ryipfY~|urhmSy^jkqxwn{agogpty|pfZziot~;5.         # |           !!!!!! <6.zq}jqfWzwu|bkQ}cmjowvo~chpgot||h\NzjVs^yc}djrwxvssuzzxwzyp}z}|zTI=            #YM?vazw   H?5 !!!{g_UH&"""""""""!"!""!!"ZQE~yz||{xupjuak_O}zsx{ajOenhovsp~demitty{ocTq\zdjmrvxxqwvtyxxx|1*"bUE|lWqn2,#gYIkvurtwq#`TDSI;{{~~y{ncWG 1+#ru|lYeXI2,$wtqtxvtuwvojhzbt]ygSbUD|zvw~cnSz`mfovuq{`ejkwwsxh\LxgRr[zbgiiknmprqtpptm-'pZlrwwwsutq|cpssuusrsyvwa-'orrpquvyxuwslg|es]xgRcWG}yws}coU{akdnvwuy^gojv{wwfYIygRt^xaglsqpqpploquvx)$&"t^wrrutptyxvtrp!tnnlnorootrvzy`% '"smkkhfgjnnjg}cw]t\oXudObVE|wsrfoU|bicizuuz_fsis{zzk_Q~nYyd~hpy|x{|zxyvy{vueXG}nY~zxwywvontrpqnpe3,#umkiqururoopuxvxyjV`SCkheilg{_y^z`}bz`v]sZmUygPrbLhYDZN?yystioU|bkbhwsz`ethv}xyl`Q}lWu_krssy}|~|{f E=2zz||ystvsrqqonqsnl~eQE7i[Htqolljmkloolkjlolkm:3){kUiijihjgjf|a}c{ay_v^pYyhRqaM^RBy~rrhoT~dhckssy_epiw|zxh\LzjUoYw`ffinmkowsrqkkopn`MokkljikjmokkkjjjighiijimjgmlhiikjfedgjgkomvfR~|}xr}jqfWw~osdoU}b}cbmxoux^|`okv|yybWIqbMziTpZv_|dihkoorrvoooih~dngeffkkffcijjnnnpmouwvvwy|""""#""""""ijƵ±òñ±{rvk]swnw~cqV{`{b}anutyw\{_mowxy~cXJqbN|lXr\xb~ejjlqrrXM?+&+&,' ?8.H?3~zxv{}{}!!!!"!""!!!TLAJC:2,'1,%0+$bWKxsmxeocUruivgoT~d|by]n{wuv[dpoww}{[N=mZDygOkRoUx\{_cijqx     KB7}{z{+&       !      ~~~zvurkvbzkXeYIrzgwhqVd~dx]izrty_epnt~{wnbSuakrzE>4!!!!!!!!!!! !k_Q~(#    A:1~zxwm~izgq_g\MrzgtitY}c~dy^jymx|bcpmx|txvl_~lv{~!!! !! !!!!!)$tcv}qa}qa}qa|p_{n^{n^}p`{n]zm]|o_{o_|o_zm]|o_}qa}qa}qa}qa{o_{n^{n^{o_{o_zm^ym]zm]{o_{o_{o`}qczm]xk\yl\ym]ym^ym^{o`          ~{t{kpfZuwhtirXeey^iylx|by]vryzqwqgY{hsx}3/'!!!!!!"""# # # "# # $!$ $ # # # # # # # # """"# # " """!""!!!!!!!!!    # PH>{j   !!!!!"4/*rwflaSuxgyhnUh}ctY|cyjv~d{^wqy}sw{qfy²Ĵɺ$"$!%"%"$"$"%"%"%#&# %"$"$"$"$!# # # # # # # $!$!# # # # # # # # # # # # """"""!!!!!!"!!""!!!zqc!  !!!"" "!vkhxb{lWg]Lt}iukrXew\sYezlx}beynv~utuj]{jt{~!""""""""!!!"!!!   !! !!  !!!!  !!  !!           !      m~|}shX       ~uswzxupjg{_sY}nUh]Go|honu\}cx^uZf{pry^ezioztqaTDsaLlUt\~ekmkmoq|dr[XK<               eYK}{}z!        g[Mpy~unzhncUuzgmqy_}cy_sYfxqpuZ|axjrwtj^Pp]ydmsw|h\OIA7("     !                      !       !""""!!!   !   ! !!!"NF=!!!  !!!!  ($KB9k`R}|vq|lqgZv}horz`~dx]qWhzrstX|`xlwutpeXwe~jouz}VK@    !! !!!!!"""!!""!   !!!!!"""!!   !  !  !                   zm]sgX          ZQE|yqlwfmcUqxfrlv\ez_pW}dwqyy^|`}oyrxzpdx|""""""# """"""""" # # # """"""""""# # # # """""""!!"""""""""""""""#""""""!!!"d[O!!!!!!!!! !  ~unvflbTtyiqlv\fy^qW}dvtyz_y]zmwrypgZwfotwzm]         !       !!  !!! !! !  !!!!   !   !!!!!!!!!"!"""""!!!!!!"4/)           {m^zuoyhmcVuxkqov\|cz_qW{b|ov|bz^wkutvaUGrbOo\u`ychlnq'"~QI>  +& ~{vq~ksakaStwntmy_{a{`rXya}ntfcwhwrvg\N{lXu`|fkkqqs'"E=1:3)      ye~}0*$% )$~xun~ixd~n\g\Msylvny_{az_v\{bysv|afudxrsncUt`|houz}~~{g 0+$#   tiYxwfpuuiZ! !!           3.'RJ?WMBYPDYPDYPD[QE[SH\SH[RFYPCYPE_VJaWK)%!!!!"  !          _UGwk[   xta uaz~}yuqk{gtbj_Orxnyqw\y_y^x]h{uty]gs{_vuxaUFtcO~mXu_yb~flmorLA5wxuo~ffYHv{y|wvhV}~|z{|xzy{|{yn`SEvvxroqtr]J?3kigmjzbt^nXueQ`TDs}tyqv\y_ex]g~xtx]dqfw|owg\M|lYs^zdlsylbWH;4+{z{zt}z}j k^M~|}}|}~}~#YPClF>4|~}~]RCzdsoi|fwczkWdYJu}q{qw\y`h|a|bwvz_fshw}oprgZ}ktz}qb*&!   !! !72+!!"dZNz"""""""""""""""mcVue!"!!!!!!!!!!!!!xg&"!!NF=!!!!!!! *&!~sd{t~nsi\q|mptXz`~d}bhu{~cdsfxrqlaSublqq    qcT}o   !!!! !!!!!lbSz!"!""!"""!!""!!!!!!r !     sn}hvdmbTq{jyqsYv]y_}bow}c{`rhy}uodYJyiVvbk{tfU~TK?OF:{z~~icWI}|{uy~{|{yx||{}f[L F>4}iz{|n\wjwc{kWg[Mr}hwzy^sYw]fm{x~d{`pe|onmcVve}jqw,'!JA6p      l_Q}bWI        z)%!*&|q}hvczkYeZLq|g~{z_w\x^gkyjcog}nti^Qq_zg{f}flv92*XM?mstz~}k  E>4zgpeV,("      "! !     x{sb+&!!!  !   <5-}wn~kyhmbUrxj|ydz`v\ek~y}gdnh{|qsi^No\xc}fjptzVK>qdS~}m  g[L~||}~~k93*          XOB~y|upkubk`Qr}hyz}bz`v\}cj|yuy^iqeyzprk`Qq]|fmwz}z     #viYt   !!!! !!!!!!!!!""""! !!!!!!"!!!!!!!!!!!""!|ik>7.    zzrutn{gq]j_Oohx{cw]rW|am~yuw]goj}pri]M}nYychmrv|JA5  0+$~yw{zyxk                wzzzstursoqooqpq{4-%E;0sqmk{dnX|lWg[Loktwhv\sYy_i}{wz_flj~}osh]M|mXt^{dfmq\4-%qpmmmjgcdhjt\WKyzvttz{yy{|tuzyzzx?7.>7-ojyd~p]j_Pllpsh|anTy^lwxzv[fnj|kwodWvdkud        PH<~r!     !!  !!!!!!!  ! !!          !      }zwwvwwy{upuuqvzPF8        t]}dw^pdQilqrhy_kQy_nsxvuZdqlzjy]OBm[IyfQl[JB7,~ee|bejjgeehhoWrojeffjjilkhjlopG>1whRs[{jTfYFi~nwzmw]|gMrXnswttYeqj|ku`TEraMzhTq[u\4,#6/&ppnmmilprrot`|nottqoruuxxwuw60(92*llwd{kYg\Nn{lsxr{ajPqWkvvwx\}aoh|msf[MzkXua~hmrvb0*$|vuzztv|{xw}h         ylZ}l~rb|ksd&#!!     k{xrzitj]krqqxu{a~iOtYhwxww[y^ni~lsdYJvgSp[u`|elkhMD7"tqmmrusruvx|gRG;nzei~ilpoqp}hzh\RE                !! !!  VNDzuptl_lpxmvt|bkQw]gz{y|a}aqkiqaUFudQ}lXr\{dhknlqprqrvvqstv~hugUwxvyyxutvxyvzewbuuswxvxvw}yvxo}ktdndWlrtjws|a~hNy_izv}f|asi}iuh^Op]xdkrwz~TJ>q|~|y}}}o f[L}||zwwtttonoqYM@noqruwyzwPF:xttp}ht`zkYh^Qqwrpzty^~iN{akwuf{`mi}jvncUwdlptv~?7.  t{zl       gZK}|yu~|~}xwwwsfYI&"zvurtvtxq<5,zsl~jvb|mZj`Rpxqtxrw\jQ|bny|}dclfpqi^Np]wc{ekz*%rdT}{}~~|~|zi eYJ~||}}|||~|{}}z{~|zoaP*$vk}gxdp]kaSmyps{sy]mR}cmvyz~cgrf{sok`Qs_zfj|       @90~{|{}zu        l`P"            tc|   !pkwdqgYn|qo~u|akQ|cmvyy|`kuetpncUtb}ipo  #q^jmkmmk~i{fmtgV dXJ{uxUI<OE9mwysl_P      lq|hs_mcUqyzuxtflRzaiy{y{`hqfrpmaSp^|hnxtgV-(!            !,(!-(!-(!+'+%+%+'*&F>4}xd            .)#zm]r}itbncVmywvyl~bpU{a}dz{y|`dliolsi[~mqzxrgX!"!!"!"!!!!  !!!""!!!!! ! !!!!             {       !!   !! !       !           nbSo~wsmvdpeXjztuym}blRz_hwz{}a{`ml~tindTygnw{z~SK@    !            !  !!!!                71*                     RH={{vpn~jtambTiv{srr~clQw]gwy{edokpkncUxfptv{h   !!!!!! !!               5/'~}},(!  t`ywsrrojwb|mZj_Qjx{mup|aoSu[fwz|ggmhfnlaRr`|hmx}{~($                              4.'{|zz{ydXI          '"~|wvp~hvb~o\j`Qmz{qwr|asXu[|btz{g}bqk|komcTtc}iqwv{2-&            !!!!!        ! !!        5/'}|~}}p  2-&}yvolvdndWow|swu|arWtZ|dtz}kz`ijlmmbTub|hnqtvvi\K5/'      pdT{zyxsoygqgZrx{rzs{_uZsY}dv{l{`hfmmh]N}n\xd~iouwu4/'{yzs`     ottlzds_|mZg\Nq~|r|uy]qVtZxav|e|alfolj`Rtb{hmsy|WNC                              !  !!   72,61+!!!!!!!!"!!!"""!""!!!!!!!!!!!"!!!!]UJ~vryhlbVq~vo}wy]qUw\xay~xey^ljnmqh[zjo|j`TJC;*&"!!!!!!"!!!"""""""""" ""# # # # # # # " """"# # "# " # # "'#±0+&"""""""""# " """""!""""# # # # # """"+'#MF=odXvxodq|zqzsy^w[tYzaywj{`jh{kmyqe{~h_T"!!""""""&#:6/# ,)%-)%,($-)%-)%+($+($-*%-*&-)%-)%-)%,)$,)$-)%-)%-)%,)$-)%,)$-)%-)%-)%-)%,)$,)$*&"HB:ǸĵĵĵƷŶĴ²´rj]2-(*'",)$-)%+)$.*%-*%+)$-)%-)$-)%,($-)%-)%$!=82)%!$!#!$!#!$!# # # " lcXyzqepu~ryv|`sXpU~dxwj}bke|lmrgX{hsv{{   %!~}|||zxzyyxutxvtuvtspvu#srsuvsomit_|kZrbRcWJms}p|z{_nTtXm|xf~clhno[N=gT@p\FxdM|gOkRv\v[sYuZv[u[tYrWC7*mRpTtXrVpUsXoSkOoSmQmRqWsXrWpToTrVqUsXrWv[v[vZtXsWw[x\uZw[y^w[{_d}az_~bz_sXtYv[u[tYtYuZuZqVoTrWsWqVqUqVpVqVpUqWB6)kPmQqVw[w[vZsWnTmS~hPzdMs_HiU@\O@ls|o}yz^rVz_p|zfeigjp^RCo^K{jUoZt]{c}d|cgnmkl*$@7+h}cw]v\w]x^w^sbN|jTf~de}b{a}bg~d}cf|b|a{`z`}c~ckkhf~d~cdjmi~c{a|a~chiffgln^K|kVtpmlikqE=1,&rouuqoi}du^v_va}lYh\Mis|rzu{_tXy^lz{f~cid}jolaQvc~iptv{           zfp\k]MH@4!pbQxvyxppvppqqqpttnppnpoppoigmqronnnuK@4D:/paOva}eumXprrrtrnkh}eu_ygRfYHgxxtz~brWuZkx{f|akh|}mrnbSvcjox|}C;2   z}y|zyxyyuxurtvxyxyyywvywturqsr[=4*phjj}c}by`w_u^nXsbMbVFgvusy|`rVuZiv|xjdjg}rqeYHziUs]zc~fknqrxx~ycnoutrprompuxvy}{z|}~xwzyvxwrwy|yXM?qjnjggzaybu^oZwgSh\Nntzswx\uYy^iwzwkdnczts]Q@o^JwfPnWr[t\y_~dfegpVI;'!)"'!8/%@7+E:-w]yaaR?ZJ9uZqVoTrWtXuZrWqVrWuYw[w[uYx]y]y]y^|`y^z^}az^|ay^z_z^z^~b~b}b{`d("yfO{iRzgQ C8,;1')")#("QD5ieefljd|bx`nWtbMfYImu}tuwZvZz_iw{xl|bm~bxruZL;hU?p]F{hOnUoUsXoTrVy\y]wZuY}auYnSuYx[}am[F}a{_cff7/$d|`~bcdefedejgeejkeec}a}ac}ax\}a~bde{`}a}almfddv1*!srrnwsqsqquuqhzcq[ziUi^Oj{{zux\pUtY}ctzxl|ajfwmwfYJxfR~lUpYw^fgf|`degifchjkrqkhgkk-'kiljggjlgihjmmllnmlkhlljiijklmozbm^Lmrsswyd}{|~}{}{{|xsq~hva{jVj^Ol{zwzy]oTtY{atvsf~cnfxoyi]O}mXr]zckkmssnpx|{yywr\{xyuuq81(nonromqsrrolutmlqrrmmqqmrrqusru|lXv_ttuxx{dt_x{}{}}{wwqmliu_{kVj^Omxxzz^qUy^ettqd}cleypxfZL}mYvaktsw|}{}}|gZK  ~izusy|zxyvvvwtw{uvxtxyvstsuyvrtZO@NE8uspmok\JcVEqnqsqstqommje{bw`pZzjUh\Mnw~yw{^qUz^hu|v~c{ameu}nqZM7.~~>8/}}|lxkZ}~}||z|~{}~}w|{{~|z{|wpi}fxd}nZk`Qsu|}bnQdeystg}cldvrueYKxhTq\xb|e~fjpommnjjnpqpotspttpptrpsuwuuvutxxzzx& yxuio}}'" ~~~}y|~~|}{zwrojwc|mZk`Ryty~|`oScgyrygem|`ssueZLyjWs_}iljnomprponopoppomkklmlj~e|c|b|c}dy`x^v]w]w]v]w^|c{bw^raL'"{du[w]x^x^x_-&*$v]u[y_w^t\|d)#sbNg~eza~effg|c{b|c~eg|b}ehiklkkmnpnopoosysqtronk}gzer_xiWg\Ouuy}{}`pT~bmxr{eekgpxs\RCo`MzkXs_vawa{d}gkniihii|dyb{c|d|dhh{c{c~fhh}eh}e|d}ew_ya|dzb|dg{dw_yb~gi~nZj{d|dv_v^zczcmlyayb~g{dyb}fjzjVhizbxa{cyaxa{cybx`yat]v^zbzcu]t\v^v^yazcx`zb{czbt\qXrYrYqYs[u\sZs[v^v_s\~kU|jTxgRp_KeVBWK=ovy{|hpTv[itswjhsio\}irsXM?hZHpaNwgSyhTnYv`zdycxaxazdzcxbycw`w`zdwaxa{dzdzczcybycxbxaxaw`ybxav_u]xa{d{d|e}f|e|eyb{dzcw`ybzcu^t\s\r[qZo_Lm_Kr[yav_t\x`w`u]v^w`t\t]r[r[r[qYqYt\qZqYpX~lUlUlU~kTmV}jSzgP{hQ|jS}jT}kU}kT|iS{iR{hRxeO{iS~lV|iS{hRmVmW~lV}kV|kUxgRtcOraMpaNk\J_Q@SI;vuio~|fqTsYiss|p~dsfJIPHEKosMB4ZL:_P=`OXKWK/MA3J>/F;-?5)=5+lyu~||lxZsXgupo~dmf{vuqvrpqqtrqrvrrtonnkkmmoollpj~ghgjjpslptsoknok}eglkiklonjhgloppqnklmmmomkmprrnnnpsspptuuw|w{|zuy||mvVx\gxotifmfsvuyuppuyxwxyyyxtqsttnmomsurpsvoqsrqprsvyzwtwxxttx{wvyyxvtrr{~}z{}}~|||{wrsstvzxusrsy{yyyyx{|y{yz{}~z~~~{||zt|yit[oSg|}hTRWj[Qem~anyuqeGDJtb||{~~~~~~}}}}}|zxxvut{|xz{}||xxzxuw{tqputqusrrtstvusronponsroollihidiihhgf~d~cfiikkjn~jXB?FoaWgny~pqaU[ZbsbQ{_z|ha]bj[PdkbrTPSr¯}pPMQ|odnl]ROMU{hWawtxcek}}}|~}||y}|}~~~z}~zzzzyy||z~~{}|ywuu~|vw}scsZuXfxw}ji~}~}}||xz|}~|xbc{^x^zr}xnqipuvyxuusqptwsrxx{~yy{|zy|~|~{~~{yx{ywxyyvy}~}yyzxxyx~}}~}~~}||}|y~iz]y\|atsqd|^w\i[QfYPtYy[uYx\y]y\uYvYwZsWsVuXvYx[w[{_z^|_y\vYx[z^bcddccgfecghfeifccgehmligghdchecghgfgljlonmihlkjnpmmllomjlokllljilmqttqimqpqqrtoqtrqqlkjzk_vh\ilxrfy[y_v|qy]w[z\t\USYSPWiQ|]{_tXw\v[vZy]w[tXvZy]{_~cd}bce|`}b~c|agheez^z^z_vZtYuYy]|`z_v[x\x]tYvZvZtXy]}a|a~b|_z^}a~cy]x\z^z^|a{_|`dc{_w[~beddge~bd}b|a~bde}add{_}aeffg~c}bc~c~cd{`dd~b~cfeedhkhhknllqrqjURXOLSgpnx||]sWt~smqtozf|iqwvnqqqtplrtqoppokmmiijlomilnnjloomkjpsttroqpoopqsrptsqpsuwttqquprvqqpqsvvsqqpspnlmmnnqpllmmpomopppsrmmqspoottyftamtroyktT~{|~}{~~|~~~~}z{|xx}}}~~|y|ywyzvtuvxzuvwtwwxz{{||~|{~ \ No newline at end of file diff --git a/demos/src/bin/bezier.rs b/demos/src/bin/bezier.rs index 1f4ec69c..a011b87f 100644 --- a/demos/src/bin/bezier.rs +++ b/demos/src/bin/bezier.rs @@ -2,10 +2,10 @@ use core::ops::ControlFlow::Continue; use re::prelude::*; -use re::geom::{Edge, Ray}; -use re::math::rand::{Distrib, Uniform, VectorsOnUnitDisk, Xorshift64}; -use re::render::raster::line; -use re_front::{Frame, dims, minifb::Window}; +use re::core::geom::{Edge, Ray}; +use re::core::math::rand::{Distrib, Uniform, VectorsOnUnitDisk, Xorshift64}; +use re::core::render::raster::line; +use re::front::{Frame, dims, minifb::Window}; fn main() { let dims @ (w, h) = dims::SVGA_800_600; diff --git a/demos/src/bin/crates.rs b/demos/src/bin/crates.rs index 9a54286a..91a6d84c 100644 --- a/demos/src/bin/crates.rs +++ b/demos/src/bin/crates.rs @@ -2,15 +2,16 @@ use core::ops::ControlFlow::*; use re::prelude::*; -use re::math::color::gray; -use re::render::{ - ModelToProj, cam::FirstPerson, cam::Fov, clip::Status::Hidden, scene::Obj, +use re::core::math::color::gray; +use re::core::render::{ + ModelToProj, cam::FirstPerson, cam::Fov, clip::Status::*, scene::Obj, + tex::SamplerClamp, }; // Try also Rgb565 or Rgba4444 -use re::util::pixfmt::Rgba8888; +use re::core::util::{pixfmt::Rgba8888, pnm::read_pnm}; -use re_front::sdl2::Window; -use re_geom::solids::Cube; +use re::front::sdl2::Window; +use re::geom::solids::{Build, Cube}; fn main() { let mut win = Window::builder() @@ -19,24 +20,36 @@ fn main() { .build() .expect("should create window"); + let tex_data = *include_bytes!("../../assets/crate.ppm"); + let tex = Texture::from(read_pnm(&tex_data[..]).expect("data exists")); + + let light_dir = vec3(-2.0, 1.0, -4.0).normalize(); + let floor_shader = shader::new( |v: Vertex3<_>, mvp: &Mat4x4| { vertex(mvp.apply(&v.pos), v.attrib) }, - |frag: Frag| frag.var.to_color4(), + |frag: Frag| { + let even_odd = (frag.var.x() > 0.5) ^ (frag.var.y() > 0.5); + gray(if even_odd { 0.8 } else { 0.1 }).to_color4() + }, ); let crate_shader = shader::new( - |v: Vertex3, mvp: &Mat4x4| { - let [x, y, z] = ((v.attrib + splat(1.0)) / 2.0).0; - vertex(mvp.apply(&v.pos), rgb(x, y, z)) + |v: Vertex3<(Normal3, TexCoord)>, mvp: &Mat4x4| { + vertex(mvp.apply(&v.pos), v.attrib) + }, + |frag: Frag<(Normal3, TexCoord)>| { + let (n, uv) = frag.var; + let kd = lerp(n.dot(&light_dir).max(0.0), 0.4, 1.0); + let col = SamplerClamp.sample(&tex, uv); + (col.to_color3f() * kd).to_color4() }, - |frag: Frag| frag.var.to_color4(), ); let (w, h) = win.dims; let mut cam = Camera::new(win.dims) .transform(FirstPerson::default()) - .viewport((10..w - 10, 10..h - 10)) + .viewport((10..w - 10, h - 10..10)) .perspective(Fov::Diagonal(degs(90.0)), 0.1..1000.0); let floor = floor(); @@ -65,22 +78,20 @@ fn main() { let ms = ep.relative_mouse_state(); cam.transform.rotate( turns(ms.x() as f32) * -0.001, - turns(ms.y() as f32) * 0.001, + turns(ms.y() as f32) * -0.001, ); cam.transform .translate(cam_vel.mul(frame.dt.as_secs_f32())); - let flip = scale3(1.0, -1.0, -1.0).to(); - // // Render // - let world_to_project = flip.then(&cam.world_to_project()); + let world_to_project = &cam.world_to_project(); let batch = Batch::new() .viewport(cam.viewport) - .context(&frame.ctx); + .context(frame.ctx); // Floor { @@ -89,7 +100,7 @@ fn main() { if bbox.visibility(&model_to_project) != Hidden { batch .clone() - .mesh(&geom) + .mesh(geom) .uniform(&model_to_project) .shader(floor_shader) .target(&mut frame.buf) @@ -112,7 +123,7 @@ fn main() { batch // TODO Try to get rid of clone .clone() - .mesh(&geom) + .mesh(geom) .uniform(&model_to_project) // TODO Allow setting shader before uniform .shader(crate_shader) @@ -131,9 +142,8 @@ fn main() { .expect("should run") } -fn crates() -> Vec> { - let krate = Cube { side_len: 2.0 }.build(); - let obj = Obj::new(krate); +fn crates() -> Vec> { + let obj = Obj::new(Cube { side_len: 2.0 }.build()); let mut res = vec![]; let n = 30; @@ -147,17 +157,18 @@ fn crates() -> Vec> { } res } -fn floor() -> Obj { +fn floor() -> Obj { let mut bld = Mesh::builder(); let size = 50; for j in -size..=size { for i in -size..=size { - let even_odd = ((i & 1) ^ (j & 1)) == 1; + let i_odd = i & 1; + let j_odd = j & 1; let pos = pt3(i as f32, -1.0, j as f32); - let col = if even_odd { gray(0.2) } else { gray(0.9) }; - bld.push_vert(pos, col); + let attrib = vec2(i_odd as f32, j_odd as f32); + bld.push_vert(pos, attrib); if j > -size && i > -size { let w = size * 2 + 1; @@ -171,7 +182,7 @@ fn floor() -> Obj { ] .map(|i| i as usize); - if even_odd { + if i_odd ^ j_odd != 0 { bld.push_face(a, c, d); bld.push_face(a, d, b); } else { diff --git a/demos/src/bin/curses.rs b/demos/src/bin/curses.rs new file mode 100644 index 00000000..ec755f69 --- /dev/null +++ b/demos/src/bin/curses.rs @@ -0,0 +1,138 @@ +use std::time::Instant; + +use pancurses::*; + +use re::prelude::*; + +use re::core::render::{ + ctx::DepthSort::BackToFront, raster::Scanline, stats::Throughput, +}; +use re::geom::solids::{Build, Torus}; + +struct Win(Window); + +impl Win { + fn new() -> Self { + let w = initscr(); + w.nodelay(true); + curs_set(0); + start_color(); + + // Create an RGB 332 palette but keep the eight standard colors + for i in 8..256 { + // Range from 0 to 1000 + let r = (i & 0b111_000_00) * 4; + let g = (i & 0b000_111_00) * 35; + let b = (i & 0b000_000_11) * 330; + + init_color(i, r, g, b); + init_pair(i, i, i); + } + Self(w) + } +} +impl Drop for Win { + fn drop(&mut self) { + endwin(); + } +} + +fn main() { + let mut win = Win::new(); + + let ctx = Context { + depth_sort: Some(BackToFront), + ..Context::default() + }; + + let shader = shader::new( + |v: Vertex3<_>, mvp: &Mat4x4| { + vertex(mvp.apply(&v.pos), v.attrib) + }, + |frag: Frag| { + let [x, y, z] = (frag.var / 2.0 + splat(0.5)).0; + rgb(x, y, z).to_color4() + }, + ); + + let torus = Torus { + major_radius: 1.0, + minor_radius: 0.3, + major_sectors: 32, + minor_sectors: 16, + } + .build(); + + let (wh, ww) = win.0.get_max_yx(); + let aspect = ww as f32 / wh as f32 / 2.0; + let project = perspective(1.0, aspect, 1.0..10.0); + let viewport = viewport(pt2(4, 2)..pt2(ww as u32 - 4, wh as u32 - 2)); + + let start = Instant::now(); + loop { + win.0.clear(); + win.0.attrset(COLOR_PAIR(0)); + win.0.mvprintw(0, 0, "Q to quit"); + + ctx.stats.borrow_mut().frames += 1.0; + let t_secs = start.elapsed().as_secs_f32(); + + let mvp = rotate_x(rads(t_secs)) + .then(&rotate_y(rads(t_secs / 1.7))) + .then(&translate3(0.0, 0.0, 3.0 + t_secs.sin())) + .to() + .then(&project); + + render( + &torus.faces, + &torus.verts, + &shader, + &mvp, + viewport, + &mut win, + &ctx, + ); + + win.0.refresh(); + napms(10); + + if let Some(Input::Character('q')) = win.0.getch() { + break; + } + } + + println!("{}", ctx.stats.borrow()); +} + +impl Target for Win { + fn rasterize( + &mut self, + mut sc: Scanline, + fs: &Fs, + _ctx: &Context, + ) -> Throughput + where + V: Vary, + Fs: FragmentShader, + { + let w = sc.xs.len(); + let y = sc.y; + + self.0.mv(y as i32, sc.xs.start as i32); + + for frag in sc.fragments() { + let Some(col) = fs.shade_fragment(frag) else { + continue; + }; + let [r, g, b, _] = col.0.map(|c| c as u32); + + let col = (r & 0b111_000_00) + | (g / 9 & 0b000_111_00) + | (b / 85 & 0b000_000_11); + + // Avoid the eight standard colors + self.0.addch(COLOR_PAIR(col.max(8))); + } + Throughput { i: w, o: w } + } +} diff --git a/demos/src/bin/hello.rs b/demos/src/bin/hello.rs index 4ccaa067..21653a24 100644 --- a/demos/src/bin/hello.rs +++ b/demos/src/bin/hello.rs @@ -1,11 +1,12 @@ use std::{env, fmt::Write, ops::ControlFlow::Continue}; use re::prelude::*; -use re::render::{ + +use re::core::render::{ Text, tex::{Atlas, Layout}, }; -use re::util::pnm::parse_pnm; +use re::core::util::pnm::parse_pnm; use re_front::{Frame, dims::SVGA_800_600, minifb::Window}; @@ -60,7 +61,7 @@ fn main() { &mvp, viewport, &mut frame.buf, - &frame.ctx, + frame.ctx, ); Continue(()) }); diff --git a/demos/src/bin/solids.rs b/demos/src/bin/solids.rs index 80ab97b2..c1d05317 100644 --- a/demos/src/bin/solids.rs +++ b/demos/src/bin/solids.rs @@ -4,12 +4,13 @@ use minifb::{Key, KeyRepeat}; use re::prelude::*; -use re::geom::Polyline; -use re::math::{color::gray, mat::RealToReal, vec::ProjVec3}; -use re::render::cam::Fov; +use re::core::geom::Polyline; +use re::core::math::{color::gray, mat::RealToReal, vec::ProjVec3}; +use re::core::render::cam::Fov; -use re_front::{Frame, minifb::Window}; -use re_geom::{io::parse_obj, solids::*}; +use re::front::{Frame, minifb::Window}; +use re::geom::io::read_obj; +use re::geom::{io::parse_obj, solids::*}; // Carousel animation for switching between objects. #[derive(Default)] @@ -59,7 +60,7 @@ fn main() { let cam = Camera::new(win.dims) .transform(scale3(1.0, -1.0, -1.0).to()) .perspective(Fov::Equiv35mm(28.0), 0.1..1000.0) - .viewport(pt2(10, 10)..pt2(w - 10, h - 10)); + .viewport(pt2(10, h - 10)..pt2(w - 10, 10)); type VertexIn = Vertex3; type VertexOut = Vertex; @@ -72,7 +73,7 @@ fn main() { let diffuse = (norm.z() + 0.2).max(0.2) * 0.8; // Visualize normal by mapping to RGB values let [r, g, b] = (0.45 * (v.attrib + splat(1.1))).0; - let col = rgb(r, g, b).mul(diffuse); + let col = diffuse * rgb(r, g, b); vertex(mvp.apply(&v.pos), col) } @@ -84,7 +85,7 @@ fn main() { let objects = objects(8); - let translate = translate(-4.0 * Vec3::Z); + let translate = translate(-3.0 * Vec3::Z); let mut carousel = Carousel::default(); win.run(|frame| { @@ -161,7 +162,7 @@ fn lathe(secs: u32) -> Mesh { vertex(pt2(0.55, -0.25), vec2(1.0, 0.5)), vertex(pt2(0.5, 0.0), vec2(1.0, 0.0)), vertex(pt2(0.55, 0.25), vec2(1.0, -0.5)), - vertex(pt2(0.75, 0.5), vec2(1.0, 1.0)), + vertex(pt2(0.75, 0.5), vec2(1.0, -1.0)), ]; Lathe::new(Polyline::new(pts), secs, pts.len() as u32) .capped(true) @@ -170,22 +171,26 @@ fn lathe(secs: u32) -> Mesh { // Loads the Utah teapot model. fn teapot() -> Mesh { - parse_obj(*include_bytes!("../../assets/teapot.obj")) + static TEAPOT: &[u8] = include_bytes!("../../teapot.obj"); + let center_and_scale = scale(splat(0.4)).then(&translate(-0.5 * Vec3::Y)); + read_obj::<()>(TEAPOT) .unwrap() - .transform( - &scale(splat(0.4)) - .then(&translate(-0.5 * Vec3::Y)) - .to(), - ) + .transform(¢er_and_scale.to()) .with_vertex_normals() .build() } // Loads the Stanford bunny model. fn bunny() -> Mesh { - parse_obj(*include_bytes!("../../assets/bunny.obj")) - .unwrap() - .transform(&scale(splat(0.15)).then(&translate(-Vec3::Y)).to()) + static BUNNY: &[u8] = include_bytes!("../../assets/bunny.obj"); + let bunny = read_obj(BUNNY).unwrap(); + let verts = &bunny.mesh.verts; + let centroid: Vec3<_> = verts.iter().map(|v| v.pos.to_vec()).sum(); + let center_and_scale = translate(-centroid.to() / verts.len() as f32) + .then(&scale(splat(0.15))); + + bunny + .transform(¢er_and_scale.to()) .with_vertex_normals() .build() } diff --git a/demos/src/bin/sprites.rs b/demos/src/bin/sprites.rs index 5d4084c7..411a6533 100644 --- a/demos/src/bin/sprites.rs +++ b/demos/src/bin/sprites.rs @@ -2,8 +2,10 @@ use core::{array::from_fn, ops::ControlFlow::Continue}; use re::prelude::*; -use re::math::rand::{Distrib, PointsInUnitBall, Xorshift64}; -use re::render::{Model, ModelToView, ViewToProj, cam::*, render}; +use re::core::math::color::gray; +use re::core::math::rand::{Distrib, PointsInUnitBall, Xorshift64}; +use re::core::render::{Model, ModelToView, ViewToProj, cam::*, render}; + use re_front::minifb::Window; fn main() { @@ -24,7 +26,7 @@ fn main() { let tris: Vec<_> = (0..count) .map(|i| from_fn(|j| 4 * i + j)) - .flat_map(|[a, b, c, d]| [Tri([a, b, d]), Tri([a, d, c])]) + .flat_map(|[a, b, c, d]| [tri(a, b, d), tri(a, d, c)]) .collect(); let mut win = Window::builder() @@ -36,15 +38,14 @@ fn main() { |v: Vertex3>, (mv, proj): (&Mat4x4, &Mat4x4)| { let vertex_pos = 0.008 * v.attrib.to_vec3().to(); - let view_pos = mv.apply_pt(&v.pos) + vertex_pos; + let view_pos = mv.apply(&v.pos) + vertex_pos; vertex(proj.apply(&view_pos), v.attrib) }, |frag: Frag>| { let d2 = frag.var.len_sqr(); (d2 < 1.0).then(|| { - // TODO ops trait for colors - let col: Vec3 = splat(1.0) - d2 * vec3(0.25, 0.5, 1.0); - rgba(col.x(), col.y(), col.z(), 1.0).to_color4() + let col = gray(1.0) - d2 * rgb(0.25, 0.5, 1.0); + col.to_color4() }) }, ); @@ -53,7 +54,7 @@ fn main() { let cam = Camera::new(win.dims) .transform(translate(0.5 * Vec3::Z).to()) .perspective(Fov::FocalRatio(1.0), 1e-2..1e3) - .viewport(pt2(10, 10)..pt2(w - 10, h - 10)); + .viewport(pt2(10, h - 10)..pt2(w - 10, 10)); win.run(|frame| { let theta = rads(frame.t.as_secs_f32()); diff --git a/demos/src/bin/square.rs b/demos/src/bin/square.rs index 7fbd19a7..63414b8d 100644 --- a/demos/src/bin/square.rs +++ b/demos/src/bin/square.rs @@ -2,10 +2,8 @@ use std::ops::ControlFlow::*; use re::prelude::*; -use re::math::{pt2, pt3}; -use re::render::{Context, ModelToProj, render, tex::SamplerClamp}; - -use re_front::minifb::Window; +use re::core::render::tex::SamplerClamp; +use re::front::minifb::Window; fn main() { // Vertices of a square @@ -56,13 +54,13 @@ fn main() { .then(&projection); render( - [Tri([0, 1, 2]), Tri([3, 2, 1])], + [tri(0, 1, 2), tri(3, 2, 1)], verts, &shader, &model_view_project, viewport, &mut frame.buf, - &frame.ctx, + frame.ctx, ); Continue(()) diff --git a/demos/wasm/Cargo.toml b/demos/wasm/Cargo.toml index 17954b65..9a34f8d6 100644 --- a/demos/wasm/Cargo.toml +++ b/demos/wasm/Cargo.toml @@ -20,14 +20,19 @@ console_error_panic_hook = "0.1.7" [dependencies.re] package = "retrofire-core" +version = "0.4.0-pre4" path = "../../core" features = ["mm"] [dependencies.re-front] package = "retrofire-front" +version = "0.4.0-pre4" path = "../../front" features = ["wasm-dev"] default-features = false [dev-dependencies] wasm-bindgen-test = "0.3.34" + +[lints] +workspace = true diff --git a/demos/wasm/src/triangle.rs b/demos/wasm/src/triangle.rs index e677d3aa..f72a6185 100644 --- a/demos/wasm/src/triangle.rs +++ b/demos/wasm/src/triangle.rs @@ -42,7 +42,7 @@ pub fn start() { ); render( - [Tri([0, 1, 2])], // + [tri(0, 1, 2)], // vs, &sh, (), diff --git a/docs/bunny.jpg b/docs/bunny.jpg new file mode 100644 index 00000000..fd230c1d Binary files /dev/null and b/docs/bunny.jpg differ diff --git a/docs/crates.jpg b/docs/crates.jpg new file mode 100644 index 00000000..3dbd473b Binary files /dev/null and b/docs/crates.jpg differ diff --git a/docs/sprites.jpg b/docs/sprites.jpg new file mode 100644 index 00000000..cb4afe48 Binary files /dev/null and b/docs/sprites.jpg differ diff --git a/front/Cargo.toml b/front/Cargo.toml index 316aa6b1..1e8dd507 100644 --- a/front/Cargo.toml +++ b/front/Cargo.toml @@ -25,7 +25,7 @@ wasm = ["dep:wasm-bindgen", "dep:web-sys"] wasm-dev = ["wasm", "dep:console_error_panic_hook"] [dependencies] -retrofire-core = { version = "0.4.0-pre3", path = "../core" } +retrofire-core = { version = "0.4.0-pre4", path = "../core" } minifb = { version = "0.27.0", optional = true } sdl2 = { version = "0.35.2", optional = true } @@ -43,3 +43,6 @@ features = [ "CanvasRenderingContext2d", "ImageData", ] + +[lints] +workspace = true diff --git a/front/README.md b/front/README.md index 45e29e40..d62ea872 100644 --- a/front/README.md +++ b/front/README.md @@ -12,23 +12,23 @@ # Retrofire-front -Simple frontends for Retrofire. +Simple frontends for [`retrofire`][1]. + +[1]: https://crates.io/crates/retrofire ## Crate features -* `minifb`: - Enables a frontend using the [`minifb`](https://crates.io/crates/minifb) - library. +* `minifb`: Enables a frontend using the [`minifb`][2] library. +* `sdl2`: Enables a frontend using the [`sdl2`][3] library. +* `wasm` Enables a frontend using WebAssembly and [`wasm-bindgen`][4]. + +All features are disabled by default. -* `sdl2`: - Enables a frontend using the [`sdl2`](https://crates.io/crates/sdl2) - library. +[2]: https://crates.io/crates/minifb -* `wasm` - Enables a frontend using WebAssembly and - [`wasm-bindgen`](https://crates.io/crates/wasm-bindgen). +[3]: https://crates.io/crates/sdl2 -All features are disabled by default. +[4]: https://crates.io/crates/wasm-bindgen ## License diff --git a/front/src/lib.rs b/front/src/lib.rs index de5d1229..a088da28 100644 --- a/front/src/lib.rs +++ b/front/src/lib.rs @@ -7,7 +7,7 @@ use core::time::Duration; use retrofire_core::{ math::Color4, - render::{Context, Framebuf, target::Colorbuf}, + render::{Colorbuf, Context, Framebuf}, util::{buf::AsMutSlice2, pixfmt::IntoPixel}, }; diff --git a/front/src/minifb.rs b/front/src/minifb.rs index 76c1dea7..6bc3d3a4 100644 --- a/front/src/minifb.rs +++ b/front/src/minifb.rs @@ -1,17 +1,18 @@ //! Frontend using the `minifb` crate for window creation and event handling. use std::ops::ControlFlow::{self, Break}; -use std::time::Instant; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; use minifb::{Key, WindowOptions}; +use super::{Frame, dims}; +use retrofire_core::math::rgb; +use retrofire_core::util::pnm::save_ppm; use retrofire_core::{ - render::{Context, target, target::Colorbuf}, + render::{Colorbuf, Context, target}, util::{Dims, buf::Buf2, buf::MutSlice2, pixfmt::Xrgb8888}, }; -use super::{Frame, dims}; - /// A lightweight wrapper of a `minibuf` window. pub struct Window { /// The wrapped minifb window. @@ -140,6 +141,20 @@ impl Window { } self.present(cbuf.data_mut()); + if self.imp.is_key_released(Key::P) { + // TODO there should be a map method in bufs... + let shot = Buf2::new_from( + cbuf.dims(), + cbuf.data().iter().map(|c| { + let [_, r, g, b] = c.to_be_bytes(); + rgb(r, g, b) + }), + ); + let now = UNIX_EPOCH.elapsed().unwrap().as_millis() + - 1_735_682_400_000; // Jan 1, 2025 + save_ppm(format!("screenshot-{now}.ppm"), shot).unwrap() + } + ctx.stats.borrow_mut().frames += 1.0; } println!("{}", ctx.stats.borrow()); diff --git a/front/src/sdl2.rs b/front/src/sdl2.rs index 607f3d94..8f9bf3d5 100644 --- a/front/src/sdl2.rs +++ b/front/src/sdl2.rs @@ -3,35 +3,35 @@ use std::{fmt, mem::replace, ops::ControlFlow, time::Instant}; use sdl2::{ - EventPump, IntegerOrSdlError, + EventPump, IntegerOrSdlError, Sdl, event::Event, - keyboard::Keycode, + keyboard::{Keycode, Scancode}, pixels::PixelFormatEnum, render::{Texture, TextureValueError, WindowCanvas}, - video::{FullscreenType, WindowBuildError}, + video::{FullscreenType, Window as SdlWindow, WindowBuildError}, }; -use retrofire_core::math::{Color4, Vary}; +use super::{Frame, dims}; +use retrofire_core::math::{Color4, Vary, rgb}; +use retrofire_core::prelude::Color3; use retrofire_core::render::{ - Context, FragmentShader, Target, raster::Scanline, stats::Throughput, - target::Colorbuf, + Colorbuf, Context, FragmentShader, Target, raster::Scanline, + stats::Throughput, target::rasterize_fb, }; +use retrofire_core::util::pnm::save_ppm; use retrofire_core::util::{ Dims, buf::{AsMutSlice2, Buf2, MutSlice2}, pixfmt::{IntoPixel, Rgb565, Rgba4444, Rgba8888}, }; -use super::{Frame, dims}; - /// Helper trait to support different pixel format types. -pub trait PixelFmt: Default { - type Pixel: AsRef<[u8]>; - const INSTANCE: Self; +pub trait PixelFmt: Copy + Default { + type Pixel: AsRef<[u8]> + Copy + Sized; const SDL_FMT: PixelFormatEnum; - fn size() -> usize { - size_of::() + fn encode>(self, color: C) -> Self::Pixel { + color.into_pixel_fmt(self) } } @@ -44,12 +44,14 @@ pub struct Window { pub canvas: WindowCanvas, /// The SDL event pump. pub ev_pump: EventPump, + /// Pending events. + pub events: Vec, /// The width and height of the window. pub dims: Dims, - /// Rendering context defaults. - pub ctx: Context, /// Framebuffer pixel format. pub pixfmt: PF, + /// Rendering context defaults. + pub ctx: Context, } /// Builder for creating `Window`s. @@ -57,12 +59,13 @@ pub struct Builder<'title, PF> { pub dims: (u32, u32), pub title: &'title str, pub vsync: bool, + pub hidpi: bool, pub fs: FullscreenType, pub pixfmt: PF, } -pub struct Framebuf<'a, PF> { - pub color_buf: Colorbuf, PF>, +pub struct Framebuf<'a, PF: PixelFmt> { + pub color_buf: Colorbuf, PF>, pub depth_buf: MutSlice2<'a, f32>, } @@ -88,6 +91,13 @@ impl<'t, PF: PixelFmt> Builder<'t, PF> { self.vsync = enabled; self } + /// Sets whether high-dpi + /// + /// If true, the physical resolution may be higher than the logical resolution. + pub fn high_dpi(mut self, enabled: bool) -> Self { + self.hidpi = enabled; + self + } /// Sets the fullscreen state of the window. pub fn fullscreen(mut self, fs: FullscreenType) -> Self { self.fs = fs; @@ -95,7 +105,7 @@ impl<'t, PF: PixelFmt> Builder<'t, PF> { } /// Sets the framebuffer pixel format. /// - /// Supported formats are [`Rgba8888`], [`Rgb565`], and [`Rgb4444]. + /// Supported formats are [`Rgba8888`], [`Rgb565`], and [`Rgba4444`]. pub fn pixel_fmt(mut self, fmt: PF) -> Self { self.pixfmt = fmt; self @@ -103,44 +113,55 @@ impl<'t, PF: PixelFmt> Builder<'t, PF> { /// Creates the window. pub fn build(self) -> Result, Error> { - let Self { dims, title, vsync, fs, pixfmt } = self; - let sdl = sdl2::init()?; + let win = self.create_window(&sdl)?; - let mut win = sdl - .video()? - .window(title, dims.0, dims.1) - .build()?; + self.set_mouse_mode(&sdl); + + let canvas = self.create_canvas(win)?; + let ev_pump = sdl.event_pump()?; + let ctx = Context::default(); + + Ok(Window { + canvas, + ev_pump, + ctx, + events: Vec::new(), + dims: self.dims, + pixfmt: self.pixfmt, + }) + } + fn create_window(&self, sdl: &Sdl) -> Result { + let Self { + dims: (w, h), title, fs, hidpi, .. + } = *self; + let mut win = sdl.video()?.window(title, w, h); + if hidpi { + win.allow_highdpi(); + } + let mut win = win.build()?; win.set_fullscreen(fs)?; - sdl.mouse().set_relative_mouse_mode(true); + Ok(win) + } - let mut canvas = win.into_canvas(); - if vsync { + fn create_canvas(&self, w: SdlWindow) -> Result { + let mut canvas = w.into_canvas(); + if self.vsync { canvas = canvas.present_vsync(); } - let canvas = canvas.accelerated().build()?; - - let ev_pump = sdl.event_pump()?; - - let ctx = Context::default(); + Ok(canvas.accelerated().build()?) + } + fn set_mouse_mode(&self, sdl: &Sdl) { let m = sdl.mouse(); m.set_relative_mouse_mode(true); m.capture(true); m.show_cursor(true); - - Ok(Window { - canvas, - ev_pump, - dims, - ctx, - pixfmt, - }) } } -impl Window { +impl, const N: usize> Window { /// Returns a window builder. pub fn builder() -> Builder<'static, PF> { Builder::default() @@ -148,13 +169,14 @@ impl Window { /// Copies the texture to the frame buffer and updates the screen. pub fn present(&mut self, tex: &Texture) -> Result<(), Error> { - self.canvas.copy(&tex, None, None)?; + self.canvas.copy(tex, None, None)?; self.canvas.present(); Ok(()) } - /// Runs the main loop of the program, invoking the callback on each - /// iteration to compute and draw the next frame. + /// Runs the main loop of the program. + /// + /// Invokes `frame_fn` on each iteration to compute and draw the next frame. /// /// The main loop stops and this function returns if: /// * the user closes the window via the GUI (e.g. a title bar button); @@ -165,44 +187,42 @@ impl Window { F: FnMut(&mut Frame>) -> ControlFlow<()>, Color4: IntoPixel, { - let (w, h) = self.dims; + let dims @ (w, h) = self.canvas.window().drawable_size(); let tc = self.canvas.texture_creator(); let mut tex = tc.create_texture_streaming(PF::SDL_FMT, w, h)?; - let mut zbuf = Buf2::new(self.dims); + let mut zbuf = Buf2::new(dims); let mut ctx = self.ctx.clone(); let start = Instant::now(); let mut last = Instant::now(); 'main: loop { + self.events.clear(); for e in self.ev_pump.poll_iter() { match e { Event::Quit { .. } | Event::KeyDown { keycode: Some(Keycode::Escape), .. } => break 'main, - _ => (), + e => self.events.push(e), } } let cf = tex.with_lock(None, |bytes, pitch| { - if let Some(c) = ctx.depth_clear { + let bytes = bytes.as_chunks_mut().0; + let pitch = pitch / N; + + if let Some(z) = ctx.depth_clear { // Z-buffer stores reciprocals - zbuf.fill(c.recip()); + zbuf.fill(z.recip()); } if let Some(c) = ctx.color_clear { - let c: PF::Pixel = c.into_pixel_fmt(PF::INSTANCE); - bytes.chunks_exact_mut(PF::size()).for_each(|ch| { - ch.copy_from_slice(c.as_ref()); - }); + bytes.fill(self.pixfmt.encode(c)); } - let color_buf = Colorbuf::new(MutSlice2::new( - (PF::size() as u32 * w, h), - pitch as u32, - bytes, - )); + let color_buf = + Colorbuf::new(MutSlice2::new(dims, pitch as u32, bytes)); let buf = Framebuf { color_buf, depth_buf: zbuf.as_mut_slice2(), @@ -215,7 +235,23 @@ impl Window { win: self, ctx: &mut ctx, }; - frame_fn(frame) + let res = frame_fn(frame); + + if frame + .win + .ev_pump + .keyboard_state() + .is_scancode_pressed(Scancode::P) + { + let data = frame.buf.color_buf.buf.data(); + let img = Buf2::new_from( + frame.win.dims, + data.iter().map(|c| rgb(c[0], c[1], c[2])), + ); + save_ppm("sdl2_screenshot.ppm", img).unwrap(); + } + + res })?; self.present(&tex)?; @@ -236,17 +272,14 @@ impl Window { impl PixelFmt for Rgba8888 { type Pixel = [u8; 4]; - const INSTANCE: Self = Self; const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGBA32; } impl PixelFmt for Rgb565 { type Pixel = [u8; 2]; - const INSTANCE: Self = Self; const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGB565; } impl PixelFmt for Rgba4444 { type Pixel = [u8; 2]; - const INSTANCE: Self = Self; const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGBA4444; } @@ -257,40 +290,18 @@ where { fn rasterize>( &mut self, - mut sl: Scanline, + sl: Scanline, fs: &Fs, ctx: &Context, ) -> Throughput { - // TODO Lots of duplicate code - - let x0 = sl.xs.start; - let x1 = sl.xs.end.max(x0); - // TODO use as_chunks once stable - let mut cbuf_span = - &mut self.color_buf.buf[sl.y][PF::size() * x0..PF::size() * x1]; - let zbuf_span = &mut self.depth_buf.as_mut_slice2()[sl.y][x0..x1]; - - let mut io = Throughput { i: x1 - x0, o: 0 }; - - for (frag, z) in sl.fragments().zip(zbuf_span) { - let c: &mut PF::Pixel; - (c, cbuf_span) = cbuf_span.split_first_chunk_mut().unwrap(); - - let new_z = frag.pos.z(); - if ctx.depth_test(new_z, *z) { - if let Some(new_c) = fs.shade_fragment(frag) { - if ctx.color_write { - // TODO Blending should happen here - io.o += 1; - *c = new_c.into_pixel_fmt(PF::INSTANCE); - } - if ctx.depth_write { - *z = new_z; - } - } - } - } - io + rasterize_fb( + &mut self.color_buf, + &mut self.depth_buf, + sl, + fs, + |c| c.into_pixel(), + ctx, + ) } } @@ -301,7 +312,8 @@ impl Default for Builder<'_, PF> { title: "// retrofire application //", vsync: true, fs: FullscreenType::Off, - pixfmt: PF::INSTANCE, + pixfmt: PF::default(), + hidpi: false, } } } diff --git a/front/src/wasm.rs b/front/src/wasm.rs index db2c64bc..b696e54f 100644 --- a/front/src/wasm.rs +++ b/front/src/wasm.rs @@ -16,7 +16,7 @@ use wasm_bindgen::{Clamped, prelude::*}; use web_sys::{ CanvasRenderingContext2d as Context2d, Document, - HtmlCanvasElement as Canvas, ImageData, + HtmlCanvasElement as Canvas, HtmlCanvasElement, ImageData, js_sys::{Uint8ClampedArray, Uint32Array}, }; @@ -24,7 +24,7 @@ use crate::{Frame, dims::SVGA_800_600}; use retrofire_core::{ math::color::rgba, - render::{Context, Stats, target, target::Colorbuf}, + render::{Colorbuf, Context, Stats, target}, util::buf::{AsMutSlice2, Buf2, MutSlice2}, util::{Dims, pixfmt::Argb8888}, }; @@ -60,7 +60,7 @@ pub type Framebuf<'a> = target::Framebuf< impl Builder { pub fn dims(self, dims: Dims) -> Self { - Self { dims, ..self } + Self { dims } } } @@ -138,17 +138,16 @@ impl Window { web_sys::window()?.document() } - fn create_canvas(dims: Dims) -> Option { - Self::document()? + fn create_canvas((w, h): Dims) -> Option { + let cvs: HtmlCanvasElement = Self::document()? .create_element("canvas") .ok()? .dyn_into() - .map(|cvs: Canvas| { - cvs.set_width(dims.0); - cvs.set_height(dims.1); - cvs - }) - .ok() + .ok()?; + + cvs.set_width(w); + cvs.set_height(h); + Some(cvs) } fn context2d(cvs: &Canvas) -> Option { diff --git a/geom/Cargo.toml b/geom/Cargo.toml index 5d7eb4ab..8266d85b 100644 --- a/geom/Cargo.toml +++ b/geom/Cargo.toml @@ -21,7 +21,10 @@ categories.workspace = true repository.workspace = true [dependencies] -re = { version = "0.4.0-pre3", path = "../core", package = "retrofire-core", default-features = false } +re = { version = "0.4.0-pre4", path = "../core", package = "retrofire-core", default-features = false } [features] std = ["re/std"] + +[lints] +workspace = true diff --git a/geom/README.md b/geom/README.md index 85e5f115..25eb84c6 100644 --- a/geom/README.md +++ b/geom/README.md @@ -12,13 +12,13 @@ # Retrofire-geom -Additional geometry types and routines for Retrofire. +Additional geometry types and routines for [`retrofire`][1]. -## Crate features +[1]: https://crates.io/crates/retrofire -* `std`: Enables file I/O. +## Crate features -All features are disabled by default. +* `std`: Enables file I/O. Disabled by default. ## License diff --git a/geom/src/io.rs b/geom/src/io.rs index 4742df71..d25b1d51 100644 --- a/geom/src/io.rs +++ b/geom/src/io.rs @@ -37,7 +37,7 @@ //! f 1//7 2//4 3//8 //! ``` -use alloc::{string::String, vec}; +use alloc::{collections::BTreeMap, string::String, vec::Vec}; use core::{ fmt::{self, Display, Formatter}, num::{ParseFloatError, ParseIntError}, @@ -59,35 +59,50 @@ use Error::*; #[derive(Debug)] pub enum Error { #[cfg(feature = "std")] - /// An input/output error during reading from a `Read`. + /// An input/output error while reading from a [`Read`]. Io(std::io::Error), /// An item that is not a face, vertex, texture coordinate, or normal. - UnsupportedItem(char), + UnsupportedItem(Vec), /// Unexpected end of line or input. UnexpectedEnd, /// An invalid integer or floating-point value. InvalidValue, /// A vertex attribute index that refers to a nonexistent attribute. IndexOutOfBounds(&'static str, usize), + /// Requested vertex attribute not contained in input + MissingVertexAttribType(&'static str), } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Default)] +pub struct Obj { + faces: Vec>, + coords: Vec>, + norms: Vec, + texcs: Vec, +} + +pub type Result = core::result::Result; + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Ord, PartialOrd)] struct Indices { pos: usize, uv: Option, n: Option, } -pub type Result = core::result::Result; +#[derive(Copy, Clone, Debug)] +enum Face { + Tri([Indices; 3]), + Quad([Indices; 4]), +} /// Loads an OBJ model from a path. /// /// # Errors /// Returns [`Error`] if I/O or OBJ parsing fails. #[cfg(feature = "std")] -pub fn load_obj(path: impl AsRef) -> Result> { - let r = &mut BufReader::new(File::open(path)?); - read_obj(r) +pub fn load_obj(path: impl AsRef) -> Result> { + read_obj(File::open(path)?) } /// Reads an OBJ format mesh from input. @@ -95,7 +110,8 @@ pub fn load_obj(path: impl AsRef) -> Result> { /// # Errors /// Returns [`Error`] if I/O or OBJ parsing fails. #[cfg(feature = "std")] -pub fn read_obj(input: impl Read) -> Result> { +pub fn read_obj(input: impl Read) -> Result> { + let input = BufReader::new(input); let mut io_res: Result<()> = Ok(()); let res = parse_obj(input.bytes().map_while(|r| match r { Err(e) => { @@ -109,17 +125,16 @@ pub fn read_obj(input: impl Read) -> Result> { /// Parses an OBJ format mesh from an iterator. /// -/// TODO Parses normals and coords but does not return them -/// /// # Errors -/// Returns [`Error`] if OBJ parsing fails. -pub fn parse_obj(src: impl IntoIterator) -> Result> { - let mut faces = vec![]; - let mut verts = vec![]; - let mut norms = vec![]; - let mut texcs = vec![]; - - let mut max_i = Indices { pos: 0, uv: None, n: None }; +/// Returns [`self::Error`] if OBJ parsing fails. +pub fn parse_obj(src: impl IntoIterator) -> Result> +where + Builder: TryFrom, +{ + let mut obj = Obj::default(); + let Obj { faces, coords, norms, texcs } = &mut obj; + + let mut max_i = Indices::default(); let mut line = String::new(); let mut it = src.into_iter().peekable(); @@ -133,66 +148,162 @@ pub fn parse_obj(src: impl IntoIterator) -> Result> { ); let tokens = &mut line.split_ascii_whitespace(); - let Some(item) = tokens.next() else { - continue; - }; - match item.as_bytes() { - // Comment; skip it - [b'#', ..] => continue, - // Vertex position - b"v" => verts.push(parse_point(tokens)?), + match tokens.next().unwrap_or("").as_bytes() { + // Skip empty lines and comments + | b"" | [b'#', ..] + // Skip group or material definitions for now + | b"g" | b"mtllib" | b"usemtl" + // Skip smoothing group names for now + | b"s" => continue, + + // Vertex coordinate + b"v" => coords.push(parse_point(tokens)?), // Texture coordinate b"vt" => texcs.push(parse_texcoord(tokens)?), // Normal vector b"vn" => norms.push(parse_normal(tokens)?), + // Face b"f" => { - let tri = parse_face(tokens)?; + let face = parse_face(tokens)?; + + let indices = match &face { + Face::Tri(is) => is.as_slice(), + Face::Quad(is) => is.as_slice(), + }; + + if max_i.n.is_some() && indices[0].n.is_none() { + todo!("return error if not all faces have normals") + } + if max_i.uv.is_some() && indices[0].uv.is_none() { + todo!("return error if not all faces have texcoords") + } + // Keep track of max indices to report error at the end of // parsing if there turned out to be out-of-bounds indices - for i in tri.0 { + for i in indices { max_i.pos = max_i.pos.max(i.pos); max_i.uv = max_i.uv.max(i.uv); max_i.n = max_i.n.max(i.n); } - faces.push(tri) + if let [a, b, c] = *indices { + faces.push(Tri([a, b, c])); + } else if let [a, b, c, d] = *indices { + faces.push(Tri([a, b, c])); + faces.push(Tri([a, c, d])); + } } // TODO Ignore unsupported lines instead? - [c, ..] => return Err(UnsupportedItem(*c as char)), - b"" => unreachable!("empty slices are filtered out"), + other => { + return Err(UnsupportedItem(other.to_vec())); + } } } - if !verts.is_empty() && max_i.pos >= verts.len() { + if !coords.is_empty() && max_i.pos >= coords.len() { return Err(IndexOutOfBounds("vertex", max_i.pos)); } - if let Some(uv) = max_i.uv.filter(|&i| i >= texcs.len()) { + if let Some(uv) = max_i.uv + && uv >= texcs.len() + { return Err(IndexOutOfBounds("texcoord", uv)); } - if let Some(n) = max_i.n.filter(|&i| i >= norms.len()) { + if let Some(n) = max_i.n + && n >= norms.len() + { return Err(IndexOutOfBounds("normal", n)); } + obj.try_into() +} + +impl TryFrom for Builder<()> { + type Error = Error; + + fn try_from(o: Obj) -> Result { + o.try_into_with(|_| Some(())) + } +} +impl TryFrom for Builder { + type Error = Error; - // TODO Support returning texcoords and normals - let faces = faces - .into_iter() - .map(|Tri(vs)| Tri(vs.map(|ics| ics.pos))); - let verts = verts.into_iter().map(|pos| vertex(pos, ())); + fn try_from(o: Obj) -> Result { + if o.norms.is_empty() { + return Err(MissingVertexAttribType("normal")); + } + o.try_into_with(|i| i.n.map(|ti| o.norms[ti])) + } +} +impl TryFrom for Builder { + type Error = Error; - Ok(Mesh::new(faces, verts).into_builder()) + fn try_from(o: Obj) -> Result { + if o.texcs.is_empty() { + return Err(MissingVertexAttribType("texcoord")); + } + o.try_into_with(|i| i.uv.map(|ti| o.texcs[ti])) + } +} +impl TryFrom for Builder<(Normal3, TexCoord)> { + type Error = Error; + + fn try_from(o: Obj) -> Result { + if o.norms.is_empty() { + return Err(MissingVertexAttribType("normal")); + } + if o.texcs.is_empty() { + return Err(MissingVertexAttribType("texcoord")); + } + o.try_into_with(|i| { + i.n.zip(i.uv) + .map(|(n, uv)| (o.norms[n], o.texcs[uv])) + }) + } +} + +impl Obj { + fn try_into_with( + &self, + mut attr_fn: impl FnMut(&Indices) -> Option, + ) -> Result> { + // HashMap not in alloc :( + let mut map: BTreeMap = BTreeMap::new(); + + let mut faces = Vec::new(); + let mut verts = Vec::new(); + + for Tri(indices) in &self.faces { + if indices.iter().any(|i| attr_fn(i).is_none()) { + return Err(MissingVertexAttribType("")); + } + let indices = indices.map(|v| { + *map.entry(v).or_insert_with(|| { + verts.push(vertex( + self.coords[v.pos], + attr_fn(&v).unwrap(), // TODO + )); + verts.len() - 1 + }) + }); + faces.push(Tri(indices)); + } + Ok(Mesh::new(faces, verts).into_builder()) + } } fn next<'a>(i: &mut impl Iterator) -> Result<&'a str> { i.next().ok_or(UnexpectedEnd) } -fn parse_face<'a>( - i: &mut impl Iterator, -) -> Result> { +fn parse_face<'a>(i: &mut impl Iterator) -> Result { let a = parse_indices(next(i)?)?; let b = parse_indices(next(i)?)?; let c = parse_indices(next(i)?)?; - Ok(Tri([a, b, c])) + if let Some(d) = i.next() { + let d = parse_indices(d)?; + Ok(Face::Quad([a, b, c, d])) + } else { + Ok(Face::Tri([a, b, c])) + } } fn parse_texcoord<'a>( @@ -258,14 +369,19 @@ impl Display for Error { match self { #[cfg(feature = "std")] Io(e) => write!(f, "I/O error: {e}"), - UnsupportedItem(c) => { - write!(f, "unsupported item type '{c}'") + UnsupportedItem(item) => { + f.write_str("unsupported item type: '")?; + f.write_str(String::from_utf8_lossy(item).as_ref()) } UnexpectedEnd => f.write_str("unexpected end of input"), InvalidValue => f.write_str("invalid numeric value"), IndexOutOfBounds(item, idx) => { write!(f, "{item} index out of bounds: {idx}") } + MissingVertexAttribType(s) => { + f.write_str("missing vertex attribute: ")?; + f.write_str(s) + } } } } @@ -299,9 +415,9 @@ impl From for Error { #[cfg(test)] mod tests { - use re::{geom::Tri, math::point::pt3}; - use super::*; + use re::geom::Vertex; + use re::{geom::Tri, math::point::pt3}; #[test] fn input_with_whitespace_and_comments() { @@ -316,48 +432,57 @@ v 1.0 0.0 0.0 v 0.0 -2.0 0.0 v 1 2 3"; - let mesh = parse_obj(input).unwrap().build(); + let m = &parse_obj::<()>(input).unwrap().build(); + + assert_eq!(m.faces.len(), 2); + assert_eq!(m.verts.len(), 4); - assert_eq!(mesh.faces, vec![Tri([0, 1, 3]), Tri([3, 0, 2])]); - assert_eq!(mesh.verts[3].pos, pt3(1.0, 2.0, 3.0)); + assert_eq!( + m.faces() + .map(|tri| tri.0.map(|v| v.pos)) + .collect::>(), + [ + [pt3(0.0, 0.0, 0.0), pt3(1.0, 0.0, 0.0), pt3(1.0, 2.0, 3.0)], + [pt3(1.0, 2.0, 3.0), pt3(0.0, 0.0, 0.0), pt3(0.0, -2.0, 0.0)] + ] + ); } #[test] - fn exp_notation() { - let input = *b"v -1.0e0 0.2e1 3.0e-2"; - let mesh = parse_obj(input).unwrap().build(); - assert_eq!(mesh.verts[0].pos, pt3(-1.0, 2.0, 0.03)); + fn float_formats() { + let input = *br" + v 1 -2 +3 + v 1.0 -2.0 +3.0 + v .1 -.2 +.3 + v 1. -2. +3. + v 1.0e0 -0.2e1 +300.0e-2 + f 1 2 3 + f 3 4 5"; + let mesh: Mesh<()> = parse_obj(input).unwrap().build(); + assert_eq!(mesh.verts[0].pos, pt3(1.0, -2.0, 3.0)); + assert_eq!(mesh.verts[4].pos, pt3(1.0, -2.0, 3.0)); } #[test] - fn positions_and_texcoords() { + fn quads() { let input = *br" - f 1/1/1 2/3/2 3/2/2 - f 4/3/2 1/2/3 3/1/3 - - vn 1.0 0.0 0.0 - vt 0.0 0.0 0.0 v 0.0 0.0 0.0 v 1.0 0.0 0.0 - vn 1.0 0.0 0.0 - v 0.0 2.0 0.0 - vt 1.0 1.0 1.0 - v 1.0 2.0 3.0 - vt 0.0 -1.0 2.0 - vn 1.0 0.0 0.0"; - - let mesh = parse_obj(input).unwrap().build(); - assert_eq!(mesh.faces, vec![Tri([0, 1, 2]), Tri([3, 0, 2])]); - - let v = mesh.verts[3]; - assert_eq!(v.pos, pt3(1.0, 2.0, 3.0)); + v 1.0 1.0 0.0 + v 0.0 1.0 0.0 + f 1 2 3 4 + "; + let mesh: Mesh<()> = parse_obj(input).unwrap().build(); + + assert_eq!(mesh.faces.len(), 2); + assert_eq!(mesh.faces, [Tri([0, 1, 2]), Tri([0, 2, 3])]); } #[test] fn positions_and_normals() { let input = *br" f 1//1 2//3 4//2 - f 4//3 1//2 3//1 + f 4//3 1//1 3//1 vn 1.0 0.0 0.0 v 0.0 0.0 0.0 @@ -367,34 +492,118 @@ v 0.0 -2.0 0.0 v 1.0 2.0 3.0 vn 0.0 0.0 -1.0"; - let mesh = parse_obj(input).unwrap().build(); - assert_eq!(mesh.faces, vec![Tri([0, 1, 3]), Tri([3, 0, 2])]); - assert_eq!(mesh.verts[3].pos, pt3(1.0, 2.0, 3.0)); + let m: Mesh = parse_obj(input).unwrap().build(); + + assert_eq!(m.faces.len(), 2); + assert_eq!(m.verts.len(), 5); + + assert_eq!( + m.faces() + .map(|tri| tri.0.map(|&Vertex { pos, attrib: n }| (pos, n))) + .collect::>(), + [ + [ + (pt3(0.0, 0.0, 0.0), vec3(1.0, 0.0, 0.0)), + (pt3(1.0, 0.0, 0.0), vec3(0.0, 0.0, -1.0)), + (pt3(1.0, 2.0, 3.0), vec3(0.0, 1.0, 0.0)), + ], + [ + (pt3(1.0, 2.0, 3.0), vec3(0.0, 0.0, -1.0)), + (pt3(0.0, 0.0, 0.0), vec3(1.0, 0.0, 0.0)), + (pt3(0.0, 2.0, 0.0), vec3(1.0, 0.0, 0.0)) + ] + ] + ); + } + + #[test] + fn positions_and_texcoords() { + let input = *br" + f 1/1 2/3 4/2 + f 4/3 1/2 3/1 + + vt 0.0 0.0 + v 0.0 0.0 0.0 + v 1.0 0.0 0.0 + v 0.0 2.0 0.0 + vt 0.0 1.0 + v 1.0 2.0 3.0 + vt 1.0 1.0"; + + let m: Mesh = parse_obj(input).unwrap().build(); + + assert_eq!(m.faces.len(), 2); + assert_eq!(m.verts.len(), 6); + + assert_eq!( + m.faces() + .map(|tri| tri.0.map(|&Vertex { pos, attrib: uv }| (pos, uv))) + .collect::>(), + [ + [ + (pt3(0.0, 0.0, 0.0), uv(0.0, 0.0)), + (pt3(1.0, 0.0, 0.0), uv(1.0, 1.0)), + (pt3(1.0, 2.0, 3.0), uv(0.0, 1.0)) + ], + [ + (pt3(1.0, 2.0, 3.0), uv(1.0, 1.0)), + (pt3(0.0, 0.0, 0.0), uv(0.0, 1.0)), + (pt3(0.0, 2.0, 0.0), uv(0.0, 0.0)) + ] + ] + ); } #[test] fn positions_texcoords_and_normals() { let input = *br" - f 1//1 2//3 4//2 - f 4//3 1//2 3//1 + f 1/1/1 2/3/2 3/2/2 + f 4/3/2 1/1/1 3/1/3 vn 1.0 0.0 0.0 + vt 0.0 0.0 v 0.0 0.0 0.0 v 1.0 0.0 0.0 - v 0.0 2.0 0.0 vn 0.0 1.0 0.0 + v 0.0 2.0 0.0 + vt 1.0 1.0 v 1.0 2.0 3.0 - vn 0.0 0.0 -1.0"; + vt 0.0 -1.0 + vn 0.0 0.0 1.0"; - let mesh = parse_obj(input).unwrap().build(); - assert_eq!(mesh.faces, vec![Tri([0, 1, 3]), Tri([3, 0, 2])]); - assert_eq!(mesh.verts[3].pos, pt3(1.0, 2.0, 3.0)); + let m = &parse_obj::<(Normal3, TexCoord)>(input) + .unwrap() + .build(); + assert_eq!(m.faces.len(), 2); + assert_eq!(m.verts.len(), 5); + + assert_eq!( + m.faces() + .map(|tri| tri + .0 + .map(|&Vertex { pos, attrib: (n, uv) }| (pos, n, uv))) + .collect::>(), + [ + [ + (pt3(0.0, 0.0, 0.0), vec3(1.0, 0.0, 0.0), uv(0.0, 0.0)), + (pt3(1.0, 0.0, 0.0), vec3(0.0, 1.0, 0.0), uv(0.0, -1.0)), + (pt3(0.0, 2.0, 0.0), vec3(0.0, 1.0, 0.0), uv(1.0, 1.0)) + ], + [ + (pt3(1.0, 2.0, 3.0), vec3(0.0, 1.0, 0.0), uv(0.0, -1.0)), + (pt3(0.0, 0.0, 0.0), vec3(1.0, 0.0, 0.0), uv(0.0, 0.0)), + (pt3(0.0, 2.0, 0.0), vec3(0.0, 0.0, 1.0), uv(0.0, 0.0)) + ] + ] + ); } #[test] fn empty_input() { - let mesh = parse_obj(*b"") - .expect("empty input should be valid") + let mesh: Mesh<()> = parse_obj(*b"") + .unwrap_or_else(|e| { + panic!("empty input should be valid, got {e:?}") + }) .build(); assert!(mesh.faces.is_empty()); assert!(mesh.verts.is_empty()); @@ -402,7 +611,7 @@ v 0.0 -2.0 0.0 #[test] fn input_only_whitespace() { - let mesh = parse_obj(*b" \n \n\n ") + let mesh: Mesh<()> = parse_obj(*b" \n \n\n ") .expect("white-space only input should be valid") .build(); assert!(mesh.faces.is_empty()); @@ -411,7 +620,7 @@ v 0.0 -2.0 0.0 #[test] fn input_only_comments() { - let mesh = parse_obj(*b"# comment\n #another comment") + let mesh: Mesh<()> = parse_obj(*b"# comment\n #another comment") .expect("comment-only input should be valid") .build(); assert!(mesh.faces.is_empty()); @@ -420,9 +629,9 @@ v 0.0 -2.0 0.0 #[test] fn unknown_item() { - let result = parse_obj(*b"f 1 2 3\nxyz 4 5 6"); + let result = parse_obj::<()>(*b"f 1 2 3\nxyz 4 5 6"); assert!( - matches!(result, Err(UnsupportedItem('x'))), + matches!(&result, Err(UnsupportedItem(item)) if item == b"xyz"), "actual was: {result:?}" ); } @@ -430,7 +639,7 @@ v 0.0 -2.0 0.0 #[test] fn vertex_index_oob() { let input = *b"f 1 2 3\nv 0.0 0.0 0.0\nv 1.0 1.0 1.0"; - let result = parse_obj(input); + let result = parse_obj::<()>(input); assert!( matches!(result, Err(IndexOutOfBounds("vertex", 2))), "actual was: {result:?}", @@ -439,7 +648,7 @@ v 0.0 -2.0 0.0 #[test] fn texcoord_index_oob() { let input = *b"f 1/1 1/4 1/2\nv 0.0 0.0 0.0\nvt 0.0 0.0\nvt 0.0 1.0"; - let result = parse_obj(input); + let result = parse_obj::<()>(input); assert!( matches!(result, Err(IndexOutOfBounds("texcoord", 3))), "actual was: {result:?}", @@ -449,7 +658,7 @@ v 0.0 -2.0 0.0 #[test] fn unexpected_end_of_input() { let input = *b"f"; - let result = parse_obj(input); + let result = parse_obj::<()>(input); assert!(matches!(result, Err(UnexpectedEnd))); } @@ -463,7 +672,7 @@ v 0.0 -2.0 0.0 fn read(&mut self, buf: &mut [u8]) -> std::io::Result { if self.0 { self.0 = false; - buf.copy_from_slice(b"t"); + buf[..1].copy_from_slice(b"t"); Ok(7) } else { Err(ErrorKind::BrokenPipe.into()) diff --git a/geom/src/solids.rs b/geom/src/solids.rs index 038a0c36..0ae2f1a5 100644 --- a/geom/src/solids.rs +++ b/geom/src/solids.rs @@ -4,7 +4,17 @@ mod lathe; mod platonic; +use re::geom::{Mesh, mesh::Builder}; + #[cfg(feature = "std")] pub use lathe::*; pub use platonic::*; + +pub trait Build: Sized { + fn build(self) -> Mesh; + + fn builder(self) -> Builder { + self.build().into_builder() + } +} diff --git a/geom/src/solids/lathe.rs b/geom/src/solids/lathe.rs index 84a9c56c..8e7cd94e 100644 --- a/geom/src/solids/lathe.rs +++ b/geom/src/solids/lathe.rs @@ -3,10 +3,17 @@ use alloc::vec::Vec; use core::ops::Range; -use re::geom::{Mesh, Normal2, Normal3, Polyline, Vertex, Vertex2, vertex}; +use re::geom::{ + Mesh, Normal2, Normal3, Polyline, Tri, Vertex, Vertex2, Vertex3, tri, + vertex, +}; use re::math::{ - Angle, Lerp, Parametric, Vary, Vec3, polar, pt2, rotate_y, turns, vec2, + Angle, Apply, Lerp, Parametric, Point3, Vary, Vec3, polar, pt2, rotate_y, + turns, vec2, }; +use re::render::{TexCoord, uv}; + +use super::Build; /// A surface of revolution generated by rotating a 2D curve around the y-axis. #[derive(Clone, Debug, Default)] @@ -87,107 +94,206 @@ impl>> Lathe

{ } /// Builds the lathe mesh. - pub fn build(self) -> Mesh { + pub fn build_with(self, mut f: F) -> Mesh + where + F: FnMut(Point3, Normal3, TexCoord) -> Vertex3, + { let secs = self.sectors as usize; let segs = self.segments as usize; + let caps = 2 * self.capped as usize; // Fencepost problem: n + 1 vertices for n segments let verts_per_sec = segs + 1; // Precompute capacity - let caps = 2 * self.capped as usize; let n_faces = segs * secs * 2 + (secs - 2) * caps; let n_verts = verts_per_sec * (secs + 1) + secs * caps; + let mut m = + Mesh::new(Vec::with_capacity(n_faces), Vec::with_capacity(n_verts)); + + create_faces(secs, verts_per_sec, &mut m.faces); + create_verts( + &mut f, + &self.points, + secs, + verts_per_sec, + self.az_range, + &mut m.verts, + ); - let mut b = - Mesh::new(Vec::with_capacity(n_faces), Vec::with_capacity(n_verts)) - .into_builder(); - - let Range { start, end } = self.az_range; - let rot = rotate_y((end - start) / secs as f32); - let start = rotate_y(start); - - // Create vertices - for Vertex { pos, attrib: n } in 0.0 - .vary_to(1.0, verts_per_sec as u32) - .map(|t| self.points.eval(t)) - { - let mut pos = start.apply_pt(&pos.to_pt3()); - let mut norm = start.apply(&n.to_vec3()); - - for _ in 0..=secs { - b.push_vert(pos, norm); - pos = rot.apply_pt(&pos); - norm = rot.apply(&norm); - } - } - // Create faces - for j in 1..verts_per_sec { - let n = secs + 1; - for i in 1..n { - let p = (j - 1) * n + i - 1; - let q = (j - 1) * n + i; - let r = j * n + i - 1; - let s = j * n + i; - b.push_face(p, s, q); - b.push_face(p, r, s); - } - } // Create optional caps if self.capped && verts_per_sec > 0 { - let l = b.mesh.verts.len(); - - let mut make_cap = |rg: Range<_>, n| { - let l = b.mesh.verts.len(); - let vs: Vec<_> = b.mesh.verts[rg] - .iter() - .map(|v| vertex(v.pos, n)) - .collect(); - b.mesh.verts.extend(vs); - let j = (n.y() < 0.0) as usize; - for i in 1..secs - 1 { - // Adjust winding depending on whether top or bottom - b.push_face(l, l + i + (1 - j), l + i + j); - } - }; + let l = m.verts.len(); // Duplicate the bottom ring of vertices to make the bottom cap... - make_cap(0..secs, -Vec3::Y); + make_cap(&mut m, &mut f, 0..secs, -Vec3::Y); // ...and the top vertices to make the top cap - make_cap(l - secs..l, Vec3::Y); + make_cap(&mut m, &mut f, l - secs..l, Vec3::Y); } - b.build() + m + } +} + +#[inline(never)] +fn create_faces(secs: usize, verts_per_sec: usize, out: &mut Vec>) { + for j in 1..verts_per_sec { + let n = secs + 1; + for i in 1..n { + let p = (j - 1) * n + i - 1; + let q = (j - 1) * n + i; + let r = j * n + i - 1; + let s = j * n + i; + // TODO could alternate direction of diagonal + // - or support quads + // _____ _____ + // |/|/| -> |\|/| + // |/|/| |/|\| + out.push(tri(p, s, q)); + out.push(tri(p, r, s)); + } + } +} + +#[inline(never)] +fn create_verts( + f: &mut dyn FnMut(Point3, Normal3, TexCoord) -> Vertex3, + pts: &dyn Parametric>, + secs: usize, + verts_per_sec: usize, + az_range: Range, + out: &mut Vec>, +) { + let Range { start, end } = az_range; + let rot = rotate_y((end - start) / secs as f32); + let start = rotate_y(start); + + // Create vertices + for (v, Vertex { pos, attrib: n }) in 0.0 + .vary_to(1.0, verts_per_sec as u32) + .map(|t| (t, pts.eval(t))) + { + let mut pos = start.apply(&pos.to_pt3()); + let mut norm = start.apply(&n.to_vec3()); + + for u in 0..=secs { + let v = f(pos.to(), norm, uv(u as f32 / secs as f32, v)); + out.push(v); + + pos = rot.apply(&pos); + norm = rot.apply(&norm); + } + } +} + +fn make_cap(m: &mut Mesh, f: &mut F, rg: Range, n: Normal3) +where + F: FnMut(Point3, Normal3, TexCoord) -> Vertex3, +{ + let verts = &mut m.verts; + let secs = rg.len(); + let l = verts.len(); + + verts.reserve(secs); + for i in rg { + let p = verts[i].pos.to(); + let uv = uv(0.0, 0.0); // TODO + verts.push(f(p, n, uv)); + } + + // Adjust winding depending on whether top or bottom + let (j, k) = if n.y() < 0.0 { (0, 1) } else { (1, 0) }; + for i in 1..secs - 1 { + m.faces.push(tri(l, l + i + j, l + i + k)); + } +} + +// +// Local trait impls +// + +impl>> Build for Lathe

{ + fn build(self) -> Mesh { + self.build_with(|p, n, _| vertex(p.to(), n)) + } +} +impl>> Build for Lathe

{ + fn build(self) -> Mesh { + self.build_with(|p, _, tc| vertex(p.to(), tc)) + } +} +impl>> Build<(Normal3, TexCoord)> + for Lathe

+{ + fn build(self) -> Mesh<(Normal3, TexCoord)> { + self.build_with(|p, n, tc| vertex(p.to(), (n, tc))) } } impl Sphere { - /// Builds the spherical mesh. - pub fn build(self) -> Mesh { + fn lathe(self) -> Lathe>> { let Self { sectors, segments, radius } = self; - - let pts = |t| { + let pts = move |t| { let a = (-0.25).lerp(&0.25, t); - let v = polar(radius, turns(a)).to_cart(); - vertex(v.to_pt(), v.normalize()) + let n = polar(1.0, turns(a)).to_cart(); + vertex((n * radius).to_pt(), n) }; - Lathe::new(pts, sectors, segments).build() + Lathe::new(pts, sectors, segments) + } +} + +impl Build for Sphere { + /// Builds a spherical mesh with normals. + fn build(self) -> Mesh { + self.lathe().build() + } +} +impl Build for Sphere { + /// Builds a spherical mesh with texture coordinates. + fn build(self) -> Mesh { + self.lathe().build() } } impl Torus { - /// Builds the toroidal mesh. - pub fn build(self) -> Mesh { - let pts = |t| { + fn lathe(self) -> Lathe>> { + let pts = move |t| { let a = 0.0.lerp(&1.0, t); let v = polar(self.minor_radius, turns(a)).to_cart(); - vertex(pt2(self.major_radius, 0.0) + v, v.normalize()) + vertex(pt2(self.major_radius, 0.0) + v.to(), v.normalize()) }; - Lathe::new(pts, self.major_sectors, self.minor_sectors).build() + Lathe::new(pts, self.major_sectors, self.minor_sectors) + } +} +impl Build for Torus { + /// Builds the toroidal mesh. + fn build(self) -> Mesh { + self.lathe().build() + } +} +impl Build for Torus { + /// Builds the toroidal mesh. + fn build(self) -> Mesh { + self.lathe().build() } } -impl Cylinder { +impl Build for Cylinder { + /// Builds the cylindrical mesh. + fn build(self) -> Mesh { + #[rustfmt::skip] + let Self { sectors, segments, capped, radius } = self; + Cone { + sectors, + segments, + capped, + base_radius: radius, + apex_radius: radius, + } + .build() + } +} +impl Build for Cylinder { /// Builds the cylindrical mesh. - pub fn build(self) -> Mesh { + fn build(self) -> Mesh { #[rustfmt::skip] let Self { sectors, segments, capped, radius } = self; Cone { @@ -202,29 +308,35 @@ impl Cylinder { } impl Cone { - /// Builds the conical mesh. - pub fn build(self) -> Mesh { + fn lathe(self) -> Lathe>> { assert!(self.segments > 0, "segments cannot be zero"); let base_pt = pt2(self.base_radius, -1.0); let apex_pt = pt2(self.apex_radius, 1.0); + let n = (base_pt - apex_pt).perp().normalize(); - let n = apex_pt - base_pt; - let n = vec2(n.y(), -n.x()).normalize(); - - let pts = |t| { + let pts = move |t| { let pt = base_pt.lerp(&apex_pt, t); vertex(pt, n) }; - Lathe::new(pts, self.sectors, self.segments) - .capped(self.capped) - .build() + Lathe::new(pts, self.sectors, self.segments).capped(self.capped) + } +} +impl Build for Cone { + /// Builds the conical mesh. + fn build(self) -> Mesh { + self.lathe().build() + } +} +impl Build for Cone { + /// Builds the conical mesh. + fn build(self) -> Mesh { + self.lathe().build() } } impl Capsule { - /// Builds the capsule mesh. - pub fn build(self) -> Mesh { + fn lathe(self) -> Lathe>> { #[rustfmt::skip] let Self { sectors, body_segments, cap_segments, radius } = self; assert!(body_segments > 0, "body segments cannot be zero"); @@ -249,14 +361,28 @@ impl Capsule { .vary_to(1.0, body_segments + 1) .map(|t| vertex(pt2(radius, t), vec2(1.0, 0.0))); - let pts = bottom_pts + let pts: Vec<_> = bottom_pts .iter() .copied() .chain(body_pts) - .chain(top_pts); + .chain(top_pts) + .collect(); let segments = 2 * cap_segments + body_segments; - Lathe::new(Polyline::new(pts), sectors, segments).build() + Lathe::new(Polyline::new(pts), sectors, segments) + } +} + +impl Build for Capsule { + /// Builds the capsule mesh. + fn build(self) -> Mesh { + self.lathe().build() + } +} +impl Build for Capsule { + /// Builds the capsule mesh. + fn build(self) -> Mesh { + self.lathe().build() } } @@ -264,11 +390,13 @@ impl Capsule { mod tests { use super::*; + type Mesh = super::Mesh; + #[test] fn sphere_verts_faces() { let sectors = 4; let segments = 3; - let s = Sphere { sectors, segments, radius: 1.0 }.build(); + let s: Mesh = Sphere { sectors, segments, radius: 1.0 }.build(); assert_eq!(s.faces.len() as u32, 2 * sectors * segments); assert_eq!(s.faces.len(), s.faces.capacity()); @@ -281,7 +409,7 @@ mod tests { fn cylinder_verts_faces_capped() { let sectors = 4; let segments = 3; - let c = Cylinder { + let c: Mesh = Cylinder { sectors, segments, capped: true, @@ -290,8 +418,8 @@ mod tests { .build(); let faces_expected = 2 * sectors * segments + 2 * (sectors - 2); - assert_eq!(c.faces.len(), c.faces.capacity()); assert_eq!(c.faces.len() as u32, faces_expected); + assert_eq!(c.faces.len(), c.faces.capacity()); let verts_expected = (sectors + 1) * (segments + 1) + 2 * sectors; assert_eq!(c.verts.len() as u32, verts_expected); @@ -302,7 +430,7 @@ mod tests { fn cylinder_verts_faces_uncapped() { let sectors = 4; let segments = 3; - let c = Cylinder { + let c: Mesh = Cylinder { sectors, segments, capped: false, @@ -311,8 +439,8 @@ mod tests { .build(); let faces_expected = 2 * sectors * segments; - assert_eq!(c.faces.len(), c.faces.capacity()); assert_eq!(c.faces.len() as u32, faces_expected); + assert_eq!(c.faces.len(), c.faces.capacity()); let verts_expected = (sectors + 1) * (segments + 1); assert_eq!(c.verts.len() as u32, verts_expected); @@ -324,7 +452,7 @@ mod tests { let sectors = 4; let body_segments = 2; let cap_segments = 2; - let c = Capsule { + let c: Mesh = Capsule { sectors, body_segments, cap_segments, @@ -334,8 +462,8 @@ mod tests { let faces_expected = 2 * sectors * body_segments + 2 * 2 * sectors * cap_segments; - assert_eq!(c.faces.len(), c.faces.capacity()); assert_eq!(c.faces.len() as u32, faces_expected); + assert_eq!(c.faces.len(), c.faces.capacity()); let verts_expected = (sectors + 1) * (body_segments + 1) + 2 * (sectors + 1) * (cap_segments); @@ -347,7 +475,7 @@ mod tests { fn torus_verts_faces_capped() { let major_sectors = 6; let minor_sectors = 4; - let t = Torus { + let t: Mesh = Torus { major_radius: 1.0, minor_radius: 0.2, major_sectors, @@ -356,8 +484,8 @@ mod tests { .build(); let faces_expected = 2 * major_sectors * minor_sectors; - assert_eq!(t.faces.len(), t.faces.capacity()); assert_eq!(t.faces.len() as u32, faces_expected); + assert_eq!(t.faces.len(), t.faces.capacity()); let verts_expected = (major_sectors + 1) * (minor_sectors + 1); assert_eq!(t.verts.len() as u32, verts_expected); diff --git a/geom/src/solids/platonic.rs b/geom/src/solids/platonic.rs index 34a8eb33..89abcba8 100644 --- a/geom/src/solids/platonic.rs +++ b/geom/src/solids/platonic.rs @@ -1,11 +1,13 @@ //! The five Platonic solids: tetrahedron, cube, octahedron, dodecahedron, //! and icosahedron. -use core::array::from_fn; +use core::{array::from_fn, f32::consts::SQRT_2, iter::zip}; -use re::geom::{Mesh, Normal3}; +use re::geom::{Mesh, Normal3, Vertex3, vertex}; use re::math::{Lerp, Point3, Vec3, pt3, vec3}; -use re::render::{TexCoord, uv}; +use re::render::{Model, TexCoord, uv}; + +use super::Build; /// A regular tetrahedron. /// @@ -14,9 +16,9 @@ use re::render::{TexCoord, uv}; /// /// `Tetrahedron`'s vertices are at: /// * (0, 1, 0), -/// * (√(8/9), -1/3, 0), -/// * (-√(2/9), -1/3, √(2/3)), and -/// * (-√(8/9), -1/3, -√(2/3)). +/// * (2√(2)/3, -1/3, 0), +/// * (-√(2)/3, -1/3, √(2/3)), and +/// * (-√(2)/3, -1/3, -√(2/3)). #[derive(Copy, Clone, Debug)] pub struct Tetrahedron; @@ -93,28 +95,33 @@ pub struct Dodecahedron; #[derive(Copy, Clone, Debug, Default)] pub struct Icosahedron; -// TODO use consts instead of sqrt -#[cfg(feature = "std")] +const SQRT_3: f32 = 1.7320508_f32; + impl Tetrahedron { const FACES: [[usize; 3]; 4] = [[0, 2, 1], [0, 3, 2], [0, 1, 3], [1, 2, 3]]; + const COORDS: [Vec3; 4] = [ + vec3(0.0, 1.0, 0.0), + vec3(SQRT_2 * 2.0 / 3.0, -1.0 / 3.0, 0.0), + vec3(-SQRT_2 / 3.0, -1.0 / 3.0, SQRT_2 / SQRT_3), + vec3(-SQRT_2 / 3.0, -1.0 / 3.0, -SQRT_2 / SQRT_3), + ]; + + const NORMS: [Vec3; 4] = [ + Self::COORDS[3], + Self::COORDS[1], + Self::COORDS[2], + Self::COORDS[0], + ]; + /// Builds the tetrahedral mesh. pub fn build(self) -> Mesh { - let sqrt = f32::sqrt; - let coords = [ - pt3(0.0, 1.0, 0.0), - pt3(sqrt(8.0 / 9.0), -1.0 / 3.0, 0.0), - pt3(-sqrt(2.0 / 9.0), -1.0 / 3.0, sqrt(2.0 / 3.0)), - pt3(-sqrt(2.0 / 9.0), -1.0 / 3.0, -sqrt(2.0 / 3.0)), - ]; - let norms = [3, 1, 2, 0].map(|i| -coords[i].to_vec()); - let mut b = Mesh::builder(); - - for (i, vs) in Self::FACES.into_iter().enumerate() { - b.push_face(3 * i, 3 * i + 1, 3 * i + 2); + for (vs, i) in zip(Self::FACES, 0..) { + b.push_face(i * 3, i * 3 + 1, i * 3 + 2); + let n = -Self::NORMS[i]; // already unit length for v in vs { - b.push_vert(coords[v], norms[i]); + b.push_vert(Self::COORDS[v].to_pt(), n); } } b.build() @@ -142,7 +149,6 @@ impl Box { vec3(0.0, 0.0, -1.0), vec3(0.0, 0.0, 1.0), ]; - #[allow(unused)] const TEX_COORDS: [TexCoord; 4] = [uv(0.0, 0.0), uv(1.0, 0.0), uv(0.0, 1.0), uv(1.0, 1.0)]; #[rustfmt::skip] @@ -182,33 +188,61 @@ impl Box { } /// Builds the cuboid mesh. - pub fn build(self) -> Mesh { + pub fn build_with( + self, + mut f: impl FnMut(Point3, Normal3, TexCoord) -> Vertex3, + ) -> Mesh { let mut b = Mesh::builder(); b.push_faces(Self::FACES); - for (pos_i, [norm_i, _uv_i]) in Self::VERTS { + for (pos_i, [norm_i, uv_i]) in Self::VERTS { let pos = from_fn(|i| { self.left_bot_near.0[i] .lerp(&self.right_top_far.0[i], Self::COORDS[pos_i][i]) }); - b.push_vert(pos.into(), Self::NORMS[norm_i]); + b.mesh.verts.push(f( + pos.into(), + Self::NORMS[norm_i], + Self::TEX_COORDS[uv_i], + )); } b.build() } } -impl Cube { +impl Build for Box { + fn build(self) -> Mesh { + self.build_with(|p, n, _| vertex(p, n)) + } +} +impl Build for Box { + fn build(self) -> Mesh { + self.build_with(|p, _, uv| vertex(p, uv)) + } +} +impl Build<(Normal3, TexCoord)> for Box { + fn build(self) -> Mesh<(Normal3, TexCoord)> { + self.build_with(|p, n, uv| vertex(p, (n, uv))) + } +} + +impl Build for Cube +where + Box: Build, +{ /// Builds the cube mesh. - pub fn build(self) -> Mesh { - let l = self.side_len / 2.0; + fn build(self) -> Mesh { + let dim = self.side_len / 2.0; Box { - left_bot_near: pt3(-l, -l, -l), - right_top_far: pt3(l, l, l), + left_bot_near: pt3(-dim, -dim, -dim), + right_top_far: pt3(dim, dim, dim), } .build() } } impl Octahedron { + // TODO Could reuse Box norms and coords, but they are in a different + // order and the coords need mapping to be valid normals const COORDS: [Point3; 6] = [ pt3(-1.0, 0.0, 0.0), pt3(0.0, -1.0, 0.0), @@ -248,15 +282,18 @@ impl Octahedron { [18, 19, 20], [21, 22, 23], ]; +} +impl Build for Octahedron { /// Builds the octahedral mesh. - pub fn build(self) -> Mesh { + fn build(self) -> Mesh { let mut b = Mesh::builder(); - for (i, vs) in Self::FACES.iter().enumerate() { - b.push_face(3 * i, 3 * i + 1, 3 * i + 2); - for vi in *vs { + for (vs, i) in zip(&Self::FACES, 0..) { + b.push_face(i * 3, i * 3 + 1, i * 3 + 2); + for &vi in vs { let pos = Self::COORDS[Self::VERTS[vi].0]; - b.push_vert(pos, Self::NORMS[i]); + let n = Self::NORMS[i].normalize(); + b.push_vert(pos, n); } } b.build() @@ -272,23 +309,17 @@ impl Dodecahedron { #[rustfmt::skip] const COORDS: [Vec3; 20] = [ // -X - vec3(-PHI, -R_PHI, 0.0), - vec3(-PHI, R_PHI, 0.0), + vec3(-PHI, -R_PHI, 0.0), vec3(-PHI, R_PHI, 0.0), // +X - vec3( PHI, -R_PHI, 0.0), - vec3( PHI, R_PHI, 0.0), + vec3( PHI, -R_PHI, 0.0), vec3( PHI, R_PHI, 0.0), // -Y - vec3(0.0, -PHI, -R_PHI), - vec3(0.0, -PHI, R_PHI), + vec3(0.0, -PHI, -R_PHI), vec3(0.0, -PHI, R_PHI), // +Y - vec3(0.0, PHI, -R_PHI), - vec3(0.0, PHI, R_PHI), + vec3(0.0, PHI, -R_PHI), vec3(0.0, PHI, R_PHI), // -Z - vec3(-R_PHI, 0.0, -PHI), - vec3( R_PHI, 0.0, -PHI), + vec3(-R_PHI, 0.0, -PHI), vec3( R_PHI, 0.0, -PHI), // +Z - vec3(-R_PHI, 0.0, PHI), - vec3( R_PHI, 0.0, PHI), + vec3(-R_PHI, 0.0, PHI), vec3( R_PHI, 0.0, PHI), // Corner verts, corresponding to the corner faces of the icosahedron. vec3(-1.0, -1.0, -1.0), @@ -313,20 +344,23 @@ impl Dodecahedron { /// The normals are exactly the vertices of the icosahedron, normalized. const NORMALS: [Vec3; 12] = Icosahedron::COORDS; +} +impl Build for Dodecahedron { /// Builds the dodecahedral mesh. - pub fn build(self) -> Mesh { + fn build(self) -> Mesh { let mut b = Mesh::builder(); - for (i, face) in Self::FACES.iter().enumerate() { + for (face, i) in zip(&Self::FACES, 0..) { let n = Self::NORMALS[i].normalize(); // Make a pentagon from three triangles - let i5 = 5 * i; + let i5 = i * 5; b.push_face(i5, i5 + 1, i5 + 2); b.push_face(i5, i5 + 2, i5 + 3); b.push_face(i5, i5 + 3, i5 + 4); for &j in face { - b.push_vert(Self::COORDS[j].normalize().to_pt(), n); + let pos = Self::COORDS[j].normalize().to_pt(); + b.push_vert(pos, n); } } b.build() @@ -363,15 +397,18 @@ impl Icosahedron { /// The normals are exactly the vertices of the dodecahedron, normalized. const NORMALS: [Vec3; 20] = Dodecahedron::COORDS; +} +impl Build for Icosahedron { /// Builds the icosahedral mesh. - pub fn build(self) -> Mesh { + fn build(self) -> Mesh { let mut b = Mesh::builder(); - for (i, vs) in Self::FACES.iter().enumerate() { + for (vs, i) in zip(&Self::FACES, 0..) { let n = Self::NORMALS[i].normalize(); - b.push_face(3 * i, 3 * i + 1, 3 * i + 2); - for vi in *vs { - b.push_vert(Self::COORDS[vi].normalize().to_pt(), n); + b.push_face(i * 3, i * 3 + 1, i * 3 + 2); + for &vi in vs { + let pos = Self::COORDS[vi].normalize().to_pt(); + b.push_vert(pos, n); } } b.build()