use crate::core::{Rect, Size};
use crate::render::math3d::Mat4;
use crate::render::mesh3d::{project_mesh, Camera, Mesh3D, ProjectedTriangle, ProjectedVertex};
use crate::render::pipeline::pixel_ops::{pixel_visible, write_pixel_with_mode};
use crate::render::pipeline::u32_to_i32_saturating;
use crate::render::{BlendMode, SoftwareSurface};
impl SoftwareSurface {
pub fn draw_mesh_3d(&mut self, mesh: &Mesh3D, model: &Mat4, camera: &Camera, viewport: Rect) {
let size = self.buffer.size();
if size.width == 0
|| size.height == 0
|| viewport.width == 0
|| viewport.height == 0
|| mesh.triangle_count() == 0
{
return;
}
let triangles = project_mesh(mesh, model, camera, viewport, size);
if triangles.is_empty() {
return;
}
self.ensure_depth_buffer(size);
let clip = self.current_clip();
let blend = self.blend_mode;
let width = size.width;
let frame = self.buffer.back_mut();
let depth = &mut self.depth_buffer;
for triangle in &triangles {
rasterize_triangle(triangle, frame, depth, width, size.height, viewport, clip, blend);
}
}
fn ensure_depth_buffer(&mut self, size: Size) {
let needed = (size.width as usize).saturating_mul(size.height as usize);
if self.depth_dirty || self.depth_buffer.len() != needed {
self.depth_buffer.clear();
self.depth_buffer.resize(needed, 1.0);
self.depth_dirty = false;
}
}
}
#[allow(clippy::too_many_arguments)]
fn rasterize_triangle(
triangle: &ProjectedTriangle,
frame: &mut [u8],
depth_buffer: &mut [f32],
width: u32,
height: u32,
viewport: Rect,
clip: Option<(i32, i32, u32, u32)>,
blend: BlendMode,
) {
let [a, b, c] = triangle.vertices;
let area = edge(a, b, c.x, c.y);
if !area.is_finite() || area == 0.0 {
return;
}
let view_left = viewport.x;
let view_top = viewport.y;
let view_right = viewport.x.saturating_add(u32_to_i32_saturating(viewport.width));
let view_bottom = viewport.y.saturating_add(u32_to_i32_saturating(viewport.height));
let min_x = (a.x.min(b.x).min(c.x).floor() as i32)
.max(0)
.max(view_left)
.max(clip.map(|(x, ..)| x).unwrap_or(i32::MIN));
let min_y = (a.y.min(b.y).min(c.y).floor() as i32)
.max(0)
.max(view_top)
.max(clip.map(|(_, y, ..)| y).unwrap_or(i32::MIN));
let max_x = (a.x.max(b.x).max(c.x).ceil() as i32)
.min(width as i32 - 1)
.min(view_right.saturating_sub(1))
.min(
clip.map(|(x, _, w, _)| x.saturating_add(u32_to_i32_saturating(w)))
.unwrap_or(i32::MAX)
.saturating_sub(1),
);
let max_y = (a.y.max(b.y).max(c.y).ceil() as i32)
.min(height as i32 - 1)
.min(view_bottom.saturating_sub(1))
.min(
clip.map(|(_, y, _, h)| y.saturating_add(u32_to_i32_saturating(h)))
.unwrap_or(i32::MAX)
.saturating_sub(1),
);
if max_x < min_x || max_y < min_y {
return;
}
for y in min_y..=max_y {
let sample_y = y as f32 + 0.5;
for x in min_x..=max_x {
if !pixel_visible(clip, x, y) {
continue;
}
let sample_x = x as f32 + 0.5;
let weight_a = edge(b, c, sample_x, sample_y) / area;
let weight_b = edge(c, a, sample_x, sample_y) / area;
let weight_c = edge(a, b, sample_x, sample_y) / area;
if weight_a < 0.0 || weight_b < 0.0 || weight_c < 0.0 {
continue;
}
let depth = weight_a * a.depth + weight_b * b.depth + weight_c * c.depth;
if !(0.0..=1.0).contains(&depth) {
continue;
}
let index = y as usize * width as usize + x as usize;
if depth < depth_buffer[index] {
depth_buffer[index] = depth;
write_pixel_with_mode(blend, frame, width, x as u32, y as u32, triangle.color, 1.0);
}
}
}
}
fn edge(a: ProjectedVertex, b: ProjectedVertex, px: f32, py: f32) -> f32 {
(b.x - a.x) * (py - a.y) - (b.y - a.y) * (px - a.x)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::{Color, Size};
use crate::render::mesh3d::{Vec3, Vertex3D};
use crate::render::{PaintBackend, RenderCommand, SoftwarePaintBackend};
const SIZE: u32 = 100;
fn triangle(z: f32, color: Color) -> [Vertex3D; 3] {
[
Vertex3D::new(Vec3::new(-1.0, -1.0, z), color),
Vertex3D::new(Vec3::new(1.0, -1.0, z), color),
Vertex3D::new(Vec3::new(0.0, 1.0, z), color),
]
}
fn two_triangle_mesh(near: &[Vertex3D; 3], far: &[Vertex3D; 3], near_first: bool) -> Mesh3D {
let mut vertices = crate::compat::Vec::with_capacity(6);
let mut indices = crate::compat::Vec::with_capacity(6);
if near_first {
vertices.extend_from_slice(near);
vertices.extend_from_slice(far);
indices.extend_from_slice(&[0, 1, 2, 3, 4, 5]);
} else {
vertices.extend_from_slice(far);
vertices.extend_from_slice(near);
indices.extend_from_slice(&[3, 4, 5, 0, 1, 2]);
}
Mesh3D::new(vertices, indices).expect("the fixture indices are valid")
}
fn camera() -> Camera {
Camera::new(Vec3::new(0.0, 0.0, 5.0), Vec3::ZERO, Vec3::new(0.0, 1.0, 0.0))
.with_field_of_view(core::f32::consts::FRAC_PI_2)
.with_clip(0.1, 100.0)
}
fn viewport() -> Rect {
Rect::new(0, 0, SIZE, SIZE)
}
fn pixel(frame: &[u8], x: u32, y: u32) -> (u8, u8, u8) {
let index = ((y * SIZE + x) * 4) as usize;
(frame[index], frame[index + 1], frame[index + 2])
}
#[test]
fn a_nearer_triangle_occludes_a_farther_one_in_either_triangle_order() {
for near_first in [true, false] {
let mesh = two_triangle_mesh(
&triangle(1.0, Color::RED),
&triangle(-1.0, Color::BLUE),
near_first,
);
let mut surface = SoftwareSurface::new(Size::new(SIZE, SIZE), 1.0);
surface.begin_frame(Color::WHITE);
surface.draw_mesh_3d(&mesh, &Mat4::IDENTITY, &camera(), viewport());
surface.end_frame();
assert_eq!(
pixel(surface.frame_rgba(), SIZE / 2, SIZE / 2),
(255, 0, 0),
"the near triangle must win with near_first = {near_first}"
);
}
}
#[test]
fn a_nearer_mesh_occludes_a_farther_one_in_either_command_order() {
let near =
Mesh3D::new(triangle(1.0, Color::RED).to_vec(), vec![0, 1, 2]).expect("valid triangle");
let far = Mesh3D::new(triangle(-1.0, Color::BLUE).to_vec(), vec![0, 1, 2])
.expect("valid triangle");
for near_first in [true, false] {
let mut surface = SoftwareSurface::new(Size::new(SIZE, SIZE), 1.0);
surface.begin_frame(Color::WHITE);
let (first, second) = if near_first { (&near, &far) } else { (&far, &near) };
surface.draw_mesh_3d(first, &Mat4::IDENTITY, &camera(), viewport());
surface.draw_mesh_3d(second, &Mat4::IDENTITY, &camera(), viewport());
surface.end_frame();
assert_eq!(
pixel(surface.frame_rgba(), SIZE / 2, SIZE / 2),
(255, 0, 0),
"depth survives between mesh commands with near_first = {near_first}"
);
}
}
#[test]
fn a_far_triangle_alone_is_visible() {
let mesh = Mesh3D::new(triangle(-1.0, Color::BLUE).to_vec(), vec![0, 1, 2]).expect("valid");
let mut surface = SoftwareSurface::new(Size::new(SIZE, SIZE), 1.0);
surface.begin_frame(Color::WHITE);
surface.draw_mesh_3d(&mesh, &Mat4::IDENTITY, &camera(), viewport());
surface.end_frame();
assert_eq!(pixel(surface.frame_rgba(), SIZE / 2, SIZE / 2), (0, 0, 255));
}
#[test]
fn a_cube_draws_a_non_empty_picture_through_the_command_path() {
let mesh = Mesh3D::cube(
Vec3::ZERO,
2.0,
[
Color::RED,
Color::GREEN,
Color::BLUE,
Color::rgb(200, 200, 0),
Color::rgb(0, 200, 200),
Color::rgb(200, 0, 200),
],
);
let camera = Camera::new(Vec3::new(3.0, 2.0, 6.0), Vec3::ZERO, Vec3::new(0.0, 1.0, 0.0));
let mut backend = SoftwarePaintBackend::new(Size::new(200, 200), 1.0);
backend.begin_frame(Color::WHITE);
backend.execute_command(&RenderCommand::DrawMesh3D {
mesh,
model: Mat4::IDENTITY,
camera,
viewport: Rect::new(0, 0, 200, 200),
});
backend.end_frame();
let frame = backend.frame_rgba();
let painted = frame
.as_chunks::<4>()
.0
.iter()
.filter(|pixel| pixel[0] != 255 || pixel[1] != 255 || pixel[2] != 255)
.count();
assert!(painted > 1000, "a cube at this camera must cover a real area, got {painted}");
let mut red = 0;
let mut blue = 0;
let mut cyan = 0;
for pixel in frame.as_chunks::<4>().0 {
red += usize::from(pixel[0] > 128 && pixel[1] < 64 && pixel[2] < 64);
blue += usize::from(pixel[2] > 128 && pixel[0] < 64 && pixel[1] < 64);
cyan += usize::from(pixel[1] > 128 && pixel[2] > 128 && pixel[0] < 64);
}
assert!(red > 100 && blue > 100 && cyan > 100, "{red} {blue} {cyan}");
}
#[test]
fn a_mesh_outside_its_viewport_paints_nothing_inside_the_viewport_elsewhere() {
let mesh = Mesh3D::new(triangle(1.0, Color::RED).to_vec(), vec![0, 1, 2]).expect("valid");
let mut surface = SoftwareSurface::new(Size::new(SIZE, SIZE), 1.0);
surface.begin_frame(Color::WHITE);
surface.draw_mesh_3d(&mesh, &Mat4::IDENTITY, &camera(), Rect::new(0, 0, 20, 20));
surface.end_frame();
assert_eq!(pixel(surface.frame_rgba(), SIZE / 2, SIZE / 2), (255, 255, 255));
}
#[test]
fn a_two_dimensional_command_after_a_mesh_paints_over_it() {
let mesh = Mesh3D::new(triangle(1.0, Color::RED).to_vec(), vec![0, 1, 2]).expect("valid");
let mut surface = SoftwareSurface::new(Size::new(SIZE, SIZE), 1.0);
surface.begin_frame(Color::WHITE);
surface.draw_mesh_3d(&mesh, &Mat4::IDENTITY, &camera(), viewport());
surface.fill_rect(Rect::new(0, 0, SIZE, SIZE), Color::GREEN);
surface.end_frame();
assert_eq!(
pixel(surface.frame_rgba(), SIZE / 2, SIZE / 2),
(0, 255, 0),
"flat drawing has no depth and keeps painter order"
);
}
#[test]
fn a_mesh_is_clipped_by_the_surface_clip_stack() {
let mesh = Mesh3D::new(triangle(1.0, Color::RED).to_vec(), vec![0, 1, 2]).expect("valid");
let mut surface = SoftwareSurface::new(Size::new(SIZE, SIZE), 1.0);
surface.begin_frame(Color::WHITE);
surface.push_clip(0, 0, SIZE / 2, SIZE);
surface.draw_mesh_3d(&mesh, &Mat4::IDENTITY, &camera(), viewport());
surface.pop_clip();
surface.end_frame();
assert_eq!(pixel(surface.frame_rgba(), 48, SIZE / 2), (255, 0, 0));
assert_eq!(pixel(surface.frame_rgba(), 52, SIZE / 2), (255, 255, 255));
}
#[test]
fn an_empty_mesh_or_viewport_draws_nothing() {
let empty = Mesh3D::new(crate::compat::Vec::new(), crate::compat::Vec::new())
.expect("an empty mesh is valid");
let mut surface = SoftwareSurface::new(Size::new(SIZE, SIZE), 1.0);
surface.begin_frame(Color::WHITE);
surface.draw_mesh_3d(&empty, &Mat4::IDENTITY, &camera(), viewport());
let mesh = Mesh3D::new(triangle(1.0, Color::RED).to_vec(), vec![0, 1, 2]).expect("valid");
surface.draw_mesh_3d(&mesh, &Mat4::IDENTITY, &camera(), Rect::new(0, 0, 0, 0));
surface.end_frame();
assert_eq!(pixel(surface.frame_rgba(), SIZE / 2, SIZE / 2), (255, 255, 255));
}
#[test]
fn a_point_outside_the_triangle_is_not_painted() {
let mesh = Mesh3D::new(triangle(1.0, Color::RED).to_vec(), vec![0, 1, 2]).expect("valid");
let mut surface = SoftwareSurface::new(Size::new(SIZE, SIZE), 1.0);
surface.begin_frame(Color::WHITE);
surface.draw_mesh_3d(&mesh, &Mat4::IDENTITY, &camera(), viewport());
surface.end_frame();
assert_eq!(pixel(surface.frame_rgba(), 2, 2), (255, 255, 255));
}
#[test]
fn the_viewport_origin_moves_the_picture() {
let mesh = Mesh3D::new(triangle(1.0, Color::RED).to_vec(), vec![0, 1, 2]).expect("valid");
let mut surface = SoftwareSurface::new(Size::new(SIZE, SIZE), 1.0);
surface.begin_frame(Color::WHITE);
surface.draw_mesh_3d(
&mesh,
&Mat4::IDENTITY,
&camera(),
Rect::new(SIZE as i32 / 2, 0, SIZE / 2, SIZE),
);
surface.end_frame();
let frame = surface.frame_rgba();
assert_eq!(pixel(frame, 75, 50), (255, 0, 0), "the picture follows the viewport origin");
assert_eq!(pixel(frame, 25, 50), (255, 255, 255), "the old centre stays clear");
}
}