use std::collections::HashSet;
use std::fmt::Debug;
use arc_swap::ArcSwap;
use cgmath::num_traits::Num;
use cgmath::{vec3, ElementWise, Matrix4, Vector2, Vector3, Zero};
use perovskite_core::constants::{
CHUNK_SIZE_U8, GENERATED_TEXTURE_CATEGORY_SOLID_FROM_CSS, PADDED_CHUNK_VOLUME,
};
use perovskite_core::coordinates::ChunkOffsetForOcclusionExt;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use perovskite_core::protocol::blocks::block_type_def::{PhysicsInfo, RenderInfo};
use perovskite_core::protocol::blocks::{
self as blocks_proto, AxisAlignedBoxes, BlockTypeDef, CubeRenderInfo, CubeVariantEffect,
};
use perovskite_core::protocol::render::{TextureCrop, TextureReference, TextureTransform};
use perovskite_core::{block_id::BlockId, coordinates::ChunkOffset, render_selector};
use anyhow::Result;
use enum_map::{enum_map, EnumMap};
use rustc_hash::FxHashMap;
use texture_packer::Rect;
use super::{RectF32, VkAllocator};
use crate::client_state::block_types::ClientBlockTypeManager;
use crate::client_state::chunk::{
ChunkDataView, LockedChunkDataView, MeshVectorReclaim, RECLAIMERS,
};
use crate::client_state::settings::GameSettings;
use crate::client_state::ClientState;
use crate::media::{load_or_generate_image, CacheManager};
use crate::vulkan::atlas::{TextureAtlas, TextureKey};
use crate::vulkan::shaders::cube_geometry::{
CubeDrawStep, CubeGeometryDrawCall, CubeGeometryVertex,
};
use crate::vulkan::shaders::raytracer::{SimpleCubeInfo, TexRef};
use crate::vulkan::shaders::{x5y5z5_pack16, VkBufferCpu, VkDrawBufferGpu};
use crate::vulkan::VulkanContext;
use perovskite_core::game_actions::ToolTarget;
use perovskite_core::protocol::game_rpc::EntityTarget;
use tracy_client::span;
use vulkano::buffer::{Buffer, BufferCreateInfo, BufferUsage, Subbuffer};
use vulkano::memory::allocator::{AllocationCreateInfo, MemoryTypeFilter};
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
#[repr(u8)]
pub(crate) enum CubeFace {
XPlus,
XMinus,
YPlus,
YMinus,
ZPlus,
ZMinus,
PlantXPlusZPlus,
PlantXPlusZMinus,
PlantXMinusZPlus,
PlantXMinusZMinus,
}
impl CubeFace {
#[inline(always)]
const fn index(&self) -> usize {
*self as usize
}
#[inline(always)]
const fn rotate_y(&self, variant: u16) -> CubeFace {
let idx = (variant % 4) as usize;
const LUT: [[usize; 4]; 10] = [
[0, 4, 1, 5],
[1, 5, 0, 4],
[2, 2, 2, 2],
[3, 3, 3, 3],
[4, 1, 5, 0],
[5, 0, 4, 1],
[6, 8, 9, 7],
[7, 6, 8, 9],
[8, 9, 7, 6],
[9, 7, 6, 8],
];
UNIFIED_FACE_ORDER[LUT[self.index()][idx]]
}
#[inline(always)]
const fn default_normal(&self) -> u16 {
match self {
CubeFace::XPlus => x5y5z5_pack16(1, 0, 0),
CubeFace::XMinus => x5y5z5_pack16(-1, 0, 0),
CubeFace::YPlus => x5y5z5_pack16(0, -1, 0),
CubeFace::YMinus => x5y5z5_pack16(0, 1, 0),
CubeFace::ZPlus => x5y5z5_pack16(0, 0, 1),
CubeFace::ZMinus => x5y5z5_pack16(0, 0, -1),
CubeFace::PlantXPlusZPlus => x5y5z5_pack16(1, 0, 1),
CubeFace::PlantXPlusZMinus => x5y5z5_pack16(1, 0, -1),
CubeFace::PlantXMinusZPlus => x5y5z5_pack16(-1, 0, 1),
CubeFace::PlantXMinusZMinus => x5y5z5_pack16(-1, 0, -1),
}
}
}
#[derive(Clone, Copy, Debug)]
struct DynamicRect {
base: RectF32,
x_selector: u32,
y_selector: u32,
x_cell_stride: f32,
y_cell_stride: f32,
x_selector_factor: f32,
y_selector_factor: f32,
flip_x_bit: u32,
flip_y_bit: u32,
extra_flip_x: bool,
extra_flip_y: bool,
transform: TextureTransform,
}
impl DynamicRect {
#[inline]
fn resolve(&self, selector: u32) -> RectF32 {
let x_min = self.base.l() + (selector & self.x_selector) as f32 * self.x_selector_factor;
let y_min = self.base.t() + (selector & self.y_selector) as f32 * self.y_selector_factor;
let (real_xmin, real_xstride) = if ((selector & self.flip_x_bit) != 0) ^ self.extra_flip_x {
(x_min + self.x_cell_stride, -self.x_cell_stride)
} else {
(x_min, self.x_cell_stride)
};
let (real_ymin, real_ystride) = if ((selector & self.flip_y_bit) != 0) ^ self.extra_flip_y {
(y_min + self.y_cell_stride, -self.y_cell_stride)
} else {
(y_min, self.y_cell_stride)
};
RectF32::new(real_xmin, real_ymin, real_xstride, real_ystride)
.with_transform(self.transform)
}
}
const DEFAULT_FACE_NORMALS: [(i8, i8, i8); 10] = [
(1, 0, 0),
(-1, 0, 0),
(0, 1, 0),
(0, -1, 0),
(0, 0, 1),
(0, 0, -1),
(1, 0, 1),
(1, 0, -1),
(-1, 0, 1),
(-1, 0, -1),
];
#[derive(Clone, Copy, Debug)]
pub struct CubeExtents {
pub verts: [Vector3<f32>; 8],
pub adjacency: [(i8, i8, i8); 10],
pub force: [bool; 6],
pub top_normal: u16,
pub top_tan: u8,
}
impl CubeExtents {
pub const fn new(x: (f32, f32), y: (f32, f32), z: (f32, f32)) -> Self {
Self {
verts: [
vec3(x.0, y.0, z.0),
vec3(x.0, y.0, z.1),
vec3(x.0, y.1, z.0),
vec3(x.0, y.1, z.1),
vec3(x.1, y.0, z.0),
vec3(x.1, y.0, z.1),
vec3(x.1, y.1, z.0),
vec3(x.1, y.1, z.1),
],
adjacency: DEFAULT_FACE_NORMALS,
force: [false; 6],
top_normal: CubeFace::YPlus.default_normal(),
top_tan: 1,
}
}
pub fn top_norm(self) -> u16 {
self.top_normal
}
pub fn top_tan(self) -> u8 {
self.top_tan
}
#[inline]
pub fn rotate_y(self, variant: u16) -> CubeExtents {
let mut vertices = self.verts;
let mut adjacency = self.adjacency;
for vtx in &mut vertices {
*vtx = Vector3::from(rotate_y((vtx.x, vtx.y, vtx.z), variant));
}
for neighbor in &mut adjacency {
*neighbor = rotate_y(*neighbor, variant);
}
let force_swizzled = match variant % 4 {
0 => self.force,
1 => [
self.force[5],
self.force[4],
self.force[2],
self.force[3],
self.force[0],
self.force[1],
],
2 => [
self.force[1],
self.force[0],
self.force[2],
self.force[3],
self.force[5],
self.force[4],
],
3 => [
self.force[4],
self.force[5],
self.force[2],
self.force[3],
self.force[1],
self.force[0],
],
_ => unreachable!(),
};
Self {
verts: vertices,
adjacency,
force: force_swizzled,
top_normal: self.top_normal,
top_tan: (self.top_tan.saturating_add(variant as u8)) & 3,
}
}
#[inline]
fn force_face(&self, i: usize) -> bool {
self.force[i]
}
fn warp_top_inplace(&mut self, top_slope_x: f32, top_slope_z: f32) {
for idx in [0, 1, 4, 5] {
self.verts[idx].y += top_slope_x * self.verts[idx].x + top_slope_z * self.verts[idx].z;
}
}
fn warp_bottom_inplace(&mut self, bottom_slope_x: f32, bottom_slope_z: f32) {
for idx in [2, 3, 6, 7] {
self.verts[idx].y +=
bottom_slope_x * self.verts[idx].x + bottom_slope_z * self.verts[idx].z;
}
}
}
#[inline]
pub fn rotate_y<T: Num + std::ops::Neg<Output = T>>(
c: (T, T, T),
angle_90_deg_units: u16,
) -> (T, T, T) {
match angle_90_deg_units % 4 {
0 => c,
1 => (c.2, c.1, -c.0),
2 => (-c.0, c.1, -c.2),
3 => (-c.2, c.1, c.0),
_ => unreachable!(),
}
}
#[allow(dead_code)]
pub fn rotate_x<T: Num + std::ops::Neg<Output = T>>(
c: (T, T, T),
angle_90_deg_units: u16,
) -> (T, T, T) {
match angle_90_deg_units % 4 {
0 => c,
1 => (c.0, -c.2, c.1),
2 => (c.0, -c.1, -c.2),
3 => (c.0, c.2, -c.1),
_ => unreachable!(),
}
}
#[allow(dead_code)]
pub fn rotate_z<T: Num + std::ops::Neg<Output = T>>(
c: (T, T, T),
angle_90_deg_units: u16,
) -> (T, T, T) {
match angle_90_deg_units % 4 {
0 => c,
1 => (c.1, -c.0, c.2),
2 => (-c.0, -c.1, c.2),
3 => (-c.1, c.0, c.2),
_ => unreachable!(),
}
}
const CUBE_EXTENTS_FACE_ORDER: [CubeFace; 6] = [
CubeFace::XPlus,
CubeFace::XMinus,
CubeFace::YPlus,
CubeFace::YMinus,
CubeFace::ZPlus,
CubeFace::ZMinus,
];
const PLANTLIKE_FACE_ORDER: [CubeFace; 4] = [
CubeFace::PlantXPlusZPlus,
CubeFace::PlantXPlusZMinus,
CubeFace::PlantXMinusZPlus,
CubeFace::PlantXMinusZMinus,
];
const UNIFIED_FACE_ORDER: [CubeFace; 10] = [
CubeFace::XPlus,
CubeFace::XMinus,
CubeFace::YPlus,
CubeFace::YMinus,
CubeFace::ZPlus,
CubeFace::ZMinus,
CubeFace::PlantXPlusZPlus,
CubeFace::PlantXPlusZMinus,
CubeFace::PlantXMinusZPlus,
CubeFace::PlantXMinusZMinus,
];
#[derive(Clone)]
pub(crate) struct VkChunkVertexDataGpu {
pub(crate) draw_buffers: EnumMap<CubeDrawStep, Option<VkDrawBufferGpu<CubeGeometryVertex>>>,
}
static RAYTRACE_CHUNK_VERSION_COUNTER: AtomicUsize = AtomicUsize::new(1);
#[derive(Clone)]
pub(crate) struct VkChunkRaytraceData {
pub(crate) blocks: Option<Box<[u32; PADDED_CHUNK_VOLUME]>>,
pub(crate) _version: usize,
}
#[derive(Clone, PartialEq)]
pub(crate) struct VkChunkVertexDataCpu {
pub(crate) draw_buffers: EnumMap<CubeDrawStep, Option<VkBufferCpu<CubeGeometryVertex>>>,
}
impl VkChunkVertexDataCpu {
pub(crate) fn to_gpu(&self, allocator: Arc<VkAllocator>) -> Result<VkChunkVertexDataGpu> {
let work = move |x: &VkBufferCpu<CubeGeometryVertex>| x.to_gpu(allocator.clone());
Ok(VkChunkVertexDataGpu {
draw_buffers: enum_map! {
step => self.draw_buffers[step].as_ref().map(&work).transpose()?.flatten()
},
})
}
}
fn get_selector_shift(bits: u32) -> u32 {
let leading_zeros = bits.leading_zeros();
let trailing_zeros = bits.trailing_zeros();
let ones = bits.count_ones();
if (leading_zeros + trailing_zeros + ones) != 32 {
log::warn!("Selector with non-contiguous bits: {:b}", bits);
}
1 << trailing_zeros
}
#[inline]
fn get_texture(
texture_coords: &FxHashMap<TextureKey, Rect>,
tex: Option<&TextureReference>,
) -> MdrWithFlags {
let crop = tex.and_then(|tex| tex.crop.as_ref());
let transform = tex
.map(|t| TextureTransform::try_from(t.texture_transform).unwrap_or(TextureTransform::None))
.unwrap_or(TextureTransform::None);
let mut result = make_maybe_dynamic(
tex.and_then(|tex| texture_coords.get(&TextureKey::from(tex)).copied())
.unwrap_or_else(|| *texture_coords.get(&TextureKey::FallbackUnknownTex).unwrap()),
crop,
);
if transform != TextureTransform::None {
if let Some(tex) = tex {
if !tex.normal_map.is_empty() {
log::warn!(
"Texture \"{}\" has a normal map and a texture transform; the normals in \
the normal map will NOT be automatically adjusted to match the new \
orientation",
tex.diffuse
);
}
}
result = match result {
MaybeDynamicRect::Static(r) => MaybeDynamicRect::Static(r.with_transform(transform)),
MaybeDynamicRect::Dynamic(mut d) => {
d.transform = transform;
MaybeDynamicRect::Dynamic(d)
}
};
}
MdrWithFlags {
rect: result,
tex_flags: tex.map(|t| t.flags).unwrap_or(0),
}
}
fn make_maybe_dynamic(rect: Rect, crop: Option<&TextureCrop>) -> MaybeDynamicRect {
let mut rect_f = RectF32::new(rect.x as f32, rect.y as f32, rect.w as f32, rect.h as f32);
if let Some(crop) = crop {
rect_f = crop_texture(crop, rect_f);
if let Some(dynamic) = crop.dynamic.as_ref() {
let x_selector_shift_factor = get_selector_shift(dynamic.x_selector_bits);
let y_selector_shift_factor = get_selector_shift(dynamic.y_selector_bits);
return MaybeDynamicRect::Dynamic(DynamicRect {
base: rect_f,
x_selector: dynamic.x_selector_bits,
y_selector: dynamic.y_selector_bits,
x_cell_stride: rect_f.w() / (dynamic.x_cells as f32),
y_cell_stride: rect_f.h() / (dynamic.y_cells as f32),
x_selector_factor: rect_f.w()
/ (dynamic.x_cells as f32 * x_selector_shift_factor as f32),
y_selector_factor: rect_f.h()
/ (dynamic.y_cells as f32 * y_selector_shift_factor as f32),
flip_x_bit: dynamic.flip_x_bit,
flip_y_bit: dynamic.flip_y_bit,
extra_flip_x: dynamic.extra_flip_x,
extra_flip_y: dynamic.extra_flip_y,
transform: TextureTransform::None,
});
}
}
MaybeDynamicRect::Static(rect_f)
}
fn crop_texture(crop: &TextureCrop, r: RectF32) -> RectF32 {
RectF32::new(
r.l() + (crop.left * r.w()),
r.t() + (crop.top * r.h()),
r.w() * (crop.right - crop.left),
r.h() * (crop.bottom - crop.top),
)
}
pub(crate) struct BlockRenderer {
block_defs: Arc<ClientBlockTypeManager>,
texture_atlas: TextureAtlas,
selection_box_tex_coord: FlaggedRectF32,
fallback_tex_coord: FlaggedRectF32,
simple_block_tex_coords: SimpleTexCoordCache,
axis_aligned_box_blocks: AxisAlignedBoxBlocksCache,
vk_ctx: Arc<VulkanContext>,
raytrace_control_ssbo: Subbuffer<[SimpleCubeInfo]>,
}
impl BlockRenderer {
pub(crate) fn raytrace_control_ssbo(&self) -> Subbuffer<[SimpleCubeInfo]> {
self.raytrace_control_ssbo.clone()
}
}
lazy_static::lazy_static! {
static ref DEBUG_AABB_1_TEX: TextureReference = TextureReference {
diffuse: GENERATED_TEXTURE_CATEGORY_SOLID_FROM_CSS.to_string() + "magenta",
normal_map: "".to_string(),
rt_specular: "".to_string(),
emissive: "".to_string(),
alt_diffuse: "".to_string(),
crop: None,
texture_transform: 0,
flags: (perovskite_core::protocol::render::TextureFlags::Walltiles1x as u32),
};
static ref DEBUG_AABB_2_TEX: TextureReference = TextureReference {
diffuse: GENERATED_TEXTURE_CATEGORY_SOLID_FROM_CSS.to_string() + "cyan",
normal_map: "".to_string(),
rt_specular: "".to_string(),
emissive: "".to_string(),
alt_diffuse: "".to_string(),
crop: None,
texture_transform: 0,
flags: (perovskite_core::protocol::render::TextureFlags::Walltiles1x as u32),
};
}
impl From<texture_packer::Rect> for FlaggedRectF32 {
fn from(r: texture_packer::Rect) -> Self {
FlaggedRectF32 {
rect: r.into(),
flags: 0,
}
}
}
impl BlockRenderer {
pub(crate) async fn new(
block_type_manager: Arc<ClientBlockTypeManager>,
cache_manager: &mut CacheManager,
ctx: Arc<VulkanContext>,
settings: Arc<ArcSwap<GameSettings>>,
) -> Result<BlockRenderer> {
let include_debug_aabbs = settings.load().render.debug_show_invisible_aa_boxes;
let mut fetch_textures = HashSet::new();
let mut pack_textures: HashSet<TextureKey> = HashSet::new();
if include_debug_aabbs {
pack_textures.insert(TextureKey::from(&*DEBUG_AABB_1_TEX));
pack_textures.insert(TextureKey::from(&*DEBUG_AABB_2_TEX));
}
let mut insert_if_present = |tex: &Option<TextureReference>| {
if let Some(tex) = tex {
fetch_textures.insert(tex.diffuse.clone());
if !tex.rt_specular.is_empty() {
fetch_textures.insert(tex.rt_specular.clone());
}
if !tex.emissive.is_empty() {
fetch_textures.insert(tex.emissive.clone());
}
if !tex.normal_map.is_empty() {
fetch_textures.insert(tex.normal_map.clone());
}
if !tex.alt_diffuse.is_empty() {
fetch_textures.insert(tex.alt_diffuse.clone());
}
pack_textures.insert(tex.into());
}
};
if include_debug_aabbs {
insert_if_present(&Some(DEBUG_AABB_1_TEX.clone()));
insert_if_present(&Some(DEBUG_AABB_2_TEX.clone()));
}
for def in block_type_manager.all_block_defs() {
match &def.render_info {
Some(RenderInfo::Cube(cube)) => {
insert_if_present(&cube.tex_back);
insert_if_present(&cube.tex_front);
insert_if_present(&cube.tex_left);
insert_if_present(&cube.tex_right);
insert_if_present(&cube.tex_top);
insert_if_present(&cube.tex_bottom);
}
Some(RenderInfo::PlantLike(plant_like)) => insert_if_present(&plant_like.tex),
Some(RenderInfo::AxisAlignedBoxes(aa_boxes)) => {
for aa_box in &aa_boxes.boxes {
insert_if_present(&aa_box.tex_back);
insert_if_present(&aa_box.tex_front);
insert_if_present(&aa_box.tex_left);
insert_if_present(&aa_box.tex_right);
insert_if_present(&aa_box.tex_top);
insert_if_present(&aa_box.tex_bottom);
insert_if_present(&aa_box.plant_like_tex);
}
}
Some(RenderInfo::Empty(_)) => {}
None => {
log::warn!("Got a block without renderinfo: {}", def.short_name)
}
}
}
let mut fetched_textures = FxHashMap::default();
for texture_name in fetch_textures {
let texture = load_or_generate_image(cache_manager, &texture_name).await?;
fetched_textures.insert(texture_name, texture);
}
let texture_atlas = TextureAtlas::new(&ctx, pack_textures, fetched_textures)?;
let simple_block_tex_coords = SimpleTexCoordCache {
blocks: block_type_manager
.block_defs()
.iter()
.map(|x| {
x.as_ref()
.and_then(|x| build_simple_cache_entry(x, &texture_atlas.texel_coords))
})
.collect(),
};
let iterator = block_type_manager
.block_defs()
.iter()
.map(|x| match x.as_ref() {
None => SimpleCubeInfo {
flags: 0.into(),
tex: [TexRef::default(); 6],
},
Some(x) => build_ssbo_entry(
&x,
&texture_atlas.texel_coords,
&block_type_manager,
texture_atlas.width,
texture_atlas.height,
),
});
let raytrace_control_ssbo =
ctx.iter_to_device_via_staging(iterator, BufferUsage::STORAGE_BUFFER)?;
let axis_aligned_box_blocks = AxisAlignedBoxBlocksCache {
blocks: block_type_manager
.block_defs()
.iter()
.map(|x| {
x.as_ref().and_then(|x| {
build_axis_aligned_box_cache_entry(
x,
&texture_atlas.texel_coords,
include_debug_aabbs,
)
})
})
.collect(),
};
let selection_rect: FlaggedRectF32 = texture_atlas
.texel_coords
.get(&TextureKey::SelectionRectangle)
.copied()
.unwrap()
.into();
let fallback_rect: FlaggedRectF32 = texture_atlas
.texel_coords
.get(&TextureKey::SelectionRectangle)
.copied()
.unwrap()
.into();
Ok(BlockRenderer {
block_defs: block_type_manager,
texture_atlas,
selection_box_tex_coord: selection_rect,
fallback_tex_coord: fallback_rect,
simple_block_tex_coords,
axis_aligned_box_blocks,
raytrace_control_ssbo,
vk_ctx: ctx,
})
}
pub(crate) fn block_types(&self) -> &ClientBlockTypeManager {
&self.block_defs
}
pub(crate) fn atlas(&self) -> &TextureAtlas {
&self.texture_atlas
}
pub(crate) fn clone_vk_allocator(&self) -> Arc<VkAllocator> {
self.vk_ctx.clone_allocator()
}
pub(crate) fn vk_ctx(&self) -> &VulkanContext {
&self.vk_ctx
}
pub(crate) fn clone_vk_ctx(&self) -> Arc<VulkanContext> {
self.vk_ctx.clone()
}
pub(crate) fn build_raytrace_data(
&self,
block_ids: &[BlockId; PADDED_CHUNK_VOLUME],
) -> Option<VkChunkRaytraceData> {
let _span = span!("build_raytrace_data");
if !block_ids
.iter()
.any(|x| self.block_defs.is_raytrace_present(*x))
{
return None;
}
let max_block_id = ((self.raytrace_control_ssbo.len() as u32) << 12) - 1;
let blocks = if block_ids
.iter()
.any(|x| x.0 > max_block_id || !self.block_defs.is_raytrace_present(*x))
{
Some(Box::new(block_ids.map(|x| {
if x.0 > max_block_id {
0
} else if !self.block_defs.is_raytrace_present(x) {
0
} else {
x.0
}
})))
} else {
None
};
Some(VkChunkRaytraceData {
blocks,
_version: RAYTRACE_CHUNK_VERSION_COUNTER.fetch_add(1, Ordering::Relaxed),
})
}
pub(crate) fn mesh_chunk(
&self,
chunk_data: &LockedChunkDataView,
) -> Result<VkChunkVertexDataCpu> {
let _span = span!("meshing");
Ok(VkChunkVertexDataCpu {
draw_buffers: enum_map! {
CubeDrawStep::OpaqueSimple => self.mesh_chunk_subpass(
chunk_data,
|id| self.block_types().is_opaque_nonspecular(id),
|block, neighbor| {
if self.block_defs.is_solid_opaque(neighbor) {
return true;
}
(self
.block_defs
.allow_face_suppress_on_same_base_block(block)
&& block.equals_ignore_variant(neighbor))
|| (self.block_defs.allow_face_suppress_on_exact_match(block)
&& block == neighbor)
},
&RECLAIMERS[CubeDrawStep::OpaqueSimple],
),
CubeDrawStep::OpaqueSpecular => self.mesh_chunk_subpass(
chunk_data,
|id| self.block_types().is_opaque_specular(id),
|block, neighbor| {
if self.block_defs.is_solid_opaque(neighbor) {
return true;
}
(self
.block_defs
.allow_face_suppress_on_same_base_block(block)
&& block.equals_ignore_variant(neighbor))
|| (self.block_defs.allow_face_suppress_on_exact_match(block)
&& block == neighbor)
},
&RECLAIMERS[CubeDrawStep::OpaqueSpecular],
),
CubeDrawStep::Transparent => self.mesh_chunk_subpass(
chunk_data,
|block| self.block_defs.is_transparent_render(block),
|block, neighbor| {
block.equals_ignore_variant(neighbor)
|| self.block_defs.is_solid_opaque(neighbor)
},
&RECLAIMERS[CubeDrawStep::Transparent],
),
CubeDrawStep::Translucent => self.mesh_chunk_subpass(
chunk_data,
|block| self.block_defs.is_translucent_render(block),
|block, neighbor| {
block.equals_ignore_variant(neighbor)
|| self.block_defs.is_solid_opaque(neighbor)
},
&RECLAIMERS[CubeDrawStep::Translucent],
),
CubeDrawStep::RaytraceFallback => self.mesh_chunk_subpass(
chunk_data,
|block| self.block_defs.is_raytrace_fallback_render(block),
|_, _| false,
&RECLAIMERS[CubeDrawStep::RaytraceFallback],
),
CubeDrawStep::TransparentSpecular => self.mesh_chunk_subpass(
chunk_data,
|block| self.block_defs.is_transparent_with_specular(block),
|block, neighbor| {
block.equals_ignore_variant(neighbor)
|| self.block_defs.is_solid_opaque(neighbor)
},
&RECLAIMERS[CubeDrawStep::TransparentSpecular],
),
},
})
}
pub(crate) fn mesh_chunk_subpass<F, G>(
&self,
chunk_data: &LockedChunkDataView,
include_block_when: F,
suppress_face_when: G,
reclaimer: &MeshVectorReclaim,
) -> Option<VkBufferCpu<CubeGeometryVertex>>
where
F: Fn(BlockId) -> bool,
G: Fn(BlockId, BlockId) -> bool,
{
let _span = span!("mesh subpass");
let (mut idx, mut vtx) = match reclaimer.take() {
Some((idx, vtx)) => (idx, vtx),
None => (Vec::new(), Vec::new()),
};
for x in 0..CHUNK_SIZE_U8 {
for z in 0..CHUNK_SIZE_U8 {
for y in 0..CHUNK_SIZE_U8 {
let offset = ChunkOffset { x, y, z };
let id = self.get_block_id(chunk_data.block_ids(), offset);
if include_block_when(id) {
self.render_single_block(
self.block_types()
.get_blockdef(id)
.unwrap_or_else(|| self.block_defs.get_fallback_blockdef()),
id,
offset,
chunk_data,
&mut vtx,
&mut idx,
&suppress_face_when,
);
}
}
}
}
if vtx.is_empty() {
None
} else {
Some(VkBufferCpu { vtx, idx })
}
}
pub(crate) fn render_single_block<G>(
&self,
block: &BlockTypeDef,
id: BlockId,
offset: ChunkOffset,
chunk_data: &impl ChunkDataView,
vtx: &mut Vec<CubeGeometryVertex>,
idx: &mut Vec<u32>,
suppress_face_when: &G,
) where
G: Fn(BlockId, BlockId) -> bool,
{
match &block.render_info {
Some(RenderInfo::Cube(cube_render_info)) => {
self.emit_full_cube(
id,
offset,
chunk_data,
vtx,
idx,
cube_render_info,
suppress_face_when,
);
}
Some(RenderInfo::PlantLike(plantlike_render_info)) => self.emit_plantlike(
block,
id,
offset,
chunk_data,
vtx,
idx,
plantlike_render_info,
),
Some(RenderInfo::AxisAlignedBoxes(render_info)) => {
self.emit_axis_aligned_boxes(block, id, offset, chunk_data, vtx, idx, render_info)
}
_ => (),
}
}
fn emit_full_cube<F>(
&self,
id: BlockId,
offset: ChunkOffset,
chunk_data: &impl ChunkDataView,
vtx: &mut Vec<CubeGeometryVertex>,
idx: &mut Vec<u32>,
render_info: &CubeRenderInfo,
suppress_face_when: F,
) where
F: Fn(BlockId, BlockId) -> bool,
{
let (e, rotator) = match render_info.variant_effect() {
CubeVariantEffect::None => (FULL_CUBE_EXTENTS, 0),
CubeVariantEffect::RotateNesw => {
(FULL_CUBE_EXTENTS.rotate_y(id.variant()), id.variant())
}
CubeVariantEffect::Liquid => (build_liquid_cube_extents(chunk_data, offset, id), 0),
CubeVariantEffect::CubeVariantHeight => {
(cube_variant_height_unblended(id.variant()), 0)
}
};
let pos = vec3(offset.x.into(), offset.y.into(), offset.z.into());
let textures = self
.simple_block_tex_coords
.get(id)
.unwrap_or([self.fallback_tex_coord; 6]);
self.emit_single_cube_impl(
e,
rotator,
offset,
suppress_face_when,
id,
chunk_data,
pos,
textures,
vtx,
idx,
);
}
#[inline]
fn emit_single_cube_impl<F>(
&self,
e: CubeExtents,
rotator: u16,
offset: ChunkOffset,
suppress_face_when: F,
id: BlockId,
chunk_data: &impl ChunkDataView,
pos: Vector3<f32>,
textures: [FlaggedRectF32; 6],
vtx: &mut Vec<CubeGeometryVertex>,
idx: &mut Vec<u32>,
) where
F: Fn(BlockId, BlockId) -> bool,
{
for i in 0..6 {
let (n_x, n_y, n_z) = e.adjacency[i];
let neighbor_index = (
offset.x as i8 + n_x,
offset.y as i8 + n_y,
offset.z as i8 + n_z,
)
.as_padded_index();
if e.force_face(i) || !suppress_face_when(id, chunk_data.block_ids()[neighbor_index]) {
emit_cube_face_vk(
pos,
textures[i],
CUBE_EXTENTS_FACE_ORDER[i],
CUBE_EXTENTS_FACE_ORDER[i].rotate_y(4 - (rotator & 3)),
vtx,
idx,
e,
chunk_data.lightmap()[neighbor_index],
0,
id.variant(),
);
}
}
}
fn emit_plantlike(
&self,
_block: &BlockTypeDef,
id: BlockId,
offset: ChunkOffset,
chunk_data: &impl ChunkDataView,
vtx: &mut Vec<CubeGeometryVertex>,
idx: &mut Vec<u32>,
plantlike_render_info: &blocks_proto::PlantLikeRenderInfo,
) {
let e = FULL_CUBE_EXTENTS;
let pos = vec3(offset.x.into(), offset.y.into(), offset.z.into());
let tex = match self.simple_block_tex_coords.get_zero(id) {
Some(x) => x,
None => self.fallback_tex_coord,
};
vtx.reserve(8);
idx.reserve(24);
for face in PLANTLIKE_FACE_ORDER {
emit_cube_face_vk(
pos,
tex,
face,
face,
vtx,
idx,
e,
chunk_data.lightmap()[offset.as_padded_index()],
(plantlike_render_info.wave_effect_scale * 255.0).clamp(0.0, 255.0) as u8,
id.variant(),
);
}
}
fn emit_axis_aligned_boxes(
&self,
_block: &BlockTypeDef,
id: BlockId,
offset: ChunkOffset,
chunk_data: &impl ChunkDataView,
vtx: &mut Vec<CubeGeometryVertex>,
idx: &mut Vec<u32>,
_render_info: &AxisAlignedBoxes,
) {
let aabb_data = self.axis_aligned_box_blocks.get(id);
if aabb_data.is_none() {
return;
}
let pos = vec3(offset.x.into(), offset.y.into(), offset.z.into());
let aabb_data = aabb_data.unwrap();
let mut selector = render_selector::from_variant(id.variant());
if aabb_data.needs_neighbor_bits {
selector |= self.compute_neighbor_bits(id, offset, chunk_data);
}
for aabb in aabb_data.boxes.iter() {
if aabb.mask & selector == 0 {
continue;
}
let mut e = aabb.extents;
let rotation = match aabb.rotation {
AabbRotation::None => 0,
AabbRotation::Nesw => id.variant() % 4,
};
e = e.rotate_y(rotation);
match aabb.textures {
CachedAabbTextures::Prism(textures) => {
for i in 0..6 {
let (n_x, n_y, n_z) = e.adjacency[i];
let neighbor_index = (
offset.x as i8 + n_x,
offset.y as i8 + n_y,
offset.z as i8 + n_z,
)
.as_padded_index();
emit_cube_face_vk(
pos,
textures[i].rect(selector),
CUBE_EXTENTS_FACE_ORDER[i],
CUBE_EXTENTS_FACE_ORDER[i].rotate_y(rotation),
vtx,
idx,
e,
max_brightness(
chunk_data.lightmap()[neighbor_index],
chunk_data.lightmap()[offset.as_padded_index()],
),
0,
id.variant(),
);
}
}
CachedAabbTextures::Plantlike(plantlike) => {
for i in 0..4 {
emit_cube_face_vk(
pos,
plantlike.rect(selector),
PLANTLIKE_FACE_ORDER[i],
PLANTLIKE_FACE_ORDER[i].rotate_y(rotation),
vtx,
idx,
e,
chunk_data.lightmap()[offset.as_padded_index()],
0,
id.variant(),
);
}
}
}
}
}
fn compute_neighbor_bits(
&self,
id: BlockId,
offset: ChunkOffset,
chunk_data: &impl ChunkDataView,
) -> u32 {
let mut bits = 0;
for (i, (dx, dy, dz)) in render_selector::NEIGHBOR_OFFSETS.iter().enumerate() {
let neighbor_index = (
offset.x as i8 + dx,
offset.y as i8 + dy,
offset.z as i8 + dz,
)
.as_padded_index();
let neighbor = chunk_data.block_ids()[neighbor_index];
if self.block_defs.neighbor_present_for_selector(id, neighbor) {
bits |= render_selector::NEIGHBOR_XPLUS << i;
}
}
bits
}
fn get_block_id(&self, ids: &[BlockId; PADDED_CHUNK_VOLUME], coord: ChunkOffset) -> BlockId {
ids[coord.as_padded_index()]
}
pub(crate) fn make_pointee_cube(
&self,
player_position: Vector3<f64>,
pointee: ToolTarget,
state: &ClientState,
tick: u64,
) -> Result<Option<CubeGeometryDrawCall>> {
let mut vtx = vec![];
let mut idx = vec![];
let frame = self.selection_box_tex_coord;
const POINTEE_SELECTION_EXTENTS: CubeExtents =
CubeExtents::new((-0.51, 0.51), (-0.51, 0.51), (-0.51, 0.51));
let e = POINTEE_SELECTION_EXTENTS;
let vk_pos = Vector3::zero();
for &face in &CUBE_EXTENTS_FACE_ORDER {
emit_cube_face_vk(vk_pos, frame, face, face, &mut vtx, &mut idx, e, 0x0f, 0, 0);
}
let vtx = Buffer::from_iter(
self.vk_ctx.clone_allocator(),
BufferCreateInfo {
usage: BufferUsage::VERTEX_BUFFER,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::HOST_SEQUENTIAL_WRITE
| MemoryTypeFilter::PREFER_DEVICE,
..Default::default()
},
vtx.into_iter(),
)?;
let idx = Buffer::from_iter(
self.vk_ctx.clone_allocator(),
BufferCreateInfo {
usage: BufferUsage::INDEX_BUFFER,
..Default::default()
},
AllocationCreateInfo {
memory_type_filter: MemoryTypeFilter::HOST_SEQUENTIAL_WRITE
| MemoryTypeFilter::PREFER_DEVICE,
..Default::default()
},
idx.into_iter(),
)?;
let model_matrix = match pointee {
ToolTarget::Block(pointee) => {
let translation = (vec3(pointee.x as f64, pointee.y as f64, pointee.z as f64)
- player_position)
.mul_element_wise(Vector3::new(1., -1., 1.));
Matrix4::from_translation(translation.cast().unwrap())
}
ToolTarget::Entity(EntityTarget {
entity_id,
trailing_entity_index,
}) => {
let task = || -> Option<Matrix4<f32>> {
let (transformation, class) = state
.entities
.lock()
.transforms_for_entity(
player_position,
tick,
&state.entity_renderer,
entity_id,
)
.map(|x| x.skip(trailing_entity_index as usize).next())
.flatten()?;
let (min, max) = state.entity_renderer.mesh_aabb(class)?;
let range = max - min;
let center = (min + max) / 2.0;
let translation = vec3(center.x, center.y, center.z);
let prescale = Matrix4::from_nonuniform_scale(range.x, range.y, range.z);
Some(transformation * Matrix4::from_translation(translation) * prescale)
};
match task() {
Some(x) => x,
None => return Ok(None),
}
}
};
let buffer = VkDrawBufferGpu::<CubeGeometryVertex> {
num_indices: idx.len() as u32,
vtx,
idx,
};
Ok(Some(CubeGeometryDrawCall {
model_matrix,
models: VkChunkVertexDataGpu {
draw_buffers: enum_map! {
CubeDrawStep::OpaqueSimple => None,
CubeDrawStep::OpaqueSpecular => None,
CubeDrawStep::Transparent => Some(buffer.clone()),
CubeDrawStep::Translucent => None,
CubeDrawStep::RaytraceFallback => Some(buffer.clone()),
CubeDrawStep::TransparentSpecular => None,
},
},
}))
}
}
fn build_axis_aligned_box_cache_entry(
x: &BlockTypeDef,
texture_coords: &FxHashMap<TextureKey, Rect>,
include_debug_aabbs: bool,
) -> Option<CachedAabbBlockEntry> {
if let Some(RenderInfo::AxisAlignedBoxes(aa_boxes)) = &x.render_info {
let mut result = Vec::new();
for (i, aa_box) in aa_boxes.boxes.iter().enumerate() {
let cached_box = make_cached_aabox(x, texture_coords, i, aa_box, None);
result.push(cached_box);
}
if include_debug_aabbs {
if let Some(PhysicsInfo::SolidCustomCollisionboxes(aa_boxes)) = &x.physics_info {
for (i, aa_box) in aa_boxes.boxes.iter().enumerate() {
let cached_box = make_cached_aabox(
x,
texture_coords,
i,
aa_box,
Some(DEBUG_AABB_1_TEX.clone()),
);
result.push(cached_box);
}
}
if let Some(aa_boxes) = &x.tool_custom_hitbox {
for (i, aa_box) in aa_boxes.boxes.iter().enumerate() {
let cached_box = make_cached_aabox(
x,
texture_coords,
i,
aa_box,
Some(DEBUG_AABB_2_TEX.clone()),
);
result.push(cached_box);
}
}
}
let needs_neighbor_bits = result
.iter()
.any(|b| b.mask != u32::MAX && b.mask & render_selector::ALL_NEIGHBOR_BITS != 0);
Some(CachedAabbBlockEntry {
boxes: result.into_boxed_slice(),
needs_neighbor_bits,
})
} else {
None
}
}
fn make_cached_aabox(
x: &BlockTypeDef,
texture_coords: &FxHashMap<TextureKey, Rect>,
i: usize,
aa_box: &blocks_proto::AxisAlignedBox,
texture_override: Option<TextureReference>,
) -> CachedAxisAlignedBox {
let mut extents = CubeExtents::new(
(aa_box.x_min, aa_box.x_max),
(-aa_box.y_max, -aa_box.y_min),
(aa_box.z_min, aa_box.z_max),
);
extents.warp_top_inplace(-aa_box.top_slope_x, -aa_box.top_slope_z);
extents.warp_bottom_inplace(-aa_box.bottom_slope_x, -aa_box.bottom_slope_z);
let get_texture_with_override = move |tk: Option<&TextureReference>| match &texture_override {
Some(override_tex) => get_texture(texture_coords, Some(override_tex)),
None => get_texture(texture_coords, tk),
};
let textures = if let Some(plantlike) = aa_box.plant_like_tex.as_ref() {
CachedAabbTextures::Plantlike(get_texture_with_override(Some(plantlike)))
} else {
CachedAabbTextures::Prism([
get_texture_with_override(aa_box.tex_right.as_ref()),
get_texture_with_override(aa_box.tex_left.as_ref()),
get_texture_with_override(aa_box.tex_top.as_ref()),
get_texture_with_override(aa_box.tex_bottom.as_ref()),
get_texture_with_override(aa_box.tex_back.as_ref()),
get_texture_with_override(aa_box.tex_front.as_ref()),
])
};
if aa_box.variant_mask & render_selector::VALID_MASK_BITS != aa_box.variant_mask {
log::warn!(
"Block {} box {} had bad variant mask: {:x}",
x.short_name,
i,
aa_box.variant_mask
);
}
let mask = match aa_box.variant_mask & render_selector::VALID_MASK_BITS {
0 => u32::MAX,
masked => masked,
};
let cached_box = CachedAxisAlignedBox {
extents,
textures,
rotation: match aa_box.rotation() {
blocks_proto::AxisAlignedBoxRotation::None => AabbRotation::None,
blocks_proto::AxisAlignedBoxRotation::Nesw => AabbRotation::Nesw,
},
mask,
};
cached_box
}
pub(crate) mod rt_flags {
pub(crate) const FLAG_BLOCK_PRESENT: u32 = 1;
pub(crate) const FLAG_BLOCK_ROTATE_NESW: u32 = 2;
pub(crate) const FLAG_BLOCK_FALLBACK: u32 = 4;
}
fn build_simple_cache_entry(
block_def: &BlockTypeDef,
texture_coords: &FxHashMap<TextureKey, Rect>,
) -> Option<SimpleTexCoordEntry> {
match &block_def.render_info {
Some(RenderInfo::Cube(render_info)) => Some(SimpleTexCoordEntry {
coords: get_cube_coords(texture_coords, render_info),
}),
Some(RenderInfo::PlantLike(render_info)) => Some(SimpleTexCoordEntry {
coords: [get_texture(texture_coords, render_info.tex.as_ref()); 6],
}),
_ => None,
}
}
#[inline]
fn get_cube_coords(
tex_coords: &FxHashMap<TextureKey, Rect>,
render_info: &CubeRenderInfo,
) -> [MdrWithFlags; 6] {
[
get_texture(tex_coords, render_info.tex_right.as_ref()),
get_texture(tex_coords, render_info.tex_left.as_ref()),
get_texture(tex_coords, render_info.tex_top.as_ref()),
get_texture(tex_coords, render_info.tex_bottom.as_ref()),
get_texture(tex_coords, render_info.tex_back.as_ref()),
get_texture(tex_coords, render_info.tex_front.as_ref()),
]
}
fn build_ssbo_entry(
block_def: &BlockTypeDef,
texture_coords: &FxHashMap<TextureKey, Rect>,
block_type_manager: &ClientBlockTypeManager,
w: u32,
h: u32,
) -> SimpleCubeInfo {
use rt_flags::*;
match &block_def.render_info {
Some(RenderInfo::Cube(render_info)) => {
let mut flags = FLAG_BLOCK_PRESENT;
if render_info.variant_effect() == CubeVariantEffect::RotateNesw {
flags |= FLAG_BLOCK_ROTATE_NESW;
}
if block_type_manager.is_raytrace_fallback_render(BlockId(block_def.id)) {
flags |= FLAG_BLOCK_FALLBACK;
}
let coords = get_cube_coords(texture_coords, render_info);
SimpleCubeInfo {
flags: flags.into(),
tex: coords.map(|x| x.rect(0).rect.rt_texref((w, h))),
}
}
Some(RenderInfo::PlantLike(render_info)) => {
let coords = get_texture(texture_coords, render_info.tex.as_ref());
SimpleCubeInfo {
flags: (1 | 4).into(),
tex: [coords.rect(0).rect.rt_texref((w, h)); 6],
}
}
_ => SimpleCubeInfo {
flags: 0.into(),
tex: [TexRef::default(); 6],
},
}
}
const FULL_CUBE_EXTENTS: CubeExtents = CubeExtents::new((-0.5, 0.5), (-0.5, 0.5), (-0.5, 0.5));
fn cube_variant_height_unblended(variant: u16) -> CubeExtents {
let y_max = variant_to_height(variant);
CubeExtents {
verts: [
vec3(-0.5, y_max, -0.5),
vec3(-0.5, y_max, 0.5),
vec3(-0.5, 0.5, -0.5),
vec3(-0.5, 0.5, 0.5),
vec3(0.5, y_max, -0.5),
vec3(0.5, y_max, 0.5),
vec3(0.5, 0.5, -0.5),
vec3(0.5, 0.5, 0.5),
],
adjacency: DEFAULT_FACE_NORMALS,
force: [false, false, variant < 7, false, false, false],
top_normal: CubeFace::YPlus.default_normal(),
top_tan: 1,
}
}
fn variant_to_height(variant: u16) -> f32 {
let height = ((variant as f32) / 7.0).clamp(0.025, 1.0);
0.5 - height
}
fn build_liquid_cube_extents(
chunk_data: &impl ChunkDataView,
offset: ChunkOffset,
id: BlockId,
) -> CubeExtents {
let variant = id.variant();
let neighbor_variant = |offset: ChunkOffset, dx: i8, dz: i8| -> u16 {
let (x, z) = (offset.x as i8 + dx, offset.z as i8 + dz);
let neighbor = chunk_data.block_ids()[(x, offset.y as i8, z).as_padded_index()];
if neighbor.equals_ignore_variant(id) {
neighbor.variant()
} else {
0
}
};
let mut y_xn_zn = variant_to_height(
variant
.max(neighbor_variant(offset, -1, 0))
.max(neighbor_variant(offset, 0, -1))
.max(neighbor_variant(offset, -1, -1)),
);
let mut y_xn_zp = variant_to_height(
variant
.max(neighbor_variant(offset, -1, 0))
.max(neighbor_variant(offset, 0, 1))
.max(neighbor_variant(offset, -1, 1)),
);
let mut y_xp_zn = variant_to_height(
variant
.max(neighbor_variant(offset, 1, 0))
.max(neighbor_variant(offset, 0, -1))
.max(neighbor_variant(offset, 1, -1)),
);
let mut y_xp_zp = variant_to_height(
variant
.max(neighbor_variant(offset, 1, 0))
.max(neighbor_variant(offset, 0, 1))
.max(neighbor_variant(offset, 1, 1)),
);
let max_xdiff = f32::max(f32::abs(y_xn_zn - y_xp_zn), f32::abs(y_xn_zp - y_xp_zp));
let max_zdiff = f32::max(f32::abs(y_xn_zn - y_xn_zp), f32::abs(y_xp_zn - y_xp_zp));
let nx = ((((y_xn_zn - y_xp_zn) + (y_xn_zp - y_xp_zp)) * -7.5) as i8).clamp(-15, 15);
let nz = ((((y_xn_zn - y_xn_zp) + (y_xp_zn - y_xp_zp)) * -7.5) as i8).clamp(-15, 15);
let mut rotation = 0;
let mut top_tan = 3;
if max_zdiff > max_xdiff {
rotation = 1;
top_tan = 0;
(y_xn_zn, y_xp_zn, y_xp_zp, y_xn_zp) = (y_xn_zp, y_xn_zn, y_xp_zn, y_xp_zp);
}
CubeExtents {
verts: [
vec3(-0.5, y_xn_zn, -0.5),
vec3(-0.5, y_xn_zp, 0.5),
vec3(-0.5, 0.5, -0.5),
vec3(-0.5, 0.5, 0.5),
vec3(0.5, y_xp_zn, -0.5),
vec3(0.5, y_xp_zp, 0.5),
vec3(0.5, 0.5, -0.5),
vec3(0.5, 0.5, 0.5),
],
adjacency: DEFAULT_FACE_NORMALS,
force: [false, false, variant < 7, false, false, false],
top_normal: x5y5z5_pack16(nx, -16, nz),
top_tan,
}
.rotate_y(rotation)
}
#[derive(Clone, Copy, Debug)]
enum MaybeDynamicRect {
Static(RectF32),
Dynamic(DynamicRect),
}
impl MaybeDynamicRect {
fn rect(&self, selector: u32) -> RectF32 {
match self {
Self::Static(rect) => *rect,
Self::Dynamic(rect) => rect.resolve(selector),
}
}
}
struct SimpleTexCoordEntry {
coords: [MdrWithFlags; 6],
}
struct SimpleTexCoordCache {
blocks: Vec<Option<SimpleTexCoordEntry>>,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct FlaggedRectF32 {
rect: RectF32,
flags: u32,
}
impl SimpleTexCoordCache {
fn get(&self, block_id: BlockId) -> Option<[FlaggedRectF32; 6]> {
let selector = render_selector::from_variant(block_id.variant());
self.blocks
.get(block_id.index())
.and_then(|x| x.as_ref())
.map(|entry| entry.coords.map(|x| x.rect(selector)))
}
fn get_zero(&self, block_id: BlockId) -> Option<FlaggedRectF32> {
self.blocks
.get(block_id.index())
.and_then(|x| x.as_ref())
.and_then(|entry| {
Some(entry.coords[0].rect(render_selector::from_variant(block_id.variant())))
})
}
}
enum AabbRotation {
None,
Nesw,
}
#[derive(Clone, Debug, Copy)]
struct MdrWithFlags {
rect: MaybeDynamicRect,
tex_flags: u32,
}
impl MdrWithFlags {
fn rect(&self, selector: u32) -> FlaggedRectF32 {
FlaggedRectF32 {
rect: self.rect.rect(selector),
flags: self.tex_flags,
}
}
}
impl From<MaybeDynamicRect> for MdrWithFlags {
fn from(value: MaybeDynamicRect) -> Self {
Self {
rect: value,
tex_flags: 0,
}
}
}
#[derive(Clone, Debug, Copy)]
enum CachedAabbTextures {
Prism([MdrWithFlags; 6]),
Plantlike(MdrWithFlags),
}
struct CachedAxisAlignedBox {
extents: CubeExtents,
textures: CachedAabbTextures,
rotation: AabbRotation,
mask: u32,
}
struct CachedAabbBlockEntry {
boxes: Box<[CachedAxisAlignedBox]>,
needs_neighbor_bits: bool,
}
struct AxisAlignedBoxBlocksCache {
blocks: Vec<Option<CachedAabbBlockEntry>>,
}
impl AxisAlignedBoxBlocksCache {
fn get(&self, block_id: BlockId) -> Option<&CachedAabbBlockEntry> {
self.blocks.get(block_id.index()).and_then(|x| x.as_ref())
}
}
#[allow(dead_code)]
const GLOBAL_BRIGHTNESS_TABLE_RAW: [f32; 16] = [
0.0, 0.0625, 0.125, 0.1875, 0.25, 0.3125, 0.375, 0.4375, 0.5, 0.5625, 0.625, 0.6875, 0.75,
0.8125, 0.875, 0.9375,
];
#[allow(dead_code)]
const BRIGHTNESS_TABLE_RAW: [f32; 16] = [
0.0625, 0.125, 0.1875, 0.25, 0.3125, 0.375, 0.4375, 0.5, 0.5625, 0.625, 0.6875, 0.75, 0.8125,
0.875, 0.9375, 1.00,
];
#[allow(dead_code)]
const CUBE_FACE_BRIGHTNESS_BIASES: [f32; 10] = [
0.975, 0.975, 1.05, 0.95, 0.975, 0.975, 0.975, 0.975, 0.975, 0.975,
];
lazy_static::lazy_static! {
static ref GLOBAL_BRIGHTNESS_TABLE: [f32; 16] = {
GLOBAL_BRIGHTNESS_TABLE_RAW.iter().map(|x| x.powf(1.5)).collect::<Vec<f32>>().try_into().unwrap()
};
static ref BRIGHTNESS_TABLE: [f32; 16] = {
BRIGHTNESS_TABLE_RAW.iter().map(|x| x.powf(1.25)).collect::<Vec<f32>>().try_into().unwrap()
};
}
#[inline]
fn cgv(
coord: Vector3<f32>,
normal: u16,
tangent_code: u8,
tex_uv: Vector2<f32>,
brightness: u8,
wave_horizontal: u8,
tex_flags: u16,
) -> CubeGeometryVertex {
CubeGeometryVertex {
position: [coord.x, coord.y, coord.z],
normal,
tangent: tangent_code,
uv_texcoord: [tex_uv.x as u16, tex_uv.y as u16],
brightness,
wave_horizontal,
_padding: 0,
tex_flags,
}
}
#[inline]
pub(crate) fn emit_cube_face_vk(
coord: Vector3<f32>,
flagged_frame: FlaggedRectF32,
source_face: CubeFace,
dest_face: CubeFace,
vert_buf: &mut Vec<CubeGeometryVertex>,
idx_buf: &mut Vec<u32>,
e: CubeExtents,
brightness: u8,
wave: u8,
variant: u16,
) {
let c = vec3(coord.x, -coord.y, coord.z);
let frame = flagged_frame.rect;
let fl = (((flagged_frame.flags & 0xff) as u16) << 8) | (variant & 0xff) as u16;
let [tl_u, tl_v] = frame.tl();
let [tr_u, tr_v] = frame.tr();
let [bl_u, bl_v] = frame.bl();
let [br_u, br_v] = frame.br();
let tl = Vector2::new(tl_u, tl_v);
let tr = Vector2::new(tr_u, tr_v);
let bl = Vector2::new(bl_u, bl_v);
let br = Vector2::new(br_u, br_v);
let lt = brightness;
const TAN_Y_DOWN: u8 = 5;
let normal = dest_face.default_normal();
let vertices = match source_face {
CubeFace::ZMinus => [
cgv(c + e.verts[0], normal, TAN_Y_DOWN, tl, lt, 0, fl),
cgv(c + e.verts[2], normal, TAN_Y_DOWN, bl, lt, 0, fl),
cgv(c + e.verts[6], normal, TAN_Y_DOWN, br, lt, 0, fl),
cgv(c + e.verts[4], normal, TAN_Y_DOWN, tr, lt, 0, fl),
],
CubeFace::ZPlus => [
cgv(c + e.verts[5], normal, TAN_Y_DOWN, tl, lt, 0, fl),
cgv(c + e.verts[7], normal, TAN_Y_DOWN, bl, lt, 0, fl),
cgv(c + e.verts[3], normal, TAN_Y_DOWN, br, lt, 0, fl),
cgv(c + e.verts[1], normal, TAN_Y_DOWN, tr, lt, 0, fl),
],
CubeFace::XMinus => [
cgv(c + e.verts[1], normal, TAN_Y_DOWN, tl, lt, 0, fl),
cgv(c + e.verts[3], normal, TAN_Y_DOWN, bl, lt, 0, fl),
cgv(c + e.verts[2], normal, TAN_Y_DOWN, br, lt, 0, fl),
cgv(c + e.verts[0], normal, TAN_Y_DOWN, tr, lt, 0, fl),
],
CubeFace::XPlus => [
cgv(c + e.verts[4], normal, TAN_Y_DOWN, tl, lt, 0, fl),
cgv(c + e.verts[6], normal, TAN_Y_DOWN, bl, lt, 0, fl),
cgv(c + e.verts[7], normal, TAN_Y_DOWN, br, lt, 0, fl),
cgv(c + e.verts[5], normal, TAN_Y_DOWN, tr, lt, 0, fl),
],
CubeFace::YMinus => [
cgv(c + e.verts[2], normal, e.top_tan(), tl, lt, 0, fl),
cgv(c + e.verts[3], normal, e.top_tan(), bl, lt, 0, fl),
cgv(c + e.verts[7], normal, e.top_tan(), br, lt, 0, fl),
cgv(c + e.verts[6], normal, e.top_tan(), tr, lt, 0, fl),
],
CubeFace::YPlus => [
cgv(c + e.verts[4], e.top_norm(), e.top_tan(), tl, lt, 0, fl),
cgv(c + e.verts[5], e.top_norm(), e.top_tan(), bl, lt, 0, fl),
cgv(c + e.verts[1], e.top_norm(), e.top_tan(), br, lt, 0, fl),
cgv(c + e.verts[0], e.top_norm(), e.top_tan(), tr, lt, 0, fl),
],
CubeFace::PlantXMinusZMinus => [
cgv(c + e.verts[1], normal, TAN_Y_DOWN, tl, lt, wave, fl),
cgv(c + e.verts[3], normal, TAN_Y_DOWN, bl, lt, 0, fl),
cgv(c + e.verts[6], normal, TAN_Y_DOWN, br, lt, 0, fl),
cgv(c + e.verts[4], normal, TAN_Y_DOWN, tr, lt, wave, fl),
],
CubeFace::PlantXPlusZPlus => [
cgv(c + e.verts[4], normal, TAN_Y_DOWN, tl, lt, wave, fl),
cgv(c + e.verts[6], normal, TAN_Y_DOWN, bl, lt, 0, fl),
cgv(c + e.verts[3], normal, TAN_Y_DOWN, br, lt, 0, fl),
cgv(c + e.verts[1], normal, TAN_Y_DOWN, tr, lt, wave, fl),
],
CubeFace::PlantXMinusZPlus => [
cgv(c + e.verts[5], normal, TAN_Y_DOWN, tl, lt, wave, fl),
cgv(c + e.verts[7], normal, TAN_Y_DOWN, bl, lt, 0, fl),
cgv(c + e.verts[2], normal, TAN_Y_DOWN, br, lt, 0, fl),
cgv(c + e.verts[0], normal, TAN_Y_DOWN, tr, lt, wave, fl),
],
CubeFace::PlantXPlusZMinus => [
cgv(c + e.verts[0], normal, TAN_Y_DOWN, tl, lt, wave, fl),
cgv(c + e.verts[2], normal, TAN_Y_DOWN, bl, lt, 0, fl),
cgv(c + e.verts[7], normal, TAN_Y_DOWN, br, lt, 0, fl),
cgv(c + e.verts[5], normal, TAN_Y_DOWN, tr, lt, wave, fl),
],
};
let si: u32 = vert_buf.len().try_into().unwrap();
if si > (u32::MAX - 8) {
panic!("vertex buffer got too big");
}
vert_buf.extend_from_slice(&vertices);
idx_buf.extend_from_slice(&[si, si + 1, si + 2, si, si + 2, si + 3]);
}
#[inline]
fn max_brightness(b1: u8, b2: u8) -> u8 {
((b1 & 0xf).max(b2 & 0xf)) | ((b1 & 0xf0).max(b2 & 0xf0))
}
#[cfg(test)]
mod tests {
use super::*;
use perovskite_core::protocol::blocks::AxisAlignedBox;
fn aabb_block_def(masks: &[u32]) -> BlockTypeDef {
BlockTypeDef {
render_info: Some(RenderInfo::AxisAlignedBoxes(AxisAlignedBoxes {
boxes: masks
.iter()
.map(|&variant_mask| AxisAlignedBox {
variant_mask,
..Default::default()
})
.collect(),
})),
..Default::default()
}
}
fn tex_coords() -> FxHashMap<TextureKey, Rect> {
let mut coords = FxHashMap::default();
coords.insert(TextureKey::FallbackUnknownTex, Rect::new(0, 0, 16, 16));
coords
}
#[test]
fn aabb_cache_normalizes_masks() {
let entry =
build_axis_aligned_box_cache_entry(&aabb_block_def(&[0, 7]), &tex_coords(), false)
.unwrap();
assert_eq!(entry.boxes[0].mask, u32::MAX);
assert_eq!(entry.boxes[1].mask, 7);
assert!(!entry.needs_neighbor_bits);
let entry = build_axis_aligned_box_cache_entry(
&aabb_block_def(&[0x40000000 | 7]),
&tex_coords(),
false,
)
.unwrap();
assert_eq!(entry.boxes[0].mask, 7);
}
#[test]
fn aabb_cache_detects_neighbor_bits() {
let entry = build_axis_aligned_box_cache_entry(
&aabb_block_def(&[0, render_selector::NEIGHBOR_XPLUS]),
&tex_coords(),
false,
)
.unwrap();
assert_eq!(entry.boxes[1].mask, render_selector::NEIGHBOR_XPLUS);
assert!(entry.needs_neighbor_bits);
let entry = build_axis_aligned_box_cache_entry(&aabb_block_def(&[0]), &tex_coords(), false)
.unwrap();
assert!(!entry.needs_neighbor_bits);
}
}