diff --git a/Cargo.toml b/Cargo.toml index 24a552a5..60a96439 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -73,7 +73,6 @@ lto = "thin" opt-level = 1 split-debuginfo = "unpacked" - [[bench]] name = "fill" harness = false @@ -85,3 +84,7 @@ harness = false [[bench]] name = "isect" harness = false + +[[bench]] +name = "vec" +harness = false diff --git a/README.md b/README.md index 2e264210..23a96e73 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,7 @@ for custom allocators is planned in order to make `alloc` optional as well. * Type-tagged affine and linear transforms and projections * Perspective-correct texture mapping * Triangle mesh data structure and a library of shapes +* Point, directional, and spotlight support * Cubic Bézier, Hermite, Catmull–Rom, and B-splines * Simple random number generation and distributions * Simple text rendering with bitmap fonts @@ -68,8 +69,7 @@ for custom allocators is planned in order to make `alloc` optional as well. ## In progress * Different camera types -* Builtin light source support -* Spherical etc UV mapping +* Spherical etc. UV mapping * Procedural noise generation * Terminal frontend with ncurses * Cube mapping and skyboxes diff --git a/benches/vec.rs b/benches/vec.rs new file mode 100644 index 00000000..177eb04b --- /dev/null +++ b/benches/vec.rs @@ -0,0 +1,32 @@ +//! Triangle clipping benchmarks. + +use divan::Bencher; +use divan::counter::ItemsCount; +use retrofire_core::{ + math::rand::{DefaultRng, Distrib}, + math::{Vec3, splat}, +}; + +#[divan::bench] +fn normalize_exact(b: Bencher) { + let rng = &mut DefaultRng::default(); + let vecs = splat(-1e6)..splat(1e6); + + b.with_inputs(|| vecs.sample(rng)) + .input_counter(|_| ItemsCount::new(1u32)) + .bench_local_values(|v: Vec3| v.normalize()); +} + +#[divan::bench] +fn normalize_approx(b: Bencher) { + let rng = &mut DefaultRng::default(); + let vecs = splat(-1e6)..splat(1e6); + + b.with_inputs(|| vecs.sample(rng)) + .input_counter(|_| ItemsCount::new(1u32)) + .bench_local_values(|v: Vec3| v.normalize_approx()); +} + +fn main() { + divan::main() +} diff --git a/core/examples/hello_tri.rs b/core/examples/hello_tri.rs index 3e33b97e..563be9f5 100644 --- a/core/examples/hello_tri.rs +++ b/core/examples/hello_tri.rs @@ -15,7 +15,7 @@ fn main() { // Interpolate vertex colors in linear color space vertex(mvp.apply(&v.pos), v.attrib.to_linear()) }, - |frag: Frag>| frag.var.to_srgb().to_color4(), + |frag: Frag>, _| frag.var.to_srgb().to_color4(), ); #[cfg(not(feature = "fp"))] let shader = shader::new( @@ -24,7 +24,7 @@ fn main() { // Interpolate vertex colors in normal sRGB color space vertex(mvp.apply(&v.pos), v.attrib) }, - |frag: Frag>| frag.var.to_color4(), + |frag: Frag>, _| frag.var.to_color4(), ); let dims @ (w, h) = (640, 480); diff --git a/core/src/geom/mesh.rs b/core/src/geom/mesh.rs index f04a50cb..c19223bf 100644 --- a/core/src/geom/mesh.rs +++ b/core/src/geom/mesh.rs @@ -7,7 +7,7 @@ use core::{ }; use crate::{ - math::{Linear, Mat4, Point3}, + math::{Linear, Mat4, Point3, degs}, render::Model, }; @@ -35,6 +35,13 @@ pub struct Builder { pub mesh: Mesh, } +#[derive(Copy, Clone, Eq, PartialEq)] +pub enum Weight { + Equal, + Area, + Angle, +} + // // Inherent impls // @@ -205,27 +212,29 @@ impl Builder { pub fn with_vertex_normals(self) -> Builder { let Mesh { verts, faces } = self.mesh; - // Compute weighted face normals... - let face_normals = faces.iter().map(|tri| { - // TODO If n-gonal faces are supported some day, the cross - // product is not proportional to area anymore - let [a, b, c] = tri.map(|i| verts[i].pos).0; - (b - a).cross(&(c - a)).to() - }); - // ...initialize vertex normals to zero... + // Initialize vertex normals to zero... let mut verts: Vec<_> = verts - .iter() + .into_iter() .map(|v| vertex(v.pos, Normal3::zero())) .collect(); // ...accumulate normals... - for (&Tri(vs), n) in zip(&faces, face_normals) { - for i in vs { - verts[i].attrib += n; - } + for tri in &faces { + let [i, j, k] = tri.0; + let [ab, ac] = tri.map(|i| verts[i]).tangents(); + let bc = ac - ab; + let n = ab.cross(&ac).to().normalize_approx(); + let a_a = ab.angle(&ac); + let a_b = (-ab).angle(&bc); + let a_c = degs(180.0) - a_a - a_b; + + // Angle unit does not matter, the normals get normalized anyway + verts[i].attrib += n; //a_a.to_rads() * n; + verts[j].attrib += n; //a_b.to_rads() * n; + verts[k].attrib += n; //a_c.to_rads() * n; } // ...and normalize to unit length. for v in &mut verts { - v.attrib = v.attrib.normalize(); + v.attrib = v.attrib.normalize_or_zero(); } // No need to sanity check again diff --git a/core/src/math.rs b/core/src/math.rs index d90a9aae..469672fb 100644 --- a/core/src/math.rs +++ b/core/src/math.rs @@ -150,6 +150,7 @@ pub fn lerp(t: f32, from: T, to: T) -> T { /// ``` #[inline] pub fn inv_lerp(t: f32, min: f32, max: f32) -> f32 { + debug_assert!(!min.approx_eq(&max)); (t - min) / (max - min) } diff --git a/core/src/math/angle.rs b/core/src/math/angle.rs index e3aeaff0..a2259b0c 100644 --- a/core/src/math/angle.rs +++ b/core/src/math/angle.rs @@ -8,9 +8,9 @@ use core::{ ops::{AddAssign, DivAssign, MulAssign, SubAssign}, }; -use crate::math::{Affine, ApproxEq, Linear, Vector, vary::ZDiv}; +use super::{Affine, ApproxEq, Linear, Vector, vary::ZDiv}; #[cfg(feature = "fp")] -use crate::math::{Vec2, Vec3, float::f32, vec2, vec3}; +use super::{Vec2, Vec3, float::f32, vec2, vec3}; // // Types @@ -77,6 +77,7 @@ pub const fn turns(a: f32) -> Angle { /// # Panics /// If `x` is outside the range [-1.0, 1.0]. #[cfg(feature = "fp")] +#[inline] pub fn asin(x: f32) -> Angle { assert!(-1.0 <= x && x <= 1.0); Angle(f32::asin(x)) @@ -96,6 +97,7 @@ pub fn asin(x: f32) -> Angle { /// # Panics /// If `x` is outside the range [-1.0, 1.0]. #[cfg(feature = "fp")] +#[inline] pub fn acos(x: f32) -> Angle { Angle(f32::acos(x)) } @@ -113,11 +115,13 @@ pub fn acos(x: f32) -> Angle { /// assert_eq!(atan2(-3.0, 0.0), degs(-90.0)); /// ``` #[cfg(feature = "fp")] +#[inline] pub fn atan2(y: f32, x: f32) -> Angle { Angle(f32::atan2(y, x)) } /// Returns a polar coordinate vector with azimuth `az` and radius `r`. +#[inline] pub const fn polar(r: f32, az: Angle) -> PolarVec { Vector::new([r, az.to_rads()]) } @@ -126,6 +130,7 @@ pub const fn polar(r: f32, az: Angle) -> PolarVec { /// altitude `alt`, and radius `r`. /// /// An altitude of +90° corresponds to straight up and -90° to straight down. +#[inline] pub const fn spherical(r: f32, az: Angle, alt: Angle) -> SphericalVec { Vector::new([r, az.to_rads(), alt.to_rads()]) } diff --git a/core/src/math/float.rs b/core/src/math/float.rs index 58612fbf..7f11d18e 100644 --- a/core/src/math/float.rs +++ b/core/src/math/float.rs @@ -105,6 +105,8 @@ pub mod mm { } pub mod fallback { + use crate::math::float::fast_recip_sqrt; + /// Returns the largest integer less than or equal to `x`. #[inline] pub fn floor(x: f32) -> f32 { @@ -119,14 +121,7 @@ pub mod fallback { /// Returns the approximate reciprocal of the square root of `x`. #[inline] pub fn recip_sqrt(x: f32) -> f32 { - if x < 0.0 { - return f32::NAN; - } - // https://en.wikipedia.org/wiki/Fast_inverse_square_root - let y = f32::from_bits(0x5f37_5a86 - (x.to_bits() >> 1)); - // Two rounds of Newton's method - let y = y * (1.5 - 0.5 * x * y * y); - y * (1.5 - 0.5 * x * y * y) + fast_recip_sqrt(x) } #[inline] pub fn sqrt(x: f32) -> f32 { @@ -134,6 +129,18 @@ pub mod fallback { } } +/// Returns a fast approximation of the reciprocal square root of a number. +#[inline] +pub fn fast_recip_sqrt(x: f32) -> f32 { + // https://en.wikipedia.org/wiki/Fast_inverse_square_root + const MAGIC: u32 = 0x5f37_5a86; + let mut y = f32::from_bits(MAGIC.saturating_sub(x.to_bits() >> 1)); + // A round of Newton's method + y = y * (1.5 - 0.5 * x * y * y); + //y = y * (1.5 - 0.5 * x * y * y); + y +} + #[cfg(feature = "std")] #[allow(non_camel_case_types)] pub type f32 = core::primitive::f32; diff --git a/core/src/math/vec.rs b/core/src/math/vec.rs index b0067e21..b341f353 100644 --- a/core/src/math/vec.rs +++ b/core/src/math/vec.rs @@ -2,12 +2,6 @@ //! //! TODO -use super::{ - Affine, ApproxEq, Linear, Point, - space::{Proj3, Real}, - vary::ZDiv, -}; -use crate::math::space::Hom; use core::{ array, fmt::{Debug, Formatter}, @@ -17,6 +11,16 @@ use core::{ ops::{AddAssign, DivAssign, MulAssign, SubAssign}, }; +use super::{ + Affine, ApproxEq, Linear, Point, + float::fast_recip_sqrt, + space::{Hom, Proj3, Real}, + vary::ZDiv, +}; + +#[cfg(feature = "fp")] +use super::{Angle, acos}; + // // Types // @@ -161,6 +165,25 @@ impl Vector<[f32; N], Sp> { *self * f32::recip_sqrt(len_sqr) } + /// Returns `self` efficiently normalized to *approximately* unit length. + /// + /// This method is several times faster than `normalize`. Its absolute + /// error is less than 0.002 (1/500), that is, the length of a vector + /// returned by this method should be in the range (0.998, 1.002) for + /// inputs at least up to ±1e15 in magnitude. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{vec2, Vec2}; + /// + /// let normalized: Vec2 = vec2(3.0, 4.0).normalize_approx(); + /// assert_eq!(normalized.len(), 0.99844766); + /// ``` + #[inline] + pub fn normalize_approx(&self) -> Self { + *self * fast_recip_sqrt(self.len_sqr()) + } + /// Returns `self` normalized to unit length, or a zero vector if the /// length of `self` is approximately zero. /// @@ -182,13 +205,29 @@ impl Vector<[f32; N], Sp> { use super::float::RecipSqrt; use super::float::f32; let len_sqr = self.len_sqr(); - if len_sqr.approx_eq_eps(&0.0, &1e-12) { + if !len_sqr.is_finite() || len_sqr.approx_eq_eps(&0.0, &1e-12) { Vector::zero() } else { *self * f32::recip_sqrt(len_sqr) } } + /// Returns the angle between `self` and another vector. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{degs, vec3, Vec3}; + /// + /// let a: Vec3 = vec3(0.0, 1.0, 0.0); + /// let b: Vec3 = vec3(2.0, 0.0, 3.0); + /// assert_eq!(a.angle(&b), degs(90.0)); + /// ``` + #[cfg(feature = "fp")] + #[inline] + pub fn angle(&self, other: &Self) -> Angle { + acos(dot(&self.0, &other.0) / (self.len() * other.len())) + } + /// Returns `self` clamped component-wise to the given range. /// /// In other words, for each component `self[i]`, the result `r` has @@ -251,7 +290,7 @@ where self.dot(self) } - /// Returns the dot product of `self` and `other`. + /// Returns the dot product of `self` and another vector. /// /// TODO docs #[inline] @@ -324,7 +363,7 @@ where /// Returns whether `self` is parallel to another vector. /// /// Two vectors **a** and **b** are parallel if and only if either: - /// * at least one is a zero vector, or + /// * at least one of them is a zero vector, or /// * there exists a nonzero scalar *k* such that *k*·**a** = **b**. /// /// # Examples @@ -332,14 +371,14 @@ where /// use retrofire_core::math::vec2; /// let vec2 = vec2::; /// - /// // Zero vector is parallel with anything + /// // Zero vector is parallel to anything /// assert!(vec2(0.0, 0.0).is_parallel_to(&vec2(0.0, 0.0))); /// assert!(vec2(0.0, 0.0).is_parallel_to(&vec2(1.0, 2.0))); /// - /// // (1, 0) is parallel with any (k, 0) + /// // (1, 0) is parallel to any (k, 0) /// assert!(vec2(1.0, 0.0).is_parallel_to(&vec2(-3.0, 0.0))); /// - /// // (2, -1) is parallel with any (2·k, -1·k) + /// // (2, -1) is parallel to any (2·k, -1·k) /// assert!(vec2(2.0, -1.0).is_parallel_to(&vec2(-4.0, 2.0))); /// /// // Counterexamples @@ -938,6 +977,8 @@ mod tests { } mod f32 { + use core::iter::chain; + use super::*; #[test] @@ -965,6 +1006,30 @@ mod tests { ); } + #[test] + fn normalize_approx() { + let pos_vs = [ + 0.001, 0.01, 0.02, 0.5, 0.1, 0.2, 0.5, 1.0, 2.0, 5.0, 10.0, + 100.0, 1000.0, 10_000.0, 1e6, 1e8, 1e10, 1e12, 1e15, + ]; + let neg_vs = pos_vs.map(|x| -x); + let vs = chain(&neg_vs, &pos_vs); + + for &x in vs.clone() { + for &y in vs.clone() { + for &z in vs.clone() { + let v = vec3(x, y, z); + let len = v.normalize_approx().len(); + let diff = (len - 1.0).abs(); + assert!( + diff < 0.002, + "v={v:?}, len={len}, diff={diff}" + ); + } + } + } + } + #[test] fn vector_addition() { assert_eq!(vec2(1.0, 2.0) + vec2(-2.0, 1.0), vec2(-1.0, 3.0)); diff --git a/core/src/render.rs b/core/src/render.rs index f6d8c6f0..97dec40a 100644 --- a/core/src/render.rs +++ b/core/src/render.rs @@ -26,6 +26,7 @@ pub(super) mod re_exports { cam::Camera, clip::Clip, ctx::Context, + light::Light, raster::Frag, shader::{FragmentShader, VertexShader}, stats::Stats, @@ -41,6 +42,7 @@ pub mod cam; pub mod clip; pub mod ctx; pub mod debug; +pub mod light; pub mod prim; pub mod raster; pub mod scene; @@ -121,12 +123,13 @@ pub type NdcToScreen = RealToReal<3, Ndc, Screen>; /// Alias for combined vertex+fragment shader types pub trait Shader: - VertexShader> + FragmentShader + VertexShader> + + FragmentShader { } impl Shader for S where S: VertexShader> - + FragmentShader + + FragmentShader { } @@ -200,7 +203,7 @@ pub fn render( // Convert to fragments, shade, and draw to target stats.frags += target .deref_mut() - .rasterize(scanline, shader, ctx); + .rasterize(scanline, uniform, shader, ctx); }); } *ctx.stats.borrow_mut() += stats.finish(); diff --git a/core/src/render/cam.rs b/core/src/render/cam.rs index c3fc9aa7..d74e9d43 100644 --- a/core/src/render/cam.rs +++ b/core/src/render/cam.rs @@ -210,11 +210,15 @@ impl Camera { } } +// TODO Should probably pass view and projection matrices separately +pub type CameraUni<'a, B, Uni> = (&'a ProjMat3, Uni); + impl Camera { - /// Returns the camera matrix. + /// Returns the camera (view, eye) matrix. pub fn world_to_view(&self) -> Mat4 { self.transform.world_to_view() } + /// Returns the inverse camera matrix. pub fn view_to_world(&self) -> Mat4 { self.world_to_view().inverse() @@ -238,15 +242,14 @@ impl Camera { ) where Prim: Render + Clone, [::Clip]: Clip, - Shd: for<'a> Shader, Uni)>, + Shd: for<'a> Shader>, { - let tf = to_world.then(&self.world_to_project()); - + let to_proj = to_world.then(&self.world_to_project()); super::render( prims.as_ref(), verts.as_ref(), shader, - (&tf, uniform), + (&to_proj, uniform), self.viewport, target, ctx, diff --git a/core/src/render/debug.rs b/core/src/render/debug.rs index b1b6646a..75cf74b5 100644 --- a/core/src/render/debug.rs +++ b/core/src/render/debug.rs @@ -30,8 +30,12 @@ impl<'a, B> VertexShader, &'a ProjMat3> for Shader { } } -impl FragmentShader for Shader { - fn shade_fragment(&self, f: Frag) -> Option { +impl<'a, B> FragmentShader> for Shader { + fn shade_fragment( + &self, + f: Frag, + _: &'a ProjMat3, + ) -> Option { Some(f.var.to_color4()) } } diff --git a/core/src/render/light.rs b/core/src/render/light.rs new file mode 100644 index 00000000..1f61cda7 --- /dev/null +++ b/core/src/render/light.rs @@ -0,0 +1,132 @@ +//! Light sources + +use core::fmt::{self, Debug, Formatter}; + +use crate::math::{Color3f, Mat4, Point3, Vec3, color::gray, inv_lerp}; + +/// A light source. +#[derive(Copy, Clone, PartialEq)] +pub struct Light { + pub color: Color3f, + pub kind: Kind, + pub falloff: u8, +} + +#[derive(Copy, Clone, PartialEq)] +pub enum Kind { + /// A light source "at infinity", so that the light rays arrive + /// approximately parallel and the direction of the light source + /// is the same for every point. For example the sun or the moon. + Directional(Vec3), + /// A light source radiating omnidirectionally from a single point. + Point(Point3), + /// A light source radiating from a point in a cone shape. + Spot { + pos: Point3, + dir: Vec3, + radii: (f32, f32), + }, +} + +impl Light { + /// Creates a new light source of the given color and kind. + pub fn new(color: Color3f, mut kind: Kind) -> Self { + match &mut kind { + Kind::Directional(dir) => *dir = dir.normalize(), + Kind::Spot { dir, .. } => *dir = dir.normalize(), + _ => {} + }; + Self { color, kind, ..Self::default() } + } + + /// Returns the normalized direction vector from a point to `self`. + #[inline] + pub fn direction(&self, pt: Point3) -> Vec3 { + match self.kind { + Kind::Point(pos) => (pos - pt).normalize_approx(), + Kind::Directional(dir) => dir, + Kind::Spot { pos, .. } => (pos - pt).normalize_approx(), + } + } + + #[inline] + pub fn eval(&self, pt: Point3) -> (Color3f, Vec3) { + let pt_dir = self.direction(pt); + let color = match self.kind { + Kind::Point(_) => self.color, + Kind::Directional(_) => self.color, + Kind::Spot { dir, radii, .. } => { + let dot = pt_dir.dot(&dir); + let (r0, r1) = (1.0 - radii.0, 1.0 - radii.1); + if dot > r0 { + self.color + } else if dot > r1 { + let t = inv_lerp(dot, r1, r0); // ok: r0 != r1 + self.color * t + } else { + gray(0.0) + } + } + }; + (color, pt_dir) + } + + pub fn transform(&self, mat: &Mat4) -> Light { + let Self { color, kind, falloff } = *self; + let kind = match kind { + Kind::Point(pos) => Kind::Point(mat.apply(&pos)), + Kind::Directional(dir) => Kind::Directional(mat.apply(&dir)), + Kind::Spot { pos, dir, radii } => Kind::Spot { + pos: mat.apply(&pos), + dir: mat.apply(&dir), + radii, + }, + }; + Light { kind, color, falloff } + } +} + +// Ugh, manual impls to avoid B: Default bound on the types... + +impl Debug for Light { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("Light") + .field("kind", &self.kind) + .field("color", &self.color) + .field("falloff", &self.falloff) + .finish() + } +} + +impl Default for Light { + fn default() -> Self { + Self { + color: gray(1.0), + kind: Kind::default(), + falloff: 0, + } + } +} + +impl Debug for Kind { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Kind::Directional(dir) => { + f.debug_tuple("Directional").field(&dir).finish() + } + Kind::Point(pt) => f.debug_tuple("Point").field(&pt).finish(), + Kind::Spot { pos, dir, radii } => f + .debug_struct("Spot") + .field("pos", &pos) + .field("dir", &dir) + .field("radii", radii) + .finish(), + } + } +} + +impl Default for Kind { + fn default() -> Self { + Self::Directional(Vec3::Y) + } +} diff --git a/core/src/render/shader.rs b/core/src/render/shader.rs index e67e3f99..425e7302 100644 --- a/core/src/render/shader.rs +++ b/core/src/render/shader.rs @@ -14,13 +14,13 @@ //! input any vertex attributes interpolated across the primitive being //! rasterized, such as color, texture coordinate, or normal vector. +use super::Frag; +use crate::math::Vec3; use crate::{ geom::Vertex, math::{Color4, ProjVec3}, }; -use super::Frag; - /// Trait for vertex shaders, used to transform vertices and perform other /// per-vertex computations. /// @@ -45,58 +45,86 @@ pub trait VertexShader { /// /// # Type parameters /// * `Var`: The varying of the input fragment. -pub trait FragmentShader { +pub trait FragmentShader { /// Computes the color of `frag`. Returns either `Some(color)`, or `None` /// if the fragment should be discarded. /// /// # Panics /// `shade_fragment` should never panic. - fn shade_fragment(&self, frag: Frag) -> Option; + fn shade_fragment(&self, frag: Frag, uniform: Uni) -> Option; +} + +/// A type that composes a vertex and a fragment shader. +#[derive(Copy, Clone)] +pub struct Shader { + pub vertex_shader: Vs, + pub fragment_shader: Fs, +} + +pub fn new(vs: Vs, fs: Fs) -> Shader +where + Vs: VertexShader>, + Fs: FragmentShader, +{ + Shader::new(vs, fs) +} + +#[inline] +fn phong( + normal: Vec3, + view_dir: Vec3, + light_dir: Vec3, + shininess: i32, +) -> f32 { + let refl_dir = light_dir.reflect(normal); + view_dir.dot(&refl_dir).max(0.0).powi(shininess) +} + +#[inline] +fn blinn_phong( + normal: Vec3, + view_dir: Vec3, + light_dir: Vec3, + shininess: i32, +) -> f32 { + let halfway = (view_dir + light_dir).normalize_approx(); + normal.dot(&halfway).max(0.0).powi(4 * shininess) } +// +// Trait impls +// + impl VertexShader for F where F: Fn(In, Uni) -> Out, { type Output = Out; + #[inline] fn shade_vertex(&self, vertex: In, uniform: Uni) -> Out { self(vertex, uniform) } } -impl FragmentShader for F +impl FragmentShader for F where - F: Fn(Frag) -> Out, + F: Fn(Frag, Uni) -> Out, Out: Into>, { - fn shade_fragment(&self, frag: Frag) -> Option { - self(frag).into() + #[inline] + fn shade_fragment(&self, frag: Frag, uniform: Uni) -> Option { + self(frag, uniform).into() } } -pub fn new(vs: Vs, fs: Fs) -> Shader -where - Vs: VertexShader>, - Fs: FragmentShader, -{ - Shader::new(vs, fs) -} - -/// A type that composes a vertex and a fragment shader. -#[derive(Copy, Clone)] -pub struct Shader { - pub vertex_shader: Vs, - pub fragment_shader: Fs, -} - impl Shader { /// Returns a new `Shader` with `vs` as the vertex shader /// and `fs` as the fragment shader. pub const fn new(vs: Vs, fs: Fs) -> Self where Vs: VertexShader>, - Fs: FragmentShader, + Fs: FragmentShader, { Self { vertex_shader: vs, @@ -111,16 +139,18 @@ where { type Output = Vs::Output; + #[inline] fn shade_vertex(&self, vertex: In, uniform: Uni) -> Self::Output { self.vertex_shader.shade_vertex(vertex, uniform) } } -impl FragmentShader for Shader +impl FragmentShader for Shader where - Fs: FragmentShader, + Fs: FragmentShader, { - fn shade_fragment(&self, frag: Frag) -> Option { - self.fragment_shader.shade_fragment(frag) + #[inline] + fn shade_fragment(&self, frag: Frag, uni: Uni) -> Option { + self.fragment_shader.shade_fragment(frag, uni) } } diff --git a/core/src/render/target.rs b/core/src/render/target.rs index ae7d8bdf..6fc81fdd 100644 --- a/core/src/render/target.rs +++ b/core/src/render/target.rs @@ -19,15 +19,17 @@ pub trait Target { /// Writes a single scanline into `self`. /// /// Returns count of fragments input and output. - fn rasterize( + fn rasterize( &mut self, scanline: Scanline, + uniform: U, frag_shader: &Fs, ctx: &Context, ) -> Throughput where V: Vary, - Fs: FragmentShader; + U: Copy, + Fs: FragmentShader; } /// Framebuffer, combining a color (pixel) buffer and a depth buffer. @@ -56,23 +58,25 @@ impl, F> AsMutSlice2 for Colorbuf { } impl Target for &mut T { - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, + uni: U, fs: &Fs, ctx: &Context, ) -> Throughput { - (*self).rasterize(sl, fs, ctx) + (*self).rasterize(sl, uni, fs, ctx) } } impl Target for &RefCell { - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, + uni: U, fs: &Fs, ctx: &Context, ) -> Throughput { - RefCell::borrow_mut(self).rasterize(sl, fs, ctx) + RefCell::borrow_mut(self).rasterize(sl, uni, fs, ctx) } } @@ -83,14 +87,15 @@ where Color4: IntoPixel, { /// Rasterizes `scanline` into this framebuffer. - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, + uni: U, fs: &Fs, ctx: &Context, ) -> Throughput { let Self { color_buf, depth_buf } = self; - rasterize_fb(color_buf, depth_buf, sl, fs, Color4::into_pixel, ctx) + rasterize_fb(color_buf, depth_buf, sl, uni, fs, Color4::into_pixel, ctx) } } @@ -101,42 +106,46 @@ where { /// Rasterizes `scanline` into this `u32` color buffer. /// Does no z-buffering. - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, + uni: U, fs: &Fs, ctx: &Context, ) -> Throughput { - rasterize(&mut self.buf, sl, fs, Color4::into_pixel, ctx) + rasterize(&mut self.buf, sl, uni, fs, Color4::into_pixel, ctx) } } impl Target for Buf2 { - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, + uni: U, fs: &Fs, ctx: &Context, ) -> Throughput { - rasterize(self, sl, fs, |c| c, ctx) + rasterize(self, sl, uni, fs, |c| c, ctx) } } impl Target for Buf2 { - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, + uni: U, fs: &Fs, ctx: &Context, ) -> Throughput { - rasterize(self, sl, fs, |c| c.to_rgb(), ctx) + rasterize(self, sl, uni, fs, |c| c.to_rgb(), ctx) } } -pub fn rasterize( +pub fn rasterize( buf: &mut impl AsMutSlice2, mut sl: Scanline, - fs: &impl FragmentShader, + uni: U, + fs: &impl FragmentShader, mut conv: impl FnMut(Color4) -> T, ctx: &Context, ) -> Throughput { @@ -148,7 +157,7 @@ pub fn rasterize( sl.fragments() .zip(cbuf_span) .for_each(|(frag, curr_col)| { - if let Some(new_col) = fs.shade_fragment(frag) + if let Some(new_col) = fs.shade_fragment(frag, uni) && ctx.color_write { io.o += 1; @@ -158,11 +167,12 @@ pub fn rasterize( io } -pub fn rasterize_fb( +pub fn rasterize_fb( cbuf: &mut impl AsMutSlice2, zbuf: &mut impl AsMutSlice2, mut sl: Scanline, - fs: &impl FragmentShader, + uni: U, + fs: &impl FragmentShader, mut conv: impl FnMut(Color4) -> T, ctx: &Context, ) -> Throughput { @@ -180,7 +190,7 @@ pub fn rasterize_fb( let new_z = frag.pos.z(); if ctx.depth_test(new_z, *curr_z) - && let Some(new_col) = fs.shade_fragment(frag) + && let Some(new_col) = fs.shade_fragment(frag, uni) { if ctx.color_write { io.o += 1; diff --git a/core/tests/rendering.rs b/core/tests/rendering.rs index 314537d7..17800f17 100644 --- a/core/tests/rendering.rs +++ b/core/tests/rendering.rs @@ -27,7 +27,7 @@ fn textured_quad() { |v: Vertex3<_>, mvp: &ProjMat3| { vertex(mvp.apply(&v.pos), v.attrib) }, - |frag: Frag<_>| SamplerClamp.sample(&checker, frag.var), + |frag: Frag<_>, _| SamplerClamp.sample(&checker, frag.var), ); let (w, h) = (256, 256); diff --git a/demos/src/bin/crates.rs b/demos/src/bin/crates.rs index 555659b3..d01f8628 100644 --- a/demos/src/bin/crates.rs +++ b/demos/src/bin/crates.rs @@ -30,7 +30,7 @@ fn main() { let floor_shader = shader::new( |v: Vertex3<_>, mvp: &ProjMat3<_>| vertex(mvp.apply(&v.pos), v.attrib), - |frag: Frag| { + |frag: Frag, _: &_| { let even_odd = (frag.var.x() > 0.5) ^ (frag.var.y() > 0.5); gray(if even_odd { 0.8 } else { 0.1 }).to_color4() }, @@ -39,7 +39,7 @@ fn main() { |v: Vertex3<(Normal3, TexCoord)>, mvp: &ProjMat3<_>| { vertex(mvp.apply(&v.pos), v.attrib) }, - |frag: Frag<(Normal3, TexCoord)>| { + |frag: Frag<(Normal3, TexCoord)>, _: &_| { let (n, uv) = frag.var; let kd = lerp(n.dot(&light_dir).max(0.0), 0.4, 1.0); let col = SamplerClamp.sample(&tex, uv); diff --git a/demos/src/bin/curses.rs b/demos/src/bin/curses.rs index 05112783..4f02b9a0 100644 --- a/demos/src/bin/curses.rs +++ b/demos/src/bin/curses.rs @@ -50,8 +50,8 @@ fn main() { |v: Vertex3<_>, mvp: &ProjMat3| { vertex(mvp.apply(&v.pos), v.attrib) }, - |frag: Frag| { - let [x, y, z] = (frag.var / 2.0 + splat(0.5)).0; + |frag: Frag, _: &_| { + let [x, y, z] = (frag.var * 0.5 + splat(0.5)).0; rgb(x, y, z).to_color4() }, ); @@ -104,23 +104,20 @@ fn main() { } impl Target for Win { - fn rasterize( + fn rasterize>( &mut self, mut sc: Scanline, + uni: U, fs: &Fs, _ctx: &Context, - ) -> Throughput - where - V: Vary, - Fs: FragmentShader, - { + ) -> Throughput { let w = sc.xs.len(); let y = sc.y; self.0.mv(y as i32, sc.xs.start as i32); for frag in sc.fragments() { - let Some(col) = fs.shade_fragment(frag) else { + let Some(col) = fs.shade_fragment(frag, uni) else { continue; }; let [r, g, b, _] = col.0.map(|c| c as u32); diff --git a/demos/src/bin/hello.rs b/demos/src/bin/hello.rs index 55c8227b..a91f71f3 100644 --- a/demos/src/bin/hello.rs +++ b/demos/src/bin/hello.rs @@ -34,7 +34,7 @@ fn main() { |v: Vertex<_, _>, mvp: &ProjMat3| { vertex(mvp.apply(&v.pos), v.attrib) }, - |frag: Frag| text.sample(frag.var).to_rgba(), + |frag: Frag, _: &_| text.sample(frag.var).to_rgba(), ); let vp: ProjMat3 = translate(vec3(0.0, 0.0, 15.0)) diff --git a/demos/src/bin/solids.rs b/demos/src/bin/solids.rs index 59d63504..e9bf94f1 100644 --- a/demos/src/bin/solids.rs +++ b/demos/src/bin/solids.rs @@ -1,15 +1,16 @@ use core::ops::ControlFlow::Continue; +use std::env; use minifb::{Key, KeyRepeat}; use re::prelude::*; -use re::core::{ - geom::Polyline, - math::{ProjMat3, ProjVec3, color::gray}, - render::cam::Fov, - render::{Model, ModelToWorld, shader}, +use re::core::geom::Polyline; +use re::core::math::{ProjMat3, ProjVec3, color::gray}; +use re::core::render::{ + Model, View, World, cam::Fov, debug::dir_to_rgb, light::Kind, shader, }; + use re::front::{Frame, minifb::Window}; use re::geom::{io::read_obj, solids::*}; @@ -21,30 +22,43 @@ struct Carousel { t: Option, } -impl Carousel { - fn start(&mut self) { - if self.t.is_none() { - self.t = Some(0.0); - self.new_idx = self.idx + 1; - } else { - // If already started, skip to next - self.new_idx += 1; - } - } - fn update(&mut self, dt: f32) -> Mat4 { - let Some(t) = self.t.as_mut() else { - return Mat4::identity(); - }; - *t += dt; - let t = *t; - if t >= 0.5 { - self.idx = self.new_idx; - } - if t >= 1.0 { - self.t = None - } - rotate_y(turns(smootherstep(t))) - } +type Varyings = (Point3, (Color3f, Normal3)); +type VertexIn = Vertex3; +type VertexOut = Vertex; +struct Uniform { + pub mv: Mat4, + pub proj: ProjMat3, + pub norm: Mat4, + pub light: Light, +} + +#[inline] +fn vtx_shader(v: VertexIn, u: &Uniform) -> VertexOut { + let view_normal = u.norm.apply(&v.attrib); + let color = dir_to_rgb(v.attrib).to_rgb(); + let view_pos = u.mv.apply(&v.pos); + let clip_pos = u.proj.apply(&view_pos); + + // (camera_dir, (light_col * color, (view_normal, (light_col, light_dir))),), + vertex(clip_pos, (view_pos, (color, view_normal))) +} + +#[inline] +fn frag_shader(f: Frag, u: &Uniform) -> Color4 { + //let (camera_dir, (light_x_col, (view_normal, (light_col, light_dir)))) = + let (view_pos, (color, view_normal)) = f.var; + + let (light_col, light_dir) = u.light.eval(view_pos); + let camera_dir = -view_pos.to_vec().normalize_approx(); + let view_normal = view_normal.normalize_approx(); + + let ambient = rgb(0.15, 0.18, 0.25); + let diffuse = 0.5 * light_dir.dot(&view_normal.to()); + let specular = + 0.5 * blinn_phong(view_normal.to(), camera_dir, light_dir, 30); + + //(light_x_col * diffuse + light_col * specular + ambient).to_color4() + (light_col * color * diffuse + light_col * specular + ambient).to_color4() } fn main() { @@ -59,36 +73,26 @@ fn main() { let (w, h) = win.dims; let cam = Camera::new(win.dims) - .transform(scale3(1.0, -1.0, -1.0).to()) + .transform(Mat4::identity()) .perspective(Fov::Equiv35mm(28.0), 0.1..1000.0) .viewport(pt2(10, h - 10)..pt2(w - 10, 10)); - type VertexIn = Vertex3; - type VertexOut = Vertex; - type Uniform<'a> = (&'a ProjMat3, &'a Mat4); - - fn vtx_shader(v: VertexIn, (mvp, spin): Uniform) -> VertexOut { - // Transform vertex normal - let norm = spin.apply(&v.attrib); - // Calculate diffuse shading - let diffuse = (norm.z() + 0.2).max(0.2) * 0.8; - // Visualize normal by mapping to RGB values - let [r, g, b] = (0.45 * (v.attrib + splat(1.1))).0; - let col = diffuse * rgb(r, g, b); - vertex(mvp.apply(&v.pos), col) - } - - fn frag_shader(f: Frag) -> Color4 { - f.var.to_color4() - } - let shader = shader::new(vtx_shader, frag_shader); - let objects = objects_n(8); + let res = env::args() + .nth(1) + .and_then(|arg| arg.parse().ok()) + .unwrap_or(24); + let objects = objects_n(res); - let translate = translate(-3.0 * Vec3::Z); + let translate = translate(3.0 * Vec3::Z); let mut carousel = Carousel::default(); + let light = Light::::new( + rgb(1.0, 0.95, 0.9), + Kind::Point(pt3(-1.0, 3.0, 1.0)), + ); + win.run(|frame| { let Frame { t, dt, win, .. } = frame; @@ -98,23 +102,26 @@ fn main() { } let theta = rads(t.as_secs_f32()); - let spin = rotate_x(theta * 0.37).then(&rotate_y(theta * 0.51)); + let spin = rotate_x(theta * 0.51).then(&rotate_y(theta * 0.37)); let carouse = carousel.update(dt.as_secs_f32()); // Compose transform stack - let model_view_project: ProjMat3 = spin - .then(&translate) - .then(&carouse) - .to::() - .then(&cam.world_to_project()); + let modelview: Mat4 = + spin.then(&translate).then(&carouse).to(); let object = &objects[carousel.idx % objects.len()]; + let uniform = &Uniform { + mv: modelview, + proj: cam.project, + norm: spin, + light: light.transform(&cam.world_to_view()), + }; Batch { prims: object.faces.clone(), verts: object.verts.clone(), - uniform: (&model_view_project, &spin), - shader: shader, + uniform, + shader, viewport: cam.viewport, target: frame.buf, ctx: &*frame.ctx, @@ -125,17 +132,43 @@ fn main() { }); } +impl Carousel { + fn start(&mut self) { + if self.t.is_none() { + self.t = Some(0.0); + self.new_idx = self.idx + 1; + } else { + // If already started, skip to next + self.new_idx += 1; + } + } + fn update(&mut self, dt: f32) -> Mat4 { + let Some(t) = self.t.as_mut() else { + return Mat4::identity(); + }; + *t += dt; + let t = *t; + if t >= 0.5 { + self.idx = self.new_idx; + } + if t >= 1.0 { + self.t = None + } + rotate_y(turns(smootherstep(t))) + } +} + // Creates the 14 objects exhibited. #[rustfmt::skip] fn objects_n(res: u32) -> [Mesh; 14] { - let segments = res; - let sectors = 2 * res; + let segments = res + 2; + let sectors = res + 3; - let cap_segments = res; - let body_segments = res; + let cap_segments = res + 1; + let body_segments = res + 1; - let major_sectors = 3 * res; - let minor_sectors = 2 * res; + let major_sectors = res + 3; + let minor_sectors = res.div_ceil(2) + 3; [ // The five Platonic solids Tetrahedron.build(), @@ -148,9 +181,9 @@ fn objects_n(res: u32) -> [Mesh; 14] { lathe(sectors), 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(), + Cone { base_radius: 1.0, apex_radius: 0.2, 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(), + Torus { major_radius: 0.9, minor_radius: 0.25, major_sectors, minor_sectors }.build(), // Traditional demo models teapot(), diff --git a/demos/src/bin/sprites.rs b/demos/src/bin/sprites.rs index 2892bcc3..1b3836e4 100644 --- a/demos/src/bin/sprites.rs +++ b/demos/src/bin/sprites.rs @@ -38,12 +38,12 @@ fn main() { let shader = shader::new( |v: Vertex3>, - (mv, proj): (&Mat4, &ProjMat3)| { + (mv, proj): &(Mat4, ProjMat3)| { let vertex_pos = 0.008 * v.attrib.to_vec3().to(); // Model->View let view_pos = mv.apply(&v.pos) + vertex_pos; vertex(proj.apply(&view_pos), v.attrib) }, - |frag: Frag>| { + |frag: Frag>, _: & _| { let d2 = frag.var.len_sqr(); (d2 < 1.0).then(|| { let col = gray(1.0) - d2 * rgb(0.25, 0.5, 1.0); @@ -66,11 +66,12 @@ fn main() { .to() .then(&cam.world_to_view()); + let uniform = (modelview, cam.project); render( &tris, &verts, &shader, - (&modelview, &cam.project), + &uniform, cam.viewport, &mut frame.buf, frame.ctx, diff --git a/demos/src/bin/square.rs b/demos/src/bin/square.rs index aa2b66cf..65d9d028 100644 --- a/demos/src/bin/square.rs +++ b/demos/src/bin/square.rs @@ -36,7 +36,7 @@ fn main() { let shader = shader::new( |v: Vertex3<_>, mvp: &ProjMat3<_>| 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; diff --git a/demos/wasm/src/triangle.rs b/demos/wasm/src/triangle.rs index 1b094263..9a2c5384 100644 --- a/demos/wasm/src/triangle.rs +++ b/demos/wasm/src/triangle.rs @@ -38,7 +38,7 @@ pub fn start() { let sh = Shader::new( |v: Vertex3, _| vertex(mvp.apply(&v.pos), v.attrib), - |f: Frag| f.var.to_color4(), + |f: Frag, _| f.var.to_color4(), ); render([tri(0, 1, 2)], vs, &sh, (), vp, &mut frame.buf, frame.ctx); diff --git a/front/src/sdl2.rs b/front/src/sdl2.rs index d340c3bc..3b0aec95 100644 --- a/front/src/sdl2.rs +++ b/front/src/sdl2.rs @@ -272,9 +272,10 @@ where PF: PixelFmt, Color4: IntoPixel, { - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, + uni: U, fs: &Fs, ctx: &Context, ) -> Throughput { @@ -282,6 +283,7 @@ where &mut self.color_buf, &mut self.depth_buf, sl, + uni, fs, |c| c.into_pixel(), ctx,