From 3e4b9affa09ed49e578af559ca1ecd50cdb28445 Mon Sep 17 00:00:00 2001 From: gered Date: Sat, 1 Apr 2023 18:48:54 -0400 Subject: [PATCH] redo triangle 2d drawing api to use enums. split indexed+rgba variants i'm not *entirely* sold on this particular enum-based api, but i think it could work. in the future i'd likely ALSO want something that could work with triangle data as an offset from a larger buffer, but we can solve that problem later when it comes up. mainly i just didn't want to have a bunch of different triangle_2d function variants exposed. same justification as with the blit enums, basically. --- ggdt/benches/triangles.rs | 54 +++++-- ggdt/src/graphics/bitmap/indexed/mod.rs | 1 + ggdt/src/graphics/bitmap/indexed/triangles.rs | 47 ++++++ ggdt/src/graphics/bitmap/rgb/mod.rs | 1 + ggdt/src/graphics/bitmap/rgb/triangles.rs | 70 +++++++++ ggdt/src/graphics/bitmap/triangles.rs | 142 +++++++----------- ggdt/src/graphics/prelude.rs | 16 +- 7 files changed, 225 insertions(+), 106 deletions(-) create mode 100644 ggdt/src/graphics/bitmap/indexed/triangles.rs create mode 100644 ggdt/src/graphics/bitmap/rgb/triangles.rs diff --git a/ggdt/benches/triangles.rs b/ggdt/benches/triangles.rs index ac06fce..55a508a 100644 --- a/ggdt/benches/triangles.rs +++ b/ggdt/benches/triangles.rs @@ -10,28 +10,52 @@ pub fn criterion_benchmark(c: &mut Criterion) { let (texture, _palette) = IndexedBitmap::load_gif_file(std::path::Path::new("./test-assets/gif/small.gif")).unwrap(); + let big_v1 = Vector2::new(47.0, 23.0); + let big_v2 = Vector2::new(60.0, 192.0); + let big_v3 = Vector2::new(280.0, 153.0); + + let small_v1 = Vector2::new(16.0, 16.0); + let small_v2 = Vector2::new(16.0, 31.0); + let small_v3 = Vector2::new(31.0, 31.0); + + let texcoord_0_0 = Vector2::new(0.0, 0.0); + let texcoord_1_0 = Vector2::new(1.0, 0.0); + let texcoord_0_1 = Vector2::new(0.0, 1.0); + let texcoord_1_1 = Vector2::new(1.0, 1.0); + c.bench_function("indexedbitmap_triangle_2d_solid_color", |b| { + let triangle = IndexedTriangle2d::SolidSingleColor { position: [big_v1, big_v2, big_v3], color: 5 }; b.iter(|| { - dest.triangle_2d_solid_color( - black_box(Vector2::new(47.0, 23.0)), - black_box(Vector2::new(60.0, 192.0)), - black_box(Vector2::new(280.0, 153.0)), - black_box(5), - ); + dest.triangle_2d(black_box(&triangle)); + }) + }); + + c.bench_function("indexedbitmap_triangle_2d_solid_color_small", |b| { + let triangle = IndexedTriangle2d::SolidSingleColor { position: [small_v1, small_v2, small_v3], color: 5 }; + b.iter(|| { + dest.triangle_2d(black_box(&triangle)); }) }); c.bench_function("indexedbitmap_triangle_2d_textured", |b| { + let triangle = IndexedTriangle2d::SolidTextured { + position: [big_v1, big_v2, big_v3], + texcoord: [texcoord_0_0, texcoord_1_0, texcoord_1_1], + bitmap: &texture, + }; b.iter(|| { - dest.triangle_2d_textured( - black_box(Vector2::new(47.0, 23.0)), - black_box(Vector2::new(0.0, 0.0)), - black_box(Vector2::new(60.0, 192.0)), - black_box(Vector2::new(1.0, 0.0)), - black_box(Vector2::new(280.0, 153.0)), - black_box(Vector2::new(1.0, 1.0)), - black_box(&texture), - ); + dest.triangle_2d(black_box(&triangle)); + }) + }); + + c.bench_function("indexedbitmap_triangle_2d_textured_small", |b| { + let triangle = IndexedTriangle2d::SolidTextured { + position: [small_v1, small_v2, small_v3], + texcoord: [texcoord_0_0, texcoord_1_0, texcoord_1_1], + bitmap: &texture, + }; + b.iter(|| { + dest.triangle_2d(black_box(&triangle)); }) }); } diff --git a/ggdt/src/graphics/bitmap/indexed/mod.rs b/ggdt/src/graphics/bitmap/indexed/mod.rs index 7cc82eb..2759e14 100644 --- a/ggdt/src/graphics/bitmap/indexed/mod.rs +++ b/ggdt/src/graphics/bitmap/indexed/mod.rs @@ -6,6 +6,7 @@ use crate::graphics::palette::Palette; pub mod blit; pub mod primitives; +pub mod triangles; pub type IndexedBitmap = Bitmap; diff --git a/ggdt/src/graphics/bitmap/indexed/triangles.rs b/ggdt/src/graphics/bitmap/indexed/triangles.rs new file mode 100644 index 0000000..4c2a8ee --- /dev/null +++ b/ggdt/src/graphics/bitmap/indexed/triangles.rs @@ -0,0 +1,47 @@ +use crate::graphics::bitmap::indexed::IndexedBitmap; +use crate::graphics::bitmap::triangles::{edge_function, per_pixel_triangle_2d}; +use crate::math::vector2::Vector2; + +#[derive(Debug, Clone, PartialEq)] +pub enum IndexedTriangle2d<'a> { + SolidSingleColor { + position: [Vector2; 3], // + color: u8, + }, + SolidTextured { + position: [Vector2; 3], // + texcoord: [Vector2; 3], + bitmap: &'a IndexedBitmap, + }, +} + +impl IndexedBitmap { + pub fn triangle_2d(&mut self, triangle: &IndexedTriangle2d) { + use IndexedTriangle2d::*; + match triangle { + SolidSingleColor { position, color } => { + per_pixel_triangle_2d( + self, // + position[0], + position[1], + position[2], + |dest_pixels, _w0, _w1, _w2| *dest_pixels = *color, + ) + } + SolidTextured { position, texcoord, bitmap } => { + let inverse_area = 1.0 / edge_function(position[0], position[1], position[2]); + per_pixel_triangle_2d( + self, // + position[0], + position[1], + position[2], + |dest_pixels, w0, w1, w2| { + let u = (w0 * texcoord[0].x + w1 * texcoord[1].x + w2 * texcoord[2].x) * inverse_area; + let v = (w0 * texcoord[0].y + w1 * texcoord[1].y + w2 * texcoord[2].y) * inverse_area; + *dest_pixels = bitmap.sample_at(u, v); + }, + ) + } + } + } +} diff --git a/ggdt/src/graphics/bitmap/rgb/mod.rs b/ggdt/src/graphics/bitmap/rgb/mod.rs index e980cf4..90ce758 100644 --- a/ggdt/src/graphics/bitmap/rgb/mod.rs +++ b/ggdt/src/graphics/bitmap/rgb/mod.rs @@ -7,6 +7,7 @@ use crate::graphics::palette::Palette; pub mod blit; pub mod primitives; +pub mod triangles; pub type RgbaBitmap = Bitmap; diff --git a/ggdt/src/graphics/bitmap/rgb/triangles.rs b/ggdt/src/graphics/bitmap/rgb/triangles.rs new file mode 100644 index 0000000..8ebcb53 --- /dev/null +++ b/ggdt/src/graphics/bitmap/rgb/triangles.rs @@ -0,0 +1,70 @@ +use crate::graphics::bitmap::rgb::RgbaBitmap; +use crate::graphics::bitmap::triangles::{edge_function, per_pixel_triangle_2d}; +use crate::math::vector2::Vector2; +use crate::prelude::{from_rgb32, from_rgb32_normalized, to_rgb32_normalized}; + +#[derive(Debug, Clone, PartialEq)] +pub enum RgbaTriangle2d<'a> { + SolidSingleColor { + position: [Vector2; 3], // + color: u32, + }, + SolidMultiColor { + position: [Vector2; 3], // + color: [u32; 3], + }, + SolidTextured { + position: [Vector2; 3], // + texcoord: [Vector2; 3], + bitmap: &'a RgbaBitmap, + }, +} + +impl RgbaBitmap { + pub fn triangle_2d(&mut self, triangle: &RgbaTriangle2d) { + use RgbaTriangle2d::*; + match triangle { + SolidSingleColor { position, color } => { + per_pixel_triangle_2d( + self, // + position[0], + position[1], + position[2], + |dest_pixels, _w0, _w1, _w2| *dest_pixels = *color, + ) + } + SolidMultiColor { position, color } => { + let inverse_area = 1.0 / edge_function(position[0], position[1], position[2]); + let (r1, g1, b1) = from_rgb32_normalized(color[0]); + let (r2, g2, b2) = from_rgb32_normalized(color[1]); + let (r3, g3, b3) = from_rgb32_normalized(color[2]); + per_pixel_triangle_2d( + self, // + position[0], + position[1], + position[2], + |dest_pixels, w0, w1, w2| { + let r = (w0 * r1 + w1 * r2 + w2 * r3) * inverse_area; + let g = (w0 * g1 + w1 * g2 + w2 * g3) * inverse_area; + let b = (w0 * b1 + w1 * b2 + w2 * b3) * inverse_area; + *dest_pixels = to_rgb32_normalized(r, g, b) + }, + ) + } + SolidTextured { position, texcoord, bitmap } => { + let inverse_area = 1.0 / edge_function(position[0], position[1], position[2]); + per_pixel_triangle_2d( + self, // + position[0], + position[1], + position[2], + |dest_pixels, w0, w1, w2| { + let u = (w0 * texcoord[0].x + w1 * texcoord[1].x + w2 * texcoord[2].x) * inverse_area; + let v = (w0 * texcoord[0].y + w1 * texcoord[1].y + w2 * texcoord[2].y) * inverse_area; + *dest_pixels = bitmap.sample_at(u, v); + }, + ) + } + } + } +} diff --git a/ggdt/src/graphics/bitmap/triangles.rs b/ggdt/src/graphics/bitmap/triangles.rs index 8e711e3..6fdafe9 100644 --- a/ggdt/src/graphics/bitmap/triangles.rs +++ b/ggdt/src/graphics/bitmap/triangles.rs @@ -3,108 +3,72 @@ use crate::graphics::Pixel; use crate::math::vector2::Vector2; #[inline] -fn cross(a: Vector2, b: Vector2, c: Vector2) -> f32 { +pub fn edge_function(a: Vector2, b: Vector2, c: Vector2) -> f32 { (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x) } -impl Bitmap { - pub fn triangle_2d_solid_color(&mut self, a: Vector2, b: Vector2, c: Vector2, color: PixelType) { - self.triangle_2d_custom( - a, // - b, - c, - |dest_pixels, _w0, _w1, _w2| { - *dest_pixels = color; - }, - ) - } +#[inline] +pub fn per_pixel_triangle_2d( + dest: &mut Bitmap, + a: Vector2, + b: Vector2, + c: Vector2, + pixel_fn: impl Fn(&mut PixelType, f32, f32, f32), +) { + // based off the triangle rasterization algorithm presented in these article series' (as well as others) + // https://fgiesen.wordpress.com/2013/02/17/optimizing-sw-occlusion-culling-index/ + // https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/rasterization-stage.html + // https://kitsunegames.com/post/development/2016/07/28/software-3d-rendering-in-javascript-pt2/ - pub fn triangle_2d_textured( - &mut self, - a: Vector2, - a_tex: Vector2, - b: Vector2, - b_tex: Vector2, - c: Vector2, - c_tex: Vector2, - texture: &Bitmap, - ) { - let inverse_area = 1.0 / cross(a, b, c); // inverting to avoid division - self.triangle_2d_custom( - a, // - b, - c, - |dest_pixels, w0, w1, w2| { - let u = (w0 * a_tex.x + w1 * b_tex.x + w2 * c_tex.x) * inverse_area; - let v = (w0 * a_tex.y + w1 * b_tex.y + w2 * c_tex.y) * inverse_area; - *dest_pixels = texture.sample_at(u, v); - }, - ) - } + // TODO: implement fill rules, probably using top-left ordering as most 3d APIs do i guess - #[inline] - pub fn triangle_2d_custom( - &mut self, - a: Vector2, - b: Vector2, - c: Vector2, - pixel_fn: impl Fn(&mut PixelType, f32, f32, f32), - ) { - // based off the triangle rasterization algorithm presented in these article series' (as well as others) - // https://fgiesen.wordpress.com/2013/02/17/optimizing-sw-occlusion-culling-index/ - // https://www.scratchapixel.com/lessons/3d-basic-rendering/rasterization-practical-implementation/rasterization-stage.html - // https://kitsunegames.com/post/development/2016/07/28/software-3d-rendering-in-javascript-pt2/ + let min_x = a.x.min(b.x).min(c.x).floor() as i32; + let min_y = a.y.min(b.y).min(c.y).floor() as i32; + let max_x = a.x.max(b.x).max(c.x).ceil() as i32; + let max_y = a.y.max(b.y).max(c.y).ceil() as i32; - // TODO: implement fill rules, probably using top-left ordering as most 3d APIs do i guess + let min_x = std::cmp::max(dest.clip_region().x, min_x); + let min_y = std::cmp::max(dest.clip_region().y, min_y); + let max_x = std::cmp::min(dest.clip_region().right(), max_x); + let max_y = std::cmp::min(dest.clip_region().bottom(), max_y); - let min_x = a.x.min(b.x).min(c.x).floor() as i32; - let min_y = a.y.min(b.y).min(c.y).floor() as i32; - let max_x = a.x.max(b.x).max(c.x).ceil() as i32; - let max_y = a.y.max(b.y).max(c.y).ceil() as i32; + let a01 = a.y - b.y; + let b01 = b.x - a.x; + let a12 = b.y - c.y; + let b12 = c.x - b.x; + let a20 = c.y - a.y; + let b20 = a.x - c.x; - let min_x = std::cmp::max(self.clip_region().x, min_x); - let min_y = std::cmp::max(self.clip_region().y, min_y); - let max_x = std::cmp::min(self.clip_region().right(), max_x); - let max_y = std::cmp::min(self.clip_region().bottom(), max_y); + let p = Vector2::new(min_x as f32, min_y as f32); + let mut w0_row = edge_function(b, c, p); + let mut w1_row = edge_function(c, a, p); + let mut w2_row = edge_function(a, b, p); - let a01 = a.y - b.y; - let b01 = b.x - a.x; - let a12 = b.y - c.y; - let b12 = c.x - b.x; - let a20 = c.y - a.y; - let b20 = a.x - c.x; + let draw_width = (max_x - min_x + 1) as usize; + let next_row_inc = dest.width() as usize; + let mut pixels = unsafe { dest.pixels_at_mut_ptr_unchecked(min_x, min_y) }; - let p = Vector2::new(min_x as f32, min_y as f32); - let mut w0_row = cross(b, c, p); - let mut w1_row = cross(c, a, p); - let mut w2_row = cross(a, b, p); + for _ in min_y..=max_y { + let mut w0 = w0_row; + let mut w1 = w1_row; + let mut w2 = w2_row; - let draw_width = (max_x - min_x + 1) as usize; - let next_row_inc = self.width() as usize; - let mut pixels = unsafe { self.pixels_at_mut_ptr_unchecked(min_x, min_y) }; - - for _ in min_y..=max_y { - let mut w0 = w0_row; - let mut w1 = w1_row; - let mut w2 = w2_row; - - let row_pixels = unsafe { std::slice::from_raw_parts_mut(pixels, draw_width) }; - for pixel in row_pixels.iter_mut() { - // note that for a counter-clockwise vertex winding order with the direction of Y+ going down instead - // of up, we need to test for *negative* area when checking if we're inside the triangle - if w0 < 0.0 && w1 < 0.0 && w2 < 0.0 { - pixel_fn(pixel, w0, w1, w2); - } - - w0 += a12; - w1 += a20; - w2 += a01; + let row_pixels = unsafe { std::slice::from_raw_parts_mut(pixels, draw_width) }; + for pixel in row_pixels.iter_mut() { + // note that for a counter-clockwise vertex winding order with the direction of Y+ going down instead + // of up, we need to test for *negative* area when checking if we're inside the triangle + if w0 < 0.0 && w1 < 0.0 && w2 < 0.0 { + pixel_fn(pixel, w0, w1, w2); } - w0_row += b12; - w1_row += b20; - w2_row += b01; - pixels = unsafe { pixels.add(next_row_inc) }; + w0 += a12; + w1 += a20; + w2 += a01; } + + w0_row += b12; + w1_row += b20; + w2_row += b01; + pixels = unsafe { pixels.add(next_row_inc) }; } } diff --git a/ggdt/src/graphics/prelude.rs b/ggdt/src/graphics/prelude.rs index d9e6510..062b816 100644 --- a/ggdt/src/graphics/prelude.rs +++ b/ggdt/src/graphics/prelude.rs @@ -5,11 +5,23 @@ pub use crate::graphics::{ general::*, gif::*, iff::*, - indexed::{blit::*, primitives::*, *}, + indexed::{ + // + blit::*, + primitives::*, + triangles::*, + *, + }, pcx::*, png::*, primitives::*, - rgb::{blit::*, primitives::*, *}, + rgb::{ + // + blit::*, + primitives::*, + triangles::*, + *, + }, triangles::*, *, },