use super::*;
use crate::three::extract::MeshData;
use crate::three::load::{MaterialData, MeshInstance};
fn model_of(tris: Vec<([f32; 9], [f32; 4])>) -> Model {
let mut model = Model::default();
for (pos, color) in tris {
let positions = vec![
[pos[0], pos[1], pos[2]],
[pos[3], pos[4], pos[5]],
[pos[6], pos[7], pos[8]],
];
let mat_idx = model.materials.len();
model.materials.push(MaterialData {
base_color: color,
..MaterialData::default()
});
model.instances.push(MeshInstance {
data: MeshData {
positions,
normals: None,
uvs: None,
colors: None,
indices: vec![0, 1, 2],
material: Some(mat_idx),
..MeshData::default()
},
world: Mat4::IDENTITY,
source_node: None,
});
}
model
}
fn tri_at(z: f32, half: f32) -> [f32; 9] {
[-half, -half, z, half, -half, z, 0.0, half, z]
}
#[test]
fn mip_level_picks_by_texel_density() {
let uv = ([0.0, 0.0], [1.0, 0.0], [0.0, 1.0]);
let lvl = mip_level(
(0.0, 0.0),
(16.0, 0.0),
(0.0, 16.0),
uv.0,
uv.1,
uv.2,
65536.0,
8,
);
assert_eq!(lvl, 4);
let lvl = mip_level(
(0.0, 0.0),
(100.0, 0.0),
(0.0, 100.0),
uv.0,
uv.1,
uv.2,
64.0,
8,
);
assert_eq!(lvl, 0);
let lvl = mip_level(
(5.0, 5.0),
(5.0, 5.0),
(5.0, 5.0),
uv.0,
uv.1,
uv.2,
65536.0,
8,
);
assert_eq!(lvl, 8);
let lvl = mip_level(
(0.0, 0.0),
(2.0, 0.0),
(0.0, 2.0),
uv.0,
uv.1,
uv.2,
16_777_216.0,
3,
);
assert_eq!(lvl, 3);
let lvl = mip_level(
(0.0, 0.0),
(16.0, 0.0),
(0.0, 16.0),
[f32::NAN, 0.0],
uv.1,
uv.2,
65536.0,
8,
);
assert_eq!(lvl, 0);
}
#[test]
fn mips_average_minified_checkerboards() {
use crate::gfx::bitmap::Bitmap;
use crate::three::extract::MeshData;
use crate::three::load::{MaterialData, MeshInstance, Model};
let checker = Bitmap::from_fn(128, 128, |x, y| {
if (x + y) % 2 == 0 {
Rgba::rgb(255, 255, 255)
} else {
Rgba::rgb(0, 0, 0)
}
});
let quad = MeshData {
positions: vec![
[-1.0, -1.0, 0.0],
[1.0, -1.0, 0.0],
[1.0, 1.0, 0.0],
[-1.0, 1.0, 0.0],
],
normals: None,
uvs: Some(vec![[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]]),
colors: None,
indices: vec![0, 1, 2, 0, 2, 3],
material: Some(0),
..MeshData::default()
};
let build = |mips: bool| {
let mut mat = MaterialData {
texture: Some(checker.clone()),
base_color: [1.0; 4],
..MaterialData::default()
};
if mips {
mat.mips = checker.mip_chain();
}
Model {
instances: vec![MeshInstance {
data: quad.clone(),
world: Mat4::IDENTITY,
source_node: None,
}],
materials: vec![mat],
rig: None,
warnings: Vec::new(),
}
};
let render_spread = |model: &Model| -> (u8, u8) {
let mut fb = Framebuffer::new(48, 48);
let mut scene = Scene::new(model, Camera::orbit(Vec3::ZERO, 12.0, 0.0, 0.0));
scene.double_sided = true;
scene.light = Light {
direction: Vec3::new(0.0, 0.0, -1.0),
ambient: 1.0,
diffuse: 0.0,
};
render(&scene, &mut fb);
let mut min = 255u8;
let mut max = 0u8;
for p in fb.bitmap().pixels() {
if p.a > 0 {
min = min.min(p.r);
max = max.max(p.r);
}
}
(min, max)
};
let (min_raw, max_raw) = render_spread(&build(false));
let (min_mip, max_mip) = render_spread(&build(true));
let spread_raw = max_raw - min_raw;
let spread_mip = max_mip - min_mip;
assert!(
spread_mip < spread_raw / 2,
"mips must collapse the minified checker toward its mean \
(raw spread {spread_raw}, mip spread {spread_mip})"
);
}
#[test]
fn camera_is_total_over_hostile_bounds() {
let cam = Camera::framing(Vec3::splat(f32::MIN), Vec3::splat(f32::MAX), 0.3, 0.2);
assert!(cam.near > 0.0 && cam.far > cam.near, "{cam:?}");
let _ = cam.projection(1.0); let cam = Camera::framing(Vec3::splat(2.0), Vec3::splat(2.0), 0.0, 0.0);
assert!(cam.near > 0.0 && cam.far > cam.near);
let _ = cam.projection(1.0);
let cam = Camera::orbit(Vec3::ZERO, f32::INFINITY, 0.0, 0.0);
assert!(cam.near > 0.0 && cam.far > cam.near && cam.distance.is_finite());
let _ = cam.projection(1.0);
}
#[test]
fn degenerate_geometry_renders_nothing_and_never_panics() {
use crate::three::extract::MeshData;
let mesh = MeshData {
positions: vec![
[f32::NAN, 0.0, 0.0], [1.0, 0.0, 0.0],
[2.0, 0.0, 0.0], [3.0, 0.0, 0.0],
[0.0, 1.0, -0.5],
],
normals: None,
uvs: None,
colors: None,
indices: vec![
0, 1, 2, 1, 2, 3, 4, 4, 4, 0, 0, 0, ],
material: None,
..MeshData::default()
};
let model = crate::three::primitives::model_of(mesh, [1.0; 4]);
let mut fb = Framebuffer::new(32, 32);
let mut scene = Scene::new(&model, Camera::orbit(Vec3::ZERO, 3.0, 0.3, 0.2));
scene.double_sided = true;
render(&scene, &mut fb);
assert_eq!(fb.coverage(), 0.0, "degenerate geometry painted pixels");
let mut model = model;
model.ensure_smooth_normals();
render(
&Scene::new(&model, Camera::orbit(Vec3::ZERO, 3.0, 0.3, 0.2)),
&mut fb,
);
assert_eq!(fb.coverage(), 0.0);
}
#[test]
fn camera_orbit_and_framing() {
let cam = Camera::orbit(Vec3::ZERO, 5.0, 0.0, 0.0);
let eye = cam.eye();
assert!((eye.z - 5.0).abs() < 1e-5 && eye.x.abs() < 1e-6);
let cam = Camera::framing(Vec3::splat(-1.0), Vec3::splat(1.0), 0.3, 0.2);
assert!(cam.distance > (Vec3::splat(1.0) - Vec3::ZERO).length());
let cam = Camera::orbit(Vec3::ZERO, 3.0, 0.0, 10.0);
let v = cam.view();
assert!(v.m.iter().all(|f| f.is_finite()));
}
#[test]
fn scene_depth_ordering_through_full_pipeline() {
let model = model_of(vec![
(tri_at(-1.0, 2.0), [1.0, 0.0, 0.0, 1.0]),
(tri_at(1.0, 1.0), [0.0, 1.0, 0.0, 1.0]),
]);
let scene = Scene::new(&model, Camera::orbit(Vec3::ZERO, 5.0, 0.0, 0.0));
let mut fb = Framebuffer::new(64, 64);
render(&scene, &mut fb);
assert!(fb.coverage() > 0.05, "coverage {}", fb.coverage());
let center = fb.bitmap().get(32, 36).unwrap();
assert!(
center.g > center.r,
"near triangle must occlude: {center:?}"
);
let outer = fb.bitmap().get(10, 50).unwrap();
assert!(
outer.r > outer.g,
"far triangle visible at edges: {outer:?}"
);
}
#[test]
fn backface_cull_and_double_sided() {
let mut back = tri_at(0.0, 1.0);
back.swap(0, 3); back.swap(1, 4);
back.swap(2, 5);
let model = model_of(vec![(back, [1.0, 1.0, 1.0, 1.0])]);
let mut scene = Scene::new(&model, Camera::orbit(Vec3::ZERO, 5.0, 0.0, 0.0));
let mut fb = Framebuffer::new(32, 32);
render(&scene, &mut fb);
assert_eq!(fb.coverage(), 0.0, "backface must cull");
scene.double_sided = true;
render(&scene, &mut fb);
assert!(fb.coverage() > 0.05, "double_sided renders it");
}
#[test]
fn camera_inside_geometry_clips_instead_of_exploding() {
let model = model_of(vec![(tri_at(4.99, 50.0), [1.0, 1.0, 1.0, 1.0])]);
let scene = Scene::new(&model, Camera::orbit(Vec3::ZERO, 5.0, 0.0, 0.0));
let mut fb = Framebuffer::new(32, 32);
render(&scene, &mut fb); for y in 0..32 {
for x in 0..32 {
let d = fb.depth_at(x, y).unwrap();
assert!(!d.is_nan(), "NaN depth at {x},{y}");
}
}
}
#[test]
fn gouraud_uses_vertex_normals() {
let mut model = model_of(vec![(tri_at(0.0, 2.0), [1.0, 1.0, 1.0, 1.0])]);
model.instances[0].data.normals = Some(vec![
[0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 0.0, 1.0],
]);
let mut scene = Scene::new(&model, Camera::orbit(Vec3::ZERO, 5.0, 0.0, 0.0));
scene.light = Light {
direction: Vec3::new(0.0, 0.0, -1.0),
ambient: 0.2,
diffuse: 0.8,
};
let mut fb = Framebuffer::new(48, 48);
render(&scene, &mut fb);
let lit = fb.bitmap().get(12, 40).unwrap();
let dim = fb.bitmap().get(36, 40).unwrap();
assert!(
lit.r as i32 > dim.r as i32 + 30,
"gouraud gradient missing: lit {lit:?} dim {dim:?}"
);
}
#[test]
fn render_is_deterministic() {
let model = model_of(vec![(tri_at(0.0, 1.5), [0.9, 0.5, 0.2, 1.0])]);
let scene = Scene::new(&model, Camera::orbit(Vec3::ZERO, 4.0, 0.4, 0.3));
let mut a = Framebuffer::new(40, 30);
let mut b = Framebuffer::new(40, 30);
render(&scene, &mut a);
render(&scene, &mut b);
assert_eq!(a.bitmap(), b.bitmap());
}