use crate::resolver::{ModelResolver, StateResolver};
use crate::resource_pack::ResourcePack;
use crate::types::{BlockPosition, Direction, InputBlock};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, PartialEq)]
enum BlockCullType {
NonSolid,
Opaque,
Transparent(String),
}
pub struct FaceCuller<'a> {
block_types: HashMap<BlockPosition, BlockCullType>,
occupied: HashSet<BlockPosition>,
pack: &'a ResourcePack,
cull_cache: HashMap<String, BlockCullType>,
}
impl<'a> FaceCuller<'a> {
pub fn new(pack: &'a ResourcePack, blocks: &[(BlockPosition, &InputBlock)]) -> Self {
let mut culler = Self {
block_types: HashMap::new(),
occupied: HashSet::new(),
pack,
cull_cache: HashMap::new(),
};
for (pos, block) in blocks {
culler.occupied.insert(*pos);
let cull_type = culler.classify_block(block);
culler.block_types.insert(*pos, cull_type);
}
culler
}
fn classify_block(&mut self, block: &InputBlock) -> BlockCullType {
if block.is_air() {
return BlockCullType::NonSolid;
}
if let Some(cached) = self.cull_cache.get(&block.name) {
return cached.clone();
}
let cull_type = self.resolve_and_classify(block);
self.cull_cache.insert(block.name.clone(), cull_type.clone());
cull_type
}
fn resolve_and_classify(&self, block: &InputBlock) -> BlockCullType {
if let Some(transparent_group) = self.get_transparent_group(&block.name) {
return BlockCullType::Transparent(transparent_group);
}
let state_resolver = StateResolver::new(self.pack);
let variants = match state_resolver.resolve(block) {
Ok(v) => v,
Err(_) => return BlockCullType::NonSolid, };
let model_resolver = ModelResolver::new(self.pack);
for variant in &variants {
let model = match model_resolver.resolve(&variant.model_location()) {
Ok(m) => m,
Err(_) => return BlockCullType::NonSolid, };
if !self.is_full_opaque_cube(&model) {
return BlockCullType::NonSolid;
}
}
if variants.is_empty() {
BlockCullType::NonSolid
} else {
BlockCullType::Opaque
}
}
fn get_transparent_group(&self, name: &str) -> Option<String> {
let block_id = name.split(':').nth(1).unwrap_or(name);
if block_id == "glass" || block_id.ends_with("_glass") {
if block_id.contains("stained") {
return Some("stained_glass".to_string());
}
if block_id == "tinted_glass" {
return Some("tinted_glass".to_string());
}
return Some("glass".to_string());
}
if block_id == "glass_pane" || block_id.ends_with("_glass_pane") {
if block_id.contains("stained") {
return Some("stained_glass_pane".to_string());
}
return Some("glass_pane".to_string());
}
if block_id == "ice" || block_id == "packed_ice" || block_id == "blue_ice" {
return Some(block_id.to_string());
}
if block_id == "frosted_ice" {
return Some("frosted_ice".to_string());
}
if block_id.ends_with("_leaves") {
return Some("leaves".to_string());
}
if block_id == "slime_block" {
return Some("slime_block".to_string());
}
if block_id == "honey_block" {
return Some("honey_block".to_string());
}
None
}
fn is_full_opaque_cube(&self, model: &crate::resource_pack::BlockModel) -> bool {
if model.elements.len() != 1 {
return false;
}
let element = &model.elements[0];
const EPSILON: f32 = 0.001;
if element.from[0].abs() > EPSILON
|| element.from[1].abs() > EPSILON
|| element.from[2].abs() > EPSILON
{
return false;
}
if (element.to[0] - 16.0).abs() > EPSILON
|| (element.to[1] - 16.0).abs() > EPSILON
|| (element.to[2] - 16.0).abs() > EPSILON
{
return false;
}
if element.faces.len() != 6 {
return false;
}
for direction in Direction::ALL.iter() {
if !element.faces.contains_key(direction) {
return false;
}
}
true
}
pub fn should_cull(&self, pos: BlockPosition, cullface: Direction) -> bool {
let neighbor_pos = pos.neighbor(cullface);
if !self.occupied.contains(&neighbor_pos) {
return false;
}
let neighbor_type = self.block_types.get(&neighbor_pos);
let current_type = self.block_types.get(&pos);
match (current_type, neighbor_type) {
(_, Some(BlockCullType::Opaque)) => true,
(Some(BlockCullType::Transparent(current_group)), Some(BlockCullType::Transparent(neighbor_group))) => {
current_group == neighbor_group
}
_ => false,
}
}
pub fn is_opaque_at(&self, pos: BlockPosition) -> bool {
match self.block_types.get(&pos) {
Some(BlockCullType::Opaque) => true,
Some(BlockCullType::Transparent(_)) => true, _ => false,
}
}
pub fn is_fully_opaque_at(&self, pos: BlockPosition) -> bool {
matches!(self.block_types.get(&pos), Some(BlockCullType::Opaque))
}
pub fn is_occupied(&self, pos: BlockPosition) -> bool {
self.occupied.contains(&pos)
}
pub fn calculate_ao(&self, pos: BlockPosition, direction: Direction) -> [u8; 4] {
let corner_neighbors = get_ao_neighbors(direction);
let mut ao_values = [3u8; 4];
for (i, (side1_offset, side2_offset, corner_offset)) in corner_neighbors.iter().enumerate() {
let side1_pos = BlockPosition::new(
pos.x + side1_offset[0],
pos.y + side1_offset[1],
pos.z + side1_offset[2],
);
let side2_pos = BlockPosition::new(
pos.x + side2_offset[0],
pos.y + side2_offset[1],
pos.z + side2_offset[2],
);
let corner_pos = BlockPosition::new(
pos.x + corner_offset[0],
pos.y + corner_offset[1],
pos.z + corner_offset[2],
);
let side1 = self.is_opaque_at(side1_pos) as u8;
let side2 = self.is_opaque_at(side2_pos) as u8;
let corner = self.is_opaque_at(corner_pos) as u8;
ao_values[i] = vertex_ao(side1, side2, corner);
}
ao_values
}
}
impl<'a> FaceCuller<'a> {
pub fn from_blocks(blocks: &[(BlockPosition, &InputBlock)]) -> FaceCullerSimple {
FaceCullerSimple::from_blocks(blocks)
}
}
#[derive(Debug, Clone, PartialEq)]
enum SimpleCullType {
NonSolid,
Opaque,
Transparent(String),
}
pub struct FaceCullerSimple {
block_types: HashMap<BlockPosition, SimpleCullType>,
occupied: HashSet<BlockPosition>,
}
impl FaceCullerSimple {
pub fn from_blocks(blocks: &[(BlockPosition, &InputBlock)]) -> Self {
let mut block_types = HashMap::new();
let mut occupied = HashSet::new();
for (pos, block) in blocks {
occupied.insert(*pos);
let cull_type = Self::classify_block_heuristic(&block.name);
block_types.insert(*pos, cull_type);
}
Self {
block_types,
occupied,
}
}
fn classify_block_heuristic(name: &str) -> SimpleCullType {
let block_id = name.split(':').nth(1).unwrap_or(name);
if name.contains("air") {
return SimpleCullType::NonSolid;
}
if let Some(group) = Self::get_transparent_group_heuristic(block_id) {
return SimpleCullType::Transparent(group);
}
if is_likely_full_cube(name) {
SimpleCullType::Opaque
} else {
SimpleCullType::NonSolid
}
}
fn get_transparent_group_heuristic(block_id: &str) -> Option<String> {
if block_id == "glass" || block_id.ends_with("_glass") {
if block_id.contains("stained") {
return Some("stained_glass".to_string());
}
if block_id == "tinted_glass" {
return Some("tinted_glass".to_string());
}
return Some("glass".to_string());
}
if block_id == "glass_pane" || block_id.ends_with("_glass_pane") {
if block_id.contains("stained") {
return Some("stained_glass_pane".to_string());
}
return Some("glass_pane".to_string());
}
if block_id == "ice" || block_id == "packed_ice" || block_id == "blue_ice" || block_id == "frosted_ice" {
return Some(block_id.to_string());
}
if block_id.ends_with("_leaves") {
return Some("leaves".to_string());
}
if block_id == "slime_block" || block_id == "honey_block" {
return Some(block_id.to_string());
}
None
}
pub fn should_cull(&self, pos: BlockPosition, cullface: Direction) -> bool {
let neighbor_pos = pos.neighbor(cullface);
if !self.occupied.contains(&neighbor_pos) {
return false;
}
let neighbor_type = self.block_types.get(&neighbor_pos);
let current_type = self.block_types.get(&pos);
match (current_type, neighbor_type) {
(_, Some(SimpleCullType::Opaque)) => true,
(Some(SimpleCullType::Transparent(current)), Some(SimpleCullType::Transparent(neighbor))) => {
current == neighbor
}
_ => false,
}
}
pub fn is_opaque_at(&self, pos: BlockPosition) -> bool {
match self.block_types.get(&pos) {
Some(SimpleCullType::Opaque) => true,
Some(SimpleCullType::Transparent(_)) => true,
_ => false,
}
}
pub fn is_occupied(&self, pos: BlockPosition) -> bool {
self.occupied.contains(&pos)
}
pub fn calculate_ao(&self, pos: BlockPosition, direction: Direction) -> [u8; 4] {
let corner_neighbors = get_ao_neighbors(direction);
let mut ao_values = [3u8; 4];
for (i, (side1_offset, side2_offset, corner_offset)) in corner_neighbors.iter().enumerate() {
let side1_pos = BlockPosition::new(
pos.x + side1_offset[0],
pos.y + side1_offset[1],
pos.z + side1_offset[2],
);
let side2_pos = BlockPosition::new(
pos.x + side2_offset[0],
pos.y + side2_offset[1],
pos.z + side2_offset[2],
);
let corner_pos = BlockPosition::new(
pos.x + corner_offset[0],
pos.y + corner_offset[1],
pos.z + corner_offset[2],
);
let side1 = self.is_opaque_at(side1_pos) as u8;
let side2 = self.is_opaque_at(side2_pos) as u8;
let corner = self.is_opaque_at(corner_pos) as u8;
ao_values[i] = vertex_ao(side1, side2, corner);
}
ao_values
}
}
fn is_likely_full_cube(name: &str) -> bool {
if name.contains("air") {
return false;
}
let non_full_patterns = [
"slab", "stairs", "fence", "wall", "door", "trapdoor",
"sign", "banner", "button", "lever", "torch", "lantern",
"pressure_plate", "carpet", "rail", "flower", "sapling",
"glass_pane", "iron_bars", "chain", "rod", "candle",
"head", "skull", "pot", "campfire", "anvil", "bell",
"brewing_stand", "cauldron", "hopper", "lectern",
"grindstone", "stonecutter", "enchanting_table",
"repeater", "comparator", "daylight_detector",
"piston", "tripwire", "string", "cobweb", "vine",
"ladder", "scaffolding", "coral_fan", "pickle",
"egg", "frogspawn", "dripleaf", "azalea", "roots",
"sprouts", "fungus", "mushroom", "grass", "fern",
"bush", "berry", "wart", "stem", "crop", "wheat",
"carrots", "potatoes", "beetroots", "cocoa", "cactus",
"sugar_cane", "bamboo", "kelp", "seagrass", "lichen",
"vein", "fire", "snow", "layer",
"poppy", "dandelion", "orchid", "allium", "tulip",
"oxeye_daisy", "cornflower", "lily_of_the_valley",
"wither_rose", "sunflower", "lilac", "rose_bush",
"peony", "pitcher_plant", "torchflower", "pink_petals",
];
for pattern in &non_full_patterns {
if name.contains(pattern) {
if name.ends_with("_block") && !name.contains("piston") {
continue;
}
return false;
}
}
true
}
fn vertex_ao(side1: u8, side2: u8, corner: u8) -> u8 {
if side1 == 1 && side2 == 1 {
0
} else {
3 - (side1 + side2 + corner)
}
}
fn get_ao_neighbors(direction: Direction) -> [([i32; 3], [i32; 3], [i32; 3]); 4] {
match direction {
Direction::Up => [
([0, 1, -1], [-1, 1, 0], [-1, 1, -1]),
([0, 1, -1], [1, 1, 0], [1, 1, -1]),
([0, 1, 1], [1, 1, 0], [1, 1, 1]),
([0, 1, 1], [-1, 1, 0], [-1, 1, 1]),
],
Direction::Down => [
([0, -1, 1], [-1, -1, 0], [-1, -1, 1]),
([0, -1, 1], [1, -1, 0], [1, -1, 1]),
([0, -1, -1], [1, -1, 0], [1, -1, -1]),
([0, -1, -1], [-1, -1, 0], [-1, -1, -1]),
],
Direction::North => [
([1, 0, -1], [0, 1, -1], [1, 1, -1]),
([-1, 0, -1], [0, 1, -1], [-1, 1, -1]),
([-1, 0, -1], [0, -1, -1], [-1, -1, -1]),
([1, 0, -1], [0, -1, -1], [1, -1, -1]),
],
Direction::South => [
([-1, 0, 1], [0, 1, 1], [-1, 1, 1]),
([1, 0, 1], [0, 1, 1], [1, 1, 1]),
([1, 0, 1], [0, -1, 1], [1, -1, 1]),
([-1, 0, 1], [0, -1, 1], [-1, -1, 1]),
],
Direction::West => [
([-1, 0, -1], [-1, 1, 0], [-1, 1, -1]),
([-1, 0, 1], [-1, 1, 0], [-1, 1, 1]),
([-1, 0, 1], [-1, -1, 0], [-1, -1, 1]),
([-1, 0, -1], [-1, -1, 0], [-1, -1, -1]),
],
Direction::East => [
([1, 0, 1], [1, 1, 0], [1, 1, 1]),
([1, 0, -1], [1, 1, 0], [1, 1, -1]),
([1, 0, -1], [1, -1, 0], [1, -1, -1]),
([1, 0, 1], [1, -1, 0], [1, -1, 1]),
],
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_likely_full_cube() {
assert!(is_likely_full_cube("minecraft:stone"));
assert!(is_likely_full_cube("minecraft:dirt"));
assert!(is_likely_full_cube("minecraft:oak_log"));
assert!(is_likely_full_cube("minecraft:diamond_block"));
assert!(is_likely_full_cube("minecraft:grass_block"));
assert!(is_likely_full_cube("minecraft:mushroom_block"));
assert!(!is_likely_full_cube("minecraft:air"));
assert!(!is_likely_full_cube("minecraft:oak_slab"));
assert!(!is_likely_full_cube("minecraft:oak_stairs"));
assert!(!is_likely_full_cube("minecraft:oak_fence"));
assert!(!is_likely_full_cube("minecraft:glass_pane"));
assert!(!is_likely_full_cube("minecraft:torch"));
assert!(!is_likely_full_cube("minecraft:poppy"));
assert!(!is_likely_full_cube("minecraft:oak_door"));
}
#[test]
fn test_simple_culler() {
let stone1 = InputBlock::new("minecraft:stone");
let stone2 = InputBlock::new("minecraft:stone");
let flower = InputBlock::new("minecraft:poppy");
let blocks = vec![
(BlockPosition::new(0, 0, 0), &stone1),
(BlockPosition::new(1, 0, 0), &stone2),
(BlockPosition::new(0, 1, 0), &flower),
];
let culler = FaceCullerSimple::from_blocks(&blocks);
assert!(culler.should_cull(BlockPosition::new(0, 0, 0), Direction::East));
assert!(culler.should_cull(BlockPosition::new(1, 0, 0), Direction::West));
assert!(!culler.should_cull(BlockPosition::new(0, 0, 0), Direction::Up));
assert!(!culler.should_cull(BlockPosition::new(0, 0, 0), Direction::West));
}
#[test]
fn test_glass_culling() {
let glass1 = InputBlock::new("minecraft:glass");
let glass2 = InputBlock::new("minecraft:glass");
let stained_glass = InputBlock::new("minecraft:red_stained_glass");
let stone = InputBlock::new("minecraft:stone");
let blocks = vec![
(BlockPosition::new(0, 0, 0), &glass1),
(BlockPosition::new(1, 0, 0), &glass2),
(BlockPosition::new(2, 0, 0), &stained_glass),
(BlockPosition::new(0, 1, 0), &stone),
];
let culler = FaceCullerSimple::from_blocks(&blocks);
assert!(culler.should_cull(BlockPosition::new(0, 0, 0), Direction::East));
assert!(culler.should_cull(BlockPosition::new(1, 0, 0), Direction::West));
assert!(!culler.should_cull(BlockPosition::new(1, 0, 0), Direction::East));
assert!(!culler.should_cull(BlockPosition::new(2, 0, 0), Direction::West));
assert!(culler.should_cull(BlockPosition::new(0, 0, 0), Direction::Up));
}
#[test]
fn test_transparent_groups() {
assert_eq!(
FaceCullerSimple::get_transparent_group_heuristic("glass"),
Some("glass".to_string())
);
assert_eq!(
FaceCullerSimple::get_transparent_group_heuristic("red_stained_glass"),
Some("stained_glass".to_string())
);
assert_eq!(
FaceCullerSimple::get_transparent_group_heuristic("tinted_glass"),
Some("tinted_glass".to_string())
);
assert_eq!(
FaceCullerSimple::get_transparent_group_heuristic("oak_leaves"),
Some("leaves".to_string())
);
assert_eq!(
FaceCullerSimple::get_transparent_group_heuristic("ice"),
Some("ice".to_string())
);
assert_eq!(
FaceCullerSimple::get_transparent_group_heuristic("stone"),
None
);
}
}