diff --git a/Cargo.toml b/Cargo.toml index 7aa205de..b6dfea4b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,27 +33,37 @@ resolver = "2" [workspace.package] edition = "2024" -version = "0.4.0-pre4" +version = "0.4.0" 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" -collapsible_if = "allow" +[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-pre4", path = "core" } -retrofire-front = { version = "0.4.0-pre4", path = "front" } -retrofire-geom = { version = "0.4.0-pre4", path = "geom" } +retrofire-core = { version = "0.4.0", path = "core" } +retrofire-front = { version = "0.4.0", path = "front" } +retrofire-geom = { version = "0.4.0", path = "geom" } [profile.release] opt-level = 2 +codegen-units = 1 lto = "thin" debug = 1 +[profile.bench] +opt-level = 3 +codegen-units = 1 +lto = "fat" + [profile.dev] opt-level = 1 split-debuginfo = "unpacked" diff --git a/README.md b/README.md index 20e7e4fe..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 @@ -88,10 +89,11 @@ for custom allocators is planned in order to make `alloc` optional as well. 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 @@ -103,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 `geom` package has no external dependencies. It only requires `alloc`; -activating the optional feature `std` enables APIs that do I/O. +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-front`. +The `retrofire-demos` package depends on `retrofire`. + +# Screenshots + +The classic Stanford bunny. +![The classic Stanford bunny 3D model.](docs/bunny.jpg) + +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) + +Ten thousand spherical particles positioned randomly in a sphere. +![Ten thousand spherical particles in random positions.](docs/sprites.jpg) # License 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 index 1aa88264..d58d2639 100644 --- a/core/examples/hello_tri.rs +++ b/core/examples/hello_tri.rs @@ -1,4 +1,3 @@ -use retrofire_core::geom::tri; use retrofire_core::{prelude::*, util::*}; fn main() { @@ -10,7 +9,7 @@ fn main() { #[cfg(feature = "fp")] let shader = shader::new( - |v: Vertex3, mvp: &Mat4x4| { + |v: Vertex3, mvp: &ProjMat3| { // Transform vertex position from model to projection space // Interpolate vertex colors in linear color space vertex(mvp.apply(&v.pos), v.attrib.to_linear()) @@ -19,7 +18,7 @@ fn main() { ); #[cfg(not(feature = "fp"))] let shader = shader::new( - |v: Vertex3, mvp: &Mat4x4| { + |v: Vertex3, mvp: &ProjMat3| { // Transform vertex position from model to projection space // Interpolate vertex colors in normal sRGB color space vertex(mvp.apply(&v.pos), v.attrib) diff --git a/core/src/geom.rs b/core/src/geom.rs index 915e12bf..98e982cb 100644 --- a/core/src/geom.rs +++ b/core/src/geom.rs @@ -4,9 +4,10 @@ use alloc::vec::Vec; use core::fmt::Debug; use crate::math::{ - Affine, Lerp, Linear, Mat4x4, Parametric, Point2, Point3, Vec2, Vec3, - Vector, mat::RealToReal, space::Real, vec2, vec3, + Affine, Lerp, Linear, Mat4, Parametric, Point2, Point3, Vec2, Vec3, Vector, + space::Real, vec2, vec3, }; + use crate::render::Model; pub use mesh::Mesh; @@ -517,7 +518,7 @@ impl Plane3 { /// /// assert_approx_eq!(m.apply(&Point3::origin()), pt3(0.0, 0.5, 0.5)); /// ``` - pub fn basis(&self) -> Mat4x4> { + pub fn basis(&self) -> Mat4 { let up = self.abc(); let right: Vec3 = @@ -531,7 +532,7 @@ impl Plane3 { let origin = self.offset() * up; - Mat4x4::from_affine(right, up, fwd, origin.to_pt()) + Mat4::from_affine(right, up, fwd, origin.to_pt()) } /// Helper that returns the plane normal non-normalized. @@ -864,8 +865,8 @@ mod tests { #[test] fn singleton_polyline_eval() { - let pl = Polyline(vec![3.14]); - assert_eq!(pl.eval(0.0), 3.14); - assert_eq!(pl.eval(1.0), 3.14); + let pl = Polyline(vec![1.23]); + assert_eq!(pl.eval(0.0), 1.23); + assert_eq!(pl.eval(1.0), 1.23); } } diff --git a/core/src/geom/mesh.rs b/core/src/geom/mesh.rs index daae554b..44c1f1e3 100644 --- a/core/src/geom/mesh.rs +++ b/core/src/geom/mesh.rs @@ -7,7 +7,7 @@ use core::{ }; use crate::{ - math::{Apply, Linear, Mat4x4, Point3, mat::RealToReal}, + math::{Linear, Mat4, Point3}, render::Model, }; @@ -79,12 +79,7 @@ impl Mesh { let faces: Vec<_> = faces.into_iter().collect(); let verts: Vec<_> = verts.into_iter().collect(); - for (i, Tri(vs)) in faces.iter().enumerate() { - assert!( - vs.iter().all(|&j| j < verts.len()), - "vertex index out of bounds at faces[{i}]: {vs:?}" - ) - } + assert_indices_in_bounds(&faces, verts.len()); Self { faces, verts } } @@ -109,6 +104,16 @@ impl Mesh { } } +#[inline(never)] +fn assert_indices_in_bounds(faces: &[Tri], len: usize) { + for (Tri(vs), i) in zip(faces.iter(), 0..) { + assert!( + vs.iter().all(|&j| j < len), + "vertex index out of bounds at faces[{i}]: {vs:?}" + ) + } +} + impl Mesh { /// Returns a new mesh builder. pub fn builder() -> Builder { @@ -173,7 +178,7 @@ impl Builder { /// /// This is an eager operation, that is, only vertices *currently* /// added to the builder are transformed. - pub fn transform(self, tf: &Mat4x4>) -> Self { + pub fn transform(self, tf: &Mat4) -> Self { self.warp(|v| vertex(tf.apply(&v.pos), v.attrib)) } diff --git a/core/src/lib.rs b/core/src/lib.rs index 5ef734b1..d3cc55fe 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -62,7 +62,7 @@ pub mod prelude { Mesh, Normal2, Normal3, Tri, Vertex, Vertex2, Vertex3, tri, vertex, }, math::*, - render::{raster::Frag, *}, + render::*, util::buf::{AsMutSlice2, AsSlice2, Buf2, MutSlice2, Slice2}, }; } diff --git a/core/src/math.rs b/core/src/math.rs index eb8211f4..e8800194 100644 --- a/core/src/math.rs +++ b/core/src/math.rs @@ -24,7 +24,7 @@ pub use { approx::ApproxEq, color::{Color, Color3, Color3f, Color4, Color4f, rgb, rgba}, mat::{ - Apply, Mat2x2, Mat3x3, Mat4x4, Matrix, orthographic, perspective, + Apply, Mat2, Mat3, Mat4, Matrix, ProjMat3, orthographic, perspective, scale, scale3, translate, translate3, viewport, }, param::Parametric, @@ -32,7 +32,7 @@ pub use { space::{Affine, Linear}, spline::{BezierSpline, CubicBezier, smootherstep, smoothstep}, vary::Vary, - vec::{Vec2, Vec2i, Vec3, Vec3i, Vector, splat, vec2, vec3}, + vec::{ProjVec3, Vec2, Vec2i, Vec3, Vec3i, Vector, splat, vec2, vec3}, }; #[cfg(feature = "fp")] pub use { @@ -177,6 +177,7 @@ where /// let p1 = pt2(-5.0, 0.0); /// assert_eq!(p0.lerp(&p1, 0.4),pt2(-8.0, 3.0)); /// ``` + #[inline] fn lerp(&self, other: &Self, t: f32) -> Self { self.add(&other.sub(self).mul(t)) } @@ -187,6 +188,7 @@ impl Lerp for () { } impl Lerp for (U, V) { + #[inline] fn lerp(&self, (u, v): &Self, t: f32) -> Self { (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 3dc2a1d0..58cd5ca0 100644 --- a/core/src/math/angle.rs +++ b/core/src/math/angle.rs @@ -45,17 +45,17 @@ pub type SphericalVec = Vector<[f32; 3], Spherical>; // /// Returns an angle of `a` radians. -pub fn rads(a: f32) -> Angle { +pub const fn rads(a: f32) -> Angle { Angle(a) } /// Returns an angle of `a` degrees. -pub fn degs(a: f32) -> Angle { +pub const fn degs(a: f32) -> Angle { Angle(a * RADS_PER_DEG) } /// Returns an angle of `a` turns. -pub fn turns(a: f32) -> Angle { +pub const fn turns(a: f32) -> Angle { Angle(a * RADS_PER_TURN) } @@ -546,6 +546,14 @@ impl Rem for Angle { } } +impl Mul for f32 { + type Output = Angle; + + fn mul(self, rhs: Angle) -> Self::Output { + rhs * self + } +} + #[cfg(feature = "fp")] impl From> for Vec2 { /// Converts a polar vector into the equivalent Cartesian vector. diff --git a/core/src/math/color.rs b/core/src/math/color.rs index d2ec147e..a214316f 100644 --- a/core/src/math/color.rs +++ b/core/src/math/color.rs @@ -1,12 +1,11 @@ //! Colors and color spaces. -use core::ops::{ - Add, AddAssign, Div, DivAssign, Index, Mul, MulAssign, Neg, Sub, SubAssign, -}; use core::{ array, fmt::{self, Debug, Display, Formatter}, marker::PhantomData, + ops::{Add, Div, Index, IndexMut, Mul, Neg, Sub}, + ops::{AddAssign, DivAssign, MulAssign, SubAssign}, }; use super::{Affine, Linear, Vector, vary::ZDiv}; @@ -65,26 +64,33 @@ pub type Color3f = Color<[f32; 3], Space>; /// outside the range can be useful as intermediate results in calculations. pub type Color4f = Color<[f32; 4], Space>; -/// Returns a new RGB color with the given color channels. +/// Returns a new RGB color with the given channels. +#[inline] pub const fn rgb(r: Ch, g: Ch, b: Ch) -> Color<[Ch; 3], Rgb> { - Color([r, g, b], PhantomData) + Color::new([r, g, b]) } -/// Returns a new RGBA color with the given color channels. +/// Returns a new RGBA color with the given channels. +#[inline] pub const fn rgba(r: Ch, g: Ch, b: Ch, a: Ch) -> Color<[Ch; 4], Rgba> { - Color([r, g, b, a], PhantomData) + Color::new([r, g, b, a]) } /// Returns a new RGB color with all channels set to the same value. +/// +/// This is the color equivalent to [`splat`][super::vec::splat]. +#[inline] pub const fn gray(lum: Ch) -> Color<[Ch; 3], Rgb> { - rgb(lum, lum, lum) + Color::new([lum; 3]) } -/// Returns a new HSL color with the given color channels. +/// Returns a new HSL color with the given channels. +#[inline] pub const fn hsl(h: Ch, s: Ch, l: Ch) -> Color<[Ch; 3], Hsl> { - Color([h, s, l], PhantomData) + Color::new([h, s, l]) } -/// Returns a new HSLA color with the given color channels. +/// Returns a new HSLA color with the given channels. +#[inline] pub const fn hsla(h: Ch, s: Ch, l: Ch, a: Ch) -> Color<[Ch; 4], Hsla> { - Color([h, s, l, a], PhantomData) + Color::new([h, s, l, a]) } /// Exponent for gamma conversion [from sRGB to linear sRGB][1]. @@ -100,6 +106,23 @@ pub const INV_GAMMA: f32 = 1.0 / GAMMA; // Inherent impls // +impl Color<[Ch; N], Sp> { + /// Returns a new `Color` with the given channels. + #[inline] + pub const fn new(chs: [Ch; N]) -> Self { + Self(chs, PhantomData) + } + + /// Returns `self` with each channel mapped with the given function. + #[inline] + pub fn map(&self, f: impl FnMut(Ch) -> C) -> Color<[C; N], Sp> + where + Ch: Copy, + { + Color::new(self.0.map(f)) + } +} + impl Color3 { /// Returns `self` as RGBA, with alpha set to 0xFF (fully opaque). #[inline] @@ -109,7 +132,7 @@ impl Color3 { } pub fn to_color3f(self) -> Color3f { - self.0.map(|c| c as f32 / 255.0).into() + self.map(|c| c as f32 / 255.0) } /// Returns the HSL color equivalent to `self`. @@ -158,7 +181,7 @@ impl Color3 { impl Color4 { /// Returns `self` as RGB, discarding the alpha channel. #[inline] - pub fn to_rgb(self) -> Color3 { + pub const fn to_rgb(self) -> Color3 { let [r, g, b, _] = self.0; rgb(r, g, b) } @@ -169,9 +192,9 @@ impl Color4 { /// Returns the HSLA color equivalent to `self`. pub fn to_hsla(self) -> Color4 { - let [r, g, b, _] = self.0; + let [r, g, b, a] = self.0; let [h, s, l] = rgb(r, g, b).to_hsl().0; - [h, s, l, self.a()].into() + hsla(h, s, l, a) } } @@ -266,7 +289,7 @@ impl Color3f { impl Color4f { /// Returns `self` as RGB, discarding the alpha channel. - pub fn to_rgb(self) -> Color3f { + pub const fn to_rgb(self) -> Color3f { let [r, g, b, _] = self.0; rgb(r, g, b) } @@ -283,6 +306,7 @@ impl Color4f { pub fn to_color4(self) -> Color4 { self.to_u8().into() } + #[inline] fn to_u8(self) -> [u8; 4] { self.0.map(|c| (c.clamp(0.0, 1.0) * 255.0) as u8) @@ -290,9 +314,9 @@ impl Color4f { /// Returns the HSLA color equivalent to `self`. pub fn to_hsla(self) -> Color4f { - let [r, g, b, _] = self.0; + let [r, g, b, a] = self.0; let [h, s, l] = rgb(r, g, b).to_hsl().0; - [h, s, l, self.a()].into() + hsla(h, s, l, a) } } @@ -397,7 +421,7 @@ fn hcx_to_rgb(h: i32, c: T, x: T, z: T) -> [T; 3] { impl Color4 { /// Returns `self` as HSL, discarding the alpha channel. - pub fn to_hsl(self) -> Color3 { + pub const fn to_hsl(self) -> Color3 { let [h, s, l, _] = self.0; hsl(h, s, l) } @@ -420,86 +444,73 @@ impl Color4f { } } -impl Color -where - R: Index, - Sc: Copy, -{ +impl Color<[Ch; N], Rgb> { /// Returns the red component of `self`. - pub fn r(&self) -> Sc { + pub const fn r(&self) -> Ch { self.0[0] } /// Returns the green component of `self`. - pub fn g(&self) -> Sc { + pub const fn g(&self) -> Ch { self.0[1] } /// Returns the blue component of `self`. - pub fn b(&self) -> Sc { + pub const fn b(&self) -> Ch { self.0[2] } } -impl Color -where - R: Index, - Sc: Copy, -{ +impl Color<[Ch; N], Rgba> { /// Returns the red component of `self`. - pub fn r(&self) -> Sc { + pub const fn r(&self) -> Ch { self.0[0] } /// Returns the green component of `self`. - pub fn g(&self) -> Sc { + pub const fn g(&self) -> Ch { self.0[1] } /// Returns the blue component of `self`. - pub fn b(&self) -> Sc { + pub const fn b(&self) -> Ch { self.0[2] } /// Returns the alpha component of `self`. - pub fn a(&self) -> Sc { + pub const fn a(&self) -> Ch { self.0[3] } } -impl Color -where - R: Index, - Sc: Copy, -{ +impl Color<[Ch; N], Hsl> { /// Returns the hue component of `self`. - pub fn h(&self) -> Sc { + pub const fn h(&self) -> Ch { self.0[0] } /// Returns the saturation component of `self`. - pub fn s(&self) -> Sc { + pub const fn s(&self) -> Ch { self.0[1] } /// Returns the luminance component of `self`. - pub fn l(&self) -> Sc { + pub const fn l(&self) -> Ch { self.0[2] } } -impl Color +impl Color<[Ch; N], Hsla> where - R: Index, - Sc: Copy, + Ch: Copy, { /// Returns the hue component of `self`. - pub fn h(&self) -> Sc { + pub const fn h(&self) -> Ch { self.0[0] } /// Returns the saturation component of `self`. - pub fn s(&self) -> Sc { + pub const fn s(&self) -> Ch { self.0[1] } /// Returns the luminance component of `self`. - pub fn l(&self) -> Sc { + pub const fn l(&self) -> Ch { self.0[2] } /// Returns the alpha component of `self`. - pub fn a(&self) -> Sc { + pub const fn a(&self) -> Ch { self.0[3] } } @@ -575,6 +586,20 @@ impl From for Color { } } +impl, Sp> Index for Color { + type Output = R::Output; + + fn index(&self, i: usize) -> &Self::Output { + &self.0[i] + } +} + +impl, Sp> IndexMut for Color { + fn index_mut(&mut self, i: usize) -> &mut Self::Output { + &mut self.0[i] + } +} + // // Arithmetic trait impls // diff --git a/core/src/math/mat.rs b/core/src/math/mat.rs index 8ce22bab..07380785 100644 --- a/core/src/math/mat.rs +++ b/core/src/math/mat.rs @@ -1,9 +1,9 @@ -#![allow(clippy::needless_range_loop)] - //! Matrices and linear and affine transforms. //! //! TODO Docs +#![allow(clippy::needless_range_loop)] + use core::{ array, fmt::{self, Debug, Formatter}, @@ -11,7 +11,7 @@ use core::{ ops::Range, }; -use crate::render::{NdcToScreen, ViewToProj}; +use crate::render::{Ndc, Screen, View}; use super::{ approx::ApproxEq, @@ -68,11 +68,16 @@ pub struct RealToProj(Pd); pub struct Matrix(pub Repr, Pd); /// Type alias for a 2x2 float matrix. -pub type Mat2x2 = Matrix<[[f32; 2]; 2], Map>; +pub type Mat2 = + Matrix<[[f32; 2]; 2], RealToReal>; /// Type alias for a 3x3 float matrix. -pub type Mat3x3 = Matrix<[[f32; 3]; 3], Map>; +pub type Mat3 = + Matrix<[[f32; 3]; 3], RealToReal>; /// Type alias for a 4x4 float matrix. -pub type Mat4x4 = Matrix<[[f32; 4]; 4], Map>; +pub type Mat4 = + Matrix<[[f32; 4]; 4], RealToReal>; + +pub type ProjMat3 = Matrix<[[f32; 4]; 4], RealToProj>; // // Inherent impls @@ -82,9 +87,9 @@ pub type Mat4x4 = Matrix<[[f32; 4]; 4], Map>; /// /// # Examples /// ``` -/// use retrofire_core::{mat, math::Mat3x3}; +/// use retrofire_core::{mat, math::Mat3}; /// -/// let m: Mat3x3 = mat![ +/// let m: Mat3 = mat![ /// 0.0, 2.0, 0.0; /// 1.0, 0.0, 0.0; /// 0.0, 0.0, 3.0; @@ -122,11 +127,18 @@ impl Matrix { { Matrix(self.0.clone(), Pd) } + + pub fn apply(&self, t: &T) -> >::Output + where + Self: Apply, + { + Apply::apply(self, t) + } } impl Matrix<[[Sc; N]; M], Map> where - Sc: Copy, + Sc: Linear + Copy, Map: LinearMap, { /// Returns the row vector of `self` with index `i`. @@ -135,16 +147,31 @@ where /// /// # Panics /// If `i >= M`. + /// + /// # Examples + /// ``` + /// use retrofire_core::{mat, math::{vec2, Mat2}}; + /// + /// let m: Mat2 = mat![1.0, 2.0; 3.0, 4.0]; + /// assert_eq!(m.row_vec(0), vec2(1.0, 2.0)); #[inline] pub fn row_vec(&self, i: usize) -> Vector<[Sc; N], Map::Source> { Vector::new(self.0[i]) } + /// Returns the column vector of `self` with index `i`. /// /// The returned vector is in space `Map::Dest`. /// /// # Panics /// If `i >= N`. + /// + /// # Examples + /// ``` + /// use retrofire_core::{mat, math::{vec2, Mat2}}; + /// + /// let m: Mat2 = mat![1.0, 2.0; 3.0, 4.0]; + /// assert_eq!(m.col_vec(1), vec2(2.0, 4.0)); #[inline] pub fn col_vec(&self, i: usize) -> Vector<[Sc; M], Map::Dest> { Vector::new(self.0.map(|row| row[i])) @@ -154,6 +181,16 @@ impl Matrix<[[Sc; N]; N], RealToReal> { /// Returns `self` with its rows and columns swapped. + /// + /// # Examples + /// ``` + /// use retrofire_core::{mat, math::{vec2, Mat2}}; + /// + /// let m: Mat2 = mat![1.0, 2.0; + /// 3.0, 4.0]; + /// assert_eq!(m.transpose(), mat![1.0, 3.0; + /// 2.0, 4.0]); + #[must_use] pub fn transpose(self) -> Matrix<[[Sc; N]; N], RealToReal> { const { assert!(N >= DIM, "map dimension >= matrix dimension") } array::from_fn(|j| array::from_fn(|i| self.0[i][j])).into() @@ -174,8 +211,9 @@ impl Matrix<[[f32; N]; N], Map> { /// 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 { + // Needs const traits to be more generic; + // const array::map/from_fn for a nicer impl let mut els = [[0.0; N]; N]; let mut i = 0; while i < N { @@ -192,37 +230,6 @@ impl Matrix<[[f32; N]; N], Map> { } } -impl Mat4x4 { - /// Constructs a matrix from a linear basis. - /// - /// The basis does not have to be orthonormal. - pub const fn from_linear( - i: Vec3, - j: Vec3, - k: Vec3, - ) -> Mat4x4> { - 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], 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 - ] - } -} - impl Matrix<[[Sc; N]; N], Map> where Sc: Linear + Copy, @@ -267,19 +274,20 @@ where } } -impl Mat2x2> { +impl Mat2 { /// Returns the determinant of `self`. /// /// # Examples /// ``` - /// use retrofire_core::math::{Mat2x2, mat::RealToReal}; + /// use retrofire_core::math::Mat2; /// - /// let double: Mat2x2> = [[2.0, 0.0], [0.0, 2.0]].into(); + /// let double: Mat2 = [[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(); + /// let singular: Mat2 = [[1.0, 0.0], [2.0, 0.0]].into(); /// assert_eq!(singular.determinant(), 0.0); /// ``` + #[inline] pub const fn determinant(&self) -> f32 { let [[a, b], [c, d]] = self.0; a * d - b * c @@ -293,20 +301,18 @@ impl Mat2x2> { /// /// # Examples /// ``` - /// use retrofire_core::math::{Mat2x2, mat::RealToReal}; + /// use retrofire_core::math::{Mat2, mat::RealToReal}; /// - /// let rotate_90: Mat2x2> = [[0.0, -1.0], [1.0, 0.0]].into(); + /// let rotate_90: Mat2> = [[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(); + /// let singular: Mat2> = [[1.0, 0.0], [2.0, 0.0]].into(); /// assert_eq!(singular.checked_inverse(), None); /// ``` #[must_use] - pub const fn checked_inverse( - &self, - ) -> Option>> { + pub const fn checked_inverse(&self) -> Option> { let det = self.determinant(); // No approx_eq in const :/ if det.abs() < 1e-6 { @@ -314,6 +320,7 @@ impl Mat2x2> { } let r_det = 1.0 / det; let [[a, b], [c, d]] = self.0; + // Inverse is transpose of cofactor matrix divided by determinant Some(mat![ r_det * d, r_det * -b; r_det * -c, r_det * a @@ -326,35 +333,89 @@ impl Mat2x2> { /// is nonzero. A non-invertible matrix is also called singular. /// /// # Panics - /// If `self` has no inverse. + /// If `self` is singular or near-singular. /// /// # Examples /// ``` - /// use retrofire_core::math::{Mat2x2, mat::RealToReal, vec2}; + /// use retrofire_core::math::{Mat2, mat::RealToReal, vec2}; /// - /// let rotate_90: Mat2x2> = [[0.0, -1.0], [1.0, 0.0]].into(); + /// let rotate_90: Mat2> = [[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()) + /// assert_eq!(rotate_90.then(&rotate_neg_90), Mat2::identity()) /// ``` /// ```should_panic - /// # use retrofire_core::math::{Mat2x2, mat::RealToReal}; + /// # use retrofire_core::math::{Mat2, mat::RealToReal}; /// /// // This matrix has no inverse - /// let singular: Mat2x2> = [[1.0, 0.0], [2.0, 0.0]].into(); + /// let singular: Mat2> = [[1.0, 0.0], [2.0, 0.0]].into(); /// /// // This will panic /// let _ = singular.inverse(); /// ``` #[must_use] - pub const fn inverse(&self) -> Mat2x2> { + pub const fn inverse(&self) -> Mat2 { self.checked_inverse() .expect("matrix cannot be singular or near-singular") } } -impl Mat3x3> { +impl Mat3 { + /// Constructs a matrix from a linear basis. + /// + /// The basis does not have to be orthonormal. + pub const fn from_linear(i: Vec2, j: Vec2) -> Self { + Self::from_affine(i, j, Point2::origin()) + } + + /// Constructs a matrix from an affine basis, or frame. + /// + /// The basis does not have to be orthonormal. + pub const fn from_affine( + i: Vec2, + j: Vec2, + o: Point2, + ) -> Self { + let (i, j, o) = (i.0, j.0, o.0); + mat![ + i[0], j[0], o[0]; + i[1], j[1], o[1]; + 0.0, 0.0, 1.0; + ] + } + + /// Returns the linear 2x2 submatrix of `self`. + /// + /// # Examples + /// ``` + /// use retrofire_core::assert_approx_eq; + /// use retrofire_core::math::*; + /// + /// // TODO translate2 does not exist (yet) + /// /*let m = rotate2(degs(90.0)).then(&translate3(1.0, 2.0, 3.0)); + /// let lin = m.linear(); + /// assert_approx_eq!(lin.apply(&pt2(1.0, 0.0, 0.0)), pt2(0.0, 0.0, -1.0));*/ + pub const fn linear(&self) -> Mat2 { + let [r, s, _] = self.0; + mat![r[0], r[1]; s[0], s[1]] + } + + /// Returns the translation column vector of `self`. + /// + /// # Example + /// ``` + /// use retrofire_core::math::*; + /// + /// // TODO translate2 does not exist (yet) + /// /*let trans = vec2(1.0, 2.0); + /// let m = rotate2(degs(45.0)).then(&translate(trans)); + /// assert_eq!(m.translation(), trans);*/ + pub const fn translation(&self) -> Vec2 { + let [r, s, _] = self.0; + vec2(r[2], s[2]) + } + /// Returns the determinant of `self`. pub const fn determinant(&self) -> f32 { let [a, b, c] = self.0[0]; @@ -376,23 +437,8 @@ impl Mat3x3> { /// 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 - /// ``` - /// 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 { + 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; @@ -402,15 +448,40 @@ impl Mat3x3> { } /// Returns the inverse of `self`, or `None` if `self` is singular. + /// + /// # Examples + /// ``` + /// use retrofire_core::{mat, math::Mat3}; + /// + /// let mat: Mat3 = mat![ + /// 2.0, 0.0, 1.0; + /// 0.0, 4.0, 2.0; + /// 0.0, 0.0, 1.0 + /// ]; + /// assert_eq!(mat.checked_inverse(), Some(mat![ + /// 0.5, 0.0, -0.5; + /// 0.0, 0.25, -0.5; + /// 0.0, 0.0, 1.0 + /// ])); + /// ``` #[must_use] - pub fn checked_inverse(&self) -> Option>> { + pub const 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 + // Inverse is transpose of cofactor matrix divided by determinant + let mut res = [[0.0; 3]; 3]; + let r_det = 1.0 / det; + let mut i = 0; + while i < 3 { + res[i][0] = r_det * self.cofactor(0, i); + res[i][1] = r_det * self.cofactor(1, i); + res[i][2] = r_det * self.cofactor(2, i); + i += 1; + } + /*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 @@ -418,28 +489,78 @@ impl Mat3x3> { 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 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)) + Some(Mat3::new(res)) } - pub fn inverse(&self) -> Mat3x3> { + pub fn inverse(&self) -> Mat3 { self.checked_inverse() .expect("matrix cannot be singular or near-singular") } +} + +impl Mat4 { + /// Constructs a matrix from a linear basis. + /// + /// The basis does not have to be orthonormal. + pub const fn from_linear(i: Vec3, j: Vec3, k: Vec3) -> Self { + Self::from_affine(i, j, k, Point3::origin()) + } - const fn from_rows(i: Vec3, j: Vec3, k: Vec3) -> Self { - Self::new([i.0, j.0, k.0]) + /// Constructs a matrix from an affine basis, or frame. + /// + /// A frame consists of three vectors defining a linear basis, plus a point + /// specifying the origin point of the frame. + /// + /// The basis does not have to be orthonormal. + pub const fn from_affine( + i: Vec3, + j: Vec3, + k: Vec3, + o: Point3, + ) -> Self { + let (o, i, j, k) = (o.0, i.0, j.0, k.0); + mat![ + 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 + ] + } + + /// Returns the linear 3x3 submatrix of `self`. + /// + /// # Examples + /// ``` + /// use retrofire_core::assert_approx_eq; + /// use retrofire_core::math::*; + /// + /// let m = rotate_y(degs(90.0)).then(&translate3(1.0, 2.0, 3.0)); + /// let lin = m.linear(); + /// assert_approx_eq!(lin.apply(&pt3(1.0, 0.0, 0.0)), pt3(0.0, 0.0, -1.0)); + pub const fn linear(&self) -> Mat3 { + let [r, s, t, _] = self.0; + mat![ + r[0], r[1], r[2]; + s[0], r[1], r[2]; + t[0], r[1], r[2]; + ] + } + + /// Returns the translation column vector of `self`. + /// + /// # Example + /// ``` + /// use retrofire_core::math::*; + /// + /// let trans = vec3(1.0, 2.0, 3.0); + /// let m = rotate_y(degs(45.0)).then(&translate(trans)); + /// assert_eq!(m.translation(), trans); + pub const fn translation(&self) -> Vec3 { + vec3(self.0[0][3], self.0[1][3], self.0[2][3]) } -} -impl Mat4x4> { /// Returns the determinant of `self`. /// /// Given a matrix M, @@ -449,15 +570,17 @@ impl Mat4x4> { /// ⎜ i j k l ⎟ /// ⎝ m n o p ⎠ /// ``` - /// 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: + /// its determinant can be computed by multiplying each element *e* on row 0 + /// with its *minors*: the determinant of the submatrix obtained by removing + /// the row and column of *e*: /// ```text /// ⎜ f g h ⎜ ⎜ e g h ⎜ - /// det(M) = a · ⎜ j k l ⎜ - b · ⎜ i k l ⎜ + - ··· + /// det(M) = a · ⎜ j k l ⎜ - b · ⎜ i k l ⎜ + c * ··· - d * ··· /// ⎜ n o p ⎜ ⎜ m o p ⎜ /// ``` pub fn determinant(&self) -> f32 { let [[a, b, c, d], r, s, t] = self.0; + let det2 = |m, n| s[m] * t[n] - s[n] * t[m]; let det3 = |j, k, l| r[j] * det2(k, l) - r[k] * det2(j, l) + r[l] * det2(j, k); @@ -466,17 +589,22 @@ impl Mat4x4> { - d * det3(0, 1, 2) } + #[must_use] + pub fn checked_inverse(&self) -> Option> { + (!self.determinant().approx_eq(&0.0)).then(|| self.inverse()) + } + /// Returns the inverse matrix of `self`. /// - /// The inverse 𝝡-1 of matrix 𝝡 is a matrix that, when - /// composed with 𝝡, results in the [identity](Self::identity) matrix: + /// The inverse **M**-1 of matrix **M** is a matrix that, when + /// composed with **M**, results in the [identity](Self::identity) matrix: /// - /// 𝝡 ∘ 𝝡-1 = 𝝡-1 ∘ 𝝡 = 𝐈 + /// **M** ∘ **M**-1 = **M**-1 ∘ **M** = **I** /// - /// In other words, it applies the transform of 𝝡 in reverse. - /// Given vectors 𝘃 and 𝘂, + /// In other words, it applies the transform of **M** in reverse. + /// Given vectors **v** and **u**, /// - /// 𝝡𝘃 = 𝘂 ⇔ 𝝡-1 𝘂 = 𝘃. + /// **Mv** = **u** ⇔ **M**-1 **u** = **v**. /// /// Only matrices with a nonzero determinant have a defined inverse. /// A matrix without an inverse is said to be singular. @@ -485,11 +613,12 @@ impl Mat4x4> { /// suffer from imprecision or numerical instability in certain cases. /// /// # Panics - /// 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). + /// 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). + // TODO example #[must_use] - pub fn inverse(&self) -> Mat4x4> { + pub fn inverse(&self) -> Mat4 { use super::float::f32; if cfg!(debug_assertions) { let det = self.determinant(); @@ -502,17 +631,17 @@ impl Mat4x4> { // Elementary row operation subtracting one row, // multiplied by a scalar, from another - fn sub_row(m: &mut Mat4x4, from: usize, to: usize, mul: f32) { + fn sub_row(m: &mut Mat4, from: usize, to: usize, mul: f32) { m.0[to] = (m.row_vec(to) - m.row_vec(from) * mul).0; } // Elementary row operation multiplying one row with a scalar - fn mul_row(m: &mut Mat4x4, row: usize, mul: f32) { + fn mul_row(m: &mut Mat4, row: usize, mul: f32) { m.0[row] = (m.row_vec(row) * mul).0; } // Elementary row operation swapping two rows - fn swap_rows(m: &mut Mat4x4, r: usize, s: usize) { + const fn swap_rows(m: &mut Mat4, r: usize, s: usize) { m.0.swap(r, s); } @@ -522,7 +651,7 @@ impl Mat4x4> { // `this` is reduced, the value of `inv` has become the inverse of // `this` and thus of `self`. - let inv = &mut Mat4x4::identity(); + let inv = &mut Mat4::identity(); let this = &mut self.to(); // Apply row operations to reduce the matrix to an upper echelon form @@ -613,12 +742,12 @@ where // Apply trait impls -impl Apply> for Mat2x2> { +impl Apply> for Mat2 { type Output = Vec2; /// Maps a real 2-vector from basis `Src` to basis `Dst`. /// - /// Computes the matrix–vector multiplication **MV** where **v** is + /// Computes the matrix–vector multiplication **Mv** where **v** is /// interpreted as a column vector: /// /// ```text @@ -630,10 +759,10 @@ impl Apply> for Mat2x2> { } } -impl Apply> for Mat2x2> { +impl Apply> for Mat2 { type Output = Point2; - /// Maps a real 2-vector from basis `Src` to basis `Dst`. + /// Maps a real 2-point from basis `Src` to basis `Dst`. /// /// Computes the matrix–point multiplication **M***p* where *p* is /// interpreted as a column vector: @@ -647,13 +776,14 @@ impl Apply> for Mat2x2> { } } -impl Apply> for Mat3x3> { +impl Apply> for Mat3 { 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: + /// Computes the affine matrix–vector multiplication **Mv**, where + /// **v** is interpreted as a homogeneous column vector with an implicit + /// *v*2 component with value 0: /// /// ```text /// ⎛ M00 · · ⎞ ⎛ v0 ⎞ ⎛ v0' ⎞ @@ -667,13 +797,14 @@ impl Apply> for Mat3x3> { } } -impl Apply> for Mat3x3> { +impl Apply> for Mat3 { 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: + /// Computes the affine matrix–point multiplication **M***p*, where + /// *p* is interpreted as a homogeneous column vector with an implicit + /// *p*2 component with value 1: /// /// ```text /// ⎛ M00 · · ⎞ ⎛ p0 ⎞ ⎛ p0' ⎞ @@ -686,7 +817,7 @@ impl Apply> for Mat3x3> { } } -impl Apply> for Mat3x3> { +impl Apply> for Mat3 { type Output = Vec3; /// Maps a real 3-vector from basis `Src` to basis `Dst`. @@ -708,7 +839,7 @@ impl Apply> for Mat3x3> { } } -impl Apply> for Mat3x3> { +impl Apply> for Mat3 { type Output = Point3; /// Maps a real 3-point from basis `Src` to basis `Dst`. @@ -726,13 +857,14 @@ impl Apply> for Mat3x3> { } } -impl Apply> for Mat4x4> { +impl Apply> for Mat4 { 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: + /// Computes the affine matrix–vector multiplication **Mv**, where + /// **v** is interpreted as a homogeneous column vector with an implicit + /// *v*3 component with value 0: /// /// ```text /// ⎛ M00 · · · ⎞ ⎛ v0 ⎞ ⎛ v0' ⎞ @@ -746,13 +878,14 @@ impl Apply> for Mat4x4> { } } -impl Apply> for Mat4x4> { +impl Apply> for Mat4 { 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: + /// Computes the affine matrix–point multiplication **M***p* where *p* + /// is interpreted as a homogeneous column vector with an implicit + /// *p*3 component with value 1: /// /// ```text /// ⎛ M00 · · · ⎞ ⎛ p0 ⎞ ⎛ p0' ⎞ @@ -766,13 +899,14 @@ impl Apply> for Mat4x4> { } } -impl Apply> for Mat4x4> { +impl Apply> for ProjMat3 { 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: + /// Computes the matrix–point multiplication **M***p*, where *p* + /// is interpreted as a homogeneous column vector with an implicit + /// *p*3 component with value 1: /// /// ```text /// ⎛ M00 · · ⎞ ⎛ p0 ⎞ ⎛ p0' ⎞ @@ -835,21 +969,26 @@ impl From for Matrix { // Free functions // -/// Returns a matrix applying a scaling by `s`. +/// Returns a matrix applying a scaling by a vector of factors. /// +/// # Examples /// Tip: use [`splat`][super::vec::splat] to scale uniformly: /// ``` -/// use retrofire_core::math::{scale, splat}; +/// use retrofire_core::math::{Apply, scale, splat, vec3}; +/// /// let m = scale(splat(2.0)); -/// assert_eq!(m.0[0][0], 2.0); -/// assert_eq!(m.0[1][1], 2.0); -/// assert_eq!(m.0[2][2], 2.0); +/// let scaled = m.apply(&vec3(1.0, -2.0, 3.0)); +/// assert_eq!(scaled, vec3(2.0, -4.0, 6.0)) /// ``` -pub const fn scale(s: Vec3) -> Mat4x4> { +pub const fn scale(s: Vec3) -> Mat4 { scale3(s.0[0], s.0[1], s.0[2]) } -pub const fn scale3(x: f32, y: f32, z: f32) -> Mat4x4> { +/// Returns a matrix applying a scaling by the given factors. +/// +/// # Examples +/// See the [`scale`] method for an example. +pub const fn scale3(x: f32, y: f32, z: f32) -> Mat4 { mat![ x, 0.0, 0.0, 0.0; 0.0, y, 0.0, 0.0; @@ -858,12 +997,33 @@ pub const fn scale3(x: f32, y: f32, z: f32) -> Mat4x4> { ] } -/// Returns a matrix applying a translation by `t`. -pub const fn translate(t: Vec3) -> Mat4x4> { +/// Returns a matrix applying a translation vector to points. +/// +/// Vectors have no defined position and are unaffected by translation. +/// +/// # Examples +/// ``` +/// use retrofire_core::math::{Apply, pt3, vec3, translate}; +/// +/// let m = translate(vec3(1.0, -2.0, 3.0)); +/// +/// // Points are moved +/// assert_eq!(m.apply(&pt3(1.0, 1.0, 1.0)), pt3(2.0, -1.0, 4.0)); +/// +/// // Vectors are unaffected +/// assert_eq!(m.apply(&vec3(1.0, 1.0, 1.0)), vec3(1.0, 1.0, 1.0)); +///``` +pub const fn translate(t: Vec3) -> Mat4 { translate3(t.0[0], t.0[1], t.0[2]) } -pub const fn translate3(x: f32, y: f32, z: f32) -> Mat4x4> { +/// Returns a matrix applying a translation to *points*. +/// +/// Vectors have no defined position and are unaffected by translation. +/// +/// # Examples +/// See the [`translate`] function for an example. +pub const fn translate3(x: f32, y: f32, z: f32) -> Mat4 { mat![ 1.0, 0.0, 0.0, x ; 0.0, 1.0, 0.0, y ; @@ -875,36 +1035,33 @@ pub const fn translate3(x: f32, y: f32, z: f32) -> Mat4x4> { #[cfg(feature = "fp")] use super::Angle; -/// 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`. +/// Returns a matrix applying a rotation that sends the y-axis to the given vector. /// +/// The new y-axis is chosen so that it's orthogonal to both `new_z` and `x`. /// Returns an orthogonal basis. If `new_y` and `x` are unit vectors, /// the basis is orthonormal. /// /// # Panics /// If `x` is approximately parallel to `new_y` and the basis would be /// degenerate. -#[cfg(feature = "fp")] -pub fn orient_y(new_y: Vec3, x: Vec3) -> Mat4x4> { +// TODO example +pub fn orient_y(new_y: Vec3, x: Vec3) -> Mat4 { 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 -/// both `new_z` and `x`. +/// Returns a matrix applying a rotation that sends the z-axis to the given vector. /// -/// Returns an orthogonal basis. If `new_z` and `x` are unit vectors, -/// the basis is orthonormal. +/// The new y-axis is chosen so that it's orthogonal to both `new_z` and `x`. +/// This function returns an orthogonal basis. If `new_z` and `x` are unit +/// vectors, the basis is orthonormal. /// /// # Panics /// If `x` is approximately parallel to `new_z` and the basis would be /// degenerate. -#[cfg(feature = "fp")] -pub fn orient_z(new_z: Vec3, x: Vec3) -> Mat4x4> { +pub fn orient_z(new_z: Vec3, x: Vec3) -> Mat4 { orient(new_z.cross(&x).normalize(), new_z) } -/// Constructs a change-of-basis matrix given y and z basis vectors. +/// Constructs a linear basis, given the y and z basis vectors. /// /// The third basis vector is the cross product of `new_y` and `new_z`. /// If the inputs are orthogonal, the resulting basis is orthogonal. @@ -913,55 +1070,79 @@ pub fn orient_z(new_z: Vec3, x: Vec3) -> Mat4x4> { /// # Panics /// If `new_y` is approximately parallel to `new_z` and the basis would /// be degenerate. -#[cfg(feature = "fp")] -fn orient(new_y: Vec3, new_z: Vec3) -> Mat4x4> { +fn orient(new_y: Vec3, new_z: Vec3) -> Mat4 { let new_x = new_y.cross(&new_z); assert!( !new_x.len_sqr().approx_eq(&0.0), "{new_y:?} × {new_z:?} non-finite or too close to zero vector" ); - Mat4x4::from_linear(new_x, new_y, new_z) + Mat4::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 (on the yz plane). +/// +/// # Example +/// ``` +/// use retrofire_core::assert_approx_eq; +/// use retrofire_core::math::{Apply, degs, rotate_x, vec3}; +/// +/// let m = rotate_x(degs(90.0)); +/// assert_approx_eq!(m.apply(&vec3(0.0, 1.0, 0.0)), vec3(0.0, 0.0, 1.0)); +/// ``` #[cfg(feature = "fp")] -pub fn rotate_x(a: Angle) -> Mat4x4> { +pub fn rotate_x(a: Angle) -> Mat4 { let (sin, cos) = a.sin_cos(); mat![ - 1.0, 0.0, 0.0, 0.0; - 0.0, cos, sin, 0.0; - 0.0, -sin, cos, 0.0; - 0.0, 0.0, 0.0, 1.0; + 1.0, 0.0, 0.0, 0.0; + 0.0, cos, -sin, 0.0; + 0.0, sin, cos, 0.0; + 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 (on the xz plane). +/// +/// # Example +/// ``` +/// use retrofire_core::assert_approx_eq; +/// use retrofire_core::math::{Apply, degs, rotate_y, vec3}; +/// +/// let m = rotate_y(degs(90.0)); +/// assert_approx_eq!(m.apply(&vec3(1.0, 0.0, 0.0)), vec3(0.0, 0.0, -1.0)); +///``` #[cfg(feature = "fp")] -pub fn rotate_y(a: Angle) -> Mat4x4> { +pub fn rotate_y(a: Angle) -> Mat4 { let (sin, cos) = a.sin_cos(); mat![ - cos, 0.0, -sin, 0.0; - 0.0, 1.0, 0.0, 0.0; - sin, 0.0, cos, 0.0; - 0.0, 0.0, 0.0, 1.0; + cos, 0.0, sin, 0.0; + 0.0, 1.0, 0.0, 0.0; + -sin, 0.0, cos, 0.0; + 0.0, 0.0, 0.0, 1.0; ] } -/// Returns a matrix applying a 3D rotation about the z axis. +/// Returns a matrix applying a 3D rotation about the z axis (on the xy plane). +/// # Example +/// ``` +/// use retrofire_core::assert_approx_eq; +/// use retrofire_core::math::{Apply, degs, rotate_z, vec3}; +/// +/// let m = rotate_z(degs(90.0)); +/// assert_approx_eq!(m.apply(&vec3(1.0, 0.0, 0.0)), vec3(0.0, 1.0, 0.0)); #[cfg(feature = "fp")] -pub fn rotate_z(a: Angle) -> Mat4x4> { +pub fn rotate_z(a: Angle) -> Mat4 { let (sin, cos) = a.sin_cos(); mat![ - cos, sin, 0.0, 0.0; - -sin, cos, 0.0, 0.0; - 0.0, 0.0, 1.0, 0.0; - 0.0, 0.0, 0.0, 1.0; + cos, -sin, 0.0, 0.0; + sin, cos, 0.0, 0.0; + 0.0, 0.0, 1.0, 0.0; + 0.0, 0.0, 0.0, 1.0; ] } /// Returns a matrix applying a 2D rotation by an angle. #[cfg(feature = "fp")] -pub fn rotate2(a: Angle) -> Mat3x3> { +pub fn rotate2(a: Angle) -> Mat3 { let (sin, cos) = a.sin_cos(); mat![ cos, sin, 0.0; @@ -972,14 +1153,12 @@ pub fn rotate2(a: Angle) -> Mat3x3> { /// Returns a matrix applying a 3D rotation about an arbitrary axis. #[cfg(feature = "fp")] -pub fn rotate(axis: Vec3, a: Angle) -> Mat4x4> { - use crate::math::approx::ApproxEq; - +pub fn rotate(axis: Vec3, a: Angle) -> Mat4 { // 1. Change of basis such that `axis` is mapped to the z-axis, // 2. Rotation about the z-axis // 3. Change of basis back to the original let mut other = Vec3::X; - if axis.cross(&other).len_sqr().approx_eq(&0.0) { + if axis.cross(&other).len_sqr() < 0.25 { // Avoid degeneracy other = Vec3::Y; } @@ -1004,11 +1183,11 @@ pub fn rotate(axis: Vec3, a: Angle) -> Mat4x4> { /// # Panics /// * If any parameter value is nonpositive. /// * If `near_far` is an empty range. -pub fn perspective( +pub const fn perspective( focal_ratio: f32, aspect_ratio: f32, near_far: Range, -) -> Mat4x4 { +) -> ProjMat3 { let (near, far) = (near_far.start, near_far.end); assert!(focal_ratio > 0.0, "focal ratio must be positive"); @@ -1033,10 +1212,13 @@ pub fn perspective( /// # Parameters /// * `lbn`: The left-bottom-near corner of the projection box. /// * `rtf`: The right-bottom-far corner of the projection box. -pub fn orthographic(lbn: Point3, rtf: Point3) -> Mat4x4 { - let half_d = (rtf - lbn) / 2.0; - let [cx, cy, cz] = (lbn + half_d).0; - let [idx, idy, idz] = half_d.map(f32::recip).0; +pub const fn orthographic(lbn: Point3, rtf: Point3) -> ProjMat3 { + // Done manually due until const traits are stable + let [x0, y0, z0] = lbn.0; + let [x1, y1, z1] = rtf.0; + let [dx, dy, dz] = [(x1 - x0) / 2.0, (y1 - y0) / 2.0, (z1 - z0) / 2.0]; + let [cx, cy, cz] = [x0 + dx, y0 + dy, z0 + dz]; + let [idx, idy, idz] = [1.0 / dx, 1.0 / dy, 1.0 / dz]; mat![ idx, 0.0, 0.0, -cx * idx; 0.0, idy, 0.0, -cy * idy; @@ -1048,17 +1230,16 @@ 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`. -pub fn viewport(bounds: Range) -> Mat4x4 { - let s = bounds.start.map(|c| c as f32); - let e = bounds.end.map(|c| c as f32); - let half_d = (e - s) / 2.0; - let [dx, dy] = half_d.0; - let [cx, cy] = (s + half_d).0; +/// screen space for rasterization. NDC coordinates (-1, -1, _) are mapped +/// to `bounds.start` and NDC coordinates (1, 1, _) to `bounds.end`. +pub const fn viewport(bounds: Range) -> Mat4 { + let Range { start, end } = bounds; + let [x0, y0] = [start.x() as f32, start.y() as f32]; + let [x1, y1] = [end.x() as f32, end.y() as f32]; + let [dx, dy] = [(x1 - x0) / 2.0 as f32, (y1 - y0) / 2.0 as f32]; mat![ - dx, 0.0, 0.0, cx; - 0.0, dy, 0.0, cy; + dx, 0.0, 0.0, x0 + dx; + 0.0, dy, 0.0, y0 + dy; 0.0, 0.0, 1.0, 0.0; 0.0, 0.0, 0.0, 1.0; ] @@ -1075,16 +1256,17 @@ mod tests { use super::*; #[derive(Debug, Default, Eq, PartialEq)] - struct Basis1; + struct B1; #[derive(Debug, Default, Eq, PartialEq)] - struct Basis2; + struct B2; - type Map = RealToReal; - type InvMap = RealToReal; + type Map = RealToReal; + type InvMap = RealToReal; const X: Vec3 = Vec3::X; const Y: Vec3 = Vec3::Y; const Z: Vec3 = Vec3::Z; + #[allow(unused)] const O: Vec3 = Vec3::new([0.0; 3]); mod mat2x2 { @@ -1092,39 +1274,39 @@ mod tests { #[test] fn determinant_of_identity_is_one() { - let id = Mat2x2::>::identity(); + let id = ::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(); + let refl: Mat2 = [[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(); + let id = ::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(); + let m: Mat2 = [[0.5, 1.5], [1.0, -0.5]].into(); + let m_inv: Mat2 = 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()); + let m: Mat2 = [[0.5, 1.5], [1.0, -0.5]].into(); + let m_inv: Mat2 = m.inverse(); + assert_approx_eq!(m.compose(&m_inv), Mat2::identity()); + assert_approx_eq!(m.then(&m_inv), Mat2::identity()); } } mod mat3x3 { use super::*; - const MAT: Mat3x3 = mat![ + const MAT: Mat3 = mat![ 0.0, 1.0, 2.0; 10.0, 11.0, 12.0; 20.0, 21.0, 22.0; @@ -1132,18 +1314,18 @@ mod tests { #[test] fn row_col_vecs() { - assert_eq!(MAT.row_vec(2), vec3::<_, Basis1>(20.0, 21.0, 22.0)); - assert_eq!(MAT.col_vec(2), vec3::<_, Basis2>(2.0, 12.0, 22.0)); + assert_eq!(MAT.row_vec(2), vec3::<_, B1>(20.0, 21.0, 22.0)); + assert_eq!(MAT.col_vec(2), vec3::<_, B2>(2.0, 12.0, 22.0)); } #[test] fn composition() { - let tr: Mat3x3> = mat![ + let tr: Mat3 = mat![ 1.0, 0.0, 2.0; 0.0, 1.0, -3.0; 0.0, 0.0, 1.0; ]; - let sc: Mat3x3> = mat![ + let sc: Mat3 = mat![ -1.0, 0.0, 0.0; 0.0, 2.0, 0.0; 0.0, 0.0, 1.0; @@ -1164,7 +1346,7 @@ mod tests { #[test] fn scaling() { - let m: Mat3x3> = mat![ + let m: Mat3 = mat![ 2.0, 0.0, 0.0; 0.0, -3.0, 0.0; 0.0, 0.0, 1.0; @@ -1175,7 +1357,7 @@ mod tests { #[test] fn translation() { - let m: Mat3x3> = mat![ + let m: Mat3 = mat![ 1.0, 0.0, 2.0; 0.0, 1.0, -3.0; 0.0, 0.0, 1.0; @@ -1186,13 +1368,13 @@ mod tests { #[test] fn inverse_of_identity_is_identity() { - let i = Mat3x3::>::identity(); + let i = ::identity(); assert_eq!(i.inverse(), i); } #[test] fn inverse_of_scale_is_reciprocal_scale() { - let scale: Mat3x3> = mat![ - 2.0, 0.0, 0.0; + let scale: Mat3 = mat![ + 2.0, 0.0, 0.0; 0.0, -3.0, 0.0; 0.0, 0.0, 4.0; ]; @@ -1201,24 +1383,26 @@ mod tests { 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 + 0.0, 0.0, 1.0/4.0; ] ); } #[test] fn matrix_composed_with_inverse_is_identity() { - let mat: Mat3x3> = mat![ + let mat: Mat3 = 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()); + let composed: Mat3 = mat.compose(&mat.inverse()); + assert_approx_eq!(composed, Mat3::identity()); + let composed: Mat3 = mat.then(&mat.inverse()); + assert_approx_eq!(composed, Mat3::identity()); } #[test] fn singular_matrix_has_no_inverse() { - let singular: Mat3x3> = mat![ + let singular: Mat3 = mat![ 1.0, 2.0, 0.0; 0.0, 0.0, 0.0; 0.0, 0.0, 1.0; @@ -1231,7 +1415,7 @@ mod tests { fn matrix_debug() { assert_eq!( alloc::format!("{MAT:?}"), - r#"Matrix[ + r#"Matrix[ [ 0.0, 1.0, 2.0] [10.0, 11.0, 12.0] [20.0, 21.0, 22.0] @@ -1240,10 +1424,10 @@ mod tests { } } - mod mat4x4 { + mod mat4 { use super::*; - const MAT: Mat4x4 = mat![ + const MAT: Mat4 = mat![ 0.0, 1.0, 2.0, 3.0; 10.0, 11.0, 12.0, 13.0; 20.0, 21.0, 22.0, 23.0; @@ -1258,18 +1442,18 @@ mod tests { #[test] fn composition() { - let t = translate3(1.0, 2.0, 3.0).to::(); - let s = scale3(3.0, 2.0, 1.0).to::(); + let tr = translate3(1.0, 2.0, 3.0).to::(); + let sc = scale3(3.0, 2.0, 1.0).to::(); - 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)); 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)); + assert_eq!(tr_sc.apply(&o.to()), pt3::<_, B1>(3.0, 4.0, 3.0)); + assert_eq!(sc_tr.apply(&o.to()), pt3::<_, B2>(1.0, 2.0, 3.0)); } #[test] @@ -1301,10 +1485,23 @@ mod tests { assert_eq!(m.apply(&O), O); - assert_approx_eq!(m.apply(&Z), Y); + // Rotates counter-clockwise on the YZ-plane as seen + // from the direction of the positive X-axis: + // + // +y + // ^ + // | <--__ + // | \ + // | | + // O-------+---> -z + // / + // v + // +x + // + assert_approx_eq!(m.apply(&Y), Z); assert_approx_eq!( - m.apply(&pt3(0.0, -2.0, 0.0)), - pt3(0.0, 0.0, 2.0) + m.apply(&pt3(0.0, 0.0, 2.0)), + pt3(0.0, -2.0, 0.0) ); } @@ -1315,10 +1512,23 @@ mod tests { assert_eq!(m.apply(&O), O); - assert_approx_eq!(m.apply(&X), Z); + // Rotates counter-clockwise on the ZX-plane as seen + // from the direction of the positive Y-axis + // + // +x + // ^ + // | <--__ + // | \ + // | | + // O-------+---> +z + // / + // v + // +y + // + assert_approx_eq!(m.apply(&Z), X); assert_approx_eq!( - m.apply(&pt3(0.0, 0.0, -2.0)), - pt3(2.0, 0.0, 0.0) + m.apply(&pt3(2.0, 0.0, 0.0)), + pt3(0.0, 0.0, -2.0) ); } @@ -1329,10 +1539,23 @@ mod tests { assert_eq!(m.apply(&O), O); - assert_approx_eq!(m.apply(&Y), X); + // Rotates counter-clockwise on the XY-plane as seen + // from the direction of the positive Z-axis + // + // +y + // ^ + // | <--__ + // | \ + // | | + // O-------+---> +x + // / + // v + // +z + // + assert_approx_eq!(m.apply(&X), Y); assert_approx_eq!( - m.apply(&(pt3(-2.0, 0.0, 0.0))), - pt3(0.0, 2.0, 0.0) + m.apply(&(pt3(0.0, 2.0, 0.0))), + pt3(-2.0, 0.0, 0.0) ); } @@ -1370,13 +1593,31 @@ mod tests { #[test] fn from_basis() { - let m = Mat4x4::from_linear(Y, 2.0 * Z, -3.0 * X); + let m = Mat4::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); + + assert_eq!(m.apply(&X.to_pt()), Y.to_pt()); + assert_eq!(m.apply(&Y.to_pt()), (2.0 * Z).to_pt()); + assert_eq!(m.apply(&Z.to_pt()), (-3.0 * X).to_pt()); + } + + #[test] + fn from_affine_basis() { + let orig = pt3(1.0, 2.0, 3.0); + let m = Mat4::from_affine(Y, 2.0 * Z, -3.0 * X, orig); + + assert_eq!(m.apply(&X), Y); + assert_eq!(m.apply(&Y), 2.0 * Z); + assert_eq!(m.apply(&Z), -3.0 * X); + + assert_eq!(m.apply(&X.to_pt()), pt3(1.0, 3.0, 3.0)); + assert_eq!(m.apply(&Y.to_pt()), pt3(1.0, 2.0, 5.0)); + assert_eq!(m.apply(&Z.to_pt()), pt3(-2.0, 2.0, 3.0)); } - #[cfg(feature = "fp")] #[test] fn orientation_no_op() { let m = orient_y(Y, X); @@ -1391,7 +1632,6 @@ mod tests { assert_eq!(m.apply(&Z.to_pt()), Z.to_pt()); } - #[cfg(feature = "fp")] #[test] fn orientation_y_to_z() { let m = orient_y(Z, X); @@ -1406,7 +1646,6 @@ mod tests { assert_eq!(m.apply(&Z.to_pt()), (-Y).to_pt()); } - #[cfg(feature = "fp")] #[test] fn orientation_z_to_y() { let m = orient_z(Y, X); @@ -1425,7 +1664,7 @@ mod tests { fn matrix_debug() { assert_eq!( alloc::format!("{MAT:?}"), - r#"Matrix[ + r#"Matrix[ [ 0.0, 1.0, 2.0, 3.0] [10.0, 11.0, 12.0, 13.0] [20.0, 21.0, 22.0, 23.0] @@ -1437,14 +1676,14 @@ mod tests { #[test] fn transposition() { - let m = Matrix::<_, Map>::new([ - [0.0, 1.0, 2.0], // - [10.0, 11.0, 12.0], - [20.0, 21.0, 22.0], - ]); + let m: Mat3 = mat![ + 0.0, 1.0, 2.0; + 10.0, 11.0, 12.0; + 20.0, 21.0, 22.0 + ]; assert_eq!( m.transpose(), - Matrix::<_, InvMap>::new([ + Mat3::::new([ [0.0, 10.0, 20.0], // [1.0, 11.0, 21.0], [2.0, 12.0, 22.0], @@ -1454,13 +1693,13 @@ mod tests { #[test] fn determinant_of_identity_is_one() { - let id: Mat4x4 = Mat4x4::identity(); + let id: Mat4 = Mat4::identity(); assert_eq!(id.determinant(), 1.0); } #[test] fn determinant_of_scaling_is_product_of_diagonal() { - let scale: Mat4x4<_> = scale3(2.0, 3.0, 4.0); + let scale: Mat4 = scale3(2.0, 3.0, 4.0); assert_eq!(scale.determinant(), 24.0); } @@ -1471,34 +1710,27 @@ mod tests { assert_approx_eq!(rot.determinant(), 1.0); } - #[cfg(feature = "fp")] #[test] fn matrix_composed_with_inverse_is_identity() { - let m = translate3(1.0e3, -2.0e2, 0.0) + let m: Mat4 = translate3(1.0e3, -2.0e2, 0.0) .then(&scale3(0.5, 100.0, 42.0)) - .to::(); + .to(); - let m_inv: Mat4x4 = m.inverse(); + let m_inv: Mat4 = m.inverse(); - assert_eq!( - m.compose(&m_inv), - Mat4x4::>::identity() - ); - assert_eq!( - m_inv.compose(&m), - Mat4x4::>::identity() - ); + assert_eq!(m.compose(&m_inv), Mat4::identity()); + assert_eq!(m_inv.compose(&m), Mat4::identity()); } #[test] fn inverse_reverts_transform() { - let m: Mat4x4 = scale3(1.0, 2.0, 0.5) + let m: Mat4 = scale3(1.0, 2.0, 0.5) .then(&translate3(-2.0, 3.0, 0.0)) .to(); - let m_inv: Mat4x4 = m.inverse(); + let m_inv: Mat4 = m.inverse(); - let v1: Vec3 = vec3(1.0, -2.0, 3.0); - let v2: Vec3 = vec3(2.0, 0.0, -2.0); + let v1: Vec3 = vec3(1.0, -2.0, 3.0); + let v2: Vec3 = vec3(2.0, 0.0, -2.0); assert_eq!(m_inv.apply(&m.apply(&v1)), v1); assert_eq!(m.apply(&m_inv.apply(&v2)), v2); diff --git a/core/src/math/point.rs b/core/src/math/point.rs index def997b4..7d8e3e88 100644 --- a/core/src/math/point.rs +++ b/core/src/math/point.rs @@ -34,16 +34,21 @@ impl Point { } /// Returns a point with value equal to `self` but in space `S`. - // TODO Cannot be const (yet?) due to E0493 :( #[inline] - pub fn to(self) -> Point { - Point(self.0, Pd) + pub const fn to(self) -> Point + where + R: Copy, // TODO Needed for now due to E0493 + { + Point::new(self.0) } /// Returns the vector equivalent to `self`. - // TODO Cannot be const (yet?) due to E0493 :( + #[inline] - pub fn to_vec(self) -> Vector { + pub const fn to_vec(self) -> Vector + where + R: Copy, // TODO Needed for now due to E0493 + { Vector::new(self.0) } } @@ -154,48 +159,41 @@ impl Point<[f32; N], Real> { } } -impl Point> -where - R: Index, - Sc: Copy, -{ +impl Point<[Sc; 2], Real<2, B>> { /// Returns the x component of `self`. #[inline] - pub fn x(&self) -> Sc { + pub const fn x(&self) -> Sc { self.0[0] } /// Returns the y component of `self`. #[inline] - pub fn y(&self) -> Sc { + pub const fn y(&self) -> Sc { self.0[1] } } impl Point2 { /// Converts `self` into a `Point3`, with z equal to 0. - pub fn to_pt3(self) -> Point3 { + #[inline] + pub const fn to_pt3(self) -> Point3 { pt3(self.x(), self.y(), 0.0) } } -impl Point> -where - R: Index, - Sc: Copy, -{ +impl Point<[Sc; 3], Real<3, B>> { /// Returns the x component of `self`. #[inline] - pub fn x(&self) -> Sc { + pub const fn x(&self) -> Sc { self.0[0] } /// Returns the y component of `self`. #[inline] - pub fn y(&self) -> Sc { + pub const fn y(&self) -> Sc { self.0[1] } /// Returns the z component of `self`. #[inline] - pub fn z(&self) -> Sc { + pub const fn z(&self) -> Sc { self.0[2] } } diff --git a/core/src/math/rand.rs b/core/src/math/rand.rs index 3917c353..5b2fbf9a 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::{Angle, Color, Point, Point2, Point3, Vec2, Vec3, Vector, rads}; // // Traits and types @@ -11,7 +11,7 @@ use super::{Point, Point2, Point3, Vec2, Vec3, Vector}; pub type DefaultRng = Xorshift64; /// Trait for generating values sampled from a probability distribution. -pub trait Distrib: Clone { +pub trait Distrib { /// The type of the elements of the sample space of `Self`, also called /// "outcomes". type Sample; @@ -35,19 +35,19 @@ pub trait Distrib: Clone { /// ``` /// use retrofire_core::math::rand::*; /// - /// // Simulate rolling a six-sided die + /// // Simulate rolling a six-sided die three times /// let rng = &mut DefaultRng::default(); - /// let mut iter = Uniform(1..7).samples(rng); + /// let mut iter = Uniform(1u32..7).samples(rng); /// - /// assert_eq!(iter.next(), Some(3)); + /// assert_eq!(iter.next(), Some(1)); /// assert_eq!(iter.next(), Some(2)); /// assert_eq!(iter.next(), Some(4)); /// ``` - fn samples( - &self, - rng: &mut DefaultRng, - ) -> impl Iterator { - Iter(self.clone(), rng) + fn samples<'a>( + &'a self, + rng: &'a mut DefaultRng, + ) -> impl Iterator + 'a { + Samples(self, rng) } } @@ -103,9 +103,11 @@ pub struct PointsInUnitBall; #[derive(Copy, Clone, Debug)] pub struct Bernoulli(pub f32); -/// Iterator returned by the [`Distrib::samples()`] method. +/// An infinite iterator of pseudorandom values sampled from a distribution. +/// +/// This type is returned by [`Distrib::samples`]. #[derive(Copy, Clone, Debug)] -struct Iter(D, R); +struct Samples(D, R); // // Inherent impls @@ -127,10 +129,10 @@ impl Xorshift64 { /// ``` /// use retrofire_core::math::rand::Xorshift64; /// - /// let mut g = Xorshift64::from_seed(123); - /// assert_eq!(g.next_bits(), 133101616827); - /// assert_eq!(g.next_bits(), 12690785413091508870); - /// assert_eq!(g.next_bits(), 7516749944291143043); + /// let mut g = Xorshift64::from_seed(11223344556677889900); + /// assert_eq!(g.next_bits(), 7782624861773764242); + /// assert_eq!(g.next_bits(), 6203733934162558527); + /// assert_eq!(g.next_bits(), 13009646309496342147); /// ``` /// /// # Panics @@ -183,10 +185,7 @@ impl Xorshift64 { // Foreign trait impls // -/// An infinite iterator of pseudorandom values sampled from a distribution. -/// -/// This type is returned by [`Distrib::samples`]. -impl Iterator for Iter { +impl Iterator for Samples { type Item = D::Sample; /// Returns the next pseudorandom sample from this iterator. @@ -217,7 +216,15 @@ impl Default for Xorshift64 { // Local trait impls // -/// Uniformly distributed integers. +impl Distrib for &D { + type Sample = D::Sample; + + fn sample(&self, rng: &mut DefaultRng) -> Self::Sample { + (*self).sample(rng) + } +} + +/// Uniformly distributed signed integers. impl Distrib for Uniform { type Sample = i32; @@ -228,11 +235,10 @@ 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 = (-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 +246,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 { @@ -268,6 +326,63 @@ impl Distrib for Uniform { } } +/// Uniformly distributed angles. +impl Distrib for Uniform { + type Sample = Angle; + + /// Returns a uniformly distributed `Angle` in the range. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{degs, rand::*}; + /// let rng = &mut DefaultRng::default(); + /// + /// // Angles in the interval [45°, 90°] + /// let range = Uniform(degs(45.0)..degs(90.0)); + /// let mut iter = range.samples(rng); + /// assert_eq!(iter.next(), Some(degs(71.9309))); + /// assert_eq!(iter.next(), Some(degs(50.205833))); + /// assert_eq!(iter.next(), Some(degs(88.19318))); + /// ``` + fn sample(&self, rng: &mut DefaultRng) -> Angle { + let Range { start, end } = self.0; + rads((start.to_rads()..end.to_rads()).sample(rng)) + } +} + +impl Distrib for Range +where + Uniform: Distrib, +{ + type Sample = S; + + fn sample(&self, rng: &mut DefaultRng) -> Self::Sample { + Uniform(self.clone()).sample(rng) + } +} + +impl Distrib for [T] { + type Sample = T; + + /// Returns a random element from the slice *with replacement*. + /// + /// That is, the elements returned are not removed from the slice and may + /// be returned again by another call to this method. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::rand::{DefaultRng, Distrib}; + /// + /// let pets = ["cat", "dog", "bird", "fish", "turtle"]; + /// let rng = &mut DefaultRng::default(); + /// + /// assert_eq!(pets.sample(rng), "cat"); + /// ``` + fn sample(&self, rng: &mut DefaultRng) -> Self::Sample { + self[Uniform(0..self.len()).sample(rng)].clone() + } +} + impl Distrib for Uniform<[T; N]> where T: Copy, @@ -329,6 +444,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 +622,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 b7bfb8b5..32405a84 100644 --- a/core/src/math/space.rs +++ b/core/src/math/space.rs @@ -188,6 +188,7 @@ where Iter { val: self, step, n } } + #[inline] fn dv_dt(&self, other: &Self, recip_dt: f32) -> Self::Diff { other.sub(self).mul(recip_dt) } diff --git a/core/src/math/spline.rs b/core/src/math/spline.rs index 04d99b36..af2f55e5 100644 --- a/core/src/math/spline.rs +++ b/core/src/math/spline.rs @@ -1,10 +1,11 @@ //! Bézier curves and splines. use alloc::vec::Vec; -use core::{array, fmt::Debug}; +use core::array; use crate::geom::{Polyline, Ray}; -use crate::math::{Affine, Lerp, Linear, Parametric}; + +use super::{Affine, Lerp, Linear, Parametric, Vector}; /// A cubic Bézier curve, defined by four control points. /// @@ -234,7 +235,33 @@ where (t2, array::from_fn(|k| self.0[idx + k].clone())) } - /// Approximates `self` as a sequence of line segments. + /// Approximates `self` as a chain of line segments. + /// + /// Recursively subdivides the curve into two half-curves, stopping once + /// the approximation error is less than `error`. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{BezierSpline, vec2, Vec2}; + /// + /// let curve = BezierSpline::::new( + /// &[vec2(0.0, 0.0), vec2(0.0, 1.0), vec2(1.0, 1.0), vec2(1.0, 0.0)] + /// ); + /// let approx = curve.approximate(0.01); + /// assert_eq!(approx.0.len(), 17); + /// ``` + /// + /// # Panics + /// If `err` ≤ 0. + pub fn approximate(&self, error: f32) -> Polyline + where + T: Affine>, + { + assert!(error > 0.0); + self.approximate_with(&|e: &T::Diff| e.len_sqr() < error * error) + } + + /// Approximates `self` as a chain of line segments. /// /// Recursively subdivides the curve into two half-curves, stopping once /// the approximation error is small enough, as determined by the `halt` @@ -266,10 +293,13 @@ where /// let curve = BezierSpline::::new( /// &[vec2(0.0, 0.0), vec2(0.0, 1.0), vec2(1.0, 1.0), vec2(1.0, 0.0)] /// ); - /// let approx = curve.approximate(|err| err.len_sqr() < 0.01*0.01); + /// let approx = curve.approximate_with(|err| err.len_sqr() < 0.01 * 0.01); /// assert_eq!(approx.0.len(), 17); /// ``` - pub fn approximate(&self, halt: impl Fn(&T::Diff) -> bool) -> Polyline { + pub fn approximate_with( + &self, + halt: impl Fn(&T::Diff) -> bool, + ) -> Polyline { let len = self.0.len(); let mut res = Vec::with_capacity(3 * len); self.do_approx(0.0, 1.0, 10 + len.ilog2(), &halt, &mut res); @@ -285,13 +315,13 @@ where halt: &impl Fn(&T::Diff) -> bool, accum: &mut Vec, ) { - let mid = a.lerp(&b, 0.5); + let mid = a.midpoint(b); let ap = self.eval(a); let bp = self.eval(b); let real = self.eval(mid); - let approx = ap.lerp(&bp, 0.5); + let approx = ap.midpoint(&bp); if max_dep == 0 || halt(&real.sub(&approx)) { accum.push(ap); diff --git a/core/src/math/vary.rs b/core/src/math/vary.rs index 27976be8..f23b5712 100644 --- a/core/src/math/vary.rs +++ b/core/src/math/vary.rs @@ -107,26 +107,33 @@ impl Vary for (T, U) { type Iter = Iter; type Diff = (T::Diff, U::Diff); + #[inline] fn vary(self, step: Self::Diff, n: Option) -> Self::Iter { Iter { val: self, step, n } } + + #[inline] fn dv_dt(&self, other: &Self, recip_dt: f32) -> Self::Diff { ( self.0.dv_dt(&other.0, recip_dt), self.1.dv_dt(&other.1, recip_dt), ) } + + #[inline] fn step(&self, (d0, d1): &Self::Diff) -> Self { (self.0.step(d0), self.1.step(d1)) } } impl ZDiv for (T, U) { + #[inline] fn z_div(self, z: f32) -> Self { (self.0.z_div(z), self.1.z_div(z)) } } impl ZDiv for f32 { + #[inline] fn z_div(self, z: f32) -> Self { self / z } diff --git a/core/src/math/vec.rs b/core/src/math/vec.rs index 84703795..c5412f8d 100644 --- a/core/src/math/vec.rs +++ b/core/src/math/vec.rs @@ -5,7 +5,7 @@ use core::{ array, fmt::{Debug, Formatter}, - iter::Sum, + iter::{Sum, zip}, marker::PhantomData as Pd, ops::{Add, Div, Index, IndexMut, Mul, Neg, Sub}, ops::{AddAssign, DivAssign, MulAssign, SubAssign}, @@ -71,12 +71,20 @@ pub const fn vec3(x: Sc, y: Sc, z: Sc) -> Vector<[Sc; 3], Real<3, B>> { /// /// # Examples /// ``` -/// use retrofire_core::math::{vec3, Vec3, splat}; +/// use retrofire_core::math::{Vec3, vec3, splat}; /// let v: Vec3 = splat(1.23); /// assert_eq!(v, vec3(1.23, 1.23, 1.23)); +/// +/// // You can also use from/into: +/// let v: Vec3 = 2.34.into(); +/// assert_eq!(v, vec3(2.34, 2.34, 2.34)); +/// ``` #[inline] -pub fn splat(s: Sc) -> Vector<[Sc; DIM], Sp> { - array::from_fn(|_| s.clone()).into() // Use array::repeat once stable +pub const fn splat( + s: Sc, +) -> Vector<[Sc; DIM], Sp> { + // TODO once array::repeat is stable, loosen Sc: Copy to Clone if needed + Vector::new([s; DIM]) } // @@ -96,16 +104,20 @@ impl Vector { /// to another in order to make types match. One use case is /// to cast a "generic" vector returned by one of the constructor /// functions to a more specific space. - // TODO Cannot be const (yet?) due to E0493 :( #[inline] - pub fn to(self) -> Vector { + pub const fn to(self) -> Vector + where + R: Copy, // Required for now due to E0493 + { Vector::new(self.0) } /// Returns the affine point equivalent to `self`. - // TODO Cannot be const (yet?) due to E0493 :( #[inline] - pub fn to_pt(self) -> Point { + pub const fn to_pt(self) -> Point + where + R: Copy, // Required for now due to E0493 + { Point::new(self.0) } } @@ -186,8 +198,8 @@ where { /// Returns the length of `self`, squared. /// - /// This avoids taking the square root in cases it's not needed and works with scalars for - /// which a square root is not defined. + /// This avoids taking the square root in cases it's not needed, + /// and works with scalars for which a square root is not defined. #[inline] pub fn len_sqr(&self) -> Sc { self.dot(self) @@ -196,9 +208,7 @@ where /// Returns the dot product of `self` and `other`. #[inline] pub fn dot(&self, other: &Self) -> Sc { - self.0 - .iter() - .zip(&other.0) + zip(&self.0, &other.0) .map(|(a, b)| a.mul(*b)) .fold(Sc::zero(), |acc, x| acc.add(&x)) } @@ -233,7 +243,7 @@ where other.mul(self.scalar_project(other)) } - /// Reflects `self` across a line. + /// Reflects `self` across an axis. /// /// # Examples /// ``` @@ -244,12 +254,12 @@ where /// /// assert_eq!(v.reflect(axis), vec3(2.0, 3.0, 1.0)); /// ``` - pub fn reflect(self, other: Self) -> Self + pub fn reflect(self, axis: Self) -> Self where Sc: Div, { - let proj_on_other = self.vector_project(&other); - proj_on_other + proj_on_other - self + let proj_on_axis = self.vector_project(&axis); + proj_on_axis + proj_on_axis - self } } @@ -291,24 +301,20 @@ impl Vector<[Sc; N], Sp> { } } -impl Vector> -where - R: Index, - Sc: Copy, -{ +impl Vector<[Sc; 2], Real<2, B>> { /// Returns the x component of `self`. #[inline] - pub fn x(&self) -> Sc { + pub const fn x(&self) -> Sc { self.0[0] } /// Returns the y component of `self`. #[inline] - pub fn y(&self) -> Sc { + pub const fn y(&self) -> Sc { self.0[1] } } -// TODO Make more general - requires a "Scalar::one()" method +// TODO These should be also impl'd for Vec2i... impl Vec2 { /// Unit vector codirectional with the positive x-axis. pub const X: Self = vec2(1.0, 0.0); @@ -316,8 +322,8 @@ impl Vec2 { /// Unit vector codirectional with the positive y-axis. pub const Y: Self = vec2(0.0, 1.0); - /// Converts `self` into a `Vec3`, with z set to 0. - pub fn to_vec3(self) -> Vec3 { + /// Converts `self` to a `Vec3`, with z set to 0. + pub const fn to_vec3(self) -> Vec3 { vec3(self.x(), self.y(), 0.0) } @@ -331,7 +337,7 @@ impl Vec2 { /// assert_eq!(::Y.perp(), -Vec2::X); /// ``` #[inline] - pub fn perp(self) -> Self { + pub const fn perp(self) -> Self { vec2(-self.y(), self.x()) } @@ -373,24 +379,23 @@ impl Vec2 { } } -impl Vector> +impl Vector<[Sc; 3], Real<3, B>> where - R: Index, Sc: Copy, { /// Returns the x component of `self`. #[inline] - pub fn x(&self) -> Sc { + pub const fn x(&self) -> Sc { self.0[0] } /// Returns the y component of `self`. #[inline] - pub fn y(&self) -> Sc { + pub const fn y(&self) -> Sc { self.0[1] } /// Returns the z component of `self`. #[inline] - pub fn z(&self) -> Sc { + pub const fn z(&self) -> Sc { self.0[2] } @@ -423,7 +428,6 @@ where pub fn cross(&self, other: &Self) -> Self where Sc: Linear, - [Sc; 3]: Into, { let (s, o) = (self, other); [ @@ -435,7 +439,7 @@ where } } -// TODO Make more general - requires a "Scalar::one()" method +// TODO These should be also impl'd for Vec3i... impl Vec3 { /// Unit vector codirectional with the positive x-axis. pub const X: Self = vec3(1.0, 0.0, 0.0); @@ -447,29 +451,25 @@ impl Vec3 { pub const Z: Self = vec3(0.0, 0.0, 1.0); } -impl Vector -where - R: Index, - Sc: Copy, -{ +impl Vector<[Sc; 4], Proj3> { /// Returns the x component of `self`. #[inline] - pub fn x(&self) -> Sc { + pub const fn x(&self) -> Sc { self.0[0] } /// Returns the y component of `self`. #[inline] - pub fn y(&self) -> Sc { + pub const fn y(&self) -> Sc { self.0[1] } /// Returns the z component of `self`. #[inline] - pub fn z(&self) -> Sc { + pub const fn z(&self) -> Sc { self.0[2] } /// Returns the w component of `self`. #[inline] - pub fn w(&self) -> Sc { + pub const fn w(&self) -> Sc { self.0[3] } } @@ -490,12 +490,12 @@ where #[inline] fn add(&self, other: &Self) -> Self { - // TODO Profile performance of array::from_fn - Self(array::from_fn(|i| self.0[i].add(&other.0[i])), Pd) + // TODO Profile performance of zip_map + self.zip_map(*other, |a, b| a.add(&b)) } #[inline] fn sub(&self, other: &Self) -> Self { - Self(array::from_fn(|i| self.0[i].sub(&other.0[i])), Pd) + self.zip_map(*other, |a, b| a.sub(&b)) } } @@ -553,7 +553,7 @@ impl Clone for Vector { impl Default for Vector> { fn default() -> Self { - Self(R::default(), Pd) + Self::new(R::default()) } } @@ -575,11 +575,11 @@ impl Debug for Vector { impl From for Vector { #[inline] fn from(repr: R) -> Self { - Self(repr, Pd) + Self::new(repr) } } -impl From for Vector<[Sc; DIM], Sp> { +impl From for Vector<[Sc; DIM], Sp> { /// Returns a vector with all components equal to `scalar`. /// /// This operation is also called "splat" or "broadcast". @@ -764,13 +764,13 @@ mod tests { use super::*; - pub const fn vec2(x: S, y: S) -> Vector<[S; 2], Real<2>> { + const fn vec2(x: S, y: S) -> Vector<[S; 2], Real<2>> { super::vec2(x, y) } - pub const fn vec3(x: S, y: S, z: S) -> Vector<[S; 3], Real<3>> { + const fn vec3(x: S, y: S, z: S) -> Vector<[S; 3], Real<3>> { super::vec3(x, y, z) } - pub const fn vec4(x: S, y: S, z: S, w: S) -> Vector<[S; 4], Real<4>> { + const fn vec4(x: S, y: S, z: S, w: S) -> Vector<[S; 4], Real<4>> { Vector::new([x, y, z, w]) } diff --git a/core/src/render.rs b/core/src/render.rs index c71eb70a..d30f117c 100644 --- a/core/src/render.rs +++ b/core/src/render.rs @@ -6,13 +6,12 @@ //! geometric shapes such as triangles. use alloc::vec::Vec; -use core::fmt::Debug; +use core::{fmt::Debug, ops::DerefMut}; use crate::geom::Vertex; use crate::math::{ - Mat4x4, Vary, + Mat4, ProjVec3, Vary, mat::{RealToProj, RealToReal}, - vec::ProjVec3, }; use self::{ @@ -26,6 +25,7 @@ pub use self::{ cam::Camera, clip::Clip, ctx::Context, + raster::Frag, shader::{FragmentShader, VertexShader}, stats::Stats, target::{Colorbuf, Framebuf, Target}, @@ -71,7 +71,7 @@ pub trait Render { } /// Transforms the argument from NDC to screen space. - fn to_screen(clip: Self::Clip, tf: &Mat4x4) -> Self::Screen; + fn to_screen(clip: Self::Clip, tf: &Mat4) -> Self::Screen; /// Rasterizes the argument by calling the function for each scanline. fn rasterize)>(scr: Self::Screen, scanline_fn: F); @@ -127,19 +127,20 @@ impl Shader for S where } /// Renders the given primitives into `target`. -pub fn render( +pub fn render( prims: impl AsRef<[Prim]>, verts: impl AsRef<[Vtx]>, shader: &Shd, uniform: Uni, - to_screen: Mat4x4, - target: &mut impl Target, + to_screen: Mat4, + mut target: &mut Tgt, ctx: &Context, ) where Prim: Render + Clone, [::Clip]: Clip, Var: Vary, Shd: Shader, + Tgt: Target, { // 0. Preparations let verts = verts.as_ref(); @@ -194,7 +195,9 @@ pub fn render( // 4. Fragment shader and rasterization Prim::rasterize(prim, |scanline| { // Convert to fragments, shade, and draw to target - stats.frags += target.rasterize(scanline, shader, ctx); + stats.frags += target + .deref_mut() + .rasterize(scanline, shader, ctx); }); } *ctx.stats.borrow_mut() += stats.finish(); diff --git a/core/src/render/batch.rs b/core/src/render/batch.rs index 06e27eb0..072e6b98 100644 --- a/core/src/render/batch.rs +++ b/core/src/render/batch.rs @@ -1,14 +1,14 @@ //! Builder for setting up geometry for rendering. use alloc::vec::Vec; -use core::borrow::Borrow; +use core::{borrow::Borrow, cell::RefCell, ops::DerefMut}; use crate::{ geom::{Mesh, Tri, Vertex3}, - math::{Mat4x4, Vary}, + math::{Mat4, Vary}, }; -use super::{Clip, Context, NdcToScreen, Render, Shader, Target}; +use super::{Clip, Context, Ndc, Render, Screen, Shader, Target}; /// A builder for rendering a chunk of geometry as a batch. /// @@ -35,7 +35,7 @@ pub struct Batch { verts: Vec, uniform: Uni, shader: Shd, - viewport: Mat4x4, + viewport: Mat4, target: Tgt, ctx: Ctx, } @@ -104,7 +104,7 @@ impl Batch { } /// Sets the viewport matrix. - pub fn viewport(self, viewport: Mat4x4) -> Self { + pub fn viewport(self, viewport: Mat4) -> Self { update!(viewport; self verts prims uniform shader target ctx) } @@ -123,7 +123,9 @@ impl Batch { } } -impl Batch { +impl + Batch, Ctx> +{ /// Renders this batch of geometry. #[rustfmt::skip] pub fn render(&mut self) @@ -142,8 +144,8 @@ impl Batch { } = self; super::render( - prims, verts, shader, *uniform, *viewport, *target, - (*ctx).borrow(), + prims, verts, shader, *uniform, *viewport, + target.borrow_mut().deref_mut(), (*ctx).borrow(), ); } } diff --git a/core/src/render/cam.rs b/core/src/render/cam.rs index e87f4f1a..693049c8 100644 --- a/core/src/render/cam.rs +++ b/core/src/render/cam.rs @@ -7,20 +7,17 @@ use crate::math::{ Angle, Vec3, orient_z, rotate_x, rotate_y, spherical, translate, turns, }; use crate::math::{ - Apply, Lerp, Mat4x4, Point3, SphericalVec, Vary, mat::RealToReal, + Lerp, Mat4, Point3, ProjMat3, SphericalVec, Vary, mat::RealToReal, orthographic, perspective, pt2, viewport, }; use crate::util::{Dims, rect::Rect}; -use super::{ - Clip, Context, NdcToScreen, RealToProj, Render, Shader, Target, View, - ViewToProj, World, WorldToView, -}; +use super::{Clip, Context, Ndc, Render, Screen, Shader, Target, View, World}; /// Trait for different modes of camera motion. pub trait Transform { /// Returns the current world-to-view matrix. - fn world_to_view(&self) -> Mat4x4; + fn world_to_view(&self) -> Mat4; } /// Camera field of view. @@ -61,9 +58,9 @@ pub struct Camera { /// Viewport width and height. pub dims: Dims, /// Projection matrix. - pub project: Mat4x4, + pub project: ProjMat3, /// Viewport matrix. - pub viewport: Mat4x4, + pub viewport: Mat4, } /// First-person camera transform. @@ -199,7 +196,7 @@ impl Camera { impl Camera { /// Returns the composed camera and projection matrix. - pub fn world_to_project(&self) -> Mat4x4> { + pub fn world_to_project(&self) -> ProjMat3 { self.transform.world_to_view().then(&self.project) } @@ -208,7 +205,7 @@ impl Camera { &self, prims: impl AsRef<[Prim]>, verts: impl AsRef<[Vtx]>, - to_world: &Mat4x4>, + to_world: &Mat4, shader: &Shd, uniform: Uni, target: &mut impl Target, @@ -216,7 +213,7 @@ impl Camera { ) where Prim: Render + Clone, [::Clip]: Clip, - Shd: for<'a> Shader>, Uni)>, + Shd: for<'a> Shader, Uni)>, { let tf = to_world.then(&self.world_to_project()); @@ -272,7 +269,7 @@ impl FirstPerson { let up = Vec3::Y; let right = up.cross(&fwd); - let to_world = Mat4x4::from_linear(right, up, fwd); + let to_world = Mat4::from_linear(right, up, fwd); self.pos += to_world.apply(&delta); } } @@ -332,7 +329,7 @@ impl Orbit { #[cfg(feature = "fp")] impl Transform for FirstPerson { - fn world_to_view(&self) -> Mat4x4 { + fn world_to_view(&self) -> Mat4 { let &Self { pos, heading, .. } = self; let fwd_move = az_alt(heading.az(), turns(0.0)).to_cart(); let fwd = heading.to_cart(); @@ -348,7 +345,7 @@ impl Transform for FirstPerson { #[cfg(feature = "fp")] impl Transform for Orbit { - fn world_to_view(&self) -> Mat4x4 { + fn world_to_view(&self) -> Mat4 { // TODO Figure out how to do this with orient //let fwd = self.dir.to_cart().normalize(); //let o = orient_z(fwd, Vec3::X - 0.1 * Vec3::Z); @@ -363,8 +360,8 @@ impl Transform for Orbit { } } -impl Transform for Mat4x4 { - fn world_to_view(&self) -> Mat4x4 { +impl Transform for Mat4 { + fn world_to_view(&self) -> Mat4 { *self } } diff --git a/core/src/render/clip.rs b/core/src/render/clip.rs index be28523c..040c0008 100644 --- a/core/src/render/clip.rs +++ b/core/src/render/clip.rs @@ -16,10 +16,11 @@ use alloc::vec::Vec; use core::{iter::zip, mem::swap}; -use view_frustum::{outcode, status}; use crate::geom::{Edge, Tri, Vertex, vertex}; -use crate::math::{Lerp, vec::ProjVec3}; +use crate::math::{Lerp, ProjVec3}; + +use view_frustum::{outcode, status}; /// Trait for types that can be [clipped][self] against convex volumes. /// @@ -236,6 +237,7 @@ pub mod view_frustum { /// is inside the plane, and 1 otherwise. It is used to determine whether /// a primitive is fully inside, partially inside, or fully outside the /// frustum. + #[inline] pub fn outcode(pt: &ClipVec) -> u8 { PLANES.iter().map(|p| p.outcode(pt)).sum() } @@ -300,6 +302,7 @@ pub fn clip_simple_polygon<'a, A: Lerp + Clone>( } impl ClipVert { + #[inline] pub fn new(Vertex { pos, attrib }: Vertex) -> Self { let outcode = outcode(&pos); Self { pos, attrib, outcode } diff --git a/core/src/render/prim.rs b/core/src/render/prim.rs index f91d3e51..7bf6de7c 100644 --- a/core/src/render/prim.rs +++ b/core/src/render/prim.rs @@ -1,10 +1,10 @@ //! Render impls for primitives and related items. use crate::geom::{Edge, Tri, Vertex, Winding}; -use crate::math::{Apply, Mat4x4, Vary, pt3, vary::ZDiv}; +use crate::math::{Mat4, Vary, pt3, vary::ZDiv}; use super::{ - NdcToScreen, Render, + Ndc, Render, Screen, clip::ClipVert, raster::{Scanline, ScreenPt, line, tri_fill}, }; @@ -28,7 +28,7 @@ impl Render for Tri { fn to_screen( clip: Tri>, - tf: &Mat4x4, + tf: &Mat4, ) -> Self::Screen { Tri(to_screen(clip.0, tf)) } @@ -49,7 +49,7 @@ impl Render for Edge { Edge(vs[i].clone(), vs[j].clone()) } - fn to_screen(e: Self::Clip, tf: &Mat4x4) -> Self::Screen { + fn to_screen(e: Self::Clip, tf: &Mat4) -> Self::Screen { let [a, b] = to_screen([e.0, e.1], tf); Edge(a, b) } @@ -61,7 +61,7 @@ impl Render for Edge { pub fn to_screen( vs: [ClipVert; N], - tf: &Mat4x4, + tf: &Mat4, ) -> [Vertex; N] { vs.map(|v| { let [x, y, _, w] = v.pos.0; diff --git a/core/src/render/scene.rs b/core/src/render/scene.rs index 4625827a..85edb5da 100644 --- a/core/src/render/scene.rs +++ b/core/src/render/scene.rs @@ -1,12 +1,12 @@ -use crate::geom::{Mesh, vertex}; -use crate::math::{ - Mat4x4, Point3, - mat::{Apply, RealToProj}, - pt3, splat, +use core::fmt::{self, Debug, Formatter}; + +use crate::{ + geom::{Mesh, vertex}, + math::{Mat4, Point3, ProjMat3, pt3}, }; use super::{ - Model, ModelToWorld, + Model, World, clip::{ClipVert, Status, view_frustum}, }; @@ -14,25 +14,25 @@ use super::{ pub struct Obj { pub geom: Mesh, pub bbox: BBox, - pub tf: Mat4x4, + pub tf: Mat4, } // TODO Decide whether upper bound is inclusive or exclusive // TODO Needs to be more generic to work with clip points -#[derive(Copy, Clone, Debug, PartialEq)] -pub struct BBox(pub Point3, pub Point3); +#[derive(Copy, Clone, PartialEq)] +pub struct BBox(pub Point3, pub Point3); impl Obj { pub fn new(geom: Mesh) -> Self { - Self::with_transform(geom, Mat4x4::identity()) + Self::with_transform(geom, Mat4::identity()) } - pub fn with_transform(geom: Mesh, tf: Mat4x4) -> Self { + pub fn with_transform(geom: Mesh, tf: Mat4) -> Self { let bbox = BBox::of(&geom); Self { geom, bbox, tf } } } -impl BBox { +impl BBox { pub fn of(mesh: &Mesh) -> Self { mesh.verts.iter().map(|v| &v.pos).collect() } @@ -78,7 +78,7 @@ impl BBox { /// geometry needs no clipping or culling. If the return value is /// `Clipped`, the box and the geometry are *potentially* visible and /// more fine-grained culling is required. - pub fn visibility(&self, tf: &Mat4x4>) -> Status { + pub fn visibility(&self, tf: &ProjMat3) -> Status { view_frustum::status( &self .verts() @@ -87,6 +87,15 @@ impl BBox { } } +impl Debug for BBox { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_tuple("BBox") + .field(&self.0) + .field(&self.1) + .finish() + } +} + impl Default for Obj { /// Returns an empty `Obj`. fn default() -> Self { @@ -98,26 +107,23 @@ impl Default for Obj { } } -impl Default for BBox { +impl Default for BBox { /// Returns an empty `BBox`. fn default() -> Self { - BBox( - splat(f32::INFINITY).to_pt(), - splat(f32::NEG_INFINITY).to_pt(), - ) + BBox([f32::INFINITY; 3].into(), [f32::NEG_INFINITY; 3].into()) } } -impl<'a, B: Default> Extend<&'a Point3> for BBox { +impl<'a, B> Extend<&'a Point3> for BBox { fn extend>>(&mut self, it: I) { it.into_iter().for_each(|pt| self.extend(pt)); } } -impl<'a, B: Default> FromIterator<&'a Point3> for BBox { +impl<'a, B> FromIterator<&'a Point3> for BBox { fn from_iter>>(it: I) -> Self { let mut bbox = BBox::default(); - it.into_iter().for_each(|pt| bbox.extend(pt)); + Extend::extend(&mut bbox, it); bbox } } diff --git a/core/src/render/shader.rs b/core/src/render/shader.rs index 9c9708d4..e67e3f99 100644 --- a/core/src/render/shader.rs +++ b/core/src/render/shader.rs @@ -16,10 +16,10 @@ use crate::{ geom::Vertex, - math::{Color4, vec::ProjVec3}, + math::{Color4, ProjVec3}, }; -use super::raster::Frag; +use super::Frag; /// Trait for vertex shaders, used to transform vertices and perform other /// per-vertex computations. diff --git a/core/src/render/target.rs b/core/src/render/target.rs index a979058e..ae7d8bdf 100644 --- a/core/src/render/target.rs +++ b/core/src/render/target.rs @@ -4,6 +4,8 @@ //! and possible auxiliary buffers. Special render targets can be used, //! for example, for visibility or occlusion computations. +use core::cell::RefCell; + use crate::math::{Color3, Color4, Vary}; use crate::util::{ buf::{AsMutSlice2, Buf2, MutSlice2}, @@ -53,6 +55,27 @@ impl, F> AsMutSlice2 for Colorbuf { } } +impl Target for &mut T { + fn rasterize>( + &mut self, + sl: Scanline, + fs: &Fs, + ctx: &Context, + ) -> Throughput { + (*self).rasterize(sl, fs, ctx) + } +} +impl Target for &RefCell { + fn rasterize>( + &mut self, + sl: Scanline, + fs: &Fs, + ctx: &Context, + ) -> Throughput { + RefCell::borrow_mut(self).rasterize(sl, fs, ctx) + } +} + impl Target for Framebuf, Dep> where Col: AsMutSlice2, @@ -66,14 +89,8 @@ where fs: &Fs, ctx: &Context, ) -> Throughput { - rasterize_fb( - &mut self.color_buf, - &mut self.depth_buf, - sl, - fs, - Color4::into_pixel, - ctx, - ) + let Self { color_buf, depth_buf } = self; + rasterize_fb(color_buf, depth_buf, sl, fs, Color4::into_pixel, ctx) } } diff --git a/core/src/util/buf.rs b/core/src/util/buf.rs index b77fe163..c73cc905 100644 --- a/core/src/util/buf.rs +++ b/core/src/util/buf.rs @@ -433,13 +433,12 @@ pub mod inner { /// or panics if either x or y is out of bounds. #[inline] fn to_index_strict(&self, x: u32, y: u32) -> usize { - self.to_index_checked(x, y).unwrap_or_else(|| { - let (w, h) = self.dims; - panic!( - "position (x={x}, y={y}) out of bounds (0..{w}, 0..{h})", - ) - }) + match self.to_index_checked(x, y) { + Some(i) => i, + None => out_of_bounds(self.dims, x, y), + } } + /// Returns the linear index corresponding to the coordinates, /// or `None` if x or y is out of bounds. #[inline] @@ -497,26 +496,19 @@ pub mod inner { } } + #[cold] + #[track_caller] + #[inline(never)] + fn out_of_bounds((w, h): Dims, x: u32, y: u32) -> ! { + panic!("position (x={x}, y={y}) out of bounds (0..{w}, 0..{h})",) + } + impl> Inner { /// # Panics /// if `stride < w` or if the slice would overflow `data`. #[rustfmt::skip] - pub(super) fn new(dims @ (w, h): Dims, stride: u32, data: D) -> Self { - assert!(w <= stride, "width ({w}) > stride ({stride})"); - - let len = data.len(); - assert!( - h <= 1 || stride as usize <= len, - "stride ({stride}) > data length ({len})" - ); - assert!(h as usize <= len, "height ({h}) > data length ({len})"); - if h > 0 { - let size = (h - 1) * stride + w; - assert!( - size as usize <= len, - "required size ({size}) > data length ({len})" - ); - } + pub(super) fn new(dims: Dims, stride: u32, data: D) -> Self { + check_preconditions(dims, stride, data.len()); Self { dims, stride, data, _pd: PhantomData } } @@ -566,6 +558,22 @@ pub mod inner { } } + fn check_preconditions((w, h): Dims, stride: u32, len: usize) { + assert!(w <= stride, "width ({w}) > stride ({stride})"); + assert!( + h <= 1 || stride as usize <= len, + "stride ({stride}) > data length ({len})" + ); + assert!(h as usize <= len, "height ({h}) > data length ({len})"); + if h > 0 { + let size = (h - 1) * stride + w; + assert!( + size as usize <= len, + "required size ({size}) > data length ({len})" + ); + } + } + impl> Inner { /// Returns a mutably borrowed rectangular slice of `self`. #[inline] diff --git a/core/src/util/pixfmt.rs b/core/src/util/pixfmt.rs index 4990b076..546330f8 100644 --- a/core/src/util/pixfmt.rs +++ b/core/src/util/pixfmt.rs @@ -16,32 +16,71 @@ pub trait IntoPixel: Sized { } } -/// Eight-bit channels in R,G,B order. +/// Three eight-bit channels in R,G,B order. +/// +/// Each channel has 2^8 = 256 distinct values, and each pixel takes three +/// bytes of memory. #[derive(Copy, Clone, Default)] pub struct Rgb888; -/// 5,6,5-bit channels in R,G,B order. +/// Three channels with 5 bits for R, 6 bits for G, and 5 bits for B. #[derive(Copy, Clone, Default)] pub struct Rgb565; +/// Three 5-bit channels plus one-bit alpha (1 = opaque, 0 = transparent). +/// +/// Each color channel has 2^5 = 32 distinct values, and each pixel takes +/// two bytes of memory. +#[derive(Copy, Clone, Default)] +pub struct Rgba5551; -/// Eight-bit channels in X,R,G,B order, where X is unused. +/// Three eight-bit channels in x,R,G,B order, where x is unused. +/// +/// Each channel has 2^8 = 256 distinct values, and each pixel takes +/// four bytes of memory. #[derive(Copy, Clone, Default)] pub struct Xrgb8888; -/// Eight-bit channels in R,G,B,A order. + +/// Four eight-bit channels in R,G,B,A order. +/// +/// Each channel has 2^8 = 256 distinct values, and each pixel takes +/// four bytes of memory. #[derive(Copy, Clone, Default)] pub struct Rgba8888; -/// Eight-bit channels in A,R,G,B order. + +/// Four eight-bit channels in A,R,G,B order. +/// +/// Each channel has 2^8 = 256 distinct values, and each pixel takes +/// four bytes of memory. #[derive(Copy, Clone, Default)] pub struct Argb8888; -/// Eight-bit channels in B,G,R,A order. + +/// Four eight-bit channels in B,G,R,A order. +/// +/// Each channel has 2^8 = 256 distinct values, and each pixel takes +/// four bytes of memory. #[derive(Copy, Clone, Default)] pub struct Bgra8888; -/// Four-bit channels in R,G,B,A order. +/// Four four-bit channels in R,G,B,A order. +/// +/// Each channel has 2^4 = 16 distinct values, and the number of different +/// RGB values is 2^12 = 4096. Each pixel takes two bytes. #[derive(Copy, Clone, Default)] pub struct Rgba4444; +/// Three channels with 3 bits for R and g and 2 bits for B. +/// +/// Red and green have 2^3 = 8 and blue 2^2 = 4 distinct values. The number of +/// different RGB values is 256, and each pixel takes a single byte. +#[derive(Copy, Clone, Default)] +pub struct Rgb332; + +/// A single 8-bit luminance channel. +#[derive(Copy, Clone, Default)] +pub struct Lum8; + // Impls for Color3 +// Rgb888 impl IntoPixel for Color3 { fn into_pixel(self) -> u32 { let [r, g, b] = self.0; @@ -55,7 +94,9 @@ impl IntoPixel<[u8; 3], Rgb888> for Color3 { } } +// Rgb565 impl IntoPixel for Color3 { + /// Packs `self` into a `u16` in `0bRRRRR_GGGGGG_BBBBB` format. fn into_pixel(self) -> u16 { let [r, g, b] = self.0; (r as u16 >> 3 & 0x1F) << 11 @@ -63,14 +104,24 @@ impl IntoPixel for Color3 { | (b as u16 >> 3 & 0x1F) } } - impl IntoPixel<[u8; 2], Rgb565> for Color3 { + /// Packs `self` into two bytes in `[0bGGG_BBBBB, 0bRRRRR_GGG]` format. + // TODO is this correct? fn into_pixel(self) -> [u8; 2] { let c: u16 = self.into_pixel(); c.to_ne_bytes() } } +// Rgb332 +impl IntoPixel for Color3 { + /// Packs `self` into a single byte in `0bRRR_GGG_BB` format. + fn into_pixel(self) -> u8 { + let [r, g, b] = self.0; + (r & 0b111_000_00) | (g >> 3 & 0b000_111_00) | (b >> 6) + } +} + // Impls for Color4 impl IntoPixel for Color4 @@ -83,6 +134,17 @@ where } } +/* +impl IntoPixel for Color4 +where + Color3: IntoPixel, +{ + fn into_pixel(self) -> T { + self.to_rgb().into_pixel() + } +}*/ + +// Xrgb8888 impl IntoPixel for Color4 { fn into_pixel(self) -> u32 { let [r, g, b, _] = self.0; @@ -90,6 +152,8 @@ impl IntoPixel for Color4 { u32::from_be_bytes([0, r, g, b]) } } + +// Rgba8888 and permutations impl IntoPixel<[u8; 4], Rgba8888> for Color4 { fn into_pixel(self) -> [u8; 4] { self.0 @@ -107,11 +171,8 @@ 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()] - } -} + +// Rgba4444 impl IntoPixel<[u8; 2], Rgba4444> for Color4 { fn into_pixel(self) -> [u8; 2] { let c: u16 = self.into_pixel_fmt(Rgba4444); @@ -125,18 +186,41 @@ impl IntoPixel for Color4 { r << 12 | g << 8 | b << 4 | a } } -impl IntoPixel for Color4 { + +// Rgba 5551 +impl IntoPixel for Color4 { + /// Packs `self` into a `u16` in a `0bRRRRR_GGGGG_BBBBB_A` format. + /// The alpha value `0xFF` is considered opaque, any other value + /// fully transparent. fn into_pixel(self) -> u16 { - self.to_rgb().into_pixel() + let [r, g, b, a] = self.0; + (r as u16 >> 3 & 0x1F) << 11 + | (g as u16 >> 3 & 0x1F) << 6 + | (b as u16 >> 3 & 0x1F) << 1 + | (a == 0xFF) as u16 } } -impl IntoPixel<[u8; 2], Rgb565> for Color4 { +impl IntoPixel<[u8; 2], Rgba5551> for Color4 { fn into_pixel(self) -> [u8; 2] { - let c: u16 = self.into_pixel_fmt(Rgb565); + let c: u16 = self.into_pixel_fmt(Rgba5551); c.to_ne_bytes() } } +// Rgb332 +impl IntoPixel for Color4 { + /// Packs `self` into a single byte in `0bRRR_GGG_BB` format. + fn into_pixel(self) -> u8 { + self.to_rgb().into_pixel() + } +} +impl IntoPixel<[u8; 1], Rgb332> for Color4 { + /// Packs `self` into a single byte in `0bRRR_GGG_BB` format. + fn into_pixel(self) -> [u8; 1] { + [self.into_pixel()] + } +} + #[cfg(test)] mod tests { use crate::math::{rgb, rgba}; @@ -160,6 +244,15 @@ mod tests { assert_eq!(pix, [0b000_00010, 0b01000_001]); } + #[test] + fn color3_to_rgb332() { + let pix: u8 = rgb(0xFF, 0x00, 0xFF).into_pixel(); + assert_eq!(pix, 0b111_000_11, "bits: {pix:b}"); + + let pix: u8 = rgb(0x00, 0x0FF, 0x00).into_pixel(); + assert_eq!(pix, 0b000_111_00, "bits: {pix:b}"); + } + #[test] fn color4_to_rgba8888() { let col = rgba(0x11u8, 0x22, 0x33, 0x44); @@ -199,4 +292,23 @@ mod tests { let pix: u16 = COL4.into_pixel_fmt(Rgba4444); assert_eq!(pix, 0x1234); } + + #[test] + fn color4_to_rgba5551_u16() { + let pix: u16 = rgba(0x40u8, 0x20, 0x10, 0).into_pixel_fmt(Rgba5551); + assert_eq!(pix, 0b01000_00100_00010_0_u16, "bits: {pix:b}"); + let pix: u16 = rgba(0x40u8, 0x20, 0x10, 0x80).into_pixel_fmt(Rgba5551); + assert_eq!(pix, 0b01000_00100_00010_0_u16, "bits: {pix:b}"); + let pix: u16 = rgba(0x40u8, 0x20, 0x10, 0xFF).into_pixel_fmt(Rgba5551); + assert_eq!(pix, 0b01000_00100_00010_1_u16, "bits: {pix:b}"); + } + + #[test] + fn color4_to_rgba5551_2u8() { + let pix: [u8; 2] = rgba(0x40u8, 0x20, 0x10, 0).into_pixel_fmt(Rgba5551); + assert_eq!(pix, [0b00_00010_0, 0b01000_001]); + let pix: [u8; 2] = + rgba(0x40u8, 0x20, 0x10, 0xFF).into_pixel_fmt(Rgba5551); + assert_eq!(pix, [0b00_00010_1, 0b01000_001]); + } } diff --git a/core/tests/rendering.rs b/core/tests/rendering.rs index 24fd3eab..953ac5ad 100644 --- a/core/tests/rendering.rs +++ b/core/tests/rendering.rs @@ -24,7 +24,7 @@ fn textured_quad() { })); let shader = shader::new( - |v: Vertex3<_>, mvp: &Mat4x4| { + |v: Vertex3<_>, mvp: &ProjMat3| { vertex(mvp.apply(&v.pos), v.attrib) }, |frag: Frag<_>| SamplerClamp.sample(&checker, frag.var), diff --git a/demos/Cargo.toml b/demos/Cargo.toml index 3a9f6976..4960e7c7 100644 --- a/demos/Cargo.toml +++ b/demos/Cargo.toml @@ -21,9 +21,8 @@ categories.workspace = true repository.workspace = true [dependencies] -re = { version = "0.4.0-pre4", path = "../core", package = "retrofire-core" } -re-front = { version = "0.4.0-pre4", path = "../front", package = "retrofire-front" } -re-geom = { version = "0.4.0-pre4", path = "../geom", package = "retrofire-geom", features = ["std"] } +re = { version = "0.4.0", path = "..", package = "retrofire" } +re-front = { version = "0.4.0", path = "../front", package = "retrofire-front" } minifb = { version = "0.27.0", optional = true } sdl2 = { version = "0.35.2", optional = true } @@ -37,7 +36,6 @@ sdl2 = ["dep:sdl2", "re-front/sdl2"] [[bin]] name = "crates" required-features = ["sdl2"] -default-features = false [[bin]] name = "curses" 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/src/bin/bezier.rs b/demos/src/bin/bezier.rs index 1f4ec69c..6fe1fc27 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; @@ -31,10 +31,10 @@ fn main() { win.ctx.depth_clear = None; win.run(|Frame { dt, buf, .. }| { + let buf = &mut buf.borrow_mut().color_buf.buf; + // Fade out previous frame a bit - buf.color_buf - .buf - .iter_mut() + buf.iter_mut() .for_each(|c| *c = c.saturating_sub(0x08_08_02)); let rays: Vec> = pos_vels @@ -44,17 +44,17 @@ fn main() { let b = BezierSpline::from_rays(rays); // Stop once error is less than one pixel - let approx = b.approximate(|err| err.len_sqr() < 1.0); + let approx = b.approximate(1.0); for Edge(p0, p1) in approx.edges() { let vs = [p0, p1].map(|p| vertex(p.to_pt3().to(), ())); line(vs, |sl| { - buf.color_buf.buf[sl.y][sl.xs].fill(0xFF_FF_FF); + buf[sl.y][sl.xs].fill(0xFF_FF_FF); }) } let dt = dt.as_secs_f32(); - for (pos, vel) in pos_vels.iter_mut() { + for (pos, vel) in &mut pos_vels { *pos = (*pos + 80.0 * *vel * dt).clamp(&min, &max); let [dx, dy] = &mut vel.0; if pos.x() == min.x() || pos.x() == max.x() { diff --git a/demos/src/bin/crates.rs b/demos/src/bin/crates.rs index fe37a59d..2ff0b47e 100644 --- a/demos/src/bin/crates.rs +++ b/demos/src/bin/crates.rs @@ -2,16 +2,17 @@ use core::ops::ControlFlow::*; use re::prelude::*; -use re::math::color::gray; -use re::render::{ - ModelToProj, cam::FirstPerson, cam::Fov, clip::Status::*, scene::Obj, +use re::core::math::color::gray; +use re::core::render::{ + cam::{FirstPerson, Fov}, + clip::Status::*, + scene::Obj, tex::SamplerClamp, }; -// Try also Rgb565 or Rgba4444 -use re::util::{pixfmt::Rgba8888, pnm::read_pnm}; - -use re_front::sdl2::Window; -use re_geom::solids::{Build, Cube}; +// Try also Rgb565, Rgba4444, or Rgb332 +use re::core::util::{pixfmt::Rgba8888, pnm::read_pnm}; +use re::front::sdl2::Window; +use re::geom::solids::{Build, Cube}; fn main() { let mut win = Window::builder() @@ -26,16 +27,14 @@ fn main() { 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) - }, + |v: Vertex3<_>, mvp: &ProjMat3<_>| vertex(mvp.apply(&v.pos), v.attrib), |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<(Normal3, TexCoord)>, mvp: &Mat4x4| { + |v: Vertex3<(Normal3, TexCoord)>, mvp: &ProjMat3<_>| { vertex(mvp.apply(&v.pos), v.attrib) }, |frag: Frag<(Normal3, TexCoord)>| { @@ -91,20 +90,20 @@ fn main() { let batch = Batch::new() .viewport(cam.viewport) + .target(frame.buf) .context(frame.ctx); // Floor { - let Obj { geom, bbox, tf } = &floor; + let Obj { bbox, tf, geom } = &floor; let model_to_project = tf.then(&world_to_project); if bbox.visibility(&model_to_project) != Hidden { - batch + let mut b = batch .clone() .mesh(geom) - .uniform(&model_to_project) .shader(floor_shader) - .target(&mut frame.buf) - .render(); + .uniform(&model_to_project); + b.render(); } } @@ -131,7 +130,6 @@ fn main() { // pass to render() instead. OTOH then a Frame::batch // helper wouldn't be as useful. Maybe just wrap the // target in a RefCell? - .target(&mut frame.buf) .render(); frame.ctx.stats.borrow_mut().objs.o += 1; diff --git a/demos/src/bin/curses.rs b/demos/src/bin/curses.rs index 3c13d59a..fc102235 100644 --- a/demos/src/bin/curses.rs +++ b/demos/src/bin/curses.rs @@ -4,11 +4,10 @@ use pancurses::*; use re::prelude::*; -use re::render::{ +use re::core::render::{ ctx::DepthSort::BackToFront, raster::Scanline, stats::Throughput, }; - -use re_geom::solids::{Build, Torus}; +use re::geom::solids::{Build, Torus}; struct Win(Window); @@ -47,7 +46,7 @@ fn main() { }; let shader = shader::new( - |v: Vertex3<_>, mvp: &Mat4x4| { + |v: Vertex3<_>, mvp: &ProjMat3| { vertex(mvp.apply(&v.pos), v.attrib) }, |frag: Frag| { @@ -132,7 +131,7 @@ impl Target for Win { | (b / 85 & 0b000_000_11); // Avoid the eight standard colors - self.0.addch(COLOR_PAIR(col.max(8))); + self.0.addch(COLOR_PAIR(col.max(8) as chtype)); } Throughput { i: w, o: w } } diff --git a/demos/src/bin/hello.rs b/demos/src/bin/hello.rs index 4d128ed3..37790783 100644 --- a/demos/src/bin/hello.rs +++ b/demos/src/bin/hello.rs @@ -1,11 +1,11 @@ use std::{env, fmt::Write, ops::ControlFlow::Continue}; use re::prelude::*; -use re::render::{ - Text, - tex::{Atlas, Layout}, + +use re::core::{ + render::{Text, tex::Atlas, tex::Layout}, + util::pnm::parse_pnm, }; -use re::util::pnm::parse_pnm; use re_front::{Frame, dims::SVGA_800_600, minifb::Window}; @@ -31,13 +31,13 @@ fn main() { win.ctx.face_cull = None; let shader = shader::new( - |v: Vertex<_, _>, mvp: &Mat4x4| { + |v: Vertex<_, _>, mvp: &ProjMat3| { vertex(mvp.apply(&v.pos), v.attrib) }, |frag: Frag| text.sample(frag.var).to_rgba(), ); - let vp: Mat4x4> = translate(vec3(0.0, 0.0, 15.0)) + let vp: ProjMat3 = translate(vec3(0.0, 0.0, 15.0)) .to() .then(&perspective(1.0, 4.0 / 3.0, 0.1..1000.0)); diff --git a/demos/src/bin/solids.rs b/demos/src/bin/solids.rs index d6a5dcaa..d828429a 100644 --- a/demos/src/bin/solids.rs +++ b/demos/src/bin/solids.rs @@ -4,12 +4,12 @@ use minifb::{Key, KeyRepeat}; use re::prelude::*; -use re::geom::Polyline; -use re::math::{Apply, color::gray, mat::RealToReal, vec::ProjVec3}; -use re::render::cam::Fov; +use re::core::geom::Polyline; +use re::core::math::{ProjMat3, ProjVec3, color::gray}; +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::parse_obj, solids::*}; // Carousel animation for switching between objects. #[derive(Default)] @@ -29,9 +29,9 @@ impl Carousel { self.new_idx += 1; } } - fn update(&mut self, dt: f32) -> Mat4x4> { + fn update(&mut self, dt: f32) -> Mat4 { let Some(t) = self.t.as_mut() else { - return Mat4x4::identity(); + return Mat4::identity(); }; *t += dt; let t = *t; @@ -63,7 +63,7 @@ fn main() { type VertexIn = Vertex3; type VertexOut = Vertex; - type Uniform<'a> = (&'a Mat4x4, &'a Mat4x4>); + type Uniform<'a> = (&'a ProjMat3, &'a Mat4); fn vtx_shader(v: VertexIn, (mvp, spin): Uniform) -> VertexOut { // Transform vertex normal @@ -100,7 +100,7 @@ fn main() { let carouse = carousel.update(dt.as_secs_f32()); // Compose transform stack - let model_view_project: Mat4x4 = spin + let model_view_project: ProjMat3 = spin .then(&translate) .then(&carouse) .to::() @@ -113,7 +113,7 @@ fn main() { .uniform((&model_view_project, &spin)) .shader(shader) .viewport(cam.viewport) - .target(&mut frame.buf) + .target(frame.buf) .context(&*frame.ctx) .render(); diff --git a/demos/src/bin/sprites.rs b/demos/src/bin/sprites.rs index edf2c08b..290ad058 100644 --- a/demos/src/bin/sprites.rs +++ b/demos/src/bin/sprites.rs @@ -2,9 +2,11 @@ use core::{array::from_fn, ops::ControlFlow::Continue}; use re::prelude::*; -use re::math::color::gray; -use re::math::rand::{Distrib, PointsInUnitBall, Xorshift64}; -use re::render::{Model, ModelToView, ViewToProj, cam::*, render}; +use re::core::math::{ + color::gray, + rand::{Distrib, PointsInUnitBall, Xorshift64}, +}; +use re::core::render::{Model, cam::*, render}; use re_front::minifb::Window; @@ -36,7 +38,7 @@ fn main() { let shader = shader::new( |v: Vertex3>, - (mv, proj): (&Mat4x4, &Mat4x4)| { + (mv, proj): (&Mat4, &ProjMat3)| { let vertex_pos = 0.008 * v.attrib.to_vec3().to(); let view_pos = mv.apply(&v.pos) + vertex_pos; vertex(proj.apply(&view_pos), v.attrib) diff --git a/demos/src/bin/square.rs b/demos/src/bin/square.rs index 4be126cb..09a920aa 100644 --- a/demos/src/bin/square.rs +++ b/demos/src/bin/square.rs @@ -1,9 +1,9 @@ -use std::ops::ControlFlow::*; +use core::ops::ControlFlow::*; use re::prelude::*; -use re::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 @@ -35,9 +35,7 @@ fn main() { })); let shader = shader::new( - |v: Vertex3<_>, mvp: &Mat4x4| { - vertex(mvp.apply(&v.pos), v.attrib) - }, + |v: Vertex3<_>, mvp: &ProjMat3<_>| vertex(mvp.apply(&v.pos), v.attrib), |frag: Frag<_>| SamplerClamp.sample(&checker, frag.var), ); diff --git a/demos/wasm/Cargo.toml b/demos/wasm/Cargo.toml index 9a34f8d6..2693d1db 100644 --- a/demos/wasm/Cargo.toml +++ b/demos/wasm/Cargo.toml @@ -20,13 +20,13 @@ console_error_panic_hook = "0.1.7" [dependencies.re] package = "retrofire-core" -version = "0.4.0-pre4" +version = "0.4.0" path = "../../core" features = ["mm"] [dependencies.re-front] package = "retrofire-front" -version = "0.4.0-pre4" +version = "0.4.0" path = "../../front" features = ["wasm-dev"] default-features = false diff --git a/demos/wasm/src/triangle.rs b/demos/wasm/src/triangle.rs index f72a6185..1b094263 100644 --- a/demos/wasm/src/triangle.rs +++ b/demos/wasm/src/triangle.rs @@ -28,7 +28,7 @@ pub fn start() { let proj = perspective(1.0, 4.0 / 3.0, 0.1..1000.0); let vp = viewport(pt2(8, 8)..pt2(DIMS.0 - 8, DIMS.1 - 8)); - win.run(move |frame| { + win.run(move |mut frame| { let t = frame.t.as_secs_f32(); let mv = rotate_z(rads(t)) @@ -41,15 +41,7 @@ pub fn start() { |f: Frag| f.var.to_color4(), ); - render( - [tri(0, 1, 2)], // - vs, - &sh, - (), - vp, - &mut frame.buf, - frame.ctx, - ); + render([tri(0, 1, 2)], vs, &sh, (), vp, &mut frame.buf, frame.ctx); Continue(()) }); } 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 1e8dd507..21da7a15 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-pre4", path = "../core" } +retrofire-core = { version = "0.4.0", path = "../core" } minifb = { version = "0.27.0", optional = true } sdl2 = { version = "0.35.2", optional = 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 a088da28..8e4507df 100644 --- a/front/src/lib.rs +++ b/front/src/lib.rs @@ -3,7 +3,7 @@ extern crate alloc; extern crate core; -use core::time::Duration; +use core::{cell::RefCell, time::Duration}; use retrofire_core::{ math::Color4, @@ -36,6 +36,7 @@ pub struct Frame<'a, Win, Buf> { pub ctx: &'a mut Context, } +#[allow(non_upper_case_globals)] pub mod dims { // Source for the names: // https://commons.wikimedia.org/wiki/File:Vector_Video_Standards8.svg @@ -43,11 +44,13 @@ pub mod dims { use retrofire_core::util::Dims; // 5:4 + pub const qSXGA_640_512: Dims = (640, 512); pub const SXGA_1280_1024: Dims = (1280, 1024); // 4:3 - #[allow(non_upper_case_globals)] pub const qVGA_320_240: Dims = (320, 240); + pub const qSVGA_400_300: Dims = (400, 300); + pub const qXGA_512_384: Dims = (512, 384); pub const VGA_640_480: Dims = (640, 480); pub const SVGA_800_600: Dims = (800, 600); pub const XGA_1024_768: Dims = (1024, 768); @@ -56,12 +59,21 @@ pub mod dims { pub const QXGA_2048_1536: Dims = (2048, 1536); // 16:10 + pub const CGA_320_200: Dims = (320, 200); + pub const MODE_13H: Dims = CGA_320_200; + pub const QCGA_640_400: Dims = (640, 400); + pub const qWXGA_640_400: Dims = (640, 640); + pub const WXGA_1280_800: Dims = (1280, 800); + pub const WXGAP_1440_900: Dims = (1440, 900); pub const WSXGAP_1680_1050: Dims = (1680, 1050); pub const WUXGA_1920_1200: Dims = (1920, 1200); pub const WQXGA_2560_1600: Dims = (2560, 1600); // 16:9 - pub const HD_1280_720: Dims = (720, 480); + // 640x360 = "qHD"? + // 800x450 = qWSXGA? + // 960x540 = qFHD + pub const HD_1280_720: Dims = (1280, 720); pub const WSXGA_1600_900: Dims = (1600, 900); pub const FHD_1920_1080: Dims = (1920, 1080); pub const QHD_2560_1440: Dims = (2560, 1440); @@ -70,26 +82,38 @@ pub mod dims { // DCI ~17:9 pub const DCI_2K_2048_1080: Dims = (2048, 1080); pub const DCI_4K_4096_2160: Dims = (4096, 2160); + + // ~21:9 + pub const qUWFHD_1280_540: Dims = (1280, 540); + pub const UWFHD_2560_1080: Dims = (2560, 1080); + pub const UWQHD_3440_1440: Dims = (3440, 1440); } impl, F: Copy, Z: AsMutSlice2> - Frame<'_, W, Framebuf, Z>> + Frame<'_, W, &RefCell, Z>>> where Color4: IntoPixel, { + /// Clears the color buffer if [color clearing][Context::color_clear] + /// is enabled and the depth buffer if [depth clearing][Context::depth_clear] + /// is enabled. pub fn clear(&mut self) { if let Some(c) = self.ctx.color_clear { // TODO Assumes pixel format self.buf + .borrow_mut() .color_buf - .buf .as_mut_slice2() .fill(c.into_pixel()); } if let Some(z) = self.ctx.depth_clear { // Depth buffer contains reciprocal depth values // TODO Assumes depth format - self.buf.depth_buf.as_mut_slice2().fill(z.recip()); + self.buf + .borrow_mut() + .depth_buf + .as_mut_slice2() + .fill(z.recip()); } } } diff --git a/front/src/minifb.rs b/front/src/minifb.rs index 5f334896..7013025f 100644 --- a/front/src/minifb.rs +++ b/front/src/minifb.rs @@ -1,6 +1,10 @@ //! Frontend using the `minifb` crate for window creation and event handling. -use std::ops::ControlFlow::{self, Break}; +use core::{ + cell::RefCell, + mem::replace, + ops::ControlFlow::{self, *}, +}; use std::time::Instant; use minifb::{Key, WindowOptions}; @@ -109,7 +113,7 @@ impl Window { /// * the callback returns `ControlFlow::Break`. pub fn run(&mut self, mut frame_fn: F) where - F: FnMut(&mut Frame) -> ControlFlow<()>, + F: FnMut(&mut Frame>) -> ControlFlow<()>, { let (w, h) = self.dims; let mut cbuf = Buf2::new((w, h)); @@ -122,19 +126,19 @@ impl Window { if self.should_quit() { break; } + let buf = Framebuf { + color_buf: Colorbuf::new(cbuf.as_mut_slice2()), + depth_buf: zbuf.as_mut_slice2(), + }; let frame = &mut Frame { t: start.elapsed(), - dt: last.elapsed(), - buf: Framebuf { - color_buf: Colorbuf::new(cbuf.as_mut_slice2()), - depth_buf: zbuf.as_mut_slice2(), - }, + dt: replace(&mut last, Instant::now()).elapsed(), + buf: &RefCell::new(buf), win: self, ctx: &mut ctx, }; frame.clear(); - last = Instant::now(); if let Break(_) = frame_fn(frame) { break; } diff --git a/front/src/sdl2.rs b/front/src/sdl2.rs index 2e789b69..2a04567d 100644 --- a/front/src/sdl2.rs +++ b/front/src/sdl2.rs @@ -1,6 +1,6 @@ //! Frontend using the `sdl2` crate for window creation and event handling. - -use std::{fmt, mem::replace, ops::ControlFlow, time::Instant}; +use core::{cell::RefCell, fmt, mem::replace, ops::ControlFlow}; +use std::time::Instant; use sdl2::{ EventPump, IntegerOrSdlError, Sdl, @@ -11,6 +11,7 @@ use sdl2::{ video::{FullscreenType, Window as SdlWindow, WindowBuildError}, }; +use super::{Frame, dims}; use retrofire_core::math::{Color4, Vary}; use retrofire_core::render::{ Colorbuf, Context, FragmentShader, Target, raster::Scanline, @@ -19,11 +20,9 @@ use retrofire_core::render::{ use retrofire_core::util::{ Dims, buf::{AsMutSlice2, Buf2, MutSlice2}, - pixfmt::{IntoPixel, Rgb565, Rgba4444, Rgba8888}, + pixfmt::*, }; -use super::{Frame, dims}; - /// Helper trait to support different pixel format types. pub trait PixelFmt: Copy + Default { type Pixel: AsRef<[u8]> + Copy + Sized; @@ -55,7 +54,7 @@ pub struct Window { /// Builder for creating `Window`s. pub struct Builder<'title, PF> { - pub dims: (u32, u32), + pub dims: Dims, pub title: &'title str, pub vsync: bool, pub hidpi: bool, @@ -74,8 +73,8 @@ pub struct Framebuf<'a, PF: PixelFmt> { impl<'t, PF: PixelFmt> Builder<'t, PF> { /// Sets the width and height of the window, in pixels. - pub fn dims(mut self, w: u32, h: u32) -> Self { - self.dims = (w, h); + pub fn dims(mut self, dims: Dims) -> Self { + self.dims = dims; self } /// Sets the title of the window. @@ -104,7 +103,12 @@ impl<'t, PF: PixelFmt> Builder<'t, PF> { } /// Sets the framebuffer pixel format. /// - /// Supported formats are [`Rgba8888`], [`Rgb565`], and [`Rgba4444`]. + /// Currently supported formats are + /// * [`Rgba8888`], + /// * [`Rgba5551`], + /// * [`Rgba4444`], + /// * [`Rgb565`], and + /// * [`Rgb332`]. pub fn pixel_fmt(mut self, fmt: PF) -> Self { self.pixfmt = fmt; self @@ -183,7 +187,7 @@ impl, const N: usize> Window { /// * the callback returns [`ControlFlow::Break`][ControlFlow]. pub fn run(&mut self, mut frame_fn: F) -> Result<(), Error> where - F: FnMut(&mut Frame>) -> ControlFlow<()>, + F: FnMut(&mut Frame>>) -> ControlFlow<()>, Color4: IntoPixel, { let dims @ (w, h) = self.canvas.window().drawable_size(); @@ -230,7 +234,7 @@ impl, const N: usize> Window { let frame = &mut Frame { t: start.elapsed(), dt: replace(&mut last, Instant::now()).elapsed(), - buf, + buf: &RefCell::new(buf), win: self, ctx: &mut ctx, }; @@ -265,6 +269,14 @@ impl PixelFmt for Rgba4444 { type Pixel = [u8; 2]; const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGBA4444; } +impl PixelFmt for Rgba5551 { + type Pixel = [u8; 2]; + const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGBA5551; +} +impl PixelFmt for Rgb332 { + type Pixel = [u8; 1]; + const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGB332; +} impl<'a, PF, const N: usize> Target for Framebuf<'a, PF> where diff --git a/front/src/wasm.rs b/front/src/wasm.rs index b696e54f..99d9d150 100644 --- a/front/src/wasm.rs +++ b/front/src/wasm.rs @@ -93,7 +93,7 @@ impl Window { pub fn run(mut self, mut frame_fn: F) where - F: FnMut(&mut Frame) -> ControlFlow<()> + 'static, + F: FnMut(Frame>) -> ControlFlow<()> + 'static, { let mut ctx = self.ctx.clone(); @@ -116,13 +116,13 @@ impl Window { let mut frame = Frame { t, dt, - buf, + buf: &RefCell::new(buf), ctx: &mut ctx, win: &mut self, }; frame.clear(); - if let Continue(_) = frame_fn(&mut frame) { + if let Continue(_) = frame_fn(frame) { requestAnimationFrame(inner.borrow().as_ref().unwrap()); } else { let _ = inner.borrow_mut().take(); diff --git a/geom/Cargo.toml b/geom/Cargo.toml index 8266d85b..e9446b4c 100644 --- a/geom/Cargo.toml +++ b/geom/Cargo.toml @@ -21,10 +21,10 @@ categories.workspace = true repository.workspace = true [dependencies] -re = { version = "0.4.0-pre4", path = "../core", package = "retrofire-core", default-features = false } +retrofire-core = { version = "0.4.0", path = "../core", default-features = false } [features] -std = ["re/std"] +std = ["retrofire-core/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 d521ae10..27ff117d 100644 --- a/geom/src/io.rs +++ b/geom/src/io.rs @@ -49,9 +49,11 @@ use std::{ path::Path, }; -use re::geom::{Mesh, Normal3, Tri, mesh::Builder, vertex}; -use re::math::{Point3, Vec3, vec3}; -use re::render::{Model, TexCoord, uv}; +use retrofire_core::{ + geom::{Mesh, Normal3, Tri, mesh::Builder, vertex}, + math::{Point3, Vec3, vec3}, + render::{Model, TexCoord, uv}, +}; use Error::*; @@ -131,13 +133,17 @@ pub fn parse_obj(src: impl IntoIterator) -> Result> where Builder: TryFrom, { + do_parse_obj(&mut src.into_iter())?.try_into() +} + +fn do_parse_obj(src: &mut dyn Iterator) -> Result { 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(); + let mut it = src.peekable(); while it.peek().is_some() { // Reuse allocation line.clear(); @@ -213,7 +219,7 @@ where { return Err(IndexOutOfBounds("normal", n)); } - obj.try_into() + Ok(obj) } impl TryFrom for Builder<()> { @@ -416,8 +422,10 @@ impl From for Error { #[cfg(test)] mod tests { use super::*; - use re::geom::Vertex; - use re::{geom::Tri, math::point::pt3}; + use retrofire_core::{ + geom::{Tri, Vertex}, + math::point::pt3, + }; #[test] fn input_with_whitespace_and_comments() { diff --git a/geom/src/solids.rs b/geom/src/solids.rs index 0ae2f1a5..52c311bd 100644 --- a/geom/src/solids.rs +++ b/geom/src/solids.rs @@ -4,7 +4,10 @@ mod lathe; mod platonic; -use re::geom::{Mesh, mesh::Builder}; +use alloc::vec::Vec; + +use retrofire_core::geom::{Mesh, Normal3, Tri, mesh::Builder, tri, vertex}; +use retrofire_core::math::{Lerp, Vec3}; #[cfg(feature = "std")] pub use lathe::*; @@ -18,3 +21,58 @@ pub trait Build: Sized { self.build().into_builder() } } + +pub struct Icosphere(pub f32, pub u8); + +impl Build for Icosphere { + fn build(self) -> Mesh { + #[derive(Default)] + struct Tessellator { + coords: Vec, + faces: Vec>, + #[cfg(feature = "std")] + map: std::collections::HashMap<(usize, usize), usize>, + #[cfg(not(feature = "std"))] + map: alloc::collections::BTreeMap<(usize, usize), usize>, + } + impl Tessellator { + fn map_get(&mut self, i: usize, j: usize) -> usize { + *self.map.entry((i, j)).or_insert_with(|| { + let a: Vec3 = self.coords[i]; + let ab = a.midpoint(&self.coords[j]); + self.coords.push(ab); + self.coords.len() - 1 + }) + } + fn recurse(&mut self, d: u8, i: usize, j: usize, k: usize) { + if d == 0 { + self.faces.push(tri(i, j, k)); + } else { + let ij = self.map_get(i, j); + let ik = self.map_get(i, k); + let jk = self.map_get(j, k); + + self.recurse(d - 1, i, ij, ik); + self.recurse(d - 1, j, jk, ij); + self.recurse(d - 1, k, ik, jk); + self.recurse(d - 1, ij, jk, ik); + } + } + } + + let mut recurser = Tessellator { + coords: Icosahedron::COORDS.to_vec(), + ..Default::default() + }; + + for [i, j, k] in Icosahedron::FACES { + recurser.recurse(self.1, i, j, k); + } + + let verts = recurser.coords.iter().map(|&p| { + vertex((p.normalize() * self.0).to().to_pt(), p.normalize()) + }); + + Mesh::new(recurser.faces, verts) + } +} diff --git a/geom/src/solids/lathe.rs b/geom/src/solids/lathe.rs index 8e7cd94e..4102cc31 100644 --- a/geom/src/solids/lathe.rs +++ b/geom/src/solids/lathe.rs @@ -3,15 +3,15 @@ use alloc::vec::Vec; use core::ops::Range; -use re::geom::{ +use retrofire_core::geom::{ Mesh, Normal2, Normal3, Polyline, Tri, Vertex, Vertex2, Vertex3, tri, vertex, }; -use re::math::{ - Angle, Apply, Lerp, Parametric, Point3, Vary, Vec3, polar, pt2, rotate_y, - turns, vec2, +use retrofire_core::math::{ + Angle, Lerp, Parametric, Point3, Vary, Vec3, polar, pt2, rotate_y, turns, + vec2, }; -use re::render::{TexCoord, uv}; +use retrofire_core::render::{TexCoord, uv}; use super::Build; @@ -184,10 +184,12 @@ fn create_verts( } } -fn make_cap(m: &mut Mesh, f: &mut F, rg: Range, n: Normal3) -where - F: FnMut(Point3, Normal3, TexCoord) -> Vertex3, -{ +fn make_cap( + m: &mut Mesh, + f: &mut dyn FnMut(Point3, Normal3, TexCoord) -> Vertex3, + rg: Range, + n: Normal3, +) { let verts = &mut m.verts; let secs = rg.len(); let l = verts.len(); diff --git a/geom/src/solids/platonic.rs b/geom/src/solids/platonic.rs index 89abcba8..d3042e9e 100644 --- a/geom/src/solids/platonic.rs +++ b/geom/src/solids/platonic.rs @@ -3,9 +3,11 @@ use core::{array::from_fn, f32::consts::SQRT_2, iter::zip}; -use re::geom::{Mesh, Normal3, Vertex3, vertex}; -use re::math::{Lerp, Point3, Vec3, pt3, vec3}; -use re::render::{Model, TexCoord, uv}; +use retrofire_core::{ + geom::{Mesh, Normal3, Vertex3, vertex}, + math::{Lerp, Point3, Vec3, pt3, vec3}, + render::{Model, TexCoord, uv}, +}; use super::Build; @@ -369,7 +371,7 @@ impl Build for Dodecahedron { impl Icosahedron { #[rustfmt::skip] - const COORDS: [Vec3; 12] = [ + pub(crate) const COORDS: [Vec3; 12] = [ vec3(-PHI, 0.0, -1.0), vec3(-PHI, 0.0, 1.0), // -X vec3( PHI, 0.0, -1.0), vec3( PHI, 0.0, 1.0), // +X @@ -380,7 +382,7 @@ impl Icosahedron { vec3(0.0, -1.0, PHI), vec3(0.0, 1.0, PHI), // +Z ]; #[rustfmt::skip] - const FACES: [[usize; 3]; 20] = [ + pub(crate) const FACES: [[usize; 3]; 20] = [ [0, 4, 1], [0, 1, 6], // -X [2, 3, 5], [2, 7, 3], // +X [4, 8, 5], [4, 5, 10], // -Y diff --git a/src/lib.rs b/src/lib.rs index 9c23ba53..15f31fb1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ pub use retrofire_core as core; -pub use retrofire_geom as geom; pub use retrofire_front as front; +pub use retrofire_geom as geom; pub use core::prelude;