use cgmath::{EuclideanSpace as _, Point3, Transform as _, Vector2, Vector3};
use std::convert::TryFrom;
use crate::block::Block;
use crate::lighting::PackedLight;
use crate::math::{Face, FaceMap, FreeCoordinate, GridCoordinate, RGBA};
use crate::space::Space;
use crate::util::ConciseDebug as _;
pub type TextureCoordinate = f32;
#[derive(Clone, Copy, PartialEq)]
pub struct BlockVertex {
pub position: Point3<FreeCoordinate>,
pub normal: Vector3<FreeCoordinate>,
pub coloring: Coloring,
}
#[derive(Clone, Copy, PartialEq)]
pub enum Coloring {
Solid(RGBA),
Texture(Vector3<TextureCoordinate>),
}
impl std::fmt::Debug for BlockVertex {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
fmt,
"{{ p: {:?} n: {:?} c: {:?} }}",
self.position.as_concise_debug(),
self.normal.cast::<i8>().unwrap().as_concise_debug(),
self.coloring
)
}
}
impl std::fmt::Debug for Coloring {
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Coloring::Solid(color) => write!(fmt, "Solid({:?})", color),
Coloring::Texture(tc) => write!(fmt, "Texture({:?})", tc.as_concise_debug()),
}
}
}
pub trait ToGfxVertex<GV>: From<BlockVertex> + Sized {
type Coordinate: cgmath::BaseNum;
fn instantiate(&self, offset: Vector3<Self::Coordinate>, lighting: PackedLight) -> GV;
}
impl ToGfxVertex<BlockVertex> for BlockVertex {
type Coordinate = FreeCoordinate;
fn instantiate(&self, offset: Vector3<FreeCoordinate>, _lighting: PackedLight) -> Self {
Self {
position: self.position + offset,
..*self
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct FaceRenderData<V: From<BlockVertex>> {
vertices: Vec<V>,
fully_opaque: bool,
}
impl<V: From<BlockVertex>> Default for FaceRenderData<V> {
fn default() -> Self {
FaceRenderData {
vertices: Vec::new(),
fully_opaque: false,
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BlockRenderData<V: From<BlockVertex>, A: TextureAllocator> {
faces: FaceMap<FaceRenderData<V>>,
textures_used: Vec<A::Tile>,
}
impl<V: From<BlockVertex>, A: TextureAllocator> Default for BlockRenderData<V, A> {
fn default() -> Self {
Self {
faces: FaceMap::generate(|_| FaceRenderData::default()),
textures_used: Vec::new(),
}
}
}
pub type BlocksRenderData<V, A> = Box<[BlockRenderData<V, A>]>;
const QUAD_VERTICES: &[Point3<FreeCoordinate>; 6] = &[
Point3::new(0.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(1.0, 0.0, 0.0),
Point3::new(0.0, 1.0, 0.0),
Point3::new(1.0, 1.0, 0.0),
];
fn push_quad_solid<V: From<BlockVertex>>(vertices: &mut Vec<V>, face: Face, color: RGBA) {
let transform = face.matrix();
for &p in QUAD_VERTICES {
vertices.push(V::from(BlockVertex {
position: transform.transform_point(p),
normal: face.normal_vector(),
coloring: Coloring::Solid(color),
}));
}
}
fn push_quad_textured<V: From<BlockVertex>>(
vertices: &mut Vec<V>,
face: Face,
depth: FreeCoordinate,
texture_tile: &impl TextureTile,
) {
let transform = face.matrix();
for &p in QUAD_VERTICES {
vertices.push(V::from(BlockVertex {
position: transform.transform_point(p + Vector3::new(0.0, 0.0, depth)),
normal: face.normal_vector(),
coloring: Coloring::Texture(texture_tile.texcoord(Vector2::new(
p.x as TextureCoordinate,
p.y as TextureCoordinate,
))),
}));
}
}
fn triangulate_block<V: From<BlockVertex>, A: TextureAllocator>(
block: &Block,
texture_allocator: &mut A,
) -> BlockRenderData<V, A> {
match block {
Block::Atom(_attributes, color) => {
let faces = FaceMap::generate(|face| {
if face == Face::WITHIN {
return FaceRenderData::default();
}
let fully_opaque = color.binary_opaque();
FaceRenderData {
vertices: if fully_opaque {
let mut face_vertices: Vec<V> = Vec::with_capacity(6);
push_quad_solid(&mut face_vertices, face, *color);
face_vertices
} else {
Vec::new()
},
fully_opaque,
}
});
BlockRenderData {
faces,
textures_used: vec![],
}
}
Block::Recur(_attributes, space_ref) => {
let space = &*space_ref.borrow();
let mut output_by_face = FaceMap::generate(|face| FaceRenderData {
vertices: Vec::new(),
fully_opaque: face != Face::WITHIN,
});
let mut textures_used = Vec::new();
let tile_size: GridCoordinate = texture_allocator.size();
for &face in Face::ALL_SIX {
let transform = face.matrix();
for layer in 0..tile_size {
let mut tile_texels: Vec<(u8, u8, u8, u8)> =
Vec::with_capacity((tile_size * tile_size) as usize);
let mut layer_is_visible_somewhere = false;
for t in 0..tile_size {
for s in 0..tile_size {
let cube: Point3<GridCoordinate> = (transform.transform_point(
(Point3::new(
FreeCoordinate::from(s),
FreeCoordinate::from(t),
FreeCoordinate::from(layer),
) + Vector3::new(0.5, 0.5, 0.5))
/ FreeCoordinate::from(tile_size),
) * FreeCoordinate::from(
tile_size,
) - Vector3::new(0.5, 0.5, 0.5))
.cast::<GridCoordinate>()
.unwrap();
let obscuring_cube = cube + face.normal_vector();
let obscured = space[obscuring_cube].color().alpha() >= 1.0;
if !obscured {
layer_is_visible_somewhere = true;
}
let color = if space.grid().contains_cube(cube) {
space[cube].color()
} else {
RGBA::new(1.0, 1.0, 0.0, 1.0)
};
if layer == 0 && color.alpha() < 1.0 {
output_by_face[face].fully_opaque = false;
}
tile_texels.push(color.to_saturating_8bpp());
}
}
if layer_is_visible_somewhere {
let mut texture_tile = texture_allocator.allocate();
texture_tile.write(tile_texels.as_ref());
push_quad_textured(
&mut output_by_face[if layer == 0 { face } else { Face::WITHIN }]
.vertices,
face,
FreeCoordinate::from(layer) / FreeCoordinate::from(tile_size),
&texture_tile,
);
textures_used.push(texture_tile);
}
}
}
BlockRenderData {
faces: output_by_face,
textures_used,
}
}
}
}
pub fn triangulate_blocks<V: From<BlockVertex>, A: TextureAllocator>(
space: &Space,
texture_allocator: &mut A,
) -> BlocksRenderData<V, A> {
space
.distinct_blocks_unfiltered()
.iter()
.map(|b| triangulate_block(b, texture_allocator))
.collect()
}
pub fn new_space_buffer<V>() -> FaceMap<Vec<V>> {
FaceMap::generate(|_| Vec::new())
}
pub fn triangulate_space<BV, GV, A>(
space: &Space,
blocks_render_data: &BlocksRenderData<BV, A>,
output_vertices: &mut FaceMap<Vec<GV>>,
) where
BV: ToGfxVertex<GV>,
A: TextureAllocator,
{
let empty_render = BlockRenderData::<BV, A>::default();
let lookup = |cube| {
match space.get_block_index(cube) {
Some(index) => &blocks_render_data
.get(index as usize)
.unwrap_or(&empty_render),
None => &empty_render,
}
};
for &face in Face::ALL_SEVEN.iter() {
output_vertices[face].clear();
}
for cube in space.grid().interior_iter() {
let precomputed = lookup(cube);
let low_corner = cube.cast::<BV::Coordinate>().unwrap();
for &face in Face::ALL_SEVEN {
let adjacent_cube = cube + face.normal_vector();
if lookup(adjacent_cube).faces[face.opposite()].fully_opaque {
continue;
}
let lighting = space.get_lighting(adjacent_cube);
for vertex in precomputed.faces[face].vertices.iter() {
output_vertices[face].push(vertex.instantiate(low_corner.to_vec(), lighting));
}
}
}
}
pub type Texel = (u8, u8, u8, u8);
pub trait TextureAllocator {
type Tile: TextureTile;
fn size(&self) -> GridCoordinate;
fn allocate(&mut self) -> Self::Tile;
}
pub trait TextureTile {
fn texcoord(&self, in_tile: Vector2<TextureCoordinate>) -> Vector3<TextureCoordinate>;
fn write(&mut self, data: &[Texel]);
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct NullTextureAllocator;
impl TextureAllocator for NullTextureAllocator {
type Tile = ();
fn size(&self) -> GridCoordinate {
14
}
fn allocate(&mut self) {}
}
impl TextureTile for () {
fn texcoord(&self, in_tile: Vector2<TextureCoordinate>) -> Vector3<TextureCoordinate> {
in_tile.extend(0.0)
}
fn write(&mut self, data: &[(u8, u8, u8, u8)]) {
assert_eq!(
GridCoordinate::try_from(data.len()).expect("tile data way too big"),
NullTextureAllocator.size() * NullTextureAllocator.size(),
"tile data did not match tile size"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::block::BlockAttributes;
use crate::blockgen::make_some_blocks;
use crate::universe::Universe;
use cgmath::MetricSpace as _;
#[test]
fn excludes_interior_faces() {
let block = make_some_blocks(1).swap_remove(0);
let mut space = Space::empty_positive(2, 2, 2);
for cube in space.grid().interior_iter() {
space.set(cube, &block);
}
let mut rendering = new_space_buffer();
triangulate_space::<BlockVertex, BlockVertex, NullTextureAllocator>(
&space,
&triangulate_blocks(&space, &mut NullTextureAllocator),
&mut rendering,
);
let rendering_flattened: Vec<BlockVertex> = rendering
.values()
.iter()
.flat_map(|r| (*r).clone())
.collect();
assert_eq!(
Vec::<&BlockVertex>::new(),
rendering_flattened
.iter()
.filter(|vertex| vertex.position.distance2(Point3::new(1.0, 1.0, 1.0)) < 0.99)
.collect::<Vec<&BlockVertex>>(),
"found an interior point"
);
assert_eq!(
rendering_flattened.len(),
6
* 4
* 6,
"wrong number of faces"
);
}
#[test]
fn no_panic_on_missing_blocks() {
let block = make_some_blocks(1).swap_remove(0);
let mut space = Space::empty_positive(2, 1, 1);
let blocks_render_data: BlocksRenderData<BlockVertex, _> =
triangulate_blocks(&space, &mut NullTextureAllocator);
assert_eq!(blocks_render_data.len(), 1);
space.set((0, 0, 0), &block);
triangulate_space(&space, &blocks_render_data, &mut new_space_buffer());
}
#[test]
fn trivial_subcube_rendering() {
let mut u = Universe::new();
let mut inner_block_space = Space::empty_positive(1, 1, 1);
inner_block_space.set((0, 0, 0), &make_some_blocks(1)[0]);
let inner_block = Block::Recur(
BlockAttributes::default(),
u.insert_anonymous(inner_block_space),
);
let mut outer_space = Space::empty_positive(1, 1, 1);
outer_space.set((0, 0, 0), &inner_block);
let blocks_render_data: BlocksRenderData<BlockVertex, _> =
triangulate_blocks(&outer_space, &mut NullTextureAllocator);
let block_render_data: BlockRenderData<_, _> = blocks_render_data[0].clone();
eprintln!("{:#?}", blocks_render_data);
let mut space_rendered = new_space_buffer();
triangulate_space(&outer_space, &blocks_render_data, &mut space_rendered);
eprintln!("{:#?}", space_rendered);
assert_eq!(
space_rendered,
block_render_data.faces.map(|_, frd| frd.vertices.to_vec())
);
}
}