use cgmath::{MetricSpace as _, Point3, Transform as _, Vector3};
use pretty_assertions::assert_eq;
use super::*;
use crate::block::{Block, BlockAttributes, Primitive, Resolution::*, AIR};
use crate::camera::{Flaws, GraphicsOptions, TransparencyOption};
use crate::content::{make_some_blocks, make_some_voxel_blocks};
use crate::math::{
Face6::{self, *},
FaceMap, FreeCoordinate, GridAab, GridPoint, GridRotation, Rgba,
};
use crate::mesh::BlockMesh;
use crate::space::{Space, SpacePhysics};
use crate::universe::Universe;
fn v_c<T>(position: [FreeCoordinate; 3], face: Face6, color: [f32; 4]) -> BlockVertex<T> {
BlockVertex {
position: position.into(),
face,
coloring: Coloring::Solid(Rgba::new(color[0], color[1], color[2], color[3])),
}
}
fn v_t(
position: [FreeCoordinate; 3],
face: Face6,
texture: [TextureCoordinate; 3],
) -> BlockVertex<TtPoint> {
let texture = texture.into();
BlockVertex {
position: position.into(),
face,
coloring: Coloring::Texture {
pos: texture,
clamp_min: texture,
clamp_max: texture,
},
}
}
fn test_block_mesh(block: Block) -> BlockMesh<BlockVertex<TtPoint>, TestTextureTile> {
BlockMesh::new(
&block.evaluate().unwrap(),
&TestTextureAllocator::new(),
&MeshOptions {
transparency: TransparencyOption::Volumetric,
..MeshOptions::dont_care_for_test()
},
)
}
fn test_block_mesh_threshold(block: Block) -> BlockMesh<BlockVertex<TtPoint>, TestTextureTile> {
BlockMesh::new(
&block.evaluate().unwrap(),
&TestTextureAllocator::new(),
&MeshOptions {
transparency: TransparencyOption::Threshold(notnan!(0.5)),
..MeshOptions::dont_care_for_test()
},
)
}
#[allow(clippy::type_complexity)]
fn mesh_blocks_and_space(
space: &Space,
) -> (
TestTextureAllocator,
BlockMeshes<BlockVertex<TtPoint>, TestTextureTile>,
SpaceMesh<BlockVertex<TtPoint>, TestTextureTile>,
) {
let options = &MeshOptions::new(&GraphicsOptions::default());
let tex = TestTextureAllocator::new();
let block_meshes = block_meshes_for_space(space, &tex, options);
let space_mesh: SpaceMesh<BlockVertex<TtPoint>, TestTextureTile> =
SpaceMesh::new(space, space.bounds(), options, &*block_meshes);
(tex, block_meshes, space_mesh)
}
fn non_uniform_fill(cube: GridPoint) -> &'static Block {
const C1: &Primitive =
&Primitive::Atom(BlockAttributes::default(), rgba_const!(1., 1., 1., 1.));
const C2: &Primitive =
&Primitive::Atom(BlockAttributes::default(), rgba_const!(0., 0., 0., 1.));
const BLOCKS: &[Block] = &[
Block::from_static_primitive(C1),
Block::from_static_primitive(C2),
];
&BLOCKS[(cube.x + cube.y + cube.z).rem_euclid(2) as usize]
}
#[test]
fn excludes_hidden_faces_of_blocks() {
let mut space = Space::empty_positive(2, 2, 2);
space
.fill(space.bounds(), |p| Some(non_uniform_fill(p)))
.unwrap();
let (_, _, space_mesh) = mesh_blocks_and_space(&space);
assert_eq!(space_mesh.flaws(), Flaws::empty());
assert_eq!(
Vec::<&BlockVertex<TtPoint>>::new(),
space_mesh
.vertices()
.iter()
.filter(|vertex| vertex.position.distance2(Point3::new(1.0, 1.0, 1.0)) < 0.99)
.collect::<Vec<&BlockVertex<TtPoint>>>(),
"found an interior point"
);
assert_eq!(
space_mesh.vertices().len(),
4
* 4
* 6,
"wrong number of faces"
);
}
#[test]
fn no_panic_on_missing_blocks() {
let [block] = make_some_blocks();
let mut space = Space::empty_positive(2, 1, 1);
let block_meshes: BlockMeshes<BlockVertex<TtPoint>, _> = block_meshes_for_space(
&space,
&TestTextureAllocator::new(),
&MeshOptions::dont_care_for_test(),
);
assert_eq!(block_meshes.len(), 1);
space.set((0, 0, 0), &block).unwrap();
let space_mesh = SpaceMesh::new(
&space,
space.bounds(),
&MeshOptions::dont_care_for_test(),
&*block_meshes,
);
assert_eq!(space_mesh.flaws(), Flaws::empty());
}
#[test]
fn trivial_voxels_equals_atom() {
let mut u = Universe::new();
let atom_block = Block::from(Rgba::new(0.0, 1.0, 0.0, 1.0));
let trivial_recursive_block = Block::builder()
.voxels_fn(&mut u, R1, |_| &atom_block)
.unwrap()
.build();
let (_, _, space_rendered_a) = mesh_blocks_and_space(&{
let mut space = Space::empty_positive(1, 1, 1);
space.set((0, 0, 0), &atom_block).unwrap();
space
});
let (tex, _, space_rendered_r) = mesh_blocks_and_space(&{
let mut space = Space::empty_positive(1, 1, 1);
space.set((0, 0, 0), &trivial_recursive_block).unwrap();
space
});
assert_eq!(space_rendered_a, space_rendered_r);
assert_eq!(tex.count_allocated(), 0);
}
#[test]
fn space_mesh_equals_block_mesh() {
let resolution = R4;
let mut u = Universe::new();
let mut blocks = Vec::from(make_some_blocks::<2>());
blocks.push(AIR);
let recursive_block = Block::builder()
.voxels_fn(&mut u, resolution, |p| {
&blocks[(p.x as usize).rem_euclid(blocks.len())]
})
.unwrap()
.build();
let mut outer_space = Space::empty_positive(1, 1, 1);
outer_space.set((0, 0, 0), &recursive_block).unwrap();
let (tex, block_meshes, space_rendered) = mesh_blocks_and_space(&outer_space);
eprintln!("{block_meshes:#?}");
eprintln!("{space_rendered:#?}");
assert_eq!(
space_rendered.vertices().to_vec(),
block_meshes[0]
.all_face_meshes()
.flat_map(|(_, face_mesh)| face_mesh.vertices.clone().into_iter())
.collect::<Vec<_>>()
);
assert_eq!(tex.count_allocated(), 1);
assert_eq!(space_rendered, SpaceMesh::from(&block_meshes[0]));
}
#[test]
fn block_resolution_greater_than_tile() {
let block_resolution = R32;
let mut u = Universe::new();
let block = Block::builder()
.voxels_fn(&mut u, block_resolution, non_uniform_fill)
.unwrap()
.build();
let mut outer_space = Space::empty_positive(1, 1, 1);
outer_space.set((0, 0, 0), &block).unwrap();
let (_, _, _) = mesh_blocks_and_space(&outer_space);
}
#[test]
#[rustfmt::skip]
fn shrunken_box_has_no_extras() {
let resolution = R8;
let mut u = Universe::new();
let less_than_full_block = Block::builder()
.voxels_fn(&mut u, resolution, |cube| {
if GridAab::from_lower_size([2, 2, 2], [4, 4, 4]).contains_cube(cube) {
non_uniform_fill(cube)
} else {
&AIR
}
})
.unwrap()
.build();
let mut outer_space = Space::empty_positive(1, 1, 1);
outer_space.set((0, 0, 0), &less_than_full_block).unwrap();
let (tex, _, space_rendered) = mesh_blocks_and_space(&outer_space);
assert_eq!(tex.count_allocated(), 1);
assert_eq!(
space_rendered.vertices().iter().map(|&v| v.remove_clamps()).collect::<Vec<_>>(),
vec![
v_t([0.250, 0.250, 0.250], NX, [2.5, 2.0, 2.0]),
v_t([0.250, 0.250, 0.750], NX, [2.5, 2.0, 6.0]),
v_t([0.250, 0.750, 0.250], NX, [2.5, 6.0, 2.0]),
v_t([0.250, 0.750, 0.750], NX, [2.5, 6.0, 6.0]),
v_t([0.250, 0.250, 0.250], NY, [2.0, 2.5, 2.0]),
v_t([0.750, 0.250, 0.250], NY, [6.0, 2.5, 2.0]),
v_t([0.250, 0.250, 0.750], NY, [2.0, 2.5, 6.0]),
v_t([0.750, 0.250, 0.750], NY, [6.0, 2.5, 6.0]),
v_t([0.250, 0.250, 0.250], NZ, [2.0, 2.0, 2.5]),
v_t([0.250, 0.750, 0.250], NZ, [2.0, 6.0, 2.5]),
v_t([0.750, 0.250, 0.250], NZ, [6.0, 2.0, 2.5]),
v_t([0.750, 0.750, 0.250], NZ, [6.0, 6.0, 2.5]),
v_t([0.750, 0.750, 0.250], PX, [5.5, 6.0, 2.0]),
v_t([0.750, 0.750, 0.750], PX, [5.5, 6.0, 6.0]),
v_t([0.750, 0.250, 0.250], PX, [5.5, 2.0, 2.0]),
v_t([0.750, 0.250, 0.750], PX, [5.5, 2.0, 6.0]),
v_t([0.750, 0.750, 0.250], PY, [6.0, 5.5, 2.0]),
v_t([0.250, 0.750, 0.250], PY, [2.0, 5.5, 2.0]),
v_t([0.750, 0.750, 0.750], PY, [6.0, 5.5, 6.0]),
v_t([0.250, 0.750, 0.750], PY, [2.0, 5.5, 6.0]),
v_t([0.250, 0.750, 0.750], PZ, [2.0, 6.0, 5.5]),
v_t([0.250, 0.250, 0.750], PZ, [2.0, 2.0, 5.5]),
v_t([0.750, 0.750, 0.750], PZ, [6.0, 6.0, 5.5]),
v_t([0.750, 0.250, 0.750], PZ, [6.0, 2.0, 5.5]),
],
);
}
#[test]
#[rustfmt::skip]
fn shrunken_box_uniform_color() {
let resolution = R8;
let mut u = Universe::new();
let filler_block = Block::from(Rgba::new(0.0, 1.0, 0.5, 1.0));
let less_than_full_block = Block::builder()
.voxels_fn(&mut u, resolution, |cube| {
if GridAab::from_lower_size([2, 2, 2], [4, 4, 4]).contains_cube(cube) {
&filler_block
} else {
&AIR
}
})
.unwrap()
.build();
let mut outer_space = Space::empty_positive(1, 1, 1);
outer_space.set((0, 0, 0), &less_than_full_block).unwrap();
let (tex, _, space_rendered) = mesh_blocks_and_space(&outer_space);
assert_eq!(tex.count_allocated(), 0);
assert_eq!(
space_rendered.vertices().to_vec(),
vec![
v_c([0.250, 0.250, 0.250], NX, [0.0, 1.0, 0.5, 1.0]),
v_c([0.250, 0.250, 0.750], NX, [0.0, 1.0, 0.5, 1.0]),
v_c([0.250, 0.750, 0.250], NX, [0.0, 1.0, 0.5, 1.0]),
v_c([0.250, 0.750, 0.750], NX, [0.0, 1.0, 0.5, 1.0]),
v_c([0.250, 0.250, 0.250], NY, [0.0, 1.0, 0.5, 1.0]),
v_c([0.750, 0.250, 0.250], NY, [0.0, 1.0, 0.5, 1.0]),
v_c([0.250, 0.250, 0.750], NY, [0.0, 1.0, 0.5, 1.0]),
v_c([0.750, 0.250, 0.750], NY, [0.0, 1.0, 0.5, 1.0]),
v_c([0.250, 0.250, 0.250], NZ, [0.0, 1.0, 0.5, 1.0]),
v_c([0.250, 0.750, 0.250], NZ, [0.0, 1.0, 0.5, 1.0]),
v_c([0.750, 0.250, 0.250], NZ, [0.0, 1.0, 0.5, 1.0]),
v_c([0.750, 0.750, 0.250], NZ, [0.0, 1.0, 0.5, 1.0]),
v_c([0.750, 0.750, 0.250], PX, [0.0, 1.0, 0.5, 1.0]),
v_c([0.750, 0.750, 0.750], PX, [0.0, 1.0, 0.5, 1.0]),
v_c([0.750, 0.250, 0.250], PX, [0.0, 1.0, 0.5, 1.0]),
v_c([0.750, 0.250, 0.750], PX, [0.0, 1.0, 0.5, 1.0]),
v_c([0.750, 0.750, 0.250], PY, [0.0, 1.0, 0.5, 1.0]),
v_c([0.250, 0.750, 0.250], PY, [0.0, 1.0, 0.5, 1.0]),
v_c([0.750, 0.750, 0.750], PY, [0.0, 1.0, 0.5, 1.0]),
v_c([0.250, 0.750, 0.750], PY, [0.0, 1.0, 0.5, 1.0]),
v_c([0.250, 0.750, 0.750], PZ, [0.0, 1.0, 0.5, 1.0]),
v_c([0.250, 0.250, 0.750], PZ, [0.0, 1.0, 0.5, 1.0]),
v_c([0.750, 0.750, 0.750], PZ, [0.0, 1.0, 0.5, 1.0]),
v_c([0.750, 0.250, 0.750], PZ, [0.0, 1.0, 0.5, 1.0]),
],
);
}
fn opacities<V, T>(mesh: &BlockMesh<V, T>) -> FaceMap<bool> {
assert!(
!mesh.interior_vertices.fully_opaque,
"interior opacity should never be true because it doesn't mean anything"
);
FaceMap::from_fn(|face| mesh.face_vertices[face].fully_opaque)
}
#[test]
fn atom_transparency() {
assert_eq!(
opacities(&test_block_mesh(Block::from(Rgba::WHITE))),
FaceMap::repeat(true)
);
assert_eq!(
opacities(&test_block_mesh(Block::from(Rgba::TRANSPARENT))),
FaceMap::repeat(false)
);
assert_eq!(
opacities(&test_block_mesh(Block::from(Rgba::new(1.0, 1.0, 1.0, 0.5)))),
FaceMap::repeat(false)
);
}
#[test]
fn atom_transparency_thresholded() {
assert_eq!(
test_block_mesh_threshold(Block::from(Rgba::new(1.0, 1.0, 1.0, 0.25))),
test_block_mesh_threshold(Block::from(Rgba::new(1.0, 1.0, 1.0, 0.0))),
);
assert_eq!(
test_block_mesh_threshold(Block::from(Rgba::new(1.0, 1.0, 1.0, 0.75))),
test_block_mesh_threshold(Block::from(Rgba::new(1.0, 1.0, 1.0, 1.0))),
);
}
#[test]
fn fully_opaque_voxels() {
let resolution = R8;
let mut u = Universe::new();
let block = Block::builder()
.voxels_fn(&mut u, resolution, |cube| {
if cube.x < 1 || cube.y < 1 || cube.z < 1 {
Block::from(Rgba::BLACK)
} else {
AIR
}
})
.unwrap()
.build();
assert_eq!(
opacities(&test_block_mesh(block)),
FaceMap {
nx: true,
ny: true,
nz: true,
px: false,
py: false,
pz: false,
}
);
}
#[test]
fn fully_opaque_partial_block() {
let mut u = Universe::new();
let block = Block::builder()
.voxels_ref(R8, {
u.insert_anonymous(
Space::builder(GridAab::from_lower_size([0, 0, 0], [4, 8, 8]))
.physics(SpacePhysics::DEFAULT_FOR_BLOCK)
.filled_with(Block::from(Rgba::WHITE))
.build(),
)
})
.build();
assert_eq!(
opacities(&test_block_mesh(block)),
FaceMap {
nx: true,
ny: false,
nz: false,
px: false,
py: false,
pz: false,
}
);
}
#[test]
fn transparency_split() {
let mut space = Space::empty_positive(3, 1, 1);
space
.set([0, 0, 0], Block::from(Rgba::new(1.0, 0.0, 0.0, 1.0)))
.unwrap();
space
.set([2, 0, 0], Block::from(Rgba::new(0.0, 0.0, 1.0, 0.5)))
.unwrap();
let (_, _, space_rendered) = mesh_blocks_and_space(&space);
assert_eq!(space_rendered.vertices().len(), 6 * 4 * 2);
assert_eq!(space_rendered.opaque_range().len(), 6 * 6);
for &r in &GridRotation::ALL {
assert_eq!(
space_rendered
.transparent_range(DepthOrdering::Direction(r))
.len(),
6 * 6
);
}
}
#[test]
fn handling_allocation_failure() {
let resolution = R8;
let mut u = Universe::new();
let [atom1, atom2] = make_some_blocks();
let complex_block = Block::builder()
.voxels_fn(&mut u, resolution, |cube| {
if (cube.x + cube.y + cube.z).rem_euclid(2) == 0 {
&atom1
} else {
&atom2
}
})
.unwrap()
.build();
let block_derived_color = complex_block.evaluate().unwrap().color;
let mut space = Space::empty_positive(1, 1, 1);
space.set((0, 0, 0), &complex_block).unwrap();
let mut tex = TestTextureAllocator::new();
tex.set_capacity(0);
let block_meshes: BlockMeshes<BlockVertex<TtPoint>, _> =
block_meshes_for_space(&space, &tex, &MeshOptions::dont_care_for_test());
assert_eq!(tex.count_allocated(), 0);
assert_eq!(1, block_meshes.len());
let mesh = &block_meshes[0];
assert_eq!(mesh.flaws(), Flaws::MISSING_TEXTURES);
let space_mesh = SpaceMesh::from(mesh);
assert_eq!(space_mesh.flaws(), Flaws::MISSING_TEXTURES);
let allowed_colors = [atom1.color(), atom2.color(), block_derived_color];
for vertex in space_mesh.vertices() {
match vertex.coloring {
Coloring::Solid(c) if allowed_colors.contains(&c) => { }
Coloring::Solid(c) => {
panic!("unexpected color {c:?}")
}
t @ Coloring::Texture { .. } => panic!("unexpected texture {t:?}"),
}
}
}
#[test]
fn space_mesh_empty() {
let t = SpaceMesh::<BlockVertex<TtPoint>, TestTextureTile>::default();
assert!(t.is_empty());
assert_eq!(t.flaws(), Flaws::empty());
assert_eq!(t.vertices(), &[]);
assert_eq!(t.indices(), &[] as &[u32]);
}
#[test]
fn depth_ordering_from_view_direction() {
let mut problems = Vec::new();
let range = -3..3;
for x in range.clone() {
for y in range.clone() {
for z in range.clone() {
let direction = Vector3::new(x, y, z);
let ordering = DepthOrdering::from_view_direction(direction);
let rotated_direction = match ordering {
DepthOrdering::Any => {
panic!("from_view_direction should not return Any")
}
DepthOrdering::Within => direction,
DepthOrdering::Direction(rotation) => {
rotation.to_rotation_matrix().transform_vector(direction)
}
};
let good = rotated_direction.x >= rotated_direction.y
&& rotated_direction.y >= rotated_direction.z;
println!(
"{:?} → {:?} → {:?}{}",
direction,
ordering,
rotated_direction,
if good { "" } else { " (wrong)" }
);
if !good {
problems.push(direction);
}
}
}
}
assert_eq!(problems, vec![]);
}
#[test]
fn texture_clamp_coordinate_ordering() {
const ALL_TRUE: Point3<bool> = Point3::new(true, true, true);
let mut universe = Universe::new();
let [block] = make_some_voxel_blocks(&mut universe);
let mesh = test_block_mesh(block);
for (face, face_mesh) in mesh.all_face_meshes() {
for vertex in face_mesh.vertices.iter() {
let mut had_any_textured = false;
match vertex.coloring {
Coloring::Solid(_) => {}
Coloring::Texture {
pos,
clamp_min,
clamp_max,
} => {
had_any_textured = true;
assert!(
clamp_min.zip(clamp_max, |min, max| min <= max) == ALL_TRUE,
"clamp should be {clamp_min:?} <= {clamp_max:?}"
);
assert!(
clamp_min.zip(pos, |min, pos| min - 0.5 <= pos) == ALL_TRUE
&& pos.zip(clamp_max, |pos, max| pos <= max + 0.5) == ALL_TRUE,
"{clamp_min:?} <= {pos:?} <= {clamp_max:?}"
);
}
}
assert!(
had_any_textured,
"test invalid: {face:?} has no textured vertices"
)
}
}
}