diff --git a/Cargo.toml b/Cargo.toml index 1627252d..295e4361 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,6 +52,7 @@ clippy.collapsible_if = "allow" [features] default = ["std"] std = ["retrofire-core/std", "retrofire-geom/std"] +stats = ["retrofire-core/stats"] [dependencies] retrofire-core = { version = "0.4.0", path = "core" } @@ -104,3 +105,11 @@ harness = false [[bench]] name = "vec" harness = false + +[[bench]] +name = "noise" +harness = false + +[[bench]] +name = "mat" +harness = false diff --git a/README.md b/README.md index 2e264210..cbb6fac0 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 @@ -62,15 +63,14 @@ for custom allocators is planned in order to make `alloc` optional as well. * Reading and writing pnm image files * Reading and writing Wavefront .obj files * Minifb, SDL2, and Wasm frontends +* Procedural noise generation * Forever emoji-free README and docs * Forever LLM-free code ## In progress * Different camera types -* Builtin light source support -* Spherical etc UV mapping -* Procedural noise generation +* Spherical etc. UV mapping * Terminal frontend with ncurses * Cube mapping and skyboxes @@ -115,8 +115,8 @@ The `retrofire-demos` package depends on `retrofire`. # Screenshots -The classic Stanford bunny. -![The classic Stanford bunny 3D model.](docs/bunny.jpg) +The classic Stanford dragon. +![A detailed 3D model of a dragon sculpture.](docs/dragon.jpg) A first-person mouse-and-keyboard scene with many "Rust crates" strewn on a checkered floor. @@ -125,6 +125,9 @@ checkered floor. Ten thousand spherical particles positioned randomly in a sphere. ![Ten thousand spherical particles in random positions.](docs/sprites.jpg) +A colorful torus rendered in a terminal. +![A torus rendered in a terminal using block characters.](docs/curses.png) + # License Copyright 2020-2025 Johannes Dahlström. diff --git a/benches/clip.rs b/benches/clip.rs index c2450ff3..473ff333 100644 --- a/benches/clip.rs +++ b/benches/clip.rs @@ -2,88 +2,74 @@ use core::{array, iter::repeat_with}; -use divan::{Bencher, counter::ItemsCount}; +use divan::{AllocProfiler, Bencher}; use retrofire_core::{ geom::{Tri, vertex}, - math::rand::{DEFAULT_RNG, DefaultRng, Distrib}, - math::{orthographic, pt3}, - render::clip::{ClipVert, view_frustum}, + math::rand::{DefaultRng, Distrib}, + math::{ProjMat3, orthographic, pt3}, + render::View, + render::clip::{Clip, ClipVert, view_frustum}, }; -//#[global_allocator] -//static ALLOC: AllocProfiler = AllocProfiler::system(); +#[global_allocator] +static ALLOC: AllocProfiler = AllocProfiler::system(); + +static PROJ: ProjMat3 = + orthographic(pt3(-1.0, -1.0, -1.0), pt3(1.0, 1.0, 1.0)); #[divan::bench(args = [1, 10, 100, 1000, 10_000])] fn clip_mixed(b: Bencher, n: usize) { let rng = &mut DefaultRng::default(); let pts = pt3(-10.0, -10.0, -10.0)..pt3(10.0, 10.0, 10.0); - let proj = orthographic(pt3(-1.0, -1.0, -1.0), pt3(1.0, 1.0, 1.0)); b.with_inputs(|| { repeat_with(|| { - let vs = array::from_fn(|_| { - ClipVert::new(vertex(proj.apply(&pts.sample(rng)), ())) - }); - Tri(vs) + Tri(array::from_fn(|_| { + ClipVert::new(vertex(PROJ.apply(&pts.sample(rng)), ())) + })) }) .take(n) .collect::>() }) - .input_counter(|tris| ItemsCount::of_iter(tris)) - .bench_local_values(|tris| { - let mut out = Vec::new(); - view_frustum::clip(tris.as_slice(), &mut out); - out - }) + .counter(n) + .bench_local_values(|tris| Tri::clip(tris, &view_frustum::PLANES)) } #[divan::bench(args = [1, 10, 100, 1000, 10_000])] fn clip_all_inside(b: Bencher, n: usize) { let rng = &mut DefaultRng::default(); let pts = pt3(-1.0, -1.0, -1.0)..pt3(1.0, 1.0, 1.0); - let proj = orthographic(pt3(-1.0, -1.0, -1.0), pt3(1.0, 1.0, 1.0)); b.with_inputs(|| { repeat_with(|| { - let vs = array::from_fn(|_| { - ClipVert::new(vertex(proj.apply(&pts.sample(rng)), ())) - }); - Tri(vs) + Tri(array::from_fn(|_| { + ClipVert::new(vertex(PROJ.apply(&pts.sample(rng)), ())) + })) }) .take(n) .collect::>() }) - .input_counter(|tris| ItemsCount::of_iter(tris)) - .bench_local_values(|tris| { - let mut out = Vec::new(); - view_frustum::clip(tris.as_slice(), &mut out); - out - }) + .counter(n) + .bench_local_values(|tris| Tri::clip(tris, &view_frustum::PLANES)) } #[divan::bench(args = [1, 10, 100, 1000, 10_000])] fn clip_all_outside(b: Bencher, n: usize) { - let mut rng = DEFAULT_RNG; + let rng = &mut DefaultRng::default(); let pts = pt3(2.0, -10.0, -10.0)..pt3(10.0, 10.0, 10.0); - let proj = orthographic(pt3(-1.0, -1.0, -1.0), pt3(1.0, 1.0, 1.0)); b.with_inputs(|| { repeat_with(|| { - let vs = ([pts.start; 3]..[pts.end; 3]) - .sample(&mut rng) - .map(|pt| ClipVert::new(vertex(proj.apply(&pt), ()))); - Tri(vs) + Tri(([pts.start; 3]..[pts.end; 3]) + .sample(rng) + .map(|pt| ClipVert::new(vertex(PROJ.apply(&pt), ())))) }) .take(n) .collect::>() }) - .input_counter(|tris| ItemsCount::of_iter(tris)) - .bench_local_values(|tris| { - let mut out = Vec::with_capacity(tris.len()); - view_frustum::clip(tris.as_slice(), &mut out); - out - }) + .counter(n) + .bench_local_values(|tris| Tri::clip(tris, &view_frustum::PLANES)) } fn main() { diff --git a/benches/e2e.rs b/benches/e2e.rs index d295bb12..49a1b9b4 100644 --- a/benches/e2e.rs +++ b/benches/e2e.rs @@ -2,47 +2,46 @@ use divan::Bencher; -use retrofire_core::geom::{Normal3, Vertex3, tri, vertex}; -use retrofire_core::math::{ - Color3f, Color4, Color4f, ProjMat3, perspective, pt2, pt3, rgb, rgba, - translate, viewport, +use retrofire_core::{ + geom::{Normal3, Tri, Vertex3, vertex}, + math::{ + Color3f, Color4, Color4f, ProjMat3, perspective, pt2, pt3, rgb, rgba, + translate, viewport, + }, + render::{Context, Frag, Model, debug::dir_to_rgb, render, shader}, + util::{Buf2, dims, pnm}, }; -use retrofire_core::render::debug::dir_to_rgb; -use retrofire_core::render::{Context, Frag, Model, render, shader}; -use retrofire_core::util::{buf::Buf2, pnm}; use retrofire_geom::solids::{Build, Sphere}; -#[cfg(false)] #[global_allocator] static ALLOC: divan::AllocProfiler = divan::AllocProfiler::system(); #[divan::bench(args = [1, 4, 16, 64, 256, 1024], max_time = 2)] fn triangle(b: Bencher, n: u32) { let verts = [ - vertex(pt3(-1.0, 1.0, 0.0), rgb(1.0, 0.0, 0.0)), - vertex(pt3(1.0, 1.0, 0.0), rgb(0.0, 0.8, 0.0)), - vertex(pt3(0.0, -1.0, 0.0), rgb(0.4, 0.4, 1.0)), + vertex(pt3(-1.0, -1.0, 0.0), rgb(1.0, 0.0, 0.0)), + vertex(pt3(0.0, 1.0, 0.0), rgb(0.0, 0.8, 0.0)), + vertex(pt3(1.0, -1.0, 0.0), rgb(0.4, 0.4, 1.0)), ]; let shader = shader::new( |v: Vertex3, mvp: &ProjMat3| { 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); + let dims = dims::VGA_640_480; let modelview = translate((0.0, 0.0, 2.0)).to(); - let project = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); - let viewport = viewport(pt2(0, h)..pt2(w, 0)); + let project = perspective(1.0, dims.aspect(), 0.1..1000.0); + let viewport = viewport(pt2(0, dims.1)..pt2(dims.0, 0)); let mut framebuf = Buf2::::new(dims); - b.bench_local(|| { + b.counter(n).bench_local(|| { for _ in 0..n { render( - [tri(0, 1, 2)], - verts, + Tri(verts), &shader, &modelview.then(&project), viewport, @@ -52,14 +51,13 @@ fn triangle(b: Bencher, n: u32) { } }); - let center_pixel = framebuf[[w / 2, h / 2]]; + let center_pixel = framebuf[[dims.0 / 2, dims.1 / 2]]; + assert_eq!(center_pixel, rgba(89, 127, 64, 255)); - assert_eq!(center_pixel, rgba(151, 128, 187, 255)); - - pnm::save_ppm("benches_e2e_triangle.ppm", framebuf).unwrap(); + pnm::save_ppm("benches/out/e2e_triangle.ppm", framebuf).unwrap(); } -#[divan::bench(args=[4, 16, 64, 256, 1024], min_time=5, max_time=5)] +#[divan::bench(args=[4, 16, 64, 256, 1024], min_time=1, max_time=2)] fn sphere(b: Bencher, res: u32) { let sphere = Sphere { sectors: res, @@ -72,20 +70,19 @@ fn sphere(b: Bencher, res: u32) { |v: Vertex3, mvp: &ProjMat3| { vertex(mvp.apply(&v.pos), dir_to_rgb(v.attrib)) }, - |frag: Frag| frag.var.to_color4(), + |frag: Frag, _: &_| frag.var.to_color4(), ); - let dims @ (w, h) = (640, 480); + let dims = dims::VGA_640_480; let modelview = translate((0.0, 0.0, 2.0)).to(); - let project = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); - let viewport = viewport(pt2(0, h)..pt2(w, 0)); + let project = perspective(1.0, dims.aspect(), 0.1..1000.0); + let viewport = viewport(pt2(0, dims.1)..pt2(dims.0, 0)); let mut framebuf = Buf2::::new(dims); - b.bench_local(|| { + b.counter(sphere.faces.len()).bench_local(|| { render( - &sphere.faces, - &sphere.verts, + sphere.clone(), &shader, &modelview.then(&project), viewport, @@ -94,11 +91,11 @@ fn sphere(b: Bencher, res: u32) { ); }); - let center_pixel = framebuf[[w / 2, h / 2]]; - + let center_pixel = framebuf[[dims.0 / 2, dims.1 / 2]]; assert_eq!(center_pixel, rgba(128, 127, 0, 255)); - pnm::save_ppm("benches_e2e_sphere.ppm", framebuf).unwrap(); + pnm::save_ppm(format!("benches/out/e2e_sphere_{res}.ppm"), framebuf) + .unwrap(); } fn main() { diff --git a/benches/fill.rs b/benches/fill.rs index 2bee07fa..cab01ffc 100644 --- a/benches/fill.rs +++ b/benches/fill.rs @@ -2,7 +2,7 @@ use core::iter::zip; -use divan::{Bencher, counter::ItemsCount}; +use divan::Bencher; use retrofire_core::{ geom::{Tri, vertex}, @@ -10,31 +10,31 @@ use retrofire_core::{ render::{ Texture, raster::ScreenPt, raster::tri_fill, tex::SamplerRepeatPot, uv, }, - util::{buf::Buf2, pnm::save_ppm}, + util::{Buf2, Dims, pnm::save_ppm}, }; const SIZES: [f32; 5] = [4.0, 16.0, 64.0, 256.0, 1024.0]; const VERTS: [ScreenPt; 3] = - [pt3(0.1, 0.1, 0.0), pt3(0.9, 0.3, 0.5), pt3(0.4, 0.9, 1.0)]; + [pt3(0.1, 0.1, 0.0), pt3(0.4, 0.9, 1.0), pt3(0.9, 0.3, 0.5)]; #[divan::bench(args = SIZES)] fn flat(b: Bencher, sz: f32) { - let mut buf: Buf2 = Buf2::new((1024, 1024)); + let mut buf: Buf2 = Buf2::new(Dims(1024, 1024)); b.with_inputs(|| VERTS.map(|p| vertex(p * sz, ()))) - .input_counter(move |vs| ItemsCount::new(Tri(*vs).area() as usize)) + .input_counter(move |vs| Tri(*vs).area() as usize) .bench_local_values(|vs| { tri_fill(vs, |sl| { buf[sl.y][sl.xs].fill(gray(0xCC)); }); }); - save_ppm("benches_fill_flat.ppm", buf).unwrap(); + save_ppm("benches/out/fill_flat.ppm", buf).unwrap(); } #[divan::bench(args = SIZES)] fn gouraud(b: Bencher, sz: f32) { - let mut buf: Buf2 = Buf2::new((1024, 1024)); + let mut buf: Buf2 = Buf2::new(Dims(1024, 1024)); b.with_inputs(|| { [ @@ -43,7 +43,7 @@ fn gouraud(b: Bencher, sz: f32) { vertex(VERTS[2] * sz, rgb(0.2, 0.3, 1.0)), ] }) - .input_counter(move |vs| ItemsCount::new(Tri(*vs).area() as usize)) + .input_counter(move |vs| Tri(*vs).area() as usize) .bench_local_values(|vs| { tri_fill(vs, |sl| { let y = sl.y; @@ -57,16 +57,16 @@ fn gouraud(b: Bencher, sz: f32) { }); let buf = - Buf2::new_from((1024, 1024), buf.data().iter().map(|c| c.to_color3())); - save_ppm("benches_fill_color.ppm", buf).unwrap(); + Buf2::new_from(buf.dims(), buf.data().iter().map(|c| c.to_color3())); + save_ppm("benches/out/fill_color.ppm", buf).unwrap(); } #[divan::bench(args = SIZES)] fn texture(b: Bencher, sz: f32) { - let mut buf: Buf2 = Buf2::new((1024, 1024)); + let mut buf: Buf2 = Buf2::new(Dims(1024, 1024)); let tex = Texture::from(Buf2::::new_from( - (2, 2), + Dims(2, 2), [gray(0xFF), gray(0x33), gray(0x33), gray(0xFF)], )); let sampler = SamplerRepeatPot::new(&tex); @@ -78,7 +78,7 @@ fn texture(b: Bencher, sz: f32) { vertex(VERTS[2] * sz, uv(0.0, 4.0)), ] }) - .input_counter(move |vs| ItemsCount::new(Tri(*vs).area() as usize)) + .input_counter(move |vs| Tri(*vs).area() as usize) .bench_local_values(|vs| { tri_fill(vs, |sl| { let y = sl.y; @@ -91,7 +91,7 @@ fn texture(b: Bencher, sz: f32) { }); }); - save_ppm("benches_fill_tex.ppm", buf).unwrap(); + save_ppm("benches/out/fill_tex.ppm", buf).unwrap(); } fn main() { diff --git a/benches/isect.rs b/benches/isect.rs index 7f23b7e5..9664ac87 100644 --- a/benches/isect.rs +++ b/benches/isect.rs @@ -2,16 +2,14 @@ use core::hint::black_box; -use divan::{Bencher, counter::ItemsCount}; +use divan::Bencher; -use retrofire::core::{ +use retrofire_core::{ geom::{Plane3, Ray, Sphere}, - math::rand::*, - math::{Point3, degs, pt3, spherical, vec3}, + math::{Point3, degs, pt3, rand::*, spherical, splat, vec3}, render::scene::BBox, }; -use retrofire::geom::Intersect; -use retrofire_core::math::splat; +use retrofire_geom::Intersect; #[divan::bench] fn ray_plane_hit(b: Bencher) { @@ -25,7 +23,7 @@ fn ray_plane_hit(b: Bencher) { let v = (splat(-1.0)..splat(0.0)).sample(&mut rng); Ray(pt3(0.0, 10.0, 0.0), 100.0 * (v - vec3(1.0, 1.0, 1.0))) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| ray.intersect(&black_box(plane))); } #[divan::bench] @@ -40,7 +38,7 @@ fn ray_plane_miss(b: Bencher) { let v = (splat(0.0)..splat(1.0)).sample(&mut rng); Ray(pt3(0.0, 10.0, 0.0), 100.0 * v) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| ray.intersect(&black_box(plane))); } #[divan::bench] @@ -55,7 +53,7 @@ fn ray_plane_mixed(b: Bencher) { let v = VectorsInUnitBall.sample(&mut rng); Ray(pt3(0.0, 10.0, 0.0), 100.0 * v) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| ray.intersect(&black_box(plane))); } @@ -68,7 +66,7 @@ fn ray_bbox_hit(b: Bencher) { let v = VectorsInUnitBall.sample(&mut rng); Ray(v.to_pt(), 100.0 * v) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| { assert!(ray.intersect(&black_box(bbox)).is_some()) }); @@ -84,7 +82,7 @@ fn ray_bbox_hit_2(b: Bencher) { let v = (min..max).sample(&mut rng); Ray(pt3(0.0, 2.0, 0.0), v.to_cart()) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| { assert!(ray.intersect(&black_box(bbox)).is_some()) }); @@ -99,7 +97,7 @@ fn ray_bbox_inside(b: Bencher) { let dir = VectorsInUnitBall.sample(&mut rng); Ray(pt, dir) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| { assert!(ray.intersect(&black_box(bbox)).is_some()) }); @@ -116,7 +114,7 @@ fn ray_bbox_miss(b: Bencher) { let v = (min..max).sample(&mut rng); Ray(pt3(0.0, 3.0, 0.0), v.to_cart()) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| { assert!(ray.intersect(&black_box(bbox)).is_none()) }); @@ -134,7 +132,7 @@ fn ray_bbox_mixed(b: Bencher) { let dir = (p..q).sample(&mut rng); Ray(2.0 * orig, 100.0 * dir.to_vec()) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| ray.intersect(&black_box(bbox))); } @@ -150,7 +148,7 @@ fn ray_sphere_miss(b: Bencher) { let v = (min..max).sample(&mut rng); Ray(pt3(0.0, 3.0, 0.0), v.to_cart()) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| { let ip = ray.intersect(&black_box(sphere)); assert!(ip.is_none()); @@ -170,7 +168,7 @@ fn ray_sphere_hit(b: Bencher) { .sample(&mut rng); Ray(pt3(0.0, 2.0f32.sqrt(), 0.0), v.to_cart()) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| { let ip = ray.intersect(&black_box(sphere)); assert!(ip.is_some()); @@ -188,7 +186,7 @@ fn ray_sphere_mixed(b: Bencher) { let v = VectorsInUnitBall.sample(&mut rng); Ray(pt3(0.0, 2.0, 0.0), v) }) - .counter(ItemsCount::new(1usize)) + .counter(1u32) .bench_local_values(|ray| ray.intersect(&black_box(sphere))); } diff --git a/benches/mat.rs b/benches/mat.rs new file mode 100644 index 00000000..bc990733 --- /dev/null +++ b/benches/mat.rs @@ -0,0 +1,298 @@ +//! Matrix manipulation benchmarks. + +use core::ops::Range; + +use divan::Bencher; + +use retrofire_core::math::{ + Mat2, Mat3, Mat4, Vec2, Vec3, + rand::{DefaultRng, Distrib}, + splat, +}; + +mod application { + use super::*; + #[divan::bench] + fn apply2(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC2S.sample(rng); + + b.with_inputs(|| random_mat2(rng)) + .counter(1u32) + .bench_local_values(|m: Mat2| m.apply(&v)); + } + + #[divan::bench] + fn apply3_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC3S.sample(rng); + + b.with_inputs(|| random_mat3_lin(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3<(), (), 3>| m.apply(&v)); + } + + #[divan::bench] + fn apply3_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC2S.sample(rng); + + b.with_inputs(|| random_mat3_aff(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3| m.apply(&v)); + } + + #[divan::bench] + fn apply4(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC3S.sample(rng); + + b.with_inputs(|| random_mat4(rng)) + .counter(1u32) + .bench_local_values(|m: Mat4| m.apply(&v)); + } +} + +mod inverse_application { + use super::*; + + #[divan::bench] + fn apply_inv2(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC2S.sample(rng); + + b.with_inputs(|| random_mat2(rng)) + .counter(1u32) + .bench_local_values(|m: Mat2| m.inverse().apply(&v)); + } + + #[divan::bench] + fn apply_inv3_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC3S.sample(rng); + + b.with_inputs(|| random_mat3_lin(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3<(), (), 3>| m.inverse().apply(&v)); + } + + #[divan::bench] + fn apply_inv3_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC2S.sample(rng); + + b.with_inputs(|| random_mat3_aff(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3| m.inverse().apply(&v)); + } + + #[divan::bench] + fn apply_inv4(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC3S.sample(rng); + + b.with_inputs(|| random_mat4(rng)) + .counter(1u32) + .bench_local_values(|m: Mat4| m.inverse().apply(&v)); + } +} + +mod composition { + use super::*; + #[divan::bench] + fn compose2(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| (random_mat2(rng), random_mat2(rng))) + .counter(1u32) + .bench_local_values(|(m, n)| m.compose(&n)); + } + + #[divan::bench] + fn compose3_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| (random_mat3_lin(rng), random_mat3_lin(rng))) + .counter(1u32) + .bench_local_values(|(m, n)| m.compose(&n)); + } + + #[divan::bench] + fn compose3_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| (random_mat3_aff(rng), random_mat3_aff(rng))) + .counter(1u32) + .bench_local_values(|(m, n)| m.compose(&n)); + } + + #[divan::bench] + fn compose4(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| (random_mat4(rng), random_mat4(rng))) + .counter(1u32) + .bench_local_values(|(m, n)| m.compose(&n)); + } +} + +mod solving { + use super::*; + #[divan::bench] + fn solve2(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC2S.sample(rng); + + b.with_inputs(|| random_mat2(rng)) + .counter(1u32) + .bench_local_values(|m: Mat2| m.solve(v)); + } + + #[divan::bench] + fn solve3(b: Bencher) { + let rng = &mut DefaultRng::default(); + let v = VEC3S.sample(rng); + + b.with_inputs(|| random_mat3_lin(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3<(), (), 3>| m.solve(v)); + } +} + +mod inversion { + use super::*; + + #[divan::bench] + fn invert2(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| random_mat2(rng)) + .counter(1u32) + .bench_local_values(|m: Mat2| m.inverse()); + } + + #[divan::bench] + fn invert3_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| random_mat3_lin(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3<(), (), 3>| m.inverse()); + } + + #[divan::bench] + fn invert3_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| random_mat3_aff(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3| m.inverse()); + } + + #[divan::bench] + fn invert4_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| random_mat4(rng)) + .counter(1u32) + .bench_local_values(|m: Mat4| m.inverse()); + } + #[divan::bench] + + fn invert4_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| { + let mut mat = random_mat4(rng); + mat.0[3][1] = 0.5; // Make non-affine (simulate projection matrix) + mat + }) + .counter(1u32) + .bench_local_values(|m: Mat4| m.inverse()); + } +} + +mod determinant { + use super::*; + + #[divan::bench] + fn det2(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| random_mat2(rng)) + .counter(1u32) + .bench_local_values(|m: Mat2| m.determinant()); + } + + #[divan::bench] + fn det3_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| random_mat3_lin(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3<(), (), 3>| m.determinant()); + } + + #[divan::bench] + fn det3_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| random_mat3_aff(rng)) + .counter(1u32) + .bench_local_values(|m: Mat3| m.determinant()); + } + + #[divan::bench] + fn det4_aff(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| random_mat4(rng)) + .counter(1u32) + .bench_local_values(|m: Mat4| m.determinant()); + } + + #[divan::bench] + fn det4_lin(b: Bencher) { + let rng = &mut DefaultRng::default(); + + b.with_inputs(|| { + let mut mat = random_mat4(rng); + mat.0[3][1] = 0.5; // Make non-affine (simulate projection matrix) + mat + }) + .counter(1u32) + .bench_local_values(|m: Mat4| m.determinant()); + } +} + +const VEC2S: Range = splat(-1e3)..splat(1e3); +const VEC3S: Range = splat(-1e3)..splat(1e3); + +fn random_mat2(rng: &mut DefaultRng) -> Mat2 { + let x = VEC2S.sample(rng); + let y = x.perp(); + Mat2::new([x.0, y.0]) +} +fn random_mat3_lin(rng: &mut DefaultRng) -> Mat3<(), (), 3> { + let x = VEC3S.sample(rng); + let y = Vec3::Z.cross(&x); + let z = x.cross(&y); + Mat3::new([x.0, y.0, z.0]) +} +fn random_mat3_aff(rng: &mut DefaultRng) -> Mat3 { + let x = VEC2S.sample(rng); + let y = x.perp(); + let o = VEC2S.sample(rng).to_pt(); + Mat3::from_affine(x, y, o) +} +fn random_mat4(rng: &mut DefaultRng) -> Mat4 { + let x = VEC3S.sample(rng); + let y = Vec3::Z.cross(&x); + let z = x.cross(&y); + let o = VEC3S.sample(rng).to_pt(); + Mat4::from_affine(x, y, z, o) +} + +fn main() { + divan::main() +} diff --git a/benches/noise.rs b/benches/noise.rs new file mode 100644 index 00000000..c27ff05b --- /dev/null +++ b/benches/noise.rs @@ -0,0 +1,57 @@ +//! Noise generation benchmarks. + +use core::hint::black_box; + +use divan::Bencher; + +use retrofire_core::math::{ + noise::{Perlin2, Perlin3}, + pt2, pt3, +}; + +const SIZES: [u32; 5] = [1, 4, 16, 64, 256]; + +#[divan::bench(args = SIZES)] +fn perlin2(b: Bencher, sz: u32) { + let noise = Perlin2::default(); + + b.counter(sz * sz).bench_local(|| { + for i in 0..sz { + for j in 0..sz { + black_box(noise.eval(pt2(i as f32, j as f32) / 64.0)); + } + } + }); +} + +#[divan::bench(args = SIZES)] +fn cache_hits(b: Bencher, sz: u32) { + let noise = Perlin2::default(); + + b.counter(sz * sz) + .counter(256 * 256u32) + .bench_local(|| { + for i in 0..256 { + for j in 0..256 { + black_box(noise.eval(pt2(i as f32, j as f32) / sz as f32)); + } + } + }); +} + +#[divan::bench(args = SIZES)] +fn perlin3(b: Bencher, sz: u32) { + let noise = Perlin3::default(); + + b.counter(sz * sz).bench_local(|| { + for i in 0..sz { + for j in 0..sz { + black_box(noise.eval(pt3(i as f32, j as f32, 0.0) / 64.0)); + } + } + }); +} + +fn main() { + divan::main() +} diff --git a/benches/out/.gitignore b/benches/out/.gitignore new file mode 100644 index 00000000..a4ca1465 --- /dev/null +++ b/benches/out/.gitignore @@ -0,0 +1 @@ +*.p?m diff --git a/benches/vec.rs b/benches/vec.rs index 177eb04b..d6e7409b 100644 --- a/benches/vec.rs +++ b/benches/vec.rs @@ -1,7 +1,7 @@ //! Triangle clipping benchmarks. use divan::Bencher; -use divan::counter::ItemsCount; + use retrofire_core::{ math::rand::{DefaultRng, Distrib}, math::{Vec3, splat}, @@ -13,7 +13,7 @@ fn normalize_exact(b: Bencher) { let vecs = splat(-1e6)..splat(1e6); b.with_inputs(|| vecs.sample(rng)) - .input_counter(|_| ItemsCount::new(1u32)) + .counter(1u32) .bench_local_values(|v: Vec3| v.normalize()); } @@ -23,7 +23,7 @@ fn normalize_approx(b: Bencher) { let vecs = splat(-1e6)..splat(1e6); b.with_inputs(|| vecs.sample(rng)) - .input_counter(|_| ItemsCount::new(1u32)) + .counter(1u32) .bench_local_values(|v: Vec3| v.normalize_approx()); } diff --git a/core/Cargo.toml b/core/Cargo.toml index e06566cf..86312a8e 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -23,6 +23,7 @@ documentation.workspace = true [features] default = ["std"] + # Use std fp functions, enable I/O and timing support. std = ["fp"] # Use fp functions from the libm crate. @@ -32,6 +33,8 @@ mm = ["fp", "dep:micromath"] # For internal use only. fp = [] +stats = [] + [dependencies] libm = { version = "0.2", optional = true } micromath = { version = "2.1", optional = true } diff --git a/core/README.md b/core/README.md index 3f1f379b..de48f142 100644 --- a/core/README.md +++ b/core/README.md @@ -28,6 +28,10 @@ customizable shaders; with more to come. and transcendental functions. Enabled by default. If this feature is disabled, the crate only depends on `alloc`. +* `stats`: + Enables collection of rendering performance data, with a slight performance + and binary size impact. + * `libm`: Provides software implementations of floating-point functions via the [`libm`](https://crates.io/crates/libm) crate. diff --git a/core/examples/hello_tri.rs b/core/examples/hello_tri.rs index 5d1af8a3..ef421c54 100644 --- a/core/examples/hello_tri.rs +++ b/core/examples/hello_tri.rs @@ -1,14 +1,18 @@ -use retrofire_core::{ - prelude::*, - render::{Model, render, shader}, -}; +use retrofire_core::prelude::*; +use retrofire_core::render::{Model, TriFan, render, shader}; +use retrofire_core::util::dims; fn main() { - let verts = [ - vertex(pt3(-1.0, 1.0, 0.0), rgb(1.0, 0.0, 0.0)), - vertex(pt3(1.0, 1.0, 0.0), rgb(0.0, 0.8, 0.0)), - vertex(pt3(0.0, -1.0, 0.0), rgb(0.4, 0.4, 1.0)), - ]; + let tri = TriFan( + [ + vertex(pt3(1.0, 1.0, 0.0), rgb(1.0, 0.2, 0.0)), + vertex(pt3(-1.0, 1.0, 0.0), rgb(0.0, 0.8, 0.2)), + vertex(pt3(0.0, -1.0, 0.0), rgb(0.4, 0.4, 1.0)), + vertex(pt3(2.0, -0.8, 0.0), rgb(0.9, 0.4, 1.0)), + vertex(pt3(1.5, 1.0, 0.0), rgb(0.9, 0.4, 0.0)), + ] + .to_vec(), + ); #[cfg(feature = "fp")] let shader = shader::new( @@ -17,7 +21,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( @@ -26,36 +30,34 @@ 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); + let dims = dims::VGA_640_480; let modelview = translate((0.0, 0.0, 2.0)).to(); - let project = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); - let viewport = viewport(pt2(0, h)..pt2(w, 0)); + let project = perspective(1.0, dims.aspect(), 0.1..1000.0); + let viewport = viewport(dims.into()); let mut framebuf = Buf2::::new(dims); render( - [tri(0, 1, 2)], - verts, + tri, &shader, &modelview.then(&project), viewport, &mut framebuf, &Context::default(), ); - - let center_pixel = framebuf[[w / 2, h / 2]]; - - if cfg!(feature = "fp") { - assert_eq!(center_pixel, rgba(151, 128, 187, 255)); - } else { - assert_eq!(center_pixel, rgba(114, 102, 128, 255)); - } #[cfg(feature = "std")] { use retrofire_core::util::pnm; - pnm::save_ppm("triangle.ppm", framebuf).unwrap(); + pnm::save_ppm("triangle.ppm", &framebuf).unwrap(); + } + + let center_pixel = framebuf[[dims.0 / 2, dims.1 / 2]]; + if cfg!(feature = "fp") { + assert_eq!(center_pixel, rgba(152, 130, 187, 255)); + } else { + assert_eq!(center_pixel, rgba(115, 114, 140, 255)); } } diff --git a/core/src/geom/prim.rs b/core/src/geom/prim.rs index 868cb783..757fb838 100644 --- a/core/src/geom/prim.rs +++ b/core/src/geom/prim.rs @@ -3,7 +3,10 @@ //! Includes vertices, polygons, planes, rays, and more. use alloc::vec::Vec; -use core::fmt::{self, Debug, Formatter}; +use core::{ + fmt::{self, Debug, Formatter}, + ops::Index, +}; use crate::math::{ Affine, ApproxEq, Lerp, Linear, Mat4, Parametric, Point, Point2, Point3, @@ -14,6 +17,16 @@ use crate::math::{ }; use crate::render::Model; +/// A trait for types that have a position. Primarily useful for APIs such as +/// `Tri` that can handle either points or vertices. +pub trait Pos { + /// The position type. + type Type; + + /// Returns the position of `self`. + fn pos(&self) -> &Self::Type; +} + /// Vertex with a position and arbitrary other attributes. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub struct Vertex { @@ -108,7 +121,7 @@ pub const fn vertex(pos: P, attrib: A) -> Vertex { /// Creates a [`Tri`] with the given vertices. #[inline] -pub const fn tri(a: V, b: V, c: V) -> Tri { +pub const fn tri

(a: P, b: P, c: P) -> Tri

{ Tri([a, b, c]) } @@ -147,38 +160,63 @@ impl Tri { } } -impl Tri> { +impl> Tri

{ /// Given a triangle ABC, returns the vectors [AB, AC]. #[inline] - pub fn tangents(&self) -> [P::Diff; 2] { + pub fn tangents(&self) -> [T::Diff; 2] { let [a, b, c] = &self.0; - [b.pos.sub(&a.pos), c.pos.sub(&a.pos)] + [b.pos().sub(a.pos()), c.pos().sub(a.pos())] } /// Returns the geometric center, or "balance point", of `self`. /// /// The centroid is simply the average of the three vertex positions. - pub fn centroid(&self) -> P + pub fn centroid(&self) -> T where - P::Diff: Linear, + T::Diff: Linear, { let [ab, ac] = self.tangents(); - self.0[0].pos.add(&ab.add(&ac).mul(1.0 / 3.0)) + self.0[0].pos().add(&ab.add(&ac).mul(1.0 / 3.0)) + } +} + +impl

Tri

{ + /// Returns the area of `self`. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{tri, vertex}; + /// use retrofire_core::math::{Point3, pt3}; + /// + /// let tri = tri::( + /// pt3(0.0, 0.0, 0.0), + /// pt3(4.0, 0.0, 0.0), + /// pt3(0.0, 3.0, 0.0), + /// ); + /// assert_eq!(tri.area(), 6.0); + /// ``` + pub fn area(&self) -> f32 + where + P: Pos> + Clone>, + { + let [a, b, c] = self.0.each_ref().map(|p| p.pos().clone().into()); + let [t, u] = tri(a, b, c).tangents(); + t.cross(&u).len() / 2.0 } } -impl Tri> { +impl>> Tri

{ /// Returns the winding order of `self`. /// /// # Examples /// ``` /// use retrofire_core::geom::{Tri, vertex, Winding}; - /// use retrofire_core::math::pt2; + /// use retrofire_core::math::{pt2, Point2}; /// - /// let mut tri = Tri([ - /// vertex(pt2::<_, ()>(0.0, 0.0), ()), - /// vertex(pt2(0.0, 3.0), ()), - /// vertex(pt2(4.0, 0.0), ()), + /// let mut tri = Tri::([ + /// pt2(0.0, 0.0), + /// pt2(0.0, 3.0), + /// pt2(4.0, 0.0), /// ]); /// assert_eq!(tri.winding(), Winding::Cw); /// @@ -201,12 +239,12 @@ impl Tri> { /// # Examples /// ``` /// use retrofire_core::geom::{Tri, vertex}; - /// use retrofire_core::math::pt2; + /// use retrofire_core::math::{pt2, Point2}; /// - /// let tri = Tri([ - /// vertex(pt2::<_, ()>(0.0, 0.0), ()), - /// vertex(pt2(0.0, 3.0), ()), - /// vertex(pt2(4.0, 0.0), ()), + /// let tri = Tri::([ + /// pt2(0.0, 0.0), + /// pt2(0.0, 3.0), + /// pt2(4.0, 0.0), /// ]); /// assert_eq!(tri.signed_area(), -6.0); /// ``` @@ -214,27 +252,9 @@ impl Tri> { let [t, u] = self.tangents(); t.perp_dot(u) / 2.0 } - - /// Returns the (positive) area of `self`. - /// - /// # Examples - /// ``` - /// use retrofire_core::geom::{vertex, Tri}; - /// use retrofire_core::math::pt2; - /// - /// let tri = Tri([ - /// vertex(pt2::<_, ()>(0.0, 0.0), ()), - /// vertex(pt2(0.0, 3.0), ()), - /// vertex(pt2(4.0, 0.0), ()), - /// ]); - /// assert_eq!(tri.area(), 6.0); - /// ``` - pub fn area(&self) -> f32 { - self.signed_area().abs() - } } -impl Tri> { +impl>> Tri

{ /// Returns the normal vector of `self`. /// /// The result is normalized to unit length. If self is degenerate and @@ -246,13 +266,13 @@ impl Tri> { /// /// use retrofire_core::assert_approx_eq; /// use retrofire_core::geom::{Tri, vertex}; - /// use retrofire_core::math::{pt3, vec3}; + /// use retrofire_core::math::{pt3, vec3, Point3}; /// /// // Triangle lying in a 45° angle - /// let tri = Tri([ - /// vertex(pt3::<_, ()>(0.0, 0.0, 0.0), ()), - /// vertex(pt3(0.0, 3.0, 3.0), ()), - /// vertex(pt3(4.0, 0.0,0.0), ()), + /// let tri = Tri::([ + /// pt3(0.0, 0.0, 0.0), + /// pt3(0.0, 3.0, 3.0), + /// pt3(4.0, 0.0,0.0), /// ]); /// assert_approx_eq!(tri.normal(), vec3(0.0, FRAC_1_SQRT_2, -FRAC_1_SQRT_2)); /// ``` @@ -267,50 +287,28 @@ impl Tri> { /// # Examples /// ``` /// use retrofire_core::geom::{Tri, Plane3, vertex}; - /// use retrofire_core::math::{pt3, Vec3}; + /// use retrofire_core::math::{Point3, Vec3, pt3}; /// - /// let tri = Tri([ - /// vertex(pt3::(0.0, 0.0, 2.0), ()), - /// vertex(pt3(1.0, 0.0, 2.0), ()), - /// vertex(pt3(0.0, 1.0, 2.0), ()) + /// let tri = Tri::([ + /// pt3(0.0, 0.0, 2.0), + /// pt3(1.0, 0.0, 2.0), + /// pt3(0.0, 1.0, 2.0) /// ]); /// assert_eq!(tri.plane().normal(), Vec3::Z); /// assert_eq!(tri.plane().offset(), 2.0); /// ``` pub fn plane(&self) -> Plane3 { let [a, b, c] = &self.0; - let [p, q, r] = [a.pos, b.pos, c.pos]; - Plane::from_points(p, q, r) + Plane::from_points(*a.pos(), *b.pos(), *c.pos()) } /// Returns the winding order of `self`, as projected to the XY plane. // TODO is this 3D version meaningful/useful enough? - pub fn winding(&self) -> Winding { - // TODO better way to xyz->xy... + pub fn winding_xy(&self) -> Winding { let [u, v] = self.tangents(); - let ([ux, uy, _], [vx, vy, _]) = (u.0, v.0); - let z = vec2::<_, ()>(ux, uy).perp_dot(vec2(vx, vy)); + let z = u.xy().perp_dot(v.xy()); if z < 0.0 { Winding::Cw } else { Winding::Ccw } } - - /// Returns the area of `self`. - /// - /// # Examples - /// ``` - /// use retrofire_core::geom::{tri, vertex}; - /// use retrofire_core::math::pt3; - /// - /// let tri = tri( - /// vertex(pt3::<_, ()>(0.0, 0.0, 0.0), ()), - /// vertex(pt3(4.0, 0.0, 0.0), ()), - /// vertex(pt3(0.0, 3.0, 0.0), ()), - /// ); - /// assert_eq!(tri.area(), 6.0); - /// ``` - pub fn area(&self) -> f32 { - let [t, u] = self.tangents(); - t.cross(&u).len() / 2.0 - } } impl Plane3 { @@ -582,7 +580,7 @@ impl Polyline { /// assert_eq!(edges.next(), None); /// ``` pub fn edges(&self) -> impl Iterator> + '_ { - self.0.array_windows().map(|[a, b]| Edge(a, b)) + self.0.array_windows().map(Edge::from) } /// Returns the sum of the lengths of the edges using a custom metric. @@ -673,7 +671,7 @@ impl Line2 { /// /// # Panics /// If the vector (a, b) is not unit-length. - pub fn new(a: f32, b: f32, c: f32) -> Self { + pub const fn new(a: f32, b: f32, c: f32) -> Self { // TODO This method can't itself normalize because const assert!((a * a + b * b - 1.0).abs() < 1e-6, "non-unit normal"); Self(Vector::new([a, b, -c])) @@ -684,6 +682,7 @@ impl Line2 { /// # Panics /// If the points coincide. pub fn from_points(p: Point2, q: Point2) -> Self { + // TODO not const due to normalize Edge(p, q).into() } @@ -698,16 +697,16 @@ impl Line2 { /// assert_approx_eq!(slope, 0.5); /// assert_approx_eq!(y_intercept, 1.5); /// ``` - pub fn slope_intercept(&self) -> Option<(f32, f32)> { + pub const fn slope_intercept(&self) -> Option<(f32, f32)> { // ax + by + c = 0 let [a, b, c] = self.coeffs(); - - (b != 0.0).then(|| { - // by = -ax - c <=> y = -a/b x - c/b - let m = -a / b; // slope - let y0 = -c / b; // y intercept - (m, y0) - }) + if b == 0.0 { + return None; + } + // by = -ax - c <=> y = -a/b x - c/b + let m = -a / b; // slope + let y0 = -c / b; // y intercept + Some((m, y0)) } /// Returns @@ -715,8 +714,8 @@ impl Line2 { vec2(self.0[0], self.0[1]).normalize() } /// Returns the signed distance of `self` from the origin. - pub fn offset(&self) -> f32 { - -self.0[2] + pub const fn offset(&self) -> f32 { + -self.0.0[2] } /// Returns the coefficients [a, b, c] of the line equation ax + by = c. @@ -729,6 +728,30 @@ impl Line2 { // Local trait impls // +impl Pos for Vertex { + type Type = P; + + fn pos(&self) -> &Self::Type { + &self.pos + } +} +impl Pos for &Vertex { + type Type = P; + + fn pos(&self) -> &Self::Type { + &self.pos + } +} + +impl Pos for Point { + type Type = Self; + + /// Returns `self` itself. + fn pos(&self) -> &Self { + self + } +} + impl Parametric for Ray where T: Affine>, @@ -739,7 +762,7 @@ where } impl Parametric for Polyline { - /// Returns the point on `self` at *t*. + /// Returns the point on `self` at the given *t* value. /// /// If the number of vertices in `self` is *n* > 1, the vertex at index /// *k* < *n* corresponds to `t` = *k* / (*n* - 1). Intermediate values @@ -788,6 +811,52 @@ impl Parametric for Polyline { } } +impl Parametric for Polygon { + /// Returns the point on `self` at the given *t* value. + /// + /// If the number of vertices in `self` is *n* > 1, the vertex at index + /// *k* < *n* corresponds to *t* = *k* / *n*. Intermediate values + /// of *t* are linearly interpolated between the two closest vertices. + /// Values *t* < 0 and *t* >= 1 "wrap around" to the range [0, 1); in + /// particular, *t* = 1 maps to the first vertex. A polygon with a single + /// vertex returns the value of that vertex for any value of *t*. + /// + /// # Panics + /// If `self` has no vertices. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{Polygon, Edge}; + /// use retrofire_core::math::{pt2, Point2, Parametric}; + /// + /// let pl = Polygon::( + /// vec![pt2(0.0, 0.0), pt2(1.0, 2.0), pt2(2.0, 1.0)]); + /// + /// assert_eq!(pl.eval(0.0), pl.0[0]); + /// assert_eq!(pl.eval(1.0/3.0), pl.0[1]); + /// assert_eq!(pl.eval(2.0/3.0), pl.0[2]); + /// assert_eq!(pl.eval(1.0), pl.0[0]); + /// + /// // Values not corresponding to a vertex are interpolated: + /// assert_eq!(pl.eval(0.5), pt2(1.5, 1.5)); + /// + /// // Values of t outside 0.0..=1.0 wrap around: + /// assert_eq!(pl.eval(-1.25), pl.eval(0.75)); + /// assert_eq!(pl.eval(3.0), pl.eval(0.0)); + /// ``` + fn eval(&self, t: f32) -> T { + use crate::math::float::f32; + assert!(!self.0.is_empty(), "cannot eval an empty polygon"); + + let t = t * self.0.len() as f32; + let t_int = f32::floor(t); + let t_fract = t - t_int; + let i = t_int as isize; + + self[i].lerp(&self[i + 1], t_fract) + } +} + impl Lerp for Vertex { fn lerp(&self, other: &Self, t: f32) -> Self { vertex( @@ -826,7 +895,7 @@ impl Default for Sphere { } impl Debug for Line2 { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { // ax + by + c = 0 let [a, _, c] = self.coeffs(); @@ -884,7 +953,7 @@ impl From>> for Line2 { impl From>> for Line2 { /// Returns the line coincident with the given edge. fn from(e: Edge>) -> Self { - Ray(e.0, e.1 - e.0).into() + Self::from(Ray(e.0, e.1 - e.0)) } } @@ -893,6 +962,50 @@ impl From<[T; 2]> for Edge { Edge(a, b) } } +impl<'a, T> From<&'a [T; 2]> for Edge<&'a T> { + fn from([a, b]: &'a [T; 2]) -> Self { + Edge(a, b) + } +} + +impl FromIterator

for Polygon

{ + fn from_iter>(it: I) -> Self { + Self::new(it) + } +} + +impl Index for Polygon { + type Output = V; + + /// Returns the vertex at an index modulo the number of vertices. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::Polygon; + /// use retrofire_core::math::{pt2, Point2}; + /// + /// let poly = Polygon::::new([ + /// pt2(0.0, 0.0), + /// pt2(1.0, 0.0), + /// pt2(0.0, 1.0) + /// ]); + /// + /// assert_eq!(poly[1], pt2(1.0, 0.0)); + /// // Out-of-range indices "wrap around": + /// assert_eq!(poly[-1], pt2(0.0, 1.0)); + /// assert_eq!(poly[3], pt2(0.0, 0.0)); + /// ``` + #[inline] + fn index(&self, index: isize) -> &Self::Output { + &self.0[index.rem_euclid(self.0.len() as isize) as usize] + } +} + +impl FromIterator

for Polyline

{ + fn from_iter>(it: I) -> Self { + Self::new(it) + } +} #[cfg(test)] mod tests { @@ -904,48 +1017,44 @@ mod tests { use super::*; - type Pt = Point<[f32; N], Real>; - - fn tri( - a: Pt, - b: Pt, - c: Pt, - ) -> Tri, ()>> { - Tri([a, b, c]).map(|p| vertex(p, ())) - } - #[test] fn triangle_winding_2_cw() { - let tri = tri(pt2(-1.0, 0.0), pt2(0.0, 1.0), pt2(1.0, -1.0)); + let tri = tri::(pt2(-1.0, 0.0), pt2(0.0, 1.0), pt2(1.0, -1.0)); assert_eq!(tri.winding(), Winding::Cw); } #[test] fn triangle_winding_2_ccw() { - let tri = tri(pt2(-2.0, 0.0), pt2(1.0, 0.0), pt2(0.0, 1.0)); + let tri = tri::(pt2(-2.0, 0.0), pt2(1.0, 0.0), pt2(0.0, 1.0)); assert_eq!(tri.winding(), Winding::Ccw); } #[test] fn triangle_winding_3_cw() { - let tri = - tri(pt3(-1.0, 0.0, 0.0), pt3(0.0, 1.0, 1.0), pt3(1.0, -1.0, 0.0)); - assert_eq!(tri.winding(), Winding::Cw); + let tri = tri::( + pt3(-1.0, 0.0, 0.0), + pt3(0.0, 1.0, 1.0), + pt3(1.0, -1.0, 0.0), + ); + assert_eq!(tri.winding_xy(), Winding::Cw); } #[test] fn triangle_winding_3_ccw() { - let tri = - tri(pt3(-1.0, 0.0, 0.0), pt3(1.0, 0.0, 0.0), pt3(0.0, 1.0, -1.0)); - assert_eq!(tri.winding(), Winding::Ccw); + let tri = tri::( + pt3(-1.0, 0.0, 0.0), + pt3(1.0, 0.0, 0.0), + pt3(0.0, 1.0, -1.0), + ); + assert_eq!(tri.winding_xy(), Winding::Ccw); } #[test] fn triangle_area_2() { - let tri = tri(pt2(-1.0, 0.0), pt2(2.0, 0.0), pt2(2.0, 1.0)); + let tri = tri::(pt2(-1.0, 0.0), pt2(2.0, 0.0), pt2(2.0, 1.0)); assert_eq!(tri.area(), 1.5); } #[test] fn triangle_area_3() { // base = 3, height = 2 - let tri = tri( + let tri = tri::( pt3(-1.0, 0.0, -1.0), pt3(2.0, 0.0, -1.0), pt3(0.0, 0.0, 1.0), @@ -955,7 +1064,7 @@ mod tests { #[test] fn triangle_plane() { - let tri = tri( + let tri = tri::( pt3(-1.0, -2.0, -1.0), pt3(2.0, -2.0, -1.0), pt3(0.0, -2.0, 1.0), @@ -1081,7 +1190,7 @@ mod tests { assert_eq!(format!("{l:?}"), "Line(x = 0)"); l = Line2::from_points(pt2(2.0, 0.0), pt2(2.0, -1.0)); - assert_eq!(l.coeffs(), [1.0, 0.0, -2.0]); + assert_approx_eq!(l.coeffs(), [1.0, 0.0, -2.0]); assert_eq!(format!("{l:?}"), "Line(x = 2)"); l = Line2::new(1.0, 0.0, 2.0); // x = 2 diff --git a/core/src/lib.rs b/core/src/lib.rs index 0a5b3e3c..30483c07 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -67,6 +67,6 @@ pub mod prelude { }, math::re_exports::*, render::re_exports::*, - util::buf::{AsMutSlice2, AsSlice2, Buf2, MutSlice2, Slice2}, + util::re_exports::*, }; } diff --git a/core/src/math.rs b/core/src/math.rs index 20e20b67..4ed2457a 100644 --- a/core/src/math.rs +++ b/core/src/math.rs @@ -77,6 +77,7 @@ pub mod color; pub mod float; pub mod grad; pub mod mat; +pub mod noise; pub mod param; pub mod point; pub mod rand; @@ -105,6 +106,7 @@ pub trait Lerp: Clone + Debug + Sized { /// /// assert_eq!(f32::lerp(&1.0, &5.0, 0.25), 2.0); /// ``` + #[must_use] fn lerp(&self, other: &Self, t: f32) -> Self; /// Returns the (unweighted) average of `self` and `other`. @@ -117,6 +119,7 @@ pub trait Lerp: Clone + Debug + Sized { /// let b = pt2(3.0, -2.0); /// assert_eq!(a.midpoint(&b), pt2(1.0, 0.0)); /// ``` + #[must_use] fn midpoint(&self, other: &Self) -> Self { self.lerp(other, 0.5) } @@ -150,11 +153,12 @@ 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) } /// The square root of three. -pub const SQRT_3: f32 = 1.7320508; +pub const SQRT_3: f32 = 1.732_050_8; impl Lerp for T where @@ -198,8 +202,14 @@ where } } +impl Lerp for [T; N] { + fn lerp(&self, other: &Self, t: f32) -> Self { + core::array::from_fn(|i| self[i].lerp(&other[i], t)) + } +} + impl Lerp for () { - fn lerp(&self, _: &Self, _: f32) {} + fn lerp(&self, (): &(), _: f32) {} } impl Lerp for (U, V) { diff --git a/core/src/math/approx.rs b/core/src/math/approx.rs index 606b3341..c7a46eb7 100644 --- a/core/src/math/approx.rs +++ b/core/src/math/approx.rs @@ -35,6 +35,17 @@ pub trait ApproxEq { /// /// This means that `self` is either strictly contained in the range /// or approximately equal to one of the endpoints. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::ApproxEq; + /// + /// assert!(1.0.approx_in(0.0..2.0)); + /// assert!((-0.000001).approx_in(0.0..2.0)); + /// assert!(2.000001.approx_in(0.0..2.0)); + /// + /// assert!(!2.001.approx_in(0.0..2.0)); + /// ``` fn approx_in(&self, rg: Range) -> bool where Self: PartialOrd + Sized, @@ -44,6 +55,17 @@ pub trait ApproxEq { } /// Returns whether `self` is less than or approximately equal to a value. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::ApproxEq; + /// + /// assert!(1.0.approx_le(&2.0)); + /// assert!(2.0.approx_le(&2.0)); + /// assert!(2.000001.approx_le(&2.0)); + /// + /// assert!(!2.001.approx_le(&2.0)); + /// ``` fn approx_le(&self, other: &Self) -> bool where Self: PartialOrd, @@ -52,6 +74,19 @@ pub trait ApproxEq { } /// Returns whether `self` is greater than or approximately equal to a value. + /// + /// TODO should be renamed to `approx_ge`! + /// + /// # Examples + /// ``` + /// use retrofire_core::math::ApproxEq; + /// + /// assert!(2.0.approx_gt(&1.0)); + /// assert!(1.0.approx_gt(&1.0)); + /// assert!(0.9999999.approx_gt(&1.0)); + /// + /// assert!(!0.999.approx_gt(&1.0)); + /// ``` fn approx_gt(&self, other: &Self) -> bool where Self: PartialOrd, diff --git a/core/src/math/color.rs b/core/src/math/color.rs index 34dccc7f..ad8c6d90 100644 --- a/core/src/math/color.rs +++ b/core/src/math/color.rs @@ -192,6 +192,15 @@ impl Color { impl Color<[Ch; N], Sp> { /// Returns `self` with each channel mapped with the given function. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::color::rgb; + /// + /// let cyan = rgb(0.2,0.8,1.0); + /// let darker_cyan = cyan.map(|ch| ch * 0.5); + /// assert_eq!(darker_cyan, rgb(0.1, 0.4, 0.5)); + /// ``` #[inline] pub fn map(&self, f: impl FnMut(Ch) -> C) -> Color<[C; N], Sp> where @@ -208,11 +217,13 @@ impl Color<[f32; N], Sp> { /// /// # Examples /// ``` - /// use retrofire_core::math::color::{Color3f, gray, rgb}; - /// let c: Color3f = rgb(-0.1, 0.5, 1.2); + /// use retrofire_core::math::color::*; /// - /// let clamped = c.clamp(&gray(0.0), &gray(1.0)); + /// let out_of_bounds = rgb(-0.1, 0.5, 1.2); + /// + /// let clamped = out_of_bounds.clamp(&gray(0.0), &gray(1.0)); /// assert_eq!(clamped, rgb(0.0, 0.5, 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. @@ -226,6 +237,14 @@ impl Color<[f32; N], Sp> { impl Color3 { /// Returns `self` as RGBA, with alpha set to 0xFF (fully opaque). + /// + /// # Examples + /// ``` + /// use retrofire_core::math::color::*; + /// + /// let red = rgb(0xFF, 0, 0); + /// assert_eq!(red.to_rgba(), rgba(0xFF, 0, 0, 0xFF)) + /// ``` #[inline] pub const fn to_rgba(self) -> Color4 { let [r, g, b] = self.0; @@ -234,8 +253,16 @@ impl Color3 { /// Returns `self` as floating-point RGB, with channels normalized /// to the range [0, 1]. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::color::rgb; + /// + /// let orange = rgb(0xE0, 0x80, 0); + /// assert_eq!(orange.to_color3f(), rgb(0.875, 0.5, 0.0)); + /// ``` #[inline] - pub fn to_color3f(self) -> Color3f { + pub const fn to_color3f(self) -> Color3f { let [r, g, b] = self.0; rgb(r as f32 / 256.0, g as f32 / 256.0, b as f32 / 256.0) } @@ -291,6 +318,16 @@ impl Color4 { rgb(r, g, b) } + /// Returns `self` as floating-point RGBA, with channels normalized + /// to the range [0, 1]. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::color::rgba; + /// + /// let orange = rgba(0xE0, 0x80, 0, 0x40); + /// assert_eq!(orange.to_color4f(), rgba(0.875, 0.5, 0.0, 0.25)); + /// ``` #[inline] pub const fn to_color4f(self) -> Color4f { let [r, g, b, a] = self.0; @@ -350,6 +387,16 @@ impl Color3f { /// [converted to sRGB][1] right before writing to the output. Conversion, /// however, incurs a small performance penalty. /// + /// # Examples + /// ``` + /// use retrofire_core::math::color::*; + /// + /// let cyan = rgb(0.0, 0.8, 1.0); + /// let linear_cyan: Color3f = [0.0, 0.6120656, 1.0].into(); + /// + /// assert_eq!(cyan.to_linear(), linear_cyan); + /// ``` + /// /// [1]: Color3f::to_srgb() #[cfg(feature = "fp")] #[inline] @@ -457,6 +504,16 @@ impl Color3f { /// before interpolation, and right before writing to the output. /// Conversion, however, incurs a small performance penalty. /// + /// # Examples + /// ``` + /// use retrofire_core::math::color::*; + /// + /// let linear_cyan: Color3f = [0.0, 0.8, 1.0].into(); + /// let gamma_cyan = rgb(0.0, 0.90354544, 1.0); + /// + /// assert_eq!(linear_cyan.to_srgb(), gamma_cyan); + /// ``` + /// /// [1]: Color3f::to_linear() #[cfg(feature = "fp")] #[inline] diff --git a/core/src/math/float.rs b/core/src/math/float.rs index 7f11d18e..b081a921 100644 --- a/core/src/math/float.rs +++ b/core/src/math/float.rs @@ -6,6 +6,7 @@ //! it also implements a critical subset of the functions even if none of //! the features is enabled. +/// Floating-point functions delegating to software implementations in `libm`. #[cfg(feature = "libm")] pub mod libm { pub use libm::floorf as floor; @@ -32,6 +33,8 @@ pub mod libm { } } +/// Floating-point functions delegating to approximate implementations +/// in the `micromath` library. #[cfg(feature = "mm")] pub mod mm { use micromath::F32Ext as mm; @@ -104,25 +107,82 @@ pub mod mm { } } +/// Fallback implementations of required floating-point functions +/// used if none of the fp features is enabled. +//#[cfg(not(feature = "fp"))] pub mod fallback { - use crate::math::float::fast_recip_sqrt; + use core::hint::cold_path; /// Returns the largest integer less than or equal to `x`. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::float::fallback::floor; + /// + /// assert_eq!(floor(1.0), 1.0); + /// assert_eq!(floor(1.9), 1.0); + /// + /// assert_eq!(floor(-1.0), -1.0); + /// assert_eq!(floor(-1.1), -2.0); + /// ``` #[inline] pub fn floor(x: f32) -> f32 { - (x as i64 - (x < 0.0) as i64) as f32 + let xi = x as i64 as f32; + if xi > x { xi - 1.0 } else { xi } } + /// Returns the least non-negative remainder of `x` (mod `m`). + /// + /// # Examples + /// ``` + /// use retrofire_core::math::float::fallback::rem_euclid; + /// + /// assert_eq!(rem_euclid(3.0, 4.0), 3.0); + /// assert_eq!(rem_euclid(4.0, 4.0), 0.0); + /// assert_eq!(rem_euclid(5.5, 4.0), 1.5); + /// assert_eq!(rem_euclid(-3.5, 4.0), 0.5); + /// ``` #[inline] pub fn rem_euclid(x: f32, m: f32) -> f32 { let r = x % m; r + if r < 0.0 { m.abs() } else { 0.0 } } - /// Returns the approximate reciprocal of the square root of `x`. + + /// Returns the (approximate) reciprocal square root of a number. + /// + /// If the argument is zero or negative, returns infinity or NaN respectively. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::float::fallback::recip_sqrt; + /// + /// assert_eq!(recip_sqrt(4.0), 0.49999782); + /// assert_eq!(recip_sqrt(9.0), 0.3333327); + /// assert_eq!(recip_sqrt(0.0), f32::INFINITY); + /// ``` #[inline] pub fn recip_sqrt(x: f32) -> f32 { - fast_recip_sqrt(x) + if x > 0.0 { + let y = super::fast_recip_sqrt(x); + return y * (1.5 - 0.5 * x * y * y); + } + cold_path(); + if x == 0.0 { f32::INFINITY } else { f32::NAN } } + + /// Returns the (approximate) square root of a number. + /// + /// If the argument is negative, returns NaN. + /// + /// # Example + /// ``` + /// use retrofire_core::math::float::fallback::sqrt; + /// + /// assert_eq!(sqrt(0.0), 0.0); + /// assert_eq!(sqrt(4.0), 2.0000088); + /// assert_eq!(sqrt(9.0), 3.0000057); + /// assert!(sqrt(-1.0).is_nan()); + /// ``` #[inline] pub fn sqrt(x: f32) -> f32 { 1.0 / recip_sqrt(x) @@ -130,8 +190,19 @@ pub mod fallback { } /// Returns a fast approximation of the reciprocal square root of a number. +/// +/// If the argument is zero or negative, the return value is unspecified. +/// +/// # Example +/// ``` +/// use retrofire_core::math::float::fast_recip_sqrt; +/// +/// assert_eq!(fast_recip_sqrt(4.0), 0.49915406); +/// assert_eq!(fast_recip_sqrt(0.25), 1.9966162); +/// +/// ``` #[inline] -pub fn fast_recip_sqrt(x: f32) -> f32 { +pub const 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)); @@ -147,6 +218,7 @@ pub type f32 = core::primitive::f32; #[allow(unused)] pub(crate) trait RecipSqrt { + /// Returns the reciprocal square root (1/√x) of a number. fn recip_sqrt(x: Self) -> Self; } @@ -225,9 +297,9 @@ mod tests { assert_approx_eq!(mm::sqrt(9.0), 3.0); assert_eq!(mm::sqrt(16.0), 4.0); assert!(mm::sqrt(-1.0).is_nan()); - assert_approx_eq!(mm::recip_sqrt(9.0), 1.0 / 3.0); + assert_approx_eq!(mm::recip_sqrt(9.0), 1.0 / 3.0, eps = 1e-3); // mm doesn't check for zero, just gives a big number - assert_approx_eq!(mm::recip_sqrt(0.0), 1.9818e19); + assert_approx_eq!(mm::recip_sqrt(0.0), 1.9818029e19); // mm doesn't check for negative, panics due to sub overflow //assert!(mm::recip_sqrt(-1.0).is_nan()); diff --git a/core/src/math/mat.rs b/core/src/math/mat.rs index 042300b7..945ebd14 100644 --- a/core/src/math/mat.rs +++ b/core/src/math/mat.rs @@ -1,12 +1,186 @@ -//! Matrices and linear and affine transforms. +//! Matrices and linear, affine, and projective transforms. //! -//! TODO Docs +//! Retrofire matrices encode their source and target spaces (coordinate frames) +//! in their types. For example, a `Mat4` may only be used to map +//! points in model space (`Point3`) to points in view space +//! (`Point3`). Similarly, matrices can only be composed (multiplied) +//! if they have compatible source and target spaces. +//! +//! The generic "base" matrix type [`Matrix`](Matrix) rarely needs +//! to be named directly; in normal use only the type aliases [`Mat2`], [`Mat3`], +//! [`Mat4`], and [`ProjMat3`] are needed. These represent 2x2, 3x3, and 4x4 real +//! matrices and 4x4 projective matrices respectively. +//! +//! # Creating matrices +//! +//! ``` +//! use retrofire_core::mat; +//! use retrofire_core::math::{ +//! Mat2, Mat3, Mat4, Vec3, pt3, vec3, +//! }; +//! use retrofire_core::render::{Model, World, View}; +//! +//! // Identity matrix: +//! let id: Mat4 = Mat4::identity(); +//! +//! // () denotes a generic "don't care" frame: +//! let id = Mat4::<()>::identity(); +//! +//! // The source parameter defaults to (); the target parameter defaults to +//! // the source parameter: +//! let id: Mat4 = Mat4::identity(); +//! let id = ::identity(); +//! +//! // Note that this way is ambiguous due to the way Rust resolves methods: +//! // let id = Mat4::identity(); +//! +//! // The `Default` impl gives the identity matrix: +//! let also_id = ::default(); +//! +//! assert_eq!(id, also_id); +//! +//! // From an array: +//! let from_array = ::new([[0.0, 2.0], [3.0, 0.0]]); +//! +//! // With a macro: +//! let from_macro: Mat2 = mat![ +//! 0.0, 2.0; +//! 3.0, 0.0; +//! ]; +//! +//! assert_eq!(from_array, from_macro); +//! +//! // From linear basis vectors: +//! let reflect_x = ::from_linear(-Vec3::X, Vec3::Y, Vec3::Z); +//! +//! // From an affine basis (basis vectors plus origin point): +//! let glide_reflect = ::from_affine( +//! -Vec3::X, Vec3::Y, Vec3::Z, pt3(0.0, 2.0, 0.0) +//! ); +//! ``` +//! +//! # Elementary affine transforms +//! ``` +//! # use retrofire_core::math::*; +//! # use retrofire_core::render::*; +//! use retrofire_core::math::{scale, rotate_x, rotate, translate}; +//! +//! // `scale` takes anything that's `Into`: +//! let sc = scale((1.0, 2.0, 3.0)); +//! let sc = scale(vec3(1.0, 2.0, 3.0)); +//! let sc_uniform = scale(3.0); +//! +//! // Rotation about one of the cardinal axes: +//! let rot_x = rotate_x(degs(90.0)); +//! +//! // Rotation about an arbitrary axis: +//! let rot_arb = rotate(vec3(1.0, 1.0, 0.0), degs(30.0)); +//! +//! // Translation: +//! let tr = translate((1.0, 2.0, 3.0)); +//! let tr_along_z = translate(4.0 * Vec3::Z); +//! +//! // The transform constructors return "()" matrices to avoid type inference +//! // ambiguities. Coerce to the desired mapping with the .to() method: +//! let model_to_view: Mat4 = translate((0.0, 0.0, -4.0)).to(); +//! ``` +//! +//! ## Projection and viewport transforms +//! +//! The `perspective` and `orthographic` functions return view-to-clip space +//! projective matrices. The `viewport` function returns NDC-to-screen space +//! matrices. +//! ``` +//! # use retrofire_core::math::*; +//! +//! // Focal ratio, aspect ratio, and the near-far plane distances. +//! let persp /*: ProjMat3 */ = perspective(1.0, 1.0, 0.1..1000.0); +//! +//! // Left-bottom-near and right-top-far corners of the orthographic clip box. +//! let ortho /*: ProjMat3 */ = orthographic( +//! pt3(-2.0, -1.0, -1.0), +//! pt3(2.0, 1.0, 1.0) +//! ); +//! +//! let viewp /*: Mat4 */ = viewport(pt2(10, 10)..pt2(630, 470)); +//! ``` +//! +//! ## Applying transforms to vectors and points +//! ``` +//! # use retrofire_core::math::*; +//! # let sc = scale((1.0, 2.0, 3.0)); +//! # let tr = translate((1.0, 2.0, 3.0)); +//! +//! let v = vec3(0.0, 1.0, -1.0); +//! +//! assert_eq!(sc.apply(&v), vec3(0.0, 2.0, -3.0)); +//! +//! // In most cases it is unnecessary to manually handle 4D homogeneous +//! // vectors, it is managed by the types. Translations do not affect vectors: +//! assert_eq!(tr.apply(&v), v); +//! // But they affect points: +//! let p = pt3(0.0, 1.0, -1.0); +//! assert_eq!(tr.apply(&p), pt3(1.0, 3.0, 2.0)); +//! ``` +//! +//! ## Composing transforms +//! +//! Transforms can be composed with the methods [`then`][Mat4::then] and +//! [`compose`][Mat4::compose]: +//! ``` +//! # use retrofire_core::math::*; +//! # let sc = scale((1.0, 2.0, 3.0)); +//! # let tr = translate((1.0, 2.0, 3.0)); +//! +//! let scale_then_translate = sc.then(&tr); +//! let translate_then_scale = sc.compose(&tr); +//! +//! let p = pt3(0.0, 1.0, -1.0); +//! assert_eq!(scale_then_translate.apply(&p), pt3(1.0, 4.0, 0.0)); +//! assert_eq!(translate_then_scale.apply(&p), pt3(1.0, 6.0, 6.0)); +//! ``` +//! +//! ## Matrix properties +//! ``` +//! # use retrofire_core::{*, math::*}; +//! # let sc = scale((1.0, 2.0, 3.0)); +//! # let tr = translate((1.0, 2.0, 3.0)); +//! +//! // Row and col vectors +//! assert_eq!(sc.col_vec(1), [0.0, 2.0, 0.0, 0.0].into()); +//! assert_approx_eq!(tr.row_vec(2), [0.0, 0.0, 1.0, 3.0].into()); +//! +//! // Determinant +//! assert_eq!(sc.determinant(), 1.0 * 2.0 * 3.0); +//! assert_approx_eq!(tr.determinant(), 1.0); +//! +//! // Inversion +//! assert_eq!(sc.inverse(), scale((1.0, 1.0/2.0, 1.0/3.0))); +//! assert_eq!(tr.inverse(), translate((-1.0, -2.0, -3.0))); +//! +//! assert_eq!(sc.then(&sc.inverse()), Mat4::identity()); +//! +//! // Checked inversion, returning None if singular: +//! let singular: Mat2 = mat![0.0, 1.0; 0.0, 2.0]; +//! assert_eq!(singular.checked_inverse(), None::); +//! +//! // Decomposition into parts: +//! assert_eq!(sc.linear(), mat![ +//! 1.0, 0.0, 0.0; +//! 0.0, 2.0, 0.0; +//! 0.0, 0.0, 3.0; +//! ]); +//! assert_eq!(tr.translation(), vec3(1.0, 2.0, 3.0)); +//! // origin() is the same as translation(), but returns a point +//! assert_eq!(tr.origin(), pt3(1.0, 2.0, 3.0)); +//! ``` #![allow(clippy::needless_range_loop)] use core::{ array, fmt::{self, Debug, Formatter}, + hint::cold_path, marker::PhantomData as Pd, ops::Range, }; @@ -48,11 +222,14 @@ pub trait Apply { type Output; /// Applies this transform to a value. + /// + /// # Examples + /// For examples, see the [module documentation][self]. #[must_use] fn apply(&self, t: &T) -> Self::Output; } -/// A change of basis in real vector space of dimension `DIM`. +/// Mapping between frames in real vector space of dimension `DIM`. #[derive(Copy, Clone, Default, Eq, PartialEq)] pub struct RealToReal( Pd<(SrcBasis, DstBasis)>, @@ -67,18 +244,19 @@ pub struct RealToProj(Pd); #[derive(Copy, Eq, PartialEq)] pub struct Matrix(pub Repr, Pd); -/// Type alias for a 2x2 float matrix. +/// Type alias for a 2x2 linear matrix. pub type Mat2 = Matrix<[[f32; 2]; 2], RealToReal>; -/// Type alias for a 3x3 float matrix. +/// Type alias for a 3x3 affine matrix. pub type Mat3 = Matrix<[[f32; 3]; 3], RealToReal>; -/// Type alias for a 4x4 float matrix. +/// Type alias for a 4x4 affine matrix. pub type Mat4 = Matrix<[[f32; 4]; 4], RealToReal>; +/// Type alias for a 4x4 projective matrix. pub type ProjMat3 = Matrix<[[f32; 4]; 4], RealToProj>; // @@ -113,28 +291,29 @@ macro_rules! mat { impl Matrix { /// Returns a matrix with the given elements. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::Mat2; + /// + /// let m = ::new([[2.0, 0.0], [0.0, 3.0]]); + /// assert_eq!(m.0, [[2.0, 0.0], [0.0, 3.0]]); + /// ``` #[inline] pub const fn new(els: Repr) -> Self { Self(els, Pd) } - /// Returns a matrix equal to `self` but with mapping `M`. + /// Applies this matrix to an object. /// - /// This method can be used to coerce a matrix to a different - /// mapping in case it is needed to make types match. + /// This is an inherent helper method delegating to the appropriate + /// [`Apply`] implementation. #[inline] - pub const fn to(&self) -> Matrix - where - Repr: Copy, - { - Matrix::new(self.0) - } - #[inline] - pub fn apply(&self, t: &T) -> >::Output + pub fn apply(&self, obj: &T) -> >::Output where Self: Apply, { - Apply::apply(self, t) + Apply::apply(self, obj) } } @@ -143,9 +322,9 @@ where Sc: Linear + Copy, Map: LinearMap, { - /// Returns the row vector of `self` with index `i`. + /// Returns the row vector of `self` with the given index. /// - /// The returned vector is in space `Map::Source`. + /// The returned vector is in the *source* space of `Self`. /// /// # Panics /// If `i >= M`. @@ -156,14 +335,16 @@ where /// /// let m: Mat2 = mat![1.0, 2.0; 3.0, 4.0]; /// assert_eq!(m.row_vec(0), vec2(1.0, 2.0)); + /// assert_eq!(m.row_vec(1), vec2(3.0, 4.0)); + /// ``` #[inline] - pub fn row_vec(&self, i: usize) -> Vector<[Sc; N], Map::Source> { + pub const fn row_vec(&self, i: usize) -> Vector<[Sc; N], Map::Source> { Vector::new(self.0[i]) } - /// Returns the column vector of `self` with index `i`. + /// Returns the column vector of `self` with the given index. /// - /// The returned vector is in space `Map::Dest`. + /// The returned vector is in the *destination* space of `Self`. /// /// # Panics /// If `i >= N`. @@ -173,17 +354,42 @@ where /// use retrofire_core::{mat, math::{vec2, Mat2}}; /// /// let m: Mat2 = mat![1.0, 2.0; 3.0, 4.0]; + /// assert_eq!(m.col_vec(0), vec2(1.0, 3.0)); /// assert_eq!(m.col_vec(1), vec2(2.0, 4.0)); + /// ``` #[inline] - pub fn col_vec(&self, i: usize) -> Vector<[Sc; M], Map::Dest> { - Vector::new(self.0.map(|row| row[i])) + pub const fn col_vec(&self, i: usize) -> Vector<[Sc; M], Map::Dest> { + // Manual loop for constness... + let mut res = [self.0[0][i]; M]; // No traits in const + let mut j = 1; + while j < M { + res[j] = self.0[j][i]; + j += 1; + } + Vector::new(res) } } -impl - Matrix<[[Sc; N]; N], RealToReal> + +impl + Matrix<[[Sc; N]; N], RealToReal> { + /// Returns a matrix equal to `self` but mapping from `S` to `D`. + /// + /// This method can be used to coerce a matrix to a different mapping + /// in cases where it is needed to make types match. + #[must_use] + #[inline] + pub const fn to( + &self, + ) -> Matrix<[[Sc; N]; N], RealToReal> { + Matrix::new(self.0) + } + /// Returns `self` with its rows and columns swapped. /// + /// Note that this also swaps the source and destination spaces and thus + /// returns a matrix of a different type. + /// /// # Examples /// ``` /// use retrofire_core::{mat, math::{vec2, Mat2}}; @@ -193,9 +399,10 @@ impl /// assert_eq!(m.transpose(), mat![1.0, 3.0; /// 2.0, 4.0]); #[must_use] + #[inline] pub const fn transpose( mut self, - ) -> Matrix<[[Sc; N]; N], RealToReal> { + ) -> Matrix<[[Sc; N]; N], RealToReal> { const { assert!(N >= DIM, "map dimension >= matrix dimension") } transpose(&mut self.0); self.to() @@ -209,9 +416,7 @@ const fn transpose(a: &mut [[Sc; N]; N]) { while i < N { let mut j = i + 1; while j < N { - let tmp = a[i][j]; - a[i][j] = a[j][i]; - a[j][i] = tmp; + (a[i][j], a[j][i]) = (a[j][i], a[i][j]); j += 1; } i += 1; @@ -232,6 +437,20 @@ impl Matrix<[[f32; N]; N], Map> { /// It is the neutral element of matrix multiplication: /// **A · I** = **I · A** = **A**, as well as matrix-vector /// multiplication: **I·v** = **v**. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{Mat4, Vec3, vec3, scale}; + /// + /// let id = ::identity(); + /// + /// let v: Vec3 = vec3(0.0, -2.0, 1.0); + /// assert_eq!(id.apply(&v), v); + /// + /// let scale = scale((1.0, 2.0, 3.0)); + /// assert_eq!(scale.then(&id), scale); + /// assert_eq!(id.then(&scale), scale); + /// ``` pub const fn identity() -> Self { // Needs const traits to be more generic; // const array::map/from_fn for a nicer impl @@ -265,6 +484,18 @@ where /// (𝗠 ∘ 𝗡) 𝘃 = 𝗠(𝗡 𝘃) /// ``` /// for some matrices 𝗠 and 𝗡 and a vector 𝘃. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{scale, translate, pt3, Point3}; + /// + /// let sc = scale(2.0); + /// let tr = translate((1.0, 2.0, 3.0)); + /// + /// let pt: Point3 = pt3(0.0, -1.0, 1.0); + /// + /// assert_eq!(tr.compose(&sc).apply(&pt), tr.apply(&sc.apply(&pt))); + /// ``` #[inline] #[must_use] pub fn compose( @@ -291,6 +522,18 @@ where /// the resulting matrix is equivalent to first applying `self` and then /// `other`. The call `self.then(other)` is thus equivalent to /// `other.compose(self)`. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{scale, translate, pt3, Point3}; + /// + /// let sc = scale(2.0); + /// let tr = translate((1.0, 2.0, 3.0)); + /// + /// let pt: Point3 = pt3(0.0, -1.0, 1.0); + /// + /// assert_eq!(sc.then(&tr).apply(&pt), tr.apply(&sc.apply(&pt))); + /// ``` #[must_use] #[inline] pub fn then>( @@ -306,18 +549,53 @@ impl Mat2 { /// /// # Examples /// ``` - /// use retrofire_core::math::Mat2; + /// use retrofire_core::{mat, math::Mat2}; /// - /// let double: Mat2 = [[2.0, 0.0], [0.0, 2.0]].into(); + /// let double: Mat2 = mat![2.0, 0.0; 0.0, 2.0]; /// assert_eq!(double.determinant(), 4.0); /// - /// let singular: Mat2 = [[1.0, 0.0], [2.0, 0.0]].into(); + /// let flip_y: Mat2 = mat![1.0, 0.0; 0.0, -1.0]; + /// assert_eq!(flip_y.determinant(), -1.0); + /// + /// let singular: Mat2 = mat![1.0, 0.0; 2.0, 0.0]; /// assert_eq!(singular.determinant(), 0.0); /// ``` #[inline] pub const fn determinant(&self) -> f32 { let [[a, b], [c, d]] = self.0; - a * d - b * c + det2(a, b, c, d) + } + + /// Solves the system of linear equations + /// ```text + /// ⎛ a b ⎞ ⎛ x ⎞ = ⎛ u ⎞ + /// ⎝ c d ⎠ ⎝ y ⎠ = ⎝ v ⎠, + /// ``` + /// that is, + /// ```text + /// ax + by = u + /// cx + dy = v, + /// ``` + /// for x and y. Returns None if no unique solution exists. + /// + /// Uses Cramer's rule: + /// ```text + /// x = det ⎛ u b ⎞ / det A = (ud - bv) / (ad - bc) + /// ⎝ v d ⎠ + /// y = det ⎛ a u ⎞ / det A = (av - uc) / (ad - bc) + /// ⎝ c v ⎠ + /// ``` + pub fn solve(&self, uv: Vec2) -> Option> { + let det = self.determinant(); + if det.approx_eq(&0.0) { + return None; + } + let [u, v] = uv.0; + let [[a, b], [c, d]] = self.0; + let x = (u * d - b * v) / det; + let y = (a * v - u * c) / det; + + Some(vec2(x, y)) } /// Returns the [inverse][Self::inverse] of `self`, or `None` if `self` @@ -381,24 +659,70 @@ impl Mat2 { /// // This will panic /// let _ = singular.inverse(); /// ``` + #[inline] #[must_use] pub const fn inverse(&self) -> Mat2 { self.checked_inverse() .expect("matrix cannot be singular or near-singular") } + + /// Returns the affine 3x3 matrix corresponding to `self`. + /// + /// # Examples + /// ``` + /// use retrofire_core::{mat, math::Mat2}; + /// + /// let m: Mat2 = mat![0.0, 2.0; 3.0, 0.0]; + /// + /// assert_eq!(m.to_affine(), mat![ + /// 0.0, 2.0, 0.0; + /// 3.0, 0.0, 0.0; + /// 0.0, 0.0, 1.0 + /// ]); + /// ``` + #[inline] + pub const fn to_affine(&self) -> Mat3 { + Mat3::from_affine(self.col_vec(0), self.col_vec(1), pt2(0.0, 0.0)) + } } impl Mat3 { /// Constructs a matrix from a linear basis. /// /// The basis does not have to be orthonormal. + /// + /// # Examples + /// ``` + /// use retrofire_core::{mat, math::{Mat3, vec2}}; + /// + /// let m = ::from_linear(vec2(0.0, 2.0), vec2(3.0, 0.0)); + /// assert_eq!(m, mat![ + /// 0.0, 3.0, 0.0; + /// 2.0, 0.0, 0.0; + /// 0.0, 0.0, 1.0; + /// ]); + /// ``` pub const fn from_linear(i: Vec2, j: Vec2) -> Self { Self::from_affine(i, j, Point2::origin()) } - /// Constructs a matrix from an affine basis, or frame. + /// Constructs a matrix from an affine basis, also called a frame. /// /// The basis does not have to be orthonormal. + /// + /// # Examples + /// ``` + /// use retrofire_core::{mat, math::{Mat3, vec2, pt2}}; + /// + /// let m = ::from_affine( + /// vec2(0.0, 2.0), vec2(3.0, 0.0), pt2(4.0, 5.0)); + /// + /// assert_eq!(m, mat![ + /// 0.0, 3.0, 4.0; + /// 2.0, 0.0, 5.0; + /// 0.0, 0.0, 1.0; + /// ]); + /// ``` pub const fn from_affine( i: Vec2, j: Vec2, @@ -416,13 +740,18 @@ impl Mat3 { /// /// # Examples /// ``` - /// use retrofire_core::assert_approx_eq; - /// use retrofire_core::math::*; + /// use retrofire_core::{mat, math::*}; /// - /// // TODO translate2 does not exist (yet) - /// /*let m = rotate2(degs(90.0)).then(&translate3(1.0, 2.0, 3.0)); - /// let lin = m.linear(); - /// assert_approx_eq!(lin.apply(&pt2(1.0, 0.0, 0.0)), pt2(0.0, 0.0, -1.0));*/ + /// let m: Mat3 = mat![ + /// 1.0, 0.0, 3.0; + /// 0.0, 2.0, 4.0; + /// 0.0, 0.0, 1.0; + /// ]; + /// assert_eq!(m.linear(), mat![ + /// 1.0, 0.0; + /// 0.0, 2.0 + /// ]); + /// ``` pub const fn linear(&self) -> Mat2 { let [r, s, _] = self.0; mat![r[0], r[1]; s[0], s[1]] @@ -430,14 +759,20 @@ impl Mat3 { /// Returns the translation column vector of `self`. /// - /// # Example + /// Use [`origin`][Self::origin] to get the translation as a point. + /// + /// # Examples /// ``` - /// use retrofire_core::math::*; + /// use retrofire_core::{mat, math::*}; /// - /// // TODO translate2 does not exist (yet) - /// /*let trans = vec2(1.0, 2.0); - /// let m = rotate2(degs(45.0)).then(&translate(trans)); - /// assert_eq!(m.translation(), trans);*/ + /// let m: Mat3 = mat![ + /// 1.0, 0.0, 3.0; + /// 0.0, 2.0, 4.0; + /// 0.0, 0.0, 1.0; + /// ]; + /// assert_eq!(m.translation(), vec2(3.0, 4.0)); + /// ``` + #[inline] pub const fn translation(&self) -> Vec2 { let [r, s, _] = self.0; vec2(r[2], s[2]) @@ -445,24 +780,101 @@ impl Mat3 { /// Returns the translation column vector of `self` as a point. /// + /// Use [`translation`][1] to get the translation as a vector. + /// /// # Example + /// ``` + /// use retrofire_core::{mat, math::*}; /// - /// TODO + /// let m: Mat3 = mat![ + /// 1.0, 0.0, 3.0; + /// 0.0, 2.0, 4.0; + /// 0.0, 0.0, 1.0; + /// ]; + /// assert_eq!(m.origin(), pt2(3.0, 4.0)); + /// ``` + /// [1]: Self::translation + #[inline] pub const fn origin(&self) -> Point2 { self.translation().to_pt() } +} - /// Returns the determinant of `self`. - pub const fn determinant(&self) -> f32 { - let [a, b, c] = self.0[0]; +impl Mat3 { + pub const fn from_linear(i: Vec3, j: Vec3, k: Vec3) -> Self { + mat![ + i.x(), j.x(), k.x(); + i.y(), j.y(), k.y(); + i.z(), j.z(), k.z(); + ] + } + /// Returns the 4x4 affine equivalent of `self`. + pub const fn to_affine(&self) -> Mat4 { + let [[a, b, c], [d, e, f], [g, h, i]] = self.0; + mat![ + a, b, c, 0.0; + d, e, f, 0.0; + g, h, i, 0.0; + 0.0, 0.0, 0.0, 1.0; + ] + } - // assert!(g == 0.0 && h == 0.0 && i == 1.0); - // TODO If affine (as should be), reduces to: - // a * e - b * d + /// Solves the system of linear equations + /// ```text + /// ⎛ a b c ⎞ ⎛ x ⎞ = ⎛ t ⎞ + /// ⎜ d e f ⎟ ⎜ y ⎟ = ⎜ u ⎟ + /// ⎝ g h i ⎠ ⎝ z ⎠ = ⎝ v ⎠, + /// ``` + /// that is, + /// ```text + /// ax + by + cz = t + /// dx + ey + fz = u + /// gx + hy + iz = v, + /// ``` + /// for x, y, and z. Returns None if no unique solution exists. + /// + /// Uses Cramer's rule: + /// + /// ```text + /// ⎛ t b c ⎞ ⎛ a t c ⎞ ⎛ a b t ⎞ + /// (x, y, z) = ( det ⎜ u e f ⎟, det ⎜ d u f ⎟, det ⎜ d e u ⎟ ) / det A + /// ⎝ v h i ⎠ ⎝ g v i ⎠ ⎝ g h v ⎠ + /// ``` + pub fn solve(&self, tuv: Vec3) -> Option> { + let det = self.determinant(); + if det.approx_eq(&0.0) { + return None; + } + let [t, u, v] = tuv.0; + let [[a, b, c], [d, e, f], [g, h, i]] = self.0; + + let ax: Self = mat![t, b, c; u, e, f; v, h, i]; + let ay: Self = mat![a, t, c; d, u, f; g, v, i]; + let az: Self = mat![a, b, t; d, e, u; g, h, v]; - a * self.cofactor(0, 0) - + b * self.cofactor(0, 1) - + c * self.cofactor(0, 2) + Some(vec3(ax.determinant(), ay.determinant(), az.determinant()) / det) + } +} + +impl Mat3 { + #[inline] + const fn is_affine(&self) -> bool { + // No array == in const... + let affine = matches!(self.0[2], [0.0, 0.0, 1.0]); + if DIM == 2 { likely(affine) } else { affine } + } + + /// Returns the determinant of `self`. + #[inline] + pub fn determinant(&self) -> f32 { + let [[a, b, c], [d, e, _], _] = self.0; + if self.is_affine() { + det2(a, b, d, e) + } else { + a * self.cofactor(0, 0) + + b * self.cofactor(0, 1) + + c * self.cofactor(0, 2) + } } /// Returns the cofactor of the element at the given row and column. @@ -472,7 +884,7 @@ impl Mat3 { /// /// 1. Remove the given row and column from `self` to get a 2x2 submatrix; /// 2. Compute its determinant; - /// 3. If exactly one of `row` and `col` is even, multiply by -1. + /// 3. If `row` is even XOR `col` is even, multiply by -1. #[inline] const fn cofactor(&self, row: usize, col: usize) -> f32 { // This automatically takes care of the negation @@ -480,9 +892,12 @@ impl Mat3 { let r2 = (row + 2) % 3; let c1 = (col + 1) % 3; let c2 = (col + 2) % 3; - self.0[r1][c1] * self.0[r2][c2] - self.0[r1][c2] * self.0[r2][c1] + let m = self.0; + det2(m[r1][c1], m[r1][c2], m[r2][c1], m[r2][c2]) } + // TODO separate impls for DIM 2 and 3 + /// Returns the inverse of `self`, or `None` if `self` is singular. /// /// # Examples @@ -501,41 +916,57 @@ impl Mat3 { /// ])); /// ``` #[must_use] - pub const fn checked_inverse(&self) -> Option> { + pub fn checked_inverse(&self) -> Option> { let det = self.determinant(); if det.abs() < 1e-6 { return None; } - - // Inverse is transpose of cofactor matrix divided by determinant - let mut res = [[0.0; 3]; 3]; let r_det = 1.0 / det; - let mut i = 0; - while i < 3 { - res[i][0] = r_det * self.cofactor(0, i); - res[i][1] = r_det * self.cofactor(1, i); - res[i][2] = r_det * self.cofactor(2, i); - i += 1; - } - /*let c_a = self.cofactor(0, 0); // = e - let c_b = self.cofactor(0, 1); // = d - let c_c = self.cofactor(0, 2); // = 0 - let c_d = self.cofactor(1, 0); // = b - let c_e = self.cofactor(1, 1); // = a - let c_f = self.cofactor(1, 2); // = 0 - let c_g = self.cofactor(2, 0); // = b * f - c * e - let c_h = self.cofactor(2, 1); // = a * f - c * d - let c_i = self.cofactor(2, 2); // = a * e - b * d*/ - Some(Mat3::new(res)) + // Inverse is transpose of cofactor matrix divided by determinant: + // + // 1 ⎛ co(a) co(d) co(g) ⎞ + // --- ⎜ co(b) co(e) co(h) ⎟ + // det ⎝ co(c) co(f) co(i) ⎠ + + if self.is_affine() { + // When (g, h, i) = (0, 0, 1), simplifies to: + // 1 ⎛ e -b bf-ce ⎞ + // ----- ⎜ -d a cd-af ⎟ + // ae-bd ⎝ 0 0 ae-bd ⎠ + // ^^^^^--- = 1 after div by det + + let [[a, b, c], [d, e, f], _] = self.0; + let a_ = a * r_det; + let b_ = b * r_det; + let d_ = d * r_det; + let e_ = e * r_det; + Some(mat![ + e_, -b_, det2(b_, c, e_, f); + -d_, a_, det2(c, a_, f, d_); + 0.0, 0.0, 1.0; + ]) + } else { + // No for or from_fn in const :( + let mut res = [[0.0; 3]; 3]; + let mut i = 0; + while i < 3 { + res[i][0] = r_det * self.cofactor(0, i); + res[i][1] = r_det * self.cofactor(1, i); + res[i][2] = r_det * self.cofactor(2, i); + i += 1; + } + Some(Mat3::new(res)) + } } - /// TODO + /// Returns the inverse of self. /// /// # Panics - /// If the matrix is singular or near-singular. + /// If the inverse does not exist (the matrix is singular or near-singular). + #[inline] #[must_use] - pub fn inverse(&self) -> Mat3 { + pub fn inverse(&self) -> Mat3 { self.checked_inverse() .expect("matrix cannot be singular or near-singular") } @@ -580,8 +1011,10 @@ impl Mat4 { /// let m = scale(5.0).then(&translate((1.0, 2.0, 3.0))); /// let pt = pt3(1.0, -1.0, 0.5); /// - /// // Only the scale is applied because the translate is not linear + /// // Only scaling is applied because translation is not linear /// assert_approx_eq!(m.linear().apply(&pt), pt3(5.0, -5.0, 2.5)); + /// ``` + #[inline] pub const fn linear(&self) -> Mat3 { let [r, s, t, _] = self.0; mat![ @@ -593,6 +1026,8 @@ impl Mat4 { /// Returns the translation column vector of `self`. /// + /// Use [`origin`][Self::origin] to get the translation as a point. + /// /// # Example /// ``` /// use retrofire_core::math::*; @@ -600,12 +1035,16 @@ impl Mat4 { /// let trans = vec3(1.0, 2.0, 3.0); /// let m = scale(5.0).then(&translate(trans)); /// assert_eq!(m.translation(), trans); + /// ``` + #[inline] pub const fn translation(&self) -> Vec3 { vec3(self.0[0][3], self.0[1][3], self.0[2][3]) } /// Returns the translation column vector of `self` as a point. /// + /// Use [`translation`][1] to get the translation as a vector. + /// /// # Example /// ``` /// use retrofire_core::math::*; @@ -613,6 +1052,9 @@ impl Mat4 { /// let trans = vec3(1.0, 2.0, 3.0); /// let m = scale(5.0).then(&translate(trans)); /// assert_eq!(m.origin(), pt3(1.0, 2.0, 3.0)); + /// ``` + /// [1]: Self::translation + #[inline] pub const fn origin(&self) -> Point3 { self.translation().to_pt() } @@ -626,23 +1068,29 @@ impl Mat4 { /// ⎜ i j k l ⎟ /// ⎝ m n o p ⎠ /// ``` - /// its determinant can be computed by multiplying each element *e* on row 0 - /// with its *minors*: the determinant of the submatrix obtained by removing - /// the row and column of *e*: + /// its determinant can be computed by multiplying each element *x* on some + /// row *n* with the determinant of its *minor*, the submatrix obtained by + /// removing the row and column of *x*. + /// + /// When M is affine, its determinant is exactly the determinant of its + /// top-right 3x3 submatrix. This is easy to show by choosing *n* = 3: /// ```text - /// ⎜ f g h ⎜ ⎜ e g h ⎜ - /// det(M) = a · ⎜ j k l ⎜ - b · ⎜ i k l ⎜ + c * ··· - d * ··· - /// ⎜ n o p ⎜ ⎜ m o p ⎜ + /// ⎜ b c d ⎜ ⎜ a c d ⎜ ⎜ a c d ⎜ + /// det(M) = 0 · ⎜ f g h ⎜ + 0 · ··· - 0 * ··· + 1 · ⎜ e g h ⎜ = ⎜ e g h ⎜ + /// ⎜ j k l ⎜ ⎜ i k l ⎜ ⎜ i k l ⎜ /// ``` pub fn determinant(&self) -> f32 { - let [[a, b, c, d], r, s, t] = self.0; - - let det2 = |m, n| s[m] * t[n] - s[n] * t[m]; - let det3 = - |j, k, l| r[j] * det2(k, l) - r[k] * det2(j, l) + r[l] * det2(j, k); - - a * det3(1, 2, 3) - b * det3(0, 2, 3) + c * det3(0, 1, 3) - - d * det3(0, 1, 2) + if self.is_affine() { + self.linear().determinant() + } else { + let [[a, b, c, d], r, s, t] = self.0; + let det2 = |m, n| s[m] * t[n] - s[n] * t[m]; + let det3 = |j, k, l| { + r[j] * det2(k, l) - r[k] * det2(j, l) + r[l] * det2(j, k) + }; + a * det3(1, 2, 3) - b * det3(0, 2, 3) + c * det3(0, 1, 3) + - d * det3(0, 1, 2) + } } #[must_use] @@ -665,13 +1113,14 @@ impl Mat4 { /// Only matrices with a nonzero determinant have a defined inverse. /// A matrix without an inverse is said to be singular. /// - /// Note: This method uses naive Gauss–Jordan elimination and may + /// This method has a fast path if `self` is affine (which it should + /// always be); otherwise it uses Gauss–Jordan elimination which may /// suffer from imprecision or numerical instability in certain cases. /// /// # Panics /// If debug assertions are enabled, panics if `self` is singular or - /// near-singular. If not enabled, the return value is unspecified and - /// may contain non-finite values (infinities and NaNs). + /// near-singular. Otherwise, the return value is unspecified and may + /// contain non-finite values (infinities and NaNs). // TODO example #[must_use] pub fn inverse(&self) -> Mat4 { @@ -702,6 +1151,15 @@ impl Mat4 { ); } + if self.is_affine() { + // M = LT <=> M^-1 = T^-1 L^-1 + let lin: Mat3<(), (), 3> = self.linear().to(); + let trans: Vec3 = self.translation().to(); + return translate(-trans) + .then(&lin.inverse().to_affine()) + .to(); + } + // This algorithm attempts to reduce `this` to the identity matrix // by simultaneously applying elementary row operations to it and // another matrix `inv` which starts as the identity matrix. Once @@ -735,9 +1193,9 @@ impl Mat4 { } // now in upper echelon form, back-substitute variables for &idx in &[3, 2, 1] { - let diag = this.0[idx][idx]; + let r_diag = this.0[idx][idx].recip(); for r in 0..idx { - let x = this.0[r][idx] / diag; + let x = this.0[r][idx] * r_diag; sub_row(this, idx, r, x); sub_row(inv, idx, r, x); @@ -752,6 +1210,26 @@ impl Mat4 { debug_assert!(inv.is_finite()); inv.to() } + + #[inline] + const fn is_affine(&self) -> bool { + // no array == in const + likely(matches!(self.0[3], [0.0, 0.0, 0.0, 1.0])) + } +} + +#[inline] +const fn likely(cond: bool) -> bool { + if !cond { + cold_path(); + } + cond +} + +/// Computes the determinant of the matrix [[a, b], [c, d]]. +#[inline] +const fn det2(a: f32, b: f32, c: f32, d: f32) -> f32 { + a * d - b * c } // @@ -788,10 +1266,12 @@ impl ApproxEq for Matrix where Repr: ApproxEq, { + #[inline] fn approx_eq_eps(&self, other: &Self, rel_eps: &E) -> bool { self.0.approx_eq_eps(&other.0, rel_eps) } + #[inline] fn relative_epsilon() -> E { Repr::relative_epsilon() } @@ -830,7 +1310,7 @@ impl Apply> for Mat2 { /// Mp = ⎛ M00 M01 ⎞ ⎛ v0 ⎞ = ⎛ v0' ⎞ /// ⎝ M10 M11 ⎠ ⎝ v1 ⎠ ⎝ v1' ⎠ /// ``` - #[inline(always)] + #[inline] fn apply(&self, pt: &Point2) -> Point2 { self.apply(&pt.to_vec()).to_pt() } @@ -914,7 +1394,7 @@ impl Apply> for Mat3 { /// M·P = ⎜ x1 y1 z1 ⎟ ⎜ p1 ⎟ = ⎜ p1' ⎟ /// ⎝ x2 y2 z2 ⎠ ⎝ p2 ⎠ ⎝ p2' ⎠ /// ``` - #[inline(always)] + #[inline] fn apply(&self, p: &Point3) -> Point3 { self.apply(&p.to_vec()).to_pt() } @@ -995,7 +1475,7 @@ impl Apply> for ProjMat3 { impl Clone for Matrix { fn clone(&self) -> Self { - self.to() + Self::new(self.0) } } @@ -1041,7 +1521,10 @@ impl From for Matrix { /// Returns a matrix applying a scaling by the given factors. /// /// # Examples -/// See the [`scale`] method for an example. +/// ``` +/// +/// +/// `` pub fn scale(factor: impl Into) -> Mat4 { let [x, y, z] = factor.into().0; mat![ @@ -1132,10 +1615,10 @@ fn orient(new_y: Vec3, new_z: Vec3) -> Mat4 { /// # Example /// ``` /// use retrofire_core::assert_approx_eq; -/// use retrofire_core::math::{Apply, degs, rotate_x, vec3}; +/// use retrofire_core::math::{Apply, degs, rotate_x, Vec3}; /// /// let m = rotate_x(degs(90.0)); -/// assert_approx_eq!(m.apply(&vec3(0.0, 1.0, 0.0)), vec3(0.0, 0.0, 1.0)); +/// assert_approx_eq!(m.apply(&Vec3::Y), Vec3::Z); /// ``` #[cfg(feature = "fp")] pub fn rotate_x(a: Angle) -> Mat4 { @@ -1147,15 +1630,15 @@ pub fn rotate_x(a: Angle) -> Mat4 { 0.0, 0.0, 0.0, 1.0; ] } -/// Returns a matrix applying a 3D rotation about the y-axis (on the xz plane). +/// Returns a matrix applying a 3D rotation about the y-axis (on the zx plane). /// /// # Example /// ``` /// use retrofire_core::assert_approx_eq; -/// use retrofire_core::math::{Apply, degs, rotate_y, vec3}; +/// use retrofire_core::math::{Apply, degs, rotate_y, Vec3}; /// /// let m = rotate_y(degs(90.0)); -/// assert_approx_eq!(m.apply(&vec3(1.0, 0.0, 0.0)), vec3(0.0, 0.0, -1.0)); +/// assert_approx_eq!(m.apply(&Vec3::X), -Vec3::Z); ///``` #[cfg(feature = "fp")] pub fn rotate_y(a: Angle) -> Mat4 { @@ -1171,10 +1654,11 @@ pub fn rotate_y(a: Angle) -> Mat4 { /// # Example /// ``` /// use retrofire_core::assert_approx_eq; -/// use retrofire_core::math::{Apply, degs, rotate_z, vec3}; +/// use retrofire_core::math::{Apply, degs, rotate_z, Vec3}; /// /// let m = rotate_z(degs(90.0)); -/// assert_approx_eq!(m.apply(&vec3(1.0, 0.0, 0.0)), vec3(0.0, 1.0, 0.0)); +/// assert_approx_eq!(m.apply(&Vec3::X), Vec3::Y); +/// ``` #[cfg(feature = "fp")] pub fn rotate_z(a: Angle) -> Mat4 { let (sin, cos) = a.sin_cos(); @@ -1204,6 +1688,9 @@ pub fn rotate_pyr(pitch: Angle, yaw: Angle, roll: Angle) -> Mat4 { } /// Returns a matrix applying a 2D rotation by an angle. +/// +/// # Examples +/// TODO #[cfg(feature = "fp")] pub fn rotate2(a: Angle) -> Mat3 { let (sin, cos) = a.sin_cos(); @@ -1215,6 +1702,9 @@ pub fn rotate2(a: Angle) -> Mat3 { } /// Returns a matrix applying a 3D rotation about an arbitrary axis. +/// +/// # Examples +/// TODO #[cfg(feature = "fp")] pub fn rotate(axis: Vec3, a: Angle) -> Mat4 { // 1. Change of basis such that `axis` is mapped to the z-axis, @@ -1246,6 +1736,9 @@ pub fn rotate(axis: Vec3, a: Angle) -> Mat4 { /// # Panics /// * If any parameter value is nonpositive. /// * If `near_far` is an empty range. +/// +/// # Examples +/// TODO pub const fn perspective( focal_ratio: f32, aspect_ratio: f32, @@ -1275,6 +1768,10 @@ pub const fn perspective( /// # Parameters /// * `lbn`: The left-bottom-near corner of the projection box. /// * `rtf`: The right-bottom-far corner of the projection box. +/// +/// # Examples +/// TODO +// TODO Take a Range like `viewport` does? Or have `viewport` take separate? pub const fn orthographic(lbn: Point3, rtf: Point3) -> ProjMat3 { // Done manually due until const traits are stable let [x0, y0, z0] = lbn.0; @@ -1295,6 +1792,9 @@ pub const fn orthographic(lbn: Point3, rtf: Point3) -> ProjMat3 { /// A viewport matrix is used to transform points from the NDC space to /// screen space for rasterization. NDC coordinates (-1, -1, _) are mapped /// to `bounds.start` and NDC coordinates (1, 1, _) to `bounds.end`. +/// +/// # Examples +/// TODO pub const fn viewport(bounds: Range) -> Mat4 { let Range { start, end } = bounds; let [x0, y0] = [start.x() as f32, start.y() as f32]; @@ -1323,9 +1823,6 @@ mod tests { #[derive(Debug, Default, Eq, PartialEq)] struct B2; - type Map = RealToReal; - type InvMap = RealToReal; - const X: Vec3 = Vec3::X; const Y: Vec3 = Vec3::Y; const Z: Vec3 = Vec3::Z; @@ -1342,7 +1839,7 @@ mod tests { } #[test] fn determinant_of_reflection_is_negative_one() { - let refl: Mat2 = [[0.0, 1.0], [1.0, 0.0]].into(); + let refl: Mat2 = mat![0.0, 1.0; 1.0, 0.0]; assert_eq!(refl.determinant(), -1.0); } @@ -1353,17 +1850,58 @@ mod tests { } #[test] fn inverse_of_inverse_is_original() { - let m: Mat2 = [[0.5, 1.5], [1.0, -0.5]].into(); + let m: Mat2 = mat![0.5, 1.5; 1.0, -0.5]; let m_inv: Mat2 = m.inverse(); assert_approx_eq!(m_inv.inverse(), m); } #[test] + fn inverse_of_singular_does_not_exist() { + let singular: Mat2 = mat![1.0, 0.0; 2.0, 0.0]; + assert_eq!(singular.checked_inverse(), None); + } + #[test] fn composition_of_inverse_is_identity() { - let m: Mat2 = [[0.5, 1.5], [1.0, -0.5]].into(); + let m: Mat2 = mat![0.5, 1.5; 1.0, -0.5]; let m_inv: Mat2 = m.inverse(); assert_approx_eq!(m.compose(&m_inv), Mat2::identity()); assert_approx_eq!(m.then(&m_inv), Mat2::identity()); } + + #[test] + fn solve_identity() { + // 1*x + 0*y = 4 + // 0*x + 1*y = -5 + let m: Mat2 = Mat2::identity(); + assert_eq!(m.solve(vec2(4.0, -5.0)), Some(vec2(4.0, -5.0))); + } + #[test] + fn solve_scale() { + // 2*x + 0*y = 4 + // 0*x - 3*y = 6 + let m: Mat2 = mat![2.0, 0.0; 0.0, -3.0]; + assert_eq!(m.solve(vec2(4.0, 6.0)), Some(vec2(2.0, -2.0))); + } + #[test] + fn solve_flip() { + // 0*x + 1*y = 2 + // 1*x + 0*y = 3 + let m: Mat2 = mat![0.0, 1.0; 1.0, 0.0]; + assert_eq!(m.solve(vec2(2.0, 3.0)), Some(vec2(3.0, 2.0))); + } + #[test] + fn solve_mixed() { + // 2*x - 3*y = 4 + // 5*x + 6*y = 7 + // <=> + // -4*x + 6*y = -8 + // 5*x + 6*y = 7 + // ---------------- + // -9*x = -15 + // x = 15/9 + // 3y = 2x - 4 <=> y = (2*15/9 - 4)/3 = (10 - 12)/9 = -2/9 + let m: Mat2 = mat![2.0, -3.0; 5.0, 6.0]; + assert_eq!(m.solve(vec2(4.0, 7.0)), Some(vec2(15.0, -2.0) / 9.0)); + } } mod mat3 { @@ -1451,6 +1989,15 @@ mod tests { ); } #[test] + fn inverse_of_singular_does_not_exist() { + let singular: Mat3 = mat![ + 1.0, 0.0, 0.0; + 2.0, 0.0, 0.0; + 0.0, 0.0, 1.0; + ]; + assert_eq!(singular.checked_inverse(), None); + } + #[test] fn matrix_composed_with_inverse_is_identity() { let mat: Mat3 = mat![ 1.0, -2.0, 2.0; @@ -1474,6 +2021,33 @@ mod tests { assert_approx_eq!(singular.checked_inverse(), None); } + #[test] + fn solve_identity() { + // 1*x + 0*y + 0*z = 3 + // 0*x + 1*y + 0*z = -4 + // 0*x + 0*y + 1*z = 5 + let m = Mat3::<(), (), 3>::identity(); + assert_eq!( + m.solve(vec3(3.0, -4.0, 5.0)), + Some(vec3(3.0, -4.0, 5.0)) + ); + } + #[test] + fn solve_scale() { + // 2*x + 0*y + 0*z = 1 + // 0*x - 3*y + 0*z = 3 + // 0*x + 0*y + 4*z = 2 + let m: Mat3<(), (), 3> = mat![ + 2.0, 0.0, 0.0; + 0.0, -3.0, 0.0; + 0.0, 0.0, 4.0; + ]; + assert_eq!( + m.solve(vec3(1.0, 3.0, 2.0)), + Some(vec3(0.5, -1.0, 0.5)) + ); + } + #[test] fn matrix_debug() { assert_eq!( @@ -1523,8 +2097,8 @@ mod tests { #[test] fn composition() { - let tr = translate((1.0, 2.0, 3.0)).to::(); - let sc = scale((3.0, 2.0, 1.0)).to::(); + let tr = translate((1.0, 2.0, 3.0)).to::(); + let sc = scale((3.0, 2.0, 1.0)).to::(); let tr_sc = tr.then(&sc); let sc_tr = sc.then(&tr); @@ -1542,6 +2116,28 @@ mod tests { ); } + #[test] + #[cfg(feature = "fp")] + fn inversion() { + let sc = scale((1.0, -2.0, 5.0)); + assert_eq!(sc.inverse(), scale((1.0, -0.5, 0.2))); + + let rot = rotate_x(degs(123.0)); + assert_approx_eq!(rot.inverse(), rotate_x(degs(-123.0))); + + let tr = translate((1.0, 2.0, -3.0)); + assert_eq!(tr.inverse(), translate((-1.0, -2.0, 3.0))); + + let sc_rot_trans = sc.then(&rot).then(&tr); + + assert_approx_eq!( + sc_rot_trans.inverse(), + translate((-1.0, -2.0, 3.0)) + .then(&rotate_x(degs(-123.0))) + .then(&scale((1.0, -0.5, 0.2))) + ); + } + #[test] fn scaling() { let m = scale((1.0, -2.0, 3.0)); @@ -1709,13 +2305,13 @@ mod tests { let m = orient_y(Y, X); assert_approx_eq!(m.apply(&X), X); - assert_eq!(m.apply(&X.to_pt()), X.to_pt()); + assert_approx_eq!(m.apply(&X.to_pt()), X.to_pt()); - assert_eq!(m.apply(&Y), Y); - assert_eq!(m.apply(&Y.to_pt()), Y.to_pt()); + assert_approx_eq!(m.apply(&Y), Y); + assert_approx_eq!(m.apply(&Y.to_pt()), Y.to_pt()); - assert_eq!(m.apply(&Z), Z); - assert_eq!(m.apply(&Z.to_pt()), Z.to_pt()); + assert_approx_eq!(m.apply(&Z), Z); + assert_approx_eq!(m.apply(&Z.to_pt()), Z.to_pt()); } #[test] @@ -1723,13 +2319,13 @@ mod tests { let m = orient_y(Z, X); assert_approx_eq!(m.apply(&X), X); - assert_eq!(m.apply(&X.to_pt()), X.to_pt()); + assert_approx_eq!(m.apply(&X.to_pt()), X.to_pt()); - assert_eq!(m.apply(&Y), Z); - assert_eq!(m.apply(&Y.to_pt()), Z.to_pt()); + assert_approx_eq!(m.apply(&Y), Z); + assert_approx_eq!(m.apply(&Y.to_pt()), Z.to_pt()); - assert_eq!(m.apply(&Z), -Y); - assert_eq!(m.apply(&Z.to_pt()), (-Y).to_pt()); + assert_approx_eq!(m.apply(&Z), -Y); + assert_approx_eq!(m.apply(&Z.to_pt()), (-Y).to_pt()); } #[test] @@ -1737,13 +2333,13 @@ mod tests { let m = orient_z(Y, X); assert_approx_eq!(m.apply(&X), X); - assert_eq!(m.apply(&X.to_pt()), X.to_pt()); + assert_approx_eq!(m.apply(&X.to_pt()), X.to_pt()); - assert_eq!(m.apply(&Y), -Z); - assert_eq!(m.apply(&Y.to_pt()), (-Z).to_pt()); + assert_approx_eq!(m.apply(&Y), -Z); + assert_approx_eq!(m.apply(&Y.to_pt()), (-Z).to_pt()); - assert_eq!(m.apply(&Z), Y); - assert_eq!(m.apply(&Z.to_pt()), Y.to_pt()); + assert_approx_eq!(m.apply(&Z), Y); + assert_approx_eq!(m.apply(&Z.to_pt()), Y.to_pt()); } #[test] @@ -1796,6 +2392,12 @@ mod tests { assert_approx_eq!(rot.determinant(), 1.0); } + #[test] + fn determinant_of_translation_is_one() { + let trans = translate((2.0, 3.0, 4.0)); + assert_eq!(trans.determinant(), 1.0); + } + #[test] fn matrix_composed_with_inverse_is_identity() { let m: Mat4 = translate((1.0e3, -2.0e2, 0.0)) @@ -1804,8 +2406,8 @@ mod tests { let m_inv: Mat4 = m.inverse(); - assert_eq!(m.compose(&m_inv), Mat4::identity()); - assert_eq!(m_inv.compose(&m), Mat4::identity()); + assert_eq!(m.then(&m_inv), >::identity()); + assert_eq!(m.compose(&m_inv), >::identity()); } #[test] diff --git a/core/src/math/noise.rs b/core/src/math/noise.rs new file mode 100644 index 00000000..51439a32 --- /dev/null +++ b/core/src/math/noise.rs @@ -0,0 +1,482 @@ +//! Procedural noise generation. +//! +//! This module implements two- and three-dimensional Perlin noise. + +use core::{array::from_fn, cell::Cell}; + +use super::{ + Lerp, Point, Point2, Point3, Vec2, Vec3, Vector, lerp, pt3, space::Real, + splat, spline::smoothstep_unit, vec2, vec3, +}; + +/// 2D-dimensional Perlin noise generator. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Perlin2 { + pub seed: u8, + + // Cache the grid points and gradients because usually several + // consecutive evaluations are likely to hit the same grid square + cache: Cell, +} + +/// 3-dimensional Perlin noise generator. +#[derive(Clone, Debug, Default, PartialEq)] +pub struct Perlin3 { + pub seed: u8, + + // Cache the grid points and gradients because usually several + // consecutive evaluations are likely to hit the same grid square + cache: Cell, +} + +#[derive(Copy, Clone, Debug, PartialEq)] +struct GridCell { + pts: [Point<[f32; DIM], Real>; N], + grads: [Vector<[f32; DIM], Real>; N], +} + +type GridCell2 = GridCell<2, 4>; +type GridCell3 = GridCell<3, 8>; + +impl Perlin2 { + pub fn new() -> Self { + Self::default() + } + + pub fn with_seed(seed: u8) -> Self { + Self { seed, ..Self::default() } + } + + /// Returns the Perlin noise value corresponding to a 2D point. + #[inline] + pub fn eval(&self, pt: Point2) -> f32 { + let GridCell { pts, grads } = self.grid_cell(pt); + + // Get the delta vectors from pt to the grid points + let deltas = pts.map(|p| pt - p); + + // Compute the dot products between gradients and deltas + let dots = from_fn(|i| grads[i].dot(&deltas[i])); + + // Smooth the interpolation variables + let tu = deltas[0].map(smoothstep_unit).0; + // Interpolate the final noise value at pt + bilerp(tu, dots) + } + + /// Returns the Perlin gradient vector corresponding to a 2D point. + /// + /// The vector is computed by taking the gradient vectors of all four + /// surrounding grid points and smoothly interpolating between them. + #[inline] + pub fn gradient(&self, pt: Point2) -> Vec2 { + let GridCell { pts, grads } = self.grid_cell(pt); + let tu = (pt - pts[0]).map(smoothstep_unit).0; + bilerp(tu, grads) + } + + #[inline] + fn grid_cell(&self, pt: Point2) -> GridCell2 { + use super::float::f32; + let pt0 = pt.map(f32::floor); + + let mut cached = self.cache.get(); + // Update cache if we're not in the same grid cell + if cached.pts[0] != pt0 { + // Find the four integer-coordinate points around pt + cached.pts = Self::grid_pts(pt0); + // Get the gradient vectors at the grid points + cached.grads = cached.pts.map(|p| self.grad(p)); + self.cache.set(cached); + } + cached + } + + /// Returns the four integer-coordinate grid points around a point. + #[inline] + fn grid_pts(pt0: Point2) -> [Point2; 4] { + [pt0, pt0 + Vec2::X, pt0 + Vec2::Y, pt0 + Vec2::X + Vec2::Y] + } + + /// Returns the gradient vector at a grid point. + #[inline] + fn grad(&self, pt: Point2) -> Vec2 { + let hash = PERM[pt.y() as i32 as u8 as usize]; + let hash = PERM[(pt.x() as i32 as u8).wrapping_add(hash) as usize]; + let hash = PERM[self.seed.wrapping_add(hash) as usize]; + GRADS_2[hash as usize & 0x7] + } +} +/// Gradient vectors for 2D noise. +static GRADS_2: [Vec2; 8] = const { + const A: f32 = 1.237; + const B: f32 = 0.513; + [ + vec2(A, B), + vec2(B, A), + vec2(-B, A), + vec2(-A, B), + vec2(-A, -B), + vec2(-B, -A), + vec2(B, -A), + vec2(A, -B), + ] +}; + +impl Perlin3 { + /// Returns the Perlin noise value corresponding to a 3D point. + // #[inline] Benchmarks appear to indicate that this reduces perf + pub fn eval(&self, pt: Point3) -> f32 { + let GridCell { pts, grads } = self.grid_cell(pt); + + // Get the delta vectors from pt to the grid points + let deltas = pts.map(|p| pt - p); + + // Compute the dot products between gradients and delts + let dots: [f32; 8] = from_fn(|i| grads[i].dot(&deltas[i])); + + // Smooth the interpolation variables + let tuv = deltas[0].map(smoothstep_unit).0; + + // Interpolate the final noise value at pt + trilerp(tuv, dots) + } + + /// Returns the Perlin gradient vector corresponding to a 3D point. + #[inline] + pub fn gradient(&self, pt: Point3) -> Vec3 { + let GridCell { pts, grads } = self.grid_cell(pt); + + // Smooth the interpolation variables + let tuv = (pt - pts[0]).map(smoothstep_unit).0; + // Interpolate the gradient at pt + trilerp(tuv, grads) + } + + #[inline] + fn grid_cell(&self, pt: Point3) -> GridCell3 { + use super::float::f32; + let pt0 = pt.map(f32::floor); + + let mut cached = self.cache.get(); + // Update cache if we're not in the same grid cell + if cached.pts[0] != pt0 { + // Find the four integer-coordinate points around pt + cached.pts = Self::grid_pts(pt0); + // Get the gradient vectors at the grid points + cached.grads = cached.pts.map(Self::grad); + self.cache.set(cached); + } + cached + } + + #[inline] + #[rustfmt::skip] + fn grid_pts(pt0: Point3) -> [Point3; 8] { + // + // 011 +--------------+ 111 + // / | / | + // / | / | + // 010 +--------------+ 110 | + // | | | | + // | 001 +--------|-----+ 101 + // | / | / y z + // | / | / | / + // 000 +--------------+ 100 O--- x + // + let [x0, y0, z0] = pt0.0; + let [x1, y1, z1] = (pt0 + splat(1.0)).0; + [ + pt3(x0, y0, z0), pt3(x0, y1, z0), pt3(x0, y0, z1), pt3(x0, y1, z1), + pt3(x1, y0, z0), pt3(x1, y1, z0), pt3(x1, y0, z1), pt3(x1, y1, z1), + ] + } + + #[inline] + fn grad(pt: Point3) -> Vec3 { + let [x, y, z] = pt.0; + let perm = perm(x as i32 + perm(y as i32 + perm(z as i32))); + GRADS_3[perm as usize & 0xF] + } +} +/// Gradient vectors for 3D noise. +static GRADS_3: [Vec3; 16] = [ + // YZ plane + vec3(0.0, -1.0, -1.0), + vec3(0.0, -1.0, 1.0), + vec3(0.0, 1.0, -1.0), + vec3(0.0, 1.0, 1.0), + // XZ plane + vec3(-1.0, 0.0, -1.0), + vec3(-1.0, 0.0, 1.0), + vec3(1.0, 0.0, -1.0), + vec3(1.0, 0.0, 1.0), + // XY plane + vec3(-1.0, -1.0, 0.0), + vec3(-1.0, 1.0, 0.0), + vec3(1.0, -1.0, 0.0), + vec3(1.0, 1.0, 0.0), + // Pad to power of two + vec3(1.0, 1.0, 0.0), + vec3(-1.0, 1.0, 0.0), + vec3(0.0, -1.0, 1.0), + vec3(0.0, -1.0, -1.0), +]; + +#[inline] +fn bilerp([t, u]: [f32; 2], [x00, x01, x10, x11]: [T; 4]) -> T { + lerp(t, x00, x01).lerp(&lerp(t, x10, x11), u) +} + +#[inline] +fn trilerp( + [t, u, v]: [f32; 3], + [v000, v001, v010, v011, v100, v101, v110, v111]: [V; 8], +) -> V { + bilerp( + [u, v], + [v000, v001, v010, v011].lerp(&[v100, v101, v110, v111], t), + ) +} + +#[inline] +fn perm(x: i32) -> i32 { + PERM[(x & 0xFF) as usize] as i32 +} + +/// Permutation table for calculating a pseudo-random index for each grid point. +#[rustfmt::skip] +static PERM: [u8; 256] = [ + 156, 2, 157, 90, 75, 199, 55, 167, 62, 92, 101, 253, 66, 134, 113, 83, + 1, 136, 78, 106, 254, 105, 248, 176, 234, 5, 195, 226, 49, 71, 87, 44, + 122, 94, 219, 140, 72, 159, 237, 212, 8, 162, 200, 124, 125, 69, 165, 74, + 245, 42, 89, 216, 158, 108, 238, 184, 217, 73, 126, 210, 14, 111, 19, 188, + 186, 45, 38, 223, 35, 112, 214, 26, 145, 95, 99, 193, 250, 189, 152, 182, + 166, 247, 148, 213, 168, 70, 96, 249, 127, 132, 4, 137, 41, 60, 102, 28, + 27, 240, 227, 155, 211, 230, 9, 80, 178, 3, 68, 153, 143, 84, 179, 181, + 12, 97, 103, 16, 225, 146, 63, 82, 203, 175, 163, 147, 11, 116, 185, 215, + 57, 120, 208, 129, 115, 198, 37, 201, 39, 98, 20, 183, 56, 118, 109, 142, + 138, 65, 117, 114, 160, 25, 43, 191, 204, 161, 22, 251, 139, 79, 131, 231, + 76, 0, 205, 206, 244, 51, 174, 13, 110, 85, 209, 77, 64, 53, 48, 221, + 133, 93, 224, 24, 33, 164, 23, 47, 171, 128, 243, 18, 52, 119, 149, 100, + 246, 233, 31, 192, 252, 190, 15, 172, 91, 229, 144, 54, 61, 58, 220, 36, + 222, 29, 50, 88, 121, 173, 232, 194, 239, 197, 32, 180, 107, 46, 7, 130, + 169, 81, 218, 67, 21, 170, 187, 59, 86, 235, 154, 123, 150, 177, 135, 228, + 104, 242, 6, 151, 255, 34, 30, 141, 202, 196, 236, 207, 241, 40, 17, 10 +]; + +impl Default for GridCell +where + [f32; DIM]: Default, +{ + fn default() -> Self { + Self { + pts: [Point::new([f32::NAN; DIM]); N], + grads: [Vector::default(); N], + } + } +} + +#[cfg(test)] +mod tests { + use alloc::string::String; + use core::fmt::Write; + + use crate::math::{pt2, pt3}; + + use super::*; + + #[derive(PartialEq, Debug)] + struct Stats { + total: f32, + avg: f32, + std: f32, + min: f32, + max: f32, + } + impl Stats { + fn new() -> Self { + Self { + total: 0.0, + avg: 0.0, + std: 0.0, + min: f32::MAX, + max: f32::MIN, + } + } + fn cum(&mut self, v: f32) { + self.total += 1.0; + self.avg += v; + self.std += v * v; + self.min = self.min.min(v); + self.max = self.max.max(v); + } + fn finish(self) -> Self { + Self { + avg: self.avg / self.total, + std: (self.std / self.total).sqrt(), + ..self + } + } + } + + #[test] + fn perlin2_statistics() { + let count = 1000u32; + let scale = 10.0; + let mut stats = Stats::new(); + let p = Perlin2::default(); + for i in 0..count { + for j in 0..count { + let pt = pt2(i as f32 / scale, j as f32 / scale); + let v = p.eval(pt); + stats.cum(v); + } + } + let stats = stats.finish(); + + assert_eq!( + stats, + Stats { + total: 1000000.0, + avg: -0.00024930664, + std: 0.28139114, + min: -0.8853002, + max: 0.8853002 + } + ); + } + #[test] + fn perlin3_statistics() { + let count = 100u32; + let scale = 10.0; + let mut stats = Stats::new(); + let p = Perlin3::default(); + for i in 0..count { + for j in 0..count { + for k in 0..count { + let pt = pt3(i, j, k).map(|c| c as f32 / scale); + let v = p.eval(pt); + stats.cum(v); + } + } + } + let stats = stats.finish(); + assert_eq!( + stats, + Stats { + total: 1000000.0, + avg: 0.0003959533, + std: 0.24879986, + min: -0.8853568, + max: 0.87813747 + } + ); + } + + const PALETTE: &[u8] = b" ..,:;=+*odO#%@WW"; + + #[test] + fn perlin2_pattern() { + #[rustfmt::skip] + static EXPECTED: &str = "\ +++++++++++++++++++oooooo+++:::,,,;;;++++++;;;;;; +oooooodddOOOOOOddddddooo+++;;;,,,,,,;;;======+++ +ooo***ddd###%%%OOOooo++++++===::::::;;;***oooddd ++++===***dddOOO***;;;;;;++++++===;;;+++ddd###OOO ++++;;;;;;++++++;;;,,,:::+++***+++===+++dddOOOooo ++++;;;:::::::::,,,,,,===oooddd***===+++oooooo=== ++++===:::,,,,,,:::===oooOOOOOO***===+++ooo***::: +++++++;;;:::;;;+++***ooodddddd+++===+++dddooo=== ++++***+++===+++oooooo+++++++++;;;;;;+++dddOOOooo +oooOOOdddooooooddd***===;;;;;;;;;:::;;;oooOOO### +OOO%%%###OOOOOOddd+++;;;;;;++++++===;;;===ddd%%% +ddd######OOOddd***;;;;;;+++dddOOOooo;;;;;;+++ddd ++++ooooooooo+++;;;,,,:::+++OOO%%%OOO+++;;;;;;+++ +;;;======;;;,,,......:::+++ddd###ddd+++===:::::: +;;;===;;;,,,......:::+++ooooooooo***++++++;;;,,, ++++***+++:::,,,;;;+++oooooo+++;;;;;;+++******=== +"; + const SIZE: usize = 16; + const SCALE: f32 = 4.0; + + let mut actual = String::new(); + let p = Perlin2::default(); + for i in 0..SIZE { + for j in 0..SIZE { + let pt = pt2(i as f32 / SCALE, j as f32 / SCALE); + let val = p.eval(pt) * 0.5 + 0.5; + + let ch = PALETTE[(val * PALETTE.len() as f32) as usize]; + _ = write!(actual, "{0}{0}{0}", ch as char); + } + _ = writeln!(actual); + } + assert_eq!(&actual, EXPECTED); + } + #[test] + fn perlin3_pattern() { + #[rustfmt::skip] + static EXPECTED: &str = "\ ++++++++++ooo+++...+++### +###***+++++++++===+++OOO ++++;;;+++;;;+++ooo+++;;; +...,,,+++;;;;;;===+++*** ++++++++++++++++;;;+++### +###%%%#########ooo+++=== ++++ooo+++++++++ooo+++;;; +;;;===+++===+++***++++++ + +ddd***===ooo***,,,+++### +###***===+++***;;;===ddd +===,,,;;;===ooo***;;;::: +...,,,+++============+++ +:::===ooo+++;;;:::===ddd +oooOOOdddOOOOOOooo+++;;; ++++***;;;***oooOOO***::: +++++++***+++***ooo***=== + +###***+++ooo+++...+++### +OOO+++===***+++,,,===ooo +;;;,,,;;;***ooo===;;;;;; + ...+++******+++++++++ +...;;;ooo+++;;;;;;;;;+++ ++++++++++oooooo***===,,, +++++++;;;ooooooOOOooo;;; +ooo*********+++******=== + +ddd+++***ooo===...+++### +***===+++ooo;;;...===ooo +;;;;;;===OOO***,,,====== +...,,,+++OOOOOOoooooo+++ +::::::***+++===+++;;;;;; +===::::::******+++;;;,,, +++++++===OOO***oooooo=== +ddd*********;;;===+++;;; + +"; + + const SIZE: usize = 8; + const SCALE: f32 = 2.0; + + let mut actual = String::new(); + let p = Perlin3::default(); + + for k in 0..4 { + for i in 0..SIZE { + for j in 0..SIZE { + let pt = + pt3(i as f32 / SCALE, j as f32 / SCALE, k as f32 / 4.0); + let val = p.eval(pt) * 0.5 + 0.5; + + let ch = PALETTE[(val * PALETTE.len() as f32) as usize]; + _ = write!(actual, "{0}{0}{0}", ch as char); + } + _ = writeln!(actual); + } + _ = writeln!(actual); + } + + assert_eq!(actual, EXPECTED); + } +} diff --git a/core/src/math/param.rs b/core/src/math/param.rs index 4fd6d4e8..9fe200aa 100644 --- a/core/src/math/param.rs +++ b/core/src/math/param.rs @@ -1,6 +1,6 @@ -use core::ops::Range; +use core::ops::{Range, RangeInclusive}; -use super::Lerp; +use super::{Lerp, Vary}; /// Represents a single-variable parametric curve. // TODO More documentation @@ -11,10 +11,30 @@ pub trait Parametric { /// The "canonical" domain of this function is `t` ∈ [0.0, 1.0], /// but implementations should return "reasonable" values outside /// the unit interval as well. - #[allow(unused)] fn eval(&self, t: f32) -> T; } +/// Returns an iterator that yields values from a parametric for a sequence +/// of regularly spaced *t* values in the (inclusive) range [0, 1]. +pub fn iter>( + param: &P, + n: usize, +) -> impl Iterator + Clone { + iter_range(param, n, 0.0..=1.0) +} + +/// Returns an iterator that yields values from a parametric for a sequence +/// of regularly spaced *t* values in the given inclusive range. +pub fn iter_range>( + param: &P, + n: usize, + r: RangeInclusive, +) -> impl Iterator + Clone { + r.start() + .vary_to(*r.end(), n as _) + .map(|t| param.eval(t)) +} + impl T, T> Parametric for F { /// Returns `self(t)`. fn eval(&self, t: f32) -> T { diff --git a/core/src/math/point.rs b/core/src/math/point.rs index 13b977ad..559607d5 100644 --- a/core/src/math/point.rs +++ b/core/src/math/point.rs @@ -341,6 +341,15 @@ impl PartialEq for Point { } } +impl From>> for Point<[Sc; 3], Real<3, B>> +where + Sc: Linear + Copy, +{ + fn from(pt: Point<[Sc; 2], Real<2, B>>) -> Self { + pt.to_pt3() + } +} + // Point <-> repr conversions impl From for Point { #[inline] diff --git a/core/src/math/space.rs b/core/src/math/space.rs index 946adefa..51fa49cb 100644 --- a/core/src/math/space.rs +++ b/core/src/math/space.rs @@ -26,11 +26,13 @@ pub trait Affine: Sized { /// Adds `diff` to `self` component-wise. /// /// `add` is commutative and associative. + #[must_use] fn add(&self, diff: &Self::Diff) -> Self; /// Subtracts `other` from `self`, returning the (signed) difference. /// /// `sub` is anti-commutative: `v.sub(w) == w.sub(v).neg()`. + #[must_use] fn sub(&self, other: &Self) -> Self::Diff; /// Returns an affine combination of points. @@ -75,6 +77,7 @@ pub trait Linear: Affine { /// Returns the additive inverse of `self`. #[inline] + #[must_use] fn neg(&self) -> Self { Self::zero().sub(self) } @@ -91,6 +94,7 @@ pub trait Linear: Affine { /// v.mul(a).add(&w.mul(a)) == v.add(&w).mul(a); /// v.mul(a).sub(&w.mul(a)) == v.add(&w).sub(&a); /// ``` + #[must_use] fn mul(&self, scalar: Self::Scalar) -> Self; } diff --git a/core/src/math/spline.rs b/core/src/math/spline.rs index b784bedf..d3e1e5e5 100644 --- a/core/src/math/spline.rs +++ b/core/src/math/spline.rs @@ -94,15 +94,27 @@ pub struct Euclidean(Spl, Vec<(f32, f32)>); /// /// Returns 0 for all `t` <= 0 and 1 for all `t` >= 1. Has a continuous /// first derivative. +#[inline] pub fn smoothstep(t: f32) -> f32 { - step(t, &0.0, &1.0, |t| t * t * (3.0 - 2.0 * t)) + step(t, &0.0, &1.0, smoothstep_unit) } /// Even smoother version of [`smoothstep`]. /// /// Has continuous first and second derivatives. +#[inline] pub fn smootherstep(t: f32) -> f32 { - step(t, &0.0, &1.0, |t| t * t * t * (10.0 + t * (6.0 * t - 15.0))) + step(t, &0.0, &1.0, smootherstep_unit) +} + +#[inline] +pub(crate) fn smoothstep_unit(t: f32) -> f32 { + t * t * (3.0 - 2.0 * t) +} + +#[inline] +pub(crate) fn smootherstep_unit(t: f32) -> f32 { + t * t * t * (10.0 + t * (6.0 * t - 15.0)) } /// Helper for defining step functions. diff --git a/core/src/math/vary.rs b/core/src/math/vary.rs index ec9fe73a..71609a85 100644 --- a/core/src/math/vary.rs +++ b/core/src/math/vary.rs @@ -95,11 +95,11 @@ impl Vary for () { type Iter = Iter<()>; type Diff = (); - fn vary(self, _: Self::Diff, n: Option) -> Self::Iter { + fn vary(self, (): (), n: Option) -> Iter<()> { Iter { val: (), step: (), n } } - fn dv_dt(&self, _: &Self, _: f32) {} - fn step(&self, _: &Self::Diff) {} + fn dv_dt(&self, (): &(), _: f32) {} + fn step(&self, (): &()) {} } impl ZDiv for () {} diff --git a/core/src/math/vec.rs b/core/src/math/vec.rs index 18bb600f..5ee51b8c 100644 --- a/core/src/math/vec.rs +++ b/core/src/math/vec.rs @@ -1,8 +1,81 @@ //! Real and projective vectors. //! -//! TODO +//! Vectors in Retrofire are parameterized by the "space", or coordinate frame, +//! that they are defined in. This helps rule out many bugs caused by +//! accidentally mixing vectors defined in different frames. //! +//! The most commonly used vector types are [`Vec2`] and [`Vec3`] as well as +//! their integer counterparts [`Vec2i`] and [`Vec3i`]. Unlike most similar +//! libraries, there's no `Vec4`, and there is in most cases no need to +//! directly manage homogeneous vectors. //! +//! # Creating vectors +//! ``` +//! use retrofire_core::math::{Vec3, vec3, splat}; +//! +//! // The following lines are equivalent: +//! let x: Vec3 = vec3(1.0, 0.0, 0.0); +//! let x = ::new([1.0, 0.0, 0.0]); +//! let x: Vec3 = Vec3::X; +//! let x: Vec3 = [1.0, 0.0, 0.0].into(); +//! +//! // Use `splat` to broadcast a scalar: +//! let one: Vec3 = splat(1.0); +//! assert_eq!(one, vec3(1.0, 1.0, 1.0)); +//! ``` +//! +//! # Components and properties +//! ``` +//! # use retrofire_core::math::{Vec3, vec3}; +//! let mut v: Vec3 = vec3(1.0, 2.0, 3.0); +//! +//! // Accessing all components (note that .into() does not work with +//! // assert_eq!() due to type inference ambiguity) +//! assert_eq!(v.0, [1.0, 2.0, 3.0]); +//! assert_eq!(<[_;_]>::from(v), [1.0, 2.0, 3.0]); +//! assert_eq!(<(_,_,_)>::from(v), (1.0, 2.0, 3.0)); +//! +//! // Accessing an ndividual component +//! assert_eq!(v.y(), 2.0); +//! assert_eq!(v[2], 3.0); +//! v[0] = 4.0; +//! assert_eq!(v.x(), 4.0); +//! ``` +//! +//! # Vector operations +//! ``` +//! # use retrofire_core::math::*; +//! let v: Vec3 = vec3(1.0, 2.0, 3.0); +//! +//! // Standard overloaded operators are provided: +//! assert_eq!(v + v, vec3(2.0, 4.0, 6.0)); +//! assert_eq!(-v, vec3(-1.0, -2.0, -3.0)); +//! assert_eq!(1.5 * v, vec3(1.5, 3.0, 4.5)); +//! +//! // Length and normalization: +//! assert_eq!(v.len(), f32::sqrt(1.0 + 4.0 + 9.0)); +//! // Use ´len_sqr` to avoid the square root when unneeded: +//! assert_eq!(v.len_sqr(), 1.0 + 4.0 + 9.0); +//! +//! assert_eq!(v.normalize().len(), 0.99999994); +//! assert_eq!(v.normalize_approx().len(), 0.9998242); +//! +//! // Dot and cross products: +//! assert_eq!(v.dot(&Vec3::Y), 2.0); +//! assert_eq!(v.cross(&Vec3::Y), vec3(-3.0, 0.0, 1.0)); +//! +//! // 2D vectors implement the "perp" and "perp dot" operations: +//! let u: Vec2 = vec2(2.0, 3.0); +//! assert_eq!(u.perp(), vec2(-3.0, 2.0)); +//! assert_eq!(u.perp_dot(Vec2::Y), 2.0); +//! +//! // Projections: +//! assert_eq!(v.scalar_project(&Vec3::Y), 2.0); +//! assert_eq!(v.vector_project(&Vec3::Z), 3.0 * Vec3::Z); +//! +//! // Mapping components: +//! assert_eq!(v.map(|c| c * c), vec3(1.0, 4.0, 9.0)); +//! ``` use core::{ array, @@ -30,6 +103,7 @@ use super::{Angle, acos}; /// A generic vector type. Represents an element of a vector space. /// +/// For more information, see the [module documentation](self). // or a module, // a generalization of a vector space where the scalars can be integers // (technically, the scalar type can be any *ring*-like type). @@ -183,6 +257,7 @@ impl Vector<[f32; N], Sp> { /// assert_eq!(normalized.len(), 0.99844766); /// ``` #[inline] + #[must_use] pub fn normalize_approx(&self) -> Self { *self * fast_recip_sqrt(self.len_sqr()) } @@ -220,10 +295,11 @@ impl Vector<[f32; N], Sp> { /// # Examples /// ``` /// use retrofire_core::math::{degs, vec3, Vec3}; + /// use retrofire_core::assert_approx_eq; /// /// 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)); + /// assert_approx_eq!(a.angle(&b), degs(90.0)); /// ``` #[cfg(feature = "fp")] #[inline] @@ -256,6 +332,19 @@ impl Vector<[f32; N], Sp> { /// Returns `true` if every component of `self` is finite, `false` otherwise. /// /// See [`f32::is_finite()`]. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::*; + /// + /// let finite: Vec2 = vec2(0.0, 2.0); + /// let has_inf: Vec3 = vec3(0.0, f32::INFINITY, 2.0); + /// let has_nan: Vec3 = vec3(0.0, 1.0, f32::NAN); + /// + /// assert!(finite.is_finite()); + /// assert!(!has_inf.is_finite()); + /// assert!(!has_nan.is_finite()); + /// ``` pub fn is_finite(&self) -> bool { self.0.iter().all(|c| c.is_finite()) } @@ -457,6 +546,14 @@ impl Vector<[Sc; 2], Real<2, B>> { } /// Converts `self` to a `Vec3`, with z set to 0. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::vec::*; + /// + /// let vector: Vec2 = vec2(1.0, 2.0); + /// assert_eq!(vector.to_vec3(), vec3(1.0, 2.0, 0.0)); + /// ``` pub fn to_vec3(self) -> Vector<[Sc; 3], Real<3, B>> where Sc: Linear, @@ -483,6 +580,7 @@ impl Vec2 { /// assert_eq!(::Y.perp(), -Vec2::X); /// ``` #[inline] + #[must_use] pub const fn perp(self) -> Self { vec2(-self.y(), self.x()) } @@ -520,8 +618,10 @@ impl Vec2 { /// assert! (v.perp_dot(Vec2::Y) > 0.0, "Y is counter-clockwise from v"); /// ``` #[inline] - pub fn perp_dot(self, other: Self) -> f32 { - self.perp().dot(&other) + pub const fn perp_dot(self, other: Self) -> f32 { + let perp = self.perp(); + // Manual dot to allow const + perp.x() * other.x() + perp.y() * other.y() } /// Returns the angle between `self` and the positive x-axis. @@ -569,6 +669,11 @@ where self.0[2] } + #[inline] + pub const fn xy(&self) -> Vector<[Sc; 2], Real<2, B>> { + vec2(self.x(), self.y()) + } + /// Returns the cross product of `self` with `other`. /// /// The result is a vector orthogonal with both input vectors, its length @@ -595,6 +700,7 @@ where /// t | / / /// +--------------- > self /// ``` + #[must_use] pub fn cross(&self, other: &Self) -> Self where Sc: Linear, @@ -650,6 +756,14 @@ impl Vector<[Sc; 4], Proj3> { } /// Projects `self` to the real plane by dividing by `w`. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::*; + /// + /// let proj = ProjVec3::new([1.0, 2.0, 3.0, 4.0]); + /// assert_eq!(proj.to_real::<()>(), pt3(0.25, 0.5, 0.75)); + /// ``` #[inline] pub fn to_real(&self) -> Point<[Sc; 3], Real<3, B>> where @@ -768,6 +882,15 @@ impl Debug for Vector { } } +impl From>> for Vector<[Sc; 3], Real<3, B>> +where + Sc: Linear + Copy, +{ + fn from(v: Vector<[Sc; 2], Real<2, B>>) -> Self { + v.to_vec3() + } +} + // Vector <-> repr conversions impl From for Vector { #[inline] @@ -793,24 +916,61 @@ impl From for Vector<[Sc; N], Sp> { // Vector <-> tuple conversions impl From<(Sc, Sc)> for Vector<[Sc; 2], Sp> { + /// Converts a 2-tuple into a 2-vector. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::vec::*; + /// + /// assert_eq!(::from((1.0, 2.0)), vec2(1.0, 2.0)); + /// ``` #[inline] fn from(xy: (Sc, Sc)) -> Self { Self::new(xy.into()) } } impl From> for (Sc, Sc) { + /// Converts a 2-vector into a 2-tuple. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::vec::*; + /// + /// let vector: Vec2 = vec2(1.0, 2.0); + /// let (x, y) = vector.into(); + /// assert_eq!((x, y), (1.0, 2.0)); + /// ``` #[inline] fn from(v: Vector<[Sc; 2], Sp>) -> Self { v.0.into() } } impl From<(Sc, Sc, Sc)> for Vector<[Sc; 3], Sp> { + /// Converts a 3-tuple into a 3-vector. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::vec::*; + /// + /// let vector: Vec3 = (1.0, 2.0, 3.0).into(); + /// assert_eq!(vector, vec3(1.0, 2.0, 3.0)); + /// ``` #[inline] fn from(xyz: (Sc, Sc, Sc)) -> Self { Self::new(xyz.into()) } } impl From> for (Sc, Sc, Sc) { + /// Converts a 3-vector into a 3-tuple. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::vec::*; + /// + /// let vector: Vec3 = vec3(1.0, 2.0, 3.0); + /// let (x, y, z) = vector.into(); + /// assert_eq!((x, y, z), (1.0, 2.0, 3.0)); + /// ``` #[inline] fn from(v: Vector<[Sc; 3], Sp>) -> Self { v.0.into() @@ -1018,29 +1178,31 @@ mod tests { mod f32 { use super::*; + const X: Vec2 = Vec2::X; + const Y: Vec2 = Vec2::Y; + + const V2: Vec2 = vec2(1.0, -2.0); + const V3: Vec3 = vec3(1.0, -2.0, 3.0); + const V4: Vector<[f32; 4], Real<4>> = vec4(1.0, -2.0, 3.0, -4.0); + #[test] fn length() { assert_approx_eq!(vec2(1.0, 1.0).len(), SQRT_2); assert_approx_eq!(vec2(-3.0, 4.0).len(), 5.0); - assert_approx_eq!(vec3(1.0, -2.0, 3.0).len(), 14.0f32.sqrt()); + assert_approx_eq!(V3.len(), 14.0f32.sqrt()); } #[test] fn length_squared() { - assert_eq!(vec2(1.0, 1.0).len_sqr(), 2.0); - assert_eq!(vec2(-4.0, 3.0).len_sqr(), 25.0); - assert_eq!(vec3(1.0, -2.0, 3.0).len_sqr(), 14.0); + assert_eq!(V2.len_sqr(), 5.0); + assert_eq!(V3.len_sqr(), 14.0); } #[test] fn normalize() { assert_approx_eq!(vec2(3.0, 4.0).normalize(), vec2(0.6, 0.8)); - let sqrt_14 = 14.0f32.sqrt(); - assert_approx_eq!( - vec3(1.0, 2.0, 3.0).normalize(), - vec3(1.0 / sqrt_14, 2.0 / sqrt_14, 3.0 / sqrt_14) - ); + assert_approx_eq!(V3.normalize(), V3.map(|x| x / 14.0f32.sqrt())); } #[test] @@ -1069,50 +1231,58 @@ mod tests { #[test] fn vector_addition() { - assert_eq!(vec2(1.0, 2.0) + vec2(-2.0, 1.0), vec2(-1.0, 3.0)); - assert_eq!( - vec3(1.0, 2.0, 0.0) + vec3(-2.0, 1.0, -1.0), - vec3(-1.0, 3.0, -1.0) - ); + assert_eq!(V2 + vec2(-2.0, 3.0), vec2(-1.0, 1.0)); + let mut v2 = V2; + v2 += vec2(-2.0, 3.0); + assert_eq!(v2, vec2(-1.0, 1.0)); + + assert_eq!(V3 + vec3(-2.0, 1.0, -1.0), vec3(-1.0, -1.0, 2.0)); + let mut v3 = V3; + v3 += vec3(-2.0, 1.0, -1.0); + assert_eq!(v3, vec3(-1.0, -1.0, 2.0)); } #[test] fn scalar_multiplication() { - assert_eq!(vec2(1.0, -2.0) * 0.0, vec2(0.0, 0.0)); - assert_eq!(vec3(1.0, -2.0, 3.0) * 3.0, vec3(3.0, -6.0, 9.0)); - assert_eq!(3.0 * vec3(1.0, -2.0, 3.0), vec3(3.0, -6.0, 9.0)); - assert_eq!( - vec4(1.0, -2.0, 0.0, -3.0) * 3.0, - vec4(3.0, -6.0, 0.0, -9.0) - ); - assert_eq!( - 3.0 * vec4(1.0, -2.0, 0.0, -3.0), - vec4(3.0, -6.0, 0.0, -9.0) - ); + assert_eq!(V2 * 0.0, vec2(0.0, 0.0)); + assert_eq!(V2 * 2.0, vec2(2.0, -4.0)); + assert_eq!(-2.0 * V2, vec2(-2.0, 4.0)); + + assert_eq!(V3 * 3.0, vec3(3.0, -6.0, 9.0)); + assert_eq!(-3.0 * V3, vec3(-3.0, 6.0, -9.0)); + + assert_eq!(V4 * 2.0, vec4(2.0, -4.0, 6.0, -8.0)); + assert_eq!(-2.0 * V4, vec4(-2.0, 4.0, -6.0, 8.0)); } #[test] fn scalar_division() { - assert_eq!(vec2(1.0, -2.0) / 1.0, vec2(1.0, -2.0)); - assert_eq!(vec3(3.0, -6.0, 9.0) / 3.0, vec3(1.0, -2.0, 3.0)); - assert_eq!( - vec4(3.0, -6.0, 0.0, -9.0) / 3.0, - vec4(1.0, -2.0, 0.0, -3.0) - ); + assert_eq!(V2 / 1.0, V2); + assert_eq!(V2 / -2.0, vec2(-0.5, 1.0)); + + assert_eq!(V3 / 0.5, vec3(2.0, -4.0, 6.0)); + + let v4 = vec4(3.0, -6.0, 0.0, -9.0); + assert_eq!(v4 / 3.0, vec4(1.0, -2.0, 0.0, -3.0)); } #[test] fn dot_product() { - assert_eq!(vec2(1.0, -2.0).dot(&vec2(2.0, 3.0)), -4.0); - assert_eq!(vec3(1.0, -2.0, 3.0).dot(&vec3(2.0, 3.0, -1.0)), -7.0); + assert_eq!(V2.dot(&V2), 5.0); + assert_eq!(V2.dot(&V2.perp()), 0.0); + assert_eq!(V2.dot(&vec2(2.0, 3.0)), -4.0); + + assert_eq!(V3.dot(&V3), 14.0); + assert_eq!(V3.dot(&-V3), -14.0); + assert_eq!(V3.dot(&vec3(2.0, 3.0, -1.0)), -7.0); } #[test] fn zero_parallel_to_anything() { // Zero vector is parallel with 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))); - assert!(vec2(0.0, 0.0).is_parallel_to(&vec2(-1.0, 2.0))); + assert!(vec2(0.0, 0.0).is_parallel_to(&V2)); + assert!(vec2(0.0, 0.0).is_parallel_to(&-V2)); } #[test] @@ -1126,11 +1296,11 @@ mod tests { #[test] fn a_b_parallel_to_ka_kb() { - // (2, -1) is parallel with any (2·k, -1·k) - assert!(vec2(2.0, -1.0).is_parallel_to(&vec2(0.0, 0.0))); - assert!(vec2(2.0, -1.0).is_parallel_to(&vec2(2.0, -1.0))); - assert!(vec2(2.0, -1.0).is_parallel_to(&vec2(-4.0, 2.0))); - assert!(vec2(2.0, -1.0).is_parallel_to(&vec2(1.0, -0.5))); + // (1, -2) is parallel with any (k, -2·k) + assert!(V2.is_parallel_to(&vec2(0.0, 0.0))); + assert!(V2.is_parallel_to(&vec2(-1.0, 2.0))); + assert!(V2.is_parallel_to(&vec2(2.0, -4.0))); + assert!(V2.is_parallel_to(&vec2(-0.125, 0.25))); } #[test] @@ -1145,41 +1315,41 @@ mod tests { #[test] fn indexing() { - let mut v = vec2(1.0, 2.0); - assert_eq!(v[1], 2.0); + let mut v = V2; + assert_eq!(v[1], -2.0); v[0] = 3.0; - assert_eq!(v.0, [3.0, 2.0]); + assert_eq!(v.0, [3.0, -2.0]); - let mut v = vec3(1.0, 2.0, 3.0); - assert_eq!(v[1], 2.0); + let mut v = V3; + assert_eq!(v[1], -2.0); v[2] = 4.0; - assert_eq!(v.0, [1.0, 2.0, 4.0]); + assert_eq!(v.0, [1.0, -2.0, 4.0]); + + let mut v = V4; + assert_eq!(v[2], 3.0); + v[3] = 5.0; + assert_eq!(v.0, [1.0, -2.0, 3.0, 5.0]); } #[test] fn from_array() { - assert_eq!(Vec2::from([1.0, -2.0]), vec2(1.0, -2.0)); - assert_eq!(Vec3::from([1.0, -2.0, 4.0]), vec3(1.0, -2.0, 4.0)); - assert_eq!( - Vector::from([1.0, -2.0, 4.0, -3.0]), - vec4(1.0, -2.0, 4.0, -3.0) - ); + assert_eq!(Vec2::from([1.0, -2.0]), V2); + assert_eq!(Vec3::from([1.0, -2.0, 3.0]), V3); + assert_eq!(Vector::from([1.0, -2.0, 3.0, -4.0]), V4); } #[test] fn perp() { - assert_eq!(Vec2::<()>::zero().perp(), Vec2::zero()); - assert_eq!(Vec2::<()>::X.perp(), Vec2::Y); - assert_eq!(vec2(-0.2, -1.5).perp(), vec2(1.5, -0.2)); + assert_eq!(::zero().perp(), Vec2::zero()); + assert_eq!(X.perp(), Y); + assert_eq!(V2.perp(), vec2(2.0, 1.0)); } #[test] fn perp_dot() { - const X: Vec2 = Vec2::X; - const Y: Vec2 = Vec2::Y; - assert_eq!(X.perp_dot(X), 0.0); assert_eq!(X.perp_dot(Y), 1.0); + assert_eq!(X.perp_dot(-Y), -1.0); assert_eq!((2.0 * Y).perp_dot(3.0 * X), -6.0); } } @@ -1187,53 +1357,80 @@ mod tests { mod i32 { use super::*; + const V2: Vec2i = vec2(1, -2); + const V3: Vec3i = vec3(1, -2, 3); + #[test] fn vector_addition() { - assert_eq!(vec2(1, 2) + vec2(-2, 1), vec2(-1, 3)); - assert_eq!(vec3(1, 2, 0) + vec3(-2, 1, -1), vec3(-1, 3, -1)); + assert_eq!(V2 + vec2(-2, 1), vec2(-1, -1)); + + let mut v2 = V2; + v2 += vec2(2, -3); + assert_eq!(v2, vec2(3, -5)); + + assert_eq!(V3 + vec3(-2, 1, -1), vec3(-1, -1, 2)); + + let mut v3 = V3; + v3 += vec3(2, 3, -4); + assert_eq!(v3, vec3(3, 1, -1)); } #[test] fn vector_subtraction() { - assert_eq!(vec2(1, 2) - vec2(-2, 3), vec2(3, -1)); - assert_eq!(vec3(1, 2, 0) - vec3(-2, 1, 2), vec3(3, 1, -2)); + assert_eq!(V2 - vec2(2, -3), vec2(-1, 1)); + + let mut v2 = V2; + v2 -= vec2(2, -3); + assert_eq!(v2, vec2(-1, 1)); + + assert_eq!(V3 - vec3(-2, 1, 2), vec3(3, -3, 1)); + + let mut v3 = V3; + v3 -= vec3(2, 3, -1); + assert_eq!(v3, vec3(-1, -5, 4)); } #[test] #[allow(clippy::erasing_op)] fn scalar_multiplication() { - assert_eq!(vec2(1, -2) * 0, vec2(0, 0)); + assert_eq!(V2 * 0, vec2(0, 0)); + assert_eq!(V2 * 2, vec2(2, -4)); + assert_eq!(-2 * V2, vec2(-2, 4)); + + let mut v2 = V2; + v2 *= 3; + assert_eq!(v2, vec2(3, -6)); - assert_eq!(vec3(1, -2, 3) * 3, vec3(3, -6, 9)); - assert_eq!(3 * vec3(1, -2, 3), vec3(3, -6, 9)); + assert_eq!(V3 * 3, vec3(3, -6, 9)); + assert_eq!(-3 * V3, vec3(-3, 6, -9)); assert_eq!(vec4(1, -2, 0, -3) * 3, vec4(3, -6, 0, -9)); - assert_eq!(3 * vec4(1, -2, 0, -3), vec4(3, -6, 0, -9)); + assert_eq!(-3 * vec4(1, -2, 0, -3), vec4(-3, 6, 0, 9)); } #[test] fn dot_product() { - assert_eq!(vec2(1, -2).dot(&vec2(2, 3)), -4); - assert_eq!(vec3(1, -2, 3).dot(&vec3(2, 3, -1)), -7); + assert_eq!(V2.dot(&vec2(2, -3)), 8); + assert_eq!(V3.dot(&vec3(2, 3, -1)), -7); } #[test] fn indexing() { - let mut v = vec2(1, 2); - assert_eq!(v[1], 2); + let mut v = V2; + assert_eq!(v[1], -2); v[0] = 3; - assert_eq!(v.0, [3, 2]); + assert_eq!(v.0, [3, -2]); - let mut v = vec3(1, 2, 3); - assert_eq!(v[1], 2); + let mut v = V3; + assert_eq!(v[1], -2); v[2] = 4; - assert_eq!(v.0, [1, 2, 4]); + assert_eq!(v.0, [1, -2, 4]); } #[test] fn from_array() { - assert_eq!(Vec2i::from([1, -2]), vec2(1, -2)); - assert_eq!(Vec3i::from([1, -2, 3]), vec3(1, -2, 3)); + assert_eq!(Vec2i::from([1, -2]), V2); + assert_eq!(Vec3i::from([1, -2, 3]), V3); } } diff --git a/core/src/render.rs b/core/src/render.rs index f6d8c6f0..9e6cb5fa 100644 --- a/core/src/render.rs +++ b/core/src/render.rs @@ -6,33 +6,31 @@ //! geometric shapes such as triangles. use alloc::vec::Vec; -use core::{fmt::Debug, ops::DerefMut}; +use core::fmt::Debug; use crate::geom::Vertex; -use crate::math::{ - Mat4, ProjVec3, Vary, - mat::{RealToProj, RealToReal}, -}; +use crate::math::{Mat4, ProjVec3, Vary}; -use self::{ - clip::{ClipVert, view_frustum}, - ctx::DepthSort, - raster::Scanline, -}; +use self::{clip::view_frustum, ctx::DepthSort}; pub(super) mod re_exports { pub use super::{ batch::Batch, cam::Camera, - clip::Clip, + clip::{Clip, ClipVert}, ctx::Context, + light::Light, + prim::Primitive, raster::Frag, + scene::{BBox, Obj}, shader::{FragmentShader, VertexShader}, - stats::Stats, target::{Colorbuf, Framebuf, Target}, tex::{TexCoord, Texture, uv}, text::Text, }; + + #[cfg(feature = "stats")] + pub use super::stats::Stats; } pub use re_exports::*; @@ -41,44 +39,44 @@ pub mod cam; pub mod clip; pub mod ctx; pub mod debug; +pub mod light; pub mod prim; pub mod raster; pub mod scene; pub mod shader; -pub mod stats; pub mod target; pub mod tex; pub mod text; -/// Renderable geometric primitive. -pub trait Render { - /// The type of this primitive in clip space - type Clip; - - /// The type for which `Clip` is implemented. - type Clips: Clip + ?Sized; +#[cfg(feature = "stats")] +pub mod stats; - /// The type of this primitive in screen space. - type Screen; +mod impls; - /// Maps the indexes of the argument to vertices. - fn inline(ixd: Self, vs: &[ClipVert]) -> Self::Clip; +pub trait Render, Vert> { + fn to_primitives( + &self, + shader: &Shd, + uniform: Uni, + ) -> impl Iterator + where + Shd: VertexShader>; +} - /// Returns the (average) depth of the argument. - fn depth(_clip: &Self::Clip) -> f32 { - f32::INFINITY - } +pub struct Indexed { + pub prims: Prims, + pub verts: Verts, +} - /// Returns whether the argument is facing away from the camera. - fn is_backface(_: &Self::Screen) -> bool { - false - } +pub struct TriFan(pub Vec); - /// Transforms the argument from NDC to screen space. - fn to_screen(clip: Self::Clip, tf: &Mat4) -> Self::Screen; +pub struct TriStrip(pub Vec); - /// Rasterizes the argument by calling the function for each scanline. - fn rasterize)>(scr: Self::Screen, scanline_fn: F); +/// Alias for combined vertex+fragment shader types +pub trait Shader: + VertexShader> + + FragmentShader +{ } /// Model space coordinate basis. @@ -101,112 +99,122 @@ pub struct Ndc; #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] pub struct Screen; -// Mapping from model space to world space. -pub type ModelToWorld = RealToReal<3, Model, World>; - -// Mapping from world space to view space. -pub type WorldToView = RealToReal<3, World, View>; - -/// Mapping from model space to view space. -pub type ModelToView = RealToReal<3, Model, View>; - -/// Mapping from model space to view space. -pub type ModelToProj = RealToProj; - -/// Mapping from view space to projective space. -pub type ViewToProj = RealToProj; - -/// Mapping from NDC space to screen space. -pub type NdcToScreen = RealToReal<3, Ndc, Screen>; - -/// Alias for combined vertex+fragment shader types -pub trait Shader: - VertexShader> + FragmentShader -{ -} -impl Shader for S where - S: VertexShader> - + FragmentShader -{ -} - /// Renders the given primitives into `target`. -pub fn render( - prims: impl AsRef<[Prim]>, - verts: impl AsRef<[Vtx]>, +pub fn render( + geometry: &Geom, shader: &Shd, uniform: Uni, to_screen: Mat4, - mut target: &mut impl Target, + target: &mut impl Target, ctx: &Context, ) where - Prim: Render + Clone, - [::Clip]: Clip, - Var: Vary, - Shd: Shader, + // FIXME Primitive currently tacitly assumes indexed representation! + Prim: Primitive + Clone, + Vert: Clone, + Geom: Render, + Var: Vary + 'static, + Uni: Copy, + Shd: Shader, { - // 0. Preparations - let verts = verts.as_ref(); - let prims = prims.as_ref(); + // 0. Setup - let mut stats = Stats::start(); - stats.calls = 1.0; - stats.prims.i = prims.len(); - stats.verts.i = verts.len(); + // #[cfg(feature = "stats")] + // let stats = Stats::start_call( + // geometry.primitives().as_ref().len(), + // geometry.vertices().as_ref().len(), + // ); // 1. Vertex shader: transform vertices to clip space - let verts: Vec<_> = verts - // verts is borrowed, can't consume - .iter() - // TODO Pass vertex as ref to shader - .cloned() - .map(|v| shader.shade_vertex(v, uniform)) - .map(ClipVert::new) - .collect(); + //let verts = vertex_transform(shader, uniform, verts); + + //let geom_in_clip = geometry.transform_to_clip(shader, uniform); // 2. Primitive assembly: map vertex indices to actual vertices - let prims: Vec<_> = prims - .iter() - .map(|tri| Prim::inline(tri.clone(), &verts)) - // Collect needed because clip takes a slice... - .collect(); + //let prims = primitive_assembly(prims, &verts); - // 3. Clipping: clip against the view frustum - // TODO capacity is just a heuristic, should retain vector between calls somehow - let mut clipped = Vec::with_capacity(prims.len() / 2); - view_frustum::clip(&prims[..], &mut clipped); + let prims = geometry.to_primitives(shader, uniform); - // Optional depth sorting for use case such as transparency - if let Some(d) = ctx.depth_sort { + // 3. Clipping: clip against the view frustum + let clipped = Clip::clip(prims, &view_frustum::PLANES); + + // 4. Rasterize: Turn visible primitives to fragments + #[allow(unused_variables)] + let (prims_out, verts_out) = if let Some(d) = ctx.depth_sort { + // Optional depth sorting for use cases such as transparency + // Only materialize to a vector if depth sort is enabled + let mut clipped = clipped.collect::>(); depth_sort::(&mut clipped, d); - } + rasterize::( + clipped, shader, uniform, to_screen, target, ctx, + ) + } else { + rasterize::( + clipped, shader, uniform, to_screen, target, ctx, + ) + }; - // For each primitive in the view frustum: + // #[cfg(feature = "stats")] + // { + // *ctx.stats.borrow_mut() += stats.finish_call(prims_out, verts_out); + // } +} + +fn rasterize( + clipped: impl IntoIterator, + shader: &Shd, + uniform: Uni, + to_screen: Mat4, + target: &mut impl Target, + ctx: &Context, +) -> (usize, usize) +where + Prim: Primitive, + Shd: FragmentShader, + Var: Vary, + Uni: Copy, +{ + let mut out = (0, 0); for prim in clipped { // Transform to screen space let prim = Prim::to_screen(prim, &to_screen); // Back/frontface culling - // TODO This could also be done earlier, before or as part of clipping + // TODO This could also be done earlier, before or as part of clipping, + // but determining back/frontface is more difficult before z-div if ctx.face_cull(Prim::is_backface(&prim)) { continue; } // Log output stats after culling - stats.prims.o += 1; - stats.verts.o += 3; // TODO Get number of verts in prim somehow + out.0 += 1; + out.1 += 3; // TODO Get number of verts in prim somehow // 4. Fragment shader and rasterization Prim::rasterize(prim, |scanline| { // Convert to fragments, shade, and draw to target - stats.frags += target - .deref_mut() - .rasterize(scanline, shader, ctx); + target.rasterize(scanline, shader, uniform, ctx); }); } - *ctx.stats.borrow_mut() += stats.finish(); + out } -fn depth_sort, V: Vary>(prims: &mut [P::Clip], d: DepthSort) { +#[inline] +fn vertex_transform<'a, Shd, Vtx: Clone + 'a, Var: Vary, Uni: Copy>( + shader: &Shd, + uniform: Uni, + verts: impl IntoIterator, +) -> Vec> +where + Shd: VertexShader>, +{ + verts + .into_iter() + // TODO Pass vertex as ref to shader + .map(|v| shader.shade_vertex(v.clone(), uniform)) + .map(ClipVert::new) + .collect() +} + +fn depth_sort, V: Vary>(prims: &mut [P::Clip], d: DepthSort) { prims.sort_unstable_by(|t, u| { let z = P::depth(t); let w = P::depth(u); @@ -217,3 +225,9 @@ fn depth_sort, V: Vary>(prims: &mut [P::Clip], d: DepthSort) { } }); } + +impl Shader for S where + S: VertexShader> + + FragmentShader +{ +} diff --git a/core/src/render/batch.rs b/core/src/render/batch.rs index 10da4af9..5b05e998 100644 --- a/core/src/render/batch.rs +++ b/core/src/render/batch.rs @@ -6,7 +6,7 @@ use core::borrow::Borrow; use crate::geom::{Edge, Mesh, Tri, Vertex3}; use crate::math::{Mat4, Vary}; -use super::{Clip, Context, Ndc, Render, Screen, Shader, Target}; +use super::{Clip, Context, Indexed, Ndc, Primitive, Screen, Shader, Target}; /// A builder for rendering a chunk of geometry as a batch. /// @@ -28,9 +28,9 @@ use super::{Clip, Context, Ndc, Render, Screen, Shader, Target}; // using the same configuration, or several [instances] of the same geometry. // [instances]: https://en.wikipedia.org/wiki/Geometry_instancing #[derive(Clone, Debug, Default)] -pub struct Batch { - pub prims: Vec, - pub verts: Vec, +pub struct Batch { + pub prims: Prims, + pub verts: Verts, pub uniform: Uni, pub shader: Shd, pub viewport: Mat4, @@ -51,86 +51,98 @@ impl Batch<(), (), (), (), (), Context> { } } -impl Batch { +impl Batch { /// Sets the primitives to be rendered. - /// - /// The primitives are copied into the batch. - pub fn primitives( + #[must_use] + pub fn primitives( self, - prims: impl AsRef<[P]>, - ) -> Batch { - let prims = prims.as_ref().to_vec(); + prims: Ps, + ) -> Batch { update!(prims; self verts uniform shader viewport target ctx) } /// Sets the vertices to be rendered. - /// - /// The vertices are cloned into the batch. - // TODO: Allow taking by reference to make cloning Batch cheap - pub fn vertices( + #[must_use] + pub fn vertices( self, - verts: impl AsRef<[V]>, - ) -> Batch { - let verts = verts.as_ref().to_vec(); + verts: Vs, + ) -> Batch { update!(verts; self prims uniform shader viewport target ctx) } /// Clones faces and vertices from a mesh to this batch. + /// + /// You can also create a new batch from a moved or borrowed mesh + /// directly using the `From` or `From<&Mesh>` impls. + #[must_use] pub fn mesh( self, mesh: &Mesh, - ) -> Batch, Vertex3, Uni, Shd, Tgt, Ctx> { - let prims = mesh.faces.clone(); - let verts = mesh.verts.clone(); + ) -> Batch<&[Tri], &[Vertex3], Uni, Shd, Tgt, Ctx> { + let prims = &mesh.faces; + let verts = &mesh.verts; update!(verts prims; self uniform shader viewport target ctx) } /// Sets the uniform data to be passed to the vertex shaders. + #[must_use] pub fn uniform( self, uniform: U, - ) -> Batch { + ) -> Batch { update!(uniform; self verts prims shader viewport target ctx) } /// Sets the combined vertex and fragment shader. - pub fn shader>( + #[must_use] + pub fn shader( self, shader: S, - ) -> Batch { + ) -> Batch + where + Var: Vary, + S: Shader, + Verts: AsRef<[Vtx]>, + { update!(shader; self verts prims uniform viewport target ctx) } /// Sets the viewport matrix. + #[must_use] pub fn viewport(self, viewport: Mat4) -> Self { update!(viewport; self verts prims uniform shader target ctx) } /// Sets the render target. // TODO what bound for T? - pub fn target(self, target: T) -> Batch { + #[must_use] + pub fn target(self, target: T) -> Batch { update!(target; self verts prims uniform shader viewport ctx) } /// Sets the rendering context. + #[must_use] pub fn context( self, ctx: &Context, - ) -> Batch { + ) -> Batch { update!(ctx; self verts prims uniform shader viewport target) } } -impl Batch { +impl Batch { /// Renders this batch of geometry. #[rustfmt::skip] - pub fn render(&mut self) + pub fn render(&mut self) where - Var: Vary, - Prim: Render + Clone, + Var: Vary + 'static, + Prim: Primitive + Clone, Vtx: Clone, + + Prims: AsRef<[Prim]>, + Verts: AsRef<[Vtx]>, + Uni: Copy, - [::Clip]: Clip, Shd: Shader, Tgt: Target, Ctx: Borrow @@ -140,13 +152,15 @@ impl Batch { } = self; super::render( - prims, verts, shader, *uniform, *viewport, - target, (*ctx).borrow(), + &Indexed { prims: prims.as_ref(), verts: verts.as_ref() }, + shader, *uniform, *viewport, target, (*ctx).borrow(), ); - } +} } -impl Batch, Vtx, Uni, Shd, Tgt, Ctx> { +impl + Batch>, Vec, Uni, Shd, Tgt, Ctx> +{ pub fn append(&mut self, other: Self) { let Batch { prims, verts, .. } = other; let n = self.verts.len(); @@ -157,7 +171,9 @@ impl Batch, Vtx, Uni, Shd, Tgt, Ctx> { } } -impl Batch, Vtx, Uni, Shd, Tgt, Ctx> { +impl + Batch>, Vec, Uni, Shd, Tgt, Ctx> +{ pub fn append(&mut self, other: Self) { let Batch { prims, verts, .. } = other; let n = self.verts.len(); @@ -167,3 +183,25 @@ impl Batch, Vtx, Uni, Shd, Tgt, Ctx> { self.prims.extend(prims); } } + +// +// Foreign trait impls +// + +impl From> + for Batch>, Vec>, (), (), (), Context> +{ + fn from(m: Mesh) -> Self { + Batch::new().primitives(m.faces).vertices(m.verts) + } +} + +impl<'a, A, B> From<&'a Mesh> + for Batch<&'a [Tri], &'a [Vertex3], (), (), (), Context> +{ + fn from(m: &'a Mesh) -> Self { + Batch::new() + .primitives(m.faces.as_slice()) + .vertices(m.verts.as_slice()) + } +} diff --git a/core/src/render/cam.rs b/core/src/render/cam.rs index 145e57bd..3ead951b 100644 --- a/core/src/render/cam.rs +++ b/core/src/render/cam.rs @@ -2,18 +2,19 @@ use core::ops::Range; -use crate::math::space::Real; #[cfg(feature = "fp")] use crate::math::{ Angle, Vec3, orient_z, rotate_pyr, rotate_x, rotate_y, spherical, turns, }; use crate::math::{ Mat4, Point3, ProjMat3, SphericalVec, Vary, orthographic, perspective, pt2, - translate, viewport, + space::Real, translate, viewport, }; -use crate::util::{Dims, rect::Rect}; +use crate::util::{Dims, Rect}; -use super::{Clip, Context, Ndc, Render, Screen, Shader, Target, View, World}; +use super::{ + Context, Ndc, Primitive, Render, Screen, Shader, Target, View, World, +}; /// Trait for different modes of camera motion. pub trait Transform { @@ -149,7 +150,7 @@ impl Camera<()> { pub fn new(dims: Dims) -> Self { Self { dims, - viewport: viewport(pt2(0, 0)..pt2(dims.0, dims.1)), + viewport: viewport(dims.into()), ..Self::default() } } @@ -171,20 +172,20 @@ impl Camera { /// Sets the viewport bounds of this camera. #[must_use] pub fn viewport(self, bounds: impl Into>) -> Self { - let (w, h) = self.dims; - let Rect { left: Some(l), top: Some(t), right: Some(r), bottom: Some(b), - } = bounds.into().intersect(&(0..w, 0..h).into()) + } = bounds + .into() + .intersect(&(0..self.dims.0, 0..self.dims.1).into()) else { unreachable!("bounded ∩ bounded should be bounded") }; Self { - dims: (r.abs_diff(l), b.abs_diff(t)), + dims: Dims(r.abs_diff(l), b.abs_diff(t)), viewport: viewport(pt2(l, t)..pt2(r, b)), ..self } @@ -201,8 +202,7 @@ impl Camera { /// * If `near_far` is an empty range. #[must_use] pub fn perspective(mut self, fov: Fov, near_far: Range) -> Self { - let aspect = self.dims.0 as f32 / self.dims.1 as f32; - + let aspect = self.dims.aspect(); self.project = perspective(fov.focal_ratio(aspect), aspect, near_far); self } @@ -215,11 +215,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() @@ -232,27 +236,26 @@ impl Camera { /// Renders the given geometry from the viewpoint of this camera. #[allow(clippy::too_many_arguments)] - pub fn render( + pub fn render( &self, - prims: impl AsRef<[Prim]>, - verts: impl AsRef<[Vtx]>, + geom: &impl Render, to_world: &Mat4, shader: &Shd, uniform: Uni, target: &mut impl Target, ctx: &Context, ) where - Prim: Render + Clone, - [::Clip]: Clip, - Shd: for<'a> Shader, Uni)>, + Prim: Primitive + Clone, + Vtx: Clone, + Var: Vary + 'static, + Uni: Copy, + 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(), + geom, shader, - (&tf, uniform), + (&to_proj, uniform), self.viewport, target, ctx, @@ -449,14 +452,14 @@ impl Transform for PitchYawRoll { #[cfg(test)] mod tests { - use super::*; - #[cfg(feature = "fp")] use crate::{ assert_approx_eq, math::{SQRT_3, degs}, }; + use super::*; + use Fov::*; #[test] diff --git a/core/src/render/clip.rs b/core/src/render/clip.rs index 2bac65a4..fe2873d3 100644 --- a/core/src/render/clip.rs +++ b/core/src/render/clip.rs @@ -14,7 +14,11 @@ //! use alloc::vec::Vec; -use core::{iter::zip, mem::swap}; +use core::{ + fmt::{Debug, Formatter}, + iter, + mem::swap, +}; use crate::geom::{Edge, Tri, Vertex, vertex}; use crate::math::{Lerp, ProjVec3}; @@ -24,42 +28,44 @@ use view_frustum::{outcode, status}; /// Trait for types that can be [clipped][self] against convex volumes. /// /// # Note to implementors -/// This trait is primarily meant to be implemented on slices or other -/// composites, so that several primitives can be clipped in a single call. -/// This allows reuse of temporary buffers, for instance. -/// /// Implementations should avoid creating degenerate primitives, such as /// triangles with only two unique vertices. pub trait Clip { - /// Type of the clipped object. For example, `Self` if implemented for - /// the type itself, or `T` if implemented for `[T]`. - type Item; - - /// Clips `self` against `planes`, returning the resulting zero or more - /// primitives in the out parameter `out`. + /// Clips primitives against a set of planes, returning the resulting + /// zero or more primitives as an iterator. /// /// If a primitive being clipped lies entirely within the bounding volume, /// it is emitted as it is. If it is entirely outside the volume, it is /// skipped. If it is partially inside, it is clipped such that no points /// outside the volume remain in the result. - /// - /// The result is unspecified if `out` is nonempty. - /// - /// TODO Investigate returning an iterator - fn clip(&self, planes: &[ClipPlane], out: &mut Vec); + #[must_use] + fn clip<'a, I: IntoIterator>( + items: I, + planes: &'a [ClipPlane], + ) -> impl Iterator; } /// A vector in clip space. pub type ClipVec = ProjVec3; /// A vertex in clip space. -#[derive(Copy, Clone, Debug, PartialEq)] +#[derive(Copy, Clone, PartialEq)] pub struct ClipVert { pub pos: ClipVec, pub attrib: A, outcode: u8, } +impl Debug for ClipVert { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.debug_struct("ClipVert") + .field("pos", &self.pos.0) + .field("attrib", &self.attrib) + .field("outcode", &format_args!("{:06b}", self.outcode)) + .finish() + } +} + /// Visibility of a shape in the view frustum. #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Status { @@ -170,26 +176,27 @@ impl ClipPlane { verts_in: &[ClipVert], verts_out: &mut Vec>, ) { - let mut verts = verts_in.iter().chain(&verts_in[..1]); - - let Some(mut v0) = verts.next() else { + let [fst, .., lst] = verts_in else { return; }; + let edges = verts_in + .array_windows() + .map(|[v0, v1]| Edge(v0, v1)) + .chain([Edge(lst, fst)]); - for v1 in verts { + for Edge(v0, v1) in edges { 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()); + // v0 is inside; emit it as-is. If v1 is also inside, no need + // to do anything; it will be emitted on the next iteration. + verts_out.push(v0.clone()); } else { - // v0 is outside, discard it. If v1 is also outside, we don't - // have to do anything; it is discarded on the next iteration. + // v0 is outside, discard it. If v1 is also outside, no need + // to do anything; it will be discarded on the next iteration. } if let Some(v) = self.intersect([v0, v1]) { verts_out.push(v); } - v0 = v1; } } } @@ -222,15 +229,6 @@ pub mod view_frustum { ClipPlane::new( 0.0, 1.0, 0.0, 1.0, 0x20), // Top ]; - /// Clips geometry against the standard view frustum. - /// - /// Returns the part that is within the frustum in the out parameter `out`. - /// - /// This is the main entry point to clipping. - pub fn clip(geom: &G, out: &mut Vec) { - geom.clip(&PLANES, out); - } - /// Returns the outcode of the given point. /// /// The outcode is a bitset where the bit of each plane is 0 if the point @@ -288,8 +286,8 @@ pub fn clip_simple_polygon<'a, A: Lerp>( ) { debug_assert!(verts_out.is_empty()); - for (p, i) in zip(planes, 0..) { - p.clip_simple_polygon(verts_in, verts_out); + for (plane, i) in iter::zip(planes, 0..) { + plane.clip_simple_polygon(verts_in, verts_out); verts_in.clear(); if verts_out.is_empty() { // Nothing left to clip; the polygon was fully outside @@ -309,71 +307,110 @@ impl ClipVert { } } -impl Clip for [Edge>] { - type Item = Edge>; - - fn clip(&self, planes: &[ClipPlane], out: &mut Vec) { - 'lines: for edge @ Edge(a, b) in self { - let both_outside = a.outcode & b.outcode != 0; - let neither_outside = a.outcode | b.outcode == 0; - - //let mut a = a.clone(); - //let mut b = b.clone(); - - let mut e = edge.clone(); - - if both_outside { +impl Clip for Edge> { + fn clip<'a, I>( + edges: I, + planes: &'a [ClipPlane], + ) -> impl Iterator + where + I: IntoIterator, + { + let mut out = Vec::new(); + + 'edges: for edge in edges { + // Bitset of planes that both ends are outside + let both_outside = edge.0.outcode & edge.1.outcode; + // Bitset of planes that at least one end is outside + let either_outside = edge.0.outcode | edge.1.outcode; + + if both_outside != 0 { + // There is at least one plane outside which both ends are. + // The edge must be fully outside and can be discarded. continue; } - if neither_outside { - out.push(e); + if either_outside == 0 { + // Neither end is outside any plane. The edge is fully visible, + // no clipping needed. + out.push(edge.clone()); continue; } - // Otherwise, clipping is needed - for p in planes { - let a_in = p.is_inside(&e.0); - let b_in = p.is_inside(&e.1); - // TODO Why not handled by both_outside check? - if !a_in && !b_in { - continue 'lines; + + // Otherwise, either only one endpoint is inside all planes, *or* + // the two endpoints are outside *different* planes. Clipping is + // needed to compute the fully-inside segment of the edge *if any*. + let Edge(mut v0, mut v1) = edge.clone(); + for plane in planes { + let v0_inside = plane.is_inside(&v0); + let v1_inside = plane.is_inside(&v1); + + if !v0_inside && !v1_inside { + // We know the original edge was not outside any *single* + // plane, but if it *was* outside the bounding volume as + // a whole, clipping will eventually result in a remainder + // that is fully outside a plane and can be discarded. + continue 'edges; } - if let Some(v) = p.intersect([&e.0, &e.1]) { - if a_in { - e.1 = v; - } else if b_in { - e.0 = v; + if let Some(v) = plane.intersect([&v0, &v1]) { + if v0_inside { + v1 = v; + } else if v1_inside { + v0 = v; } } } - out.push(e); + out.push(Edge(v0, v1)); } + out.into_iter() } } -impl Clip for [Tri>] { - type Item = Tri>; - - fn clip(&self, planes: &[ClipPlane], out: &mut Vec) { - debug_assert!(out.is_empty()); - - // Avoid unnecessary allocations by reusing these - let mut verts_in = Vec::with_capacity(10); - let mut verts_out = Vec::with_capacity(10); +impl Clip for Tri> { + fn clip<'a, I>( + tris: I, + planes: &'a [ClipPlane], + ) -> impl Iterator + where + I: IntoIterator, + { + // Avoid unnecessary allocations by reusing these. + // + // Clipping an N-gon against M planes can result in a polygon of at most + // N+M sides, so for a triangle against a frustum the result is at most + // a 9-gon and when triangulated, at most seven triangles. + let mut verts_in = Vec::with_capacity(9); + let mut verts_out = Vec::with_capacity(9); + let mut tris_out = Vec::with_capacity(7); + + let mut tris_in = tris.into_iter(); + // TODO Could use a custom named iterator type + iter::from_fn(move || { + 'next_tri: loop { + // If there are buffered output triangles pending, just pop and + // return one. The order does not matter, LIFO is fine. + if let tri_out @ Some(_) = tris_out.pop() { + return tri_out; + } - for tri @ Tri(vs) in self { - match status(vs) { - Status::Visible => { - out.push(tri.clone()); - continue; + // Otherwise, get the next input tri; if there are none, + // we're done because there are no pending output tris either. + let Some(tri_in) = tris_in.next() else { + return None; + }; + + // TODO This status check could be moved to clip_simple_polygon + match status(&tri_in.0) { + // If the input tri is fully visible, just return it + Status::Visible => return Some(tri_in), + // Otherwise if it's fully hidden, ignore it and try the + // next one + Status::Hidden => continue 'next_tri, + // Otherwise, it needs clipping + Status::Clipped => { /* go ahead and clip */ } } - Status::Hidden => continue, - Status::Clipped => { /* go on and clip */ } - } - verts_in.extend(vs.clone()); - clip_simple_polygon(planes, &mut verts_in, &mut verts_out); + verts_in.extend(tri_in.0); + clip_simple_polygon(planes, &mut verts_in, &mut verts_out); - if let [p, rest @ ..] = &verts_out[..] { // Clipping a triangle results in an n-gon, where n depends on // how many planes the triangle intersects. For example, here // clipping triangle ABC generated three new vertices, resulting @@ -388,18 +425,20 @@ impl Clip for [Tri>] { // |.../.....\ // / |./.........\ // |/............\ - // C _ _ _R_______________A + // C _ _ _ R_______________A // | // | // - out.extend( - rest.array_windows() - .cloned() - .map(|[a, b]| Tri([p.clone(), a, b])), - ); + if let [p, rest @ ..] = &verts_out[..] { + tris_out.extend( + rest.array_windows() + .cloned() + .map(|[a, b]| Tri([p.clone(), a, b])), + ); + } + verts_out.clear(); } - verts_out.clear(); - } + }) } } @@ -425,6 +464,10 @@ mod tests { Tri([a, b, c]).map(vtx) } + fn poly(pts: [ClipVec; N]) -> [ClipVert; N] { + pts.map(vtx) + } + #[test] fn signed_distance() { assert_eq!(FAR_PLANE.signed_dist(&vec(0.0, 0.0, -1.0)), -2.0); @@ -456,279 +499,317 @@ mod tests { assert_eq!(outcode(&vec(-2.0, 0.0, 2.0)), 0b00_01_10); } - #[test] - fn edge_clip_inside() { - let e = [vec(2.0, 0.0, -1.0), vec(-1.0, 1.0, 1.0)].map(vtx); - let mut res = vec![]; - FAR_PLANE.clip_simple_polygon(&e, &mut res); - assert_eq!(res, e); - } - #[test] - fn edge_clip_outside() { - let e = [vec(2.0, 0.0, 1.5), vec(-1.0, 1.0, 2.0)].map(vtx); - let mut res = vec![]; - FAR_PLANE.clip_simple_polygon(&e, &mut res); - assert_eq!(res, []); - } - #[test] - fn edge_clip_in_out() { - let e = [vec(2.0, 0.0, 0.0), vec(-1.0, 1.0, 2.0)].map(vtx); - let mut res = vec![]; - FAR_PLANE.clip_simple_polygon(&e, &mut res); - // clip_simple_polygon treats a single edge as a degenerate polygon, - // inserting an additional vertex - assert_eq!(res[..2], [e[0], vtx(vec(0.5, 0.5, 1.0))]); - } - #[test] - fn edge_clip_out_in() { - let e = [vec(2.0, 0.0, 4.0), vec(-1.0, 1.0, 0.0)].map(vtx); - let mut res = vec![]; - FAR_PLANE.clip_simple_polygon(&e, &mut res); - // clip_simple_polygon treats a single edge as a degenerate polygon, - // inserting an additional vertex - assert_eq!(res[..2], [vtx(vec(-0.25, 0.75, 1.0)), e[1]]); - } + /// Clipping against a single plane + mod far_plane { + use super::*; - #[test] - fn tri_clip_fully_inside() { - let tri = - tri(vec(0.0, -1.0, 0.0), vec(2.0, 0.0, 0.5), vec(-1.0, 1.5, 0.0)); - let res = &mut vec![]; - [tri].clip(&[FAR_PLANE], res); - assert_eq!(res, &[tri]); - } - #[test] - fn tri_clip_fully_outside() { - let tri = - tri(vec(0.0, -1.0, 1.5), vec(2.0, 0.0, 1.5), vec(-1.0, 1.5, 2.0)); - let res = &mut vec![]; - [tri].clip(&[FAR_PLANE], res); - assert_eq!(res, &[]); - } + // Clipping degenerate polys (single edge) + // TODO Probably not very useful tests, should rather test + // the actual Clip impl for Edge... - #[test] - fn tri_clip_inside_on_on() { - // - // 1.0 --on1------------on2-- plane - // \ / - // \ / - // \ / - // 0.0 ins - // -1.0 0.0 1.0 2.0 - let tri = - tri(vec(0.0, -1.0, 0.0), vec(2.0, 0.0, 1.0), vec(-1.0, 1.5, 1.0)); - let res = &mut vec![]; - [tri].clip(&[FAR_PLANE], res); - assert_eq!(res, &[tri]); - } + #[test] + fn edge_inside() { + let e = [vec(2.0, 0.0, -1.0), vec(-1.0, 1.0, 1.0)].map(vtx); + let mut res = vec![]; + FAR_PLANE.clip_simple_polygon(&e, &mut res); + assert_eq!(res, e); + } + #[test] + fn edge_outside() { + let e = poly([vec(2.0, 0.0, 1.5), vec(-1.0, 1.0, 2.0)]); + let mut res = vec![]; + FAR_PLANE.clip_simple_polygon(&e, &mut res); + assert_eq!(res, []); + } + #[test] + fn edge_in_out() { + let e = poly([vec(2.0, 0.0, 0.0), vec(-1.0, 1.0, 2.0)]); + let mut res = vec![]; + FAR_PLANE.clip_simple_polygon(&e, &mut res); + // clip_simple_polygon treats a single edge as a degenerate polygon, + // inserting an additional vertex + assert_eq!(res[..2], [e[0], vtx(vec(0.5, 0.5, 1.0))]); + } + #[test] + fn edge_out_in() { + let e = poly([vec(2.0, 0.0, 4.0), vec(-1.0, 1.0, 0.0)]); + let mut res = vec![]; + FAR_PLANE.clip_simple_polygon(&e, &mut res); + // clip_simple_polygon treats a single edge as a degenerate polygon, + // inserting an additional vertex + assert_eq!(res[..2], [vtx(vec(-0.25, 0.75, 1.0)), e[1]]); + } - #[test] - fn tri_clip_outside_inside_inside() { - // 2.0 out - // | \ - // | \ - // 1.0 -----+---+----- plane - // | \ - // | \ - // 0.0 in1----in2 - // 0.0 1.0 - let out = vec(0.0, 0.0, 2.0); - let in1 = vec(0.0, 1.0, 0.0); - let in2 = vec(1.0, 0.0, 0.0); - let tr = tri(out, in1, in2); - - let res = &mut vec![]; - [tr].clip(&[FAR_PLANE], res); - assert_eq!( - res, - &[ - // Clipping `out` leaves a quadrilateral - tri(vec(0.0, 0.5, 1.0), in1, in2), - tri(vec(0.0, 0.5, 1.0), in2, vec(0.5, 0.0, 1.0)) - ] - ); - } - #[test] - fn tri_clip_outside_on_inside() { - // 2.0 out - // | \ - // | \ - // 1.0 -----+----on--- plane - // | / - // | / - // 0.0 . ins . . . - // 0.0 1.0 - let out = vec(0.0, 0.0, 2.0); - let on = vec(1.0, 0.0, 1.0); - let ins = vec(0.0, -1.0, 0.0); - let tr = tri(out, on, ins); - - let res = &mut vec![]; - [tr].clip(&[FAR_PLANE], res); - assert_eq!(res, &[tri(on, ins, vec(0.0, -0.5, 1.0))]); - } - #[test] - fn tri_clip_outside_on_on() { - // 2.0 out - // | \ - // | \ - // 1.0 ---on2---on1-- plane - // . - // . - // 0.0 . o . . . - // 0.0 1.0 - let out = vec(0.0, 0.0, 2.0); - let on1 = vec(1.0, 0.0, 1.0); - let on2 = vec(0.0, -1.0, 1.0); - let tr = tri(out, on1, on2); - - let res = &mut vec![]; - [tr].clip(&[FAR_PLANE], res); - assert_eq!(res, &[]); - } + #[test] + fn tri_fully_inside() { + let tr = poly([ + vec(0.0, -1.0, 0.0), + vec(2.0, 0.0, 1.0), + vec(-1.0, 1.5, -1.0), + ]); + + let mut res = vec![]; + FAR_PLANE.clip_simple_polygon(&tr, &mut res); + assert_eq!(res, tr); + } - #[test] - fn tri_clip_against_frustum_fully_inside() { - let tr = tri( - vec(-1.0, -1.0, -1.0), - vec(1.0, 1.0, 0.0), - vec(0.0, 1.0, 1.0), - ); - let res = &mut vec![]; - [tr].clip(&PLANES, res); - assert_eq!(res, &[tr]); - } - #[test] - fn tri_clip_against_frustum_fully_outside() { - // z - // ^ - // 2-------0 - // · \ | - // --1---+ | - // · | \ | - // + - 1 - 2 - - > x - // | - // ------+ - - let tr = - tri(vec(2.0, 2.0, 2.0), vec(2.0, -2.0, 0.0), vec(0.0, -1.0, 2.0)); - - let res = &mut vec![]; - [tr].clip(&PLANES, res); - assert_eq!(res, &[]); - } - #[test] - fn tri_clip_against_frustum_result_is_quad() { - // z - // ^ - // 2 - // | \ - // - 1---+ - // | | \ - // 0---1---2 - - > x - // | - // - ----+ - - let tr = - tri(vec(0.0, 0.0, 0.0), vec(2.0, 0.0, 0.0), vec(0.0, 0.0, 2.0)); - - let res = &mut vec![]; - [tr].clip(&PLANES, res); - assert_eq!( - res, - &[ - tri(vec(0.0, 0.0, 0.0), vec(1.0, 0.0, 0.0), vec(1.0, 0.0, 1.0)), - tri(vec(0.0, 0.0, 0.0), vec(1.0, 0.0, 1.0), vec(0.0, 0.0, 1.0)) - ] - ); - } + #[test] + fn tri_fully_outside() { + let tr = poly([ + vec(0.0, -1.0, 1.5), + vec(2.0, 0.0, 1.0 + 1e-6), + vec(-1.0, 1.5, 2.0), + ]); + + let mut res = vec![]; + FAR_PLANE.clip_simple_polygon(&tr, &mut res); + assert_eq!(res, []); + } - #[test] - fn tri_clip_against_frustum_result_is_heptagon() { - // z - // ^ 2 - // · / / - // +---/---+ / - // / · |/ - // 0 | · o · / · · > x - // \ · /| - // +-\---/-+ - // 1 - - let tr = - tri(vec(-1.5, 0.0, 0.0), vec(0.0, 0.0, -1.5), vec(2.0, 0.0, 2.0)); - - let res = &mut vec![]; - [tr].clip(&PLANES, res); - - // 7 intersection points -> clipped shape made of 5 triangles - assert_eq!(res.len(), 5); - assert!(res.iter().all(in_bounds)); + #[test] + fn tri_inside_on_on() { + // + // 1.0 --on1------------on2-- plane + // \ / + // \ / + // \ / + // 0.0 ins + // -1.0 0.0 1.0 2.0 + let ins = vec(0.0, -1.0, 0.0); + let on1 = vec(2.0, 0.0, 1.0); + let on2 = vec(-1.0, 1.5, 1.0); + + let tr = poly([ins, on1, on2]); + + let mut res = vec![]; + FAR_PLANE.clip_simple_polygon(&tr, &mut res); + assert_eq!(res, tr); + } + + #[test] + fn tri_outside_inside_inside() { + // 2.0 out + // | \ + // | \ + // 1.0 -----p---q----- plane + // | \ + // | \ + // 0.0 in1----in2 + // 0.0 1.0 + let out = vec(0.0, 0.0, 2.0); + let in1 = vec(0.0, 1.0, 0.0); + let in2 = vec(1.0, 0.0, 0.0); + + let tr = poly([out, in1, in2]); + + let mut res = vec![]; + FAR_PLANE.clip_simple_polygon(&tr, &mut res); + let p = vec(0.0, 0.5, 1.0); + let q = vec(0.5, 0.0, 1.0); + // Clipping `out` leaves a quadrilateral + assert_eq!(res, poly([p, in1, in2, q])); + } + #[test] + fn tri_outside_on_inside() { + // 2.0 out + // | \ + // | \ + // 1.0 -----p----on--- plane + // | / + // | / + // 0.0 . ins . . . + // 0.0 1.0 + let out = vec(0.0, 0.0, 2.0); + let on = vec(1.0, 0.0, 1.0); + let ins = vec(0.0, -1.0, 0.0); + + let tr = poly([out, on, ins]); + + let mut res = vec![]; + FAR_PLANE.clip_simple_polygon(&tr, &mut res); + let p = vec(0.0, -0.5, 1.0); + assert_eq!(res, poly([on, ins, p])); + } + #[test] + fn tri_outside_on_on() { + // 2.0 out + // | \ + // | \ + // 1.0 ---on2---on1-- plane + // . + // . + // 0.0 . o . . . + // 0.0 1.0 + let out = vec(0.0, 0.0, 2.0); + let on1 = vec(1.0, 0.0, 1.0); + let on2 = vec(0.0, -1.0, 1.0); + let tr = poly([out, on1, on2]); + + let mut res = vec![]; + FAR_PLANE.clip_simple_polygon(&tr, &mut res); + // Returns a degenerate poly with a single vertex, such polys are + // discarded by the Tri Clip impl which only returns full triangles + assert_eq!(res, poly([on1, on2])); + } } - #[test] - #[allow(unused)] - fn tri_clip_against_frustum_all_cases() { - // Methodically go through every possible combination of every - // vertex inside/outside every plane, including degenerate cases. - - let xs = || (-2.0).vary(1.0, Some(5)); - - let pts: Vec<_> = xs() - .flat_map(move |x| { - xs().flat_map(move |y| xs().map(move |z| vec(x, y, z))) - }) - .collect(); - - let tris = pts.iter().flat_map(|a| { - pts.iter() - .flat_map(|b| pts.iter().map(|c| tri(*a, *b, *c))) - }); - - 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\ - input: {:#?}\n\ - output: {:#?}", - tr, - &res + /// Clipping against the whole view frustum (six planes) + mod frustum { + use super::*; + + #[test] + fn tri_fully_inside_result_no_change() { + let tr = tri( + vec(-1.0, -1.0, -1.0), + vec(1.0, 1.0, 0.0), + vec(0.0, 1.0, 1.0), ); - 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(); + let mut res = Tri::clip([tr], &PLANES); + assert_eq!(res.next(), Some(tr)); + assert_eq!(res.next(), None); } - #[cfg(feature = "std")] - { - use std::dbg; - dbg!(in_tris); - dbg!(in_degen); - dbg!(out_degen); - dbg!(out_total); + #[test] + fn tri_fully_outside_result_empty() { + // z + // ^ + // 2-------0 + // · \ | + // --1---+ | + // · | \ | + // + - 1 - 2 - - > x + // | + // ------+ + + let tr = tri( + vec(2.0, 2.0, 2.0), + vec(2.0, -2.0, 0.0), + vec(0.0, -1.0, 2.0), + ); + assert_eq!(Tri::clip([tr], &PLANES).next(), None); + } + #[test] + fn tri_result_is_quad() { + // z + // ^ + // 2 + // | \ + // - 1---+ + // | | \ + // 0---1---2 - - > x + // | + // - ----+ + + let tr = + tri(vec(0.0, 0.0, 0.0), vec(2.0, 0.0, 0.0), vec(0.0, 0.0, 2.0)); + + let mut res = Tri::clip([tr], &PLANES); + assert_eq!( + res.next(), + Some(tri( + vec(0.0, 0.0, 0.0), + vec(1.0, 0.0, 1.0), + vec(0.0, 0.0, 1.0) + )) + ); + assert_eq!( + res.next(), + Some(tri( + vec(0.0, 0.0, 0.0), + vec(1.0, 0.0, 0.0), + vec(1.0, 0.0, 1.0) + )) + ); + assert_eq!(res.next(), None); } - assert_eq!(in_tris, 5i32.pow(9)); - assert_eq!( - out_tris, - [559754, 536199, 537942, 254406, 58368, 6264, 192, 0] - ); - } - fn is_degenerate(Tri([a, b, c]): &Tri>) -> bool { - a.pos == b.pos || a.pos == c.pos || b.pos == c.pos - } + #[test] + fn tri_result_is_heptagon() { + // z + // ^ 2 + // · / / + // +---/---+ / + // / · |/ + // 0 | · o · / · · > x + // \ · /| + // +-\---/-+ + // 1 + + let tr = tri( + vec(-1.5, 0.0, 0.0), + vec(0.0, 0.0, -1.5), + vec(2.0, 0.0, 2.0), + ); + + let res = Tri::clip([tr], &PLANES).collect::>(); - fn in_bounds(Tri(vs): &Tri>) -> bool { - vs.iter() - .flat_map(|v| (v.pos / v.pos.w()).0) - .all(|a| a.abs() <= 1.00001) + // 7 intersection points -> clipped shape made of 5 triangles + assert_eq!(res.len(), 5); + assert!(res.iter().all(in_bounds)); + } + + #[test] + #[allow(unused)] + fn tri_all_cases() { + // Methodically go through every possible combination of every + // vertex inside/outside every plane, including degenerate cases. + + let xs = || (-2.0).vary(1.0, Some(5)); + + let pts: Vec<_> = xs() + .flat_map(move |x| { + xs().flat_map(move |y| xs().map(move |z| vec(x, y, z))) + }) + .collect(); + + let tris = pts.iter().flat_map(|a| { + pts.iter() + .flat_map(|b| pts.iter().map(|c| tri(*a, *b, *c))) + }); + + 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 = Tri::clip([tr], &PLANES).collect::>(); + assert!( + res.iter().all(in_bounds), + "clip returned oob vertex:\n\ + input: {:?}\n\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!( + out_tris, + [559754, 536199, 537942, 254406, 58368, 6264, 192, 0] + ); + } + + 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 f3ed5fcb..f83f02cb 100644 --- a/core/src/render/ctx.rs +++ b/core/src/render/ctx.rs @@ -1,11 +1,9 @@ //! Rendering context and parameters. -use core::{cell::RefCell, cmp::Ordering}; +use core::cmp::Ordering; use crate::math::{Color4, rgba}; -use super::Stats; - /// Context and parameters used by the renderer. #[derive(Clone, Debug)] pub struct Context { @@ -61,7 +59,8 @@ pub struct Context { pub depth_write: bool, /// Collecting rendering statistics. - pub stats: RefCell, + #[cfg(feature = "stats")] + pub stats: core::cell::RefCell, } /// Whether to sort faces front to back or back to front. @@ -121,7 +120,8 @@ impl Default for Context { color_write: true, depth_test: Some(Ordering::Less), depth_write: true, - stats: Default::default(), + #[cfg(feature = "stats")] + stats: super::Stats::new().into(), } } } diff --git a/core/src/render/debug.rs b/core/src/render/debug.rs index 4ce276ce..c57e24cc 100644 --- a/core/src/render/debug.rs +++ b/core/src/render/debug.rs @@ -1,11 +1,9 @@ //! Routines for drawing wireframe visualizations of geometric objects //! for debugging purposes. Includes normals, bounding boxes, and more. -#[cfg(feature = "fp")] use alloc::vec::Vec; -use core::fmt::Debug; -use crate::geom::{Edge, Tri, Vertex, Vertex3, vertex}; +use crate::geom::{Edge, Mesh, Normal3, Pos, Tri, Vertex, Vertex3, vertex}; use crate::math::{ Color, Color4, Color4f, Mat4, Point3, Vec3, color::gray, mat::ProjMat3, pt3, vec::ProjVec3, @@ -15,29 +13,28 @@ use crate::math::{Vary, polar, turns, vec3}; use super::{Context, Frag, FragmentShader, VertexShader, scene::BBox}; -#[derive(Default)] +#[derive(Copy, Clone, Default)] pub struct Shader; -impl<'a, B> VertexShader, &'a ProjMat3> for Shader { - type Output = Vertex; - - fn shade_vertex( - &self, - v: Vertex3, - m: &'a ProjMat3, - ) -> Self::Output { - vertex(m.apply(&v.pos), v.attrib) - } -} - -impl FragmentShader for Shader { - fn shade_fragment(&self, f: Frag) -> Option { - Some(f.var.to_color4()) - } -} +#[derive(Clone, Default)] +/// A type used to draw debug visualizations of meshes. +/// +/// Various mesh properties can be visualized: +/// * Edges ("wireframe" rendering) +/// * Face normals +/// * Vertex normals (if any) +/// * Bounding box +/// * Model-space origin and coordinate axes. +pub struct DbgMesh(Mesh, DbgBatch); -pub type DbgBatch = - super::Batch, Vertex3, (), Shader, (), Context>; +pub type DbgBatch = super::Batch< + Vec>, + Vec>, + (), + Shader, + (), + Context, +>; /// Returns a color visualizing the direction of a vector. /// @@ -60,37 +57,22 @@ pub fn dir_to_rgb(v: Vec3) -> Color4f { } /// Draws an illustration of a ray. -pub fn ray(o: Point3, dir: Vec3) -> DbgBatch { - let mut b = dir.cross(&Vec3::Y); - if b.len_sqr() < 1e-6 { - b = dir.cross(&Vec3::X); - } - let b = b.normalize_or_zero(); - let c = dir.cross(&b).normalize_or_zero(); - - let (head_w, head_h) = (0.04, 0.1); - let a = o + dir - head_h * dir.normalize_or_zero(); - let b = head_w * b; - let c = head_w * c; - - let verts = [o, o + dir, a + b, a - b, a + c, a - c] - .map(|p| vertex(p, dir_to_rgb(dir))); - #[rustfmt::skip] - let edges = [ - [0, 1], [1, 2], [1, 3], [1, 4], [1, 5], - [2, 4], [2, 5], [3, 4], [3, 5], - ].map(Edge::from); +pub fn ray(orig: Point3, dir: Vec3) -> DbgBatch { + let mut batch = DbgBatch::new(); + batch.ray(orig, dir); + batch +} - DbgBatch::new(&edges, &verts) +/// Draws a unit-length ray denoting the normal vector of a vertex. +pub fn vertex_normal(v: &Vertex3, scale: f32) -> DbgBatch { + ray(v.pos, scale * v.attrib.to()) } /// Draws a unit-length ray denoting the normal vector of a triangle. /// /// The ray originates from the triangle's centroid. -pub fn face_normal( - tri: &Tri>, -) -> DbgBatch { - ray(tri.centroid(), tri.normal().to()) +pub fn face_normal(tri: &Tri>, scale: f32) -> DbgBatch { + ray(tri.centroid(), scale * tri.normal().to()) } /// Draws a visualization of an affine basis. @@ -128,15 +110,97 @@ pub fn cuboid(v0: Point3, v1: Point3) -> DbgBatch { [0, 4], [1, 5], [2, 6], [3, 7], ].map(Edge::from); - DbgBatch::new(&edges, &verts) + DbgBatch::with(edges.to_vec(), verts.to_vec()) } /// Draws the smallest axis-aligned box that contains a set of vertices. -pub fn bbox(vs: &[Vertex3]) -> DbgBatch { - let BBox(min, max) = vs.iter().map(|v| &v.pos).collect(); +pub fn bbox(pts: &[impl Pos>]) -> DbgBatch { + let BBox(min, max) = pts.iter().map(Pos::pos).collect(); cuboid(min, max) } +/// Creates a `DbgMesh` object from a mesh. +pub fn mesh(mesh: Mesh) -> DbgMesh { + DbgMesh(mesh, DbgBatch::default()) +} + +// +// Inherent impls +// + +impl DbgMesh { + /// Enables drawing the edges as a wireframe representation. + #[must_use] + pub fn edges(mut self) -> Self { + let Mesh { faces, verts } = &self.0; + + let n_verts = self.1.verts.len(); + let edges = faces + .iter() + .flat_map(Tri::edges) + .map(|Edge(a, b)| Edge(a + n_verts, b + n_verts)); + let verts = verts + .iter() + .map(|v| vertex(v.pos, dir_to_rgb(v.pos.to_vec()))); + + self.1.prims.extend(edges); + self.1.verts.extend(verts); + + self + } + + /// Enables drawing the normal vectors of the faces. + #[must_use] + pub fn face_normals(mut self, scale: f32) -> Self { + for tri in self.0.faces() { + self.1.face_normal(&tri.map(Clone::clone), scale); + } + self + } + + /// Enables drawing the local-space bounding box of the mesh. + #[must_use] + pub fn bbox(mut self) -> Self { + self.1.append(bbox(&self.0.verts)); + self + } + + /// Enables drawing the coordinate axes of the local space. + #[must_use] + pub fn basis(mut self) -> Self { + self.1.append(basis(Mat4::::identity())); + self + } + + /// Returns a batch for rendering the enabled visualizations. + #[must_use] + pub fn batch(&self) -> &DbgBatch { + &self.1 + } +} + +impl DbgMesh { + #[must_use] + pub fn vertex_normals(mut self, scale: f32) -> Self { + for v in &self.0.verts { + self.1.vertex_normal(v, scale); + } + self + } +} + +/// Draws a wireframe representation of a mesh. +/// +/// This is a convenience shortcut for +/// ```text +/// # use retrofire_core::render::debug; +/// debug::mesh(mesh).edges().patch() +/// ``` +#[must_use] +pub fn wireframe(m: Mesh) -> DbgBatch { + mesh(m).edges().batch().clone() +} + /// Draws a circle on the XY plane with the given center and radius. #[cfg(feature = "fp")] pub fn circle(o: Point3, r: f32) -> DbgBatch { @@ -152,7 +216,7 @@ pub fn circle(o: Point3, r: f32) -> DbgBatch { let edges: Vec<_> = (0..RES).map(|i| Edge(i, i + 1)).collect(); - DbgBatch::new(&edges, &verts) + DbgBatch::with(edges, verts) } /// Draws a wireframe sphere with the given center and radius. @@ -182,14 +246,88 @@ pub fn sphere(o: Point3, r: f32) -> DbgBatch { }) .collect(); - DbgBatch::new(&edges, &verts) + DbgBatch::with(edges, verts) } impl DbgBatch { - fn new(prims: &[Edge], verts: &[Vertex3]) -> Self { + fn new() -> Self { + DbgBatch::::default() + } + + fn with(prims: Vec>, verts: Vec>) -> Self { DbgBatch::::default() .primitives(prims) .vertices(verts) - .shader(Shader) + } + + fn ray(&mut self, orig: Point3, dir: Vec3) -> &mut Self { + let mut b = dir.cross(&Vec3::Y); + if b.len_sqr() < 1e-6 { + b = dir.cross(&Vec3::X); + } + let b = b.normalize_or_zero(); + let c = dir.cross(&b).normalize_or_zero(); + + let (head_w, head_h) = (0.02, 0.04); + let a = orig + dir - head_h * dir.normalize_or_zero(); + let b = head_w * b; + let c = head_w * c; + + let verts = [orig, orig + dir, a + b, a - b, a + c, a - c] + .map(|p| vertex(p, dir_to_rgb(dir))); + #[rustfmt::skip] + let edges = [ + [0, 1], [1, 2], [1, 3], [1, 4], [1, 5], + [2, 4], [2, 5], [3, 4], [3, 5], + ].map(Edge::from); + + let n = self.verts.len(); + self.verts.extend(verts); + self.prims + .extend(edges.into_iter().map(|e| Edge(e.0 + n, e.1 + n))); + + self + } + + fn vertex_normal( + &mut self, + v: &Vertex3, + scale: f32, + ) -> &mut Self { + self.ray(v.pos, scale * v.attrib.to()) + } + + fn face_normal( + &mut self, + tri: &Tri>, + scale: f32, + ) -> &mut Self { + self.ray(tri.centroid(), scale * tri.normal().to()) + } +} + +// +// Trait impls +// + +impl<'a, B> VertexShader, &'a ProjMat3> for Shader { + type Output = Vertex; + + fn shade_vertex( + &self, + v: Vertex3, + m: &'a ProjMat3, + ) -> Self::Output { + vertex(m.apply(&v.pos), v.attrib) + } +} + +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/impls.rs b/core/src/render/impls.rs new file mode 100644 index 00000000..4e051395 --- /dev/null +++ b/core/src/render/impls.rs @@ -0,0 +1,160 @@ +use alloc::vec::Vec; +use core::{iter, mem::replace}; + +use crate::{ + geom::{Edge, Mesh, Polyline, Tri, Vertex, Vertex3, tri}, + math::{ProjVec3, Vary}, +}; + +use super::{ + ClipVert, Indexed, Primitive, Render, TriFan, VertexShader, + vertex_transform, +}; + +impl Render> for Tri> +where + V: Vary + 'static, + Vertex: Clone, +{ + fn to_primitives( + &self, + shader: &Shd, + uniform: Uni, + ) -> impl Iterator>> + where + Shd: VertexShader, Uni, Output = Vertex>, + Uni: Copy, + { + iter::once( + self.clone() + .map(|v| ClipVert::new(shader.shade_vertex(v, uniform))), + ) + } +} + +impl Render>, Vertex> + for Vec>> +where + V: Vary + 'static, + Vertex: Clone, +{ + fn to_primitives( + &self, + shader: &Shd, + uniform: Uni, + ) -> impl Iterator>> + where + Shd: VertexShader, Uni, Output = Vertex>, + Uni: Copy, + { + self.iter().map(move |tri| { + tri.clone() + .map(move |v| ClipVert::new(shader.shade_vertex(v, uniform))) + }) + } +} + +impl Render>, Vertex> + for TriFan> +where + V: Vary + 'static, + Vertex: Clone, +{ + fn to_primitives( + &self, + shader: &Shd, + uniform: Uni, + ) -> impl Iterator>> + where + Shd: VertexShader, Uni, Output = Vertex>, + Uni: Copy, + { + let verts = vertex_transform(shader, uniform, &self.0); + let mut it = verts.into_iter(); + + let a = it.next().unwrap(); + let mut b = it.next().unwrap(); + iter::from_fn(move || { + let c = it.next()?; + let b = replace(&mut b, c.clone()); + Some(tri(a.clone(), b, c)) + }) + } +} + +impl Render>, Vertex> + for Polyline> +where + V: Vary + 'static, + Vertex: Clone, +{ + fn to_primitives( + &self, + shader: &Shd, + uniform: Uni, + ) -> impl Iterator>> + where + Shd: VertexShader, Uni, Output = Vertex>, + Uni: Copy, + { + let mut it = self + .0 + .iter() + .cloned() + .map(move |v| ClipVert::new(shader.shade_vertex(v, uniform))); + // Collect to avoid lifetime bound on Uni + + let mut a = it.next().unwrap(); + iter::from_fn(move || { + let b = it.next()?; + let a = replace(&mut a, b.clone()); + Some(Edge(a.clone(), b)) + }) + } +} + +impl Render, Vertex3> for Mesh +where + V: Vary + 'static, + Vertex3: Clone, +{ + fn to_primitives( + &self, + shader: &Shd, + uniform: Uni, + ) -> impl Iterator>> + where + Shd: VertexShader, Uni, Output = Vertex>, + Uni: Copy, + { + let verts = vertex_transform(shader, uniform, &self.verts); + self.faces + .iter() + .cloned() + .map(move |prim| Tri::inline(prim, &verts)) + } +} + +impl<'a, V, Prim, Vert> Render + for Indexed<&'a [Prim], &'a [Vert]> +where + V: Vary + 'static, + Prim: Primitive + Clone, + Vert: Clone, +{ + fn to_primitives( + &self, + shader: &Shd, + uniform: Uni, + ) -> impl Iterator + where + Shd: VertexShader>, + Uni: Copy, + { + let verts = vertex_transform(shader, uniform, self.verts); + self.prims + .iter() + .cloned() + .map(move |prim| Prim::inline(prim, &verts)) + } +} diff --git a/core/src/render/light.rs b/core/src/render/light.rs new file mode 100644 index 00000000..a1d14e82 --- /dev/null +++ b/core/src/render/light.rs @@ -0,0 +1,130 @@ +//! 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 { + if let Kind::Directional(dir) | Kind::Spot { dir, .. } = &mut kind { + *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) | Kind::Spot { pos, .. } => { + (pos - pt).normalize_approx() + } + Kind::Directional(dir) => dir, + } + } + + #[inline] + pub fn eval(&self, pt: Point3) -> (Color3f, Vec3) { + let pt_dir = self.direction(pt); + let color = match self.kind { + Kind::Point(_) | 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 { color, kind, 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/prim.rs b/core/src/render/prim.rs index 5e546ab2..28c7ece6 100644 --- a/core/src/render/prim.rs +++ b/core/src/render/prim.rs @@ -1,17 +1,80 @@ //! Render impls for primitives and related items. -use crate::geom::{Edge, Tri, Vertex, Winding}; -use crate::math::{Mat4, Vary, pt3, vary::ZDiv}; +use crate::{ + geom::{Edge, Tri, Vertex, Winding}, + math::{Mat4, Vary, pt3, vary::ZDiv}, +}; use super::{ - Ndc, Render, Screen, + Clip, Ndc, Screen, clip::ClipVert, raster::{Scanline, ScreenPt, line, tri_fill}, }; -impl Render for Tri { +/// Renderable geometric primitive. +pub trait Primitive { + /// The type of this primitive in clip space + type Clip: Clip; + + /// The type of this primitive in screen space. + type Screen; + + /// Maps the indices of the argument to vertices. + fn inline(ixd: Self, vs: &[ClipVert]) -> Self::Clip; + + /// Returns the (average) depth of the argument. + fn depth(_clip: &Self::Clip) -> f32 { + f32::INFINITY + } + + /// Returns whether the argument is facing away from the camera. + fn is_backface(_: &Self::Screen) -> bool { + false + } + + /// Transforms the argument from NDC to screen space. + fn to_screen(clip: Self::Clip, tf: &Mat4) -> Self::Screen; + + /// Rasterizes the argument by calling the function for each scanline. + fn rasterize)>(scr: Self::Screen, scanline_fn: F); +} + +impl Primitive for Tri> { + type Clip = Tri>; + type Screen = Tri>; + + fn inline(_: Self, _: &[ClipVert]) -> Self::Clip { + unimplemented!("should never be called for this impl") + } + + fn to_screen(clip: Self::Clip, tf: &Mat4) -> Self::Screen { + Tri(to_screen(clip.0, tf)) + } + + fn rasterize)>(scr: Self::Screen, scanline_fn: F) { + tri_fill(scr.0, scanline_fn); + } +} + +impl Primitive for Edge> { + type Clip = Edge>; + type Screen = Edge>; + + fn inline(_: Self, _: &[ClipVert]) -> Self::Clip { + unimplemented!("should never be called for this impl") + } + + fn to_screen(clip: Self::Clip, tf: &Mat4) -> Self::Screen { + to_screen([clip.0, clip.1], tf).into() + } + + fn rasterize)>(scr: Self::Screen, scanline_fn: F) { + line([scr.0, scr.1], scanline_fn); + } +} + +impl Primitive for Tri { type Clip = Tri>; - type Clips = [Tri>]; type Screen = Tri>; #[inline] @@ -26,7 +89,7 @@ impl Render for Tri { #[inline] fn is_backface(tri: &Self::Screen) -> bool { - tri.winding() == Winding::Cw + tri.winding_xy() == Winding::Cw } #[inline] @@ -43,11 +106,8 @@ impl Render for Tri { } } -impl Render for Edge { +impl Primitive for Edge { type Clip = Edge>; - - type Clips = [Self::Clip]; - type Screen = Edge>; #[inline] diff --git a/core/src/render/raster.rs b/core/src/render/raster.rs index e7347bad..8373ab4a 100644 --- a/core/src/render/raster.rs +++ b/core/src/render/raster.rs @@ -13,6 +13,7 @@ use core::{ fmt::{Debug, Formatter}, + iter::zip, mem::swap, ops::Range, }; @@ -142,10 +143,12 @@ where // Adjust y0 to match the rounded x0 let y0 = v0.pos.y() + dy_dx * (x0 - v0.pos.x()); + let vs = + (v0.pos, v0.attrib).vary_to((v1.pos, v1.attrib), dx.abs() as u32); + let (xs, mut y) = (x0 as usize..x1 as usize, y0); - for x in xs { - let vs = (v0.pos, v0.attrib.clone()); - let vs = vs.clone().vary_to(vs, 1); // TODO a bit silly + for (x, v) in zip(xs, vs) { + let vs = v.clone().vary_to(v, 1); // TODO a bit silly scan_fn(Scanline { y: y as usize, xs: x..x + 1, @@ -162,10 +165,12 @@ where // Adjust x0 to match the rounded y0 let x0 = v0.pos.x() + dx_dy * (y0 - v0.pos.y()); + let vs = (v0.pos, v0.attrib).vary_to((v1.pos, v1.attrib), dy as u32); + let mut x = x0; - for y in y0 as usize..y1 as usize { - let vs = (v0.pos, v0.attrib.clone()); - let vs = vs.clone().vary_to(vs.clone(), 1); + let ys = y0 as usize..y1 as usize; + for (y, v) in zip(ys, vs) { + let vs = v.clone().vary_to(v, 1); // silly... scan_fn(Scanline { y, xs: x as usize..x as usize + 1, @@ -315,7 +320,7 @@ mod tests { assert_approx_eq, geom::vertex, math::{point::pt3, vary::Vary, vary::ZDiv}, - util::buf::Buf2, + util::{Buf2, Dims}, }; use super::{Scanline, tri_fill}; @@ -324,7 +329,7 @@ mod tests { #[test] fn shared_edge_should_not_have_gaps_or_overdraw() { - let mut buf = Buf2::new((20, 10)); + let mut buf = Buf2::new(Dims(20, 10)); let verts = [ pt3(8.0, 0.0, 0.0), diff --git a/core/src/render/scene.rs b/core/src/render/scene.rs index 261e2251..d78326f7 100644 --- a/core/src/render/scene.rs +++ b/core/src/render/scene.rs @@ -102,9 +102,9 @@ impl Default for Obj { /// Returns an empty `Obj`. fn default() -> Self { Self { - geom: Default::default(), - bbox: Default::default(), - tf: Default::default(), + geom: Mesh::default(), + bbox: BBox::default(), + tf: Mat4::default(), } } } diff --git a/core/src/render/shader.rs b/core/src/render/shader.rs index b2560654..c7e7b759 100644 --- a/core/src/render/shader.rs +++ b/core/src/render/shader.rs @@ -45,13 +45,13 @@ 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; } impl VertexShader for F @@ -66,21 +66,21 @@ where } } -impl FragmentShader for F +impl FragmentShader for F where - F: Fn(Frag) -> Out, + F: Fn(Frag, Uni) -> Out, Out: Into>, { #[inline] - fn shade_fragment(&self, frag: Frag) -> Option { - self(frag).into() + 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, + Fs: FragmentShader, { Shader::new(vs, fs) } @@ -98,7 +98,7 @@ impl Shader { pub const fn new(vs: Vs, fs: Fs) -> Self where Vs: VertexShader>, - Fs: FragmentShader, + Fs: FragmentShader, { Self { vertex_shader: vs, @@ -119,12 +119,12 @@ where } } -impl FragmentShader for Shader +impl FragmentShader for Shader where - Fs: FragmentShader, + Fs: FragmentShader, { #[inline] - fn shade_fragment(&self, frag: Frag) -> Option { - self.fragment_shader.shade_fragment(frag) + fn shade_fragment(&self, frag: Frag, uni: Uni) -> Option { + self.fragment_shader.shade_fragment(frag, uni) } } diff --git a/core/src/render/stats.rs b/core/src/render/stats.rs index 598c6124..00ae4382 100644 --- a/core/src/render/stats.rs +++ b/core/src/render/stats.rs @@ -1,9 +1,12 @@ //! Rendering statistics. use alloc::{format, string::String}; -use core::fmt::{self, Display, Formatter}; -use core::ops::AddAssign; -use core::time::Duration; +use core::{ + fmt::{self, Display, Formatter}, + ops::AddAssign, + time::Duration, +}; + #[cfg(feature = "std")] use std::time::Instant; @@ -12,10 +15,15 @@ use std::time::Instant; // /// Collects and accumulates rendering statistics and performance data. -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug)] pub struct Stats { + #[cfg(feature = "std")] + pub start: Instant, + + /// Wall clock time elapsed. + pub wall_time: Duration, /// Time spent rendering. - pub time: Duration, + pub render_time: Duration, /// Number of render calls issued. pub calls: f32, /// Number of frames rendered. @@ -26,9 +34,6 @@ pub struct Stats { pub prims: Throughput, pub verts: Throughput, pub frags: Throughput, - - #[cfg(feature = "std")] - start: Option, } #[derive(Copy, Clone, Debug, Default)] @@ -46,19 +51,35 @@ pub struct Throughput { impl Stats { /// Creates a new zeroed `Stats` instance. pub fn new() -> Self { - Self::default() + Self { + #[cfg(feature = "std")] + start: Instant::now(), + wall_time: Duration::default(), + render_time: Duration::default(), + calls: 0.0, + frames: 0.0, + objs: Throughput::default(), + prims: Throughput::default(), + verts: Throughput::default(), + frags: Throughput::default(), + } } - /// Creates a `Stats` instance that records the time of its creation. - /// - /// Call [`finish`][Self::finish] to write the elapsed time to `self.time`. - /// Useful for timing frames, rendering calls, etc. - /// - /// Equivalent to [`Stats::new`] if the `std` feature is not enabled. - pub fn start() -> Self { + + pub fn start_call(n_prims: usize, n_verts: usize) -> Self { + Self { + calls: 1.0, + prims: Throughput { i: n_prims, o: 0 }, + verts: Throughput { i: n_verts, o: 0 }, + ..Self::new() + } + } + + #[must_use] + pub fn finish(self) -> Self { Self { #[cfg(feature = "std")] - start: Some(Instant::now()), - ..Self::default() + wall_time: self.start.elapsed(), + ..self } } @@ -67,51 +88,81 @@ impl Stats { /// No-op if the timer was not running. This method is also no-op unless /// the `std` feature is enabled. #[must_use] - pub fn finish(self) -> Self { + pub fn finish_call(mut self, prims_out: usize, verts_out: usize) -> Self { + self.prims.o += prims_out; + self.verts.o += verts_out; Self { #[cfg(feature = "std")] - time: self.start.map_or(self.time, |st| st.elapsed()), + render_time: self.start.elapsed(), ..self } } /// Returns the average throughput in items per second. - pub fn per_sec(&self) -> Self { - let secs = if self.time.is_zero() { + #[must_use] + pub fn per_render_sec(&self) -> Self { + let secs = if self.render_time.is_zero() { 1.0 } else { - self.time.as_secs_f32() + self.render_time.as_secs_f32() }; let [objs, prims, verts, frags] = self.throughput().map(|stat| stat.per_sec(secs)); Self { + #[cfg(feature = "std")] + start: self.start, + render_time: Duration::from_secs(1), + wall_time: self.wall_time.div_f32(secs), frames: self.frames / secs, calls: self.calls / secs, - time: Duration::from_secs(1), objs, prims, verts, frags, + } + } + + #[must_use] + pub fn per_wall_sec(&self) -> Self { + let secs = if self.wall_time.is_zero() { + 1.0 + } else { + self.wall_time.as_secs_f32() + }; + let [objs, prims, verts, frags] = + self.throughput().map(|stat| stat.per_sec(secs)); + Self { #[cfg(feature = "std")] - start: None, + start: self.start, + wall_time: Duration::from_secs(1), + render_time: self.render_time.div_f32(secs), + frames: self.frames / secs, + calls: self.calls / secs, + objs, + prims, + verts, + frags, } } + /// Returns the average throughput in items per frame. + #[must_use] pub fn per_frame(&self) -> Self { - let frames = self.frames.max(1.0); + let frames = self.frames.max(1.0) as u32; let [objs, prims, verts, frags] = self .throughput() .map(|stat| stat.per_frame(frames)); Self { + #[cfg(feature = "std")] + start: self.start, frames: 1.0, - calls: self.calls / frames, - time: self.time.div_f32(frames), + render_time: self.render_time / frames, + wall_time: self.wall_time / frames, + calls: self.calls / frames as f32, objs, prims, verts, frags, - #[cfg(feature = "std")] - start: None, } } @@ -132,7 +183,7 @@ impl Throughput { o: (self.o as f32 / secs) as usize, } } - fn per_frame(&self, frames: f32) -> Self { + fn per_frame(&self, frames: u32) -> Self { Self { i: self.i / frames as usize, o: self.o / frames as usize, @@ -140,37 +191,63 @@ impl Throughput { } } +impl Default for Stats { + fn default() -> Self { + Self::new() + } +} + impl Display for Stats { #[rustfmt::skip] #[inline(never)] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let w = f.width().unwrap_or(16); - let per_s = self.per_sec(); + let per_ws = self.per_wall_sec(); + let per_rs = self.per_render_sec(); let per_f = self.per_frame(); - write!(f, - " STATS {:>w$} │ {:>w$} │ {:>w$}\n\ - ────────{empty:─>w$}─┼─{empty:─>w$}─┼─{empty:─>w$}─\n \ - time {:>w$} │ {empty:w$} │ {:>w$}\n \ - calls {:>w$} │ {:>w$.1} │ {:>w$.1}\n \ - frames {:>w$} │ {:>w$.1} │\n\ - ────────{empty:─>w$}─┼─{empty:─>w$}─┼─{empty:─>w$}─\n", - "TOTAL", "PER SEC", "PER FRAME", - human_time(self.time), human_time(per_f.time), - self.calls, per_s.calls, per_f.calls, - self.frames, per_s.frames, - empty = "" + + let ws_per_rs = self.wall_time.div_duration_f32(self.render_time); + + let w = f.width().unwrap_or(14); + let e = ""; + + writeln!(f, + " STATS {:>w$} │ {:>w$} │ {:>w$} │ {:>w$}\n\ + ───────────{e:─>w$}─┼─{e:─>w$}─┼─{e:─>w$}─┼─{e:─>w$}─", + "TOTAL", "PER WALL-SEC", "PER REND-SEC", "PER FRAME" + )?; + writeln!(f, + " wall-time {:>w$} │ {e:w$} │ {:>w$.2} │ {:>w$}", + human_time(self.wall_time), ws_per_rs, + human_time(per_f.wall_time) + )?; + + + let rs_per_ws = 1.0 / ws_per_rs; + + writeln!(f, + " rend-time {:>w$} │ {:>w$.2} │ {e:w$} │ {:>w$}\n \ + calls {:>w$} │ {:>w$.1} │ {:>w$.1} │ {:>w$.1}\n \ + frames {:>w$} │ {:>w$.1} │ {:>w$.1} │\n\ + ───────────{e:─>w$}─┼─{e:─>w$}─┼─{e:─>w$}─┼─{e:─>w$}─", + + human_time(self.render_time), rs_per_ws, human_time(per_f.render_time), + self.calls, per_ws.calls, per_rs.calls, per_f.calls, + self.frames, per_ws.frames, per_rs.frames, + e = "" )?; let labels = ["objs", "prims", "verts", "frags"]; for (i, lbl) in (0..4).zip(labels) { - let [tot, per_s, per_f] = [self, &per_s, &per_f].map(|s| s.throughput()[i]); + let [tot, per_ws, per_rs, per_f] = + [self, &per_ws, &per_rs, &per_f].map(|s| s.throughput()[i]); if f.alternate() { - writeln!(f, " {lbl:6} {tot:#w$} │ {per_s:#w$} │ {per_f:#w$}")?; + writeln!(f, " {lbl:9} {tot:#w$} │ {per_ws:#w$} │ {per_rs:#w$} │ {per_f:#w$}")?; } else { - writeln!(f, " {lbl:6} {tot:w$} │ {per_s:w$} │ {per_f:w$}")?; + writeln!(f, " {lbl:9} {tot:w$} │ {per_ws:w$} │ {per_rs:w$} │ {per_f:w$}")?; } } + Ok(()) } } @@ -197,7 +274,8 @@ impl Display for Throughput { impl AddAssign for Stats { /// Appends the stats of `other` to `self`. fn add_assign(&mut self, other: Self) { - self.time += other.time; + self.wall_time += other.wall_time; + self.render_time += other.render_time; self.calls += other.calls; self.frames += other.frames; for i in 0..4 { @@ -248,8 +326,7 @@ fn human_time(d: Duration) -> String { #[cfg(test)] mod tests { - use core::array::from_fn; - use core::time::Duration; + use core::{array::from_fn, time::Duration}; use super::*; @@ -262,44 +339,46 @@ mod tests { let stats = Stats { frames: 1234.0, calls: 5678.0, - time: Duration::from_millis(4321), + wall_time: Duration::from_millis(5432), + render_time: Duration::from_millis(4321), objs, prims, verts, frags, - #[cfg(feature = "std")] - start: None, + ..Stats::new() }; assert_eq!( format!("{stats}"), " \ - STATS TOTAL │ PER SEC │ PER FRAME -─────────────────────────┼──────────────────┼────────────────── - time 4.3s │ │ 3.5ms - calls 5678 │ 1314.0 │ 4.6 - frames 1234 │ 285.6 │ -─────────────────────────┼──────────────────┼────────────────── - objs 12.3k / 4.3k │ 2.9k / 1.0k │ 10 / 3 - prims 24.7k / 8.6k │ 5.7k / 2.0k │ 20 / 7 - verts 37.0k / 13.0k │ 8.6k / 3.0k │ 30 / 10 - frags 49.4k / 17.3k │ 11.4k / 4.0k │ 40 / 14 + STATS TOTAL │ PER WALL-SEC │ PER REND-SEC │ PER FRAME +──────────────────────────┼────────────────┼────────────────┼──────────────── + wall-time 5.4s │ │ 1.26 │ 4.4ms + rend-time 4.3s │ 0.80 │ │ 3.5ms + calls 5678 │ 1045.3 │ 1314.0 │ 4.6 + frames 1234 │ 227.2 │ 285.6 │ +──────────────────────────┼────────────────┼────────────────┼──────────────── + objs 12.3k / 4.3k │ 2.3k / 795 │ 2.9k / 1.0k │ 10 / 3 + prims 24.7k / 8.6k │ 4.5k / 1.6k │ 5.7k / 2.0k │ 20 / 7 + verts 37.0k / 13.0k │ 6.8k / 2.4k │ 8.6k / 3.0k │ 30 / 10 + frags 49.4k / 17.3k │ 9.1k / 3.2k │ 11.4k / 4.0k │ 40 / 14 " ); assert_eq!( format!("{stats:#}"), " \ - STATS TOTAL │ PER SEC │ PER FRAME -─────────────────────────┼──────────────────┼────────────────── - time 4.3s │ │ 3.5ms - calls 5678 │ 1314.0 │ 4.6 - frames 1234 │ 285.6 │ -─────────────────────────┼──────────────────┼────────────────── - objs 35.0% │ 35.0% │ 30.0% - prims 35.0% │ 35.0% │ 35.0% - verts 35.0% │ 35.0% │ 33.3% - frags 35.0% │ 35.0% │ 35.0% + STATS TOTAL │ PER WALL-SEC │ PER REND-SEC │ PER FRAME +──────────────────────────┼────────────────┼────────────────┼──────────────── + wall-time 5.4s │ │ 1.26 │ 4.4ms + rend-time 4.3s │ 0.80 │ │ 3.5ms + calls 5678 │ 1045.3 │ 1314.0 │ 4.6 + frames 1234 │ 227.2 │ 285.6 │ +──────────────────────────┼────────────────┼────────────────┼──────────────── + objs 35.0% │ 35.0% │ 35.0% │ 30.0% + prims 35.0% │ 35.0% │ 35.0% │ 35.0% + verts 35.0% │ 35.0% │ 35.0% │ 33.3% + frags 35.0% │ 35.0% │ 35.0% │ 35.0% " ); } diff --git a/core/src/render/target.rs b/core/src/render/target.rs index 6316d778..57c65a51 100644 --- a/core/src/render/target.rs +++ b/core/src/render/target.rs @@ -7,27 +7,22 @@ use core::cell::RefCell; use crate::math::{Color3, Color4, Vary}; -use crate::util::{ - buf::{AsMutSlice2, Buf2, MutSlice2}, - pixfmt::IntoPixel, -}; +use crate::util::{AsMutSlice2, Buf2, IntoPixel, MutSlice2}; -use super::{Context, FragmentShader, raster::Scanline, stats::Throughput}; +use super::{Context, FragmentShader, raster::Scanline}; /// Trait for types that can be used as render targets. pub trait Target { /// Writes a single scanline into `self`. /// /// Returns count of fragments input and output. - fn rasterize( + fn rasterize>( &mut self, scanline: Scanline, frag_shader: &Fs, + uniform: U, ctx: &Context, - ) -> Throughput - where - V: Vary, - Fs: FragmentShader; + ); } /// Framebuffer, combining a color (pixel) buffer and a depth buffer. @@ -59,128 +54,177 @@ impl AsMutSlice2 for Colorbuf { impl Target for &mut T { #[inline] - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, fs: &Fs, + uni: U, ctx: &Context, - ) -> Throughput { - (*self).rasterize(sl, fs, ctx) + ) { + (*self).rasterize(sl, fs, uni, ctx); } } impl Target for &RefCell { #[inline] - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, fs: &Fs, + uni: U, ctx: &Context, - ) -> Throughput { - RefCell::borrow_mut(self).rasterize(sl, fs, ctx) + ) { + RefCell::borrow_mut(self).rasterize(sl, fs, uni, ctx); } } impl Target for Framebuf, Dep> where Col: AsMutSlice2, + Fmt: Copy, Dep: AsMutSlice2, Color4: IntoPixel, { /// Rasterizes `scanline` into this framebuffer. #[inline] - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, fs: &Fs, + uni: U, ctx: &Context, - ) -> Throughput { + ) { let Self { color_buf, depth_buf } = self; - rasterize_fb(color_buf, depth_buf, sl, fs, Color4::into_pixel, ctx) + let fmt = color_buf.fmt; // borrowck... + let conv = |c: Color4| c.into_pixel(fmt); + rasterize_fb(color_buf, depth_buf, sl, fs, uni, conv, ctx); } } impl Target for Colorbuf where Buf: AsMutSlice2, + Fmt: Copy, Color4: IntoPixel, { /// Rasterizes `scanline` into this `u32` color buffer. /// Does no z-buffering. #[inline] - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, fs: &Fs, + uni: U, ctx: &Context, - ) -> Throughput { - rasterize(&mut self.buf, sl, fs, Color4::into_pixel, ctx) + ) { + let conv = |c: Color4| c.into_pixel(self.fmt); + rasterize(&mut self.buf, sl, fs, uni, conv, ctx); } } impl Target for Buf2 { #[inline] - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, fs: &Fs, + uni: U, ctx: &Context, - ) -> Throughput { - rasterize(self, sl, fs, |c| c, ctx) + ) { + rasterize(self, sl, fs, uni, |c| c, ctx); } } impl Target for Buf2 { #[inline] - fn rasterize>( + fn rasterize>( &mut self, sl: Scanline, fs: &Fs, + uni: U, ctx: &Context, - ) -> Throughput { - rasterize(self, sl, fs, |c| c.to_rgb(), ctx) + ) { + rasterize(self, sl, fs, uni, |c| c.to_rgb(), ctx); } } -pub fn rasterize( +impl Target for MutSlice2<'_, Color4> { + #[inline] + fn rasterize>( + &mut self, + sl: Scanline, + fs: &Fs, + uni: U, + ctx: &Context, + ) { + rasterize(self, sl, fs, uni, |c| c, ctx) + } +} + +impl Target for MutSlice2<'_, Color3> { + #[inline] + fn rasterize>( + &mut self, + sl: Scanline, + fs: &Fs, + uni: U, + ctx: &Context, + ) { + rasterize(self, sl, fs, uni, |c| c.to_rgb(), ctx) + } +} + +pub fn rasterize( buf: &mut B, mut sl: Scanline, - fs: &impl FragmentShader, + fs: &impl FragmentShader, + uni: U, mut conv: impl FnMut(Color4) -> B::Elem, ctx: &Context, -) -> Throughput { +) { let x0 = sl.xs.start; let x1 = sl.xs.end.max(x0); - let mut io = Throughput { i: x1 - x0, o: 0 }; + #[cfg(feature = "stats")] + let mut frags_out = 0; + let cbuf_span = &mut buf.as_mut_slice2()[sl.y][x0..x1]; sl.fragments() .zip(cbuf_span) .for_each(|(frag, curr_col)| { - if let Some(new_col) = fs.shade_fragment(frag) + if let Some(new_col) = fs.shade_fragment(frag, uni) && ctx.color_write { - io.o += 1; + #[cfg(feature = "stats")] + { + frags_out += 1; + } *curr_col = conv(new_col); } }); - io + #[cfg(feature = "stats")] + { + ctx.stats.borrow_mut().frags += + super::stats::Throughput { i: x1 - x0, o: frags_out }; + }; } -pub fn rasterize_fb( +pub fn rasterize_fb( cbuf: &mut B, zbuf: &mut impl AsMutSlice2, mut sl: Scanline, - fs: &impl FragmentShader, + fs: &impl FragmentShader, + uni: U, mut conv: impl FnMut(Color4) -> B::Elem, ctx: &Context, -) -> Throughput { +) { let x0 = sl.xs.start; let x1 = sl.xs.end.max(x0); let cbuf_span = &mut cbuf.as_mut_slice2()[sl.y][x0..x1]; let zbuf_span = &mut zbuf.as_mut_slice2()[sl.y][x0..x1]; - let mut io = Throughput { i: x1 - x0, o: 0 }; + #[cfg(feature = "stats")] + let mut frags_out = 0; sl.fragments() .zip(cbuf_span) @@ -189,10 +233,13 @@ 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; + #[cfg(feature = "stats")] + { + frags_out += 1; + } // TODO Blending should happen here *curr_col = conv(new_col); } @@ -201,5 +248,9 @@ pub fn rasterize_fb( } } }); - io + #[cfg(feature = "stats")] + { + ctx.stats.borrow_mut().frags += + super::stats::Throughput { i: x1 - x0, o: frags_out }; + }; } diff --git a/core/src/render/tex.rs b/core/src/render/tex.rs index 762b0726..b77fa4c9 100644 --- a/core/src/render/tex.rs +++ b/core/src/render/tex.rs @@ -2,10 +2,7 @@ use crate::geom::Normal3; use crate::math::{Point2u, Vec2, Vec3, Vector, pt2, splat, vec2}; -use crate::util::{ - Dims, - buf::{AsSlice2, Buf2, Slice2}, -}; +use crate::util::{AsSlice2, Buf2, Dims, Slice2}; /// Basis of the texture space. #[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] @@ -146,11 +143,16 @@ impl Atlas { Self { layout, texture } } + /// Creates a texture atlas with a grid layout. + pub fn grid(sub_dims: Dims, texture: Texture>) -> Self { + Self::new(Layout::Grid { sub_dims }, texture) + } + /// Returns the top-left and bottom-right pixel coordinates /// of the sub-texture with index `i`. fn rect(&self, i: u32) -> [Point2u; 2] { match self.layout { - Layout::Grid { sub_dims: (sub_w, sub_h) } => { + Layout::Grid { sub_dims: Dims(sub_w, sub_h) } => { let subs_per_row = self.texture.data.width() / sub_w; let top_left = pt2(i % subs_per_row * sub_w, i / subs_per_row * sub_h); @@ -364,14 +366,14 @@ mod tests { use alloc::vec; use crate::math::{Color3, Linear, rgb}; - use crate::util::buf::Buf2; + use crate::util::Buf2; use super::*; #[rustfmt::skip] fn tex() -> Texture> { Texture::from(Buf2::new_from( - (2, 2), vec![ + Dims(2, 2), vec![ rgb(0xFF, 0, 0), rgb(0, 0xFF, 0), rgb(0, 0, 0xFF), diff --git a/core/src/render/text.rs b/core/src/render/text.rs index e36aa116..a30b2867 100644 --- a/core/src/render/text.rs +++ b/core/src/render/text.rs @@ -2,22 +2,50 @@ use core::fmt; #[cfg(feature = "std")] use std::io; -use crate::geom::{Mesh, tri, vertex}; -use crate::math::{Color3, Point2, Vec2, pt2, vec2, vec3}; -use crate::util::buf::Buf2; +use crate::geom::{Mesh, Tri, Vertex3, tri, vertex}; +use crate::math::{ + Color3, Color4, Mat4, Point2, ProjMat3, Vec2, color::gray, orthographic, + pt2, pt3, vec2, vec3, viewport, +}; +use crate::util::{Buf2, Dims}; -use super::tex::{Atlas, Layout, SamplerClamp, TexCoord}; +use super::tex::*; +use super::{BBox, Context, Frag, Model, Shader, Target, shader}; /// Text represented as texture-mapped geometry, one quad per glyph. #[derive(Clone)] pub struct Text { pub font: Atlas, pub geom: Mesh, - // TODO Private until fixed - _anchor: Vec2, - cursor: Point2, + pub color: Color3, + pub anchor: Point2, + pub align: Align, + cursor: Point2, } +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub enum Align { + #[default] + TopLeft, + TopCenter, + TopRight, + CenterLeft, + Center, + CenterRight, + BottomLeft, + BottomCenter, + BottomRight, +} + +pub type Batch<'a, Shd> = super::Batch< + &'a [Tri], + &'a [Vertex3], + (), + Shd, + (), + Context, +>; + // // Inherent impls // @@ -28,23 +56,17 @@ impl Text { Self { font, geom: Mesh::default(), - _anchor: Vec2::default(), + color: gray(0xFF), + anchor: Point2::default(), + align: Align::default(), cursor: Point2::default(), } } /// Sets the anchor point of the text. - /// - /// The anchor is a vector that determines how the text is aligned relative - /// to the (local) origin. The default is (0, 0) which places the origin to - /// the top left corner. Use (0.5, 0.5) to center the text vertically and - /// horizontally relative to the origin. - /// - /// Note that this value does not affect how individual lines of text - /// are aligned relative to each other. - // TODO private until fixed - fn _anchor(mut self, x: f32, y: f32) -> Self { - self._anchor = vec2(x, y); + #[must_use] + pub fn anchor(mut self, pt: impl Into) -> Self { + self.anchor = pt.into(); self } @@ -55,17 +77,76 @@ impl Text { self.geom.verts.clear(); } - /// Samples the font at `uv`. - pub fn sample(&self, uv: TexCoord) -> Color3 { - // TODO Figure out why coords go out of bounds -> SamplerOnce panics - SamplerClamp.sample(&self.font.texture, uv) + /// Returns a shader for rendering text. + pub fn shader( + &self, + ) -> impl Shader, TexCoord, &ProjMat3> { + shader::new( + |v: Vertex3<_>, tf: &ProjMat3<_>| { + vertex(tf.apply(&v.pos.to()), v.attrib) + }, + |frag: Frag, _| self.sample(frag.var), + ) + } + + /// Renders this text to a render target in 2D. + /// + /// For more customizable rendering, see the [`batch`][Self::batch] function. + pub fn render(&self, target: &mut impl Target) { + let BBox(_lt, rb) = BBox::of(&self.geom); + let [r, b, _] = rb.0; + + use Align::*; + let off = match self.align { + TopLeft => (0.0, 0.0), + TopCenter => (0.5, 0.0), + TopRight => (1.0, 0.0), + CenterLeft => (0.0, 0.5), + Center => (0.5, 0.5), + CenterRight => (1.0, 0.5), + BottomLeft => (0.0, 1.0), + BottomCenter => (0.5, 1.0), + BottomRight => (1.0, 1.0), + }; + let pos = self.anchor - Vec2::from(off) * vec2(r, b); + + let proj: ProjMat3 = Mat4::identity().then(&orthographic( + pt3(0.0, 0.0, -1.0), + pt3(self.cursor.x(), b, 1.0), + )); + let pos = pt2(pos.x() as _, pos.y() as _); + let wh = vec2(r as _, b as _); + let viewport = viewport(pos..pos + wh); + + self.batch() + .uniform(&proj) + .viewport(viewport) + .target(target) + .render(); + } + + /// Returns a `Batch` with the geometry and shader set to render this text. + /// + /// Useful for customized text rendering. + pub fn batch( + &self, + ) -> Batch<'_, impl Shader, TexCoord, &ProjMat3>> + { + super::Batch::from(&self.geom).shader(self.shader()) + } + + /// Samples the font at a texture coordinate. + #[inline] + fn sample(&self, uv: TexCoord) -> Option { + let col = SamplerClamp.sample(&self.font.texture, uv); + (col != gray(0)).then_some(self.color.to_rgba()) } fn write_char(&mut self, idx: u32) { let Self { font, geom, cursor, .. } = self; - let Layout::Grid { sub_dims: (gw, gh) } = font.layout; - let (glyph_w, glyph_h) = (gw as f32, gh as f32); + let Layout::Grid { sub_dims } = font.layout; + let (glyph_w, glyph_h) = (sub_dims.0 as f32, sub_dims.1 as f32); let [tl, tr, bl, br] = font.coords(idx); // TODO doesn't work when the text is written in several pieces, @@ -174,11 +255,11 @@ where num_cols = num_cols.max(row.len() as u32); } if num_rows == 0 || num_cols == 0 { - return Buf2::new((0, 0)); + return Buf2::new(Dims(0, 0)); } - let Layout::Grid { sub_dims: (gw, gh) } = font.layout; - let mut buf = Buf2::new((num_cols * gw, num_rows * gh)); + let Layout::Grid { sub_dims: Dims(gw, gh) } = font.layout; + let mut buf = Buf2::new(Dims(num_cols * gw, num_rows * gh)); let (mut x, mut y) = (0, 0); for row in rows { diff --git a/core/src/util.rs b/core/src/util.rs index db561ea9..aa7063c2 100644 --- a/core/src/util.rs +++ b/core/src/util.rs @@ -1,8 +1,17 @@ //! Various utility types and functions. -pub mod buf; +mod buf; +pub mod dims; pub mod pixfmt; pub mod pnm; -pub mod rect; +mod rect; -pub type Dims = (u32, u32); +pub(super) mod re_exports { + pub use super::{ + buf::{AsMutSlice2, AsSlice2, Buf2, MutSlice2, Slice2}, + dims::Dims, + pixfmt::IntoPixel, + rect::Rect, + }; +} +pub use re_exports::*; diff --git a/core/src/util/buf.rs b/core/src/util/buf.rs index 421308e0..62d74a96 100644 --- a/core/src/util/buf.rs +++ b/core/src/util/buf.rs @@ -20,7 +20,7 @@ use inner::Inner; /// A trait for types that can provide a view of their data as a [`Slice2`]. pub trait AsSlice2 { type Elem; - /// Returns a borrowed `Slice2` view of `Self`. + /// Returns a borrowed `Slice2` view of `self`. fn as_slice2(&self) -> Slice2<'_, Self::Elem>; } @@ -28,7 +28,7 @@ pub trait AsSlice2 { /// as a [`MutSlice2`]. pub trait AsMutSlice2 { type Elem; - /// Returns a mutably borrowed `MutSlice2` view of `Self`. + /// Returns a mutably borrowed `MutSlice2` view of `self`. fn as_mut_slice2(&mut self) -> MutSlice2<'_, Self::Elem>; } @@ -47,14 +47,18 @@ pub trait AsMutSlice2 { /// /// # Examples /// ``` -/// # use retrofire_core::util::buf::Buf2; +/// # use retrofire_core::util::{Dims, Buf2}; /// # use retrofire_core::math::point::pt2; +/// /// // Elements initialized with `Default::default()` -/// let mut buf = Buf2::new((4, 4)); +/// let mut buf = Buf2::new(Dims(4, 4)); +/// /// // 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: +/// +/// // Indexing with a usize i yields row with index i as a slice: /// assert_eq!(buf[1], [0, 0, 123, 0]); +/// /// // Thus you can also do this, row first, column second: /// assert_eq!(buf[1][2], 123) /// ``` @@ -65,7 +69,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 non-contiguous: +/// The backing data of a `Slice2` may be non-contiguous: /// ```text /// +------stride-----+ /// | ____w____ | @@ -74,6 +78,9 @@ pub struct Buf2(Inner>); /// | |r2_______| | /// +-----------------+ /// ``` +/// Internally, `Slice2` borrows a contiguous slice of the backing buffer, +/// but disallows access to any elements not within its 2D extents. +/// /// TODO More documentation #[derive(Copy, Clone, Eq, PartialEq)] #[repr(transparent)] @@ -89,28 +96,29 @@ pub struct MutSlice2<'a, T>(Inner); // impl Buf2 { - /// Returns a buffer of size `w` × `h`, with elements initialized + /// Returns a buffer of the given dimensions, with elements initialized /// with values yielded by `init`. /// /// The elements are initialized in row-major order. Does not allocate - /// or consume items from `init` if `w` = 0 or `h` = 0. + /// or consume items from `init` if either the width or the height is zero. /// /// # Examples /// ``` - /// use retrofire_core::util::buf::Buf2; + /// use retrofire_core::util::{Dims, Buf2}; /// - /// let buf = Buf2::new_from((3, 3), 1..); + /// let buf = Buf2::new_from(Dims(4, 3), 1..); /// - /// assert_eq!(buf.dims(), (3, 3)); - /// assert_eq!(buf.data(), [1, 2, 3, - /// 4, 5, 6, - /// 7, 8, 9]); + /// assert_eq!(buf.width(), 4); + /// assert_eq!(buf.height(), 3); + /// assert_eq!(buf.data(), [1, 2, 3, 4, + /// 5, 6, 7, 8, + /// 9, 10, 11, 12]); /// ``` /// /// # Panics - /// * If `w * h > isize::MAX`, or - /// * if `init` has fewer than `w * h` elements. - pub fn new_from((w, h): Dims, init: I) -> Self + /// * If width × height > `isize::MAX`., or + /// * if `init` has fewer than width × height elements. + pub fn new_from(Dims(w, h): Dims, init: I) -> Self where I: IntoIterator, { @@ -130,69 +138,71 @@ impl Buf2 { "insufficient items in iterator ({} < {len}", data.len() ); - Self(Inner::new((w, h), w, data)) + Self(Inner::new(Dims(w, h), w, data)) } - /// Returns a buffer of size `w` × `h`, with every element initialized to - /// `T::default()`. + /// Returns a buffer of the given dimensions, with every element initialized + /// to `T::default()`. /// - /// Does not allocate if `w` = 0 or `h` = 0. + /// Does not allocate if either the width or the height is zero. /// /// # Examples /// ``` - /// use retrofire_core::util::buf::Buf2; + /// use retrofire_core::util::{Dims, Buf2}; /// - /// let buf: Buf2 = Buf2::new((3, 3)); + /// let buf: Buf2 = Buf2::new(Dims(4, 3)); /// - /// assert_eq!(buf.dims(), (3, 3)); - /// assert_eq!(buf.data(), [0, 0, 0, - /// 0, 0, 0, - /// 0, 0, 0]); + /// assert_eq!(buf.width(), 4); + /// assert_eq!(buf.height(), 3); + /// assert_eq!(buf.data(), [0, 0, 0, 0, + /// 0, 0, 0, 0, + /// 0, 0, 0, 0]); /// ``` /// /// # Panics - /// If `w * h > isize::MAX`. + /// If width × height > `isize::MAX`. #[inline] - pub fn new((w, h): Dims) -> Self + pub fn new(dims: Dims) -> Self where T: Default + Clone, { - let data = vec![T::default(); (w * h) as usize]; - Self(Inner::new((w, h), w, data)) + let data = vec![T::default(); dims.count()]; + Self(Inner::new(dims, dims.0, data)) } - /// Returns a buffer of size `w` × `h`, initialized by repeatedly calling - /// the given function. + /// Returns a buffer of the given dimensions, 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 /// elements are initialized in row-major order. /// - /// Does not allocate or call `init_fn` if `w` = 0 or `h` = 0. + /// Does not allocate or call `init_fn` if either the width or the height is zero. + /// + /// # Panics + /// If width × height > `isize::MAX`. /// /// # Examples /// ``` - /// use retrofire_core::util::buf::Buf2; + /// use retrofire_core::util::{Dims, 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]); - /// ``` + /// let buf = Buf2::new_with(Dims(4, 3), |x, y| 10 * y + x); /// - /// # Panics - /// If `w * h > isize::MAX`. - pub fn new_with((w, h): Dims, mut init_fn: F) -> Self + /// assert_eq!(buf.data(), [ 0, 1, 2, 3, + /// 10, 11, 12, 13, + /// 20, 21, 22, 23]); + /// ``` + pub fn new_with(dims: Dims, mut init_fn: F) -> Self where F: FnMut(u32, u32) -> T, { let (mut x, mut y) = (0, 0); Self::new_from( - (w, h), + dims, iter::from_fn(|| { let res = init_fn(x, y); x += 1; - if x == w { + if x == dims.0 { (x, y) = (0, y + 1); } Some(res) @@ -203,20 +213,39 @@ impl Buf2 { /// Returns a view of the backing data of `self`. #[inline] pub fn data(&self) -> &[T] { - self.0.data() + &self.0.data } /// Returns a mutable view of the backing data of `self`. #[inline] pub fn data_mut(&mut self) -> &mut [T] { - self.0.data_mut() + &mut self.0.data } - /// Reinterprets `self` as a buffer of different dimensions but same area. + /// Reinterprets `self` as a buffer of different dimensions but the same area. + /// + /// Does not reallocate or move data. /// /// # Panics - /// If `nw` * `nh` != `cw` * `ch` for the new dimensions (`nw`, `nh`) + /// If `nw` × `nh` ≠ `cw` × `ch` for the new dimensions (`nw`, `nh`) /// and current dimensions (`cw`, `ch`). + /// + /// # Examples + /// ``` + /// use retrofire_core::util::{Dims, Buf2}; + /// + /// let mut buf = Buf2::new_from(Dims(3, 2), [1, 2, 3, 4, 5, 6]); + /// + /// buf.reshape(Dims(2, 3)); + /// + /// assert_eq!(buf.stride(), 2); + /// assert_eq!(buf.width(), 2); + /// assert_eq!(buf.height(), 3); + /// + /// assert_eq!(buf[0], [1, 2]); + /// assert_eq!(buf[1], [3, 4]); + /// assert_eq!(buf[2], [5, 6]); + /// ``` pub fn reshape(&mut self, dims: Dims) { self.0.reshape(dims); } @@ -228,9 +257,11 @@ impl<'a, T> Slice2<'a, T> { /// /// # Examples /// ``` - /// # use retrofire_core::util::buf::Slice2; + /// use retrofire_core::util::{Dims, Slice2}; + /// /// let data = &[0, 1, 2, 3, 4, 5, 6]; - /// let slice = Slice2::new((2, 2), 3, data); + /// let slice = Slice2::new(Dims(2, 2), 3, data); + /// /// assert_eq!(&slice[0], &[0, 1]); /// assert_eq!(&slice[1], &[3, 4]); /// ``` @@ -374,6 +405,30 @@ impl DerefMut for MutSlice2<'_, T> { } } +impl From<&[[T; N]]> for Buf2 { + /// Creates a `Buf2` from a slice of arrays. + /// + /// # Examples + /// ``` + /// use retrofire_core::util::Buf2; + /// + /// let buf = Buf2::from([[1, 2, 3], [4, 5, 6]].as_slice()); + /// + /// assert_eq!(buf.stride(), 3); + /// assert_eq!(buf.width(), 3); + /// assert_eq!(buf.height(), 2); + /// + /// assert_eq!(buf[0], [1, 2, 3]); + /// assert_eq!(buf[1], [4, 5, 6]); + /// ``` + fn from(slice: &[[T; N]]) -> Self { + Self::new_from( + Dims(N as u32, slice.len() as u32), + slice.as_flattened().iter().cloned(), + ) + } +} + pub mod inner { use core::{ fmt::Formatter, @@ -396,7 +451,7 @@ pub mod inner { pub struct Inner { dims: Dims, stride: u32, - data: D, + pub(super) data: D, _pd: PhantomData, } @@ -429,7 +484,7 @@ pub mod inner { /// height is 1, or if it is empty. #[inline] pub fn is_contiguous(&self) -> bool { - let (w, h) = self.dims; + let Dims(w, h) = self.dims; self.stride == w || h <= 1 || w == 0 } /// Returns whether `self` contains no elements. @@ -459,14 +514,13 @@ pub mod inner { /// 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)) + (x < self.dims.0 && y < self.dims.1).then(|| self.to_index(x, y)) } /// Returns the dimensions and linear range corresponding to the rect. #[inline(never)] fn resolve_bounds(&self, rect: &Rect) -> (Dims, Range) { - let (w, h) = self.dims; + let Dims(w, h) = self.dims; let l = rect.left.unwrap_or(0); let t = rect.top.unwrap_or(0); @@ -491,7 +545,7 @@ pub mod inner { // b != 0 because b >= t && b != t self.to_index(r, b - 1) }; - ((r - l, b - t), start..end) + (Dims(r - l, b - t), start..end) } /// A helper for implementing `Debug`. @@ -509,7 +563,11 @@ pub mod inner { pub(super) fn reshape(&mut self, dims: Dims) { assert!(self.is_contiguous()); - assert_eq!(dims.0 * dims.1, self.dims.0 * self.dims.1); + assert_eq!( + dims.0 as u64 * dims.1 as u64, + self.dims.0 as u64 * self.dims.1 as u64 + ); + self.stride = dims.0; self.dims = dims; } } @@ -517,8 +575,8 @@ pub mod inner { #[cold] #[track_caller] #[inline(never)] - fn out_of_bounds((w, h): Dims, x: u32, y: u32) -> ! { - panic!("position (x={x}, y={y}) out of bounds (0..{w}, 0..{h})",) + fn out_of_bounds(Dims(w, h): Dims, x: u32, y: u32) -> ! { + panic!("position (x={x}, y={y}) out of bounds (0..{w}, 0..{h})") } impl> Inner { @@ -531,12 +589,6 @@ pub mod inner { Self { dims, stride, data, _pd: PhantomData } } - /// Returns the data of `self` as a linear slice. - #[inline] - pub(super) fn data(&self) -> &[T] { - &self.data - } - /// Borrows `self` as a `Slice2`. #[inline] pub fn as_slice2(&self) -> Slice2<'_, T> { @@ -580,7 +632,7 @@ pub mod inner { } } - fn check_preconditions((w, h): Dims, stride: u32, len: usize) { + fn check_preconditions(Dims(w, h): Dims, stride: u32, len: usize) { assert!(w <= stride, "width ({w}) > stride ({stride})"); assert!( h <= 1 || stride as usize <= len, @@ -605,12 +657,6 @@ pub mod inner { MutSlice2(Inner { dims, stride, data, _pd }) } - /// Returns the data of `self` as a single mutable slice. - #[inline] - pub(super) fn data_mut(&mut self) -> &mut [T] { - &mut self.data - } - /// Returns an iterator over the rows of this buffer as `&mut [T]`. /// /// The length of each slice equals [`self.width()`](Self::width). @@ -776,25 +822,25 @@ mod tests { #[test] fn buf_new_from() { - let buf = Buf2::new_from((3, 2), 1..); + let buf = Buf2::new_from(Dims(3, 2), 1..); assert_eq!(buf.data(), &[1, 2, 3, 4, 5, 6]); } #[test] fn buf_new() { - let buf: Buf2 = Buf2::new((3, 2)); + let buf: Buf2 = Buf2::new(Dims(3, 2)); assert_eq!(buf.data(), &[0, 0, 0, 0, 0, 0]); } #[test] fn buf_new_with() { - let buf = Buf2::new_with((3, 2), |x, y| x + y); + let buf = Buf2::new_with(Dims(3, 2), |x, y| x + y); assert_eq!(buf.data(), &[0, 1, 2, 1, 2, 3]); } #[test] fn buf_extents() { - let buf: Buf2<()> = Buf2::new((4, 5)); + let buf: Buf2<()> = Buf2::new(Dims(4, 5)); assert_eq!(buf.width(), 4); assert_eq!(buf.height(), 5); assert_eq!(buf.stride(), 4); @@ -802,7 +848,7 @@ mod tests { #[test] fn buf_index_and_get() { - let buf = Buf2::new_with((4, 5), |x, y| x * 10 + y); + let buf = Buf2::new_with(Dims(4, 5), |x, y| x * 10 + y); assert_eq!(buf[2usize], [2, 12, 22, 32]); @@ -817,7 +863,7 @@ mod tests { #[test] fn buf_index_mut_and_get_mut() { - let mut buf = Buf2::new_with((4, 5), |x, y| x * 10 + y); + let mut buf = Buf2::new_with(Dims(4, 5), |x, y| x * 10 + y); buf[2usize][1] = 123; assert_eq!(buf[2usize], [2, 123, 22, 32]); @@ -834,27 +880,27 @@ mod tests { #[test] #[should_panic = "position (x=4, y=0) out of bounds (0..4, 0..5)"] fn buf_index_x_out_of_bounds_should_panic() { - let buf = Buf2::new((4, 5)); + let buf = Buf2::new(Dims(4, 5)); let _: i32 = buf[[4, 0]]; } #[test] #[should_panic = "position (x=0, y=4) out of bounds (0..5, 0..4)"] fn buf_index_y_out_of_bounds_should_panic() { - let buf = Buf2::new((5, 4)); + let buf = Buf2::new(Dims(5, 4)); let _: i32 = buf[[0, 4]]; } #[test] #[should_panic = "position (x=0, y=5) out of bounds (0..4, 0..5)"] fn buf_index_row_out_of_bounds_should_panic() { - let buf = Buf2::new((4, 5)); + let buf = Buf2::new(Dims(4, 5)); let _: &[i32] = &buf[5usize]; } #[test] fn buf_slice_range_full() { - let buf: Buf2<()> = Buf2::new((4, 5)); + let buf: Buf2<()> = Buf2::new(Dims(4, 5)); let slice = buf.slice(..); assert_eq!(slice.width(), 4); @@ -869,7 +915,7 @@ mod tests { #[test] fn buf_slice_range_inclusive() { - let buf: Buf2<()> = Buf2::new((4, 5)); + let buf: Buf2<()> = Buf2::new(Dims(4, 5)); let slice = buf.slice((1..=3, 0..=3)); assert_eq!(slice.width(), 3); assert_eq!(slice.height(), 4); @@ -878,7 +924,7 @@ mod tests { #[test] fn buf_slice_range_to() { - let buf: Buf2<()> = Buf2::new((4, 5)); + let buf: Buf2<()> = Buf2::new(Dims(4, 5)); let slice = buf.slice((..2, ..4)); assert_eq!(slice.width(), 2); @@ -888,7 +934,7 @@ mod tests { #[test] fn buf_slice_range_from() { - let buf: Buf2<()> = Buf2::new((4, 5)); + let buf: Buf2<()> = Buf2::new(Dims(4, 5)); let slice = buf.slice((3.., 2..)); assert_eq!(slice.width(), 1); @@ -898,7 +944,7 @@ mod tests { #[test] fn buf_slice_empty_range() { - let buf: Buf2<()> = Buf2::new((4, 5)); + let buf: Buf2<()> = Buf2::new(Dims(4, 5)); let empty = buf.slice(pt2(1, 1)..pt2(1, 3)); assert_eq!(empty.width(), 0); @@ -914,43 +960,43 @@ mod tests { #[test] #[should_panic = "range right (5) > width (4)"] fn buf_slice_x_out_of_bounds_should_panic() { - let buf: Buf2<()> = Buf2::new((4, 5)); + let buf: Buf2<()> = Buf2::new(Dims(4, 5)); buf.slice((0..5, 1..3)); } #[test] #[should_panic = "range bottom (6) > height (5)"] fn buf_slice_y_out_of_bounds_should_panic() { - let buf: Buf2<()> = Buf2::new((4, 5)); + let buf: Buf2<()> = Buf2::new(Dims(4, 5)); buf.slice((1..3, 0..6)); } #[test] #[should_panic = "width (4) > stride (3)"] fn slice_stride_less_than_width_should_panic() { - let _ = Slice2::new((4, 4), 3, &[0; 16]); + let _ = Slice2::new(Dims(4, 4), 3, &[0; 16]); } #[test] #[should_panic = "required size (19) > data length (16)"] fn slice_larger_than_data_should_panic() { - let _ = Slice2::new((4, 4), 5, &[0; 16]); + let _ = Slice2::new(Dims(4, 4), 5, &[0; 16]); } #[test] fn slice_extents() { - let buf: Buf2<()> = Buf2::new((10, 10)); + let buf: Buf2<()> = Buf2::new(Dims(10, 10)); let slice = buf.slice((1..4, 2..8)); assert_eq!(slice.width(), 3); assert_eq!(slice.height(), 6); assert_eq!(slice.stride(), 10); - assert_eq!(slice.data().len(), 5 * 10 + 3); + assert_eq!(slice.data.len(), 5 * 10 + 3); } #[test] fn slice_contiguity() { - let buf: Buf2<()> = Buf2::new((10, 10)); + let buf: Buf2<()> = Buf2::new(Dims(10, 10)); // Buf2 is always contiguous assert!(buf.is_contiguous()); @@ -972,7 +1018,7 @@ mod tests { #[test] #[rustfmt::skip] fn slice_fill() { - let mut buf = Buf2::new((5, 4)); + let mut buf = Buf2::new(Dims(5, 4)); let mut slice = buf.slice_mut((2.., 1..3)); slice.fill(1); @@ -989,7 +1035,7 @@ mod tests { #[test] #[rustfmt::skip] fn slice_fill_with() { - let mut buf = Buf2::new((5, 4)); + let mut buf = Buf2::new(Dims(5, 4)); let mut slice = buf.slice_mut((2.., 1..3)); slice.fill_with(|x, y| x + y); @@ -1006,8 +1052,8 @@ mod tests { #[test] #[rustfmt::skip] fn slice_copy_from() { - let mut dest = Buf2::new((5, 4)); - let src = Buf2::new_with((3, 3), |x, y| x + y); + let mut dest = Buf2::new(Dims(5, 4)); + let src = Buf2::new_with(Dims(3, 3), |x, y| x + y); dest.slice_mut((1..4, 1..)).copy_from(src); @@ -1022,7 +1068,7 @@ mod tests { #[test] fn slice_index() { - let buf = Buf2::new_with((5, 4), |x, y| x * 10 + y); + let buf = Buf2::new_with(Dims(5, 4), |x, y| x * 10 + y); let slice = buf.slice((2.., 1..3)); assert_eq!(slice[[0, 0]], 21); @@ -1035,7 +1081,7 @@ mod tests { #[test] fn slice_index_mut() { - let mut buf = Buf2::new_with((5, 5), |x, y| x * 10 + y); + let mut buf = Buf2::new_with(Dims(5, 5), |x, y| x * 10 + y); let mut slice = buf.slice_mut((2.., 1..3)); slice[[2, 1]] = 123; @@ -1051,7 +1097,7 @@ mod tests { #[test] fn slice_rows() { - let buf = Buf2::new_with((5, 4), |x, y| x * 10 + y); + let buf = Buf2::new_with(Dims(5, 4), |x, y| x * 10 + y); let slice = buf.slice((2..4, 1..)); let mut rows = slice.rows(); @@ -1063,7 +1109,7 @@ mod tests { #[test] fn slice_rows_mut() { - let mut buf = Buf2::new_with((5, 4), |x, y| x * 10 + y); + let mut buf = Buf2::new_with(Dims(5, 4), |x, y| x * 10 + y); let mut slice = buf.slice_mut((2..4, 1..)); let mut rows = slice.rows_mut(); @@ -1078,7 +1124,7 @@ mod tests { fn foo>(buf: T) -> u32 { buf.as_slice2().width() } - let buf = Buf2::new((2, 2)); + let buf = Buf2::new(Dims(2, 2)); let w = foo(&buf); assert_eq!(w, buf.width()); } @@ -1088,7 +1134,7 @@ mod tests { fn foo>(mut buf: T) { buf.as_mut_slice2()[[1, 1]] = 42; } - let mut buf = Buf2::new((2, 2)); + let mut buf = Buf2::new(Dims(2, 2)); foo(&mut buf); assert_eq!(buf[[1, 1]], 42); } diff --git a/core/src/util/dims.rs b/core/src/util/dims.rs new file mode 100644 index 00000000..6c6ea421 --- /dev/null +++ b/core/src/util/dims.rs @@ -0,0 +1,90 @@ +// Source for the names: +// https://commons.wikimedia.org/wiki/File:Vector_Video_Standards8.svg + +#![allow(non_upper_case_globals)] + +use core::ops::Range; + +use crate::math::{Point2u, pt2}; +use crate::util::Rect; + +/// A width, height tuple for representing 2D buffer or window dimensions, +/// screen resolutions, and similar. +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct Dims(pub u32, pub u32); + +// 5:4 +pub const qSXGA_640_512: Dims = Dims(640, 512); +pub const SXGA_1280_1024: Dims = Dims(1280, 1024); + +// 4:3 +pub const qVGA_320_240: Dims = Dims(320, 240); +pub const qSVGA_400_300: Dims = Dims(400, 300); +pub const qXGA_512_384: Dims = Dims(512, 384); +pub const VGA_640_480: Dims = Dims(640, 480); +pub const SVGA_800_600: Dims = Dims(800, 600); +pub const XGA_1024_768: Dims = Dims(1024, 768); +pub const QVGA_1280_960: Dims = Dims(1280, 960); +pub const UXGA_1600_1200: Dims = Dims(1600, 1200); +pub const QXGA_2048_1536: Dims = Dims(2048, 1536); + +// 16:10 +pub const CGA_320_200: Dims = Dims(320, 200); +pub const MODE_13H: Dims = CGA_320_200; +pub const QCGA_640_400: Dims = Dims(640, 400); +pub const qWXGA_640_400: Dims = Dims(640, 400); +pub const WXGA_1280_800: Dims = Dims(1280, 800); +pub const WXGAP_1440_900: Dims = Dims(1440, 900); +pub const WSXGAP_1680_1050: Dims = Dims(1680, 1050); +pub const WUXGA_1920_1200: Dims = Dims(1920, 1200); +pub const WQXGA_2560_1600: Dims = Dims(2560, 1600); + +// 16:9 +// 640x360 = "qHD"? +// 800x450 = qWSXGA? +// 960x540 = qFHD +pub const HD_1280_720: Dims = Dims(1280, 720); +pub const WSXGA_1600_900: Dims = Dims(1600, 900); +pub const FHD_1920_1080: Dims = Dims(1920, 1080); +pub const QHD_2560_1440: Dims = Dims(2560, 1440); +pub const UHD_4K_3840_2160: Dims = Dims(3840, 2160); + +// DCI ~17:9 +pub const DCI_2K_2048_1080: Dims = Dims(2048, 1080); +pub const DCI_4K_4096_2160: Dims = Dims(4096, 2160); + +// ~21:9 +pub const qUWFHD_1280_540: Dims = Dims(1280, 540); +pub const UWFHD_2560_1080: Dims = Dims(2560, 1080); +pub const UWQHD_3440_1440: Dims = Dims(3440, 1440); + +impl Dims { + /// Returns the number of elements in a buffer of this size. + pub fn count(&self) -> usize { + (self.0 as u64 * self.1 as u64) + .try_into() + .expect("count should fit in usize") + } + + /// Returns the width-to-height aspect ratio of `self`. + pub fn aspect(&self) -> f32 { + self.0 as f32 / self.1 as f32 + } +} + +impl From for Rect { + fn from(Dims(w, h): Dims) -> Self { + Rect { + left: Some(0), + top: Some(0), + right: Some(w), + bottom: Some(h), + } + } +} + +impl From for Range> { + fn from(Dims(w, h): Dims) -> Self { + pt2(0, 0)..pt2(w, h) + } +} diff --git a/core/src/util/pixfmt.rs b/core/src/util/pixfmt.rs index 88113f33..6c3aa043 100644 --- a/core/src/util/pixfmt.rs +++ b/core/src/util/pixfmt.rs @@ -5,214 +5,394 @@ use crate::math::{Color3, Color4}; pub trait IntoPixel: Sized { /// Converts `self` to `T` in format `F`. - fn into_pixel(self) -> T; - - /// Converts `self` to `T`, taking an `F` to help type inference. - /// - /// This can be used to avoid the awkward fully-qualified syntax - /// `IntoPixel::<_, F>::into_pixel(self)`. - fn into_pixel_fmt(self, _: F) -> T { - self.into_pixel() - } + fn into_pixel(self, fmt: F) -> T; } +// RGB + +/// Eight-bit channels in X,R,G,B order, where X is unused. +#[derive(Copy, Clone, Debug, Default)] +pub struct Xrgb8888; + /// Eight-bit channels in R,G,B order. -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct Rgb888; /// 5,6,5-bit channels in R,G,B order. -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct Rgb565; +/// 3,3,2-bit channels in R,G,B order. +#[derive(Copy, Clone, Debug, Default)] +pub struct Rgb332; + +// RGBA -/// Eight-bit channels in X,R,G,B order, where X is unused. -#[derive(Copy, Clone, Default)] -pub struct Xrgb8888; /// Eight-bit channels in R,G,B,A order. -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct Rgba8888; /// Eight-bit channels in A,R,G,B order. -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct Argb8888; /// Eight-bit channels in B,G,R,A order. -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct Bgra8888; /// Four-bit channels in R,G,B,A order. -#[derive(Copy, Clone, Default)] +#[derive(Copy, Clone, Debug, Default)] pub struct Rgba4444; -// Impls for Color3 +/// 5-bit RGB channels and 1-bit alpha. +#[derive(Copy, Clone, Debug, Default)] +pub struct Rgba5551; + +// Indexed + +#[derive(Copy, Clone)] +pub struct Indexed8(pub [C; 256]); + +// +// IntoPixel impl for Color3 +// + +impl IntoPixel for Color3 +where + Color4: IntoPixel, +{ + /// Converts `self` to any of the pixel formats implemented for [`Color4`], + /// with alpha set to fully opaque. + #[inline] + fn into_pixel(self, fmt: F) -> T { + self.to_rgba().into_pixel(fmt) + } +} + +// +// IntoPixel impls for Color4 +// -impl IntoPixel for Color3 { +impl IntoPixel for Color4 +where + Self: IntoPixel<[u8; 4], F>, +{ + /// Converts `self` to `u32` using any pixel format for which the equivalent + /// `[u8;4]` conversion exists. #[inline] - fn into_pixel(self) -> u32 { - let [r, g, b] = self.0; + fn into_pixel(self, fmt: F) -> u32 { + // From [0xAA, 0xBB, 0xCC, 0xDD] to 0xAA_BB_CC_DD -> big-endian! + u32::from_be_bytes(self.into_pixel(fmt)) + } +} + +impl IntoPixel for Color4 { + /// Converts `self` to a `u32` in 0x00_RR_GG_BB format, discarding alpha. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{rgb, Color3}; + /// use retrofire_core::util::pixfmt::{IntoPixel, Xrgb8888}; + /// + /// let color: Color3 = rgb(0x33, 0x66, 0x99); + /// + /// assert_eq!(color.into_pixel(Xrgb8888), 0x00_33_66_99); + /// ``` + #[inline] + fn into_pixel(self, _: Xrgb8888) -> u32 { + let [r, g, b, _] = self.0; // [0x00, 0xRR, 0xGG, 0xBB] -> 0x00_RR_GG_BB u32::from_be_bytes([0, r, g, b]) } } -impl IntoPixel<[u8; 3], Rgb888> for Color3 { +impl IntoPixel<[u8; 3], Rgb888> for Color4 { + /// Converts `self` to [0xRR, 0xGG, 0xBB] bytes, discarding alpha. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{rgb, Color3}; + /// use retrofire_core::util::pixfmt::{IntoPixel, Rgb888}; + /// + /// let color: Color3 = rgb(0x33, 0x66, 0x99); + /// + /// assert_eq!(color.into_pixel(Rgb888), [0x33, 0x66, 0x99]); + /// ``` #[inline] - fn into_pixel(self) -> [u8; 3] { - self.0 + fn into_pixel(self, _: Rgb888) -> [u8; 3] { + let [rgb @ .., _] = self.0; + rgb } } -impl IntoPixel for Color3 { +impl IntoPixel for Color4 { + /// Converts `self` to a `u16` in 0bRRRRR_GGGGGG_BBBBB format, discarding + /// alpha. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{rgb, Color3}; + /// use retrofire_core::util::pixfmt::{IntoPixel, Rgb565}; + /// + /// let color: Color3 = rgb(0x80, 0x40, 0x20); + /// + /// let word_565: u16 = color.into_pixel(Rgb565); + /// assert_eq!(word_565, 0x8204); // 0b10000_010000_00100 + /// ``` #[inline] - fn into_pixel(self) -> u16 { - let [r, g, b] = self.0; + fn into_pixel(self, _: Rgb565) -> u16 { + let [r, g, b, _] = self.0; (r as u16 >> 3 & 0x1F) << 11 | (g as u16 >> 2 & 0x3F) << 5 | (b as u16 >> 3 & 0x1F) } } - -impl IntoPixel<[u8; 2], Rgb565> for Color3 { +impl IntoPixel<[u8; 2], Rgb565> for Color4 { #[inline] - fn into_pixel(self) -> [u8; 2] { - let c: u16 = self.into_pixel(); + fn into_pixel(self, _: Rgb565) -> [u8; 2] { + let c: u16 = self.into_pixel(Rgb565); c.to_ne_bytes() } } -// Impls for Color4 - -impl IntoPixel for Color4 -where - Self: IntoPixel<[u8; 4], F>, -{ - #[inline] - fn into_pixel(self) -> u32 { - // From [0xAA, 0xBB, 0xCC, 0xDD] to 0xAA_BB_CC_DD -> big-endian! - u32::from_be_bytes(self.into_pixel()) +impl IntoPixel for Color4 { + /// Packs `self` into a single byte in `0bRRR_GGG_BB` format, discarding + /// alpha. + fn into_pixel(self, _: Rgb332) -> u8 { + let [r, g, b, _] = self.0; + (r >> 5) << 5 | (g >> 5) << 2 | b >> 6 } } - -impl IntoPixel for Color4 { - #[inline] - fn into_pixel(self) -> u32 { - let [r, g, b, _] = self.0; - // From [0x00, 0xRR, 0xGG, 0xBB] to 0x00_RR_GG_BB -> big-endian! - u32::from_be_bytes([0, r, g, b]) +impl IntoPixel<[u8; 1], Rgb332> for Color4 { + /// Packs `self` into a single-byte array in `0bRRR_GGG_BB` format, + /// discarding alpha. + fn into_pixel(self, _: Rgb332) -> [u8; 1] { + let pix: u8 = self.into_pixel(Rgb332); + [pix] } } + impl IntoPixel<[u8; 4], Rgba8888> for Color4 { + /// Converts `self` to [0xRR, 0xGG, 0xBB, 0xAA] bytes. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{rgba, Color4}; + /// use retrofire_core::util::pixfmt::{IntoPixel, Rgba8888}; + /// + /// let color: Color4 = rgba(0x11, 0x22, 0x33, 0x44); + /// let rgba: [u8; 4] = color.into_pixel(Rgba8888); + /// + /// assert_eq!(rgba, [0x11, 0x22, 0x33, 0x44]); + /// ``` #[inline] - fn into_pixel(self) -> [u8; 4] { + fn into_pixel(self, _: Rgba8888) -> [u8; 4] { self.0 } } impl IntoPixel<[u8; 4], Argb8888> for Color4 { + /// Converts `self` to [0xAA, 0xRR, 0xGG, 0xBB] bytes. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{rgba, Color4}; + /// use retrofire_core::util::pixfmt::{Argb8888, IntoPixel}; + /// + /// let color: Color4 = rgba(0x11, 0x22, 0x33, 0x44); + /// let argb: [u8; 4] = color.into_pixel(Argb8888); + /// + /// assert_eq!(argb, [0x44, 0x11, 0x22, 0x33]); + /// ``` #[inline] - fn into_pixel(self) -> [u8; 4] { + fn into_pixel(self, _: Argb8888) -> [u8; 4] { let [r, g, b, a] = self.0; [a, r, g, b] } } impl IntoPixel<[u8; 4], Bgra8888> for Color4 { + /// Converts `self` to [0xBB, 0xGG, 0xRR, 0xAA] bytes. + /// + /// # Examples + /// ``` + /// use retrofire_core::math::{rgba, Color4}; + /// use retrofire_core::util::pixfmt::{Bgra8888, IntoPixel}; + /// + /// let color: Color4 = rgba(0x11, 0x22, 0x33, 0x44); + /// let bgra: [u8; 4] = color.into_pixel(Bgra8888); + /// + /// assert_eq!(bgra, [0x33, 0x22, 0x11, 0x44]); + /// ``` #[inline] - fn into_pixel(self) -> [u8; 4] { + fn into_pixel(self, _: Bgra8888) -> [u8; 4] { let [r, g, b, a] = self.0; [b, g, r, a] } } -impl IntoPixel<[u8; 3], Rgb888> for Color4 { - #[inline] - fn into_pixel(self) -> [u8; 3] { - [self.r(), self.g(), self.b()] - } -} -impl IntoPixel<[u8; 2], Rgba4444> for Color4 { - #[inline] - fn into_pixel(self) -> [u8; 2] { - let c: u16 = self.into_pixel_fmt(Rgba4444); - c.to_ne_bytes() - } -} + impl IntoPixel for Color4 { + /// Converts `self` to a `u16` in 0xRGBA format (four bits per channel). #[inline] - fn into_pixel(self) -> u16 { + fn into_pixel(self, _: Rgba4444) -> u16 { let [r, g, b, a] = self.0; - - // [0xBA, 0xRG] in little-endian (r as u16 >> 4) << 12 | (g as u16 >> 4) << 8 | (b as u16 >> 4) << 4 | (a as u16 >> 4) } } -impl IntoPixel for Color4 { +impl IntoPixel<[u8; 2], Rgba4444> for Color4 { + /// Converts `self` to 0xRG and 0xBA bytes (four bits per channel) + /// in native byte order. + /// + /// For example, the result is [0xB, 0xRG] on little-endian systems. #[inline] - fn into_pixel(self) -> u16 { - self.to_rgb().into_pixel() + fn into_pixel(self, _: Rgba4444) -> [u8; 2] { + let c: u16 = self.into_pixel(Rgba4444); + c.to_ne_bytes() } } -impl IntoPixel<[u8; 2], Rgb565> for Color4 { - #[inline] - fn into_pixel(self) -> [u8; 2] { - let c: u16 = self.into_pixel_fmt(Rgb565); + +impl IntoPixel for Color4 { + /// Packs `self` into a `u16` in a 0bRRRRR_GGGGG_BBBBB_A format. + /// An alpha value of `0xFF` is considered opaque, any other value + /// fully transparent. + fn into_pixel(self, _: Rgba5551) -> u16 { + let [r, g, b, a] = self.0; + (r as u16 >> 3 & 0x1F) << 11 + | (g as u16 >> 3 & 0x1F) << 6 + | (b as u16 >> 3 & 0x1F) << 1 + | (a == 0xFF) as u16 + } +} +impl IntoPixel<[u8; 2], Rgba5551> for Color4 { + fn into_pixel(self, _: Rgba5551) -> [u8; 2] { + let c: u16 = self.into_pixel(Rgba5551); c.to_ne_bytes() } } +impl IntoPixel> for u8 { + fn into_pixel(self, fmt: Indexed8) -> C { + fmt.0[self as usize] + } +} + #[cfg(test)] #[allow(clippy::unusual_byte_groupings)] mod tests { use super::*; - use crate::math::{color::hex, rgb}; + use crate::math::{color::hex, rgb, rgba}; const COL3: Color3 = hex("#112233"); #[test] fn color3_to_rgb888() { - let pix: u32 = COL3.into_pixel_fmt(Rgb888); + let pix: [u8; 3] = COL3.into_pixel(Rgb888); + assert_eq!(pix, [0x11, 0x22, 0x33]); + } + #[test] + fn color3_to_xrgb8888() { + let pix: u32 = COL3.into_pixel(Xrgb8888); assert_eq!(pix, 0x00_11_22_33); } + #[test] + fn color3_to_rgba8888() { + let pix: u32 = COL3.into_pixel(Rgba8888); + assert_eq!(pix, 0x11_22_33_FF); + } + #[test] + fn color3_to_argb8888() { + let pix: u32 = COL3.into_pixel(Argb8888); + assert_eq!(pix, 0xFF_11_22_33); + } #[test] fn color3_to_rgb565() { - let pix: u16 = rgb(0x40, 0x20, 0x10).into_pixel(); + let pix: u16 = rgb(0x40, 0x20, 0x10).into_pixel(Rgb565); assert_eq!(pix, 0b01000_001000_00010_u16); - let pix: [u8; 2] = rgb(0x40u8, 0x20, 0x10).into_pixel_fmt(Rgb565); + let pix: [u8; 2] = rgb(0x40u8, 0x20, 0x10).into_pixel(Rgb565); assert_eq!(pix, [0b000_00010, 0b01000_001]); } - const COL4: Color4 = hex("#11223344"); + #[test] + fn color3_to_rgb332() { + let pix: u8 = rgb(0xFF, 0x00, 0xFF).into_pixel(Rgb332); + assert_eq!(pix, 0b111_000_11, "bits: {pix:b}"); + + let pix: u8 = rgb(0x00, 0x0FF, 0x00).into_pixel(Rgb332); + assert_eq!(pix, 0b000_111_00, "bits: {pix:b}"); + } + + const COL4: Color4 = hex("#112233AA"); #[test] - fn color4_to_rgba8888() { - let pix: u32 = COL4.into_pixel_fmt(Rgba8888); - assert_eq!(pix, 0x11_22_33_44); + fn color4_to_rgb888() { + let pix: [u8; 3] = COL4.into_pixel(Rgb888); + assert_eq!(pix, [0x11, 0x22, 0x33]); + } + #[test] + fn color4_to_xrgb8888() { + let pix: u32 = COL4.into_pixel(Xrgb8888); + assert_eq!(pix, 0x112233); + } + + #[test] + fn color4_to_rgb565() { + let pix: u16 = rgba(0x40, 0x20, 0x10, 0xAA).into_pixel(Rgb565); + assert_eq!(pix, 0b01000_001000_00010_u16); - let pix: [u8; 4] = COL4.into_pixel_fmt(Rgba8888); - assert_eq!(pix, [0x11, 0x22, 0x33, 0x44]); + let pix: [u8; 2] = rgba(0x40u8, 0x20, 0x10, 0xAA).into_pixel(Rgb565); + assert_eq!(pix, [0b000_00010, 0b01000_001]); } #[test] - fn color4_to_argb8888() { - let pix: u32 = COL4.into_pixel_fmt(Argb8888); - assert_eq!(pix, 0x44_11_22_33); + fn color4_to_rgba8888() { + let pix: u32 = COL4.into_pixel(Rgba8888); + assert_eq!(pix, 0x11_22_33_AA); - let pix: [u8; 4] = COL4.into_pixel_fmt(Argb8888); - assert_eq!(pix, [0x44, 0x11, 0x22, 0x33]); + let pix: [u8; 4] = COL4.into_pixel(Rgba8888); + assert_eq!(pix, [0x11, 0x22, 0x33, 0xAA]); } + #[test] + fn color4_to_argb8888() { + let pix: u32 = COL4.into_pixel(Argb8888); + assert_eq!(pix, 0xAA_11_22_33); + let pix: [u8; 4] = COL4.into_pixel(Argb8888); + assert_eq!(pix, [0xAA, 0x11, 0x22, 0x33]); + } #[test] fn color4_to_bgra8888() { - let pix: u32 = COL4.into_pixel_fmt(Bgra8888); - assert_eq!(pix, 0x33_22_11_44); + let pix: u32 = COL4.into_pixel(Bgra8888); + assert_eq!(pix, 0x33_22_11_AA); - let pix: [u8; 4] = COL4.into_pixel_fmt(Bgra8888); - assert_eq!(pix, [0x33, 0x22, 0x11, 0x44]); + let pix: [u8; 4] = COL4.into_pixel(Bgra8888); + assert_eq!(pix, [0x33, 0x22, 0x11, 0xAA]); } #[test] fn color4_to_rgba4444() { - let pix: [u8; 2] = COL4.into_pixel_fmt(Rgba4444); - assert_eq!(pix, [0x34, 0x12]); + let pix: [u8; 2] = COL4.into_pixel(Rgba4444); + assert_eq!(pix, [0x3A, 0x12]); + + let pix: u16 = COL4.into_pixel(Rgba4444); + assert_eq!(pix, 0x123A); + } + + #[test] + fn color4_to_rgba5551_u16() { + let pix: u16 = rgba(0x40u8, 0x20, 0x10, 0).into_pixel(Rgba5551); + assert_eq!(pix, 0b01000_00100_00010_0_u16, "bits: {pix:b}"); + + let pix: u16 = rgba(0x40u8, 0x20, 0x10, 0x80).into_pixel(Rgba5551); + assert_eq!(pix, 0b01000_00100_00010_0_u16, "bits: {pix:b}"); + + let pix: u16 = rgba(0x40u8, 0x20, 0x10, 0xFF).into_pixel(Rgba5551); + assert_eq!(pix, 0b01000_00100_00010_1_u16, "bits: {pix:b}"); + } + + #[test] + fn color4_to_rgba5551_2u8() { + let pix: [u8; 2] = rgba(0x40u8, 0x20, 0x10, 0).into_pixel(Rgba5551); + assert_eq!(pix, [0b00_00010_0, 0b01000_001]); - let pix: u16 = COL4.into_pixel_fmt(Rgba4444); - assert_eq!(pix, 0x1234); + let pix: [u8; 2] = rgba(0x40u8, 0x20, 0x10, 0xFF).into_pixel(Rgba5551); + assert_eq!(pix, [0b00_00010_1, 0b01000_001]); } } diff --git a/core/src/util/pnm.rs b/core/src/util/pnm.rs index 064ec6c8..a965615d 100644 --- a/core/src/util/pnm.rs +++ b/core/src/util/pnm.rs @@ -26,12 +26,9 @@ use std::{ use crate::math::{Color3, color::gray}; -use super::{Dims, buf::Buf2}; #[cfg(feature = "std")] -use super::{ - buf::AsSlice2, - pixfmt::{IntoPixel, Rgb888}, -}; +use super::{AsSlice2, IntoPixel, pixfmt::Rgb888}; +use super::{Buf2, Dims}; use Error::*; use Format::*; @@ -134,11 +131,27 @@ pub fn parse_pnm(input: impl IntoIterator) -> Result> { BinaryGraymap => it // .map(gray) .collect(), - BinaryBitmap => it - .flat_map(|byte| (0..8).rev().map(move |i| (byte >> i) & 1)) - // Conventionally in PBM 0 is white, 1 is black - .map(|bit| gray((1 - bit) * 0xFF)) - .collect(), + BinaryBitmap => { + // In P4, pixel values are packed in bytes, most significant bit first. + // Each row is padded to the next byte boundary, taking ⌈width/8⌉ bytes. + // For example, a 3x3 image takes three bytes, but a 9x1 image + // only takes two. + let w = h.dims.0 as usize; + let mut data = Vec::new(); + let mut it = it.peekable(); + while it.peek().is_some() { + // For each row + (&mut it) + .take(w.div_ceil(8)) + .flat_map(|byte| (0..8).rev().map(move |i| (byte >> i) & 1)) + .take(w) + .for_each(|bit| { + // Conventionally in PBM 0 is white, 1 is black + data.push(gray((1 - bit) * 0xFF)); + }); + } + data + } TextPixmap => { let mut col = [0u8; 3]; (0..3) @@ -208,11 +221,9 @@ where } .write(&mut out)?; - // Appease the borrow checker slice - .rows() - .flatten() - .map(|c| c.into_pixel()) + .iter() + .map(|c| c.into_pixel(Rgb888)) .try_for_each(|rgb| out.write_all(&rgb[..])) } @@ -264,18 +275,19 @@ impl Header { it.next().ok_or(UnexpectedEnd)?, ]; let format = magic.try_into()?; - let dims = (parse_u16(&mut it)?.into(), parse_u16(&mut it)?.into()); + let w = parse_u16(&mut it)?.into(); + let h = parse_u16(&mut it)?.into(); let max: u16 = match &format { TextBitmap | BinaryBitmap => 1, _ => parse_u16(&mut it)?, }; - Ok(Self { format, dims, max }) + Ok(Self { format, dims: Dims(w, h), max }) } /// Writes `self` to `dest` as a valid PNM header, /// including a trailing newline. #[cfg(feature = "std")] fn write(&self, mut dest: impl io::Write) -> io::Result<()> { - let Self { format, dims: (w, h), max } = *self; + let Self { format, dims: Dims(w, h), max } = *self; let max: &dyn Display = match format { TextBitmap | BinaryBitmap => &"", _ => &max, @@ -399,7 +411,7 @@ mod tests { Header::parse(*b"P6 123\t \n\r321 255 "), Ok(Header { format: BinaryPixmap, - dims: (123, 321), + dims: Dims(123, 321), max: 255, }) ); @@ -411,7 +423,7 @@ mod tests { Header::parse(*b"P6 # foo 42\n 123\n#bar\n#baz\n321 255 "), Ok(Header { format: BinaryPixmap, - dims: (123, 321), + dims: Dims(123, 321), max: 255, }) ); @@ -423,7 +435,7 @@ mod tests { Header::parse(*b"P2 123 456 789"), Ok(Header { format: TextGraymap, - dims: (123, 456), + dims: Dims(123, 456), max: 789, }) ); @@ -435,7 +447,7 @@ mod tests { Header::parse(*b"P3 123 456 789"), Ok(Header { format: TextPixmap, - dims: (123, 456), + dims: Dims(123, 456), max: 789, }) ); @@ -447,7 +459,7 @@ mod tests { Header::parse(*b"P4 123 456 "), Ok(Header { format: BinaryBitmap, - dims: (123, 456), + dims: Dims(123, 456), max: 1, }) ); @@ -459,7 +471,7 @@ mod tests { Header::parse(*b"P5 123 456 789 "), Ok(Header { format: BinaryGraymap, - dims: (123, 456), + dims: Dims(123, 456), max: 789, }) ); @@ -471,7 +483,7 @@ mod tests { Header::parse(*b"P6 123 456 789 "), Ok(Header { format: BinaryPixmap, - dims: (123, 456), + dims: Dims(123, 456), max: 789, }) ); @@ -508,7 +520,7 @@ mod tests { let mut out = Vec::new(); let hdr = Header { format: TextBitmap, - dims: (123, 456), + dims: Dims(123, 456), max: 1, }; hdr.write(&mut out).unwrap(); @@ -521,7 +533,7 @@ mod tests { let mut out = Vec::new(); let hdr = Header { format: BinaryPixmap, - dims: (123, 456), + dims: Dims(123, 456), max: 789, }; hdr.write(&mut out).unwrap(); @@ -549,7 +561,7 @@ mod tests { let buf = parse_pnm(data).unwrap(); - assert_eq!(buf.dims(), (2, 2)); + assert_eq!(buf.dims(), Dims(2, 2)); assert_eq!(buf[[0, 0]], rgb(0, 0, 0)); assert_eq!(buf[[1, 0]], rgb(123, 0, 42)); @@ -559,13 +571,16 @@ mod tests { #[test] fn read_pnm_p4() { - // 0x69 == 0b0110_1001 - let buf = parse_pnm(*b"P4 4 2\n\x69").unwrap(); + // In P4 each row is padded to the next byte boundary + // -> 0110 + // 1001 + // = 0b0110_0000 0b1001_0000 = 0x60 0x90 + let buf = parse_pnm(*b"P4 4 2\n\x60\x90").unwrap(); - assert_eq!(buf.dims(), (4, 2)); + assert_eq!(buf.dims(), Dims(4, 2)); - let b = rgb(0u8, 0, 0); - let w = rgb(0xFFu8, 0xFF, 0xFF); + let b = gray(0x00_u8); + let w = gray(0xFF_u8); assert_eq!(buf[0usize], [w, b, b, w]); assert_eq!(buf[1usize], [b, w, w, b]); @@ -575,7 +590,7 @@ mod tests { fn read_pnm_p5() { let buf = parse_pnm(*b"P5 2 2 255\n\x01\x23\x45\x67").unwrap(); - assert_eq!(buf.dims(), (2, 2)); + assert_eq!(buf.dims(), 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)]); @@ -592,7 +607,7 @@ mod tests { ) .unwrap(); - assert_eq!(buf.dims(), (2, 2)); + assert_eq!(buf.dims(), 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)]); @@ -610,7 +625,7 @@ mod tests { ]; let mut out = vec![]; - super::write_ppm(&mut out, Buf2::new_from((2, 2), buf)).unwrap(); + super::write_ppm(&mut out, Buf2::new_from(Dims(2, 2), buf)).unwrap(); assert_eq!( &out, diff --git a/core/tests/rendering.rs b/core/tests/rendering.rs index 1aefbcf7..d010c946 100644 --- a/core/tests/rendering.rs +++ b/core/tests/rendering.rs @@ -1,9 +1,10 @@ #![allow(unused)] +use retrofire_core::render::Indexed; use retrofire_core::{ prelude::*, render::{Model, render, shader, tex::SamplerClamp}, - util::{self, pixfmt::Xrgb8888, pnm::parse_pnm}, + util::{self, Dims, pixfmt::Xrgb8888, pnm::parse_pnm}, }; const VERTS: [Vertex3; 4] = [ @@ -16,7 +17,7 @@ const FACES: [Tri; 2] = [tri(0, 1, 2), tri(3, 2, 1)]; #[test] fn textured_quad() { - let checker = Texture::from(Buf2::new_with((8, 8), |x, y| { + let checker = Texture::from(Buf2::new_with(Dims(8, 8), |x, y| { let xor = (x ^ y) & 1; // Blue if x == y, dark red otherwise. rgba(0x7F * xor as u8, 0, 0xFF * (1 - xor) as u8, 0) @@ -26,18 +27,28 @@ 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); - let project = perspective(1.0, 1.0, 0.1..1000.0); - let viewport = viewport(pt2(0, 0)..pt2(w, h)); + let dims = Dims(256, 256); + let project = perspective(1.0, dims.aspect(), 0.1..1000.0); + let viewport = viewport(dims.into()); let mvp = translate((0.0, 0.0, 1.0)).to().then(&project); - let mut framebuf = Buf2::::new((w, h)); + let mut framebuf = Buf2::::new(dims); let mut ctx = Context::default(); - render(FACES, VERTS, &shader, &mvp, viewport, &mut framebuf, &ctx); + render( + &Indexed { + prims: FACES.as_slice(), + verts: VERTS.as_slice(), + }, + &shader, + &mvp, + viewport, + &mut framebuf, + &ctx, + ); assert_eq!(framebuf[0][0], rgb(0, 0, 0xFF)); assert_eq!(framebuf[255][0], rgb(0x7F, 0, 0)); diff --git a/core/triangle.ppm b/core/triangle.ppm index a0887752..702e7110 100644 Binary files a/core/triangle.ppm and b/core/triangle.ppm differ diff --git a/demos/Cargo.toml b/demos/Cargo.toml index 4e6e3ac5..294e22f2 100644 --- a/demos/Cargo.toml +++ b/demos/Cargo.toml @@ -22,7 +22,7 @@ repository.workspace = true documentation.workspace = true [dependencies] -re = { version = "0.4.0", path = "..", package = "retrofire" } +re = { version = "0.4.0", path = "..", package = "retrofire", features = ["std"], default-features = false } re-front = { version = "0.4.0", path = "../front", package = "retrofire-front" } minifb = { version = "0.27.0", optional = true } @@ -33,6 +33,7 @@ pancurses = { version = "0.17.0", optional = true } default = ["minifb"] minifb = ["dep:minifb", "re-front/minifb"] sdl2 = ["dep:sdl2", "re-front/sdl2"] +stats = ["re-front/stats"] [[bin]] name = "crates" diff --git a/demos/README.md b/demos/README.md index a60be49a..5d1bfac7 100644 --- a/demos/README.md +++ b/demos/README.md @@ -20,11 +20,16 @@ Simple demo programs showcasing [`retrofire`][1] features. * `bezier` : A Bézier curve bouncing around, like in a 90s screensaver. * `crates` : A scene demonstrating a first-person camera and controls. +* `curses` : A colorful torus rendered in the terminal using ncurses. * `hello` : A bouncing message, a custom message on the cmd line. * `solids` : A collection of solid shapes, hit space to switch. * `sprites`: A ball made of a large number of spherical particles. * `square` : A minimal example rendering a textured, transformed quad. +## Crate features + +* `stats`: Enables the collection and display of rendering statistics. + ## License Copyright 2020-2025 Johannes Dahlström. diff --git a/demos/nostd/src/main.rs b/demos/nostd/src/main.rs index 2486cd57..413509a2 100644 --- a/demos/nostd/src/main.rs +++ b/demos/nostd/src/main.rs @@ -10,7 +10,6 @@ use libc::{abort, c_char, c_int, free, malloc, putchar, puts}; use re::prelude::*; -use re::math::mat::ProjMat3; use re::render::{Model, render, shader}; #[global_allocator] @@ -29,7 +28,7 @@ unsafe impl GlobalAlloc for Malloc { #[panic_handler] unsafe fn panic(_info: &PanicInfo) -> ! { - unsafe { abort() } + unsafe { abort(); _info.message(). } } #[unsafe(no_mangle)] diff --git a/demos/src/bin/bezier.rs b/demos/src/bin/bezier.rs index 80c1d9d5..45eb7006 100644 --- a/demos/src/bin/bezier.rs +++ b/demos/src/bin/bezier.rs @@ -2,16 +2,19 @@ use core::ops::ControlFlow::Continue; use re::prelude::*; +use re::core::render::View; use re::core::{ - geom::Ray, + geom::{Polyline, Ray}, + math::color::gray, math::rand::{Distrib, Uniform, VectorsOnUnitDisk, Xorshift64}, math::spline::approximate, - render::raster::line, + render::{render, shader}, + util::dims, }; -use re::front::{Frame, dims, minifb::Window}; +use re::front::{Frame, minifb::Window}; fn main() { - let dims @ (w, h) = dims::SVGA_800_600; + let dims @ Dims(w, h) = dims::SVGA_800_600; let mut win = Window::builder() .title("retrofire//bezier") @@ -19,26 +22,36 @@ fn main() { .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 (min, max) = (pt2(0.0, 0.0), pt2(w as f32, h as f32)); let rng = &mut Xorshift64::from_time(); - let pos = Uniform::(min..max); + let pos = Uniform::>(min..max); let vel = VectorsOnUnitDisk; - let mut pos_vels: Vec<(Point2, Vec2)> = - (pos, vel).samples(rng).take(32).collect(); + let mut pos_vels: Vec<(Point2<_>, Vec2)> = (pos, vel) + .samples(rng) + .take(32) + .map(|(p, v)| (p, v.to())) + .collect(); // Disable some unneeded things win.ctx.color_clear = None; win.ctx.depth_clear = None; - win.run(|Frame { dt, buf, .. }| { - let buf = &mut buf.borrow_mut().color_buf.buf; + let proj = orthographic( + pt3(100.0, h as f32 - 100.0, -1.0), + pt3(w as f32 - 100.0, 100.0, 1.0), + ); + let vp = viewport(pt2(100, 100)..pt2(w - 100, h - 100)); + + win.run(|Frame { dt, buf, ctx, .. }| { + let buf = &mut buf.borrow_mut().color_buf; // Fade out previous frame a bit - buf.iter_mut() - .for_each(|c| *c = c.saturating_sub(0x08_08_02)); + buf.buf + .slice_mut(pt2(100, 100)..pt2(w - 100, h - 100)) + .iter_mut() + .for_each(|c| *c = c.saturating_sub(0x10_10_04)); let rays: Vec> = pos_vels .chunks(2) @@ -49,12 +62,18 @@ fn main() { // Stop once error is less than one pixel let approx = approximate(&b, 1.0); - for e in approx.edges() { - let vs = [e.0, e.1].map(|p| vertex(p.to_pt3().to(), ())); - line(vs, |sl| { - buf[sl.y][sl.xs].fill(0xFF_FF_FF); - }) - } + let approx = Polyline::new( + approx + .0 + .into_iter() + .map(|v| vertex(v.to_pt3(), ())), + ); + + let shader = shader::new( + |v: Vertex<_, _>, tf: &ProjMat3<_>| vertex(tf.apply(&v.pos), ()), + |_f, _| gray(0xFFu8).to_rgba(), + ); + render(approx, &shader, &proj, vp, buf, ctx); let dt = dt.as_secs_f32(); for (pos, vel) in &mut pos_vels { diff --git a/demos/src/bin/crates.rs b/demos/src/bin/crates.rs index d9f652db..70399e1c 100644 --- a/demos/src/bin/crates.rs +++ b/demos/src/bin/crates.rs @@ -10,27 +10,30 @@ use re::core::render::{ shader, tex::SamplerClamp, }; -// Try also Rgb565 or Rgba4444 -use re::core::util::{pixfmt::Rgba8888, pnm::read_pnm}; +use re::core::util::{pixfmt, pnm::read_pnm}; use re::front::sdl2::Window; use re::geom::solids::{Build, Cube}; +static CRATE_TEX: &[u8] = include_bytes!("../../assets/crate.ppm"); + +// Try also Rgba4444, Rgb565, Rgb5551, or Rgb332 +const PIXFMT: pixfmt::Rgba8888 = pixfmt::Rgba8888; + fn main() { let mut win = Window::builder() .title("retrofire//crates") - .pixel_fmt(Rgba8888) + .pixel_fmt(PIXFMT) .build() .expect("should create window"); - let tex_data = *include_bytes!("../../assets/crate.ppm"); - let tex = Texture::from(read_pnm(&tex_data[..]).expect("data exists")); + let tex = Texture::from(read_pnm(CRATE_TEX).expect("data exists")); let light_dir = vec3(-2.0, 1.0, -4.0).normalize(); 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 +42,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); @@ -47,7 +50,7 @@ fn main() { }, ); - let (w, h) = win.dims; + let Dims(w, h) = win.dims; let mut cam = Camera::new(win.dims) .transform(FirstPerson::default()) .viewport((10..w - 10, h - 10..10)) @@ -112,7 +115,10 @@ fn main() { // Crates for Obj { geom, bbox, tf } in &crates { - frame.ctx.stats.borrow_mut().objs.i += 1; + #[cfg(feature = "stats")] + { + frame.ctx.stats.borrow_mut().objs.i += 1; + } let model_to_project = tf.then(world_to_project); @@ -129,7 +135,10 @@ fn main() { .uniform(&model_to_project) .render(); - frame.ctx.stats.borrow_mut().objs.o += 1; + #[cfg(feature = "stats")] + { + frame.ctx.stats.borrow_mut().objs.o += 1; + } } Continue(()) diff --git a/demos/src/bin/curses.rs b/demos/src/bin/curses.rs index b3180849..103ca12a 100644 --- a/demos/src/bin/curses.rs +++ b/demos/src/bin/curses.rs @@ -1,14 +1,12 @@ -#![allow(clippy::unusual_byte_groupings)] - use std::time::Instant; use pancurses::*; use re::prelude::*; +use re::core::render::{Model, render, shader}; use re::core::render::{ - Model, ctx::DepthSort::BackToFront, raster::Scanline, render, shader, - stats::Throughput, + ctx::DepthSort::BackToFront, debug::dir_to_rgb, raster::Scanline, }; use re::geom::solids::{Build, Torus}; @@ -21,15 +19,13 @@ impl Win { curs_set(0); start_color(); - // Create an RGB 332 palette but keep the eight standard colors - for i in 8..256 { - // Range from 0 to 1000 - let r = (i & 0b111_000_00) * 4; - let g = (i & 0b000_111_00) * 35; - let b = (i & 0b000_000_11) * 330; - - init_color(i, r, g, b); - init_pair(i, i, i); + // Use the standard xterm 8-bit palette: + // 0..8: basic dark + // 8..16: basic bright + // 16..232: 6x6x6 RGB cube + // 232..256: sixteen shades of gray + for i in 16..232 { + init_pair(i, 15, i); } Self(w) } @@ -52,17 +48,14 @@ 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; - rgb(x, y, z).to_color4() - }, + |frag: Frag, _: &_| dir_to_rgb(frag.var).to_color4(), ); let torus = Torus { major_radius: 1.0, minor_radius: 0.3, - major_sectors: 32, - minor_sectors: 16, + major_sectors: 19, + minor_sectors: 13, } .build(); @@ -77,7 +70,11 @@ fn main() { win.0.attrset(COLOR_PAIR(0)); win.0.mvprintw(0, 0, "Q to quit"); - ctx.stats.borrow_mut().frames += 1.0; + #[cfg(feature = "stats")] + { + ctx.stats.borrow_mut().frames += 1.0; + } + let t_secs = start.elapsed().as_secs_f32(); let mvp = rotate_x(rads(t_secs)) @@ -86,15 +83,7 @@ fn main() { .to() .then(&project); - render( - &torus.faces, - &torus.verts, - &shader, - &mvp, - viewport, - &mut win, - &ctx, - ); + render(torus.clone(), &shader, &mvp, viewport, &mut win, &ctx); win.0.refresh(); napms(10); @@ -103,37 +92,36 @@ fn main() { break; } } + + // Return to normal terminal mode + drop(win); + + #[cfg(feature = "stats")] + { + println!("{}", ctx.stats.into_inner().finish()); + } } impl Target for Win { - fn rasterize( + fn rasterize>( &mut self, mut sc: Scanline, fs: &Fs, + uni: U, _ctx: &Context, - ) -> Throughput - where - V: Vary, - Fs: FragmentShader, - { - let w = sc.xs.len(); - let y = sc.y; - - self.0.mv(y as i32, sc.xs.start as i32); + ) { + self.0.mv(sc.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); - - let col = (r & 0b111_000_00) - | ((g / 9) & 0b000_111_00) - | ((b / 85) & 0b000_000_11); - - // Avoid the eight standard colors - self.0.addch(COLOR_PAIR(col.max(8) as chtype)); + // Map the RGB to the closest color in the 6x6x6 xterm RGB cube + let [r, g, b, _] = col.0.map(|c| c / 43); + let col = 16 + 36 * r + 6 * g + b; + self.0 + .addch(COLOR_PAIR(col as chtype) | ' ' as chtype); } - Throughput { i: w, o: w } + //Throughput { i: sc.xs.len(), o: sc.xs.len() } } } diff --git a/demos/src/bin/hello.rs b/demos/src/bin/hello.rs index 605f5fc0..7375039b 100644 --- a/demos/src/bin/hello.rs +++ b/demos/src/bin/hello.rs @@ -3,65 +3,63 @@ use std::{env, fmt::Write, ops::ControlFlow::Continue}; use re::prelude::*; use re::core::{ - render::{Model, Text, World, render, shader, tex::Atlas, tex::Layout}, - util::pnm::parse_pnm, + math::color::hsl, + render::{Text, World, tex::Atlas}, + util::{dims, pnm::read_pnm}, }; +use re_front::{Frame, minifb::Window}; -use re_front::{Frame, dims::SVGA_800_600, minifb::Window}; +const FONT: &[u8] = include_bytes!("../../assets/font_16x24.pbm"); fn main() { - let font = *include_bytes!("../../assets/font_16x24.pbm"); - let font = parse_pnm(font).expect("valid image"); - let font = Atlas::new(Layout::Grid { sub_dims: (16, 24) }, font.into()); + let font = read_pnm(FONT).expect("valid image"); + let font = Atlas::grid(Dims(16, 24), font.into()); - let msg = env::args().nth(1); // Borrow checker... - let msg = msg + let arg = env::args().nth(1); // Borrow checker... + let msg = arg .as_deref() .unwrap_or(" Hello,\nRetrocomputing\n World!"); - let mut text = Text::new(font); + let mut text = Text::new(font.clone()); write!(text, "{msg}").expect("cannot fail"); let mut win = Window::builder() .title("retrofire//text") - .dims(SVGA_800_600) + .dims(dims::SVGA_800_600) .build() .unwrap(); win.ctx.face_cull = None; - let shader = shader::new( - |v: Vertex<_, _>, mvp: &ProjMat3| { - vertex(mvp.apply(&v.pos), v.attrib) - }, - |frag: Frag| text.sample(frag.var).to_rgba(), - ); - - let vp: ProjMat3 = translate(vec3(0.0, 0.0, 15.0)) + let dims = win.dims; + let world_to_project: ProjMat3 = translate(vec3(0.0, 0.0, 15.0)) .to() - .then(&perspective(1.0, 4.0 / 3.0, 0.1..1000.0)); + .then(&perspective(1.0, dims.aspect(), 0.1..1000.0)); - let viewport = viewport(pt2(10, 10)..pt2(790, 590)); + let viewport = viewport(pt2(10, 10)..pt2(dims.0 - 10, dims.1 - 10)); win.run(|frame: &mut Frame<_, _>| { let secs = frame.t.as_secs_f32(); - let mvp = scale(0.1) + let model_to_world = scale(0.1) .then(&translate((-10.0, -5.0, 5.0 * secs.sin()))) .then(&rotate_y(rads(secs * 0.59))) .then(&rotate_z(rads((secs * 1.13).sin()))) - .to() - .then(&vp); - - render( - &text.geom.faces, - &text.geom.verts, - &shader, - &mvp, - viewport, - &mut frame.buf, - frame.ctx, - ); + .to(); + + let model_to_project = model_to_world.then(&world_to_project); + + text.color = hsl(secs / 10.0 % 1.0, 0.8, 0.6) + .to_rgb() + .to_color3(); + + text.batch() + .uniform(&model_to_project) + .viewport(viewport) + .target(&mut frame.buf) + .context(frame.ctx) + .render(); + Continue(()) }); } diff --git a/demos/src/bin/solids.rs b/demos/src/bin/solids.rs index e1a7255a..903bc9b9 100644 --- a/demos/src/bin/solids.rs +++ b/demos/src/bin/solids.rs @@ -1,4 +1,5 @@ use core::ops::ControlFlow::Continue; +use std::{env, sync::LazyLock}; use minifb::{Key, KeyRepeat}; @@ -6,48 +7,31 @@ use re::prelude::*; use re::core::{ geom::{Polyline, Ray}, - math::{ProjMat3, ProjVec3, color::gray, spline::HermiteSpline}, - render::{Model, ModelToWorld, cam::Fov, shader}, + math::{ProjVec3, color::gray, spline::HermiteSpline}, + render::{Model, cam::Fov, debug, debug::DbgMesh, shader}, }; -use re::front::{Frame, minifb::Window}; +use re::front::minifb::Window; use re::geom::{io::read_obj, solids::*}; -// Carousel animation for switching between objects. #[derive(Default)] -struct Carousel { +struct State { + lod: u32, + objects: [Mesh; N_OBJS], + debug_mesh: DbgMesh, + debug_flags: [bool; 6], + idx: usize, new_idx: usize, - 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))) - } + anim_t: Option, } fn main() { - eprintln!("Press Space to cycle between objects..."); + eprintln!( + "[Space] : cycle between objects\n\ + [.] and [,] : adjust level of detail\n\ + [0] : toggle mesh rendering\n\ + [1] to [5] : toggle visualizations" + ); let mut win = Window::builder() .title("retrofire//solids") @@ -56,7 +40,7 @@ fn main() { win.ctx.color_clear = Some(gray(0x33).to_rgba()); - let (w, h) = win.dims; + let Dims(w, h) = win.dims; let cam = Camera::new(win.dims) .transform(scale((1.0, -1.0, -1.0)).to()) .perspective(Fov::Equiv35mm(28.0), 0.1..1000.0) @@ -76,57 +60,84 @@ fn main() { let col = diffuse * rgb(r, g, b); vertex(mvp.apply(&v.pos), col) } - - fn frag_shader(f: Frag) -> Color4 { + fn frag_shader(f: Frag, _: Uniform) -> Color4 { f.var.to_color4() } - let shader = shader::new(vtx_shader, frag_shader); - let objects = objects_n(8); - let translate = translate(-3.0 * Vec3::Z); - let mut carousel = Carousel::default(); - win.run(|frame| { - let Frame { t, dt, win, .. } = frame; + let mut state = State::new(); + if let Some(idx) = env::args().nth(1) { + let Some(idx) = idx.parse().ok().filter(|&i| i < N_OBJS) else { + panic!("argument must be integer in range 0..={}", N_OBJS - 1) + }; + state.idx = idx; + } - // Press Space to trigger carousel animation - if win.imp.is_key_pressed(Key::Space, KeyRepeat::No) { - carousel.start(); + win.run(|frame| { + for key in frame.win.imp.get_keys_pressed(KeyRepeat::No) { + use Key::*; + match key { + Space => state.start_carousel(), + + Comma | Period => { + let (num, denom) = + if key == Comma { (3, 4) } else { (4, 3) }; + state.set_lod((state.lod * num / denom).clamp(3, 50)); + } + + digit @ (Key0 | Key1 | Key2 | Key3 | Key4 | Key5) => { + state.toggle_flag(digit as usize); + } + + _ => (), + } } - let theta = rads(t.as_secs_f32()); + let theta = rads(frame.t.as_secs_f32()); let spin = rotate_x(theta * 0.37).then(&rotate_y(theta * 0.51)); - let carouse = carousel.update(dt.as_secs_f32()); + let carouse = state.update(frame.dt.as_secs_f32()); // Compose transform stack let model_view_project: ProjMat3 = spin .then(&translate) .then(&carouse) - .to::() + .to() .then(&cam.world_to_project()); - let object = &objects[carousel.idx % objects.len()]; - - Batch { - prims: object.faces.clone(), - verts: object.verts.clone(), - uniform: (&model_view_project, &spin), - shader: shader, - viewport: cam.viewport, - target: frame.buf, - ctx: &*frame.ctx, + state + .debug_mesh + .batch() + .clone() + .uniform(&model_view_project) + .viewport(cam.viewport) + .target(frame.buf) + .render(); + + if state.debug_flags[0] { + let object = state.object(); + Batch { + prims: &object.faces, + verts: &object.verts, + uniform: (&model_view_project, &spin), + shader: shader, + viewport: cam.viewport, + target: frame.buf, + ctx: &*frame.ctx, + } + .render(); } - .render(); Continue(()) }); } +const N_OBJS: usize = 14; + // Creates the 14 objects exhibited. #[rustfmt::skip] -fn objects_n(res: u32) -> [Mesh; 14] { +fn objects_n(res: u32) -> [Mesh; N_OBJS] { let segments = res; let sectors = 2 * res; @@ -152,9 +163,9 @@ fn objects_n(res: u32) -> [Mesh; 14] { Torus { major_radius: 0.9, minor_radius: 0.3, major_sectors, minor_sectors }.build(), // Traditional demo models - teapot(), - bunny(), - dragon() + teapot().clone(), + bunny().clone(), + dragon().clone() ] } @@ -180,30 +191,112 @@ fn lathe(secs: u32) -> Mesh { } // Loads the Utah teapot model. -fn teapot() -> Mesh { - static TEAPOT: &[u8] = include_bytes!("../../assets/teapot.obj"); - read_obj(TEAPOT) - .unwrap() - .transform(&scale(0.4).then(&translate(-0.5 * Vec3::Y)).to()) - .build() +fn teapot() -> &'static Mesh { + static TEAPOT: LazyLock> = LazyLock::new(|| { + let obj: &[_] = include_bytes!("../../assets/teapot.obj"); + read_obj::(obj) + .unwrap() + .transform(&scale(0.4).then(&translate(-0.5 * Vec3::Y)).to()) + .build() + }); + &TEAPOT } // Loads the Stanford bunny model. -fn bunny() -> Mesh { - static BUNNY: &[u8] = include_bytes!("../../assets/bunny.obj"); - read_obj::<()>(BUNNY) - .unwrap() - .transform(&scale(0.12).then(&translate(-Vec3::Y)).to()) - .with_vertex_normals() - .build() +fn bunny() -> &'static Mesh { + static BUNNY: LazyLock> = LazyLock::new(|| { + let obj: &[_] = include_bytes!("../../assets/bunny.obj"); + read_obj::<()>(obj) + .unwrap() + .transform(&scale(0.12).then(&translate(-Vec3::Y)).to()) + .with_vertex_normals() + .build() + }); + &BUNNY } // Loads the Stanford dragon model. -fn dragon() -> Mesh { - static DRAGON: &[u8] = include_bytes!("../../assets/dragon.obj"); - read_obj::<()>(DRAGON) - .unwrap() - .with_vertex_normals() - .transform(&scale(0.18).then(&translate(-0.5 * Vec3::Y)).to()) - .build() +fn dragon() -> &'static Mesh { + static DRAGON: LazyLock> = LazyLock::new(|| { + let obj: &[_] = include_bytes!("../../assets/dragon.obj"); + read_obj::<()>(obj) + .unwrap() + .with_vertex_normals() + .transform(&scale(0.18).then(&translate(-0.5 * Vec3::Y)).to()) + .build() + }); + &DRAGON +} + +impl State { + fn new() -> Self { + let mut state = State::default(); + state.lod = 10; + state.objects = objects_n(state.lod); + state.debug_flags[0] = true; + state + } + + fn object(&self) -> &Mesh { + &self.objects[self.idx] + } + + fn set_lod(&mut self, lod: u32) { + self.lod = lod; + self.objects = objects_n(lod); + self.build_debug_mesh(); + } + + fn toggle_flag(&mut self, flag: usize) { + self.debug_flags[flag] ^= true; + self.build_debug_mesh(); + } + + fn start_carousel(&mut self) { + if self.anim_t.is_none() { + self.anim_t = Some(0.0); + self.new_idx = self.idx + 1; + } else { + // If already started, skip to next + self.new_idx += 1; + } + self.new_idx %= self.objects.len(); + } + + fn update(&mut self, dt: f32) -> Mat4 { + let Some(t) = self.anim_t.as_mut() else { + return Mat4::identity(); + }; + *t += dt; + let t = *t; + if t >= 0.5 && self.idx != self.new_idx { + self.idx = self.new_idx; + self.build_debug_mesh(); + } + if t >= 1.0 { + self.anim_t = None + } + rotate_y(turns(smootherstep(t))) + } + + fn build_debug_mesh(&mut self) { + let [_, ed, fns, vns, bb, bas] = self.debug_flags; + let mut dm = debug::mesh(self.objects[self.idx].clone()); + if ed { + dm = dm.edges(); + } + if fns { + dm = dm.face_normals(0.2); + } + if vns { + dm = dm.vertex_normals(0.2); + } + if bb { + dm = dm.bbox(); + } + if bas { + dm = dm.basis(); + } + self.debug_mesh = dm; + } } diff --git a/demos/src/bin/sprites.rs b/demos/src/bin/sprites.rs index 2892bcc3..af58271f 100644 --- a/demos/src/bin/sprites.rs +++ b/demos/src/bin/sprites.rs @@ -6,7 +6,7 @@ use re::core::math::{ color::gray, rand::{Distrib, PointsInUnitBall, Xorshift64}, }; -use re::core::render::{Model, View, cam::*, render, shader}; +use re::core::render::{Indexed, Model, View, cam::*, render, shader}; use re_front::minifb::Window; @@ -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); @@ -52,7 +52,7 @@ fn main() { }, ); - let (w, h) = win.dims; + let Dims(w, h) = win.dims; let cam = Camera::new(win.dims) .transform(translate(0.5 * Vec3::Z).to()) .perspective(Fov::FocalRatio(1.0), 1e-2..1e3) @@ -66,11 +66,14 @@ fn main() { .to() .then(&cam.world_to_view()); + let uniform = (modelview, cam.project); render( - &tris, - &verts, + Indexed { + prims: tris.as_slice(), + verts: verts.as_slice(), + }, &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 2bc53df8..332aa350 100644 --- a/demos/src/bin/square.rs +++ b/demos/src/bin/square.rs @@ -2,7 +2,7 @@ use core::ops::ControlFlow::*; use re::prelude::*; -use re::core::render::{render, shader, tex::SamplerClamp}; +use re::core::render::{Indexed, render, shader, tex::SamplerClamp}; use re::front::minifb::Window; fn main() { @@ -29,19 +29,19 @@ fn main() { }; // Texture with a check pattern - let checker = Texture::from(Buf2::new_with((8, 8), |x, y| { + let checker = Texture::from(Buf2::new_with(Dims(8, 8), |x, y| { let xor = (x ^ y) & 1; rgba(xor as u8 * 255, 128, 255 - xor as u8 * 128, 0) })); 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; - let projection = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); - let viewport = viewport(pt2(10, 10)..pt2(w - 10, h - 10)); + let dims = win.dims; + let projection = perspective(1.0, dims.aspect(), 0.1..1000.0); + let viewport = viewport(pt2(10, 10)..pt2(dims.0 - 10, dims.1 - 10)); win.run(|frame| { let time = frame.t.as_secs_f32(); @@ -52,8 +52,10 @@ fn main() { .then(&projection); render( - [tri(0, 1, 2), tri(3, 2, 1)], - verts, + Indexed { + prims: [tri(0, 1, 2), tri(3, 2, 1)].as_slice(), + verts: verts.as_slice(), + }, &shader, &model_view_project, viewport, diff --git a/demos/wasm/src/triangle.rs b/demos/wasm/src/triangle.rs index d2d20734..e80e50fd 100644 --- a/demos/wasm/src/triangle.rs +++ b/demos/wasm/src/triangle.rs @@ -4,14 +4,14 @@ use wasm_bindgen::prelude::*; use re::prelude::*; -use re::render::{ModelToView, render, shader::Shader}; -use re::util::Dims; -use re_front::{dims::SVGA_800_600, wasm::Window}; +use re::render::{Model, View, render, shader::Shader}; +use re::util::{Dims, dims}; +use re_front::wasm::Window; // Entry point from JS #[wasm_bindgen(start)] pub fn start() { - const DIMS: Dims = SVGA_800_600; + const DIMS: Dims = dims::SVGA_800_600; console_error_panic_hook::set_once(); @@ -24,7 +24,7 @@ pub fn start() { vertex(pt3(2.0, 2.0, 0.0), rgba(0.2, 0.9, 0.1, 0.8)), ]; - let proj = perspective(1.0, 4.0 / 3.0, 0.1..1000.0); + let proj = perspective(1.0, DIMS.aspect(), 0.1..1000.0); let vp = viewport(pt2(8, 8)..pt2(DIMS.0 - 8, DIMS.1 - 8)); win.run(move |mut frame| { @@ -32,12 +32,12 @@ pub fn start() { let mv = rotate_z(rads(t)) .then(&translate((3.0 + 2.0 * t.sin()) * Vec3::Z)) - .to::(); + .to::(); let mvp = mv.then(&proj); let sh = Shader::new( - |v: Vertex3, _| vertex(mvp.apply(&v.pos), v.attrib), - |f: Frag| f.var.to_color4(), + |v: Vertex3, ()| vertex(mvp.apply(&v.pos), v.attrib), + |f: Frag, ()| f.var.to_color4(), ); render([tri(0, 1, 2)], vs, &sh, (), vp, &mut frame.buf, frame.ctx); diff --git a/docs/curses.png b/docs/curses.png new file mode 100644 index 00000000..36197afa Binary files /dev/null and b/docs/curses.png differ diff --git a/docs/dragon.jpg b/docs/dragon.jpg new file mode 100644 index 00000000..c64b3667 Binary files /dev/null and b/docs/dragon.jpg differ diff --git a/front/Cargo.toml b/front/Cargo.toml index 792fbb06..57cdf81e 100644 --- a/front/Cargo.toml +++ b/front/Cargo.toml @@ -22,6 +22,7 @@ repository.workspace = true documentation.workspace = true [features] +stats = ["retrofire-core/stats"] wasm = ["dep:wasm-bindgen", "dep:web-sys"] wasm-dev = ["wasm", "dep:console_error_panic_hook"] diff --git a/front/README.md b/front/README.md index d62ea872..b9fbfcdc 100644 --- a/front/README.md +++ b/front/README.md @@ -12,7 +12,8 @@ # Retrofire-front -Simple frontends for [`retrofire`][1]. +Simple frontends for [`retrofire`][1], managing window creation and basic event +handling. [1]: https://crates.io/crates/retrofire @@ -20,7 +21,8 @@ Simple frontends for [`retrofire`][1]. * `minifb`: Enables a frontend using the [`minifb`][2] library. * `sdl2`: Enables a frontend using the [`sdl2`][3] library. -* `wasm` Enables a frontend using WebAssembly and [`wasm-bindgen`][4]. +* `wasm`: Enables a frontend using WebAssembly and [`wasm-bindgen`][4]. +* `stats`: Enables collection and display of performance statistics. All features are disabled by default. @@ -30,6 +32,63 @@ All features are disabled by default. [4]: https://crates.io/crates/wasm-bindgen +## Example + +```rust +use core::ops::ControlFlow::*; + +use re::core::render::{Model, render, shader}; +use re::front::minifb::{Frame, Window}; +use re::prelude::*; + +fn main() { + let mut win = Window::builder() + .title("retrofire//example") + .build() + .expect("should create window"); + + // Initialize + let triangle = [ + vertex(pt3(-1.0, 0.6, 0.0), rgb(1.0, 0.0, 0.0)), + vertex(pt3(1.0, 0.6, 0.0), rgb(0.0, 0.8, 0.0)), + vertex(pt3(0.0, -1.2, 0.0), rgb(0.4, 0.4, 1.0)), + ]; + let shader = shader::new( + |v: Vertex3, mvp: &ProjMat3| { + vertex(mvp.apply(&v.pos), v.attrib) + }, + |frag: Frag>, _: &_| frag.var.to_color4(), + ); + let Dims(w, h) = win.dims; + let to_view = translate((0.0, 0.0, 2.0)); + let project = perspective(1.0, w as f32 / h as f32, 0.1..1000.0); + let viewport = viewport(pt2(0, h)..pt2(w, 0)); + + // Run the main loop + win.run(|frame: &mut Frame| { + // Handle events + // Automatically quits if window is closed or ESC is pressed + + // Update state + let to_world = rotate_z(rads(frame.t.as_secs_f32())); + + // Render + render( + [tri(0, 1, 2)], + triangle, + &shader, + &to_world.then(&to_view).to().then(&project), + viewport, + &mut frame.buf, + frame.ctx, + ); + + // Returning Break(()) quits the application + Continue(()) + }); +} +``` + ## License Copyright 2020-2025 Johannes Dahlström. diff --git a/front/assets/font_6x10.pbm b/front/assets/font_6x10.pbm new file mode 100644 index 00000000..e889aa4c Binary files /dev/null and b/front/assets/font_6x10.pbm differ diff --git a/front/src/lib.rs b/front/src/lib.rs index 24baa639..ca30b4ca 100644 --- a/front/src/lib.rs +++ b/front/src/lib.rs @@ -6,9 +6,9 @@ extern crate core; use core::{cell::RefCell, time::Duration}; use retrofire_core::{ - math::Color4, - render::{Colorbuf, Context, Framebuf}, - util::{buf::AsMutSlice2, pixfmt::IntoPixel}, + math::{Color3, Color4}, + render::{Colorbuf, Context, Framebuf, tex::Atlas}, + util::{AsMutSlice2, Dims, IntoPixel, pnm::read_pnm}, }; #[cfg(feature = "minifb")] @@ -21,6 +21,14 @@ pub mod sdl2; #[cfg(feature = "wasm")] pub mod wasm; +pub static FONT_6X10: &[u8] = include_bytes!("../assets/font_6x10.pbm"); + +/// Returns a 6x10 bitmap font, e.g. for rendering debug messages. +pub fn font_6x10() -> Atlas { + let font = read_pnm(FONT_6X10).expect("font statically included"); + Atlas::grid(Dims(6, 10), font.into()) +} + /// 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, Buf> { @@ -36,59 +44,6 @@ pub struct Frame<'a, Win, Buf> { pub ctx: &'a mut Context, } -#[allow(non_upper_case_globals)] -pub mod dims { - // Source for the names: - // https://commons.wikimedia.org/wiki/File:Vector_Video_Standards8.svg - - use retrofire_core::util::Dims; - - // 5:4 - pub const qSXGA_640_512: Dims = (640, 512); - pub const SXGA_1280_1024: Dims = (1280, 1024); - - // 4:3 - pub const qVGA_320_240: Dims = (320, 240); - pub const qSVGA_400_300: Dims = (400, 300); - pub const qXGA_512_384: Dims = (512, 384); - pub const VGA_640_480: Dims = (640, 480); - pub const SVGA_800_600: Dims = (800, 600); - pub const XGA_1024_768: Dims = (1024, 768); - pub const QVGA_1280_960: Dims = (1280, 960); - pub const UXGA_1600_1200: Dims = (1600, 1200); - pub const QXGA_2048_1536: Dims = (2048, 1536); - - // 16:10 - pub const CGA_320_200: Dims = (320, 200); - pub const MODE_13H: Dims = CGA_320_200; - pub const QCGA_640_400: Dims = (640, 400); - pub const qWXGA_640_400: Dims = (640, 640); - pub const WXGA_1280_800: Dims = (1280, 800); - pub const WXGAP_1440_900: Dims = (1440, 900); - pub const WSXGAP_1680_1050: Dims = (1680, 1050); - pub const WUXGA_1920_1200: Dims = (1920, 1200); - pub const WQXGA_2560_1600: Dims = (2560, 1600); - - // 16:9 - // 640x360 = "qHD"? - // 800x450 = qWSXGA? - // 960x540 = qFHD - pub const HD_1280_720: Dims = (1280, 720); - pub const WSXGA_1600_900: Dims = (1600, 900); - pub const FHD_1920_1080: Dims = (1920, 1080); - pub const QHD_2560_1440: Dims = (2560, 1440); - pub const UHD_4K_3840_2160: Dims = (3840, 2160); - - // DCI ~17:9 - pub const DCI_2K_2048_1080: Dims = (2048, 1080); - pub const DCI_4K_4096_2160: Dims = (4096, 2160); - - // ~21:9 - pub const qUWFHD_1280_540: Dims = (1280, 540); - pub const UWFHD_2560_1080: Dims = (2560, 1080); - pub const UWQHD_3440_1440: Dims = (3440, 1440); -} - impl Frame<'_, Win, &RefCell, Zbuf>>> where @@ -102,11 +57,9 @@ where /// is enabled. pub fn clear(&mut self) { if let Some(c) = self.ctx.color_clear { - self.buf - .borrow_mut() - .color_buf - .as_mut_slice2() - .fill(c.into_pixel()); + let cbuf = &mut self.buf.borrow_mut().color_buf; + let fmt = cbuf.fmt; + cbuf.as_mut_slice2().fill(c.into_pixel(fmt)); } if let Some(z) = self.ctx.depth_clear { // Depth buffer contains reciprocal depth values diff --git a/front/src/minifb.rs b/front/src/minifb.rs index 0ec0c42c..71c7f7db 100644 --- a/front/src/minifb.rs +++ b/front/src/minifb.rs @@ -2,6 +2,7 @@ use core::{ cell::RefCell, + fmt::Write, mem::replace, ops::ControlFlow::{self, *}, }; @@ -10,11 +11,16 @@ use std::time::Instant; use minifb::{Key, WindowOptions}; use retrofire_core::{ - render::{Colorbuf, Context, Stats, target}, - util::{Dims, buf::Buf2, buf::MutSlice2, pixfmt::Xrgb8888}, + render::{Colorbuf, Context, Text, target}, + util::{Buf2, Dims, MutSlice2, dims, pixfmt::Xrgb8888}, }; -use super::{Frame, dims}; +use super::font_6x10; + +#[cfg(feature = "stats")] +use retrofire_core::render::Stats; +#[cfg(not(feature = "stats"))] +pub type Stats = (); /// A lightweight wrapper of a `minibuf` window. pub struct Window { @@ -39,6 +45,8 @@ pub type Framebuf<'a> = target::Framebuf< MutSlice2<'a, f32>, >; +pub type Frame<'a> = super::Frame<'a, Window, &'a RefCell>>; + impl Default for Builder<'_> { fn default() -> Self { Self { @@ -102,7 +110,7 @@ impl Window { /// # Panics /// If `fb.len() < self.size.0 * self.size.1`. pub fn present(&mut self, fb: &[u32]) { - let (w, h) = self.dims; + let Dims(w, h) = self.dims; self.imp .update_with_buffer(fb, w as usize, h as usize) .unwrap(); @@ -117,13 +125,15 @@ impl Window { /// * the callback returns `ControlFlow::Break`. pub fn run(&mut self, mut frame_fn: F) -> Stats where - F: FnMut(&mut Frame>) -> ControlFlow<()>, + F: FnMut(&mut Frame) -> ControlFlow<()>, { - let (w, h) = self.dims; - let mut cbuf = Buf2::new((w, h)); - let mut zbuf = Buf2::new((w, h)); + let mut cbuf = Buf2::new(self.dims); + let mut zbuf = Buf2::new(self.dims); let mut ctx = self.ctx.clone(); + let mut fps = Text::new(font_6x10()); + fps.anchor = (2.0, 2.0).into(); + let start = Instant::now(); let mut last = Instant::now(); loop { @@ -146,13 +156,24 @@ impl Window { if let Break(_) = frame_fn(frame) { break; } + + fps.clear(); + _ = write!(fps, "{:>6.1}", frame.dt.as_secs_f32().recip()); + fps.render(&mut frame.buf); + self.present(cbuf.data_mut()); - ctx.stats.borrow_mut().frames += 1.0; + #[cfg(feature = "stats")] + { + ctx.stats.borrow_mut().frames += 1.0; + } + } + #[cfg(feature = "stats")] + { + let stats = ctx.stats.into_inner().finish(); + println!("{stats}"); + stats } - let stats = ctx.stats.into_inner(); - println!("{stats}"); - stats } fn should_quit(&self) -> bool { diff --git a/front/src/sdl2.rs b/front/src/sdl2.rs index 9c57d50e..f98d30e8 100644 --- a/front/src/sdl2.rs +++ b/front/src/sdl2.rs @@ -1,5 +1,5 @@ //! Frontend using the `sdl2` crate for window creation and event handling. -use core::{cell::RefCell, fmt, mem::replace, ops::ControlFlow}; +use core::{cell::RefCell, fmt, fmt::Write, mem::replace, ops::ControlFlow}; use std::time::Instant; use sdl2::{ @@ -11,15 +11,21 @@ use sdl2::{ video::{FullscreenType, Window as SdlWindow, WindowBuildError}, }; -use retrofire_core::math::Color4; -use retrofire_core::render::{Colorbuf, Context, Stats, target}; -use retrofire_core::util::{ - Dims, - buf::{AsMutSlice2, Buf2, MutSlice2}, - pixfmt::{IntoPixel, Rgb565, Rgba4444, Rgba8888}, +use retrofire_core::{ + math::Color4, + render::{Colorbuf, Context, Text, target}, + util::pixfmt::{Rgb565, Rgba4444, Rgba8888}, + util::{AsMutSlice2, Buf2, Dims, IntoPixel, MutSlice2, dims}, }; -use super::{Frame, dims}; +use super::{Frame, font_6x10}; + +#[cfg(feature = "stats")] +use retrofire_core::render::Stats; +use retrofire_core::util::pixfmt::{Rgb332, Rgba5551}; + +#[cfg(not(feature = "stats"))] +pub type Stats = (); /// Helper trait to support different pixel format types. pub trait PixelFmt: Copy + Default { @@ -28,7 +34,7 @@ pub trait PixelFmt: Copy + Default { #[inline] fn encode>(self, color: C) -> Self::Pixel { - color.into_pixel_fmt(self) + color.into_pixel(self) } } @@ -134,10 +140,8 @@ impl<'t, PF: PixelFmt> Builder<'t, PF> { } fn create_window(&self, sdl: &Sdl) -> Result { - let Self { - dims: (w, h), title, fs, hidpi, .. - } = *self; - let mut win = sdl.video()?.window(title, w, h); + let Self { dims, title, fs, hidpi, .. } = *self; + let mut win = sdl.video()?.window(title, dims.0, dims.1); if hidpi { win.allow_highdpi(); } @@ -190,7 +194,8 @@ impl, const N: usize> Window { ) -> ControlFlow<()>, Color4: IntoPixel, { - let dims @ (w, h) = self.canvas.window().drawable_size(); + let (w, h) = self.canvas.window().drawable_size(); + let dims = Dims(w, h); let tc = self.canvas.texture_creator(); let mut tex = tc.create_texture_streaming(PF::SDL_FMT, w, h)?; @@ -198,8 +203,11 @@ impl, const N: usize> Window { let mut zbuf = Buf2::new(dims); let mut ctx = self.ctx.clone(); + let mut fps = Text::new(font_6x10()); + fps.anchor = (2.0, 2.0).into(); + let start = Instant::now(); - let mut last = Instant::now(); + let mut last = start; 'main: loop { self.events.clear(); for e in self.ev_pump.poll_iter() { @@ -232,19 +240,37 @@ impl, const N: usize> Window { }; frame.clear(); - frame_fn(frame) + let cf = frame_fn(frame); + + fps.clear(); + _ = write!(fps, "{:>6.1}", frame.dt.as_secs_f32().recip()); + fps.render(&mut frame.buf); + + cf })?; self.present(&tex)?; - ctx.stats.borrow_mut().frames += 1.0; + + #[cfg(feature = "stats")] + { + ctx.stats.borrow_mut().frames += 1.0; + } if cf.is_break() { break; } } - let stats = ctx.stats.into_inner(); - println!("{stats}"); - Ok(stats) + + #[cfg(feature = "stats")] + { + let stats = ctx.stats.into_inner().finish(); + println!("{stats}"); + Ok(stats) + } + #[cfg(not(feature = "stats"))] + { + Ok(()) + } } } @@ -256,15 +282,24 @@ impl PixelFmt for Rgba8888 { type Pixel = [u8; 4]; const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGBA32; } -impl PixelFmt for Rgb565 { +impl PixelFmt for Rgba5551 { type Pixel = [u8; 2]; - const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGB565; + const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGBA5551; } impl PixelFmt for Rgba4444 { type Pixel = [u8; 2]; const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGBA4444; } +impl PixelFmt for Rgb565 { + type Pixel = [u8; 2]; + const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGB565; +} +impl PixelFmt for Rgb332 { + type Pixel = [u8; 1]; + const SDL_FMT: PixelFormatEnum = PixelFormatEnum::RGB332; +} + impl Default for Builder<'_, PF> { fn default() -> Self { Self { diff --git a/front/src/wasm.rs b/front/src/wasm.rs index b9f6555f..b20e4553 100644 --- a/front/src/wasm.rs +++ b/front/src/wasm.rs @@ -20,15 +20,14 @@ use web_sys::{ js_sys::{Uint8ClampedArray, Uint32Array}, }; -use crate::{Frame, dims::SVGA_800_600}; - use retrofire_core::{ math::color::rgba, - render::{Colorbuf, Context, Stats, target}, - util::buf::{AsMutSlice2, Buf2, MutSlice2}, - util::{Dims, pixfmt::Rgba8888}, + render::{Colorbuf, Context, target}, + util::{AsMutSlice2, Buf2, Dims, MutSlice2, dims, pixfmt::Rgba8888}, }; +use super::Frame; + #[wasm_bindgen] extern "C" { #[wasm_bindgen(js_namespace = console)] @@ -68,7 +67,7 @@ impl Builder { impl Default for Builder { fn default() -> Self { - Self { dims: SVGA_800_600 } + Self { dims: dims::SVGA_800_600 } } } @@ -140,15 +139,15 @@ impl Window { web_sys::window()?.document() } - fn create_canvas((w, h): Dims) -> Option { + fn create_canvas(dims: Dims) -> Option { let cvs: HtmlCanvasElement = Self::document()? .create_element("canvas") .ok()? .dyn_into() .ok()?; - cvs.set_width(w); - cvs.set_height(h); + cvs.set_width(dims.0); + cvs.set_height(dims.1); Some(cvs) } diff --git a/geom/src/io.rs b/geom/src/io.rs index 885f9e6d..6dda2446 100644 --- a/geom/src/io.rs +++ b/geom/src/io.rs @@ -355,11 +355,11 @@ fn parse_indices(param: &str) -> Result { let pos = next(indices).and_then(parse_index)?; // Texcoord and normal are optional let uv = if let Some(uv) = indices.next() { - if !uv.is_empty() { - Some(parse_index(uv)?) - } else { + if uv.is_empty() { // `1//2`: only position and normal None + } else { + Some(parse_index(uv)?) } } else { None @@ -513,7 +513,7 @@ v 0.0 -2.0 0.0 assert_eq!( m.faces() - .map(|tri| tri.0.map(|&Vertex { pos, attrib: n }| (pos, n))) + .map(|tri| tri.0.map(|v| (v.pos, v.attrib))) .collect::>(), [ [ @@ -551,7 +551,7 @@ v 0.0 -2.0 0.0 assert_eq!( m.faces() - .map(|tri| tri.0.map(|&Vertex { pos, attrib: uv }| (pos, uv))) + .map(|tri| tri.0.map(|v| (v.pos, v.attrib))) .collect::>(), [ [ diff --git a/geom/src/isect.rs b/geom/src/isect.rs index df32cea8..d6a2e633 100644 --- a/geom/src/isect.rs +++ b/geom/src/isect.rs @@ -1,10 +1,13 @@ -use core::fmt::{Debug, Formatter}; +use core::{ + fmt::{Debug, Formatter}, + iter::zip, +}; +use retrofire_core::{assert_approx_eq, mat}; use retrofire_core::{ - geom::{Edge, Line2, Plane3, Ray, Ray2, Ray3}, - mat, - math::{ApproxEq, Mat2, Point2, Point3, pt2, vec3}, - render::scene::BBox, + geom::{Edge, Line2, Mesh, Plane3, Pos, Ray, Ray2, Ray3, Tri}, + math::{ApproxEq, Mat2, Mat3, Point2, Point3, vec2, vec3}, + render::{Model, Obj, World, scene::BBox}, }; #[cfg(feature = "std")] @@ -48,19 +51,6 @@ impl LineIntersect { } } -// -// Trait impls -// - -impl Debug for LineIntersect { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - match self { - Self::Point(p) => write!(f, "Point({p:?})"), - Self::Coincident => f.write_str("Coincident"), - } - } -} - // // 3D Intersect impls // @@ -131,7 +121,88 @@ impl Intersect> for Ray3 { } } -impl Intersect> for Ray3 { +impl>> Intersect> for Ray3 { + type Result = RayIntersect3; + + /// Returns the intersection of `self` and a triangle, or `None` if they + /// do not intersect. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{tri, Ray, Ray3}; + /// use retrofire_core::math::{pt3, vec3}; + /// use retrofire_geom::Intersect; + /// + /// let t = tri(pt3(0.0, 0.0, 0.0), pt3(2.0, 0.0, 0.0), pt3(0.0, 2.0, 0.0)); + /// + /// let ray: Ray3 = Ray(pt3(1.0, 0.5, 4.0), vec3(0.0, 0.0, -1.0)); + /// assert_eq!(ray.intersect(&t), Some((4.0, pt3(1.0, 0.5, 0.0)))); + /// + /// let ray: Ray3 = Ray(pt3(0.0, 0.0, 4.0), vec3(0.0, -1.0, -1.0)); + /// assert_eq!(ray.intersect(&t), None); + /// ``` + fn intersect(&self, tri: &Tri

) -> Self::Result { + // tri ABC, ab = B-A, ac = C-A + // ray P = O + t * d + // + // triangle plane parametric: + // P = A + u * ab + v * ac + // + // solve linear equation for (t, u, v): + // O + t * d = a + u * ab + v * ac + // + // ( t ) + // <=> O - A = ( -d ab ac ) ( u ) + // ( v ) + // + // inside triangle iff 0 <= t && 0 <= u && 0 <= v && u + v <= 1 + + struct Plane; + + let &Ray(orig, dir) = self; + let &a = tri.0[0].pos(); + let [ab, ac] = tri.tangents(); + + let m = Mat3::::from_linear(-dir, ab, ac); + let [t, u, v] = m.solve(orig - a)?.0; + + // If inside the triangle + (t >= 0.0 && u >= 0.0 && v >= 0.0 && u + v <= 1.0) + .then(|| (t, orig + t * dir)) + } +} + +impl>> Intersect> for Ray2 { + type Result = RayIntersect2; + + /// Returns the intersection of `self` and a triangle, or `None` if they + /// do not intersect. + /// + /// # Examples + /// ``` + /// use retrofire_core::geom::{tri, Ray, Ray2}; + /// use retrofire_core::math::{pt2, vec2}; + /// use retrofire_geom::Intersect; + /// + /// let t = tri(pt2(0.0, 0.0), pt2(2.0, 0.0), pt2(0.0, 2.0)); + /// + /// let ray: Ray2 = Ray(pt2(-2.0, 1.0), vec2(1.0, 0.0)); + /// assert_eq!(ray.intersect(&t), Some((2.0, pt2(0.0, 1.0)))); + /// + /// let ray: Ray2 = Ray(pt2(-2.0, 1.0), vec2(1.0, 11.0)); + /// assert_eq!(ray.intersect(&t), None); + /// ``` + fn intersect(&self, tri: &Tri

) -> Self::Result { + let tri = Tri(tri.0.each_ref().map(|p| p.pos().clone())); + + tri.edges() + .iter() + .filter_map(|e| self.intersect(&Edge(*e.0, *e.1))) + .min_by(|(t, _), (u, _)| t.total_cmp(u)) + } +} + +impl Intersect> for Ray3 { type Result = RayIntersect3; // Only closest for now /// Returns the nearest intersection point of `self` and a box, @@ -169,9 +240,9 @@ impl Intersect> for Ray3 { return None; } - let r_d = vec3(1.0 / dir.x(), 1.0 / dir.y(), 1.0 / dir.z()); - let low = (low - orig) * r_d; - let upp = (upp - orig) * r_d; + let r_dir = dir.map(f32::recip); + let low = (low - orig) * r_dir; + let upp = (upp - orig) * r_dir; let near = low.zip_map(upp, f32::min); let far = low.zip_map(upp, f32::max); @@ -286,6 +357,46 @@ impl Intersect> for Ray3 { } } +pub type RayMeshIntersect3 = Option<(f32, Point3, usize)>; + +impl Intersect> for Ray3 { + type Result = RayMeshIntersect3; + + /// Returns the closest intersection of `self` and a mesh, or `None` if + /// they do not intersect. + fn intersect(&self, mesh: &Mesh) -> Self::Result { + zip(mesh.faces(), 0..) + .filter_map(|(face, i)| { + let (t, pt) = self.intersect(&face)?; + Some((t, pt, i)) + }) + .min_by(|(t, ..), (u, ..)| t.total_cmp(u)) + } +} + +impl Intersect> for Ray3 { + type Result = RayMeshIntersect3; + + /// Returns the closest intersection of `self` and an `Obj`, or `None` if + /// they do not intersect. + fn intersect(&self, obj: &Obj) -> Self::Result { + let inv_tf = obj.tf.inverse(); + + let ray: Ray3 = + Ray(inv_tf.apply(&self.0), inv_tf.apply(&self.1)); + + ray.intersect(&obj.bbox)?; + let (_, model_pt, face) = ray.intersect(&obj.geom)?; + + let world_pt = obj.tf.apply(&model_pt); + let t = (world_pt - self.0).len() / self.1.len(); + + assert_approx_eq!(self.0 + t * self.1, world_pt, eps = 1e-4); + + Some((t, world_pt, face)) + } +} + // // 2D intersection // @@ -302,16 +413,18 @@ impl Intersect for Line2 { /// /// # Examples /// ``` + /// use core::assert_matches; + /// /// use retrofire_core::{ - /// assert_approx_eq, geom::{Ray, Line2}, math::{pt2, vec2}, + /// geom::{Ray, Line2}, math::{pt2, vec2, ApproxEq}, /// }; /// use retrofire_geom::{Intersect, isect::LineIntersect::*}; /// /// let horiz: Line2 = Ray(pt2(0.0, 2.0), vec2(1.0, 0.0)).into(); /// let vert: Line2 = Ray(pt2(3.0, 0.0), vec2(0.0, 1.0)).into(); /// - /// let isect = horiz.intersect(&vert).and_then(|i| i.point()); - /// assert_approx_eq!(isect, Some(pt2(3.0, 2.0))); + /// let isect = horiz.intersect(&vert); + /// assert_matches!(isect, Some(Point(p)) if p.approx_eq(&pt2(3.0, 2.0))); /// /// let horiz2 = ::from(Ray(pt2(0.0, 3.0), vec2(1.0, 0.0))); /// assert_eq!(horiz.intersect(&horiz2), None); @@ -327,23 +440,9 @@ impl Intersect for Line2 { // Solve the system of equations for x and y: // ax + by + c = 0 // self // dx + ey + f = 0 // other - // - // Write in matrix form and solve: - // (a b) (x) = (-c) - // (d e) (y) (-f) - // - // -1 - // (x) = (a b) (-c) - // (y) (d e) (-f) - let abde: Mat2 = mat![ - a, b; - d, e; - ]; - match abde.checked_inverse() { - Some(inv) => { - let res = inv.apply(&pt2(-c, -f)); - Some(LineIntersect::Point(res)) - } + let abde: Mat2 = mat![a, b; d, e]; + match abde.solve(vec2(-c, -f)) { + Some(res) => Some(LineIntersect::Point(res.to_pt())), None if [a, b, c].approx_eq(&[d, e, f]) => { Some(LineIntersect::Coincident) } @@ -451,11 +550,9 @@ impl Intersect>> for Ray2 { return None; } let t1 = (edge.1 - pt).dot(&self.1); - if t0 <= t1 { - Some((t0 / self.1.len_sqr(), edge.0)) - } else { - Some((t1 / self.1.len_sqr(), edge.1)) - } + + let (t, pt) = if t0 <= t1 { (t0, edge.0) } else { (t1, edge.1) }; + Some((t / self.1.len_sqr(), pt)) } } @@ -497,6 +594,19 @@ impl Intersect for Edge> { } } +// +// Foreign trait impls +// + +impl Debug for LineIntersect { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + match self { + Self::Point(p) => Debug::fmt(p, f), + Self::Coincident => f.write_str("Coincident"), + } + } +} + #[cfg(test)] mod tests { use retrofire_core::math::{Linear, Vec3, pt3}; @@ -707,5 +817,102 @@ mod tests { assert_eq!(ray.intersect(&SPHERE), None); } } + + mod ray_mesh { + use retrofire_core::geom::Normal3; + + use super::*; + use crate::solids::{Build, Cube}; + + #[test] + fn ray_hits_cube_from_outside() { + let cube: Mesh = Cube { side_len: 1.0 }.build(); + + let along_z = Ray(pt3(0.2, -0.1, -3.0), vec3(0.0, 0.0, 1.0)); + assert_eq!( + along_z.intersect(&cube), + Some((2.5, pt3(0.2, -0.1, -0.5), 8)) + ); + + let along_y = Ray(pt3(0.0, 10.0, 0.0), vec3(0.0, -2.0, 0.0)); + assert_eq!( + along_y.intersect(&cube), + Some((4.75, pt3(0.0, 0.5, 0.0), 6)) + ); + + let diagonal = Ray(pt3(4.0, -4.0, 4.0), vec3(-1.0, 1.0, -1.0)); + assert_eq!( + diagonal.intersect(&cube), + Some((3.5, pt3(0.5, -0.5, 0.5), 2)) + ); + } + #[test] + fn ray_hits_cube_from_inside() { + let cube: Mesh = Cube { side_len: 1.0 }.build(); + + let from_origin = Ray(pt3(0.0, 0.0, 0.0), vec3(-0.5, 0.25, -1.0)); + assert_eq!( + from_origin.intersect(&cube), + Some((0.5, pt3(-0.25, 0.125, -0.5), 9)) + ); + + let along_z = Ray(pt3(0.2, 0.3, 0.25), vec3(0.0, 0.0, 2.0)); + assert_eq!( + along_z.intersect(&cube), + Some((0.125, pt3(0.2, 0.3, 0.5), 10)) + ); + } + #[test] + fn ray_misses_cube() { + let cube: Mesh = Cube { side_len: 1.0 }.build(); + + let passes_cube = Ray(pt3(1.0, 0.0, -3.0), vec3(0.0, 0.0, 1.0)); + assert_eq!(passes_cube.intersect(&cube), None); + + let opposite_dir = Ray(pt3(0.0, 0.0, 1.0), vec3(0.0, 0.0, 1.0)); + assert_eq!(opposite_dir.intersect(&cube), None); + } + + #[test] + #[ignore = "TODO"] + #[cfg(feature = "std")] + fn torus() { + let torus: Mesh = crate::solids::Torus { + major_radius: 1.0, + minor_radius: 0.3, + major_sectors: 16, + minor_sectors: 8, + } + .build(); + + let ray = Ray(pt3(0.0, 0.0, -3.0), vec3(0.0, 0.0, 1.0)); + let ip = ray.intersect(&torus); + + assert_eq!(ip, None); + } + } + + mod ray_obj { + use retrofire_core::{geom::Normal3, math::translate}; + + use super::*; + use crate::solids::{Build, Cube}; + + #[test] + #[ignore = "TODO"] + fn obj() { + let cube = Obj::::with_transform( + Cube { side_len: 1.0 }.build(), + translate((1.0, 2.0, 3.0)).to(), + ); + + let ray = Ray(pt3(1.0, 2.0, 1.0), vec3(0.0, 0.0, 2.0)); + assert_eq!(ray.intersect(&cube), None /* TODO */); + + let ray = Ray(pt3(0.0, 0.0, -3.0), vec3(0.0, 0.0, 1.0)); + assert_eq!(ray.intersect(&cube), None); + } + } + // TODO 2D tests from stash } diff --git a/geom/src/solids/lathe.rs b/geom/src/solids/lathe.rs index a681ae9b..55cc2038 100644 --- a/geom/src/solids/lathe.rs +++ b/geom/src/solids/lathe.rs @@ -8,8 +8,8 @@ use retrofire_core::geom::{ vertex, }; use retrofire_core::math::{ - Angle, Lerp, Parametric, Point3, Vary, Vec3, polar, pt2, pt3, rotate2, - turns, vec2, vec3, + Angle, Lerp, Parametric, Point3, Vary, Vec3, param, polar, pt2, pt3, + rotate2, turns, vec2, vec3, }; use retrofire_core::render::{TexCoord, uv}; @@ -176,9 +176,8 @@ fn create_verts( let start = rotate2(start); // Create vertices - for (v, Vertex { pos, attrib: n }) in 0.0 - .vary_to(1.0, verts_per_sec as u32) - .map(|t| (t, pts.eval(t))) + for (v, Vertex { pos, attrib: n }) in + param::iter(&(0.0..1.0), verts_per_sec).map(|t| (t, pts.eval(t))) { let mut pos_xz = start.apply(&pt2(pos.x(), 0.0)); let mut n_xz = start.apply(&vec2(n.x(), 0.0)); @@ -224,11 +223,14 @@ fn make_cap( // Local trait impls // +// TODO impl Build<()> + impl>> Build for Lathe

{ fn build(self) -> Mesh { self.build_with(&mut |p, n, _| vertex(p.to(), n)) } } +// TODO Shouldn't need parametric with normal if normal not used impl>> Build for Lathe

{ fn build(self) -> Mesh { self.build_with(&mut |p, _, tc| vertex(p.to(), tc)) diff --git a/geom/src/solids/platonic.rs b/geom/src/solids/platonic.rs index dda3279e..40e9aab2 100644 --- a/geom/src/solids/platonic.rs +++ b/geom/src/solids/platonic.rs @@ -1,7 +1,11 @@ //! The five Platonic solids: tetrahedron, cube, octahedron, dodecahedron, //! and icosahedron. -use core::{array::from_fn, f32::consts::SQRT_2, iter::zip}; +use core::{ + array::from_fn, + f32::consts::{GOLDEN_RATIO, SQRT_2}, + iter::zip, +}; use retrofire_core::{ geom::{Mesh, Normal3, Vertex3, vertex}, @@ -301,7 +305,7 @@ impl Build for Octahedron { } /// The golden ratio constant φ. -const PHI: f32 = 1.618034_f32; +const PHI: f32 = GOLDEN_RATIO; /// Reciprocal of φ. const R_PHI: f32 = 1.0 / PHI;