Files
inkr/src/rasterizer.rs

323 lines
9.3 KiB
Rust

use core::f32;
use egui::{Color32, ColorImage, Pos2, Rect, Vec2, emath::TSTransform, epaint::Vertex};
use std::ops::Range;
pub trait BlendFn {
fn blend(a: Color32, b: Color32) -> Color32;
}
pub mod blend {
pub struct Normal;
pub struct Add;
pub struct Multiply;
}
/// Rasterize some triangles onto a new image,
///
/// Triangle positions must be in image-local point-coords.
/// `width` and `height` are in pixel coords.
pub fn rasterize<'a, Blend: BlendFn>(
width: usize,
height: usize,
point_to_pixel: TSTransform,
triangles: impl Iterator<Item = [&'a Vertex; 3]>,
) -> ColorImage {
let mut image = ColorImage::new([width, height], Color32::TRANSPARENT);
rasterize_onto::<Blend>(&mut image, point_to_pixel, triangles);
image
}
/// Rasterize some triangles onto an image,
///
/// Triangle positions must be in image-local point-coords.
pub fn rasterize_onto<'a, Blend: BlendFn>(
image: &mut ColorImage,
point_to_pixel: TSTransform,
triangles: impl Iterator<Item = [&'a Vertex; 3]>,
) {
let width = image.width();
let height = image.height();
let mut set_pixel = |x: usize, y: usize, color| {
let pixel = &mut image.pixels[y * width + x];
*pixel = Blend::blend(*pixel, color);
};
let image_box = PxBoundingBox {
x_from: 0,
y_from: 0,
x_to: width,
y_to: height,
};
let pixel_to_point = point_to_pixel.inverse();
for triangle in triangles {
let [a, b, c] = triangle;
if triangle_area(a.pos, b.pos, c.pos) == 0.0 {
continue;
}
// Check all pixels within the triangle's bounding box.
let bounding_box =
triangle_bounding_box(&triangle, point_to_pixel).intersection(&image_box);
// TODO: consider subdividing the triangle if it's very large.
let pixels = pixels_in_box(bounding_box);
for [x, y] in pixels {
// Calculate point-coordinate of the pixel
let pt_pos = pixel_to_point * Pos2::new(x as f32, y as f32);
let point_in_triangle = point_in_triangle(pt_pos, triangle);
// If the pixel is within the triangle, fill it in.
if point_in_triangle.inside {
let [c0, c1, c2] = [0, 1, 2].map(|i| {
triangle[i]
.color
.linear_multiply(point_in_triangle.weights[i])
});
let color = c0 + c1 + c2;
set_pixel(x, y, color);
}
}
}
}
/// Rasterize a single triangles onto an image,
///
/// Triangle positions must be in image-local point-coords.
pub fn rasterize_triangle_onto<'a, Blend: BlendFn>(
image: &mut ColorImage,
point_to_pixel: TSTransform,
triangle: [&'a Vertex; 3],
) {
rasterize_onto::<Blend>(image, point_to_pixel, [triangle].into_iter());
}
/// A bounding box, measured in pixels.
#[derive(Debug, PartialEq, Eq)]
pub struct PxBoundingBox {
pub x_from: usize,
pub y_from: usize,
pub x_to: usize,
pub y_to: usize,
}
impl PxBoundingBox {
pub fn intersection(&self, other: &PxBoundingBox) -> PxBoundingBox {
PxBoundingBox {
x_from: self.x_from.max(other.x_from),
y_from: self.y_from.max(other.y_from),
x_to: self.x_to.min(other.x_to),
y_to: self.y_to.min(other.y_to),
}
}
pub fn union(&self, other: &PxBoundingBox) -> PxBoundingBox {
PxBoundingBox {
x_from: self.x_from.min(other.x_from),
y_from: self.y_from.min(other.y_from),
x_to: self.x_to.max(other.x_to),
y_to: self.y_to.max(other.y_to),
}
}
pub fn x_range(&self) -> Range<usize> {
self.x_from..self.x_to
}
pub fn y_range(&self) -> Range<usize> {
self.y_from..self.y_to
}
/// Test whether two boxes do NOT overlap
pub fn overlaps_with(&self, other: &PxBoundingBox) -> bool {
!self.is_disjoint_from(other)
}
pub fn is_disjoint_from(&self, other: &PxBoundingBox) -> bool {
false
|| self.x_from > other.x_to
|| self.y_from > other.y_to
|| other.x_from > self.x_to
|| other.y_from > self.y_to
}
}
pub fn triangle_bounding_box(
triangle: &[&Vertex; 3],
point_to_pixel: TSTransform,
) -> PxBoundingBox {
// calculate bounding box in point coords
let mut rect = Rect::NOTHING;
for vertex in triangle {
rect.min = rect.min.min(vertex.pos);
rect.max = rect.max.max(vertex.pos);
}
// convert bounding box to pixel coords
let rect = point_to_pixel.mul_rect(rect);
PxBoundingBox {
x_from: rect.min.x.floor() as usize,
y_from: rect.min.y.floor() as usize,
x_to: rect.max.x.ceil() as usize,
y_to: rect.max.y.ceil() as usize,
}
}
/// Calculate the perpendicular vector (90 degrees from the given vector)
fn perpendicular(v: Vec2) -> Vec2 {
Vec2::new(v.y, -v.x)
}
#[derive(Clone, Debug)]
struct PointInTriangle {
/// Is the point inside the triangle?
inside: bool,
/// Normalized weights describing the vicinity between the point and the three verticies of
/// thre triangle.
weights: [f32; 3],
}
/// Calculate whether a point is within a triangle, and the relative vicinities between the point
/// and each triangle vertex. The triangle must have a non-zero area.
fn point_in_triangle(point: Pos2, triangle: [&Vertex; 3]) -> PointInTriangle {
let [a, b, c] = triangle;
let sides = [[b, c], [c, a], [a, b]];
// For each side of the triangle, imagine a new triangle consisting of the side and `point`.
// Calculate the areas of those triangles.
let areas = sides.map(|[start, end]| signed_triangle_area(start.pos, end.pos, point));
// Use the areas to determine the side of the line at which the point exists.
// If the area is positive, the point is on the right side of the triangle line.
let [side_ab, side_bc, side_ca] = areas.map(|area| area >= 0.0);
// Total area of the traingle.
let triangle_area: f32 = areas.iter().sum();
// egui does not wind the triangles in a consistent order, otherwise we might check if the
// point is on a *specific* side of each line. As it is, we just check if the point is on the
// same side of each line.
let inside = side_ab == side_bc && side_bc == side_ca;
// Normalize the weights.
let weights = areas.map(|area| area / triangle_area);
if cfg!(debug_assertions) && weights.into_iter().any(f32::is_nan) {
panic!("weights must not be NaN! {weights:?} {triangle_area:?} {areas:?} {sides:?}");
}
PointInTriangle { inside, weights }
}
/// Calculate the area of a triangle.
fn triangle_area(a: Pos2, b: Pos2, c: Pos2) -> f32 {
signed_triangle_area(a, b, c).abs()
}
/// Calculate the area of a triangle.
///
/// The area will be positive if the triangle is wound clockwise, and negative otherwise.
fn signed_triangle_area(a: Pos2, b: Pos2, c: Pos2) -> f32 {
// Vector of an arbitrary "base" side of the triangle.
let base = c - a;
let base_perp = perpendicular(base);
let diagonal = c - b;
base_perp.dot(diagonal) / 2.0
}
/// Iterate over every pixel coordinate in a box.
#[inline(always)]
fn pixels_in_box(
PxBoundingBox {
x_from,
y_from,
x_to,
y_to,
}: PxBoundingBox,
) -> impl ExactSizeIterator<Item = [usize; 2]> {
struct IterWithLen<I>(I, usize);
impl<I: Iterator> ExactSizeIterator for IterWithLen<I> {}
impl<I: Iterator> Iterator for IterWithLen<I> {
type Item = I::Item;
fn size_hint(&self) -> (usize, Option<usize>) {
(self.1, Some(self.1))
}
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
let len = (x_from..x_to).len() * (y_from..y_to).len();
let iter = (x_from..x_to).flat_map(move |x| (y_from..y_to).map(move |y| [x, y]));
debug_assert_eq!(len, iter.clone().count());
IterWithLen(iter, len)
}
#[cfg(test)]
mod test {
use egui::{Color32, Pos2, Vec2, emath::TSTransform, epaint::Vertex};
use super::triangle_bounding_box;
#[test]
fn px_bounding_box() {
let triangle = [
Vertex {
pos: Pos2::new(56.3, 18.9),
uv: Default::default(),
color: Color32::WHITE,
},
Vertex {
pos: Pos2::new(56.4, 19.6),
uv: Default::default(),
color: Color32::WHITE,
},
Vertex {
pos: Pos2::new(55.8, 20.5),
uv: Default::default(),
color: Color32::WHITE,
},
];
let pixels_per_point = 2.0;
let point_to_pixel = TSTransform {
scaling: pixels_per_point,
translation: Vec2::new(-55.8, -18.9) * pixels_per_point,
};
let bounding_box = triangle_bounding_box(&triangle.each_ref(), point_to_pixel);
insta::assert_debug_snapshot!((triangle, point_to_pixel, bounding_box));
}
}
impl BlendFn for blend::Normal {
fn blend(a: Color32, b: Color32) -> Color32 {
a.blend(b)
}
}
impl BlendFn for blend::Add {
fn blend(a: Color32, b: Color32) -> Color32 {
a + b
}
}
impl BlendFn for blend::Multiply {
fn blend(a: Color32, b: Color32) -> Color32 {
a * b
}
}