diff --git a/.cargo/config.toml b/.cargo/config.toml index 674f95e3..993c7a81 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,2 +1,3 @@ [alias] -run-demo = "run --release -p retrofire-demos -F minifb,re-geom --bin" +run-demo = "run --release -p retrofire-demos -F minifb,sdl2,re-geom --bin" +rd = "run --release -q -p retrofire-demos -F minifb,sdl2,re-geom --bin" diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 5c6ec472..60feb80e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -14,19 +14,18 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Install libsdl - run: sudo apt-get update && sudo apt-get -y install libsdl2-dev + - uses: actions/checkout@v2 + - name: Install libsdl + run: sudo apt-get update && sudo apt-get -y install libsdl2-dev - - name: Build core, no features - run: cargo build -p retrofire-core --verbose --all-targets --no-default-features - - name: Build core, micromath - run: cargo build -p retrofire-core --verbose --all-targets --no-default-features --features "mm" - - name: Build core, libm - run: cargo build -p retrofire-core --verbose --all-targets --no-default-features --features "libm" - - - name: Build workspace, std - run: cargo build --workspace --verbose --all-targets --features "std" - - name: Run tests, std - run: cargo test --workspace --verbose --features "std" --exclude 'retrofire-demos*' + - name: Build core, no features + run: cargo build -p retrofire-core --verbose --all-targets --no-default-features + - name: Build core, micromath + run: cargo build -p retrofire-core --verbose --all-targets --no-default-features --features "mm" + - name: Build core, libm + run: cargo build -p retrofire-core --verbose --all-targets --no-default-features --features "libm" + - name: Build workspace, std + run: cargo build --workspace --verbose --all-targets --all-features + - name: Run tests, std + run: cargo test --workspace --verbose --features "std" --exclude 'retrofire-demos*' diff --git a/core/src/geom.rs b/core/src/geom.rs index d0b5208c..fb6577ab 100644 --- a/core/src/geom.rs +++ b/core/src/geom.rs @@ -1,6 +1,7 @@ //! Basic geometric primitives. -use crate::math::vec::{Vec2, Vec3}; +use crate::math::{Point2, Point3, Vec2, Vec3}; +use crate::render::Model; pub use mesh::Mesh; @@ -13,6 +14,12 @@ pub struct Vertex { pub attrib: A, } +/// Two-dimensional vertex type. +pub type Vertex2 = Vertex, A>; + +/// Three-dimensional vertex type. +pub type Vertex3 = Vertex, A>; + /// Triangle, defined by three vertices. #[derive(Copy, Clone, Debug, Eq, PartialEq)] #[repr(transparent)] @@ -31,3 +38,5 @@ pub type Normal2 = Vec2; pub const fn vertex(pos: P, attrib: A) -> Vertex { Vertex { pos, attrib } } + +pub struct Ray(pub Orig, pub Dir); diff --git a/core/src/geom/mesh.rs b/core/src/geom/mesh.rs index ce204d5b..92b1a024 100644 --- a/core/src/geom/mesh.rs +++ b/core/src/geom/mesh.rs @@ -1,23 +1,17 @@ //! Triangle meshes. +use alloc::{vec, vec::Vec}; use core::{ fmt::{Debug, Formatter}, iter::zip, }; -use alloc::{vec, vec::Vec}; - -use crate::math::{ - mat::{Mat4x4, RealToReal}, - space::Linear, - vec::Vec3, +use crate::{ + math::{mat::RealToReal, Linear, Mat4x4, Point3}, + render::Model, }; -use crate::render::Model; -use super::{vertex, Normal3, Tri}; - -/// Convenience type alias for a mesh vertex. -pub type Vertex = super::Vertex, A>; +use super::{vertex, Normal3, Tri, Vertex3}; /// A triangle mesh. /// @@ -32,7 +26,7 @@ pub struct Mesh { /// to the `verts` vector. Several faces can share a vertex. pub faces: Vec>, /// The vertices of the mesh. - pub verts: Vec>, + pub verts: Vec>, } /// A builder type for creating meshes. @@ -53,17 +47,19 @@ impl Mesh { /// /// # Examples /// ``` - /// # use retrofire_core::geom::{Tri, Mesh, vertex}; - /// # use retrofire_core::math::vec3; + /// use retrofire_core::geom::{Tri, Mesh, vertex}; + /// use retrofire_core::math::pt3; + /// /// let verts = [ - /// vec3(0.0, 0.0, 0.0), - /// vec3(1.0, 0.0, 0.0), - /// vec3(0.0, 1.0, 0.0), - /// vec3(0.0, 0.0, 1.0) + /// pt3(0.0, 0.0, 0.0), + /// pt3(1.0, 0.0, 0.0), + /// pt3(0.0, 1.0, 0.0), + /// pt3(0.0, 0.0, 1.0) /// ] /// .map(|v| vertex(v, ())); /// /// let faces = [ + /// // Indices point to the verts array /// Tri([0, 1, 2]), /// Tri([0, 1, 3]), /// Tri([0, 2, 3]), @@ -78,7 +74,7 @@ impl Mesh { pub fn new(faces: F, verts: V) -> Self where F: IntoIterator>, - V: IntoIterator>, + V: IntoIterator>, { let faces: Vec<_> = faces.into_iter().collect(); let verts: Vec<_> = verts.into_iter().collect(); @@ -129,16 +125,16 @@ impl Builder { } /// Appends a vertex with the given position and attribute. - pub fn push_vert(&mut self, pos: Vec3, attrib: A) { + pub fn push_vert(&mut self, pos: Point3, attrib: A) { self.mesh.verts.push(vertex(pos.to(), attrib)); } /// Appends all the vertices yielded by the given iterator. pub fn push_verts(&mut self, verts: Vs) where - Vs: IntoIterator, + Vs: IntoIterator, { - let vs = verts.into_iter().map(|(v, a)| vertex(v.to(), a)); + let vs = verts.into_iter().map(|(p, a)| vertex(p.to(), a)); self.mesh.verts.extend(vs); } @@ -167,7 +163,7 @@ impl Builder<()> { .mesh .verts .into_iter() - .map(|v| vertex(tf.apply(&v.pos), v.attrib)) + .map(|v| vertex(tf.apply_pt(&v.pos), v.attrib)) .collect(), }; mesh.into_builder() @@ -191,8 +187,8 @@ impl Builder<()> { // Compute weighted face normals... let face_normals = faces.iter().map(|Tri(vs)| { - // TODO If n-gonal faces are supported some day, - // the cross product is not proportional to area anymore + // TODO If n-gonal faces are supported some day, the cross + // product is not proportional to area anymore let [a, b, c] = vs.map(|i| verts[i].pos); (b - a).cross(&(c - a)).to() }); @@ -256,9 +252,10 @@ impl Default for Builder { mod tests { use core::f32::consts::FRAC_1_SQRT_2; - use crate::geom::vertex; - use crate::math::vec3; - use crate::prelude::splat; + use crate::{ + geom::vertex, + math::{pt3, splat, vec3}, + }; use super::*; @@ -268,9 +265,9 @@ mod tests { let _: Mesh<()> = Mesh::new( [Tri([0, 1, 2]), Tri([1, 2, 3])], [ - vertex(vec3(0.0, 0.0, 0.0), ()), - vertex(vec3(1.0, 1.0, 1.0), ()), - vertex(vec3(2.0, 2.0, 2.0), ()), + vertex(pt3(0.0, 0.0, 0.0), ()), + vertex(pt3(1.0, 1.0, 1.0), ()), + vertex(pt3(2.0, 2.0, 2.0), ()), ], ); } @@ -281,9 +278,9 @@ mod tests { let mut b = Mesh::builder(); b.push_faces([[0, 1, 2], [1, 2, 3]]); b.push_verts([ - (vec3(0.0, 0.0, 0.0), ()), - (vec3(1.0, 1.0, 1.0), ()), - (vec3(2.0, 2.0, 2.0), ()), + (pt3(0.0, 0.0, 0.0), ()), + (pt3(1.0, 1.0, 1.0), ()), + (pt3(2.0, 2.0, 2.0), ()), ]); _ = b.build(); } @@ -295,10 +292,10 @@ mod tests { let mut b = Mesh::builder(); b.push_faces([[0, 2, 1], [0, 1, 3], [0, 3, 2]]); b.push_verts([ - (vec3(0.0, 0.0, 0.0), ()), - (vec3(1.0, 0.0, 0.0), ()), - (vec3(0.0, 1.0, 0.0), ()), - (vec3(0.0, 0.0, 1.0), ()), + (pt3(0.0, 0.0, 0.0), ()), + (pt3(1.0, 0.0, 0.0), ()), + (pt3(0.0, 1.0, 0.0), ()), + (pt3(0.0, 0.0, 1.0), ()), ]); let b = b.with_vertex_normals(); diff --git a/core/src/lib.rs b/core/src/lib.rs index 4f580117..ceea8267 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -51,25 +51,10 @@ pub mod util; /// Prelude module exporting many frequently used items. pub mod prelude { - #[cfg(feature = "fp")] - pub use crate::math::mat::{rotate_x, rotate_y, rotate_z}; - pub use crate::math::{ - angle::{degs, rads, turns, Angle}, - color::{hsl, hsla, rgb, rgba, Color3, Color3f, Color4, Color4f}, - mat::{ - perspective, scale, translate, viewport, Mat3x3, Mat4x4, Matrix, - }, - rand::Distrib, - space::{Affine, Linear}, - vary::{lerp, Vary}, - vec::{splat, vec2, vec3, Vec2, Vec2i, Vec2u, Vec3, Vec3i, Vector}, - }; - - pub use crate::geom::{vertex, Mesh, Normal2, Normal3, Tri, Vertex}; - - pub use crate::render::{raster::Frag, shader::Shader}; - - pub use crate::util::buf::{ - AsMutSlice2, AsSlice2, Buf2, MutSlice2, Slice2, + pub use crate::{ + geom::{vertex, Mesh, Normal2, Normal3, Tri, Vertex, Vertex2, Vertex3}, + math::*, + render::{raster::Frag, shader::Shader, *}, + util::buf::{AsMutSlice2, AsSlice2, Buf2, MutSlice2, Slice2}, }; } diff --git a/core/src/math.rs b/core/src/math.rs index c3e41915..9b5539ef 100644 --- a/core/src/math.rs +++ b/core/src/math.rs @@ -17,21 +17,93 @@ //! to matching vectors. Angles are strongly typed as well, to allow working //! with different angular units without confusion. -pub use angle::{degs, polar, rads, spherical, turns, Angle}; -pub use approx::ApproxEq; -pub use mat::{Mat3x3, Mat4x4, Matrix}; -pub use space::{Affine, Linear}; -pub use vary::{lerp, Vary}; -pub use vec::{vec2, vec3}; -pub use vec::{Vec2, Vec2i, Vec3, Vec3i, Vector}; +#[cfg(feature = "fp")] +pub use { + angle::{acos, asin, atan2}, + mat::{orient_y, orient_z, rotate_x, rotate_y, rotate_z}, +}; +pub use { + angle::{ + degs, polar, rads, spherical, turns, Angle, PolarVec, SphericalVec, + }, + approx::ApproxEq, + color::{rgb, rgba, Color, Color3, Color3f, Color4, Color4f}, + mat::{ + orthographic, perspective, scale, translate, viewport, Mat3x3, Mat4x4, + Matrix, + }, + point::{pt2, pt3, Point, Point2, Point2u, Point3}, + space::{Affine, Linear}, + spline::{smootherstep, smoothstep, BezierSpline, CubicBezier}, + vary::Vary, + vec::{splat, vec2, vec3, Vec2, Vec2i, Vec2u, Vec3, Vec3i, Vector}, +}; pub mod angle; pub mod approx; pub mod color; pub mod float; pub mod mat; +pub mod point; pub mod rand; pub mod space; pub mod spline; pub mod vary; pub mod vec; + +/// Trait for linear interpolation between two values. +pub trait Lerp { + fn lerp(&self, other: &Self, t: f32) -> Self; +} + +impl Lerp for T +where + T: Affine>, +{ + /// Linearly interpolates between `self` and `other`. + /// + /// if `t` = 0, returns `self`; if `t` = 1, returns `other`. + /// For 0 < `t` < 1, returns the affine combination + /// ```text + /// self * (1 - t) + other * t + /// ``` + /// or rearranged: + /// ```text + /// self + t * (other - self) + /// ``` + /// + /// This method does not panic if `t < 0.0` or `t > 1.0`, or if `t` + /// is a `NaN`, but the return value in those cases is unspecified. + /// Individual implementations may offer stronger guarantees. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::*; + /// + /// assert_eq!(2.0.lerp(&5.0, 0.0), 2.0); + /// assert_eq!(2.0.lerp(&5.0, 0.25), 2.75); + /// assert_eq!(2.0.lerp(&5.0, 0.75), 4.25); + /// assert_eq!(2.0.lerp(&5.0, 1.0), 5.0); + /// + /// let v0: Vec2 = vec2(-2.0, 1.0); + /// let v1 = vec2(3.0, -1.0); + /// assert_eq!(v0.lerp(&v1, 0.8), vec2(2.0, -0.6)); + /// + /// let p0: Point2 = pt2(-10.0, 5.0); + /// let p1 = pt2(-5.0, 0.0); + /// assert_eq!(p0.lerp(&p1, 0.4),pt2(-8.0, 3.0)); + /// ``` + fn lerp(&self, other: &Self, t: f32) -> Self { + self.add(&other.sub(self).mul(t)) + } +} + +impl Lerp for () { + fn lerp(&self, _: &Self, _: f32) {} +} + +impl Lerp for (U, V) { + 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 b823a7bb..5ebd0953 100644 --- a/core/src/math/angle.rs +++ b/core/src/math/angle.rs @@ -1,17 +1,15 @@ //! Angular quantities, including scalar angles and angular vectors. -use core::f32::consts::{PI, TAU}; -use core::fmt::{self, Debug, Display}; -use core::ops::{Add, Div, Mul, Neg, Rem, Sub}; +use core::{ + f32::consts::{PI, TAU}, + fmt::{self, Debug, Display}, + ops::{Add, Div, Mul, Neg, Rem, Sub}, +}; -use crate::math::approx::ApproxEq; -use crate::math::space::{Affine, Linear}; -use crate::math::vec::Vector; +use crate::math::{vary::ZDiv, Affine, ApproxEq, Linear, Vector}; #[cfg(feature = "fp")] -use crate::math::float::f32; -#[cfg(feature = "fp")] -use crate::math::vec::{vec2, vec3, Vec2, Vec3}; +use crate::math::{float::f32, vec2, vec3, Vec2, Vec3}; // // Types @@ -66,8 +64,9 @@ pub fn turns(a: f32) -> Angle { /// /// # Examples /// ``` -/// # use retrofire_core::assert_approx_eq; -/// # use retrofire_core::math::angle::*; +/// use retrofire_core::assert_approx_eq; +/// use retrofire_core::math::{degs, asin}; +/// /// assert_approx_eq!(asin(1.0), degs(90.0)); /// assert_approx_eq!(asin(-1.0), degs(-90.0)); /// ``` @@ -85,8 +84,9 @@ pub fn asin(x: f32) -> Angle { /// /// # Examples /// ``` -/// # use retrofire_core::assert_approx_eq; -/// # use retrofire_core::math::angle::*; +/// use retrofire_core::assert_approx_eq; +/// use retrofire_core::math::{acos, degs}; +/// /// assert_approx_eq!(acos(1.0), degs(0.0)); /// ``` /// # Panics @@ -102,7 +102,8 @@ pub fn acos(x: f32) -> Angle { /// /// # Examples /// ``` -/// # use retrofire_core::math::angle::*; +/// use retrofire_core::math::{atan2, degs}; +/// /// assert_eq!(atan2(0.0, 1.0), degs(0.0)); /// assert_eq!(atan2(2.0, 2.0), degs(45.0)); /// assert_eq!(atan2(3.0, 0.0), degs(90.0)); @@ -145,8 +146,9 @@ impl Angle { /// Returns the value of `self` in radians. /// # Examples /// ``` - /// # use std::f32; - /// # use retrofire_core::math::degs; + /// use std::f32; + /// use retrofire_core::math::degs; + /// /// assert_eq!(degs(90.0).to_rads(), f32::consts::FRAC_PI_2); /// ``` pub const fn to_rads(self) -> f32 { @@ -155,7 +157,8 @@ impl Angle { /// Returns the value of `self` in degrees. /// # Examples /// ``` - /// # use retrofire_core::math::turns; + /// use retrofire_core::math::turns; + /// /// assert_eq!(turns(2.0).to_degs(), 720.0); pub fn to_degs(self) -> f32 { self.0 / RADS_PER_DEG @@ -163,7 +166,8 @@ impl Angle { /// Returns the value of `self` in turns. /// # Examples /// ``` - /// # use retrofire_core::math::degs; + /// use retrofire_core::math::degs; + /// /// assert_eq!(degs(180.0).to_turns(), 0.5); /// ``` pub fn to_turns(self) -> f32 { @@ -182,13 +186,12 @@ impl Angle { /// /// # Examples /// ``` - /// # use retrofire_core::math::angle::degs; + /// use retrofire_core::math::degs; + /// /// let (min, max) = (degs(0.0), degs(45.0)); /// /// assert_eq!(degs(100.0).clamp(min, max), max); - /// /// assert_eq!(degs(30.0).clamp(min, max), degs(30.0)); - /// /// assert_eq!(degs(-10.0).clamp(min, max), min); /// ``` #[must_use] @@ -202,8 +205,9 @@ impl Angle { /// Returns the sine of `self`. /// # Examples /// ``` - /// # use retrofire_core::assert_approx_eq; - /// # use retrofire_core::math::angle::*; + /// use retrofire_core::assert_approx_eq; + /// use retrofire_core::math::degs; + /// /// assert_approx_eq!(degs(30.0).sin(), 0.5) /// ``` pub fn sin(self) -> f32 { @@ -212,8 +216,9 @@ impl Angle { /// Returns the cosine of `self`. /// # Examples /// ``` - /// # use retrofire_core::assert_approx_eq; - /// # use retrofire_core::math::angle::*; + /// use retrofire_core::assert_approx_eq; + /// use retrofire_core::math::degs; + /// /// assert_approx_eq!(degs(60.0).cos(), 0.5) /// ``` pub fn cos(self) -> f32 { @@ -222,8 +227,9 @@ impl Angle { /// Simultaneously computes the sine and cosine of `self`. /// # Examples /// ``` - /// # use retrofire_core::assert_approx_eq; - /// # use retrofire_core::math::angle::*; + /// use retrofire_core::assert_approx_eq; + /// use retrofire_core::math::degs; + /// /// let (sin, cos) = degs(90.0).sin_cos(); /// assert_approx_eq!(sin, 1.0); /// assert_approx_eq!(cos, 0.0); @@ -234,7 +240,7 @@ impl Angle { /// Returns the tangent of `self`. /// # Examples /// ``` - /// # use retrofire_core::math::angle::*; + /// use retrofire_core::math::degs; /// assert_eq!(degs(45.0).tan(), 1.0) /// ``` pub fn tan(self) -> f32 { @@ -245,8 +251,10 @@ impl Angle { /// /// # Examples /// ``` - /// # use retrofire_core::assert_approx_eq; - /// # use retrofire_core::math::angle::*; + /// use retrofire_core::assert_approx_eq; + /// use retrofire_core::math::{degs, turns}; + /// + /// // 400 (mod 360) = 40 /// assert_approx_eq!(degs(400.0).wrap(turns(0.0), turns(1.0)), degs(40.0)) /// ``` #[must_use] @@ -284,14 +292,12 @@ impl PolarVec { /// /// # Examples /// ``` - /// # use retrofire_core::assert_approx_eq; - /// # use retrofire_core::math::angle::{polar, degs}; - /// # use retrofire_core::math::vec::vec2; - /// assert_approx_eq!(polar(2.0, degs(0.0)).to_cart(), vec2(2.0, 0.0)); - /// - /// assert_approx_eq!(polar(2.0, degs(90.0)).to_cart(), vec2(0.0, 2.0)); + /// use retrofire_core::assert_approx_eq; + /// use retrofire_core::math::{vec2, polar, degs}; /// - /// assert_approx_eq!(polar(2.0, degs(-180.0)).to_cart(), vec2(-2.0, 0.0)); + /// assert_approx_eq!(polar(2.0, degs(0.0)).to_cart(), vec2(2.0, 0.0)); + /// assert_approx_eq!(polar(3.0, degs(90.0)).to_cart(), vec2(0.0, 3.0)); + /// assert_approx_eq!(polar(4.0, degs(-180.0)).to_cart(), vec2(-4.0, 0.0)); /// /// ``` #[cfg(feature = "fp")] @@ -321,7 +327,6 @@ impl SphericalVec { /// Returns `self` converted to the equivalent Cartesian 3-vector. /// /// # Examples - /// /// TODO examples #[cfg(feature = "fp")] pub fn to_cart(&self) -> Vec3 { @@ -355,8 +360,9 @@ impl Vec2 { /// /// # Examples /// ``` - /// # use retrofire_core::assert_approx_eq; - /// # use retrofire_core::math::{*, angle::*}; + /// use retrofire_core::assert_approx_eq; + /// use retrofire_core::math::{vec2, degs}; + /// /// // A non-negative x and zero y maps to zero azimuth /// assert_eq!(vec2(0.0, 0.0).to_polar().az(), degs(0.0)); /// assert_eq!(vec2(1.0, 0.0).to_polar().az(), degs(0.0)); @@ -391,7 +397,8 @@ impl Vec3 { /// /// # Examples /// ``` - /// # use retrofire_core::math::{*, angle::*}; + /// use retrofire_core::math::{vec3, spherical, degs}; + /// /// // The positive x-axis lies at zero azimuth and altitude /// assert_eq!( /// vec3(2.0, 0.0, 0.0).to_spherical(), @@ -463,6 +470,8 @@ impl Linear for Angle { } } +impl ZDiv for Angle {} + // // Foreign trait impls // @@ -570,8 +579,10 @@ impl From for SphericalVec { mod tests { use core::f32::consts::{PI, TAU}; - use crate::assert_approx_eq; - use crate::math::vary::Vary; + use crate::{ + assert_approx_eq, + math::{Lerp, Vary}, + }; use super::*; diff --git a/core/src/math/approx.rs b/core/src/math/approx.rs index f602ff35..093d8b2c 100644 --- a/core/src/math/approx.rs +++ b/core/src/math/approx.rs @@ -138,13 +138,13 @@ macro_rules! assert_approx_eq { } }; ($a:expr, $b:expr, $fmt:literal $(, $args:expr)*) => {{ - use $crate::math::approx::ApproxEq; + use $crate::math::ApproxEq; match (&$a, &$b) { (a, b) => assert!(ApproxEq::approx_eq(a, b), $fmt $(, $args)*) } }}; ($a:expr, $b:expr, eps = $eps:literal, $fmt:literal $(, $args:expr)*) => {{ - use $crate::math::approx::ApproxEq; + use $crate::math::ApproxEq; match (&$a, &$b) { (a, b) => assert!( ApproxEq::approx_eq_eps(a, b, &$eps), @@ -196,16 +196,23 @@ mod tests { fn zero_not_approx_eq_to_one() { assert_approx_eq!(0.0, 1.0); } + #[test] #[should_panic] - fn one_not_approx_eq_to_1_00001() { - assert_approx_eq!(1.0, 1.00001); + fn one_not_approx_eq_to_1_01() { + if cfg!(any(feature = "std", feature = "libm")) { + assert_approx_eq!(1.0, 1.00001); + } else { + assert_approx_eq!(1.0, 1.01); + } } + #[test] #[should_panic] fn inf_not_approx_eq_to_inf() { assert_approx_eq!(f32::INFINITY, f32::INFINITY); } + #[test] #[should_panic] fn nan_not_approx_eq_to_nan() { diff --git a/core/src/math/color.rs b/core/src/math/color.rs index 3c8c5850..0c21e9dc 100644 --- a/core/src/math/color.rs +++ b/core/src/math/color.rs @@ -1,13 +1,13 @@ //! Colors and color spaces. -use core::array; -use core::fmt::{self, Debug, Formatter}; -use core::marker::PhantomData; -use core::ops::Index; +use core::{ + array, + fmt::{self, Debug, Formatter}, + marker::PhantomData, + ops::Index, +}; -use crate::math::float::f32; -use crate::math::space::{Affine, Linear}; -use crate::math::vec::Vector; +use crate::math::{float::f32, vary::ZDiv, Affine, Linear, Vector}; // // Types @@ -531,6 +531,8 @@ impl Linear for Color<[f32; DIM], Sp> { } } +impl ZDiv for Color<[Sc; N], Sp> where Sc: ZDiv + Copy {} + // // Foreign trait impls // diff --git a/core/src/math/mat.rs b/core/src/math/mat.rs index 9a19e2e4..02eb080b 100644 --- a/core/src/math/mat.rs +++ b/core/src/math/mat.rs @@ -1,16 +1,24 @@ #![allow(clippy::needless_range_loop)] //! Matrices and linear transforms. +//! +//! TODO Docs -use core::array; -use core::fmt::{self, Debug, Formatter}; -use core::marker::PhantomData; -use core::ops::Range; +use core::{ + array, + fmt::{self, Debug, Formatter}, + marker::PhantomData, + ops::Range, +}; use crate::render::{NdcToScreen, ViewToProj}; -use super::space::{Linear, Proj4, Real}; -use super::vec::{ProjVec4, Vec2, Vec2u, Vec3, Vector}; +use super::{ + float::f32, + point::{Point2, Point2u, Point3}, + space::{Linear, Proj4, Real}, + vec::{ProjVec4, Vec2, Vec3, Vector}, +}; /// A linear transform from one space (or basis) to another. /// @@ -82,6 +90,7 @@ where Map: LinearMap, { /// Returns the row vector of `self` with index `i`. + /// /// The returned vector is in space `Map::Source`. /// /// # Panics @@ -133,10 +142,11 @@ impl Matrix<[[f32; N]; N], Map> { impl Mat4x4 { /// Constructs a matrix from a set of basis vectors. pub const fn from_basis(i: Vec3, j: Vec3, k: Vec3) -> Self { + let (i, j, k) = (i.0, j.0, k.0); Self::new([ - [i.0[0], i.0[1], i.0[2], 0.0], - [j.0[0], j.0[1], j.0[2], 0.0], - [k.0[0], k.0[1], k.0[2], 0.0], + [i[0], j[0], k[0], 0.0], + [i[1], j[1], k[1], 0.0], + [i[2], j[2], k[2], 0.0], [0.0, 0.0, 0.0, 1.0], ]) } @@ -199,9 +209,16 @@ impl Mat3x3> { /// ``` #[must_use] pub fn apply(&self, v: &Vec2) -> Vec2 { - let v = [v.x(), v.y(), 1.0].into(); + let v = [v.x(), v.y(), 1.0].into(); // TODO w=0.0 array::from_fn(|i| self.row_vec(i).dot(&v)).into() } + + // TODO Add trait to overload apply or similar + #[must_use] + pub fn apply_pt(&self, p: &Point2) -> Point2 { + let p = [p.x(), p.y(), 1.0].into(); + array::from_fn(|i| self.row_vec(i).dot(&p)).into() + } } impl Mat4x4> { @@ -218,10 +235,17 @@ impl Mat4x4> { /// ``` #[must_use] pub fn apply(&self, v: &Vec3) -> Vec3 { - let v = [v.x(), v.y(), v.z(), 1.0].into(); + let v = [v.x(), v.y(), v.z(), 1.0].into(); // TODO w=0.0 array::from_fn(|i| self.row_vec(i).dot(&v)).into() } + // TODO Add trait to overload apply or similar + #[must_use] + pub fn apply_pt(&self, p: &Point3) -> Point3 { + let p = [p.x(), p.y(), p.z(), 1.0].into(); + array::from_fn(|i| self.row_vec(i).dot(&p)).into() + } + /// Returns the determinant of `self`. /// /// Given a matrix M, @@ -365,8 +389,8 @@ impl Mat4x4> { /// \ · · M33 / \ 1 / /// ``` #[must_use] - pub fn apply(&self, v: &Vec3) -> ProjVec4 { - let v = Vector::from([v.x(), v.y(), v.z(), 1.0]); + pub fn apply(&self, p: &Point3) -> ProjVec4 { + let v = Vector::from([p.x(), p.y(), p.z(), 1.0]); [ self.row_vec(0).dot(&v), self.row_vec(1).dot(&v), @@ -460,7 +484,7 @@ impl From for Matrix { /// /// Tip: use [`splat`][super::vec::splat] to scale uniformly: /// ``` -/// # use retrofire_core::math::{mat::scale, vec::splat}; +/// use retrofire_core::math::{scale, splat}; /// let m = scale(splat(2.0)); /// assert_eq!(m.0[0][0], 2.0); /// assert_eq!(m.0[1][1], 2.0); @@ -510,7 +534,7 @@ pub fn orient_z(new_z: Vec3, x: Vec3) -> Mat4x4> { #[cfg(feature = "fp")] fn orient(new_y: Vec3, new_z: Vec3) -> Mat4x4> { - use crate::math::{space::Linear, ApproxEq}; + use crate::math::{ApproxEq, Linear}; assert!(!new_y.approx_eq(&Vec3::zero())); assert!(!new_z.approx_eq(&Vec3::zero())); @@ -602,30 +626,33 @@ 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: Vec3, rtf: Vec3) -> Mat4x4 { - let [dx, dy, dz] = (rtf - lbn).0; - let [sx, sy, sz] = (rtf + lbn).0; +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; [ - [2.0 / dx, 0.0, 0.0, -sx / dx], - [0.0, 2.0 / dy, 0.0, -sy / dy], - [0.0, 0.0, 2.0 / dz, -sz / dz], + [idx, 0.0, 0.0, -cx * idx], + [0.0, idy, 0.0, -cy * idy], + [0.0, 0.0, idz, -cz * idz], [0.0, 0.0, 0.0, 1.0], ] .into() } -/// Creates a viewport transform matrix. A viewport matrix is used to -/// transform points from the NDC space to screen space for rasterization. +/// Creates a viewport transform matrix with the given pixel space bounds. /// -/// # Parameters -/// * `bounds`: the left-top and right-bottom coordinates of the viewport. -pub fn viewport(bounds: Range) -> Mat4x4 { - let Range { start, end } = bounds; - let h = (end.x() - start.x()) as f32 / 2.0; - let v = (end.y() - start.y()) as f32 / 2.0; +/// 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; [ - [h, 0.0, 0.0, h + start.x() as f32], - [0.0, v, 0.0, v + start.y() as f32], + [dx, 0.0, 0.0, cx], + [0.0, dy, 0.0, cy], [0.0, 0.0, 1.0, 0.0], [0.0, 0.0, 0.0, 1.0], ] @@ -635,10 +662,10 @@ pub fn viewport(bounds: Range) -> Mat4x4 { #[cfg(test)] mod tests { use crate::assert_approx_eq; - use crate::math::vec::{splat, vec2, vec3}; + use crate::math::{pt2, pt3, splat, vec2, vec3}; #[cfg(feature = "fp")] - use crate::math::angle::degs; + use crate::math::degs; use super::*; @@ -650,8 +677,13 @@ mod tests { type Map = RealToReal; type InvMap = RealToReal; + const X: Vec3 = vec3(1.0, 0.0, 0.0); + const Y: Vec3 = vec3(0.0, 1.0, 0.0); + const Z: Vec3 = vec3(0.0, 0.0, 1.0); + mod mat3x3 { use super::*; + use crate::math::pt2; const MAT: Mat3x3 = Matrix::new([ [0.0, 1.0, 2.0], // @@ -696,6 +728,7 @@ mod tests { [0.0, 0.0, 1.0], ]); assert_eq!(m.apply(&vec2(1.0, 2.0)), vec2(2.0, -6.0)); + assert_eq!(m.apply_pt(&pt2(2.0, -1.0)), pt2(4.0, 3.0)); } #[test] @@ -706,6 +739,7 @@ mod tests { [0.0, 0.0, 1.0], ]); assert_eq!(m.apply(&vec2(1.0, 2.0)), vec2(3.0, -1.0)); + assert_eq!(m.apply_pt(&pt2(2.0, -1.0)), pt2(4.0, -4.0)); } #[test] @@ -723,6 +757,7 @@ mod tests { mod mat4x4 { use super::*; + use crate::math::pt3; const MAT: Mat4x4 = Matrix::new([ [0.0, 1.0, 2.0, 3.0], @@ -753,17 +788,25 @@ mod tests { } #[test] - fn scaling_vec3() { + fn scaling() { let m = scale(vec3(1.0, -2.0, 3.0)); + let v = vec3(0.0, 4.0, -3.0); assert_eq!(m.apply(&v), vec3(0.0, -8.0, -9.0)); + + let p = pt3(4.0, 0.0, -3.0); + assert_eq!(m.apply_pt(&p), pt3(4.0, 0.0, -9.0)); } #[test] - fn translation_vec3() { + fn translation() { let m = translate(vec3(1.0, 2.0, 3.0)); + let v = vec3(0.0, 5.0, -3.0); assert_eq!(m.apply(&v), vec3(1.0, 7.0, 0.0)); + + let p = pt3(3.0, 5.0, 0.0); + assert_eq!(m.apply_pt(&p), pt3(4.0, 7.0, 3.0)); } #[cfg(feature = "fp")] @@ -775,6 +818,10 @@ mod tests { m.apply(&vec3(0.0, 0.0, 1.0)), vec3(0.0, 1.0, 0.0) ); + assert_approx_eq!( + m.apply_pt(&pt3(0.0, -2.0, 0.0)), + pt3(0.0, 0.0, 2.0) + ); } #[cfg(feature = "fp")] @@ -786,6 +833,10 @@ mod tests { m.apply(&vec3(1.0, 0.0, 0.0)), vec3(0.0, 0.0, 1.0) ); + assert_approx_eq!( + m.apply_pt(&pt3(0.0, 0.0, -2.0)), + pt3(2.0, 0.0, 0.0) + ); } #[cfg(feature = "fp")] @@ -797,6 +848,63 @@ mod tests { m.apply(&vec3(0.0, 1.0, 0.0)), vec3(1.0, 0.0, 0.0) ); + assert_approx_eq!( + m.apply_pt(&pt3(-2.0, 0.0, 0.0)), + pt3(0.0, 2.0, 0.0) + ); + } + + #[test] + fn from_basis() { + let m = Mat4x4::>::from_basis(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); + } + + #[cfg(feature = "fp")] + #[test] + fn orientation_no_op() { + let m = orient_y(Y, X); + + assert_eq!(m.apply(&X), X); + assert_eq!(m.apply_pt(&X.to_pt()), X.to_pt()); + + assert_eq!(m.apply(&Y), Y); + assert_eq!(m.apply_pt(&Y.to_pt()), Y.to_pt()); + + assert_eq!(m.apply(&Z), Z); + assert_eq!(m.apply_pt(&Z.to_pt()), Z.to_pt()); + } + + #[cfg(feature = "fp")] + #[test] + fn orientation_y_to_z() { + let m = orient_y(Z, X); + + assert_eq!(m.apply(&X), X); + assert_eq!(m.apply_pt(&X.to_pt()), X.to_pt()); + + assert_eq!(m.apply(&Y), Z); + assert_eq!(m.apply_pt(&Y.to_pt()), Z.to_pt()); + + assert_eq!(m.apply(&Z), -Y); + assert_eq!(m.apply_pt(&Z.to_pt()), (-Y).to_pt()); + } + + #[cfg(feature = "fp")] + #[test] + fn orientation_z_to_y() { + let m = orient_z(Y, X); + + assert_eq!(m.apply(&X), X); + assert_eq!(m.apply_pt(&X.to_pt()), X.to_pt()); + + assert_eq!(m.apply(&Y), -Z); + assert_eq!(m.apply_pt(&Y.to_pt()), (-Z).to_pt()); + + assert_eq!(m.apply(&Z), Y); + assert_eq!(m.apply_pt(&Z.to_pt()), Y.to_pt()); } #[test] @@ -884,19 +992,19 @@ mod tests { #[test] fn orthographic_box_maps_to_unit_cube() { - let lbn = vec3(-20.0, 0.0, 0.01); - let rtf = vec3(100.0, 50.0, 100.0); + let lbn = pt3(-20.0, 0.0, 0.01); + let rtf = pt3(100.0, 50.0, 100.0); let m = orthographic(lbn, rtf); assert_approx_eq!(m.apply(&lbn.to()), [-1.0, -1.0, -1.0, 1.0].into()); - assert_approx_eq!(m.apply(&rtf.to()), splat(1.0)); + assert_approx_eq!(m.apply(&rtf.to()), [1.0, 1.0, 1.0, 1.0].into()); } #[test] fn perspective_frustum_maps_to_unit_cube() { - let left_bot_near = vec3(-0.125, -0.0625, 0.1); - let right_top_far = vec3(125.0, 62.5, 100.0); + let left_bot_near = pt3(-0.125, -0.0625, 0.1); + let right_top_far = pt3(125.0, 62.5, 100.0); let m = perspective(0.8, 2.0, 0.1..100.0); @@ -904,6 +1012,14 @@ mod tests { assert_approx_eq!(lbn / lbn.w(), [-1.0, -1.0, -1.0, 1.0].into()); let rtf = m.apply(&right_top_far); - assert_approx_eq!(rtf / rtf.w(), splat(1.0)); + assert_approx_eq!(rtf / rtf.w(), [1.0, 1.0, 1.0, 1.0].into()); + } + + #[test] + fn viewport_maps_ndc_to_screen() { + let m = viewport(pt2(20, 10)..pt2(620, 470)); + + assert_eq!(m.apply_pt(&pt3(-1.0, -1.0, 0.2)), pt3(20.0, 10.0, 0.2)); + assert_eq!(m.apply_pt(&pt3(1.0, 1.0, 0.6)), pt3(620.0, 470.0, 0.6)); } } diff --git a/core/src/math/point.rs b/core/src/math/point.rs new file mode 100644 index 00000000..c3f49176 --- /dev/null +++ b/core/src/math/point.rs @@ -0,0 +1,296 @@ +use core::{ + array, + fmt::{Debug, Formatter}, + marker::PhantomData as Pd, + ops::{Add, AddAssign, Index, Sub, SubAssign}, +}; + +use crate::math::{space::Real, vary::ZDiv, Affine, ApproxEq, Linear, Vector}; + +#[repr(transparent)] +pub struct Point(pub Repr, Pd); + +/// A 2-point with `f32` components. +pub type Point2 = Point<[f32; 2], Real<2, Basis>>; +/// A 3-point with `f32` components. +pub type Point3 = Point<[f32; 3], Real<3, Basis>>; + +/// A 2-point with `u32` components. +pub type Point2u = Point<[u32; 2], Real<2, Basis>>; + +/// Returns a real 2-point with `x` and `y` components. +pub const fn pt2(x: Sc, y: Sc) -> Point<[Sc; 2], Real<2, B>> { + Point([x, y], Pd) +} +/// Returns a real 3-point with `x`, `y`, and `z` components. +pub const fn pt3(x: Sc, y: Sc, z: Sc) -> Point<[Sc; 3], Real<3, B>> { + Point([x, y, z], Pd) +} + +impl Point { + #[inline] + pub fn new(repr: R) -> Self { + Self(repr, Pd) + } + + /// 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) + } + + /// Returns the vector equivalent to `self`. + // TODO Cannot be const (yet?) due to E0493 :( + #[inline] + pub fn to_vec(self) -> Vector { + Vector::new(self.0) + } +} + +impl Point<[Sc; N], Sp> { + /// Returns a point of the same dimension as `self` by applying `f` + /// component-wise. + #[inline] + #[must_use] + pub fn map(self, f: impl FnMut(Sc) -> T) -> Point<[T; N], Sp> { + self.0.map(f).into() + } +} + +impl Point<[f32; N], Real> { + /// Returns the Euclidean distance between `self` and another point. + /// + /// # Example + /// ``` + /// use retrofire_core::math::{Point2, pt2}; + /// + /// let x3: Point2 = pt2(3.0, 0.0); + /// let y4 = pt2(0.0, 4.0); + /// assert_eq!(x3.distance(&y4), 5.0); + /// ``` + #[cfg(feature = "fp")] + #[inline] + pub fn distance(&self, other: &Self) -> f32 { + self.sub(other).len() + } + /// Returns the square of the Euclidean distance between `self` and another + /// point. + /// + /// Faster to compute than [distance][Self::distance]. + /// + /// # Example + /// ``` + /// use retrofire_core::math::{Point2, pt2}; + /// + /// let x3: Point2 = pt2(3.0, 0.0); + /// let y4 = pt2(0.0, 4.0); + /// assert_eq!(x3.distance_sqr(&y4), 5.0 * 5.0); + /// ``` + #[inline] + pub fn distance_sqr(&self, other: &Self) -> f32 { + self.sub(other).len_sqr() + } + + /// Returns `self` clamped component-wise to the given range. + /// + /// In other words, for each component `self[i]`, the result `r` has + /// `r[i]` equal to `self[i].clamp(min[i], max[i])`. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{pt3, Point3}; + /// + /// let pt: Point3 = pt3(0.5, 1.5, -2.0); + /// // Clamp to the unit cube + /// let clamped = pt.clamp(&pt3(0.0, 0.0, 0.0), &pt3(1.0, 1.0, 1.0)); + /// assert_eq!(clamped, pt3(0.5, 1.0, 0.0)); + #[must_use] + pub fn clamp(&self, min: &Self, max: &Self) -> Self { + array::from_fn(|i| self.0[i].clamp(min.0[i], max.0[i])).into() + } +} + +impl Point> +where + R: Index, + Sc: Copy, +{ + /// Returns the x component of `self`. + #[inline] + pub fn x(&self) -> Sc { + self.0[0] + } + /// Returns the y component of `self`. + #[inline] + pub fn y(&self) -> Sc { + self.0[1] + } +} + +impl Point> +where + R: Index, + Sc: Copy, +{ + /// Returns the x component of `self`. + #[inline] + pub fn x(&self) -> Sc { + self.0[0] + } + /// Returns the y component of `self`. + #[inline] + pub fn y(&self) -> Sc { + self.0[1] + } + /// Returns the z component of `self`. + #[inline] + pub fn z(&self) -> Sc { + self.0[2] + } +} + +// +// Local trait impls +// + +impl Affine for Point<[Sc; N], Sp> +where + Sc: Linear + Copy, +{ + type Space = Sp; + type Diff = Vector<[Sc; N], Sp>; + const DIM: usize = N; + + #[inline] + fn add(&self, other: &Self::Diff) -> Self { + // TODO Profile performance of array::from_fn + Self(array::from_fn(|i| self.0[i].add(&other.0[i])), Pd) + } + #[inline] + fn sub(&self, other: &Self) -> Self::Diff { + Vector::new(array::from_fn(|i| self.0[i].sub(&other.0[i]))) + } +} + +impl ZDiv for Point<[Sc; N], Sp> +where + Sc: ZDiv + Copy, +{ + fn z_div(self, z: f32) -> Self { + self.map(|c| c.z_div(z)) + } +} + +impl ApproxEq + for Point<[Sc; N], Sp> +{ + fn approx_eq_eps(&self, other: &Self, eps: &Sc) -> bool { + self.0.approx_eq_eps(&other.0, eps) + } + fn relative_epsilon() -> Sc { + Sc::relative_epsilon() + } +} + +// +// Foreign trait impls +// + +// Manual impls of Copy, Clone, Eq, and PartialEq to avoid +// superfluous where S: Trait bound + +impl Copy for Point {} + +impl Clone for Point { + fn clone(&self) -> Self { + Self(self.0.clone(), Pd) + } +} + +impl Default for Point { + fn default() -> Self { + Self(R::default(), Pd) + } +} + +impl Debug for Point { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + write!(f, "Point<{:?}>", Sp::default())?; + Debug::fmt(&self.0, f) + } +} + +impl Eq for Point {} + +impl PartialEq for Point { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl From for Point { + #[inline] + fn from(repr: R) -> Self { + Self(repr, Pd) + } +} + +impl, Sp> Index for Point { + type Output = R::Output; + + fn index(&self, i: usize) -> &Self::Output { + self.0.index(i) + } +} + +impl Add<::Diff> for Point +where + Self: Affine, +{ + type Output = Self; + + fn add(self, other: ::Diff) -> Self { + Affine::add(&self, &other) + } +} + +impl AddAssign<::Diff> for Point +where + Self: Affine, +{ + fn add_assign(&mut self, other: ::Diff) { + *self = Affine::add(self, &other); + } +} + +impl Sub<::Diff> for Point +where + Self: Affine, +{ + type Output = Self; + + fn sub(self, other: ::Diff) -> Self { + Affine::add(&self, &other.neg()) + } +} + +impl SubAssign<::Diff> for Point +where + Self: Affine, +{ + fn sub_assign(&mut self, other: ::Diff) { + *self = Affine::add(self, &other.neg()); + } +} + +impl Sub for Point +where + Self: Affine, +{ + type Output = ::Diff; + + fn sub(self, other: Self) -> Self::Output { + Affine::sub(&self, &other) + } +} diff --git a/core/src/math/rand.rs b/core/src/math/rand.rs index 0c64e26a..b6782199 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::vec::{Vec2, Vec3, Vector}; +use super::{Point, Point2, Point3, Vec2, Vec3, Vector}; // // Traits and types @@ -11,7 +11,7 @@ use super::vec::{Vec2, Vec3, Vector}; pub type DefaultRng = Xorshift64; /// Trait for generating values sampled from a probability distribution. -pub trait Distrib: Clone { +pub trait Distrib: Clone { /// The type of the elements of the sample space of `Self`, also called /// "outcomes". type Sample; @@ -21,26 +21,32 @@ pub trait Distrib: Clone { /// # Examples /// ``` /// use retrofire_core::math::rand::*; + /// /// // Simulate rolling a six-sided die - /// let mut rng = DefaultRng::default(); - /// let d6 = Uniform(1..7).sample(&mut rng); + /// let rng = &mut DefaultRng::default(); + /// let d6 = Uniform(1..7).sample(rng); /// assert_eq!(d6, 3); /// ``` - fn sample(&self, rng: &mut R) -> Self::Sample; + fn sample(&self, rng: &mut DefaultRng) -> Self::Sample; - /// Returns an iterator that yields samples from `self`. + /// Returns an iterator that yields samples from `self` indefinitely. /// /// # Examples /// ``` /// use retrofire_core::math::rand::*; + /// /// // Simulate rolling a six-sided die - /// let rng = DefaultRng::default(); + /// let rng = &mut DefaultRng::default(); /// let mut iter = Uniform(1..7).samples(rng); + /// /// assert_eq!(iter.next(), Some(3)); /// assert_eq!(iter.next(), Some(2)); /// assert_eq!(iter.next(), Some(4)); /// ``` - fn samples(&self, rng: R) -> Iter { + fn samples( + &self, + rng: &mut DefaultRng, + ) -> impl Iterator { Iter(self.clone(), rng) } } @@ -63,21 +69,29 @@ pub struct Xorshift64(pub u64); #[derive(Clone, Debug)] pub struct Uniform(pub Range); -/// A uniform distribution of 2-vectors on the (perimeter of) the unit circle. +/// A uniform distribution of unit 2-vectors. #[derive(Copy, Clone, Debug)] pub struct UnitCircle; -/// A uniform distribution of 2-vectors inside the (closed) unit disk. +/// A uniform distribution of unit 3-vectors. #[derive(Copy, Clone, Debug, Default)] -pub struct UnitDisk; +pub struct UnitSphere; -/// A uniform distribution of 3-vectors on the (surface of) the unit sphere. +/// A uniform distribution of 2-vectors inside the (closed) unit disk. #[derive(Copy, Clone, Debug, Default)] -pub struct UnitSphere; +pub struct VectorsOnUnitDisk; /// A uniform distribution of 3-vectors inside the (closed) unit ball. #[derive(Copy, Clone, Debug, Default)] -pub struct UnitBall; +pub struct VectorsInUnitBall; + +/// A uniform distribution of 2-points inside the (closed) unit disk. +#[derive(Copy, Clone, Debug, Default)] +pub struct PointsOnUnitDisk; + +/// A uniform distribution of 3-points inside the (closed) unit ball. +#[derive(Copy, Clone, Debug, Default)] +pub struct PointsInUnitBall; /// A Bernoulli distribution. /// @@ -91,7 +105,7 @@ pub struct Bernoulli(pub f32); /// Iterator returned by the [`Distrib::samples()`] method. #[derive(Copy, Clone, Debug)] -pub struct Iter(D, R); +struct Iter(D, R); // // Inherent impls @@ -111,7 +125,8 @@ impl Xorshift64 { /// /// # Examples /// ``` - /// # use retrofire_core::math::rand::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); @@ -134,8 +149,9 @@ impl Xorshift64 { /// /// # Examples /// ``` - /// # use std::thread; - /// # use retrofire_core::math::rand::Xorshift64; + /// use std::thread; + /// use retrofire_core::math::rand::Xorshift64; + /// /// let mut g = Xorshift64::from_time(); /// thread::sleep_ms(1); // Just to be sure /// let mut h = Xorshift64::from_time(); @@ -170,7 +186,7 @@ impl Xorshift64 { /// An infinite iterator of pseudorandom values sampled from a distribution. /// /// This type is returned by [`Distrib::samples`]. -impl Iterator for Iter { +impl Iterator for Iter { type Item = D::Sample; /// Returns the next pseudorandom sample from this iterator. @@ -187,6 +203,7 @@ impl Default for Xorshift64 { /// # Examples /// ``` /// use retrofire_core::math::rand::Xorshift64; + /// /// let mut g = Xorshift64::default(); /// assert_eq!(g.next_bits(), 11039719294064252060); /// ``` @@ -209,7 +226,7 @@ impl Distrib for Uniform { /// # Examples /// ``` /// use retrofire_core::math::rand::*; - /// let rng = DefaultRng::default(); + /// let rng = &mut DefaultRng::default(); /// /// // Simulate rolling a six-sided die /// let mut iter = Uniform(1..7).samples(rng); @@ -233,7 +250,7 @@ impl Distrib for Uniform { /// # Examples /// ``` /// use retrofire_core::math::rand::*; - /// let rng = DefaultRng::default(); + /// let rng = &mut DefaultRng::default(); /// /// // Floats in the interval [-1, 1) /// let mut iter = Uniform(-1.0..1.0).samples(rng); @@ -258,19 +275,20 @@ where { type Sample = [T; N]; - /// Returns the coordinates of a uniformly distributed point within - /// the N-dimensional rectangular volume bounded by the range `self.0`. + /// Returns the coordinates of a point sampled from a uniform distribution + /// within the N-dimensional rectangular volume bounded by `self.0`. /// /// # Examples /// ``` /// use retrofire_core::math::rand::*; - /// let rng = DefaultRng::default(); + /// let rng = &mut DefaultRng::default(); /// /// // Pairs of integers [X, Y] such that 0 <= X < 4 and -2 <= Y <= 3 - /// let mut iter = Uniform([0, -2]..[4, 3]).samples(rng); - /// assert_eq!(iter.next(), Some([0, -1])); - /// assert_eq!(iter.next(), Some([1, 0])); - /// assert_eq!(iter.next(), Some([3, 1])); + /// let mut int_pairs = Uniform([0, -2]..[4, 3]).samples(rng); + /// + /// assert_eq!(int_pairs.next(), Some([0, -1])); + /// assert_eq!(int_pairs.next(), Some([1, 0])); + /// assert_eq!(int_pairs.next(), Some([3, 1])); /// ``` fn sample(&self, rng: &mut DefaultRng) -> [T; N] { let Range { start, end } = self.0; @@ -286,8 +304,25 @@ where { type Sample = Vector<[Sc; DIM], Sp>; - /// Returns a uniformly distributed vector within the rectangular volume - /// bounded by the range `self.0`. + /// Returns a vector 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() + } +} + +/// Uniformly distributed points within a rectangular volume. +impl Distrib for Uniform> +where + Sc: Copy, + 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) @@ -299,20 +334,40 @@ where impl Distrib for UnitCircle { type Sample = Vec2; - /// Returns a 2-vector uniformly distributed on the unit circle. + /// Returns a unit 2-vector uniformly sampled from the unit circle. + /// + /// # Example + /// ``` + /// use retrofire_core::math::{ApproxEq, rand::*}; + /// let rng = &mut DefaultRng::default(); + /// + /// let vec = UnitCircle.sample(rng); + /// assert!(vec.len_sqr().approx_eq(&1.0)); + /// ``` fn sample(&self, rng: &mut DefaultRng) -> Vec2 { let d = Uniform([-1.0; 2]..[1.0; 2]); + // Normalization preserves uniformity Vec2::from(d.sample(rng)).normalize() } } -impl Distrib for UnitDisk { +impl Distrib for VectorsOnUnitDisk { type Sample = Vec2; - /// Returns a 2-vector uniformly distributed within the unit disk. + /// Returns a 2-vector uniformly sampled from the unit disk. + /// + /// # Example + /// ``` + /// use retrofire_core::math::rand::*; + /// let rng = &mut DefaultRng::default(); + /// + /// let vec = VectorsOnUnitDisk.sample(rng); + /// assert!(vec.len_sqr() <= 1.0); + /// ``` fn sample(&self, rng: &mut DefaultRng) -> Vec2 { let d = Uniform([-1.0f32; 2]..[1.0; 2]); loop { + // Rejection sampling let v = Vec2::from(d.sample(rng)); if v.len_sqr() <= 1.0 { return v; @@ -325,20 +380,40 @@ impl Distrib for UnitDisk { impl Distrib for UnitSphere { type Sample = Vec3; - /// Returns a vector uniformly distributed on the unit sphere. + /// Returns a unit 3-vector uniformly sampled from the unit sphere. + /// + /// # Example + /// ``` + /// use retrofire_core::assert_approx_eq; + /// use retrofire_core::math::rand::*; + /// let rng = &mut DefaultRng::default(); + /// + /// let vec = UnitSphere.sample(rng); + /// assert_approx_eq!(vec.len_sqr(), 1.0); + /// ``` fn sample(&self, rng: &mut DefaultRng) -> Vec3 { - let d = Uniform([-1.0f32; 3]..[1.0; 3]); + let d = Uniform([-1.0; 3]..[1.0; 3]); Vec3::from(d.sample(rng)).normalize() } } -impl Distrib for UnitBall { +impl Distrib for VectorsInUnitBall { type Sample = Vec3; - /// Returns a vector uniformly distributed within the unit ball. + /// Returns a 3-vector uniformly sampled from the unit ball. + /// + /// # Example + /// ``` + /// use retrofire_core::math::rand::*; + /// let rng = &mut DefaultRng::default(); + /// + /// let vec = VectorsInUnitBall.sample(rng); + /// assert!(vec.len_sqr() <= 1.0); + /// ``` fn sample(&self, rng: &mut DefaultRng) -> Vec3 { let d = Uniform([-1.0; 3]..[1.0; 3]); loop { + // Rejection sampling let v = Vec3::from(d.sample(rng)); if v.len_sqr() <= 1.0 { return v; @@ -347,10 +422,46 @@ impl Distrib for UnitBall { } } +impl Distrib for PointsOnUnitDisk { + type Sample = Point2; + + /// Returns a 2-point uniformly sampled from the unit disk. + /// + /// See [`VectorsOnUnitDisk::sample`]. + fn sample(&self, rng: &mut DefaultRng) -> Point2 { + VectorsOnUnitDisk.sample(rng).to_pt() + } +} + +impl Distrib for PointsInUnitBall { + type Sample = Point3; + + /// Returns a 3-point uniformly sampled from the unit ball. + /// + /// See [`VectorsInUnitBall::sample`]. + fn sample(&self, rng: &mut DefaultRng) -> Point3 { + VectorsInUnitBall.sample(rng).to_pt() + } +} + impl Distrib for Bernoulli { type Sample = bool; - /// Returns boolean values sampled from a Bernoulli distribution. + /// Returns booleans sampled from a Bernoulli distribution. + /// + /// The result is `true` with probability `self.0` and false + /// with probability 1 - `self.0`. + /// + /// # Example + /// ``` + /// use core::array; + /// use retrofire_core::math::rand::*; + /// let rng = &mut DefaultRng::default(); + /// + /// let bern = Bernoulli(0.6); // P(true) = 0.6 + /// let bools = array::from_fn(|_| bern.sample(rng)); + /// assert_eq!(bools, [true, true, false, true, false, true]); + /// ``` fn sample(&self, rng: &mut DefaultRng) -> bool { Uniform(0.0f32..1.0).sample(rng) < self.0 } @@ -368,8 +479,7 @@ impl Distrib for (D, E) { #[cfg(test)] #[allow(clippy::manual_range_contains)] mod tests { - use crate::assert_approx_eq; - use crate::math::vec::vec3; + use crate::math::vec3; use super::*; @@ -382,7 +492,7 @@ mod tests { #[test] fn uniform_i32() { let dist = Uniform(-123..456); - for r in dist.samples(rng()).take(COUNT) { + for r in dist.samples(&mut rng()).take(COUNT) { assert!(-123 <= r && r < 456); } } @@ -390,7 +500,7 @@ mod tests { #[test] fn uniform_f32() { let dist = Uniform(-1.23..4.56); - for r in dist.samples(rng()).take(COUNT) { + for r in dist.samples(&mut rng()).take(COUNT) { assert!(-1.23 <= r && r < 4.56); } } @@ -400,7 +510,7 @@ mod tests { let dist = Uniform([0, -10]..[10, 15]); let sum = dist - .samples(rng()) + .samples(&mut rng()) .take(COUNT) .inspect(|&[x, y]| { assert!(0 <= x && x < 10); @@ -417,7 +527,7 @@ mod tests { Uniform(vec3::(-2.0, 0.0, -1.0)..vec3(1.0, 2.0, 3.0)); let mean = dist - .samples(rng()) + .samples(&mut rng()) .take(COUNT) .inspect(|v| { assert!(-2.0 <= v.x() && v.x() < 1.0); @@ -432,25 +542,24 @@ mod tests { #[test] fn bernoulli() { - let approx_100 = Bernoulli(0.1) - .samples(rng()) - .take(COUNT) - .filter(|&b| b) - .count(); + let rng = &mut rng(); + let bools = Bernoulli(0.1).samples(rng).take(COUNT); + let approx_100 = bools.filter(|&b| b).count(); assert_eq!(approx_100, 82); } #[cfg(feature = "fp")] #[test] fn unit_circle() { - for v in UnitCircle.samples(rng()).take(COUNT) { + use crate::assert_approx_eq; + for v in UnitCircle.samples(&mut rng()).take(COUNT) { assert_approx_eq!(v.len_sqr(), 1.0, "non-unit vector: {v:?}"); } } #[test] - fn unit_disk() { - for v in UnitDisk.samples(rng()).take(COUNT) { + fn vectors_on_unit_disk() { + for v in VectorsOnUnitDisk.samples(&mut rng()).take(COUNT) { assert!(v.len_sqr() <= 1.0, "vector of len > 1.0: {v:?}"); } } @@ -458,24 +567,25 @@ mod tests { #[cfg(feature = "fp")] #[test] fn unit_sphere() { - for v in UnitSphere.samples(rng()).take(COUNT) { + use crate::assert_approx_eq; + for v in UnitSphere.samples(&mut rng()).take(COUNT) { assert_approx_eq!(v.len_sqr(), 1.0, "non-unit vector: {v:?}"); } } #[test] - fn unit_ball() { - for v in UnitBall.samples(rng()).take(COUNT) { + fn vectors_in_unit_ball() { + for v in VectorsInUnitBall.samples(&mut rng()).take(COUNT) { assert!(v.len_sqr() <= 1.0, "vector of len > 1.0: {v:?}"); } } #[test] fn zipped_pair() { - let mut rng = rng(); + let rng = &mut rng(); let dist = (Bernoulli(0.8), Uniform(0..4)); - assert_eq!(dist.sample(&mut rng), (true, 1)); - assert_eq!(dist.sample(&mut rng), (false, 3)); - assert_eq!(dist.sample(&mut rng), (true, 2)); + assert_eq!(dist.sample(rng), (true, 1)); + assert_eq!(dist.sample(rng), (false, 3)); + assert_eq!(dist.sample(rng), (true, 2)); } } diff --git a/core/src/math/space.rs b/core/src/math/space.rs index 7e37f0d8..49fa6bbd 100644 --- a/core/src/math/space.rs +++ b/core/src/math/space.rs @@ -2,14 +2,14 @@ //! //! TODO +use core::fmt::{Debug, Formatter}; use core::marker::PhantomData; -use crate::math::vary::{Iter, Vary}; +use crate::math::vary::{Iter, Vary, ZDiv}; /// Trait for types representing elements of an affine space. /// -/// # TODO -/// * More documentation, definition of affine space +/// TODO More documentation, definition of affine space pub trait Affine: Sized { /// The space that `Self` is the element type of. type Space; @@ -40,8 +40,7 @@ pub trait Affine: Sized { /// * The type has an additive identity, returned by the [`zero`][Self::zero] method /// * Every value has an additive inverse, returned by the [`neg`][Self::neg] method /// -/// # TODO -/// * More documentation +/// TODO More documentation pub trait Linear: Affine { /// The scalar type associated with `Self`. type Scalar: Sized; @@ -67,12 +66,14 @@ pub trait Linear: Affine { fn mul(&self, scalar: Self::Scalar) -> Self; } -/// Tag type for real vector spaces (Euclidean spaces) of dimension `DIM`. +/// Tag type for real vector spaces and Euclidean spaces. +/// /// For example, the type `Real<3>` corresponds to ℝ³. #[derive(Copy, Clone, Default, Eq, PartialEq)] pub struct Real(PhantomData); /// Tag type for the projective 4-space over reals, 𝗣4(ℝ). +/// /// The properties of this space make it useful for implementing perspective /// projection. Clipping is also done in the projective space. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] @@ -155,7 +156,7 @@ impl Affine for u32 { impl Vary for V where - Self: Linear, + Self: Affine + Clone> + ZDiv, { type Iter = Iter; type Diff = ::Diff; @@ -174,9 +175,20 @@ where fn step(&self, delta: &Self::Diff) -> Self { self.add(delta) } +} - fn z_div(&self, z: f32) -> Self { - self.mul(z.recip()) +impl Debug for Real +where + B: Debug + Default, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + const DIMS: [&str; 4] = ["", "²", "³", "⁴"]; + let b = B::default(); + if let Some(dim) = DIMS.get(DIM - 1) { + write!(f, "ℝ{dim}<{b:?}>") + } else { + write!(f, "ℝ^{DIM}<{b:?}>") + } } } diff --git a/core/src/math/spline.rs b/core/src/math/spline.rs index 796e1755..819a3272 100644 --- a/core/src/math/spline.rs +++ b/core/src/math/spline.rs @@ -1,23 +1,26 @@ -//! Bezier curves and splines. +//! Bézier curves and splines. -use alloc::vec::Vec; +use alloc::{vec, vec::Vec}; +use core::{array, fmt::Debug}; -use crate::math::space::Linear; -use crate::math::Vary; +use crate::geom::Ray; +use crate::math::{Affine, Lerp, Linear}; -/// A cubic Bezier curve, defined by four control points. +/// A cubic Bézier curve, defined by four control points. /// -/// TODO More info about Beziers +/// TODO More info about Béziers /// /// ```text -/// p3 -/// p1 ____ \ -/// \ _-´ `--_ \ -/// \ / `-_ |\ -/// \ | `-_ | \ -/// \| `-_ | \ -/// \ `-__ / \ -/// p0 `---____-´ p2 +/// +/// p1 +/// \ ____ +/// \ _-´ `--_ p3 +/// \ / `-_ \ +/// \ | `-_ |\ +/// \| `-__ / \ +/// \ `---_____--´ \ +/// p0 \ +/// p2 /// ``` #[derive(Debug, Clone, Eq, PartialEq)] pub struct CubicBezier(pub [T; 4]); @@ -56,13 +59,14 @@ where impl CubicBezier where - // T: Affine + Clone, - // T::Diff: Linear + Clone, - T: Linear + Clone, + T: Affine> + Clone, { - /// Evaluates the value of `self` at `t` + /// Evaluates the value of `self` at `t`. + /// + /// For t < 0, returns the first control point. For t > 1, returns the last + /// control point. Uses [De Casteljau's algorithm][1]. /// - /// Uses De Casteljau's algorithm. + /// [1]: https://en.wikipedia.org/wiki/De_Casteljau%27s_algorithm pub fn eval(&self, t: f32) -> T { let [p0, p1, p2, p3] = &self.0; step(t, p0, p3, |t| { @@ -72,57 +76,99 @@ where p01.lerp(&p12, t).lerp(&p12.lerp(&p23, t), t) }) } -} -impl CubicBezier -where - T: Linear + Clone, -{ + + /// Evaluates the value of `self` at `t`. + /// + /// For t < 0, returns the first control point. For t > 1, returns the last + /// control point. + /// + /// Directly evaluates the cubic. Faster but possibly less numerically + /// stable than [`Self::eval`]. pub fn fast_eval(&self, t: f32) -> T { - let [p0, p1, p2, p3] = &self.0; + let [p0, .., p3] = &self.0; step(t, p0, p3, |t| { - // (p3 - p0) * t^3 + (p1 - p2) * 3t^3 - // + (p0 + p2) * 3t^2 - p1 * 6t^2 - // + (p1 - p0) * 3t - // + p0 - - let term3 = &p1.sub(p2).mul(3.0).add(&p3.sub(p0)).mul(t); - let term2 = &p1.mul(-2.0).add(p0).add(p2).mul(3.0); - let term1 = &p1.sub(p0).mul(3.0 * t); - let term0 = p0; - - term3.add(term2).mul(t * t).add(term1).add(term0) + // Add a linear combination of the three coefficients + // to `p0` to get the result + let [co3, co2, co1] = self.coefficients(); + p0.add(&co3.mul(t).add(&co2).mul(t).add(&co1).mul(t)) }) } /// Returns the tangent, or direction vector, of `self` at `t`. /// /// Clamps `t` to the range [0, 1]. - pub fn tangent(&self, t: f32) -> T { + pub fn tangent(&self, t: f32) -> T::Diff { let [p0, p1, p2, p3] = &self.0; let t = t.clamp(0.0, 1.0); - // (p1 - p2) * 9t^2 + (p3 - p0) * 3t^2 - // + (p0 + p2) * 6t - p1 * 12t - // + (p1 - p0) * 3 + // 3 (3 (p1 - p2) + (p3 - p0)) * t^2 + // + 6 ((p0 - p1 + p2 - p1) * t + // + 3 (p1 - p0) - let term2 = p1.sub(p2).mul(3.0).add(&p3.sub(p0)).mul(t * t); - let term1 = p1.mul(-2.0).add(p0).add(p2).mul(2.0 * t); - let term0 = p1.sub(p0); + let co2: T::Diff = p1.sub(p2).mul(3.0).add(&p3.sub(p0)); + let co1: T::Diff = p0.sub(p1).add(&p2.sub(p1)).mul(2.0); + let co0: T::Diff = p1.sub(p0); - term2.add(&term1).add(&term0).mul(3.0) + co2.mul(t).add(&co1).mul(t).add(&co0).mul(3.0) + } + + /// Returns the coefficients used to evaluate the spline. + /// + /// These are constant as long as the control points do not change, + /// so they can be precomputed when the spline is evaluated several times, + /// for example by an iterator. + /// + /// The coefficient values are, from the first to the last: + /// ```text + /// co3 = (p3 - p0) + 3 * (p1 - p2) + /// co2 = 3 * (p0 - p1) + 3 * (p2 - p1) + /// co1 = 3 * (p1 - p0) + /// ``` + /// The value of the spline at *t* is then computed as: + /// ```text + /// co3 * t^3 + co2 * t^2 + co1 * t + p0 + /// + /// = (((co3 * t) + co2 * t) + co1 * t) + p0. + /// ``` + fn coefficients(&self) -> [T::Diff; 3] { + let [p0, p1, p2, p3] = &self.0; + + // Rewrite the parametric equation into a form where three of the + // coefficients are vectors, their linear combination added to `p0` + // so the equation can be expressed for affine types: + // + // (p3 - p0) * t^3 + (p1 - p2) * 3t^3 + // + (p0 + p2) * 3t^2 - p1 * 6t^2 + // + (p1 - p0) * 3t + // + p0 + // = ((p3 - p0 + 3(p1 - p2)) * t^3 + // + 3(p0 - p1 + p2 - p1) * t^2 + // + 3(p1 - p0) * t + // + p0 + // = ((((p3 - p0 + 3(p1 - p2))) * t + // + 3(p0 - p1 + p2 - p1)) * t) + // + 3(p1 - p0)) * t) + // + p0 + let p3_p0 = p3.sub(p0); + let p1_p0_3 = p1.sub(p0).mul(3.0); + let p1_p2_3 = p1.sub(p2).mul(3.0); + [p3_p0.add(&p1_p2_3), p1_p0_3.add(&p1_p2_3).neg(), p1_p0_3] } } /// A curve composed of one or more concatenated -/// [cubic Bezier curves][CubicBezier]. +/// [cubic Bézier curves][CubicBezier]. #[derive(Debug, Clone, Eq, PartialEq)] pub struct BezierSpline(Vec); -impl + Copy> BezierSpline { - /// Creates a bezier curve from the given control points. The number of +impl BezierSpline +where + T: Affine + Clone> + Clone, +{ + /// Creates a Bézier curve from the given control points. The number of /// elements in `pts` must be 3n + 1 for some positive integer n. /// - /// Consecutive points in `pts` make up Bezier curves such that: + /// Consecutive points in `pts` make up Bézier curves such that: /// * `pts[0..=3]` define the first curve, /// * `pts[3..=6]` define the second curve, /// @@ -139,6 +185,26 @@ impl + Copy> BezierSpline { Self(pts.to_vec()) } + pub fn from_rays(rays: I) -> Self + where + I: IntoIterator>, + { + let mut rays = rays.into_iter().peekable(); + let mut first = true; + let mut pts = vec![]; + while let Some(Ray(p, v)) = rays.next() { + if !first { + pts.push(p.add(&v.neg())); + } + first = false; + pts.push(p.clone()); + if rays.peek().is_some() { + pts.push(p.add(&v)); + } + } + Self::new(&pts) + } + /// Evaluates `self` at position `t`. /// /// Returns the first point if `t` < 0 and the last point if `t` > 1. @@ -153,7 +219,7 @@ impl + Copy> BezierSpline { /// Returns the tangent of `self` at `t`. /// /// Clamps `t` to the range [0, 1]. - pub fn tangent(&self, t: f32) -> T { + pub fn tangent(&self, t: f32) -> T::Diff { let (t, seg) = self.segment(t); CubicBezier(seg).tangent(t) } @@ -164,7 +230,7 @@ impl + Copy> BezierSpline { let seg = ((t * segs) as u32 as f32).min(segs - 1.0); let t2 = t * segs - seg; let idx = 3 * (seg as usize); - (t2, (self.0[idx..idx + 4]).try_into().unwrap()) + (t2, array::from_fn(|k| self.0[idx + k].clone())) } /// Approximates `self` as a sequence of line segments. @@ -194,18 +260,19 @@ impl + Copy> BezierSpline { /// /// # Examples /// ``` - /// # use retrofire_core::math::{spline::BezierSpline, vec::{vec2, Vec2}}; + /// 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(|err| err.len_sqr() < 0.01*0.01); /// assert_eq!(approx.len(), 17); /// ``` - pub fn approximate(&self, halt: impl Fn(&T) -> bool) -> Vec { + pub fn approximate(&self, halt: impl Fn(&T::Diff) -> bool) -> Vec { 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); - res.push(self.0[len - 1]); + res.push(self.0[len - 1].clone()); res } @@ -214,7 +281,7 @@ impl + Copy> BezierSpline { a: f32, b: f32, max_dep: u32, - halt: &impl Fn(&T) -> bool, + halt: &impl Fn(&T::Diff) -> bool, accum: &mut Vec, ) { let mid = a.lerp(&b, 0.5); @@ -239,7 +306,7 @@ mod tests { use alloc::vec; use crate::assert_approx_eq; - use crate::math::{vec2, Vec2}; + use crate::math::{pt2, vec2, Point2, Vec2}; use super::*; @@ -270,7 +337,7 @@ mod tests { } #[test] - fn bezier_spline_eval_eq_fasteval() { + fn bezier_spline_eval_eq_fast_eval() { let b: CubicBezier = CubicBezier( [[0.0, 0.0], [0.0, 2.0], [1.0, -1.0], [1.0, 1.0]].map(Vec2::from), ); @@ -296,7 +363,7 @@ mod tests { } #[test] - fn bezier_spline_eval_2d() { + fn bezier_spline_eval_2d_vec() { let b = CubicBezier( [[0.0, 0.0], [0.0, 2.0], [1.0, -1.0], [1.0, 1.0]] .map(Vec2::<()>::from), @@ -311,6 +378,22 @@ mod tests { assert_eq!(b.eval(2.00), vec2(1.0, 1.0)); } + #[test] + fn bezier_spline_eval_2d_point() { + let b = CubicBezier( + [[0.0, 0.0], [0.0, 2.0], [1.0, -1.0], [1.0, 1.0]] + .map(Point2::<()>::from), + ); + + assert_eq!(b.eval(-1.0), pt2(0.0, 0.0)); + assert_eq!(b.eval(0.00), pt2(0.0, 0.0)); + assert_eq!(b.eval(0.25), pt2(0.15625, 0.71875)); + assert_eq!(b.eval(0.50), pt2(0.5, 0.5)); + assert_eq!(b.eval(0.75), pt2(0.84375, 0.281250)); + assert_eq!(b.eval(1.00), pt2(1.0, 1.0)); + assert_eq!(b.eval(2.00), pt2(1.0, 1.0)); + } + #[test] fn bezier_spline_tangent_1d() { let b = CubicBezier([0.0, 2.0, -1.0, 1.0]); @@ -328,7 +411,7 @@ mod tests { fn bezier_spline_tangent_2d() { let b = CubicBezier( [[0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0]] - .map(Vec2::<()>::from), + .map(Point2::<()>::from), ); assert_eq!(b.tangent(-1.0), vec2(0.0, 3.0),); diff --git a/core/src/math/vary.rs b/core/src/math/vary.rs index 5474d4ce..623dd721 100644 --- a/core/src/math/vary.rs +++ b/core/src/math/vary.rs @@ -5,13 +5,22 @@ use core::mem; +use crate::math::Lerp; + +pub trait ZDiv: Sized { + #[must_use] + fn z_div(self, _z: f32) -> Self { + self + } +} + /// A trait for types that can be linearly interpolated and distributed /// between two endpoints. /// /// This trait is designed particularly for *varyings:* types that are /// meant to be interpolated across the face of a polygon when rendering, /// but the methods are useful for various purposes. -pub trait Vary: Sized + Clone { +pub trait Vary: Lerp + ZDiv + Sized + Clone { /// The iterator returned by the [vary][Self::vary] method. type Iter: Iterator; /// The difference type of `Self`. @@ -24,8 +33,10 @@ pub trait Vary: Sized + Clone { /// /// # Examples /// ``` - /// # use retrofire_core::math::vary::Vary; + /// use retrofire_core::math::Vary; + /// /// let mut iter = 0.0f32.vary(0.2, Some(5)); + /// /// assert_eq!(iter.next(), Some(0.0)); /// assert_eq!(iter.next(), Some(0.2)); /// assert_eq!(iter.next(), Some(0.4)); @@ -49,28 +60,6 @@ pub trait Vary: Sized + Clone { /// `self + delta`. #[must_use] fn step(&self, delta: &Self::Diff) -> Self; - - /// Performs perspective division. - #[must_use] - fn z_div(&self, z: f32) -> Self; - - /// Linearly interpolates between `self` and `other`. - /// - /// This method does not panic if `t < 0.0` or `t > 1.0`, or if `t` - /// is a `NaN`, but the return value in those cases is unspecified. - /// Individual implementations may offer stronger guarantees. - /// - /// # Examples - /// ``` - /// # use retrofire_core::math::vary::Vary; - /// assert_eq!(2.0.lerp(&5.0, 0.0), 2.0); - /// assert_eq!(2.0.lerp(&5.0, 0.5), 3.5); - /// assert_eq!(2.0.lerp(&5.0, 1.0), 5.0); - /// ``` - #[inline] - fn lerp(&self, other: &Self, t: f32) -> Self { - self.step(&self.dv_dt(other, t)) - } } #[derive(Copy, Clone, Debug)] @@ -80,11 +69,6 @@ pub struct Iter { pub n: Option, } -#[inline] -pub fn lerp(t: f32, from: V, to: V) -> V { - from.lerp(&to, t) -} - impl Vary for () { type Iter = Iter<()>; type Diff = (); @@ -94,9 +78,8 @@ impl Vary for () { } fn dv_dt(&self, _: &Self, _: f32) {} fn step(&self, _: &Self::Diff) {} - - fn z_div(&self, _: f32) {} } +impl ZDiv for () {} impl Vary for (T, U) { type Iter = Iter; @@ -114,12 +97,19 @@ impl Vary for (T, U) { fn step(&self, (d0, d1): &Self::Diff) -> Self { (self.0.step(d0), self.1.step(d1)) } - - fn z_div(&self, z: f32) -> Self { +} +impl ZDiv for (T, U) { + fn z_div(self, z: f32) -> Self { (self.0.z_div(z), self.1.z_div(z)) } } +impl ZDiv for f32 { + fn z_div(self, z: f32) -> Self { + self / z + } +} + impl Iterator for Iter { type Item = T; diff --git a/core/src/math/vec.rs b/core/src/math/vec.rs index c5432ebc..868ca2ad 100644 --- a/core/src/math/vec.rs +++ b/core/src/math/vec.rs @@ -2,16 +2,21 @@ //! //! TODO -use core::array; -use core::fmt::{Debug, Formatter}; -use core::iter::Sum; -use core::marker::PhantomData; -use core::ops::{Add, Div, Index, IndexMut, Mul, Neg, Sub}; -use core::ops::{AddAssign, DivAssign, MulAssign, SubAssign}; - -use crate::math::approx::ApproxEq; -use crate::math::float::f32; -use crate::math::space::{Affine, Linear, Proj4, Real}; +use core::{ + array, + fmt::{Debug, Formatter}, + iter::Sum, + marker::PhantomData as Pd, + ops::{Add, Div, Index, IndexMut, Mul, Neg, Sub}, + ops::{AddAssign, DivAssign, MulAssign, SubAssign}, +}; + +use crate::math::{ + float::f32, + space::{Proj4, Real}, + vary::ZDiv, + Affine, ApproxEq, Linear, Point, +}; // // Types @@ -28,13 +33,13 @@ use crate::math::space::{Affine, Linear, Proj4, Real}; /// prevent mixing up vectors of different spaces and bases. /// /// # Examples -/// TODO +/// TODO examples #[repr(transparent)] -pub struct Vector(pub Repr, PhantomData); +pub struct Vector(pub Repr, Pd); /// A 2-vector with `f32` components. pub type Vec2 = Vector<[f32; 2], Real<2, Basis>>; -/// A 2-vector with `f32` components. +/// A 3-vector with `f32` components. pub type Vec3 = Vector<[f32; 3], Real<3, Basis>>; /// A `f32` 4-vector in the projective 3-space over ℝ, aka P3(ℝ). pub type ProjVec4 = Vector<[f32; 4], Proj4>; @@ -54,12 +59,12 @@ pub type Vec2u = Vector<[u32; 2], Real<2, Basis>>; /// Returns a real 2-vector with components `x` and `y`. pub const fn vec2(x: Sc, y: Sc) -> Vector<[Sc; 2], Real<2, B>> { - Vector([x, y], PhantomData) + Vector([x, y], Pd) } /// Returns a real 3-vector with components `x`, `y`, and `z`. pub const fn vec3(x: Sc, y: Sc, z: Sc) -> Vector<[Sc; 3], Real<3, B>> { - Vector([x, y, z], PhantomData) + Vector([x, y, z], Pd) } /// Returns a vector with all components equal to a scalar. @@ -68,13 +73,12 @@ pub const fn vec3(x: Sc, y: Sc, z: Sc) -> Vector<[Sc; 3], Real<3, B>> { /// /// # Examples /// ``` -/// # use retrofire_core::math::{vec3, Vec3}; -/// # use retrofire_core::math::vec::splat; +/// use retrofire_core::math::{vec3, Vec3, splat}; /// let v: Vec3 = splat(1.23); /// assert_eq!(v, vec3(1.23, 1.23, 1.23)); #[inline] pub fn splat(s: Sc) -> Vector<[Sc; DIM], Sp> { - s.into() + array::from_fn(|_| s.clone()).into() } // @@ -85,7 +89,7 @@ impl Vector { /// Returns a new vector with representation `repr`. #[inline] pub const fn new(repr: R) -> Self { - Self(repr, PhantomData) + Self(repr, Pd) } /// Returns a vector with value equal to `self` but in space `S`. @@ -99,6 +103,13 @@ impl Vector { pub fn to(self) -> Vector { 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 { + Point::new(self.0) + } } // TODO Many of these functions could be more generic @@ -114,8 +125,9 @@ impl Vector<[f32; N], Sp> { /// /// # Examples /// ``` - /// # use retrofire_core::math::vec::*; - /// # use retrofire_core::assert_approx_eq; + /// use retrofire_core::assert_approx_eq; + /// use retrofire_core::math::{vec2, Vec2}; + /// /// let normalized: Vec2 = vec2(3.0, 4.0).normalize(); /// assert_approx_eq!(normalized, vec2(0.6, 0.8), eps=1e-2); /// assert_approx_eq!(normalized.len_sqr(), 1.0, eps=1e-2); @@ -142,11 +154,15 @@ impl Vector<[f32; N], Sp> { /// /// # Examples /// ``` - /// # use retrofire_core::math::vec::{vec3, Vec3, splat}; + /// use retrofire_core::math::vec::{vec3, Vec3, splat}; /// let v: Vec3 = vec3(0.5, 1.5, -2.0); + /// /// // Clamp to the unit cube - /// let v = v.clamp(&splat(-1.0), &splat(1.0)); - /// assert_eq!(v, vec3(0.5, 1.0, -1.0)); + /// let clamped = v.clamp(&splat(-1.0), &splat(1.0)); + /// assert_eq!(clamped, vec3(0.5, 1.0, -1.0)); + // TODO f32 and f64 have inherent clamp methods because they're not Ord. + // A generic clamp for Sc: Ord would conflict with this one. There is + // currently no clean way to support both floats and impl Ord types. #[must_use] pub fn clamp(&self, min: &Self, max: &Self) -> Self { array::from_fn(|i| self[i].clamp(min[i], max[i])).into() @@ -206,13 +222,15 @@ where { other.mul(self.scalar_project(other)) } +} +impl Vector<[Sc; N], Sp> { /// Returns a vector of the same dimension as `self` by applying `f` /// component-wise. #[inline] #[must_use] pub fn map(self, mut f: impl FnMut(Sc) -> T) -> Vector<[T; N], Sp> { - array::from_fn(|i| f(self[i])).into() + array::from_fn(|i| f(self.0[i])).into() } } @@ -325,10 +343,10 @@ where impl Affine for Vector<[Sc; DIM], Sp> where - Sc: Affine, - Sc::Diff: Linear + Copy, + Sc: Affine + Copy>, { type Space = Sp; + // TODO Vectors always Linear once Point used for affine stuff type Diff = Vector<[Sc::Diff; DIM], Sp>; /// The dimension (number of components) of `Self`. @@ -337,17 +355,16 @@ where #[inline] fn add(&self, other: &Self::Diff) -> Self { // TODO Profile performance of array::from_fn - array::from_fn(|i| self.0[i].add(&other.0[i])).into() + Self(array::from_fn(|i| self.0[i].add(&other.0[i])), Pd) } #[inline] fn sub(&self, other: &Self) -> Self::Diff { - array::from_fn(|i| self.0[i].sub(&other.0[i])).into() + Vector(array::from_fn(|i| self.0[i].sub(&other.0[i])), Pd) } } impl Linear for Vector<[Sc; DIM], Sp> where - Self: Affine, Sc: Linear + Copy, { type Scalar = Sc; @@ -359,11 +376,20 @@ where } #[inline] fn neg(&self) -> Self { - array::from_fn(|i| self.0[i].neg()).into() + self.map(|c| c.neg()) } #[inline] fn mul(&self, scalar: Self::Scalar) -> Self { - array::from_fn(|i| self.0[i].mul(scalar)).into() + self.map(|c| c.mul(scalar)) + } +} + +impl ZDiv for Vector<[Sc; N], Sp> +where + Sc: ZDiv + Copy, +{ + fn z_div(self, z: f32) -> Self { + self.map(|c| c.z_div(z)) } } @@ -389,13 +415,13 @@ impl Copy for Vector {} impl Clone for Vector { fn clone(&self) -> Self { - Self(self.0.clone(), PhantomData) + Self(self.0.clone(), Pd) } } impl Default for Vector { fn default() -> Self { - Self(R::default(), PhantomData) + Self(R::default(), Pd) } } @@ -407,21 +433,6 @@ impl PartialEq for Vector { } } -impl Debug for Real -where - B: Debug + Default, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - const DIMS: [char; 3] = ['²', '³', '⁴']; - let b = B::default(); - if let Some(dim) = DIMS.get(DIM - 2) { - write!(f, "ℝ{dim}<{b:?}>") - } else { - write!(f, "ℝ^{DIM}<{b:?}>") - } - } -} - impl Debug for Vector { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { write!(f, "Vec<{:?}>", Sp::default())?; @@ -431,8 +442,8 @@ impl Debug for Vector { impl From for Vector { #[inline] - fn from(els: R) -> Self { - Self(els, PhantomData) + fn from(repr: R) -> Self { + Self(repr, Pd) } } @@ -442,7 +453,7 @@ impl From for Vector<[Sc; DIM], Sp> { /// This operation is also called "splat" or "broadcast". #[inline] fn from(scalar: Sc) -> Self { - array::from_fn(|_| scalar.clone()).into() + splat(scalar) } } @@ -506,7 +517,7 @@ macro_rules! impl_op { Self: $bnd, { type Output = Self; - /// TODO + /// TODO docs for macro-generated operators #[inline] fn $method(mut self, rhs: $rhs) -> Self { self $op rhs; self @@ -562,7 +573,7 @@ where { #[inline] fn div_assign(&mut self, rhs: f32) { - debug_assert!(f32::abs(rhs) > 1e-7); + debug_assert!(f32::abs(rhs) > 1e-7, "divisor {rhs} < epsilon"); *self = Linear::mul(&*self, rhs.recip()); } } diff --git a/core/src/render.rs b/core/src/render.rs index ffc14e3e..afbeadef 100644 --- a/core/src/render.rs +++ b/core/src/render.rs @@ -11,16 +11,27 @@ use core::fmt::Debug; use crate::geom::{Tri, Vertex}; use crate::math::{ mat::{Mat4x4, RealToProj, RealToReal}, - vary::Vary, - vec::{vec3, ProjVec4, Vec3}, + vary::{Vary, ZDiv}, + vec::{vec3, ProjVec4}, + Lerp, }; -use clip::{view_frustum, Clip, ClipVert}; -use ctx::{Context, DepthSort, FaceCull}; -use raster::tri_fill; -use shader::{FragmentShader, VertexShader}; -use stats::Stats; -use target::Target; +use { + clip::{view_frustum, ClipVert}, + ctx::{DepthSort, FaceCull}, + raster::{tri_fill, ScreenPt}, +}; + +pub use { + batch::Batch, + cam::Camera, + clip::Clip, + ctx::Context, + shader::{FragmentShader, VertexShader}, + stats::Stats, + target::{Framebuf, Target}, + tex::{uv, TexCoord, Texture}, +}; pub mod batch; pub mod cam; @@ -82,7 +93,7 @@ impl Shader for S where } /// Renders the given triangles into `target`. -pub fn render( +pub fn render( tris: impl AsRef<[Tri]>, verts: impl AsRef<[Vtx]>, shader: &Shd, @@ -93,32 +104,34 @@ pub fn render( ) where Shd: Shader, { + let verts = verts.as_ref(); + let tris = tris.as_ref(); let mut stats = Stats::start(); stats.calls = 1.0; - stats.prims.i += tris.as_ref().len(); - stats.verts.i += verts.as_ref().len(); + stats.prims.i += tris.len(); + stats.verts.i += verts.len(); // Vertex shader: transform vertices to clip space let verts: Vec<_> = verts - .as_ref() .iter() // TODO Pass vertex as ref to shader .cloned() - .map(|v| ClipVert::new(shader.shade_vertex(v, uniform))) + .map(|v| shader.shade_vertex(v, uniform)) + .map(ClipVert::new) .collect(); // Map triangle vertex indices to actual vertices let tris: Vec<_> = tris - .as_ref() .iter() .map(|Tri(vs)| Tri(vs.map(|i| verts[i].clone()))) .collect(); // Clip against the view frustum let mut clipped = vec![]; - tris.clip(&view_frustum::PLANES, &mut clipped); + view_frustum::clip(&tris[..], &mut clipped); + // Optional depth sorting for use case such as transparency if let Some(d) = ctx.depth_sort { depth_sort(&mut clipped, d); } @@ -136,13 +149,15 @@ pub fn render( let pos = vec3(x, y, 1.0).z_div(w); Vertex { // Viewport transform - pos: to_screen.apply(&pos), + pos: to_screen.apply(&pos).to_pt(), // Perspective correction attrib: v.attrib.z_div(w), } }); // Back/frontface culling + // + // TODO This could also be done earlier, before or as part of clipping match ctx.face_cull { Some(FaceCull::Back) if is_backface(&vs) => continue, Some(FaceCull::Front) if !is_backface(&vs) => continue, @@ -174,7 +189,7 @@ fn depth_sort(tris: &mut [Tri>], d: DepthSort) { }); } -fn is_backface(vs: &[Vertex, V>]) -> bool { +fn is_backface(vs: &[Vertex]) -> bool { let v = vs[1].pos - vs[0].pos; let u = vs[2].pos - vs[0].pos; v[0] * u[1] - v[1] * u[0] > 0.0 diff --git a/core/src/render/batch.rs b/core/src/render/batch.rs index 9de94c1e..33d39ebb 100644 --- a/core/src/render/batch.rs +++ b/core/src/render/batch.rs @@ -3,10 +3,12 @@ use alloc::vec::Vec; use core::borrow::Borrow; -use crate::geom::{mesh, Mesh, Tri}; -use crate::math::{mat::Mat4x4, vary::Vary}; +use crate::{ + geom::{Mesh, Tri, Vertex3}, + math::{mat::Mat4x4, vary::Vary, Lerp}, +}; -use super::{ctx::Context, target::Target, NdcToScreen, Shader}; +use super::{Context, NdcToScreen, Shader, Target}; /// A builder for rendering a chunk of geometry as a batch. /// @@ -77,7 +79,7 @@ impl Batch { pub fn mesh( self, mesh: &Mesh, - ) -> Batch, Uni, Shd, Tgt, Ctx> { + ) -> Batch, Uni, Shd, Tgt, Ctx> { let faces = mesh.faces.clone(); let verts = mesh.verts.clone(); update!(verts faces; self uniform shader viewport target ctx) @@ -118,7 +120,7 @@ where Ctx: Borrow, { /// Renders this batch of geometry. - pub fn render(&mut self) + pub fn render(&mut self) where Shd: Shader, { diff --git a/core/src/render/cam.rs b/core/src/render/cam.rs index 3f8382f6..397c333a 100644 --- a/core/src/render/cam.rs +++ b/core/src/render/cam.rs @@ -6,25 +6,16 @@ use crate::geom::{Tri, Vertex}; use crate::math::{ angle::{spherical, turns, SphericalVec}, mat::{orthographic, perspective, viewport, Mat4x4, RealToReal}, - space::Linear, - vary::Vary, - vec::{vec2, Vec3}, + pt2, Lerp, Linear, Point3, Vary, Vec3, }; use crate::util::{rect::Rect, Dims}; #[cfg(feature = "fp")] -use crate::math::{ - angle::Angle, - mat::{orient_z, translate}, - vec::vec3, -}; +use crate::math::{orient_z, translate, vec3, Angle}; use super::{ - clip::ClipVec, - ctx::Context, - shader::{FragmentShader, VertexShader}, - target::Target, - NdcToScreen, RealToProj, ViewToProj, World, WorldToView, + clip::ClipVec, Context, FragmentShader, NdcToScreen, RealToProj, Target, + VertexShader, ViewToProj, World, WorldToView, }; /// Camera movement mode. @@ -69,7 +60,7 @@ impl Camera<()> { pub fn new(dims: Dims) -> Self { Self { dims, - viewport: viewport(vec2(0, 0)..vec2(dims.0, dims.1)), + viewport: viewport(pt2(0, 0)..pt2(dims.0, dims.1)), ..Default::default() } } @@ -97,7 +88,7 @@ impl Camera { Self { dims: (r.abs_diff(l), b.abs_diff(t)), - viewport: viewport(vec2(l, t)..vec2(r, b)), + viewport: viewport(pt2(l, t)..pt2(r, b)), ..self } } @@ -114,7 +105,7 @@ impl Camera { } /// Sets up orthographic projection. - pub fn orthographic(mut self, bounds: Range) -> Self { + pub fn orthographic(mut self, bounds: Range) -> Self { self.project = orthographic(bounds.start, bounds.end); self } @@ -127,7 +118,7 @@ impl Camera { } /// Renders the given geometry from the viewpoint of this camera. - pub fn render( + pub fn render( &self, tris: impl AsRef<[Tri]>, verts: impl AsRef<[Vtx]>, @@ -193,13 +184,8 @@ impl FirstPerson { let up = vec3(0.0, 1.0, 0.0); let right = up.cross(&fwd); - // / rx ux fx \ / dx \ / rx ry rz \ T / dx \ - // | ry uy fy | | dy | = | ux uy uz | | dy | - // \ rz uz fz / \ dz / \ fx fy fz / \ dz / - - self.pos += Mat4x4::>::from_basis(right, up, fwd) - .transpose() - .apply(&delta); + self.pos += + Mat4x4::>::from_basis(right, up, fwd).apply(&delta); } } @@ -215,8 +201,9 @@ impl Mode for FirstPerson { let fwd = self.heading; let right = vec3(0.0, 1.0, 0.0).cross(&fwd_move.to_cart()); + // World to view is the inverse of the camera's world transform let transl = translate(-pos); - let orient = orient_z(fwd.into(), right); + let orient = orient_z(fwd.into(), right).transpose(); transl.then(&orient).to() } diff --git a/core/src/render/clip.rs b/core/src/render/clip.rs index b204cf50..357007e4 100644 --- a/core/src/render/clip.rs +++ b/core/src/render/clip.rs @@ -13,15 +13,14 @@ //! doing it for every scanline individually. //! -use alloc::vec; -use alloc::vec::Vec; - -use crate::geom::{Plane, Tri, Vertex}; -use crate::math::vary::Vary; -use crate::math::vec::{ProjVec4, Vec3}; +use alloc::{vec, vec::Vec}; +use core::iter::zip; use view_frustum::{outcode, status}; +use crate::geom::{vertex, Tri, Vertex}; +use crate::math::{vec::ProjVec4, Lerp}; + /// Trait for types that can be [clipped][self] against planes. /// /// # Note to implementors @@ -53,41 +52,31 @@ pub trait Clip { /// A vector in clip space. pub type ClipVec = ProjVec4; -/// A plane in clip space. -pub type ClipPlane = Plane; - /// A vertex in clip space. #[derive(Copy, Clone, Debug, PartialEq)] pub struct ClipVert { pub pos: ClipVec, pub attrib: A, - outcode: Outcode, + outcode: u8, } -/// Records whether a point is inside or outside of each frustum plane. -/// -/// Each plane is represented by a single bit, 1 meaning "inside". -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -struct Outcode(u8); - /// Visibility status of a polygon. enum Status { /// Entirely inside view frustum Visible, - /// Only partly inside, needs clipping + /// Either outside or partly inside, needs clipping Clipped, /// Entirely outside view frustum Hidden, } +#[derive(Debug, Copy, Clone)] +pub struct ClipPlane(ClipVec, u8); + impl ClipPlane { - /// Creates a clip plane given a normal and offset from origin. - /// - /// TODO Floating-point arithmetic is not permitted in const functions - /// so the offset must be negated for now. - pub const fn new(normal: Vec3, neg_offset: f32) -> Self { - let [x, y, z] = normal.0; - Self(ClipVec::new([x, y, z, neg_offset])) + /// Creates a clip plane given a normal, offset, and outcode bit. + const fn new(x: f32, y: f32, z: f32, off: f32, bit: u8) -> Self { + Self(ClipVec::new([x, y, z, -off]), bit) } /// Returns the signed distance between `pt` and `self`. @@ -112,6 +101,22 @@ impl ClipPlane { self.0.dot(pt) } + /// Computes the outcode bit for `pt`. + /// + /// The result is `self.1` if `pt` is outside this plane, 0 otherwise. + #[inline] + pub fn outcode(&self, pt: &ClipVec) -> u8 { + (self.signed_dist(pt) > 0.0) as u8 * self.1 + } + + /// Checks the outcode of `v` against `self`. + /// + /// Returns `true` if this plane's outcode bit is 0, `false` otherwise. + #[inline] + pub fn is_inside(&self, v: &ClipVert) -> bool { + self.1 & v.outcode == 0 + } + /// Clips the convex polygon given by `verts_in` against `self` and /// returns the resulting vertices in `verts_out`. /// @@ -121,29 +126,27 @@ impl ClipPlane { /// /// ```text /// b - /// / \ - /// / \ outside - /// ------p-----q-----self--- - /// / \ inside - /// a-__ \ + /// / \ outside + /// / \ + /// ------p-----q-------self----- + /// / \ + /// a-__ \ inside /// `--__ \ /// `--c /// ``` - pub fn clip_simple_polygon( + pub fn clip_simple_polygon( &self, verts_in: &[ClipVert], verts_out: &mut Vec>, ) { - let mut verts = verts_in - .iter() - .chain(&verts_in[..1]) - .map(|v| (v, self.signed_dist(&v.pos))); + let mut verts = verts_in.iter().chain(&verts_in[..1]); - let Some((mut v0, mut d0)) = &verts.next() else { + let Some(mut v0) = verts.next() else { return; }; - for (v1, d1) in verts { - if d0 <= 0.0 { + + for v1 in verts { + if self.is_inside(v0) { // v0 is inside; emit it as-is. If v1 is also inside, we don't // have to do anything; it is emitted on the next iteration. verts_out.push((*v0).clone()); @@ -151,9 +154,14 @@ impl ClipPlane { // v0 is outside, discard it. If v1 is also outside, we don't // have to do anything; it is discarded on the next iteration. } + // TODO Doesn't use is_inside because it can't distinguish the case + // where a vertex lies exactly on the plane. Though that's mostly + // a theoretical edge case (heh). + let d0 = self.signed_dist(&v0.pos); + let d1 = self.signed_dist(&v1.pos); if d0 * d1 < 0.0 { - // Edge crosses the plane surface. Split the edge in two by - // interpolating and emitting a new vertex at intersection. + // The edge crosses the plane surface. Split the edge in two + // by interpolating and emitting a new vertex at intersection. // The new vertex becomes one of the endpoints of a new "clip" // edge coincident with the plane. @@ -161,13 +169,12 @@ impl ClipPlane { // point. If condition guarantees that `d1 - d0` is nonzero. let t = -d0 / (d1 - d0); - verts_out.push(ClipVert { - pos: v0.pos.lerp(&v1.pos, t), - attrib: v0.attrib.lerp(&v1.attrib, t), - outcode: Outcode(0b111111), // inside! - }); + verts_out.push(ClipVert::new(vertex( + v0.pos.lerp(&v1.pos, t), + v0.attrib.lerp(&v1.attrib, t), + ))); } - (v0, d0) = (v1, d1); + v0 = v1; } } } @@ -186,47 +193,57 @@ impl ClipPlane { /// /// TODO Describe clip space pub mod view_frustum { - use crate::geom::Plane; - use crate::math::vec3; - use super::*; /// The near, far, left, right, bottom, and top clipping planes, /// in that order. pub const PLANES: [ClipPlane; 6] = [ - Plane::new(vec3(0.0, 0.0, -1.0), -1.0), // Near - Plane::new(vec3(0.0, 0.0, 1.0), -1.0), // Far - Plane::new(vec3(-1.0, 0.0, 0.0), -1.0), // Left - Plane::new(vec3(1.0, 0.0, 0.0), -1.0), // Right - Plane::new(vec3(0.0, -1.0, 0.0), -1.0), // Bottom - Plane::new(vec3(0.0, 1.0, 0.0), -1.0), // Top + ClipPlane::new(0.0, 0.0, -1.0, 1.0, 1), // Near + ClipPlane::new(0.0, 0.0, 1.0, 1.0, 2), // Far + ClipPlane::new(-1.0, 0.0, 0.0, 1.0, 4), // Left + ClipPlane::new(1.0, 0.0, 0.0, 1.0, 8), // Right + ClipPlane::new(0.0, -1.0, 0.0, 1.0, 16), // Bottom + ClipPlane::new(0.0, 1.0, 0.0, 1.0, 32), // Top ]; - /// Returns the outcode of the given point. - pub(super) fn outcode(pt: &ClipVec) -> Outcode { - // Top Btm Rgt Lft Far Near - // 1 2 4 8 16 32 - let code = PLANES - .iter() - .fold(0, |code, p| code << 1 | (p.signed_dist(pt) <= 0.0) as u8); + /// Clips geometry against the standard view frustum. + /// + /// This is the main entry point to clipping. + pub fn clip(geom: &G, out: &mut Vec) { + geom.clip(&PLANES, out); + } - Outcode(code) + /// Returns the outcode of the given point. + /// + /// The outcode is a bitset where the bit of each plane is 0 if the point + /// 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. + pub(super) fn outcode(pt: &ClipVec) -> u8 { + PLANES.iter().map(|p| p.outcode(pt)).sum() } /// Returns the visibility status of the polygon given by `vs`. pub(super) fn status(vs: &[ClipVert]) -> Status { - let (all, any) = vs.iter().fold((!0, 0), |(all, any), v| { - (all & v.outcode.0, any | v.outcode.0) - }); - if any != 0b111111 { - // If there's at least one plane that all vertices are outside of, - // then the whole polygon is hidden + // The set of planes outside which all vertices are + let all_outside = vs.iter().fold(!0, |a, b| a & b.outcode); + + // The set of planes outside which at least one vertex is. + let any_outside = vs.iter().fold(0, |a, b| a | b.outcode); + + if all_outside != 0 { + // If all vertices are outside at least one plane, the whole + // polygon is hidden and can be culled. Note that they must be + // outside the *same* lane; it isn't enough that they are all + // outside at least *some* plane! Status::Hidden - } else if all == 0b111111 { - // If each vertex is inside all planes, the polygon is fully visible + } else if any_outside == 0 { + // If no vertex is outside any plane, the whole polygon is visible Status::Visible } else { - // Otherwise the polygon may have to be clipped + // Otherwise, at least one of the vertices is outside the frustum + // and the polygon will have to be clipped (and may end up getting + // culled completely). Status::Clipped } } @@ -246,14 +263,14 @@ pub mod view_frustum { /// /// [^1]: Ivan Sutherland, Gary W. Hodgman: Reentrant Polygon Clipping. /// Communications of the ACM, vol. 17, pp. 32–42, 1974 -pub fn clip_simple_polygon<'a, A: Vary>( - planes: &[Plane], +pub fn clip_simple_polygon<'a, A: Lerp + Clone>( + planes: &[ClipPlane], verts_in: &'a mut Vec>, verts_out: &'a mut Vec>, ) { debug_assert!(verts_out.is_empty()); - for (i, p) in planes.iter().enumerate() { + for (p, i) in zip(planes, 0..) { p.clip_simple_polygon(verts_in, verts_out); if verts_out.is_empty() { // Nothing left to clip; the polygon was fully outside @@ -267,16 +284,13 @@ pub fn clip_simple_polygon<'a, A: Vary>( } impl ClipVert { - pub fn new(v: Vertex) -> Self { - ClipVert { - pos: v.pos, - attrib: v.attrib, - outcode: outcode(&v.pos), - } + pub fn new(Vertex { pos, attrib }: Vertex) -> Self { + let outcode = outcode(&pos); + Self { pos, attrib, outcode } } } -impl Clip for [Tri>] { +impl Clip for [Tri>] { type Item = Tri>; fn clip(&self, planes: &[ClipPlane], out: &mut Vec) { @@ -299,7 +313,7 @@ impl Clip for [Tri>] { verts_in.extend(vs.clone()); clip_simple_polygon(planes, &mut verts_in, &mut verts_out); - if let Some((a, rest)) = verts_out.split_first() { + if let [a, rest @ ..] = &verts_out[..] { // Clipping a triangle results in an n-gon, where n depends on // how many planes the triangle intersects. Turn the resulting // n-gon into a fan of triangles with common vertex `a`, for @@ -326,11 +340,9 @@ impl Clip for [Tri>] { #[cfg(test)] mod tests { - use crate::geom::vertex; - use crate::math::vary::Vary; + use crate::{geom::vertex, math::Vary}; - use super::view_frustum::*; - use super::*; + use super::{view_frustum::*, *}; const FAR_PLANE: ClipPlane = PLANES[1]; @@ -356,25 +368,25 @@ mod tests { #[test] fn outcode_inside() { - assert_eq!(outcode(&vec(0.0, 0.0, 0.0)).0, 0b111111); - assert_eq!(outcode(&vec(1.0, 0.0, 0.0)).0, 0b111111); - assert_eq!(outcode(&vec(0.0, -1.0, 0.0)).0, 0b111111); - assert_eq!(outcode(&vec(0.0, 1.0, 1.0)).0, 0b111111); + assert_eq!(outcode(&vec(0.0, 0.0, 0.0)), 0); + assert_eq!(outcode(&vec(1.0, 0.0, 0.0)), 0); + assert_eq!(outcode(&vec(0.0, -1.0, 0.0)), 0); + assert_eq!(outcode(&vec(0.0, 1.0, 1.0)), 0); } #[test] fn outcode_outside() { // Top Btm Rgt Lft Far Near - // 1 2 4 8 16 32 - - // Outside near == 32 - assert_eq!(outcode(&vec(0.0, 0.0, -1.5)).0, 0b011111); - // Outside right == 4 - assert_eq!(outcode(&vec(2.0, 0.0, 0.0)).0, 0b111011); - // Outside bottom == 2 - assert_eq!(outcode(&vec(0.0, -1.01, 0.0)).0, 0b111101); - // Outside far left == 16|8 - assert_eq!(outcode(&vec(-2.0, 0.0, 2.0)).0, 0b100111); + // 32 16 8 4 2 1 + + // Outside near == 1 + assert_eq!(outcode(&vec(0.0, 0.0, -1.5)), 0b00_0_01); + // Outside right == 8 + assert_eq!(outcode(&vec(2.0, 0.0, 0.0)), 0b00_10_00); + // Outside bottom == 16 + assert_eq!(outcode(&vec(0.0, -1.01, 0.0)), 0b01_00_00); + // Outside far left == 2|4 + assert_eq!(outcode(&vec(-2.0, 0.0, 2.0)), 0b00_01_10); } #[test] @@ -509,7 +521,7 @@ mod tests { } #[test] - fn tri_clip_all_planes_fully_inside() { + fn tri_clip_against_frustum_fully_inside() { let tr = tri( vec(-1.0, -1.0, -1.0), vec(1.0, 1.0, 0.0), @@ -520,7 +532,7 @@ mod tests { assert_eq!(res, &[tr]); } #[test] - fn tri_clip_all_planes_fully_outside() { + fn tri_clip_against_frustum_fully_outside() { // z // ^ // 2-------0 @@ -539,7 +551,7 @@ mod tests { assert_eq!(res, &[]); } #[test] - fn tri_clip_all_planes_result_is_quad() { + fn tri_clip_against_frustum_result_is_quad() { // z // ^ // 2 @@ -565,7 +577,7 @@ mod tests { } #[test] - fn tri_clip_all_planes_result_is_heptagon() { + fn tri_clip_against_frustum_result_is_heptagon() { // z // ^ 2 // · / / @@ -588,9 +600,10 @@ mod tests { } #[test] - fn tri_clip_all_cases() { + #[allow(unused)] + fn tri_clip_against_frustum_all_cases() { // Methodically go through every possible combination of every - // vertex inside/outside of every plane, including degenerate cases. + // vertex inside/outside every plane, including degenerate cases. let xs = || (-2.0).vary(1.0, Some(5)); @@ -606,18 +619,34 @@ mod tests { }); let mut in_tris = 0; + let mut in_degen = 0; let mut out_tris = [0; 8]; + let mut out_degen = 0; + let mut out_total = 0; for tr in tris { let res = &mut vec![]; [tr].clip(&PLANES, res); assert!( res.iter().all(in_bounds), - "clip returned oob vertex:\n from: {:#?}\n to: {:#?}", + "clip returned oob vertex:\n\ + input: {:#?}\n\ + output: {:#?}", tr, &res ); in_tris += 1; + in_degen += is_degenerate(&tr) as u32; out_tris[res.len()] += 1; + out_total += res.len(); + out_degen += res.iter().filter(|t| is_degenerate(t)).count() + } + #[cfg(feature = "std")] + { + use std::dbg; + dbg!(in_tris); + dbg!(in_degen); + dbg!(out_degen); + dbg!(out_total); } assert_eq!(in_tris, 5i32.pow(9)); assert_eq!( @@ -626,9 +655,12 @@ mod tests { ); } - fn in_bounds(tri: &Tri>) -> bool { - tri.0 - .iter() + fn is_degenerate(Tri([a, b, c]): &Tri>) -> bool { + a.pos == b.pos || a.pos == c.pos || b.pos == c.pos + } + + fn in_bounds(Tri(vs): &Tri>) -> bool { + vs.iter() .flat_map(|v| (v.pos / v.pos.w()).0) .all(|a| a.abs() <= 1.00001) } diff --git a/core/src/render/ctx.rs b/core/src/render/ctx.rs index 4953dbc7..e520bcc6 100644 --- a/core/src/render/ctx.rs +++ b/core/src/render/ctx.rs @@ -1,11 +1,10 @@ //! Rendering context and parameters. -use core::cell::RefCell; -use core::cmp::Ordering; +use core::{cell::RefCell, cmp::Ordering}; -use crate::math::color::{rgba, Color4}; +use crate::math::{rgba, Color4}; -use super::stats::Stats; +use super::Stats; /// Context and parameters used by the renderer. #[derive(Clone, Debug)] diff --git a/core/src/render/raster.rs b/core/src/render/raster.rs index b68a2874..75796e62 100644 --- a/core/src/render/raster.rs +++ b/core/src/render/raster.rs @@ -11,18 +11,19 @@ //! passes the depth test, a color is computed by the fragment shader and //! written into the framebuffer. Fragments that fail the test are discarded. -use core::fmt::Debug; -use core::ops::Range; +use core::{fmt::Debug, ops::Range}; -use crate::geom::Vertex; -use crate::math::{Vary, Vec3}; +use crate::{ + geom::Vertex, + math::{point::Point3, Lerp, Vary}, +}; use super::Screen; /// A fragment, or a single "pixel" in a rasterized primitive. #[derive(Clone, Debug)] pub struct Frag { - pub pos: ScreenVec, + pub pos: ScreenPt, pub var: V, } @@ -46,12 +47,12 @@ pub struct ScanlineIter { n: u32, } -/// Vector in screen space. +/// Point in screen space. /// `x` and `y` are viewport pixel coordinates, `z` is depth. -pub type ScreenVec = Vec3; +pub type ScreenPt = Point3; /// Values to interpolate across a rasterized primitive. -pub type Varyings = (ScreenVec, V); +pub type Varyings = (ScreenPt, V); impl Scanline { pub fn fragments(&mut self) -> impl Iterator> + '_ { @@ -106,7 +107,7 @@ impl Iterator for ScanlineIter { /// `scanline_fn` for each scanline. The scanlines are guaranteed to cover /// exactly those pixels whose center point lies inside the triangle. For more /// information on the scanline conversion, see [`scan`]. -pub fn tri_fill(mut verts: [Vertex; 3], mut scanline_fn: F) +pub fn tri_fill(mut verts: [Vertex; 3], mut scanline_fn: F) where V: Vary, F: FnMut(Scanline), @@ -241,11 +242,12 @@ mod tests { use alloc::string::{String, ToString}; use core::iter::once; - use crate::assert_approx_eq; - use crate::geom::vertex; - use crate::math::vary::Vary; - use crate::math::vec3; - use crate::util::buf::Buf2; + use crate::{ + assert_approx_eq, + geom::vertex, + math::{point::pt3, vary::Vary, vary::ZDiv}, + util::buf::Buf2, + }; use super::{tri_fill, Frag, Scanline}; @@ -256,10 +258,10 @@ mod tests { let mut buf = Buf2::new((20, 10)); let verts = [ - vec3(8.0, 0.0, 0.0), - vec3(0.0, 6.0, 0.0), - vec3(14.0, 10.0, 0.0), - vec3(20.0, 3.0, 0.0), + pt3(8.0, 0.0, 0.0), + pt3(0.0, 6.0, 0.0), + pt3(14.0, 10.0, 0.0), + pt3(20.0, 3.0, 0.0), ] .map(|pos| vertex(pos, 0.0)); @@ -299,12 +301,8 @@ mod tests { #[test] fn gradient() { use core::fmt::Write; - let verts = [ - vec3::<_, ()>(15.0, 2.0, 0.0), - vec3(2.0, 8.0, 1.0), - vec3(26.0, 14.0, 0.5), - ] - .map(|pos| vertex(vec3(pos.x(), pos.y(), 1.0), pos.z())); + let verts = [(15.0, 2.0, 0.0), (2.0, 8.0, 1.0), (26.0, 14.0, 0.5)] + .map(|(x, y, val)| vertex(pt3(x, y, 1.0), val)); let expected = r" 0 @@ -341,8 +339,8 @@ mod tests { y: 42, xs: 8..16, vs: Vary::vary_to( - (vec3(8.0, 42.0, 1.0 / w0), 3.0.z_div(w0)), - (vec3(16.0, 42.0, 1.0 / w1), 5.0.z_div(w1)), + (pt3(8.0, 42.0, 1.0 / w0), 3.0f32.z_div(w0)), + (pt3(16.0, 42.0, 1.0 / w1), 5.0f32.z_div(w1)), 8, ), }; @@ -359,7 +357,7 @@ mod tests { let mut x = 8.0; for ((Frag { pos, var }, z), v) in sl.fragments().zip(zs).zip(vars) { - assert_approx_eq!(pos, vec3(x, 42.0, z.recip())); + assert_approx_eq!(pos, pt3(x, 42.0, z.recip())); assert_approx_eq!(var, v); x += 1.0; diff --git a/core/src/render/shader.rs b/core/src/render/shader.rs index 41eec390..ae27dd52 100644 --- a/core/src/render/shader.rs +++ b/core/src/render/shader.rs @@ -14,8 +14,7 @@ //! input any vertex attributes interpolated across the primitive being //! rasterized, such as color, texture coordinate, or normal vector. -use crate::geom::Vertex; -use crate::math::color::Color4; +use crate::{geom::Vertex, math::color::Color4}; use super::raster::Frag; diff --git a/core/src/render/target.rs b/core/src/render/target.rs index 502f7260..c13f158d 100644 --- a/core/src/render/target.rs +++ b/core/src/render/target.rs @@ -4,12 +4,9 @@ //! and possible auxiliary buffers. Special render targets can be used, //! for example, for visibility or occlusion computations. -use crate::math::vary::Vary; -use crate::util::buf::AsMutSlice2; +use crate::{math::Vary, util::buf::AsMutSlice2}; -use super::{ - ctx::Context, raster::Scanline, shader::FragmentShader, stats::Throughput, -}; +use super::{raster::Scanline, stats::Throughput, Context, FragmentShader}; /// Trait for types that can be used as render targets. pub trait Target { diff --git a/core/src/render/tex.rs b/core/src/render/tex.rs index 56a48190..00a8c6eb 100644 --- a/core/src/render/tex.rs +++ b/core/src/render/tex.rs @@ -1,7 +1,9 @@ //! Textures and texture samplers. -use crate::math::vec::{Vec2, Vector}; -use crate::util::buf::{AsSlice2, Buf2, Slice2}; +use crate::{ + math::vec::{Vec2, Vector}, + util::buf::{AsSlice2, Buf2, Slice2}, +}; /// Basis of the texture space. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] @@ -228,7 +230,7 @@ impl SamplerOnce { mod tests { use alloc::vec; - use crate::math::color::{rgb, Color3}; + use crate::math::{rgb, Color3}; use crate::util::buf::Buf2; use super::*; diff --git a/core/src/util/buf.rs b/core/src/util/buf.rs index c3385070..3dc038a8 100644 --- a/core/src/util/buf.rs +++ b/core/src/util/buf.rs @@ -3,10 +3,14 @@ //! Useful for storing pixel data of any kind, among other things. use alloc::{vec, vec::Vec}; -use core::fmt::{Debug, Formatter}; -use core::ops::{Deref, DerefMut}; +use core::{ + fmt::{self, Debug, Formatter}, + iter, + ops::{Deref, DerefMut}, +}; use crate::util::Dims; + use inner::Inner; // @@ -36,24 +40,21 @@ pub trait AsMutSlice2 { /// without explicitly copying the contents to a new, larger buffer. /// /// `Buf2` stores its elements contiguously, in standard row-major order, -/// such that the coordinate pair (x, y) maps to the index -/// ```text -/// buf.width() * y + x -/// ``` +/// such that the coordinate pair (x, y) maps to index `buf.width() * y + x` /// in the backing vector. /// /// # Examples /// ``` -/// # use retrofire_core::util::buf::*; -/// # use retrofire_core::math::vec::*; +/// # use retrofire_core::util::buf::Buf2; +/// # use retrofire_core::math::point::pt2; /// // Elements initialized with `Default::default()` /// let mut buf = Buf2::new((4, 4)); -/// // Indexing with a 2D vector (x, y) yields element at row y, column x: -/// buf[vec2(2, 1)] = 123; +/// // Indexing with a 2D point (x, y) yields element at row y, column x: +/// buf[pt2(2, 1)] = 123; /// // Indexing with an usize i yields row with index i as a slice: -/// assert_eq!(buf[1usize], [0, 0, 123, 0]); +/// assert_eq!(buf[1], [0, 0, 123, 0]); /// // Thus you can also do this, row first, column second: -/// assert_eq!(buf[1usize][2], 123) +/// assert_eq!(buf[1][2], 123) /// ``` #[derive(Clone)] #[repr(transparent)] @@ -62,7 +63,7 @@ pub struct Buf2(Inner>); /// An immutable rectangular view to a region of a [`Buf2`], another `Slice2`, /// or in general any `&[T]` slice of memory. A two-dimensional analog to `&[T]`. /// -/// A `Slice2` may be discontiguous: +/// A `Slice2` may be non-contiguous: /// ```text /// +------stride-----+ /// | ____w____ | @@ -89,44 +90,106 @@ impl Buf2 { /// Returns a buffer with the given dimensions, with elements initialized /// in row-major order with values yielded by `init`. /// + /// # Examples + /// ``` + /// use retrofire_core::util::buf::Buf2; + /// + /// let buf = Buf2::new_from((3, 3), 1..); + /// assert_eq!(buf.data(), [1, 2, 3, + /// 4, 5, 6, + /// 7, 8, 9]); + /// ``` + /// /// # Panics - /// If there are fewer than `w * h` elements in `init`. - pub fn new_from(dims: Dims, init: I) -> Self + /// If `w * h > isize::MAX`, or if `init` has fewer than `w * h` elements. + pub fn new_from((w, h): Dims, init: I) -> Self where I: IntoIterator, { - let len = (dims.0 * dims.1) as usize; - let data: Vec<_> = init.into_iter().take(len).collect(); - assert_eq!(data.len(), len); - Self(Inner::new(dims, dims.0, data)) - } - /// Returns a buffer with size `w` × `h`, with every element - /// initialized by calling `T::default()`. - pub fn new(dims: Dims) -> Self + let ww = isize::try_from(w).ok(); + let hh = isize::try_from(h).ok(); + let len = ww.and_then(|w| hh.and_then(|h| w.checked_mul(h))); + let Some(len) = len else { + panic!( + "w * h cannot exceed isize::MAX ({w} * {h} > {})", + isize::MAX + ); + }; + let data: Vec<_> = init.into_iter().take(len as usize).collect(); + assert_eq!( + data.len(), + len as usize, + "insufficient items in iterator ({} < {len}", + data.len() + ); + Self(Inner::new((w, h), w, data)) + } + + /// Returns a buffer of size `w` × `h`, with every element initialized to + /// `T::default()`. + /// + /// # Examples + /// ``` + /// use retrofire_core::util::buf::Buf2; + /// + /// let buf: Buf2 = Buf2::new((3, 3)); + /// assert_eq!(buf.data(), [0, 0, 0, + /// 0, 0, 0, + /// 0, 0, 0]); + /// ``` + /// + /// # Panics + /// If `w * h > isize::MAX`. + pub fn new((w, h): Dims) -> Self where - T: Clone + Default, + T: Default + Clone, { - let data = vec![T::default(); (dims.0 * dims.1) as usize]; - Self(Inner::new(dims, dims.0, data)) + let data = vec![T::default(); (w * h) as usize]; + Self(Inner::new((w, h), w, data)) } - /// Returns a buffer with size `w` × `h`, with every element - /// initialized by calling `init_fn(x, y)` where x is the column index - /// and y the row index of the element being initialized. - pub fn new_with(dims: Dims, init_fn: F) -> Self + + /// Returns a buffer of size `w` × `h`, initialized by repeatedly calling + /// the given function. + /// + /// For each element, `init_fn(x, y)` is invoked, where `x` is the column + /// index and `y` the row index of the element being initialized. The + /// function invocations occur in row-major order. + /// + /// # Examples + /// ``` + /// use retrofire_core::util::buf::Buf2; + /// + /// let buf = Buf2::new_with((3, 3), |x, y| 10 * y + x); + /// assert_eq!(buf.data(), [ 0, 1, 2, + /// 10, 11, 12, + /// 20, 21, 22]); + /// ``` + /// + /// # Panics + /// If `w * h > isize::MAX`. + pub fn new_with((w, h): Dims, mut init_fn: F) -> Self where - F: Clone + FnMut(u32, u32) -> T, + F: FnMut(u32, u32) -> T, { - let init = (0..dims.1).flat_map(move |y| { - let mut init_fn = init_fn.clone(); - (0..dims.0).map(move |x| init_fn(x, y)) // - }); - Self::new_from(dims, init) + let (mut x, mut y) = (0, 0); + Self::new_from( + (w, h), + iter::from_fn(|| { + let res = init_fn(x, y); + x += 1; + if x == w { + (x, y) = (0, y + 1); + } + Some(res) + }), + ) } /// Returns a view of the backing data of `self`. pub fn data(&self) -> &[T] { self.0.data() } + /// Returns a mutable view of the backing data of `self`. pub fn data_mut(&mut self) -> &mut [T] { self.0.data_mut() @@ -142,8 +205,8 @@ impl<'a, T> Slice2<'a, T> { /// # use retrofire_core::util::buf::Slice2; /// let data = &[0, 1, 2, 3, 4, 5, 6]; /// let slice = Slice2::new((2, 2), 3, data); - /// assert_eq!(&slice[0usize], &[0, 1]); - /// assert_eq!(&slice[1usize], &[3, 4]); + /// assert_eq!(&slice[0], &[0, 1]); + /// assert_eq!(&slice[1], &[3, 4]); /// ``` /// Above, `slice` represents a 2×2 rectangle with stride 3, such that /// the first row maps to `data[0..2]` and the second to `data[3..5]`: @@ -231,17 +294,17 @@ impl AsMutSlice2 for MutSlice2<'_, T> { // impl Debug for Buf2 { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.debug_fmt(f, "Buf2") } } impl Debug for Slice2<'_, T> { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.debug_fmt(f, "Slice2") } } impl Debug for MutSlice2<'_, T> { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { self.0.debug_fmt(f, "Slice2Mut") } } @@ -279,13 +342,17 @@ impl<'a, T> DerefMut for MutSlice2<'a, T> { } pub mod inner { - use core::fmt::Formatter; - use core::marker::PhantomData; - use core::ops::{Deref, DerefMut, Index, IndexMut, Range}; - - use crate::math::vec::Vec2u; - use crate::util::rect::Rect; - use crate::util::Dims; + use core::{ + fmt::Formatter, + iter::zip, + marker::PhantomData, + ops::{Deref, DerefMut, Index, IndexMut, Range}, + }; + + use crate::{ + math::point::Point2u, + util::{rect::Rect, Dims}, + }; use super::{AsSlice2, MutSlice2, Slice2}; @@ -311,6 +378,11 @@ pub mod inner { pub fn height(&self) -> u32 { self.dims.1 } + /// Returns the width and height of `self`. + #[inline] + pub fn dims(&self) -> Dims { + self.dims + } /// Returns the stride of `self`. #[inline] pub fn stride(&self) -> u32 { @@ -331,10 +403,15 @@ pub mod inner { self.dims.0 == 0 || self.dims.1 == 0 } + /// Returns the linear index corresponding to the coordinates, + /// even if out of bounds. #[inline] fn to_index(&self, x: u32, y: u32) -> usize { (y * self.stride + x) as usize } + + /// Returns the linear index corresponding to the coordinates, + /// 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(|| { @@ -344,30 +421,39 @@ pub mod inner { ) }) } + /// Returns the linear index corresponding to the coordinates, + /// or `None` if x or y is out of bounds. #[inline] fn to_index_checked(&self, x: u32, y: u32) -> Option { let (w, h) = self.dims; (x < w && y < h).then(|| self.to_index(x, y)) } + /// Returns the dimensions and linear range corresponding to the rect. fn resolve_bounds(&self, rect: &Rect) -> (Dims, Range) { let (w, h) = self.dims; + let l = rect.left.unwrap_or(0); let t = rect.top.unwrap_or(0); let r = rect.right.unwrap_or(w); let b = rect.bottom.unwrap_or(h); + // Assert that left <= right <= width and top <= bottom <= height. + // Note that this permits left == width or top == height, but only + // when left == right or top == bottom, that is, when the range is + // empty. This matches the way slice indexing works. assert!(l <= r, "range left ({l}) > right ({r})"); assert!(t <= b, "range top ({l}) > bottom ({r})"); assert!(r <= w, "range right ({r}) > width ({w})"); assert!(b <= h, "range bottom ({b}) > height ({h})"); + // (l, t) is now guaranteed to be in bounds let start = self.to_index(l, t); // Slice end is the end of the last row let end = if b == t { self.to_index(r, t) } else { - // b != 0 because b > t + // b != 0 because b >= t && b != t self.to_index(r, b - 1) }; ((r - l, b - t), start..end) @@ -390,19 +476,19 @@ pub mod inner { /// # Panics /// if `stride < w` or if the slice would overflow `data`. #[rustfmt::skip] - pub(super) fn new(dims: Dims, stride: u32, data: D) -> Self { - let (w, h) = dims; - let len = data.len() as u32; + 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 <= len, + h <= 1 || stride as usize <= len, "stride ({stride}) > data length ({len})" ); - assert!(h <= len, "height ({h}) > data length ({len})"); + assert!(h as usize <= len, "height ({h}) > data length ({len})"); if h > 0 { let size = (h - 1) * stride + w; assert!( - size <= len, + size as usize <= len, "required size ({size}) > data length ({len})" ); } @@ -430,7 +516,7 @@ pub mod inner { /// Returns a reference to the element at `pos`, /// or `None` if `pos` is out of bounds. - pub fn get(&self, pos: impl Into) -> Option<&T> { + pub fn get(&self, pos: impl Into) -> Option<&T> { let [x, y] = pos.into().0; self.to_index_checked(x, y).map(|i| &self.data[i]) } @@ -444,10 +530,10 @@ pub mod inner { .map(|row| &row[..self.dims.0 as usize]) } - /// Returns an iterator over all the elements of `self` in row-major order. + /// Returns an iterator over the elements of `self` in row-major order. /// - /// First returns the elements on row 0 from left to right, followed by the elements - /// on row 1, and so on. + /// First returns the elements on row 0 from left to right, followed by + /// the elements on row 1, and so on. pub fn iter(&self) -> impl Iterator { self.rows().flatten() } @@ -473,14 +559,13 @@ pub mod inner { .map(|row| &mut row[..self.dims.0 as usize]) } - /// Returns an iterator over all the elements of `self` in row-major - /// order: first the elements on row 0 from left to right, followed - /// by the elements on row 1, and so on. + /// Returns a mutable iterator over all the elements of `self`, + /// yielded in row-major order. pub fn iter_mut(&mut self) -> impl Iterator { self.rows_mut().flatten() } - /// Fills `self` with clones of `val`. + /// Fills `self` with clones of the value. pub fn fill(&mut self, val: T) where T: Clone, @@ -492,49 +577,47 @@ pub mod inner { .for_each(|row| row.fill(val.clone())); } } - /// Fills `self` by invoking `f(x, y)` for every element, where - /// `x` and `y` are the column and row of the element, respectively. + /// Fills `self` by calling a function for each element. + /// + /// Calls `f(x, y)` for every element, where `x` and `y` are the column + /// and row indices of the element. Proceeds in row-major order. pub fn fill_with(&mut self, mut fill_fn: F) where - F: Copy + FnMut(u32, u32) -> T, + F: FnMut(u32, u32) -> T, { - let (w, h) = self.dims; - let mut fill = (0..h).flat_map(move |y| { - (0..w).map(move |x| fill_fn(x, y)) // - }); - if self.is_contiguous() { - self.data.fill_with(|| fill.next().unwrap()); - } else { - self.rows_mut().for_each(|row| { - row.fill_with(|| fill.next().unwrap()); // - }) + for (row, y) in zip(self.rows_mut(), 0..) { + for (item, x) in zip(row, 0..) { + *item = fill_fn(x, y); + } } } - /// Copies each element in `src` to the same position in `self`. + /// Copies each element in `other` to the same position in `self`. /// /// This operation is often called "blitting". /// /// # Panics - /// if the dimensions of `self` and `src` don't match. + /// if the dimensions of `self` and `other` do not match. #[doc(alias = "blit")] - pub fn copy_from(&mut self, src: impl AsSlice2) + pub fn copy_from(&mut self, other: impl AsSlice2) where T: Copy, { - let src = src.as_slice2(); - let (sw, sh) = self.dims; - let (ow, oh) = src.dims; - assert_eq!(sw, ow, "width ({sw}) != source width ({ow})",); - assert_eq!(sh, oh, "height ({sh}) != source height ({oh})",); - for (dest, src) in self.rows_mut().zip(src.rows()) { + let other = other.as_slice2(); + + assert_eq!( + self.dims, other.dims, + "dimension mismatch (self: {:?}, other: {:?})", + self.dims, other.dims + ); + for (dest, src) in self.rows_mut().zip(other.rows()) { dest.copy_from_slice(src); } } /// Returns a mutable reference to the element at `pos`, /// or `None` if `pos` is out of bounds. - pub fn get_mut(&mut self, pos: impl Into) -> Option<&mut T> { + pub fn get_mut(&mut self, pos: impl Into) -> Option<&mut T> { let [x, y] = pos.into().0; self.to_index_checked(x, y) .map(|i| &mut self.data[i]) @@ -553,8 +636,12 @@ pub mod inner { impl> Index for Inner { type Output = [T]; - /// Returns a reference to the row of `self` at index `i`. + /// Returns a reference to the row at index `i`. + /// /// The returned slice has length `self.width()`. + /// + /// # Panics + /// If `row >= self.height()`. #[inline] fn index(&self, i: usize) -> &[T] { let idx = self.to_index_strict(0, i as u32); @@ -568,8 +655,12 @@ pub mod inner { Self: Index, D: DerefMut, { - /// Returns a mutable reference to the row of `self` at index `i`. + /// Returns a mutable reference to the row at index `i`. + /// /// The returned slice has length `self.width()`. + /// + /// # Panics + /// If `row >= self.height()`. #[inline] fn index_mut(&mut self, row: usize) -> &mut [T] { let idx = self.to_index_strict(0, row as u32); @@ -581,13 +672,14 @@ pub mod inner { impl Index for Inner where D: Deref, - Pos: Into, + Pos: Into, { type Output = T; - /// Returns a reference to the element of `self` at position `pos`. + /// Returns a reference to the element at position `pos`. + /// /// # Panics - /// If `pos` is out of bounds of `self`. + /// If `pos` is out of bounds. #[inline] fn index(&self, pos: Pos) -> &T { let [x, y] = pos.into().0; @@ -598,12 +690,12 @@ pub mod inner { impl IndexMut for Inner where D: DerefMut, - Pos: Into, + Pos: Into, { - /// Returns a mutable reference to the element of `self` - /// at position `pos`. + /// Returns a mutable reference to the element at position `pos`. + /// /// # Panics - /// If `pos` is out of bounds of `self`. + /// If `pos` is out of bounds. #[inline] fn index_mut(&mut self, pos: Pos) -> &mut T { let [x, y] = pos.into().0; @@ -615,7 +707,7 @@ pub mod inner { #[cfg(test)] mod tests { - use crate::math::vec::vec2; + use crate::math::vec2; use super::*; @@ -652,8 +744,8 @@ mod tests { assert_eq!(buf[2usize], [2, 12, 22, 32]); assert_eq!(buf[[0, 0]], 0); - assert_eq!(buf[vec2(1, 0)], 10); - assert_eq!(buf[vec2(3, 4)], 34); + assert_eq!(buf[[1, 0]], 10); + assert_eq!(buf[[3, 4]], 34); assert_eq!(buf.get([2, 3]), Some(&23)); assert_eq!(buf.get([4, 4]), None); @@ -667,7 +759,7 @@ mod tests { buf[2usize][1] = 123; assert_eq!(buf[2usize], [2, 123, 22, 32]); - buf[vec2(2, 3)] = 234; + buf[[2, 3]] = 234; assert_eq!(buf[[2, 3]], 234); *buf.get_mut([3, 4]).unwrap() = 345; @@ -870,12 +962,12 @@ mod tests { let buf = Buf2::new_with((5, 4), |x, y| x * 10 + y); let slice = buf.slice((2.., 1..3)); - assert_eq!(slice[vec2(0, 0)], 21); - assert_eq!(slice[vec2(1, 0)], 31); - assert_eq!(slice[vec2(2, 1)], 42); + assert_eq!(slice[[0, 0]], 21); + assert_eq!(slice[[1, 0]], 31); + assert_eq!(slice[[2, 1]], 42); - assert_eq!(slice.get(vec2(2, 1)), Some(&42)); - assert_eq!(slice.get(vec2(2, 2)), None); + assert_eq!(slice.get([2, 1]), Some(&42)); + assert_eq!(slice.get([2, 2]), None); } #[test] @@ -884,10 +976,10 @@ mod tests { let mut slice = buf.slice_mut((2.., 1..3)); slice[[2, 1]] = 123; - assert_eq!(slice[vec2(2, 1)], 123); + assert_eq!(slice[[2, 1]], 123); - assert_eq!(slice.get_mut(vec2(2, 1)), Some(&mut 123)); - assert_eq!(slice.get(vec2(2, 2)), None); + assert_eq!(slice.get_mut([2, 1]), Some(&mut 123)); + assert_eq!(slice.get([2, 2]), None); buf[[2, 2]] = 321; let slice = buf.slice((1.., 2..)); diff --git a/core/src/util/pnm.rs b/core/src/util/pnm.rs index cab4efa2..6794b06a 100644 --- a/core/src/util/pnm.rs +++ b/core/src/util/pnm.rs @@ -1,14 +1,14 @@ //! PNM, also known as NetPBM, file format support. //! //! PNM is a venerable family of extremely simple image formats, each -//! consisting of a simple textual header followed by either text or +//! consisting of a simple textual header followed by either textual or //! binary pixel data. //! -//! Type | Magic | Pixel format -//! ----- | ------| ------------ -//! PBM | P1/P4 | 1 bpp monochrome -//! PGM | P2/P5 | 8 bpp grayscale -//! PPM | P3/P6 | 3x8 bpp RGB +//! Type | Txt | Bin | Pixel format +//! ------+-----+-----+----------------- +//! PBM | P1 | P4 | 1 bpp monochrome +//! PGM | P2 | P5 | 8 bpp grayscale +//! PPM | P3 | P6 | 3x8 bpp RGB use alloc::{string::String, vec::Vec}; use core::{ @@ -26,18 +26,18 @@ use std::{ use Error::*; use Format::*; -use crate::math::color::{rgb, Color3}; -use crate::util::buf::Buf2; +use crate::math::{rgb, Color3}; #[cfg(feature = "std")] -use crate::util::buf::AsSlice2; +use super::buf::AsSlice2; -/// The header of a PNM image +use super::{buf::Buf2, Dims}; + +/// The header of a PNM image. #[derive(Copy, Clone, Debug, Eq, PartialEq)] struct Header { format: Format, - width: u32, - height: u32, + dims: Dims, #[allow(unused)] // TODO Currently not used max: u16, @@ -76,6 +76,7 @@ impl TryFrom<[u8; 2]> for Format { type Error = Error; fn try_from(magic: [u8; 2]) -> Result { Ok(match &magic { + b"P2" => TextGraymap, b"P3" => TextPixmap, b"P4" => BinaryBitmap, b"P5" => BinaryGraymap, @@ -102,13 +103,19 @@ pub enum Error { /// Result of loading or decoding a PNM file. pub type Result = core::result::Result; -// TODO use core::error::Error once stabilized -#[cfg(feature = "std")] -impl std::error::Error for Error {} +impl core::error::Error for Error {} impl Display for Error { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "error decoding pnm image: {self:?}") + match *self { + #[cfg(feature = "std")] + Io(kind) => write!(f, "i/o error {kind}"), + Unsupported([c, d]) => { + write!(f, "unsupported magic number {}{}", c as char, d as char) + } + UnexpectedEnd => write!(f, "unexpected end of input"), + InvalidNumber => write!(f, "invalid numeric value"), + } } } @@ -130,60 +137,73 @@ impl From for Error { } impl Header { - /// Attempts to parse a PNM header from `src`. + /// Attempts to parse a PNM header from an iterator. /// - /// Currently supported formats are P3, P4, P5, and P6. - fn parse(src: impl IntoIterator) -> Result { - let mut it = src.into_iter(); + /// Currently supported formats are P2, P3, P4, P5, and P6. + fn parse(input: impl IntoIterator) -> Result { + let mut it = input.into_iter(); let magic = [ it.next().ok_or(UnexpectedEnd)?, it.next().ok_or(UnexpectedEnd)?, ]; let format = magic.try_into()?; - let width: u32 = parse_num(&mut it)?; - let height: u32 = parse_num(&mut it)?; + let dims = (parse_num(&mut it)?, parse_num(&mut it)?); let max: u16 = match &format { TextBitmap | BinaryBitmap => 1, _ => parse_num(&mut it)?, }; - Ok(Self { format, width, height, max }) + Ok(Self { format, dims, max }) } /// Writes `self` to `dest` as a valid PNM header, /// including a trailing newline. #[cfg(feature = "std")] fn write(&self, mut dest: impl Write) -> io::Result<()> { - let Self { format, width, height, max } = *self; + let Self { format, dims: (w, h), max } = *self; let max: &dyn Display = match format { TextBitmap | BinaryBitmap => &"", _ => &max, }; - writeln!(dest, "{} {} {} {}", format, width, height, max) + writeln!(dest, "{format} {w} {h} {max}") } } /// Loads a PNM image from a path into a buffer. /// -/// Currently supported formats are P3, P4, P5, and P6. +/// Currently supported formats are P2, P3, P4, P5, and P6. +/// Read more about the formats in the [module docs][self]. /// /// # Errors /// Returns [`pnm::Error`][Error] in case of an I/O error or invalid PNM image. #[cfg(feature = "std")] pub fn load_pnm(path: impl AsRef) -> Result> { let r = &mut BufReader::new(File::open(path)?); - read_pnm(r.bytes().map_while(io::Result::ok)) + read_pnm(r) +} + +/// Reads a PNM image into a buffer. +/// +/// Currently supported PNM formats are P2, P3, P4, P5, and P6. +/// Read more about the formats in the [module docs][self]. +/// +/// # Errors +/// Returns [`pnm::Error`][Error] in case of an I/O error or invalid PNM image. +#[cfg(feature = "std")] +pub fn read_pnm(input: impl Read) -> Result> { + parse_pnm(input.bytes().map_while(io::Result::ok)) } /// Attempts to decode a PNM image from an iterator of bytes. /// -/// Currently supported formats are P3, P4, P5, and P6. +/// Currently supported PNM formats are P2, P3, P4, P5, and P6. +/// Read more about the formats in the [module docs][self]. /// /// # Errors -/// Returns [`pnm::Error`][Error] in case of an invalid PNM image. -pub fn read_pnm(src: impl IntoIterator) -> Result> { - let mut it = src.into_iter(); +/// Returns [`Error`] in case of an invalid or unrecognized PNM image. +pub fn parse_pnm(input: impl IntoIterator) -> Result> { + let mut it = input.into_iter(); let h = Header::parse(&mut it)?; - let count = h.width * h.height; + let count = h.dims.0 * h.dims.1; let data: Vec = match h.format { BinaryPixmap => { let mut col = [0u8; 3]; @@ -220,13 +240,19 @@ pub fn read_pnm(src: impl IntoIterator) -> Result> { .take(count as usize) .collect::>>()? } - _ => unimplemented!(), + TextGraymap => (0..count) + .map(|_| { + let val = parse_num(&mut it)?; + Ok(rgb(val, val, val)) + }) + .collect::>>()?, + _ => return Err(Unsupported((h.format as u16).to_be_bytes())), }; - if data.len() < (h.width * h.height) as usize { + if data.len() < count as usize { Err(UnexpectedEnd) } else { - Ok(Buf2::new_from((h.width, h.height), data)) + Ok(Buf2::new_from(h.dims, data)) } } @@ -259,19 +285,18 @@ pub fn write_ppm( ) -> io::Result<()> { let slice = data.as_slice2(); Header { - format: Format::BinaryPixmap, - width: slice.width(), - height: slice.height(), + format: BinaryPixmap, + dims: slice.dims(), max: 255, } .write(&mut out)?; + // Appease the borrow checker let res = slice .rows() .flatten() .map(|c| c.0) .try_for_each(|rgb| out.write_all(&rgb[..])); - res } @@ -281,27 +306,26 @@ where T: FromStr, Error: From, { - // Skip whitespace and comments - let mut in_comment = false; - let mut whitespace_or_comment = |b| match b { - b'#' => { - in_comment = true; - true - } - b'\n' => { - in_comment = false; - true + let mut whitespace_or_comment = { + let mut in_comment = false; + move |b: &u8| match *b { + b'#' => { + in_comment = true; + true + } + b'\n' => { + in_comment = false; + true + } + _ => in_comment || b.is_ascii_whitespace(), } - _ => in_comment || b.is_ascii_whitespace(), }; - let str = src .into_iter() - .skip_while(|&b| whitespace_or_comment(b)) - .take_while(|&b| !b.is_ascii_whitespace()) + .skip_while(whitespace_or_comment) + .take_while(|b| !whitespace_or_comment(b)) .map(char::from) .collect::(); - Ok(str.parse()?) } @@ -326,18 +350,22 @@ mod tests { } #[test] - fn parse_num_with_comment() { + fn parse_num_with_comment_before() { assert_eq!(parse_num(*b"# this is a comment\n42"), Ok(42)); } + #[test] + fn parse_num_with_comment_after() { + assert_eq!(parse_num(*b"42#this is a comment"), Ok(42)); + } + #[test] fn parse_header_whitespace() { assert_eq!( Header::parse(*b"P6 123\t \n\r321 255 "), Ok(Header { format: BinaryPixmap, - width: 123, - height: 321, + dims: (123, 321), max: 255, }) ); @@ -349,21 +377,43 @@ mod tests { Header::parse(*b"P6 # foo 42\n 123\n#bar\n#baz\n321 255 "), Ok(Header { format: BinaryPixmap, - width: 123, - height: 321, + dims: (123, 321), max: 255, }) ); } + #[test] + fn parse_header_p2() { + assert_eq!( + Header::parse(*b"P2 123 456 789"), + Ok(Header { + format: TextGraymap, + dims: (123, 456), + max: 789, + }) + ); + } + + #[test] + fn parse_header_p3() { + assert_eq!( + Header::parse(*b"P3 123 456 789"), + Ok(Header { + format: TextPixmap, + dims: (123, 456), + max: 789, + }) + ); + } + #[test] fn parse_header_p4() { assert_eq!( Header::parse(*b"P4 123 456 "), Ok(Header { format: BinaryBitmap, - width: 123, - height: 456, + dims: (123, 456), max: 1, }) ); @@ -372,20 +422,31 @@ mod tests { #[test] fn parse_header_p5() { assert_eq!( - Header::parse(*b"P5 123 456 64 "), + Header::parse(*b"P5 123 456 789 "), Ok(Header { format: BinaryGraymap, - width: 123, - height: 456, - max: 64, + dims: (123, 456), + max: 789, + }) + ); + } + + #[test] + fn parse_header_p6() { + assert_eq!( + Header::parse(*b"P6 123 456 789 "), + Ok(Header { + format: BinaryPixmap, + dims: (123, 456), + max: 789, }) ); } #[test] fn parse_header_unsupported_magic() { - let res = Header::parse(*b"P2 1 1 1 "); - assert_eq!(res, Err(Unsupported(*b"P2"))); + let res = Header::parse(*b"P7 1 1 1 "); + assert_eq!(res, Err(Unsupported(*b"P7"))); } #[test] @@ -404,7 +465,7 @@ mod tests { #[test] fn parse_pnm_truncated() { let data = *b"P3 2 2 256 \n 0 0 0 123 0 42 0 64 128"; - assert_eq!(read_pnm(data).err(), Some(UnexpectedEnd)); + assert_eq!(parse_pnm(data).err(), Some(UnexpectedEnd)); } #[cfg(feature = "std")] @@ -412,13 +473,12 @@ mod tests { fn write_header_p1() { let mut out = Vec::new(); let hdr = Header { - format: Format::TextBitmap, - width: 16, - height: 32, + format: TextBitmap, + dims: (123, 456), max: 1, }; hdr.write(&mut out).unwrap(); - assert_eq!(&out, b"P1 16 32 \n"); + assert_eq!(&out, b"P1 123 456 \n"); } #[cfg(feature = "std")] @@ -426,24 +486,37 @@ mod tests { fn write_header_p6() { let mut out = Vec::new(); let hdr = Header { - format: Format::BinaryPixmap, - width: 64, - height: 16, - max: 4, + format: BinaryPixmap, + dims: (123, 456), + max: 789, }; hdr.write(&mut out).unwrap(); - assert_eq!(&out, b"P6 64 16 4\n"); + assert_eq!(&out, b"P6 123 456 789\n"); } #[test] - fn read_pnm_p3() { - let data = *b"P3 2 2 256 \n 0 0 0 123 0 42 0 64 128 255 255 255"; + fn read_pnm_p2() { + let data = *b"P2 2 2 128 \n 12 34 56 78"; - let buf = read_pnm(data).unwrap(); + let buf = parse_pnm(data).unwrap(); assert_eq!(buf.width(), 2); assert_eq!(buf.height(), 2); + assert_eq!(buf[[0, 0]], rgb(12, 12, 12)); + assert_eq!(buf[[1, 0]], rgb(34, 34, 34)); + assert_eq!(buf[[0, 1]], rgb(56, 56, 56)); + assert_eq!(buf[[1, 1]], rgb(78, 78, 78)); + } + + #[test] + fn read_pnm_p3() { + let data = *b"P3 2 2 256 \n 0 0 0 123 0 42 0 64 128 255 255 255"; + + let buf = parse_pnm(data).unwrap(); + + assert_eq!(buf.dims(), (2, 2)); + assert_eq!(buf[[0, 0]], rgb(0, 0, 0)); assert_eq!(buf[[1, 0]], rgb(123, 0, 42)); assert_eq!(buf[[0, 1]], rgb(0, 64, 128)); @@ -453,10 +526,9 @@ mod tests { #[test] fn read_pnm_p4() { // 0x69 == 0b0110_1001 - let buf = read_pnm(*b"P4 4 2\n\x69").unwrap(); + let buf = parse_pnm(*b"P4 4 2\n\x69").unwrap(); - assert_eq!(buf.width(), 4); - assert_eq!(buf.height(), 2); + assert_eq!(buf.dims(), (4, 2)); let b = rgb(0u8, 0, 0); let w = rgb(0xFFu8, 0xFF, 0xFF); @@ -467,10 +539,9 @@ mod tests { #[test] fn read_pnm_p5() { - let buf = read_pnm(*b"P5 2 2 255\n\x01\x23\x45\x67").unwrap(); + let buf = parse_pnm(*b"P5 2 2 255\n\x01\x23\x45\x67").unwrap(); - assert_eq!(buf.width(), 2); - assert_eq!(buf.height(), 2); + assert_eq!(buf.dims(), (2, 2)); assert_eq!(buf[0usize], [rgb(0x01, 0x01, 0x01), rgb(0x23, 0x23, 0x23)]); assert_eq!(buf[1usize], [rgb(0x45, 0x45, 0x45), rgb(0x67, 0x67, 0x67)]); @@ -478,7 +549,7 @@ mod tests { #[test] fn read_pnm_p6() { - let buf = read_pnm( + let buf = parse_pnm( *b"P6 2 2 255\n\ \x01\x12\x23\ \x34\x45\x56\ @@ -487,8 +558,7 @@ mod tests { ) .unwrap(); - assert_eq!(buf.width(), 2); - assert_eq!(buf.height(), 2); + assert_eq!(buf.dims(), (2, 2)); assert_eq!(buf[0usize], [rgb(0x01, 0x12, 0x23), rgb(0x34, 0x45, 0x56)]); assert_eq!(buf[1usize], [rgb(0x67, 0x78, 0x89), rgb(0x9A, 0xAB, 0xBC)]); diff --git a/core/src/util/rect.rs b/core/src/util/rect.rs index 53d5f6fc..42805427 100644 --- a/core/src/util/rect.rs +++ b/core/src/util/rect.rs @@ -3,7 +3,7 @@ use core::fmt::Debug; use core::ops::{Bound::*, Range, RangeBounds, RangeFull, Sub}; -use crate::math::vec::Vec2u; +use crate::math::Vec2u; /// An axis-aligned rectangular region. /// @@ -155,7 +155,7 @@ impl From for Rect { #[cfg(test)] mod tests { - use crate::math::vec::vec2; + use crate::math::vec2; use super::*; diff --git a/demos/Cargo.toml b/demos/Cargo.toml index 40b912e7..8902cdfb 100644 --- a/demos/Cargo.toml +++ b/demos/Cargo.toml @@ -16,6 +16,7 @@ re-front = { path = "../front", package = "retrofire-front" } re-geom = { path = "../geom", package = "retrofire-geom", optional = true } minifb = { version = "0.27.0", optional = true } +sdl2 = { version = "0.35.2", optional = true } softbuffer = { version = "0.3.0", optional = true } winit = { version = "0.28.6", optional = true } @@ -23,6 +24,7 @@ winit = { version = "0.28.6", optional = true } default = ["std"] std = ["re/std"] minifb = ["dep:minifb", "re-front/minifb"] +sdl2 = ["dep:sdl2", "re-front/sdl2"] [[bin]] name = "square" @@ -38,7 +40,7 @@ required-features = ["minifb", "re-geom"] [[bin]] name = "crates" -required-features = ["minifb", "re-geom"] +required-features = ["sdl2", "re-geom"] [[bin]] name = "sprites" diff --git a/demos/src/bin/bezier.rs b/demos/src/bin/bezier.rs index 0b4837cb..4b754c95 100644 --- a/demos/src/bin/bezier.rs +++ b/demos/src/bin/bezier.rs @@ -1,15 +1,13 @@ -use std::mem::swap; -use std::ops::ControlFlow::Continue; +use re::geom::Ray; +use std::{mem::swap, ops::ControlFlow::Continue}; -use re::math::rand::{Distrib, Uniform, UnitDisk, Xorshift64}; -use re::math::spline::BezierSpline; use re::prelude::*; -use re::util::Dims; -use re_front::minifb::Window; -use re_front::Frame; +use re::math::rand::{Distrib, Uniform, VectorsOnUnitDisk, Xorshift64}; -fn line([mut p0, mut p1]: [Vec2; 2]) -> impl Iterator { +use re_front::{dims::SVGA_800_600, minifb::Window, Frame}; + +fn line([mut p0, mut p1]: [Point2; 2]) -> impl Iterator { if p0.y() > p1.y() { swap(&mut p0, &mut p1); } @@ -22,27 +20,36 @@ fn line([mut p0, mut p1]: [Vec2; 2]) -> impl Iterator { (vec2(dx / dy, 1.0), dy) }; - p0.vary(step, Some(n as u32)) - .map(|p| vec2(p.x() as u32, p.y() as u32)) + p0.vary(step, Some(n as u32 + 1)) + .map(|p| p.map(|c| c as u32)) } fn main() { - let dims @ (w, h) = (640, 480); + let dims @ (w, h) = SVGA_800_600; let mut win = Window::builder() .title("retrofire//bezier") .dims(dims) - .build(); + .build() + .expect("should create window"); + + let (min, max) = + (pt2(100.0, 100.0), pt2(w as f32 - 100.0, h as f32 - 100.0)); - let rng = Xorshift64::from_time(); - let pos = Uniform(vec2(0.0, 0.0)..vec2(w as f32, h as f32)); - let vel = UnitDisk; + let rng = &mut Xorshift64::from_time(); + let pos = Uniform::(min..max); + let vel = VectorsOnUnitDisk; - let (mut pts, mut deltas): (Vec, Vec) = - (pos, vel).samples(rng).take(4).unzip(); + let mut pos_vels: Vec<(Point2, Vec2)> = + (pos, vel).samples(rng).take(32).collect(); win.run(|Frame { dt, buf, .. }| { - let b = BezierSpline::new(&pts); + let rays: Vec> = pos_vels + .chunks(2) + .map(|ch| Ray(ch[0].0, (ch[1].0 - ch[0].0) * 0.4)) + .collect(); + + let b = BezierSpline::from_rays(rays); // Stop once error is less than one pixel let apx = b.approximate(|err| err.len_sqr() < 1.0); @@ -53,16 +60,15 @@ fn main() { } } - let max = vec2((w - 1) as f32, (h - 1) as f32); let secs = dt.as_secs_f32(); - for (p, d) in pts.iter_mut().zip(deltas.iter_mut()) { - *p = (*p + *d * 200.0 * secs).clamp(&Vec2::zero(), &max); - - if p[0] == 0.0 || p[0] == max.x() { - d[0] = -d[0]; + for (pos, vel) in pos_vels.iter_mut() { + *pos = (*pos + *vel * 200.0 * secs).clamp(&min, &max); + let [dx, dy] = &mut vel.0; + if pos.x() == min.x() || pos.x() == max.x() { + *dx = -*dx; } - if p[1] == 0.0 || p[1] == max.y() { - d[1] = -d[1]; + if pos.y() == min.y() || pos.y() == max.y() { + *dy = -*dy; } } Continue(()) diff --git a/demos/src/bin/crates.rs b/demos/src/bin/crates.rs index dc1bc1e9..425aea97 100644 --- a/demos/src/bin/crates.rs +++ b/demos/src/bin/crates.rs @@ -1,31 +1,27 @@ use core::ops::ControlFlow::*; -use minifb::MouseMode; - use re::prelude::*; use re::math::color::gray; -use re::render::{ - batch::Batch, - cam::{Camera, FirstPerson}, - ModelToProj, -}; -use re_front::minifb::Window; -use re_geom::solids::*; +use re::render::{cam::FirstPerson, Batch, Camera, ModelToProj}; + +use re_front::sdl2::Window; +use re_geom::solids::Box; fn main() { let mut win = Window::builder() .title("retrofire//crates") - .build(); + .build() + .expect("should create window"); let floor_shader = Shader::new( - |v: Vertex<_, _>, mvp: &Mat4x4| { + |v: Vertex3<_>, mvp: &Mat4x4| { vertex(mvp.apply(&v.pos), v.attrib) }, |frag: Frag| frag.var.to_color4(), ); let crate_shader = Shader::new( - |v: Vertex<_, _>, mvp: &Mat4x4| { + |v: Vertex3<_>, mvp: &Mat4x4| { vertex(mvp.apply(&v.pos), v.attrib) }, |frag: Frag| { @@ -48,22 +44,24 @@ fn main() { let mut cam_vel = Vec3::zero(); - let imp = &frame.win.imp; + let ep = &frame.win.ev_pump; - for key in imp.get_keys() { - use minifb::Key::*; + for key in ep.keyboard_state().pressed_scancodes() { + use sdl2::keyboard::Scancode as Sc; match key { - W => cam_vel[2] += 4.0, - S => cam_vel[2] -= 2.0, - D => cam_vel[0] += 3.0, - A => cam_vel[0] -= 3.0, + Sc::W => cam_vel[2] += 4.0, + Sc::S => cam_vel[2] -= 2.0, + Sc::D => cam_vel[0] += 3.0, + Sc::A => cam_vel[0] -= 3.0, _ => {} } } - let (mx, my) = imp.get_mouse_pos(MouseMode::Pass).unwrap(); - cam.mode - .rotate_to(degs(-0.4 * mx), degs(0.4 * (my - 240.0))); + let ms = ep.relative_mouse_state(); + let d_az = turns(ms.x() as f32) * -0.001; + let d_alt = turns(ms.y() as f32) * 0.001; + + cam.mode.rotate(d_az, d_alt); cam.mode .translate(cam_vel.mul(frame.dt.as_secs_f32())); @@ -106,7 +104,8 @@ fn main() { } } Continue(()) - }); + }) + .expect("should run") } fn floor() -> Mesh { @@ -117,7 +116,7 @@ fn floor() -> Mesh { for i in -size..=size { let even_odd = ((i & 1) ^ (j & 1)) == 1; - let pos = vec3(i as f32, -1.0, j as f32); + let pos = pt3(i as f32, -1.0, j as f32); let col = if even_odd { gray(0.2) } else { gray(0.9) }; bld.push_vert(pos, col); diff --git a/demos/src/bin/solids.rs b/demos/src/bin/solids.rs index bc48c14f..2facbd4d 100644 --- a/demos/src/bin/solids.rs +++ b/demos/src/bin/solids.rs @@ -5,10 +5,13 @@ use minifb::{Key, KeyRepeat}; use re::prelude::*; use re::math::{ - color::gray, mat::RealToReal, spline::smootherstep, vec::ProjVec4, + color::gray, mat::RealToReal, point::pt2, spline::smootherstep, + vec::ProjVec4, }; -use re::render::batch::Batch; -use re::render::{cam::Camera, Model, ModelToProj, ModelToWorld}; +use re::render::{ + raster::Frag, shader::Shader, Batch, Camera, ModelToProj, ModelToWorld, +}; + use re_front::{minifb::Window, Frame}; use re_geom::{io::parse_obj, solids::*}; @@ -51,7 +54,8 @@ fn main() { let mut win = Window::builder() .title("retrofire//solids") - .build(); + .build() + .expect("should create window"); win.ctx.color_clear = Some(gray(32).to_rgba()); @@ -61,7 +65,7 @@ fn main() { .perspective(1.5, 0.1..1000.0) .viewport(vec2(10, 10)..vec2(w - 10, h - 10)); - type VertexIn = Vertex, Normal3>; + type VertexIn = Vertex3; type VertexOut = Vertex; type Uniform<'a> = (&'a Mat4x4, &'a Mat4x4>); @@ -106,8 +110,10 @@ fn main() { .to::() .then(&cam.world_to_project()); + let object = &objects[carousel.idx % objects.len()]; + Batch::new() - .mesh(&objects[carousel.idx % objects.len()]) + .mesh(object) .uniform((&model_view_project, &spin)) .shader(shader) .viewport(cam.viewport) @@ -126,6 +132,7 @@ fn objects(res: u32) -> [Mesh; 13] { let sectors = 2 * res; let cap_segments = res; + let body_segments = res; let major_sectors = 3 * res; let minor_sectors = 2 * res; @@ -139,11 +146,11 @@ fn objects(res: u32) -> [Mesh; 13] { // Surfaces of revolution lathe(sectors), - Sphere { sectors, segments, radius: 1.0, }.build(), - Cylinder { sectors, radius: 0.8, capped: true }.build(), - Cone { sectors, base_radius: 1.1, apex_radius: 0.3, capped: true, }.build(), - Capsule { sectors, cap_segments, radius: 0.5, }.build(), - Torus { major_radius: 0.9, minor_radius: 0.3, major_sectors, minor_sectors, }.build(), + Sphere { radius: 1.0, sectors, segments, }.build(), + Cylinder { radius: 0.8, sectors, segments, capped: true }.build(), + Cone { base_radius: 1.1, apex_radius: 0.3, sectors, segments, capped: true }.build(), + Capsule { radius: 0.5, sectors, body_segments, cap_segments }.build(), + Torus { major_radius: 0.9, minor_radius: 0.3, major_sectors, minor_sectors }.build(), // Traditional demo models teapot(), @@ -154,12 +161,12 @@ fn objects(res: u32) -> [Mesh; 13] { // Creates a Lathe mesh. fn lathe(secs: u32) -> Mesh { Lathe::new( - vec![ - vertex(vec2(0.75, -0.5), vec2(1.0, 1.0)), - vertex(vec2(0.55, -0.25), vec2(1.0, 0.5)), - vertex(vec2(0.5, 0.0), vec2(1.0, 0.0)), - vertex(vec2(0.55, 0.25), vec2(1.0, -0.5)), - vertex(vec2(0.75, 0.5), vec2(1.0, 1.0)), + [ + vertex(pt2(0.75, -0.5), vec2(1.0, 1.0)), + vertex(pt2(0.55, -0.25), vec2(1.0, 0.5)), + vertex(pt2(0.5, 0.0), vec2(1.0, 0.0)), + vertex(pt2(0.55, 0.25), vec2(1.0, -0.5)), + vertex(pt2(0.75, 0.5), vec2(1.0, 1.0)), ], secs, ) diff --git a/demos/src/bin/sprites.rs b/demos/src/bin/sprites.rs index daab5525..6f10f8c8 100644 --- a/demos/src/bin/sprites.rs +++ b/demos/src/bin/sprites.rs @@ -1,13 +1,9 @@ -use core::array::from_fn; -use core::ops::ControlFlow::Continue; +use core::{array::from_fn, ops::ControlFlow::Continue}; use re::prelude::*; -use re::math::rand::{UnitBall, Xorshift64}; -use re::render::{ - cam::{Camera, Mode}, - render, Model, ModelToView, ViewToProj, -}; +use re::math::rand::{Distrib, PointsInUnitBall, Xorshift64}; +use re::render::{cam::Mode, render, Model, ModelToView, ViewToProj}; use re_front::minifb::Window; fn main() { @@ -18,8 +14,9 @@ fn main() { vec2(1.0, 1.0), ]; let count = 10000; - let rng = Xorshift64::default(); - let verts: Vec, Vec2<_>>> = UnitBall + let rng = &mut Xorshift64::default(); + + let verts: Vec>> = PointsInUnitBall .samples(rng) .take(count) .flat_map(|pos| verts.map(|v| vertex(pos.to(), v))) @@ -32,18 +29,19 @@ fn main() { let mut win = Window::builder() .title("retrofire//sprite") - .build(); + .build() + .expect("should create window"); let shader = Shader::new( - |v: Vertex, Vec2<_>>, + |v: Vertex3>, (mv, proj): (&Mat4x4, &Mat4x4)| { let vertex_pos = 0.008 * vec3(v.attrib.x(), v.attrib.y(), 0.0); - let view_pos = mv.apply(&v.pos) + vertex_pos; + let view_pos = mv.apply_pt(&v.pos) + vertex_pos; vertex(proj.apply(&view_pos), v.attrib) }, |frag: Frag>| { let d2 = frag.var.len_sqr(); - (d2 < 1.0).then_some({ + (d2 < 1.0).then(|| { // TODO ops trait for colors let col: Vec3 = splat(1.0) - d2 * vec3(0.25, 0.5, 1.0); rgba(col.x(), col.y(), col.z(), 1.0).to_color4() diff --git a/demos/src/bin/square.rs b/demos/src/bin/square.rs index 46f0bbcf..6f3a22ba 100644 --- a/demos/src/bin/square.rs +++ b/demos/src/bin/square.rs @@ -2,23 +2,31 @@ use std::ops::ControlFlow::*; use re::prelude::*; -use re::render::tex::{uv, SamplerClamp, TexCoord, Texture}; -use re::render::{render, Model, ModelToProj}; +use re::math::{pt2, pt3}; +use re::render::{render, tex::SamplerClamp, Context, ModelToProj}; + use re_front::minifb::Window; fn main() { - let verts: [Vertex, TexCoord>; 4] = [ - vertex(vec3(-1.0, -1.0, 0.0), uv(0.0, 0.0)), - vertex(vec3(-1.0, 1.0, 0.0), uv(0.0, 1.0)), - vertex(vec3(1.0, -1.0, 0.0), uv(1.0, 0.0)), - vertex(vec3(1.0, 1.0, 0.0), uv(1.0, 1.0)), + let verts: [Vertex3; 4] = [ + vertex(pt3(-1.0, -1.0, 0.0), uv(0.0, 0.0)), + vertex(pt3(-1.0, 1.0, 0.0), uv(0.0, 1.0)), + vertex(pt3(1.0, -1.0, 0.0), uv(1.0, 0.0)), + vertex(pt3(1.0, 1.0, 0.0), uv(1.0, 1.0)), ]; let mut win = Window::builder() .title("retrofire//square") - .build(); + .build() + .expect("should create window"); - win.ctx.face_cull = None; + win.ctx = Context { + face_cull: None, + depth_test: None, + depth_clear: None, + depth_write: false, + ..win.ctx + }; let checker = Texture::from(Buf2::new_with((8, 8), |x, y| { let xor = (x ^ y) & 1; @@ -26,15 +34,15 @@ fn main() { })); let shader = Shader::new( - |v: Vertex<_, TexCoord>, mvp: &Mat4x4| { + |v: Vertex3<_>, mvp: &Mat4x4| { vertex(mvp.apply(&v.pos), v.attrib) }, - |frag: Frag| SamplerClamp.sample(&checker, frag.var), + |frag: Frag<_>| SamplerClamp.sample(&checker, frag.var), ); let (w, h) = win.dims; let project = perspective(1.0, 4.0 / 3.0, 0.1..1000.0); - let viewport = viewport(vec2(10, 10)..vec2(w - 10, h - 10)); + let viewport = viewport(pt2(10, 10)..pt2(w - 10, h - 10)); win.run(|frame| { let secs = frame.t.as_secs_f32(); diff --git a/demos/wasm/src/triangle.rs b/demos/wasm/src/triangle.rs index 40c549bf..f871e182 100644 --- a/demos/wasm/src/triangle.rs +++ b/demos/wasm/src/triangle.rs @@ -2,17 +2,12 @@ use core::ops::ControlFlow::*; use wasm_bindgen::prelude::*; -use re::geom::{vertex, Tri, Vertex}; -use re::math::{ - color::{rgba, Color4f}, - mat::{perspective, rotate_z, translate, viewport}, - rads, vec2, vec3, -}; -use re::render::{raster::Frag, render, shader::Shader, ModelToView}; +use re::prelude::*; + +use re::render::{render, ModelToView}; use re::util::Dims; -use re_front::dims::SVGA_800_600; -use re_front::wasm::Window; +use re_front::{dims::SVGA_800_600, wasm::Window}; // Entry point from JS #[wasm_bindgen(start)] @@ -25,13 +20,13 @@ pub fn start() { win.ctx.color_clear = Some(rgba(0, 0, 0, 0x80)); let vs = [ - vertex(vec3(-2.0, 1.0, 0.0), rgba(1.0, 0.2, 0.1, 0.9)), - vertex(vec3(2.0, 2.0, 0.0), rgba(0.2, 0.9, 0.1, 0.8)), - vertex(vec3(0.0, -2.0, 0.0), rgba(0.3, 0.4, 1.0, 1.0)), + vertex(pt3(-2.0, 1.0, 0.0), rgba(1.0, 0.2, 0.1, 0.9)), + vertex(pt3(2.0, 2.0, 0.0), rgba(0.2, 0.9, 0.1, 0.8)), + vertex(pt3(0.0, -2.0, 0.0), rgba(0.3, 0.4, 1.0, 1.0)), ]; let proj = perspective(1.0, 4.0 / 3.0, 0.1..1000.0); - let vp = viewport(vec2(8, 8)..vec2(DIMS.0 - 8, DIMS.1 - 8)); + let vp = viewport(pt2(8, 8)..pt2(DIMS.0 - 8, DIMS.1 - 8)); win.run(move |frame| { let t = frame.t.as_secs_f32(); @@ -42,7 +37,7 @@ pub fn start() { let mvp = mv.then(&proj); let sh = Shader::new( - |v: Vertex<_, Color4f>, _| vertex(mvp.apply(&v.pos), v.attrib), + |v: Vertex3, _| vertex(mvp.apply(&v.pos), v.attrib), |f: Frag| f.var.to_color4(), ); diff --git a/front/src/lib.rs b/front/src/lib.rs index 3cc9113a..503d52f1 100644 --- a/front/src/lib.rs +++ b/front/src/lib.rs @@ -5,8 +5,10 @@ extern crate core; use core::time::Duration; -use retrofire_core::render::{ctx::Context, target::Framebuf}; -use retrofire_core::util::buf::MutSlice2; +use retrofire_core::{ + render::{Context, Framebuf}, + util::buf::AsMutSlice2, +}; #[cfg(feature = "minifb")] pub mod minifb; @@ -20,13 +22,13 @@ pub mod wasm; /// Per-frame state. The window run method passes an instance of `Frame` /// to the callback function on every iteration of the main loop. -pub struct Frame<'a, Win> { +pub struct Frame<'a, Win, Buf> { /// Elapsed time since the start of the first frame. pub t: Duration, /// Elapsed time since the start of the previous frame. pub dt: Duration, /// Framebuffer in which to draw. - pub buf: Framebuf, MutSlice2<'a, f32>>, + pub buf: Buf, /// Reference to the window object. pub win: &'a mut Win, /// Rendering context and config. @@ -68,3 +70,20 @@ pub mod dims { pub const DCI_2K_2048_1080: Dims = (2048, 1080); pub const DCI_4K_4096_2160: Dims = (4096, 2160); } + +impl, Z: AsMutSlice2> Frame<'_, W, Framebuf> { + pub fn clear(&mut self) { + if let Some(c) = self.ctx.color_clear { + // TODO Assumes pixel format + self.buf + .color_buf + .as_mut_slice2() + .fill(c.to_argb_u32()); + } + 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()); + } + } +} diff --git a/front/src/minifb.rs b/front/src/minifb.rs index beeac00e..007f6f8a 100644 --- a/front/src/minifb.rs +++ b/front/src/minifb.rs @@ -1,12 +1,16 @@ //! Frontend using the `minifb` crate for window creation and event handling. -use core::ops::ControlFlow::{self, Break}; -use std::time::Instant; +use std::{ + ops::ControlFlow::{self, Break}, + time::Instant, +}; use minifb::{Key, WindowOptions}; -use retrofire_core::render::{ctx::Context, target::Framebuf}; -use retrofire_core::util::{buf::Buf2, Dims}; +use retrofire_core::{ + render::{target, Context}, + util::{buf::Buf2, buf::MutSlice2, Dims}, +}; use crate::{dims::SVGA_800_600, Frame}; @@ -28,6 +32,9 @@ pub struct Builder<'title> { pub opts: WindowOptions, } +pub type Framebuf<'a> = + target::Framebuf, MutSlice2<'a, f32>>; + impl Default for Builder<'_> { fn default() -> Self { Self { @@ -63,16 +70,15 @@ impl<'t> Builder<'t> { } /// Creates the window. - pub fn build(self) -> Window { + pub fn build(self) -> minifb::Result { let Self { dims, title, target_fps, opts } = self; let mut imp = - minifb::Window::new(title, dims.0 as usize, dims.1 as usize, opts) - .unwrap(); + minifb::Window::new(title, dims.0 as usize, dims.1 as usize, opts)?; if let Some(fps) = target_fps { imp.set_target_fps(fps as usize); } let ctx = Context::default(); - Window { imp, dims, ctx } + Ok(Window { imp, dims, ctx }) } } @@ -103,7 +109,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)); @@ -116,14 +122,6 @@ impl Window { if self.should_quit() { break; } - if let Some(c) = ctx.color_clear { - cbuf.fill(c.to_argb_u32()); - } - if let Some(c) = ctx.depth_clear { - // Depth buffer contains reciprocal depth values - zbuf.fill(c.recip()); - } - let frame = &mut Frame { t: start.elapsed(), dt: last.elapsed(), @@ -134,6 +132,8 @@ impl Window { 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 8b137891..7716688f 100644 --- a/front/src/sdl2.rs +++ b/front/src/sdl2.rs @@ -1 +1,275 @@ +//! Frontend using the `sdl2` crate for window creation and event handling. +use std::{fmt, mem::replace, ops::ControlFlow, time::Instant}; + +use sdl2::{ + event::Event, + keyboard::Keycode, + render::{Texture, TextureValueError, WindowCanvas}, + video::{FullscreenType, WindowBuildError}, + EventPump, IntegerOrSdlError, +}; + +use retrofire_core::{ + math::Vary, + render::{ + raster::Scanline, stats::Throughput, Context, FragmentShader, Target, + }, + util::buf::{Buf2, MutSlice2}, +}; + +use crate::{dims, Frame}; + +#[derive(Debug)] +pub struct Error(String); + +/// A lightweight wrapper of an `SDL2` window. +pub struct Window { + /// The SDL canvas. + pub canvas: WindowCanvas, + /// The SDL event pump. + pub ev_pump: EventPump, + /// The width and height of the window. + pub dims: (u32, u32), + /// Rendering context defaults. + pub ctx: Context, +} + +/// Builder for creating `Window`s. +pub struct Builder<'title> { + pub dims: (u32, u32), + pub title: &'title str, + pub vsync: bool, + pub fs: FullscreenType, +} + +pub struct Framebuf<'a> { + pub color_buf: MutSlice2<'a, u8>, + pub depth_buf: MutSlice2<'a, f32>, +} + +// +// Inherent impls +// + +impl<'t> Builder<'t> { + /// Sets the width and height of the window. + pub fn dims(mut self, w: u32, h: u32) -> Self { + self.dims = (w, h); + self + } + /// Sets the title of the window. + pub fn title(mut self, title: &'t str) -> Self { + self.title = title; + self + } + /// Sets whether vertical sync is enabled. + /// + /// If true, frame rate is tied to the monitor's refresh rate. + pub fn vsync(mut self, enabled: bool) -> Self { + self.vsync = enabled; + self + } + /// Sets the fullscreen state of the window. + pub fn fullscreen(mut self, fs: FullscreenType) -> Self { + self.fs = fs; + self + } + + /// Creates the window. + pub fn build(self) -> Result { + let Self { dims, title, vsync, fs } = self; + + let sdl = sdl2::init()?; + + let mut win = sdl + .video()? + .window(title, dims.0, dims.1) + .build()?; + + win.set_fullscreen(fs)?; + sdl.mouse().set_relative_mouse_mode(true); + + let mut canvas = win.into_canvas(); + if vsync { + canvas = canvas.present_vsync(); + } + let canvas = canvas.accelerated().build()?; + + let ev_pump = sdl.event_pump()?; + + let ctx = Context::default(); + + let m = sdl.mouse(); + m.set_relative_mouse_mode(true); + m.capture(true); + m.show_cursor(true); + + Ok(Window { canvas, ev_pump, dims, ctx }) + } +} + +impl Window { + /// Returns a window builder. + pub fn builder() -> Builder<'static> { + Builder::default() + } + + pub fn present(&mut self, tex: &Texture) -> Result<(), Error> { + self.canvas.copy(&tex, None, None)?; + self.canvas.present(); + Ok(()) + } + + /// Runs the main loop of the program, invoking the callback on each + /// iteration to compute and draw the next frame. + /// + /// The main loop stops and this function returns if: + /// * the user closes the window via the GUI (e.g. title bar close button); + /// * the Esc key is pressed; or + /// * the callback returns `ControlFlow::Break`. + pub fn run(&mut self, mut frame_fn: F) -> Result<(), Error> + where + F: FnMut(&mut Frame) -> ControlFlow<()>, + { + let (w, h) = self.dims; + + let tc = self.canvas.texture_creator(); + let mut tex = tc.create_texture_streaming(None, w, h)?; + + let mut zbuf = Buf2::new(self.dims); + let mut ctx = self.ctx.clone(); + + let start = Instant::now(); + let mut last = Instant::now(); + 'main: loop { + for e in self.ev_pump.poll_iter() { + match e { + Event::Quit { .. } + | Event::KeyDown { + keycode: Some(Keycode::Escape), .. + } => break 'main, + _ => (), + } + } + + let cf = tex.with_lock(None, |cbuf, pitch| { + if let Some(c) = ctx.depth_clear { + // Z-buffer stores reciprocals + zbuf.fill(c.recip()); + } + if let Some(c) = ctx.color_clear { + let [r, g, b, a] = c.0; + cbuf.chunks_exact_mut(4).for_each(|ch| { + ch.copy_from_slice(&[b, g, r, a]); + }); + } + + let buf = Framebuf { + color_buf: MutSlice2::new((4 * w, h), pitch as u32, cbuf), + depth_buf: zbuf.as_mut_slice2(), + }; + + let frame = &mut Frame { + t: start.elapsed(), + dt: replace(&mut last, Instant::now()).elapsed(), + buf, + win: self, + ctx: &mut ctx, + }; + frame_fn(frame) + })?; + + self.present(&tex)?; + ctx.stats.borrow_mut().frames += 1.0; + + if cf.is_break() { + break; + } + } + println!("{}", ctx.stats.borrow()); + Ok(()) + } +} + +impl<'a> Target for Framebuf<'a> { + fn rasterize( + &mut self, + mut sl: Scanline, + fs: &Fs, + ctx: &Context, + ) -> Throughput + where + V: Vary, + Fs: FragmentShader, + { + // TODO Lots of duplicate code + + let x0 = sl.xs.start; + let x1 = sl.xs.end.max(sl.xs.start); + let cbuf_span = + &mut self.color_buf.as_mut_slice2()[sl.y][4 * x0..4 * x1]; + let zbuf_span = &mut self.depth_buf.as_mut_slice2()[sl.y][x0..x1]; + + let mut io = Throughput { i: x1 - x0, o: 0 }; + sl.fragments() + .zip(cbuf_span.chunks_exact_mut(4)) + .zip(zbuf_span) + .for_each(|((frag, c), z)| { + let new_z = frag.pos.z(); + if ctx.depth_test(new_z, *z) { + if let Some(new_c) = fs.shade_fragment(frag) { + if ctx.color_write { + // TODO Blending should happen here + io.o += 1; + let [r, g, b, a] = new_c.0; + c.copy_from_slice(&[b, g, r, a]); + } + if ctx.depth_write { + *z = new_z; + } + } + } + }); + io + } +} + +// +// Trait impls +// + +impl Default for Builder<'_> { + fn default() -> Self { + Self { + dims: dims::SVGA_800_600, + title: "// retrofire application //", + vsync: true, + fs: FullscreenType::Off, + } + } +} + +impl std::error::Error for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +macro_rules! impl_from_error { + ($($e:ty)+) => { $( + impl From<$e> for Error { + fn from(e: $e) -> Self { Self(e.to_string()) } + } + )+ }; +} + +impl_from_error! { + String + sdl2::Error + WindowBuildError + TextureValueError + IntegerOrSdlError +} diff --git a/front/src/wasm.rs b/front/src/wasm.rs index 925acced..cd267caf 100644 --- a/front/src/wasm.rs +++ b/front/src/wasm.rs @@ -2,28 +2,30 @@ #![allow(unused)] -use core::cell::RefCell; -use core::mem::transmute; -use core::ops::{ControlFlow, ControlFlow::*, Deref}; -use core::ptr::slice_from_raw_parts_mut; -use core::slice; -use core::time::Duration; - use alloc::rc::Rc; +use core::{ + cell::RefCell, + mem::transmute, + ops::{ControlFlow, ControlFlow::*, Deref}, + ptr::slice_from_raw_parts_mut, + slice, + time::Duration, +}; -use wasm_bindgen::prelude::*; -use wasm_bindgen::Clamped; +use wasm_bindgen::{prelude::*, Clamped}; -use web_sys::js_sys::{Uint32Array, Uint8ClampedArray}; use web_sys::{ + js_sys::{Uint32Array, Uint8ClampedArray}, CanvasRenderingContext2d as Context2d, Document, HtmlCanvasElement as Canvas, ImageData, }; -use retrofire_core::math::color::rgba; -use retrofire_core::render::{ctx::Context, stats::Stats, target::Framebuf}; -use retrofire_core::util::buf::{AsMutSlice2, Buf2, MutSlice2}; -use retrofire_core::util::Dims; +use retrofire_core::{ + math::color::rgba, + render::{target, Context, Stats}, + util::buf::{AsMutSlice2, Buf2, MutSlice2}, + util::Dims, +}; use crate::{dims::SVGA_800_600, Frame}; @@ -51,6 +53,9 @@ pub struct Builder { dims: Dims, } +pub type Framebuf<'a> = + target::Framebuf, MutSlice2<'a, f32>>; + impl Builder { pub fn dims(self, dims: Dims) -> Self { Self { dims, ..self } @@ -86,7 +91,7 @@ impl Window { pub fn run(mut self, mut frame_fn: F) where - F: FnMut(&mut Frame) -> ControlFlow<()> + 'static, + F: FnMut(&mut Frame) -> ControlFlow<()> + 'static, { let mut ctx = self.ctx.clone(); @@ -100,15 +105,6 @@ impl Window { outer .borrow_mut() .replace(Closure::new(move |ms| { - // TODO add clear method to Framebuf? - if let Some(c) = ctx.color_clear { - cbuf.fill(c.to_argb_u32()); - } - if let Some(z) = ctx.depth_clear { - // Depth buffer contains reciprocal depth values - zbuf.fill(z.recip()); - } - let t = Duration::from_secs_f32(ms / 1e3); let dt = t - t_last; let buf = Framebuf { @@ -122,6 +118,7 @@ impl Window { ctx: &mut ctx, win: &mut self, }; + frame.clear(); if let Continue(_) = frame_fn(&mut frame) { requestAnimationFrame(inner.borrow().as_ref().unwrap()); diff --git a/geom/src/io.rs b/geom/src/io.rs index f16b52d0..c5872ace 100644 --- a/geom/src/io.rs +++ b/geom/src/io.rs @@ -23,6 +23,9 @@ //! ## In the simplest case vertices only have positions: //! f 1 2 3 //! +//! ## Faces can have more than three indices: +//! f 1 2 3 4 5 +//! //! ## If vertices have extra attributes, indices are separated with a slash, //! ## in the order position/texcoord/normal: //! f 1/5/7 2/4/5 3/5/8 @@ -32,45 +35,39 @@ //! //! ## Normals can be specified without texture coordinates: //! f 1//7 2//4 3//8 -//! -//! ## Faces can have more than three indices: -//! f 1 2 3 4 5 //! ``` -use alloc::string::String; -use alloc::vec; -use core::fmt; -use core::num::{ParseFloatError, ParseIntError}; - +use alloc::{string::String, vec}; +use core::{ + fmt::{self, Display, Formatter}, + num::{ParseFloatError, ParseIntError}, +}; #[cfg(feature = "std")] use std::{ - error, fs::File, io::{BufReader, Read}, path::Path, }; use re::geom::{mesh::Builder, vertex, Mesh, Normal3, Tri}; -use re::math::vec::{vec3, Vec3, Vector}; -use re::render::{ - tex::{uv, TexCoord}, - Model, -}; +use re::math::{vec3, Point3, Vec3}; +use re::render::{uv, Model, TexCoord}; use Error::*; +/// Represents errors that may occur during reading OBJ data. #[derive(Debug)] pub enum Error { #[cfg(feature = "std")] - /// Input/output error during reading from a `Read`. + /// An input/output error during reading from a `Read`. Io(std::io::Error), - /// Unsupported line item identifier + /// An item that is not a face, vertex, texture coordinate, or normal. UnsupportedItem(char), /// Unexpected end of line or input. UnexpectedEnd, - /// Invalid integer or floating-point value. + /// An invalid integer or floating-point value. InvalidValue, - /// Vertex attribute index referring to nonexistent attribute. + /// A vertex attribute index that refers to a nonexistent attribute. IndexOutOfBounds(&'static str, usize), } @@ -86,43 +83,43 @@ pub type Result = core::result::Result; /// Loads an OBJ model from a path. /// /// # Errors -/// TODO +/// Returns [`Error`] if I/O or OBJ parsing fails. #[cfg(feature = "std")] pub fn load_obj(path: impl AsRef) -> Result> { let r = &mut BufReader::new(File::open(path)?); - read_from(r) + read_obj(r) } /// Reads an OBJ format mesh from input. /// /// # Errors -/// +/// Returns [`Error`] if I/O or OBJ parsing fails. #[cfg(feature = "std")] -pub fn read_from(input: impl Read) -> Result> { - let mut io_err = None; +pub fn read_obj(input: impl Read) -> Result> { + let mut io_res: Result<()> = Ok(()); let res = parse_obj(input.bytes().map_while(|r| match r { Err(e) => { - io_err = Some(e.into()); + io_res = Err(e.into()); None } Ok(b) => Some(b), })); - if let Some(e) = io_err { - Err(e) - } else { - res - } + io_res.and(res) } /// Parses an OBJ format mesh from an iterator. +/// +/// TODO Parses normals and coords but does not return them +/// +/// # Errors +/// Returns [`Error`] if OBJ parsing fails. pub fn parse_obj(src: impl IntoIterator) -> Result> { let mut faces = vec![]; let mut verts = vec![]; let mut norms = vec![]; - let mut tcrds = vec![]; - - let mut max = Indices { pos: 0, uv: None, n: None }; + let mut texcs = vec![]; + let mut max_i = Indices { pos: 0, uv: None, n: None }; let mut line = String::new(); let mut it = src.into_iter().peekable(); @@ -136,48 +133,50 @@ pub fn parse_obj(src: impl IntoIterator) -> Result> { ); let tokens = &mut line.split_ascii_whitespace(); - let Some(item) = tokens.next() else { continue; }; match item.as_bytes() { - [b'#', ..] => continue, // Skip comment - - b"v" => verts.push(parse_vector(tokens)?), - b"vt" => tcrds.push(parse_texcoord(tokens)?), + // Comment; skip it + [b'#', ..] => continue, + // Vertex position + b"v" => verts.push(parse_point(tokens)?), + // Texture coordinate + b"vt" => texcs.push(parse_texcoord(tokens)?), + // Normal vector b"vn" => norms.push(parse_normal(tokens)?), - + // Face b"f" => { - let tri @ Tri([a, b, c]) = parse_face(tokens)?; - - // TODO Ugly :( - max.pos = max.pos.max(a.pos).max(b.pos).max(c.pos); - max.uv = max.uv.max(a.uv).max(b.uv).max(c.uv); - max.n = max.n.max(a.n).max(b.n).max(c.n); - + let tri = parse_face(tokens)?; + // Keep track of max indices to report error at the end of + // parsing if there turned out to be out-of-bounds indices + for i in tri.0 { + max_i.pos = max_i.pos.max(i.pos); + max_i.uv = max_i.uv.max(i.uv); + max_i.n = max_i.n.max(i.n); + } faces.push(tri) } - // TODO Ignore unsupported lines instead? [c, ..] => return Err(UnsupportedItem(*c as char)), b"" => unreachable!("empty slices are filtered out"), } } - if !verts.is_empty() && max.pos >= verts.len() { - return Err(IndexOutOfBounds("vertex", max.pos)); + if !verts.is_empty() && max_i.pos >= verts.len() { + return Err(IndexOutOfBounds("vertex", max_i.pos)); } - if max.uv.is_some_and(|i| i >= tcrds.len()) { - return Err(IndexOutOfBounds("texcoord", max.uv.unwrap())); + if let Some(uv) = max_i.uv.filter(|&i| i >= texcs.len()) { + return Err(IndexOutOfBounds("texcoord", uv)); } - if max.n.is_some_and(|i| i >= norms.len()) { - return Err(IndexOutOfBounds("normal", max.n.unwrap())); + if let Some(n) = max_i.n.filter(|&i| i >= norms.len()) { + return Err(IndexOutOfBounds("normal", n)); } + // TODO Support returning texcoords and normals let faces = faces .into_iter() - .map(|Tri(vs)| Tri(vs.map(|i| i.pos))); - + .map(|Tri(vs)| Tri(vs.map(|ics| ics.pos))); let verts = verts.into_iter().map(|pos| vertex(pos, ())); Ok(Mesh::new(faces, verts).into_builder()) @@ -214,7 +213,13 @@ fn parse_vector<'a>( } fn parse_normal<'a>(i: &mut impl Iterator) -> Result { - parse_vector(i).map(Vector::to) + Ok(parse_vector(i)?.to()) +} + +fn parse_point<'a>( + i: &mut impl Iterator, +) -> Result> { + Ok(parse_vector(i)?.to_pt()) } fn parse_index(s: &str) -> Result { @@ -224,11 +229,15 @@ fn parse_index(s: &str) -> Result { fn parse_indices(param: &str) -> Result { let indices = &mut param.split('/'); - - let pos = parse_index(next(indices)?)?; - + let pos = next(indices).and_then(parse_index)?; + // Texcoord and normal are optional let uv = if let Some(uv) = indices.next() { - Some(parse_index(uv)?) + if !uv.is_empty() { + Some(parse_index(uv)?) + } else { + // `1//2`: only position and normal + None + } } else { None }; @@ -244,8 +253,8 @@ fn parse_indices(param: &str) -> Result { // Foreign trait impls // -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl Display for Error { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { #[cfg(feature = "std")] Io(e) => write!(f, "I/O error: {e}"), @@ -261,14 +270,13 @@ impl fmt::Display for Error { } } -#[cfg(feature = "std")] -impl error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { +impl core::error::Error for Error { + fn source(&self) -> Option<&(dyn core::error::Error + 'static)> { + #[cfg(feature = "std")] if let Io(e) = self { - Some(e) - } else { - None + return Some(e); } + None } } @@ -291,28 +299,96 @@ impl From for Error { #[cfg(test)] mod tests { - use re::geom::Tri; - use re::math::vec3; + use re::{geom::Tri, math::point::pt3}; use super::*; #[test] - fn valid_input() { + fn input_with_whitespace_and_comments() { let input = *br" # comment f 1 2 4 f 4 1 3 #anothercomment - v 0.0 0.0 0.0 + v 0.0 0.0 0.0 v 1.0 0.0 0.0 # comment with leading whitespace -v 0.0 2.0 0.0 -v 1.0 2.0 0.0"; +v 0.0 -2.0 0.0 + v 1 2 3"; + + let mesh = parse_obj(input).unwrap().build(); + + assert_eq!(mesh.faces, vec![Tri([0, 1, 3]), Tri([3, 0, 2])]); + assert_eq!(mesh.verts[3].pos, pt3(1.0, 2.0, 3.0)); + } + + #[test] + fn exp_notation() { + let input = *b"v -1.0e0 0.2e1 3.0e-2"; + let mesh = parse_obj(input).unwrap().build(); + assert_eq!(mesh.verts[0].pos, pt3(-1.0, 2.0, 0.03)); + } + + #[test] + fn positions_and_texcoords() { + let input = *br" + f 1/1/1 2/3/2 3/2/2 + f 4/3/2 1/2/3 3/1/3 + + vn 1.0 0.0 0.0 + vt 0.0 0.0 0.0 + v 0.0 0.0 0.0 + v 1.0 0.0 0.0 + vn 1.0 0.0 0.0 + v 0.0 2.0 0.0 + vt 1.0 1.0 1.0 + v 1.0 2.0 3.0 + vt 0.0 -1.0 2.0 + vn 1.0 0.0 0.0"; let mesh = parse_obj(input).unwrap().build(); + assert_eq!(mesh.faces, vec![Tri([0, 1, 2]), Tri([3, 0, 2])]); + let v = mesh.verts[3]; + assert_eq!(v.pos, pt3(1.0, 2.0, 3.0)); + } + + #[test] + fn positions_and_normals() { + let input = *br" + f 1//1 2//3 4//2 + f 4//3 1//2 3//1 + + vn 1.0 0.0 0.0 + v 0.0 0.0 0.0 + v 1.0 0.0 0.0 + v 0.0 2.0 0.0 + vn 0.0 1.0 0.0 + v 1.0 2.0 3.0 + vn 0.0 0.0 -1.0"; + + let mesh = parse_obj(input).unwrap().build(); + assert_eq!(mesh.faces, vec![Tri([0, 1, 3]), Tri([3, 0, 2])]); + assert_eq!(mesh.verts[3].pos, pt3(1.0, 2.0, 3.0)); + } + + #[test] + fn positions_texcoords_and_normals() { + let input = *br" + f 1//1 2//3 4//2 + f 4//3 1//2 3//1 + + vn 1.0 0.0 0.0 + v 0.0 0.0 0.0 + v 1.0 0.0 0.0 + v 0.0 2.0 0.0 + vn 0.0 1.0 0.0 + v 1.0 2.0 3.0 + vn 0.0 0.0 -1.0"; + + let mesh = parse_obj(input).unwrap().build(); assert_eq!(mesh.faces, vec![Tri([0, 1, 3]), Tri([3, 0, 2])]); - assert_eq!(mesh.verts[3].pos, vec3(1.0, 2.0, 0.0)); + assert_eq!(mesh.verts[3].pos, pt3(1.0, 2.0, 3.0)); } #[test] @@ -395,7 +471,7 @@ v 1.0 2.0 0.0"; } } - let result = read_from(R(true)); + let result = read_obj(R(true)); if let Err(Io(e)) = result { assert_eq!(e.kind(), ErrorKind::BrokenPipe); diff --git a/geom/src/solids.rs b/geom/src/solids.rs index eb3382a5..3b526414 100644 --- a/geom/src/solids.rs +++ b/geom/src/solids.rs @@ -1,613 +1,7 @@ //! Mesh approximations of various geometric shapes. -use core::array::from_fn; -use core::ops::Range; +mod lathe; +mod platonic; -use alloc::{vec, vec::Vec}; - -use re::geom::{mesh::Builder, vertex, Mesh, Normal2, Normal3, Vertex}; -use re::math::{ - angle::{degs, polar, turns, Angle}, - mat::rotate_y, - vary::Vary, - vec::{splat, vec2, vec3, Vec2, Vec3}, -}; -use re::render::tex::{uv, TexCoord}; - -/// A regular tetrahedron. -/// -/// A Platonic solid with four vertices and four equilateral triangle faces. -/// The tetrahedron is its own dual. -/// -/// `Tetrahedron`'s vertices are at: -/// * (0, 1, 0), -/// * (√(8/9), -1/3, 0), -/// * (-√(2/9), -1/3, √(2/3)), and -/// * (-√(8/9), -1/3, -√(2/3)). -#[derive(Copy, Clone, Debug)] -pub struct Tetrahedron; - -/// A rectangular cuboid. -/// -/// Defined by the left-bottom-near and right-top-far vertices of the box. -/// -/// An equilateral box is a cube, a platonic solid with six square faces. -/// The dual of the cube is the octahedron. -/// -/// Assuming the two defining vertices are (l, b, n) and (r, t, f), -/// the vertices of a `Box` are at -/// * (l, b, n) -/// * (l, b, f) -/// * (l, t, n) -/// * (l, t, f) -/// * (r, b, n) -/// * (r, b, f) -/// * (r, t, n) -/// * (r, t, f) -#[derive(Copy, Clone, Debug)] -pub struct Box { - /// The left bottom near corner of the box. - pub left_bot_near: Vec3, - /// The right top far corner of the box. - pub right_top_far: Vec3, -} - -/// Regular octahedron. -/// -/// A Platonic solid with six vertices and eight equilateral triangle faces. -/// The octahedron is the dual of the cube. -/// -/// `Octahedron`'s vertices are at (±1, 0, 0), (0, ±1, 0), and (0, 0, ±1). -#[derive(Copy, Clone, Debug, Default)] -pub struct Octahedron; - -/// Regular dodecahedron. -/// -/// A Platonic solid with twenty vertices and twelve regular pentagonal faces. -/// Three edges meet at every vertex. The dual of the dodecahedron is the -/// icosahedron. -/// -/// `Dodecahedron`'s vertices are at: -/// * (±1, ±1, ±1) -/// * (±φ, ±1/φ, 0) -/// * (±1/φ, ±φ, 0) -/// * (±φ, 0, ±1/φ) -/// * (±1/φ, 0, ±φ) -/// * (0, ±φ, ±1/φ) -/// * (0, ±1/φ, ±φ) -/// -/// where φ ≈ 1.618 is the golden ratio constant. -#[derive(Copy, Clone, Debug, Default)] -pub struct Dodecahedron; - -/// Regular icosahedron. -/// -/// A Platonic solid with twelve vertices and twenty equilateral triangle -/// faces. Five edges meet at every vertex. The dual of the icosahedron is -/// the dodecahedron. -/// -/// `Icosahedron`'s vertices are at: -/// * (±1, 0, ±φ) -/// * (±φ, ±1, 0) -/// * (0, ±φ, ±1), -/// -/// where φ ≈ 1.618 is the golden ratio constant. -#[derive(Copy, Clone, Debug, Default)] -pub struct Icosahedron; - -/// A surface-of-revolution shape generated by rotating a polyline -/// lying on the xy-plane one full revolution around the y-axis. -#[derive(Clone, Debug, Default)] -pub struct Lathe { - /// The polyline defining the shape. - pub pts: Vec>, - /// The number of facets used to approximate the surface of revolution. - pub sectors: u32, - /// Whether to add flat caps to both ends of the object. Has no effect - /// if the endpoints already lie on the y-axis. - pub capped: bool, - // The range of angles to rotate over. - pub az_range: Range, -} - -/// TODO -pub struct Sphere { - pub sectors: u32, - pub segments: u32, - pub radius: f32, -} - -/// Toroidal polyhedron. -pub struct Torus { - /// Distance from the origin to the center of the tube. - pub major_radius: f32, - /// Radius of the cross-section of the tube. - pub minor_radius: f32, - - pub major_sectors: u32, - pub minor_sectors: u32, -} - -/// Right cylinder with regular *n*-gonal cross-section. -pub struct Cylinder { - pub sectors: u32, - pub capped: bool, - pub radius: f32, -} - -/// TODO -pub struct Cone { - pub sectors: u32, - pub capped: bool, - pub base_radius: f32, - pub apex_radius: f32, -} - -/// Cylinder with hemispherical caps. -pub struct Capsule { - pub sectors: u32, - pub cap_segments: u32, - pub radius: f32, -} - -// -// Inherent impls -// - -impl Tetrahedron { - const FACES: [[usize; 3]; 4] = [[0, 2, 1], [0, 3, 2], [0, 1, 3], [1, 2, 3]]; - - /// Builds the tetrahedral mesh. - pub fn build(self) -> Mesh { - use re::math::float::f32; - let sqrt = f32::sqrt; - let coords = [ - vec3(0.0, 1.0, 0.0), - vec3(sqrt(8.0 / 9.0), -1.0 / 3.0, 0.0), - vec3(-sqrt(2.0 / 9.0), -1.0 / 3.0, sqrt(2.0 / 3.0)), - vec3(-sqrt(2.0 / 9.0), -1.0 / 3.0, -sqrt(2.0 / 3.0)), - ]; - let norms = [-coords[3], -coords[1], -coords[2], -coords[0]]; - - let mut b = Mesh::builder(); - - for (i, vs) in Self::FACES.into_iter().enumerate() { - b.push_face(3 * i, 3 * i + 1, 3 * i + 2); - for v in vs { - b.push_vert(coords[v], norms[i]); - } - } - b.build() - } -} - -impl Box { - const COORDS: [Vec3; 8] = [ - // left - vec3(0.0, 0.0, 0.0), // 0b000 - vec3(0.0, 0.0, 1.0), // 0b001 - vec3(0.0, 1.0, 0.0), // 0b010 - vec3(0.0, 1.0, 1.0), // 0b011 - // right - vec3(1.0, 0.0, 0.0), // 0b100 - vec3(1.0, 0.0, 1.0), // 0b101 - vec3(1.0, 1.0, 0.0), // 0b110 - vec3(1.0, 1.0, 1.0), // 0b111 - ]; - const NORMS: [Normal3; 6] = [ - vec3(-1.0, 0.0, 0.0), - vec3(1.0, 0.0, 0.0), - vec3(0.0, -1.0, 0.0), - vec3(0.0, 1.0, 0.0), - vec3(0.0, 0.0, -1.0), - vec3(0.0, 0.0, 1.0), - ]; - #[allow(unused)] - const TEX_COORDS: [TexCoord; 4] = - [uv(0.0, 0.0), uv(1.0, 0.0), uv(0.0, 1.0), uv(1.0, 1.0)]; - #[rustfmt::skip] - const VERTS: [(usize, [usize; 2]); 24] = [ - // left - (0b011, [0, 0]), (0b010, [0, 1]), (0b001, [0, 2]), (0b000, [0, 3]), - // right - (0b110, [1, 0]), (0b111, [1, 1]), (0b100, [1, 2]), (0b101, [1, 3]), - // bottom - (0b000, [2, 0]), (0b100, [2, 1]), (0b001, [2, 2]), (0b101, [2, 3]), - // top - (0b011, [3, 0]), (0b111, [3, 1]), (0b010, [3, 2]), (0b110, [3, 3]), - // front - (0b010, [4, 0]), (0b110, [4, 1]), (0b000, [4, 2]), (0b100, [4, 3]), - // back - (0b111, [5, 0]), (0b011, [5, 1]), (0b101, [5, 2]), (0b001, [5, 3]), - ]; - #[rustfmt::skip] - const FACES: [[usize; 3]; 12] = [ - // left - [0, 1, 3], [0, 3, 2], - // right - [4, 5, 7], [4, 7, 6], - // bottom - [8, 9, 11], [8, 11, 10], - // top - [12, 13, 15], [12, 15, 14], - // front - [16, 17, 19], [16, 19, 18], - // back - [20, 21, 23], [20, 23, 22], - ]; - - /// Returns a cube centered on the origin, with the given side length. - pub fn cube(side_len: f32) -> Self { - Self { - left_bot_near: splat(-0.5 * side_len), - right_top_far: splat(0.5 * side_len), - } - } - - /// Builds the cuboid mesh. - pub fn build(self) -> Mesh { - let mut b = Mesh::builder(); - b.push_faces(Self::FACES); - for (pos_i, [norm_i, _uv_i]) in Self::VERTS { - let pos = from_fn(|i| { - self.left_bot_near[i] - .lerp(&self.right_top_far[i], Self::COORDS[pos_i][i]) - }); - b.push_vert(pos.into(), Self::NORMS[norm_i]); - } - b.build() - } -} - -impl Octahedron { - const COORDS: [Vec3; 6] = [ - vec3(-1.0, 0.0, 0.0), - vec3(0.0, -1.0, 0.0), - vec3(0.0, 0.0, -1.0), - vec3(0.0, 1.0, 0.0), - vec3(0.0, 0.0, 1.0), - vec3(1.0, 0.0, 0.0), - ]; - const NORMS: [Normal3; 8] = [ - vec3(-1.0, -1.0, -1.0), - vec3(-1.0, 1.0, -1.0), - vec3(-1.0, 1.0, 1.0), - vec3(-1.0, -1.0, 1.0), - vec3(1.0, -1.0, -1.0), - vec3(1.0, 1.0, -1.0), - vec3(1.0, 1.0, 1.0), - vec3(1.0, -1.0, 1.0), - ]; - #[rustfmt::skip] - const VERTS: [(usize, usize); 24] = [ - (0, 0), (2, 0), (1, 0), - (0, 1), (3, 1), (2, 1), - (0, 2), (4, 2), (3, 2), - (0, 3), (1, 3), (4, 3), - (1, 4), (2, 4), (5, 4), - (2, 5), (3, 5), (5, 5), - (3, 6), (4, 6), (5, 6), - (1, 7), (5, 7), (4, 7), - ]; - const FACES: [[usize; 3]; 8] = [ - [0, 1, 2], - [3, 4, 5], - [6, 7, 8], - [9, 10, 11], - [12, 13, 14], - [15, 16, 17], - [18, 19, 20], - [21, 22, 23], - ]; - - /// Builds the octahedral mesh. - pub fn build(self) -> Mesh { - let mut b = Mesh::builder(); - for (i, vs) in Self::FACES.iter().enumerate() { - b.push_face(3 * i, 3 * i + 1, 3 * i + 2); - for vi in *vs { - let pos = Self::COORDS[Self::VERTS[vi].0]; - b.push_vert(pos, Self::NORMS[i]); - } - } - b.build() - } -} - -/// The golden ratio constant φ. -const PHI: f32 = 1.618034_f32; -/// Reciprocal of φ. -const R_PHI: f32 = 1.0 / PHI; - -impl Dodecahedron { - #[rustfmt::skip] - const COORDS: [Vec3; 20] = [ - // -X - vec3(-PHI, -R_PHI, 0.0), - vec3(-PHI, R_PHI, 0.0), - // +X - vec3( PHI, -R_PHI, 0.0), - vec3( PHI, R_PHI, 0.0), - // -Y - vec3(0.0, -PHI, -R_PHI), - vec3(0.0, -PHI, R_PHI), - // +Y - vec3(0.0, PHI, -R_PHI), - vec3(0.0, PHI, R_PHI), - // -Z - vec3(-R_PHI, 0.0, -PHI), - vec3( R_PHI, 0.0, -PHI), - // +Z - vec3(-R_PHI, 0.0, PHI), - vec3( R_PHI, 0.0, PHI), - - // Corner verts, corresponding to the corner faces of the icosahedron. - vec3(-1.0, -1.0, -1.0), - vec3(-1.0, -1.0, 1.0), - vec3(-1.0, 1.0, -1.0), - vec3(-1.0, 1.0, 1.0), - vec3( 1.0, -1.0, -1.0), - vec3( 1.0, -1.0, 1.0), - vec3( 1.0, 1.0, -1.0), - vec3( 1.0, 1.0, 1.0), - - ]; - #[rustfmt::skip] - const FACES: [[usize; 5]; 12] = [ - [ 0, 1, 14, 8, 12], [ 1, 0, 13, 10, 15], - [ 3, 2, 16, 9, 18], [ 2, 3, 19, 11, 17], - [ 4, 5, 13, 0, 12], [ 5, 4, 16, 2, 17], - [ 7, 6, 14, 1, 15], [ 6, 7, 19, 3, 18], - [ 8, 9, 16, 4, 12], [ 9, 8, 14, 6, 18], - [11, 10, 13, 5, 17], [10, 11, 19, 7, 15], - ]; - - /// The normals are exactly the vertices of the icosahedron, normalized. - const NORMALS: [Vec3; 12] = Icosahedron::COORDS; - - /// Builds the dodecahedral mesh. - pub fn build(self) -> Mesh { - let mut b = Mesh::builder(); - - for (i, face) in Self::FACES.iter().enumerate() { - let n = Self::NORMALS[i].normalize(); - // Make a pentagon from three triangles - let i5 = 5 * i; - b.push_face(i5, i5 + 1, i5 + 2); - b.push_face(i5, i5 + 2, i5 + 3); - b.push_face(i5, i5 + 3, i5 + 4); - for &j in face { - b.push_vert(Self::COORDS[j].normalize(), n); - } - } - b.build() - } -} - -impl Icosahedron { - #[rustfmt::skip] - 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 - - vec3(-1.0, -PHI, 0.0), vec3(1.0, -PHI, 0.0), // -Y - vec3(-1.0, PHI, 0.0), vec3(1.0, PHI, 0.0), // +Y - - vec3(0.0, -1.0, -PHI), vec3(0.0, 1.0, -PHI), // -Z - vec3(0.0, -1.0, PHI), vec3(0.0, 1.0, PHI), // +Z - ]; - #[rustfmt::skip] - 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 - [6, 7, 9], [6, 11, 7], // +Y - [8, 0, 9], [8, 9, 2], // -Z - [10, 11, 1], [10, 3, 11], // +Z - - // Corner faces, corresponding to the corner verts of the dodecahedron. - [0, 8, 4], [1, 4, 10], // -X-Y -Z,+Z - [0, 6, 9], [1, 11, 6], // -X+Y " - [2, 5, 8], [3, 10, 5], // +X-Y " - [2, 9, 7], [3, 7, 11], // +X+Y " - ]; - - /// The normals are exactly the vertices of the dodecahedron, normalized. - const NORMALS: [Vec3; 20] = Dodecahedron::COORDS; - - /// Builds the icosahedral mesh. - pub fn build(self) -> Mesh { - let mut b = Mesh::builder(); - for (i, vs) in Self::FACES.iter().enumerate() { - let n = Self::NORMALS[i].normalize(); - b.push_face(3 * i, 3 * i + 1, 3 * i + 2); - for vi in *vs { - b.push_vert(Self::COORDS[vi].normalize(), n); - } - } - b.build() - } -} - -impl Lathe { - pub fn new(pts: Vec>, sectors: u32) -> Self { - assert!(sectors >= 3, "sectors must be at least 3, was {sectors}"); - Self { - pts, - sectors, - capped: false, - az_range: turns(0.0)..turns(1.0), - } - } - - pub fn capped(self, capped: bool) -> Self { - Self { capped, ..self } - } - - /// Builds the lathe mesh. - pub fn build(self) -> Mesh { - let Self { pts, sectors, capped, az_range } = self; - let secs = sectors as usize; - - let mut b = Builder { - mesh: Mesh { - verts: Vec::with_capacity(pts.len() * (secs + 1) + 2), - faces: Vec::with_capacity(pts.len() * secs * 2), - }, - }; - - let start = rotate_y(az_range.start); - let rot = rotate_y((az_range.end - az_range.start) / secs as f32); - - // Create vertices - for Vertex { pos, attrib: n } in &pts { - let mut pos = start.apply(&vec3(pos.x(), pos.y(), 0.0)); - let mut norm = start.apply(&vec3(n.x(), n.y(), 0.0)).normalize(); - - for _ in 0..=secs { - b.push_vert(pos, norm); - pos = rot.apply(&pos); - norm = rot.apply(&norm); - } - } - // Create faces - for j in 1..pts.len() { - let n = secs + 1; - for i in 1..n { - let p = (j - 1) * n + i - 1; - let q = (j - 1) * n + i; - let r = j * n + i - 1; - let s = j * n + i; - b.push_face(p, s, q); - b.push_face(p, r, s); - } - } - // Create optional caps - if capped && !pts.is_empty() { - let l = b.mesh.verts.len(); - let bottom_rng = 0..=secs; - let top_rng = (l - secs - 1)..l; - - // Duplicate the bottom ring of vertices to make the bottom cap... - let mut bottom_vs: Vec<_> = b.mesh.verts[bottom_rng] - .iter() - .map(|v| vertex(v.pos, vec3(0.0, -1.0, 0.0))) - .collect(); - b.mesh.verts.append(&mut bottom_vs); - for i in 1..secs { - b.push_face(l, l + i, l + i + 1); - } - - // ...and the top vertices to make the top cap - let l = b.mesh.verts.len(); - let mut top_vs: Vec<_> = b.mesh.verts[top_rng] - .iter() - .map(|v| vertex(v.pos, vec3(0.0, 1.0, 0.0))) - .collect(); - b.mesh.verts.append(&mut top_vs); - for i in 1..secs { - b.push_face(l, l + i + 1, l + i); - } - } - b.build() - } -} - -impl Sphere { - /// Builds the spherical mesh. - pub fn build(self) -> Mesh { - let Self { sectors, segments, radius } = self; - - let pts = degs(-90.0) - .vary_to(degs(90.0), segments) - .map(|alt| polar(radius, alt).to_cart()) - .map(|pos| vertex(pos, pos)) - .collect(); - - Lathe::new(pts, sectors).build() - } -} - -impl Torus { - /// Builds the toroidal mesh. - pub fn build(self) -> Mesh { - let pts = turns(0.0) - .vary_to(turns(1.0), self.minor_sectors) - .map(|alt| polar(self.minor_radius, alt).to_cart()) - .map(|v| vertex(vec2(self.major_radius, 0.0) + v, v)) - .collect(); - - Lathe::new(pts, self.major_sectors).build() - } -} - -impl Cylinder { - /// Builds the cylindrical mesh. - pub fn build(self) -> Mesh { - let Self { sectors, capped, radius } = self; - Cone { - sectors, - capped, - base_radius: radius, - apex_radius: radius, - } - .build() - } -} - -impl Cone { - /// Builds the conical mesh. - pub fn build(self) -> Mesh { - let base_pt = vec2(self.base_radius, -1.0); - let apex_pt = vec2(self.apex_radius, 1.0); - let n = apex_pt - base_pt; - let n = vec2(n.y(), -n.x()); - let pts = vec![vertex(base_pt, n), vertex(apex_pt, n)]; - Lathe::new(pts, self.sectors) - .capped(self.capped) - .build() - } -} - -impl Capsule { - /// Builds the capsule mesh. - pub fn build(self) -> Mesh { - let Self { sectors, cap_segments, radius } = self; - - // Bottom hemisphere - let bottom_pts: Vec<_> = degs(-90.0) - .vary_to(degs(0.0), cap_segments) - .map(|alt| polar(radius, alt).to_cart()) - .map(|v| vertex(vec2(0.0, -1.0) + v, v)) - .collect(); - - // Top hemisphere - let top_pts = bottom_pts - .iter() - .map(|Vertex { pos, attrib: n }| { - vertex(vec2(pos.x(), -pos.y()), vec2(n.x(), -n.y())) - }) - .rev(); - - Lathe::new( - bottom_pts - .iter() - .copied() - .chain(top_pts) - .collect(), - sectors, - ) - .build() - } -} - -// -// Trait impls -// - -impl Default for Box { - /// Creates a cube with unit-length edges, centered at the origin. - fn default() -> Self { - Self::cube(1.0) - } -} +pub use lathe::*; +pub use platonic::*; diff --git a/geom/src/solids/lathe.rs b/geom/src/solids/lathe.rs new file mode 100644 index 00000000..0cb1361f --- /dev/null +++ b/geom/src/solids/lathe.rs @@ -0,0 +1,259 @@ +use alloc::vec::Vec; +use core::ops::Range; + +use re::geom::{vertex, Mesh, Normal2, Normal3, Vertex, Vertex2}; +use re::math::{ + degs, polar, pt2, pt3, rotate_y, turns, vec2, vec3, Angle, Vary, +}; + +/// A surface-of-revolution shape generated by rotating a polyline +/// lying on the xy-plane one full revolution around the y-axis. +#[derive(Clone, Debug, Default)] +pub struct Lathe { + /// The polyline defining the shape. + pub points: Vec>, + /// The number of facets used to approximate the surface of revolution. + pub sectors: u32, + /// Whether to add flat caps to both ends of the object. Has no effect + /// if the endpoints already lie on the y-axis. + pub capped: bool, + // The range of angles to rotate over. + pub az_range: Range, +} + +/// TODO +pub struct Sphere { + pub sectors: u32, + pub segments: u32, + pub radius: f32, +} + +/// Toroidal polyhedron. +pub struct Torus { + /// Distance from the origin to the center of the tube. + pub major_radius: f32, + /// Radius of the cross-section of the tube. + pub minor_radius: f32, + + pub major_sectors: u32, + pub minor_sectors: u32, +} + +/// Right cylinder with regular *n*-gonal cross-section. +pub struct Cylinder { + pub sectors: u32, + pub segments: u32, + pub capped: bool, + pub radius: f32, +} + +/// TODO +pub struct Cone { + pub sectors: u32, + pub segments: u32, + pub capped: bool, + pub base_radius: f32, + pub apex_radius: f32, +} + +/// Cylinder with hemispherical caps. +pub struct Capsule { + pub sectors: u32, + pub body_segments: u32, + pub cap_segments: u32, + pub radius: f32, +} + +// +// Inherent impls +// + +impl Lathe { + pub fn new(points: Pts, sectors: u32) -> Self + where + Pts: IntoIterator>, + { + assert!(sectors >= 3, "sectors must be at least 3, was {sectors}"); + Self { + points: points.into_iter().collect(), + sectors, + capped: false, + az_range: turns(0.0)..turns(1.0), + } + } + + pub fn capped(self, capped: bool) -> Self { + Self { capped, ..self } + } + + /// Builds the lathe mesh. + pub fn build(self) -> Mesh { + let Self { points, sectors, az_range, .. } = self; + let secs = sectors as usize; + + let n_points = points.len(); + let mut b = Mesh { + verts: Vec::with_capacity(n_points * (secs + 1) + 2), + faces: Vec::with_capacity(n_points * secs * 2), + } + .into_builder(); + + let start = rotate_y(az_range.start); + let rot = rotate_y((az_range.end - az_range.start) / secs as f32); + + // Create vertices + for Vertex { pos, attrib: n } in &points { + let mut pos = start.apply_pt(&pt3(pos.x(), pos.y(), 0.0)); + let mut norm = start.apply(&vec3(n.x(), n.y(), 0.0)).normalize(); + + for _ in 0..=secs { + b.push_vert(pos, norm); + pos = rot.apply_pt(&pos); + norm = rot.apply(&norm); + } + } + // Create faces + for j in 1..n_points { + let n = secs + 1; + for i in 1..n { + let p = (j - 1) * n + i - 1; + let q = (j - 1) * n + i; + let r = j * n + i - 1; + let s = j * n + i; + b.push_face(p, s, q); + b.push_face(p, r, s); + } + } + // Create optional caps + if self.capped && n_points > 0 { + let l = b.mesh.verts.len(); + let bottom_rng = 0..=secs; + let top_rng = (l - secs - 1)..l; + + // Duplicate the bottom ring of vertices to make the bottom cap... + let mut bottom_vs: Vec<_> = b.mesh.verts[bottom_rng] + .iter() + .map(|v| vertex(v.pos, vec3(0.0, -1.0, 0.0))) + .collect(); + b.mesh.verts.append(&mut bottom_vs); + for i in 1..secs { + b.push_face(l, l + i, l + i + 1); + } + + // ...and the top vertices to make the top cap + let l = b.mesh.verts.len(); + let mut top_vs: Vec<_> = b.mesh.verts[top_rng] + .iter() + .map(|v| vertex(v.pos, vec3(0.0, 1.0, 0.0))) + .collect(); + b.mesh.verts.append(&mut top_vs); + for i in 1..secs { + b.push_face(l, l + i + 1, l + i); + } + } + b.build() + } +} + +impl Sphere { + /// Builds the spherical mesh. + pub fn build(self) -> Mesh { + let Self { sectors, segments, radius } = self; + + let pts = degs(-90.0) + .vary_to(degs(90.0), segments) + .map(|alt| polar(radius, alt).to_cart()) + .map(|pos| vertex(pos.to_pt(), pos)); + + Lathe::new(pts, sectors).build() + } +} + +impl Torus { + /// Builds the toroidal mesh. + pub fn build(self) -> Mesh { + let pts = turns(0.0) + .vary_to(turns(1.0), self.minor_sectors) + .map(|alt| polar(self.minor_radius, alt).to_cart()) + .map(|v| vertex(pt2(self.major_radius, 0.0) + v, v)); + + Lathe::new(pts, self.major_sectors).build() + } +} + +impl Cylinder { + /// Builds the cylindrical mesh. + pub fn build(self) -> Mesh { + #[rustfmt::skip] + let Self { sectors, segments, capped, radius } = self; + Cone { + sectors, + segments, + capped, + base_radius: radius, + apex_radius: radius, + } + .build() + } +} + +impl Cone { + /// Builds the conical mesh. + pub fn build(self) -> Mesh { + assert!(self.segments > 0, "segments cannot be zero"); + + let base_pt = pt2(self.base_radius, -1.0); + let apex_pt = pt2(self.apex_radius, 1.0); + + let n = apex_pt - base_pt; + let n = vec2(n.y(), -n.x()); + + let pts = base_pt + .vary_to(apex_pt, self.segments) + .map(|pt| vertex(pt, n)); + + Lathe::new(pts, self.sectors) + .capped(self.capped) + .build() + } +} + +impl Capsule { + /// Builds the capsule mesh. + pub fn build(self) -> Mesh { + #[rustfmt::skip] + let Self { sectors, body_segments, cap_segments, radius } = self; + assert!(body_segments > 0, "body segments cannot be zero"); + assert!(cap_segments > 0, "cap segments cannot be zero"); + + // Must be collected to allow rev() + let bottom_pts: Vec<_> = degs(-90.0) + .vary_to(degs(0.0), cap_segments) + .map(|alt| polar(radius, alt).to_cart()) + .map(|v| vertex(pt2(0.0, -1.0) + v, v)) + .collect(); + + let top_pts = bottom_pts + .iter() + .map(|Vertex { pos, attrib: n }| { + vertex(pt2(pos.x(), -pos.y()), vec2(n.x(), -n.y())) + }) + .rev(); + + let body_pts = pt2(radius, -1.0) + .vary_to(pt2(radius, 1.0), body_segments) + .map(|pt| vertex(pt, vec2(1.0, 0.0))) + .skip(1) + .take(body_segments as usize - 1); + + Lathe::new( + bottom_pts + .iter() + .copied() + .chain(body_pts) + .chain(top_pts), + sectors, + ) + .build() + } +} diff --git a/geom/src/solids/platonic.rs b/geom/src/solids/platonic.rs new file mode 100644 index 00000000..f8476916 --- /dev/null +++ b/geom/src/solids/platonic.rs @@ -0,0 +1,376 @@ +//! The five Platonic solids: tetrahedron, cube, octahedron, dodecahedron, +//! and icosahedron. + +use core::array::from_fn; + +use re::geom::{Mesh, Normal3}; +use re::math::{pt3, vec3, Lerp, Point3, Vec3}; +use re::render::{uv, TexCoord}; + +/// A regular tetrahedron. +/// +/// A Platonic solid with four vertices and four equilateral triangle faces. +/// The tetrahedron is its own dual. +/// +/// `Tetrahedron`'s vertices are at: +/// * (0, 1, 0), +/// * (√(8/9), -1/3, 0), +/// * (-√(2/9), -1/3, √(2/3)), and +/// * (-√(8/9), -1/3, -√(2/3)). +#[derive(Copy, Clone, Debug)] +pub struct Tetrahedron; + +/// A rectangular cuboid. +/// +/// Defined by the left-bottom-near and right-top-far vertices of the box. +/// +/// An equilateral box is a cube, a platonic solid with six square faces. +/// The dual of the cube is the octahedron. +/// +/// Assuming the two defining vertices are (l, b, n) and (r, t, f), +/// the vertices of a `Box` are at +/// * (l, b, n) +/// * (l, b, f) +/// * (l, t, n) +/// * (l, t, f) +/// * (r, b, n) +/// * (r, b, f) +/// * (r, t, n) +/// * (r, t, f) +#[derive(Copy, Clone, Debug)] +pub struct Box { + /// The left bottom near corner of the box. + pub left_bot_near: Point3, + /// The right top far corner of the box. + pub right_top_far: Point3, +} + +/// Regular octahedron. +/// +/// A Platonic solid with six vertices and eight equilateral triangle faces. +/// The octahedron is the dual of the cube. +/// +/// `Octahedron`'s vertices are at (±1, 0, 0), (0, ±1, 0), and (0, 0, ±1). +#[derive(Copy, Clone, Debug, Default)] +pub struct Octahedron; + +/// Regular dodecahedron. +/// +/// A Platonic solid with twenty vertices and twelve regular pentagonal faces. +/// Three edges meet at every vertex. The dual of the dodecahedron is the +/// icosahedron. +/// +/// `Dodecahedron`'s vertices are at: +/// * (±1, ±1, ±1) +/// * (±φ, ±1/φ, 0) +/// * (±1/φ, ±φ, 0) +/// * (±φ, 0, ±1/φ) +/// * (±1/φ, 0, ±φ) +/// * (0, ±φ, ±1/φ) +/// * (0, ±1/φ, ±φ) +/// +/// where φ ≈ 1.618 is the golden ratio constant. +#[derive(Copy, Clone, Debug, Default)] +pub struct Dodecahedron; + +/// Regular icosahedron. +/// +/// A Platonic solid with twelve vertices and twenty equilateral triangle +/// faces. Five edges meet at every vertex. The dual of the icosahedron is +/// the dodecahedron. +/// +/// `Icosahedron`'s vertices are at: +/// * (±1, 0, ±φ) +/// * (±φ, ±1, 0) +/// * (0, ±φ, ±1), +/// +/// where φ ≈ 1.618 is the golden ratio constant. +#[derive(Copy, Clone, Debug, Default)] +pub struct Icosahedron; + +impl Tetrahedron { + const FACES: [[usize; 3]; 4] = [[0, 2, 1], [0, 3, 2], [0, 1, 3], [1, 2, 3]]; + + /// Builds the tetrahedral mesh. + pub fn build(self) -> Mesh { + use re::math::float::f32; + let sqrt = f32::sqrt; + let coords = [ + pt3(0.0, 1.0, 0.0), + pt3(sqrt(8.0 / 9.0), -1.0 / 3.0, 0.0), + pt3(-sqrt(2.0 / 9.0), -1.0 / 3.0, sqrt(2.0 / 3.0)), + pt3(-sqrt(2.0 / 9.0), -1.0 / 3.0, -sqrt(2.0 / 3.0)), + ]; + let norms = [3, 1, 2, 0].map(|i| -coords[i].to_vec()); + + let mut b = Mesh::builder(); + + for (i, vs) in Self::FACES.into_iter().enumerate() { + b.push_face(3 * i, 3 * i + 1, 3 * i + 2); + for v in vs { + b.push_vert(coords[v], norms[i]); + } + } + b.build() + } +} + +impl Box { + const COORDS: [Vec3; 8] = [ + // left + vec3(0.0, 0.0, 0.0), // 0b000 + vec3(0.0, 0.0, 1.0), // 0b001 + vec3(0.0, 1.0, 0.0), // 0b010 + vec3(0.0, 1.0, 1.0), // 0b011 + // right + vec3(1.0, 0.0, 0.0), // 0b100 + vec3(1.0, 0.0, 1.0), // 0b101 + vec3(1.0, 1.0, 0.0), // 0b110 + vec3(1.0, 1.0, 1.0), // 0b111 + ]; + const NORMS: [Normal3; 6] = [ + vec3(-1.0, 0.0, 0.0), + vec3(1.0, 0.0, 0.0), + vec3(0.0, -1.0, 0.0), + vec3(0.0, 1.0, 0.0), + vec3(0.0, 0.0, -1.0), + vec3(0.0, 0.0, 1.0), + ]; + #[allow(unused)] + const TEX_COORDS: [TexCoord; 4] = + [uv(0.0, 0.0), uv(1.0, 0.0), uv(0.0, 1.0), uv(1.0, 1.0)]; + #[rustfmt::skip] + const VERTS: [(usize, [usize; 2]); 24] = [ + // left + (0b011, [0, 0]), (0b010, [0, 1]), (0b001, [0, 2]), (0b000, [0, 3]), + // right + (0b110, [1, 0]), (0b111, [1, 1]), (0b100, [1, 2]), (0b101, [1, 3]), + // bottom + (0b000, [2, 0]), (0b100, [2, 1]), (0b001, [2, 2]), (0b101, [2, 3]), + // top + (0b011, [3, 0]), (0b111, [3, 1]), (0b010, [3, 2]), (0b110, [3, 3]), + // front + (0b010, [4, 0]), (0b110, [4, 1]), (0b000, [4, 2]), (0b100, [4, 3]), + // back + (0b111, [5, 0]), (0b011, [5, 1]), (0b101, [5, 2]), (0b001, [5, 3]), + ]; + #[rustfmt::skip] + const FACES: [[usize; 3]; 12] = [ + // left + [0, 1, 3], [0, 3, 2], + // right + [4, 5, 7], [4, 7, 6], + // bottom + [8, 9, 11], [8, 11, 10], + // top + [12, 13, 15], [12, 15, 14], + // front + [16, 17, 19], [16, 19, 18], + // back + [20, 21, 23], [20, 23, 22], + ]; + + /// Returns a cube centered on the origin, with the given side length. + pub fn cube(side_len: f32) -> Self { + let l = 0.5 * side_len; + Self { + left_bot_near: pt3(-l, -l, -l), + right_top_far: pt3(l, l, l), + } + } + + /// Builds the cuboid mesh. + pub fn build(self) -> Mesh { + let mut b = Mesh::builder(); + b.push_faces(Self::FACES); + for (pos_i, [norm_i, _uv_i]) in Self::VERTS { + let pos = from_fn(|i| { + self.left_bot_near.0[i] + .lerp(&self.right_top_far.0[i], Self::COORDS[pos_i][i]) + }); + b.push_vert(pos.into(), Self::NORMS[norm_i]); + } + b.build() + } +} + +impl Octahedron { + const COORDS: [Point3; 6] = [ + pt3(-1.0, 0.0, 0.0), + pt3(0.0, -1.0, 0.0), + pt3(0.0, 0.0, -1.0), + pt3(0.0, 1.0, 0.0), + pt3(0.0, 0.0, 1.0), + pt3(1.0, 0.0, 0.0), + ]; + const NORMS: [Normal3; 8] = [ + vec3(-1.0, -1.0, -1.0), + vec3(-1.0, 1.0, -1.0), + vec3(-1.0, 1.0, 1.0), + vec3(-1.0, -1.0, 1.0), + vec3(1.0, -1.0, -1.0), + vec3(1.0, 1.0, -1.0), + vec3(1.0, 1.0, 1.0), + vec3(1.0, -1.0, 1.0), + ]; + #[rustfmt::skip] + const VERTS: [(usize, usize); 24] = [ + (0, 0), (2, 0), (1, 0), + (0, 1), (3, 1), (2, 1), + (0, 2), (4, 2), (3, 2), + (0, 3), (1, 3), (4, 3), + (1, 4), (2, 4), (5, 4), + (2, 5), (3, 5), (5, 5), + (3, 6), (4, 6), (5, 6), + (1, 7), (5, 7), (4, 7), + ]; + const FACES: [[usize; 3]; 8] = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [9, 10, 11], + [12, 13, 14], + [15, 16, 17], + [18, 19, 20], + [21, 22, 23], + ]; + + /// Builds the octahedral mesh. + pub fn build(self) -> Mesh { + let mut b = Mesh::builder(); + for (i, vs) in Self::FACES.iter().enumerate() { + b.push_face(3 * i, 3 * i + 1, 3 * i + 2); + for vi in *vs { + let pos = Self::COORDS[Self::VERTS[vi].0]; + b.push_vert(pos, Self::NORMS[i]); + } + } + b.build() + } +} + +/// The golden ratio constant φ. +const PHI: f32 = 1.618034_f32; +/// Reciprocal of φ. +const R_PHI: f32 = 1.0 / PHI; + +impl Dodecahedron { + #[rustfmt::skip] + const COORDS: [Vec3; 20] = [ + // -X + vec3(-PHI, -R_PHI, 0.0), + vec3(-PHI, R_PHI, 0.0), + // +X + vec3( PHI, -R_PHI, 0.0), + vec3( PHI, R_PHI, 0.0), + // -Y + vec3(0.0, -PHI, -R_PHI), + vec3(0.0, -PHI, R_PHI), + // +Y + vec3(0.0, PHI, -R_PHI), + vec3(0.0, PHI, R_PHI), + // -Z + vec3(-R_PHI, 0.0, -PHI), + vec3( R_PHI, 0.0, -PHI), + // +Z + vec3(-R_PHI, 0.0, PHI), + vec3( R_PHI, 0.0, PHI), + + // Corner verts, corresponding to the corner faces of the icosahedron. + vec3(-1.0, -1.0, -1.0), + vec3(-1.0, -1.0, 1.0), + vec3(-1.0, 1.0, -1.0), + vec3(-1.0, 1.0, 1.0), + vec3( 1.0, -1.0, -1.0), + vec3( 1.0, -1.0, 1.0), + vec3( 1.0, 1.0, -1.0), + vec3( 1.0, 1.0, 1.0), + + ]; + #[rustfmt::skip] + const FACES: [[usize; 5]; 12] = [ + [ 0, 1, 14, 8, 12], [ 1, 0, 13, 10, 15], + [ 3, 2, 16, 9, 18], [ 2, 3, 19, 11, 17], + [ 4, 5, 13, 0, 12], [ 5, 4, 16, 2, 17], + [ 7, 6, 14, 1, 15], [ 6, 7, 19, 3, 18], + [ 8, 9, 16, 4, 12], [ 9, 8, 14, 6, 18], + [11, 10, 13, 5, 17], [10, 11, 19, 7, 15], + ]; + + /// The normals are exactly the vertices of the icosahedron, normalized. + const NORMALS: [Vec3; 12] = Icosahedron::COORDS; + + /// Builds the dodecahedral mesh. + pub fn build(self) -> Mesh { + let mut b = Mesh::builder(); + + for (i, face) in Self::FACES.iter().enumerate() { + let n = Self::NORMALS[i].normalize(); + // Make a pentagon from three triangles + let i5 = 5 * i; + b.push_face(i5, i5 + 1, i5 + 2); + b.push_face(i5, i5 + 2, i5 + 3); + b.push_face(i5, i5 + 3, i5 + 4); + for &j in face { + b.push_vert(Self::COORDS[j].normalize().to_pt(), n); + } + } + b.build() + } +} + +impl Icosahedron { + #[rustfmt::skip] + 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 + + vec3(-1.0, -PHI, 0.0), vec3(1.0, -PHI, 0.0), // -Y + vec3(-1.0, PHI, 0.0), vec3(1.0, PHI, 0.0), // +Y + + vec3(0.0, -1.0, -PHI), vec3(0.0, 1.0, -PHI), // -Z + vec3(0.0, -1.0, PHI), vec3(0.0, 1.0, PHI), // +Z + ]; + #[rustfmt::skip] + 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 + [6, 7, 9], [6, 11, 7], // +Y + [8, 0, 9], [8, 9, 2], // -Z + [10, 11, 1], [10, 3, 11], // +Z + + // Corner faces, corresponding to the corner verts of the dodecahedron. + [0, 8, 4], [1, 4, 10], // -X-Y -Z,+Z + [0, 6, 9], [1, 11, 6], // -X+Y " + [2, 5, 8], [3, 10, 5], // +X-Y " + [2, 9, 7], [3, 7, 11], // +X+Y " + ]; + + /// The normals are exactly the vertices of the dodecahedron, normalized. + const NORMALS: [Vec3; 20] = Dodecahedron::COORDS; + + /// Builds the icosahedral mesh. + pub fn build(self) -> Mesh { + let mut b = Mesh::builder(); + for (i, vs) in Self::FACES.iter().enumerate() { + let n = Self::NORMALS[i].normalize(); + b.push_face(3 * i, 3 * i + 1, 3 * i + 2); + for vi in *vs { + b.push_vert(Self::COORDS[vi].normalize().to_pt(), n); + } + } + b.build() + } +} + +// +// Trait impls +// + +impl Default for Box { + /// Creates a cube with unit-length edges, centered at the origin. + fn default() -> Self { + Self::cube(1.0) + } +}