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.
This commit is contained in:
Gered 2023-04-01 18:48:54 -04:00
parent 7a5ea75e1f
commit 3e4b9affa0
7 changed files with 225 additions and 106 deletions

View file

@ -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));
})
});
}

View file

@ -6,6 +6,7 @@ use crate::graphics::palette::Palette;
pub mod blit;
pub mod primitives;
pub mod triangles;
pub type IndexedBitmap = Bitmap<u8>;

View file

@ -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);
},
)
}
}
}
}

View file

@ -7,6 +7,7 @@ use crate::graphics::palette::Palette;
pub mod blit;
pub mod primitives;
pub mod triangles;
pub type RgbaBitmap = Bitmap<u32>;

View file

@ -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);
},
)
}
}
}
}

View file

@ -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<PixelType: Pixel> Bitmap<PixelType> {
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<PixelType: Pixel>(
dest: &mut Bitmap<PixelType>,
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<PixelType>,
) {
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) };
}
}

View file

@ -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::*,
*,
},