pub mod fragment;
pub mod framebuffer;
pub mod rasterize;
pub mod vertex;
use crate::render3d::camera::{Camera, Projection};
use crate::render3d::scene::Scene;
use rasterize::rasterize_triangle;
use vertex::{clip_near, clip_to_screen, transform_to_clip};
pub use framebuffer::Framebuffer;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Pipeline {
#[default]
Rasterize,
}
pub fn render(scene: &Scene, camera: &Camera, fb: &mut Framebuffer) {
fb.clear(scene.background);
let vw = fb.width as f32;
let vh = fb.height as f32;
if vw < 1.0 || vh < 1.0 {
return;
}
let aspect = vw / vh;
let view = camera.view_matrix();
let proj = camera.projection_matrix(aspect);
let view_proj = proj * view;
let Projection::Perspective { near, .. } = camera.projection;
for obj in &scene.objects {
if !obj.visible {
continue;
}
let model = obj.transform.matrix();
let normal_matrix = model.inverse().transpose();
let mesh = &obj.mesh;
for tri in 0..mesh.triangle_count() {
let i0 = mesh.indices[tri * 3] as usize;
let i1 = mesh.indices[tri * 3 + 1] as usize;
let i2 = mesh.indices[tri * 3 + 2] as usize;
let to_clip = |v: &crate::render3d::mesh::Vertex| {
transform_to_clip(
v.position,
v.normal,
v.uv,
&model,
&view_proj,
&normal_matrix,
)
};
let tri = [
to_clip(&mesh.vertices[i0]),
to_clip(&mesh.vertices[i1]),
to_clip(&mesh.vertices[i2]),
];
let (poly, n) = clip_near(tri, near);
for k in 1..n.saturating_sub(1) {
let tv0 = clip_to_screen(&poly[0], vw, vh);
let tv1 = clip_to_screen(&poly[k], vw, vh);
let tv2 = clip_to_screen(&poly[k + 1], vw, vh);
let all_outside = (tv0.screen_pos.x < 0.0
&& tv1.screen_pos.x < 0.0
&& tv2.screen_pos.x < 0.0)
|| (tv0.screen_pos.x > vw && tv1.screen_pos.x > vw && tv2.screen_pos.x > vw)
|| (tv0.screen_pos.y < 0.0 && tv1.screen_pos.y < 0.0 && tv2.screen_pos.y < 0.0)
|| (tv0.screen_pos.y > vh && tv1.screen_pos.y > vh && tv2.screen_pos.y > vh);
if !all_outside {
rasterize_triangle(
&tv0,
&tv1,
&tv2,
&obj.material,
&scene.lights,
camera.position,
scene.fog,
scene.background,
fb,
);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::render;
use crate::render3d::camera::Camera;
use crate::render3d::color::Rgb;
use crate::render3d::light::Light;
use crate::render3d::material::Material;
use crate::render3d::math::Vec3;
use crate::render3d::object::SceneObject;
use crate::render3d::pipeline::framebuffer::Framebuffer;
use crate::render3d::primitives;
use crate::render3d::scene::Scene;
#[test]
fn renders_a_lit_cube_over_the_frame_center() {
let mut scene = Scene::new();
scene.add_object(
SceneObject::new(primitives::cube())
.with_material(Material::default().with_color(Rgb(200, 120, 60))),
);
scene.add_light(Light::ambient(Rgb::WHITE, 0.4));
scene.add_light(Light::directional(Vec3::new(-1.0, -1.0, -1.0), Rgb::WHITE));
let mut fb = Framebuffer::new(128, 128);
render(&scene, &Camera::default(), &mut fb);
let covered = fb.alpha.iter().filter(|&&a| a == 255).count();
assert!(
covered > 200,
"cube should cover many pixels, covered {covered}"
);
let center = fb.index(64, 64);
assert_eq!(fb.alpha[center], 255, "cube should cover the frame center");
assert!(fb.depth[center].is_finite(), "center depth must be written");
assert_ne!(
fb.color[center], scene.background,
"center should be shaded cube, not background",
);
}
#[test]
fn near_plane_clip_keeps_a_floor_whose_near_edge_is_behind_the_camera() {
use crate::render3d::camera::Projection;
use crate::render3d::mesh::{Mesh, Vertex};
let mut scene = Scene::new();
let (y, x0, x1, z_near, z_far) = (-1.0f32, -20.0, 20.0, 4.0, -40.0);
let floor = Mesh::new(
vec![
Vertex::new(Vec3::new(x0, y, z_near), Vec3::Y),
Vertex::new(Vec3::new(x1, y, z_near), Vec3::Y),
Vertex::new(Vec3::new(x1, y, z_far), Vec3::Y),
Vertex::new(Vec3::new(x0, y, z_far), Vec3::Y),
],
vec![0, 1, 2, 0, 2, 3, 0, 2, 1, 0, 3, 2],
);
scene.add_object(
SceneObject::new(floor).with_material(
Material::default()
.with_color(Rgb(200, 160, 120))
.with_ambient(3.0),
),
);
scene.add_light(Light::ambient(Rgb::WHITE, 0.5));
let camera = Camera {
position: Vec3::new(0.0, 1.5, 0.0),
target: Vec3::new(0.0, 0.0, -8.0),
up: Vec3::Y,
projection: Projection::Perspective {
fov_y: std::f32::consts::FRAC_PI_4,
near: 0.1,
far: 100.0,
},
};
let mut fb = Framebuffer::new(128, 128);
render(&scene, &camera, &mut fb);
let covered = fb.alpha.iter().filter(|&&a| a == 255).count();
assert!(
covered > 2000,
"the clipped floor should fill much of the frame, covered {covered}"
);
let low = fb.index(64, 112); assert_eq!(
fb.alpha[low], 255,
"a floor whose near edge is behind the camera must still render"
);
}
}