From c631ef8248e5aed54bda2881d024ff66974bea1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 5 Dec 2024 01:27:26 +0200 Subject: [PATCH 1/7] WIP Use points in screen space Requires solution to zdiv problem to make points Vary --- core/src/math/space.rs | 7 ++++--- core/src/render.rs | 8 ++++---- core/src/render/raster.rs | 37 +++++++++++++++++-------------------- 3 files changed, 25 insertions(+), 27 deletions(-) diff --git a/core/src/math/space.rs b/core/src/math/space.rs index 5475dda9..a098a356 100644 --- a/core/src/math/space.rs +++ b/core/src/math/space.rs @@ -158,7 +158,7 @@ impl Affine for u32 { impl Vary for V where - Self: Linear, + Self: Affine + Clone>, { type Iter = Iter; type Diff = ::Diff; @@ -178,8 +178,9 @@ where self.add(delta) } - fn z_div(&self, z: f32) -> Self { - self.mul(z.recip()) + fn z_div(&self, _z: f32) -> Self { + //todo!() + self.clone() } } diff --git a/core/src/render.rs b/core/src/render.rs index 7f57582a..dc896db9 100644 --- a/core/src/render.rs +++ b/core/src/render.rs @@ -18,7 +18,7 @@ use crate::math::{ use clip::{view_frustum, Clip, ClipVert}; use ctx::{Context, DepthSort, FaceCull}; -use raster::{tri_fill, ScreenVec}; +use raster::{tri_fill, ScreenPt}; use shader::{FragmentShader, VertexShader}; use stats::Stats; use target::Target; @@ -134,10 +134,10 @@ pub fn render( // of the original view-space depth. The interpolated reciprocal // is used in fragment processing for depth testing (larger values // are closer) and for perspective correction of the varyings. - let pos = vec3(x, y, 1.0).z_div(w); + let pos = vec3(x, y, 1.0) / w; // TODO Vertex { // Viewport transform - pos: to_screen.apply(&pos), + pos: to_screen.apply(&pos).to_pt(), // Perspective correction attrib: v.attrib.z_div(w), } @@ -175,7 +175,7 @@ fn depth_sort(tris: &mut [Tri>], d: DepthSort) { }); } -fn is_backface(vs: &[Vertex]) -> 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/raster.rs b/core/src/render/raster.rs index b68a2874..62571722 100644 --- a/core/src/render/raster.rs +++ b/core/src/render/raster.rs @@ -15,14 +15,15 @@ use core::fmt::Debug; use core::ops::Range; use crate::geom::Vertex; -use crate::math::{Vary, Vec3}; +use crate::math::point::Point3; +use crate::math::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), @@ -243,8 +244,8 @@ mod tests { use crate::assert_approx_eq; use crate::geom::vertex; + use crate::math::point::pt3; use crate::math::vary::Vary; - use crate::math::vec3; use crate::util::buf::Buf2; use super::{tri_fill, Frag, Scanline}; @@ -256,10 +257,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 +300,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 +338,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.0.z_div(w0)), + (pt3(16.0, 42.0, 1.0 / w1), 5.0.z_div(w1)), 8, ), }; @@ -359,7 +356,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; From bd52da5c53e884016632e843fa1ece6fe7b35dd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 5 Dec 2024 01:50:00 +0200 Subject: [PATCH 2/7] WIP point orthographic bounds --- core/src/math/mat.rs | 21 ++++++++++++--------- core/src/render/cam.rs | 3 ++- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/core/src/math/mat.rs b/core/src/math/mat.rs index ea9ce48b..1a86a8bc 100644 --- a/core/src/math/mat.rs +++ b/core/src/math/mat.rs @@ -386,8 +386,8 @@ impl Mat4x4> { /// \ · · M33 / \ 1 / /// ``` #[must_use] - pub fn apply(&self, v: &Point3) -> 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), @@ -624,13 +624,16 @@ pub fn perspective( /// * `lbn`: The left-bottom-near corner of the projection box. /// * `rtf`: The right-bottom-far corner of the projection box. // TODO Change to take points -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 = 0.5 * (rtf - lbn); + let center_pt = lbn + half_d; + + let [dx, dy, dz] = half_d.0; + let [cx, cy, cz] = center_pt.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], + [1.0 / dx, 0.0, 0.0, -cx / dx], + [0.0, 1.0 / dy, 0.0, -cy / dy], + [0.0, 0.0, 1.0 / dz, -cz / dz], [0.0, 0.0, 0.0, 1.0], ] .into() @@ -934,7 +937,7 @@ mod tests { let lbn = pt3(-20.0, 0.0, 0.01); let rtf = pt3(100.0, 50.0, 100.0); - let m = orthographic(lbn.to_vec(), rtf.to_vec()); + 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()), [1.0, 1.0, 1.0, 1.0].into()); diff --git a/core/src/render/cam.rs b/core/src/render/cam.rs index c23500ce..07476031 100644 --- a/core/src/render/cam.rs +++ b/core/src/render/cam.rs @@ -13,6 +13,7 @@ use crate::math::{ }; use crate::util::{rect::Rect, Dims}; +use crate::math::point::Point3; #[cfg(feature = "fp")] use crate::math::{ angle::Angle, @@ -115,7 +116,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 } From b03a64f1c65795006d5688edecac8204319b4778 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Thu, 5 Dec 2024 22:52:40 +0200 Subject: [PATCH 3/7] Add trait ZDiv for optional perspective correction --- core/src/math/angle.rs | 3 +++ core/src/math/color.rs | 23 +++++++++++++++-------- core/src/math/point.rs | 10 ++++++++++ core/src/math/space.rs | 9 ++------- core/src/math/vary.rs | 27 ++++++++++++++++++--------- core/src/math/vec.rs | 25 +++++++++++++++++++------ core/src/render.rs | 11 +++++++---- core/src/render/raster.rs | 6 +++--- 8 files changed, 77 insertions(+), 37 deletions(-) diff --git a/core/src/math/angle.rs b/core/src/math/angle.rs index b823a7bb..91c7f2f4 100644 --- a/core/src/math/angle.rs +++ b/core/src/math/angle.rs @@ -10,6 +10,7 @@ use crate::math::vec::Vector; #[cfg(feature = "fp")] use crate::math::float::f32; +use crate::math::vary::ZDiv; #[cfg(feature = "fp")] use crate::math::vec::{vec2, vec3, Vec2, Vec3}; @@ -463,6 +464,8 @@ impl Linear for Angle { } } +impl ZDiv for Angle {} + // // Foreign trait impls // diff --git a/core/src/math/color.rs b/core/src/math/color.rs index 3c8c5850..59af4840 100644 --- a/core/src/math/color.rs +++ b/core/src/math/color.rs @@ -1,13 +1,18 @@ //! Colors and color spaces. -use core::array; -use core::fmt::{self, Debug, Formatter}; -use core::marker::PhantomData; -use core::ops::Index; - -use crate::math::float::f32; -use crate::math::space::{Affine, Linear}; -use crate::math::vec::Vector; +use core::{ + array, + fmt::{self, Debug, Formatter}, + marker::PhantomData, + ops::Index, +}; + +use crate::math::{ + float::f32, + space::{Affine, Linear}, + vary::ZDiv, + vec::Vector, +}; // // Types @@ -531,6 +536,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/point.rs b/core/src/math/point.rs index e3fe3021..62b73f00 100644 --- a/core/src/math/point.rs +++ b/core/src/math/point.rs @@ -5,6 +5,7 @@ use core::{ ops::{Add, Index, Sub}, }; +use crate::math::vary::ZDiv; use crate::math::{ space::{Affine, Linear, Real}, vec::Vector, @@ -149,6 +150,15 @@ where } } +impl ZDiv for Point<[Sc; N], Sp> +where + Sc: ZDiv + Copy, +{ + fn z_div(self, z: f32) -> Self { + Self(self.0.map(|c| c.z_div(z)), Pd) + } +} + impl ApproxEq for Point<[Sc; N], Sp> { diff --git a/core/src/math/space.rs b/core/src/math/space.rs index a098a356..e6321c5d 100644 --- a/core/src/math/space.rs +++ b/core/src/math/space.rs @@ -5,7 +5,7 @@ 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. /// @@ -158,7 +158,7 @@ impl Affine for u32 { impl Vary for V where - Self: Affine + Clone>, + Self: Affine + Clone> + ZDiv, { type Iter = Iter; type Diff = ::Diff; @@ -177,11 +177,6 @@ where fn step(&self, delta: &Self::Diff) -> Self { self.add(delta) } - - fn z_div(&self, _z: f32) -> Self { - //todo!() - self.clone() - } } impl Debug for Real diff --git a/core/src/math/vary.rs b/core/src/math/vary.rs index 5474d4ce..4f37dab0 100644 --- a/core/src/math/vary.rs +++ b/core/src/math/vary.rs @@ -5,13 +5,20 @@ use core::mem; +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: ZDiv + Sized + Clone { /// The iterator returned by the [vary][Self::vary] method. type Iter: Iterator; /// The difference type of `Self`. @@ -50,10 +57,6 @@ pub trait Vary: Sized + Clone { #[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` @@ -94,9 +97,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 +116,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 30d6482a..ccd56b45 100644 --- a/core/src/math/vec.rs +++ b/core/src/math/vec.rs @@ -13,6 +13,7 @@ use crate::math::approx::ApproxEq; use crate::math::float::f32; use crate::math::point::Point; use crate::math::space::{Affine, Linear, Proj4, Real}; +use crate::math::vary::ZDiv; // // Types @@ -155,6 +156,9 @@ impl Vector<[f32; N], Sp> { /// // 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)); + // 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() @@ -214,13 +218,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() } } @@ -333,8 +339,7 @@ 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 @@ -356,7 +361,6 @@ where impl Linear for Vector<[Sc; DIM], Sp> where - Self: Affine, Sc: Linear + Copy, { type Scalar = Sc; @@ -368,11 +372,20 @@ where } #[inline] fn neg(&self) -> Self { - Self(array::from_fn(|i| self.0[i].neg()), Pd) + self.map(|c| c.neg()) } #[inline] fn mul(&self, scalar: Self::Scalar) -> Self { - Self(array::from_fn(|i| self.0[i].mul(scalar)), Pd) + 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)) } } diff --git a/core/src/render.rs b/core/src/render.rs index dc896db9..876fd4fe 100644 --- a/core/src/render.rs +++ b/core/src/render.rs @@ -94,15 +94,16 @@ 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() @@ -111,7 +112,6 @@ pub fn render( // 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(); @@ -120,6 +120,7 @@ pub fn render( let mut clipped = vec![]; tris.clip(&view_frustum::PLANES, &mut clipped); + // Optional depth sorting for use case such as transparency if let Some(d) = ctx.depth_sort { depth_sort(&mut clipped, d); } @@ -144,6 +145,8 @@ pub fn render( }); // 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, diff --git a/core/src/render/raster.rs b/core/src/render/raster.rs index 62571722..4070ec45 100644 --- a/core/src/render/raster.rs +++ b/core/src/render/raster.rs @@ -245,7 +245,7 @@ mod tests { use crate::assert_approx_eq; use crate::geom::vertex; use crate::math::point::pt3; - use crate::math::vary::Vary; + use crate::math::vary::{Vary, ZDiv}; use crate::util::buf::Buf2; use super::{tri_fill, Frag, Scanline}; @@ -338,8 +338,8 @@ mod tests { y: 42, xs: 8..16, vs: Vary::vary_to( - (pt3(8.0, 42.0, 1.0 / w0), 3.0.z_div(w0)), - (pt3(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, ), }; From adffd9e4018c8e59acf117175ff587762fed1f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sat, 7 Dec 2024 17:57:22 +0200 Subject: [PATCH 4/7] Improve assert message --- core/src/math/vec.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/math/vec.rs b/core/src/math/vec.rs index ccd56b45..ffda8b8d 100644 --- a/core/src/math/vec.rs +++ b/core/src/math/vec.rs @@ -569,7 +569,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()); } } From 9332ada60373e5977a1089c35f9b244de1ec83da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sat, 7 Dec 2024 18:34:13 +0200 Subject: [PATCH 5/7] WIP clipping --- core/src/render.rs | 7 +- core/src/render/clip.rs | 181 ++++++++++++++++++++++++---------------- 2 files changed, 114 insertions(+), 74 deletions(-) diff --git a/core/src/render.rs b/core/src/render.rs index 876fd4fe..475e1aee 100644 --- a/core/src/render.rs +++ b/core/src/render.rs @@ -16,7 +16,7 @@ use crate::math::{ Lerp, }; -use clip::{view_frustum, Clip, ClipVert}; +use clip::{view_frustum, ClipVert}; use ctx::{Context, DepthSort, FaceCull}; use raster::{tri_fill, ScreenPt}; use shader::{FragmentShader, VertexShader}; @@ -107,7 +107,8 @@ pub fn render( .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 @@ -118,7 +119,7 @@ pub fn render( // 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 { diff --git a/core/src/render/clip.rs b/core/src/render/clip.rs index 7367ad51..f5c05ec0 100644 --- a/core/src/render/clip.rs +++ b/core/src/render/clip.rs @@ -14,11 +14,12 @@ //! use alloc::{vec, vec::Vec}; +use core::iter::zip; use view_frustum::{outcode, status}; -use crate::geom::{Plane, Tri, Vertex}; -use crate::math::{vec::ProjVec4, Lerp, Vec3}; +use crate::geom::{vertex, Tri, Vertex}; +use crate::math::{vec::ProjVec4, Lerp}; /// Trait for types that can be [clipped][self] against planes. /// @@ -51,9 +52,6 @@ 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 { @@ -78,14 +76,17 @@ enum Status { Hidden, } +impl Outcode { + const INSIDE: u8 = 0b00_11_11_11; +} + +#[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])) + pub 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`. @@ -110,6 +111,22 @@ impl ClipPlane { self.0.dot(pt) } + /// Computes the outcode bit for `pt`. + /// + /// The result is nonzero if `pt` is inside this plane, zero 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 set, `false` otherwise. + #[inline] + pub fn is_inside(&self, v: &ClipVert) -> bool { + self.1 & v.outcode.0 != 0 + } + /// Clips the convex polygon given by `verts_in` against `self` and /// returns the resulting vertices in `verts_out`. /// @@ -132,16 +149,14 @@ impl ClipPlane { 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()); @@ -149,23 +164,29 @@ 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. + let d0 = self.signed_dist(&v0.pos); + let d1 = self.signed_dist(&v1.pos); // `t` is the fractional distance from `v0` to the intersection // 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; } } } @@ -184,30 +205,27 @@ 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 ]; + /// Clips geometry against the standard view frustum. + pub fn clip(geom: &G, out: &mut Vec) { + geom.clip(&PLANES, out); + } + /// 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); - + let code = PLANES.iter().map(|p| p.outcode(pt)).sum(); Outcode(code) } @@ -216,11 +234,11 @@ pub mod view_frustum { 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, + if any != Outcode::INSIDE { + // If there's at least one plane outside which all vertices are, // then the whole polygon is hidden Status::Hidden - } else if all == 0b111111 { + } else if all == Outcode::INSIDE { // If each vertex is inside all planes, the polygon is fully visible Status::Visible } else { @@ -245,13 +263,13 @@ 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: Lerp + Clone>( - planes: &[Plane], + 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 @@ -294,7 +312,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 @@ -351,25 +369,26 @@ 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); + let inside = Outcode::INSIDE; + assert_eq!(outcode(&vec(0.0, 0.0, 0.0)).0, inside); + assert_eq!(outcode(&vec(1.0, 0.0, 0.0)).0, inside); + assert_eq!(outcode(&vec(0.0, -1.0, 0.0)).0, inside); + assert_eq!(outcode(&vec(0.0, 1.0, 1.0)).0, inside); } #[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)).0, 0b11_11_10); + // Outside right == 8 + assert_eq!(outcode(&vec(2.0, 0.0, 0.0)).0, 0b11_01_11); + // Outside bottom == 16 + assert_eq!(outcode(&vec(0.0, -1.01, 0.0)).0, 0b10_11_11); + // Outside far left == 2|4 + assert_eq!(outcode(&vec(-2.0, 0.0, 2.0)).0, 0b11_10_01); } #[test] @@ -504,7 +523,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), @@ -515,7 +534,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 @@ -534,7 +553,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 @@ -560,7 +579,7 @@ mod tests { } #[test] - fn tri_clip_all_planes_result_is_heptagon() { + fn tri_clip_against_frustum_result_is_heptagon() { // z // ^ 2 // · / / @@ -583,9 +602,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)); @@ -601,18 +621,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!( @@ -621,9 +657,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) } From b26b7b80443cc2e297d73eaf054c19f2c7bb63db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sat, 7 Dec 2024 20:04:40 +0200 Subject: [PATCH 6/7] Change pnm header to use Dims --- core/src/util/buf.rs | 5 ++++ core/src/util/pnm.rs | 62 ++++++++++++++++++-------------------------- 2 files changed, 30 insertions(+), 37 deletions(-) diff --git a/core/src/util/buf.rs b/core/src/util/buf.rs index 0b20018c..47eb07d3 100644 --- a/core/src/util/buf.rs +++ b/core/src/util/buf.rs @@ -378,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 { diff --git a/core/src/util/pnm.rs b/core/src/util/pnm.rs index cab4efa2..a130b6b2 100644 --- a/core/src/util/pnm.rs +++ b/core/src/util/pnm.rs @@ -27,17 +27,17 @@ use Error::*; use Format::*; use crate::math::color::{rgb, Color3}; -use crate::util::buf::Buf2; #[cfg(feature = "std")] -use crate::util::buf::AsSlice2; +use super::buf::AsSlice2; + +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, @@ -140,24 +140,23 @@ impl Header { 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) } } @@ -183,7 +182,7 @@ pub fn read_pnm(src: impl IntoIterator) -> Result> { let mut it = src.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]; @@ -223,10 +222,10 @@ pub fn read_pnm(src: impl IntoIterator) -> Result> { _ => unimplemented!(), }; - 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,9 +258,8 @@ 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)?; @@ -336,8 +334,7 @@ mod tests { Header::parse(*b"P6 123\t \n\r321 255 "), Ok(Header { format: BinaryPixmap, - width: 123, - height: 321, + dims: (123, 321), max: 255, }) ); @@ -349,8 +346,7 @@ 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, }) ); @@ -362,8 +358,7 @@ mod tests { Header::parse(*b"P4 123 456 "), Ok(Header { format: BinaryBitmap, - width: 123, - height: 456, + dims: (123, 456), max: 1, }) ); @@ -375,8 +370,7 @@ mod tests { Header::parse(*b"P5 123 456 64 "), Ok(Header { format: BinaryGraymap, - width: 123, - height: 456, + dims: (123, 456), max: 64, }) ); @@ -412,9 +406,8 @@ mod tests { fn write_header_p1() { let mut out = Vec::new(); let hdr = Header { - format: Format::TextBitmap, - width: 16, - height: 32, + format: TextBitmap, + dims: (16, 32), max: 1, }; hdr.write(&mut out).unwrap(); @@ -426,9 +419,8 @@ mod tests { fn write_header_p6() { let mut out = Vec::new(); let hdr = Header { - format: Format::BinaryPixmap, - width: 64, - height: 16, + format: BinaryPixmap, + dims: (64, 16), max: 4, }; hdr.write(&mut out).unwrap(); @@ -441,8 +433,7 @@ mod tests { let buf = read_pnm(data).unwrap(); - assert_eq!(buf.width(), 2); - assert_eq!(buf.height(), 2); + assert_eq!(buf.dims(), (2, 2)); assert_eq!(buf[[0, 0]], rgb(0, 0, 0)); assert_eq!(buf[[1, 0]], rgb(123, 0, 42)); @@ -455,8 +446,7 @@ mod tests { // 0x69 == 0b0110_1001 let buf = read_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); @@ -469,8 +459,7 @@ mod tests { fn read_pnm_p5() { let buf = read_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)]); @@ -487,8 +476,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)]); From 934cbbdaf177220f6c74163f29e76bdfb8f703b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Dahlstr=C3=B6m?= Date: Sun, 8 Dec 2024 02:57:23 +0200 Subject: [PATCH 7/7] Remove lerp from Vary * Make Lerp supertrait of Vary * impl Lerp for () and (,) * Also add doc comments and doctest --- core/src/math.rs | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/core/src/math.rs b/core/src/math.rs index 557969ee..324e2f03 100644 --- a/core/src/math.rs +++ b/core/src/math.rs @@ -37,6 +37,7 @@ 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; } @@ -45,7 +46,51 @@ 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::{Lerp, vec::vec2, point::pt2}; + /// + /// 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); + /// + /// assert_eq!( + /// vec2::(-2.0, 1.0).lerp(&vec2(3.0, -1.0), 0.8), + /// vec2(2.0, -0.6) + /// ); + /// assert_eq!( + /// pt2::(-10.0, 5.0).lerp(&pt2(-5.0, 0.0), 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)) + } +}