use schematic_mesher::{
export_raw, resource_pack::TextureData, AtlasBuilder, BlockPosition as MesherBlockPosition,
BlockSource, BoundingBox as MesherBoundingBox, InputBlock, Mesher, MesherConfig, MesherOutput,
RawMeshData, ResourcePack, TextureAtlas,
};
pub mod cache;
pub mod item_model;
pub use item_model::{
build_resource_pack, ItemModelConfig, ItemModelResult, ItemModelScale, ItemModelStats,
};
pub use schematic_mesher::{MeshLayer, MeshOutput};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use crate::entity::{Entity, NbtValue};
use crate::{BlockState, Region, UniversalSchematic};
#[derive(Debug, thiserror::Error)]
pub enum MeshError {
#[error("Resource pack error: {0}")]
ResourcePack(String),
#[error("Meshing error: {0}")]
Meshing(String),
#[error("Export error: {0}")]
Export(String),
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
}
pub type Result<T> = std::result::Result<T, MeshError>;
pub struct ResourcePackSource {
pack: ResourcePack,
}
impl ResourcePackSource {
pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
let pack = schematic_mesher::load_resource_pack(path)
.map_err(|e| MeshError::ResourcePack(e.to_string()))?;
Ok(Self { pack })
}
pub fn from_bytes(data: &[u8]) -> Result<Self> {
let pack = schematic_mesher::load_resource_pack_from_bytes(data)
.map_err(|e| MeshError::ResourcePack(e.to_string()))?;
Ok(Self { pack })
}
pub fn from_files<I, P>(paths: I) -> Result<Self>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
let pack = schematic_mesher::load_resource_packs(paths)
.map_err(|e| MeshError::ResourcePack(e.to_string()))?;
Ok(Self { pack })
}
pub fn from_bytes_list<I, B>(datas: I) -> Result<Self>
where
I: IntoIterator<Item = B>,
B: AsRef<[u8]>,
{
let pack = schematic_mesher::load_resource_packs_from_bytes(datas)
.map_err(|e| MeshError::ResourcePack(e.to_string()))?;
Ok(Self { pack })
}
pub fn from_resource_pack(pack: ResourcePack) -> Self {
Self { pack }
}
pub fn pack(&self) -> &ResourcePack {
&self.pack
}
pub fn pack_mut(&mut self) -> &mut ResourcePack {
&mut self.pack
}
pub fn list_blockstates(&self) -> Vec<String> {
let mut names = Vec::new();
for (namespace, blocks) in &self.pack.blockstates {
for block_id in blocks.keys() {
names.push(format!("{}:{}", namespace, block_id));
}
}
names
}
pub fn list_models(&self) -> Vec<String> {
let mut names = Vec::new();
for (namespace, models) in &self.pack.models {
for model_path in models.keys() {
names.push(format!("{}:{}", namespace, model_path));
}
}
names
}
pub fn list_textures(&self) -> Vec<String> {
let mut names = Vec::new();
for (namespace, textures) in &self.pack.textures {
for texture_path in textures.keys() {
names.push(format!("{}:{}", namespace, texture_path));
}
}
names
}
pub fn get_blockstate_json(&self, name: &str) -> Option<String> {
let def = self.pack.get_blockstate(name)?;
Some(blockstate_definition_to_json(def))
}
pub fn get_model_json(&self, name: &str) -> Option<String> {
let model = self.pack.get_model(name)?;
serde_json::to_string(model).ok()
}
pub fn get_texture_info(&self, name: &str) -> Option<(u32, u32, bool, u32)> {
let tex = self.pack.get_texture(name)?;
Some((tex.width, tex.height, tex.is_animated, tex.frame_count))
}
pub fn get_texture_pixels(&self, name: &str) -> Option<&[u8]> {
let tex = self.pack.get_texture(name)?;
Some(&tex.pixels)
}
pub fn add_blockstate_json(&mut self, name: &str, json: &str) -> Result<()> {
let (namespace, path) = split_resource_name(name)?;
let def: schematic_mesher::BlockstateDefinition =
serde_json::from_str(json).map_err(|e| MeshError::ResourcePack(e.to_string()))?;
self.pack.add_blockstate(&namespace, &path, def);
Ok(())
}
pub fn add_model_json(&mut self, name: &str, json: &str) -> Result<()> {
let (namespace, path) = split_resource_name(name)?;
let model: schematic_mesher::BlockModel =
serde_json::from_str(json).map_err(|e| MeshError::ResourcePack(e.to_string()))?;
self.pack.add_model(&namespace, &path, model);
Ok(())
}
pub fn add_texture(
&mut self,
name: &str,
width: u32,
height: u32,
pixels: Vec<u8>,
) -> Result<()> {
let (namespace, path) = split_resource_name(name)?;
let texture = TextureData::new(width, height, pixels);
self.pack.add_texture(&namespace, &path, texture);
Ok(())
}
pub fn stats(&self) -> ResourcePackStats {
ResourcePackStats {
blockstate_count: self.pack.blockstate_count(),
model_count: self.pack.model_count(),
texture_count: self.pack.texture_count(),
namespaces: self
.pack
.namespaces()
.into_iter()
.map(|s| s.to_string())
.collect(),
}
}
}
#[derive(Debug, Clone)]
pub struct ResourcePackStats {
pub blockstate_count: usize,
pub model_count: usize,
pub texture_count: usize,
pub namespaces: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct MeshConfig {
pub cull_hidden_faces: bool,
pub ambient_occlusion: bool,
pub ao_intensity: f32,
pub biome: Option<String>,
pub atlas_max_size: u32,
pub cull_occluded_blocks: bool,
pub greedy_meshing: bool,
}
impl Default for MeshConfig {
fn default() -> Self {
Self {
cull_hidden_faces: true,
ambient_occlusion: true,
ao_intensity: 0.4,
biome: None,
atlas_max_size: 4096,
cull_occluded_blocks: true,
greedy_meshing: false,
}
}
}
impl MeshConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_culling(mut self, enabled: bool) -> Self {
self.cull_hidden_faces = enabled;
self
}
pub fn with_ambient_occlusion(mut self, enabled: bool) -> Self {
self.ambient_occlusion = enabled;
self
}
pub fn with_ao_intensity(mut self, intensity: f32) -> Self {
self.ao_intensity = intensity;
self
}
pub fn with_biome(mut self, biome: impl Into<String>) -> Self {
self.biome = Some(biome.into());
self
}
pub fn with_atlas_max_size(mut self, size: u32) -> Self {
self.atlas_max_size = size;
self
}
pub fn with_cull_occluded_blocks(mut self, enabled: bool) -> Self {
self.cull_occluded_blocks = enabled;
self
}
pub fn with_greedy_meshing(mut self, enabled: bool) -> Self {
self.greedy_meshing = enabled;
self
}
fn to_mesher_config(&self) -> MesherConfig {
let mut config = MesherConfig::default();
config.cull_hidden_faces = self.cull_hidden_faces;
config.ambient_occlusion = self.ambient_occlusion;
config.ao_intensity = self.ao_intensity;
config.atlas_max_size = self.atlas_max_size;
config.cull_occluded_blocks = self.cull_occluded_blocks;
config.greedy_meshing = self.greedy_meshing;
if let Some(biome) = &self.biome {
config = config.with_biome(biome);
}
config
}
}
pub type MeshResult = MeshOutput;
pub type MultiMeshResult = HashMap<String, MeshOutput>;
#[derive(Debug)]
pub struct ChunkMeshResult {
pub meshes: HashMap<(i32, i32, i32), MeshOutput>,
pub total_vertex_count: usize,
pub total_triangle_count: usize,
}
#[derive(Debug)]
pub struct RawMeshExport {
pub(crate) inner: RawMeshData,
}
impl RawMeshExport {
pub fn positions_flat(&self) -> Vec<f32> {
self.inner.positions_flat()
}
pub fn normals_flat(&self) -> Vec<f32> {
self.inner.normals_flat()
}
pub fn uvs_flat(&self) -> Vec<f32> {
self.inner.uvs_flat()
}
pub fn colors_flat(&self) -> Vec<f32> {
self.inner.colors_flat()
}
pub fn indices(&self) -> &[u32] {
&self.inner.indices
}
pub fn texture_rgba(&self) -> &[u8] {
&self.inner.texture_rgba
}
pub fn texture_width(&self) -> u32 {
self.inner.texture_width
}
pub fn texture_height(&self) -> u32 {
self.inner.texture_height
}
pub fn vertex_count(&self) -> usize {
self.inner.vertex_count()
}
pub fn triangle_count(&self) -> usize {
self.inner.triangle_count()
}
}
fn mesh_output_from_mesher(
output: MesherOutput,
chunk_coord: Option<(i32, i32, i32)>,
) -> MeshOutput {
let mut mesh = MeshOutput::from(output);
mesh.chunk_coord = chunk_coord;
mesh
}
struct RegionBlockSource {
palette: Vec<InputBlock>,
blocks: Vec<(MesherBlockPosition, u32)>,
bounds: MesherBoundingBox,
}
impl RegionBlockSource {
fn new(region: &Region) -> Self {
let bbox = region.get_bounding_box();
let mut palette: Vec<InputBlock> = Vec::new();
let mut blocks: Vec<(MesherBlockPosition, u32)> = Vec::new();
let mut min = [f32::MAX; 3];
let mut max = [f32::MIN; 3];
collect_region_blocks(region, &mut blocks, &mut palette, &mut min, &mut max);
if blocks.is_empty() {
min = [bbox.min.0 as f32, bbox.min.1 as f32, bbox.min.2 as f32];
max = [
bbox.max.0 as f32 + 1.0,
bbox.max.1 as f32 + 1.0,
bbox.max.2 as f32 + 1.0,
];
}
let bounds = MesherBoundingBox::new(min, max);
Self {
palette,
blocks,
bounds,
}
}
}
impl BlockSource for RegionBlockSource {
fn get_block(&self, pos: MesherBlockPosition) -> Option<&InputBlock> {
self.blocks
.iter()
.find(|(p, _)| *p == pos)
.map(|(_, idx)| &self.palette[*idx as usize])
}
fn iter_blocks(&self) -> Box<dyn Iterator<Item = (MesherBlockPosition, &InputBlock)> + '_> {
Box::new(
self.blocks
.iter()
.map(|(pos, idx)| (*pos, &self.palette[*idx as usize])),
)
}
fn bounds(&self) -> MesherBoundingBox {
self.bounds
}
}
struct ChunkBlockSource {
blocks: HashMap<MesherBlockPosition, InputBlock>,
bounds: MesherBoundingBox,
}
impl ChunkBlockSource {
fn new(blocks: HashMap<MesherBlockPosition, InputBlock>, bounds: MesherBoundingBox) -> Self {
Self { blocks, bounds }
}
}
impl BlockSource for ChunkBlockSource {
fn get_block(&self, pos: MesherBlockPosition) -> Option<&InputBlock> {
self.blocks.get(&pos)
}
fn iter_blocks(&self) -> Box<dyn Iterator<Item = (MesherBlockPosition, &InputBlock)> + '_> {
let mut v: Vec<(MesherBlockPosition, &InputBlock)> = self
.blocks
.iter()
.map(|(pos, block)| (*pos, block))
.collect();
v.sort_unstable_by_key(|(p, _)| (p.x, p.y, p.z));
Box::new(v.into_iter())
}
fn bounds(&self) -> MesherBoundingBox {
self.bounds
}
}
struct VecBlockSource {
palette: Vec<InputBlock>,
blocks: Vec<(MesherBlockPosition, u32)>,
bounds: MesherBoundingBox,
}
impl VecBlockSource {
fn new(
palette: Vec<InputBlock>,
blocks: Vec<(MesherBlockPosition, u32)>,
bounds: MesherBoundingBox,
) -> Self {
Self {
palette,
blocks,
bounds,
}
}
}
impl BlockSource for VecBlockSource {
fn get_block(&self, pos: MesherBlockPosition) -> Option<&InputBlock> {
self.blocks
.iter()
.find(|(p, _)| *p == pos)
.map(|(_, idx)| &self.palette[*idx as usize])
}
fn iter_blocks(&self) -> Box<dyn Iterator<Item = (MesherBlockPosition, &InputBlock)> + '_> {
Box::new(
self.blocks
.iter()
.map(|(pos, idx)| (*pos, &self.palette[*idx as usize])),
)
}
fn bounds(&self) -> MesherBoundingBox {
self.bounds
}
}
fn block_state_to_input_block(block_state: &BlockState) -> InputBlock {
let mut input = InputBlock::new(block_state.name.to_string());
for (key, value) in &block_state.properties {
input.properties.insert(key.to_string(), value.to_string());
}
input
}
fn entity_to_input_block(entity: &Entity) -> InputBlock {
let entity_id = entity.id.strip_prefix("minecraft:").unwrap_or(&entity.id);
let mesher_id = match entity_id {
"furnace_minecart"
| "chest_minecart"
| "tnt_minecart"
| "hopper_minecart"
| "spawner_minecart"
| "command_block_minecart" => "minecart",
id => id,
};
let mut input = InputBlock::new(format!("entity:{}", mesher_id));
if let Some(NbtValue::List(rotation)) = entity.nbt.get("Rotation") {
if let Some(NbtValue::Float(yaw)) = rotation.first() {
let facing = yaw_to_facing(*yaw);
input
.properties
.insert("facing".to_string(), facing.to_string());
}
}
if let Some(NbtValue::Byte(1)) = entity.nbt.get("IsBaby") {
input
.properties
.insert("is_baby".to_string(), "true".to_string());
}
if let Some(NbtValue::Int(age)) = entity.nbt.get("Age") {
if *age < 0 {
input
.properties
.insert("is_baby".to_string(), "true".to_string());
}
}
if let Some(NbtValue::Byte(color)) = entity.nbt.get("Color") {
input
.properties
.insert("color".to_string(), dye_color_name(*color as u8));
}
for pose_key in &[
"RightArmPose",
"LeftArmPose",
"RightLegPose",
"LeftLegPose",
"HeadPose",
"BodyPose",
] {
if let Some(NbtValue::List(angles)) = entity.nbt.get(*pose_key) {
let angle_strs: Vec<String> = angles
.iter()
.filter_map(|v| match v {
NbtValue::Float(f) => Some(format!("{}", f)),
_ => None,
})
.collect();
if !angle_strs.is_empty() {
input
.properties
.insert(pose_key.to_string(), angle_strs.join(","));
}
}
}
input
}
fn yaw_to_facing(yaw: f32) -> &'static str {
let normalized = ((yaw % 360.0) + 360.0) % 360.0;
if !(45.0..315.0).contains(&normalized) {
"south"
} else if (45.0..135.0).contains(&normalized) {
"west"
} else if (135.0..225.0).contains(&normalized) {
"north"
} else {
"east"
}
}
fn dye_color_name(color: u8) -> String {
match color {
0 => "white",
1 => "orange",
2 => "magenta",
3 => "light_blue",
4 => "yellow",
5 => "lime",
6 => "pink",
7 => "gray",
8 => "light_gray",
9 => "cyan",
10 => "purple",
11 => "blue",
12 => "brown",
13 => "green",
14 => "red",
15 => "black",
_ => "white",
}
.to_string()
}
fn collect_region_entities(
region: &Region,
blocks: &mut Vec<(MesherBlockPosition, u32)>,
palette: &mut Vec<InputBlock>,
min: &mut [f32; 3],
max: &mut [f32; 3],
) {
for entity in ®ion.entities {
let input_block = entity_to_input_block(entity);
let idx = palette.len() as u32;
palette.push(input_block);
let x = entity.position.0.floor() as i32;
let y = entity.position.1.floor() as i32;
let z = entity.position.2.floor() as i32;
let pos = MesherBlockPosition::new(x, y, z);
blocks.push((pos, idx));
min[0] = min[0].min(x as f32);
min[1] = min[1].min(y as f32);
min[2] = min[2].min(z as f32);
max[0] = max[0].max(x as f32 + 1.0);
max[1] = max[1].max(y as f32 + 1.0);
max[2] = max[2].max(z as f32 + 1.0);
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum MeshPhase {
BuildingAtlas,
MeshingChunks,
Complete,
}
#[derive(Clone, Debug)]
pub struct MeshProgress {
pub phase: MeshPhase,
pub chunks_done: u32,
pub chunks_total: u32,
pub vertices_so_far: u64,
pub triangles_so_far: u64,
}
pub fn build_global_atlas(
schematic: &UniversalSchematic,
pack: &ResourcePackSource,
config: &MeshConfig,
) -> Result<TextureAtlas> {
let mesher_config = config.to_mesher_config();
let mut unique_states: std::collections::HashSet<BlockState> = std::collections::HashSet::new();
for state in &schematic.default_region.palette {
if state.name != "minecraft:air" {
unique_states.insert(state.clone());
}
}
for region in schematic.other_regions.values() {
for state in ®ion.palette {
if state.name != "minecraft:air" {
unique_states.insert(state.clone());
}
}
}
let mut blocks = HashMap::new();
let mut pos_idx = 0i32;
for state in &unique_states {
let pos = MesherBlockPosition::new(pos_idx, 0, 0);
let input = block_state_to_input_block(state);
blocks.insert(pos, input);
pos_idx += 1; }
if blocks.is_empty() {
return Ok(TextureAtlas::empty());
}
let bounds = MesherBoundingBox::new([0.0, 0.0, 0.0], [pos_idx as f32, 1.0, 1.0]);
let mut discovery_config = mesher_config.clone();
discovery_config.cull_hidden_faces = false;
discovery_config.cull_occluded_blocks = false;
let source = ChunkBlockSource::new(blocks, bounds);
let mesher = Mesher::with_config(pack.pack.clone(), discovery_config);
let texture_refs = mesher.discover_textures(&source);
let mut atlas_builder =
AtlasBuilder::new(mesher_config.atlas_max_size, mesher_config.atlas_padding);
for texture_ref in &texture_refs {
if let Some(texture) = pack.pack.get_texture(texture_ref) {
atlas_builder.add_texture(texture_ref.clone(), texture.first_frame());
}
}
atlas_builder
.build()
.map_err(|e| MeshError::Meshing(e.to_string()))
}
impl UniversalSchematic {
fn compute_mesh_output(
&self,
pack: &ResourcePackSource,
config: &MeshConfig,
) -> Result<MesherOutput> {
let _prof = std::env::var("NUCLEATION_MESH_PROFILE").is_ok();
macro_rules! phase {
($t:expr, $label:expr) => {
if _prof {
if let Some(t) = $t {
eprintln!("PROFILE\t{}\t{}", $label, t.elapsed().as_micros());
}
}
};
}
let t0 = prof_now();
let mesher_config = config.to_mesher_config();
let mesher = Mesher::with_config(pack.pack.clone(), mesher_config);
phase!(t0, "config+pack_clone");
let t1 = prof_now();
let mut all_blocks: Vec<(MesherBlockPosition, u32)> = Vec::new();
let mut palette: Vec<InputBlock> = Vec::new();
let mut min = [f32::MAX; 3];
let mut max = [f32::MIN; 3];
collect_region_blocks(
&self.default_region,
&mut all_blocks,
&mut palette,
&mut min,
&mut max,
);
for region in self.other_regions.values() {
collect_region_blocks(region, &mut all_blocks, &mut palette, &mut min, &mut max);
}
phase!(t1, "collect_region_blocks");
if all_blocks.is_empty() {
return Err(MeshError::Meshing("No blocks to mesh".to_string()));
}
let t2 = prof_now();
let bounds = MesherBoundingBox::new(min, max);
let source = VecBlockSource::new(palette, all_blocks, bounds);
phase!(t2, "build_source");
let t3 = prof_now();
let out = mesher
.mesh(&source)
.map_err(|e| MeshError::Meshing(e.to_string()));
phase!(t3, "mesher.mesh");
out
}
pub fn to_mesh(&self, pack: &ResourcePackSource, config: &MeshConfig) -> Result<MeshOutput> {
let output = self.compute_mesh_output(pack, config)?;
Ok(mesh_output_from_mesher(output, None))
}
pub fn to_animated_glb(
&self,
pack: &ResourcePackSource,
timeline_json: &str,
) -> Result<Vec<u8>> {
let timeline: schematic_mesher::Timeline = serde_json::from_str(timeline_json)
.map_err(|e| MeshError::Meshing(format!("timeline parse: {e}")))?;
let mut blocks: Vec<(MesherBlockPosition, u32)> = Vec::new();
let mut palette: Vec<InputBlock> = Vec::new();
let mut min = [f32::MAX; 3];
let mut max = [f32::MIN; 3];
collect_region_blocks(
&self.default_region,
&mut blocks,
&mut palette,
&mut min,
&mut max,
);
for region in self.other_regions.values() {
collect_region_blocks(region, &mut blocks, &mut palette, &mut min, &mut max);
}
let initial: Vec<(MesherBlockPosition, InputBlock)> = blocks
.iter()
.map(|(pos, idx)| (*pos, palette[*idx as usize].clone()))
.collect();
schematic_mesher::build_animated_glb(&pack.pack, &initial, &timeline)
.map_err(|e| MeshError::Meshing(e.to_string()))
}
pub fn to_usdz(&self, pack: &ResourcePackSource, config: &MeshConfig) -> Result<MeshOutput> {
let output = self.compute_mesh_output(pack, config)?;
Ok(mesh_output_from_mesher(output, None))
}
pub fn to_raw_mesh(
&self,
pack: &ResourcePackSource,
config: &MeshConfig,
) -> Result<RawMeshExport> {
let output = self.compute_mesh_output(pack, config)?;
let raw = export_raw(&output);
Ok(RawMeshExport { inner: raw })
}
pub fn mesh_by_region(
&self,
pack: &ResourcePackSource,
config: &MeshConfig,
) -> Result<MultiMeshResult> {
let mesher_config = config.to_mesher_config();
let mut meshes = HashMap::new();
if let Some(result) = mesh_region(&pack.pack, &self.default_region, &mesher_config)? {
meshes.insert(self.default_region_name.clone(), result);
}
for (name, region) in &self.other_regions {
if let Some(result) = mesh_region(&pack.pack, region, &mesher_config)? {
meshes.insert(name.clone(), result);
}
}
Ok(meshes)
}
pub fn mesh_by_chunk(
&self,
pack: &ResourcePackSource,
config: &MeshConfig,
) -> Result<ChunkMeshResult> {
self.mesh_by_chunk_size(pack, config, 16)
}
pub fn mesh_by_chunk_size(
&self,
pack: &ResourcePackSource,
config: &MeshConfig,
chunk_size: i32,
) -> Result<ChunkMeshResult> {
let mesher_config = config.to_mesher_config();
let mut chunks: HashMap<(i32, i32, i32), HashMap<MesherBlockPosition, InputBlock>> =
HashMap::new();
collect_region_blocks_by_chunk(&self.default_region, &mut chunks, chunk_size);
for region in self.other_regions.values() {
collect_region_blocks_by_chunk(region, &mut chunks, chunk_size);
}
let mut meshes = HashMap::new();
let mut total_vertex_count = 0;
let mut total_triangle_count = 0;
for (chunk_coord, blocks) in chunks {
if blocks.is_empty() {
continue;
}
let mut min = [f32::MAX; 3];
let mut max = [f32::MIN; 3];
for pos in blocks.keys() {
min[0] = min[0].min(pos.x as f32);
min[1] = min[1].min(pos.y as f32);
min[2] = min[2].min(pos.z as f32);
max[0] = max[0].max(pos.x as f32 + 1.0);
max[1] = max[1].max(pos.y as f32 + 1.0);
max[2] = max[2].max(pos.z as f32 + 1.0);
}
let bounds = MesherBoundingBox::new(min, max);
let source = ChunkBlockSource::new(blocks, bounds);
let mesher = Mesher::with_config(pack.pack.clone(), mesher_config.clone());
let output = mesher
.mesh(&source)
.map_err(|e| MeshError::Meshing(e.to_string()))?;
let result = mesh_output_from_mesher(output, Some(chunk_coord));
total_vertex_count += result.total_vertices();
total_triangle_count += result.total_triangles();
meshes.insert(chunk_coord, result);
}
Ok(ChunkMeshResult {
meshes,
total_vertex_count,
total_triangle_count,
})
}
pub fn mesh_chunks_parallel(
&self,
pack: &ResourcePackSource,
config: &MeshConfig,
chunk_size: i32,
max_threads: usize,
) -> Result<Vec<MeshOutput>> {
let mesher_config = config.to_mesher_config();
let mut chunks: HashMap<(i32, i32, i32), HashMap<MesherBlockPosition, InputBlock>> =
HashMap::new();
collect_region_blocks_by_chunk(&self.default_region, &mut chunks, chunk_size);
for region in self.other_regions.values() {
collect_region_blocks_by_chunk(region, &mut chunks, chunk_size);
}
let chunk_list: Vec<_> = chunks.into_iter().filter(|(_, b)| !b.is_empty()).collect();
if chunk_list.is_empty() {
return Err(MeshError::Meshing("No blocks to mesh".to_string()));
}
let max_threads = max_threads.max(1);
let pack_ref = &pack.pack;
let config_ref = &mesher_config;
let results: Vec<Result<MeshOutput>> = std::thread::scope(|scope| {
let mut handles = Vec::new();
for batch in chunk_list.chunks(max_threads) {
let batch_handles: Vec<_> = batch
.iter()
.map(|(chunk_coord, blocks)| {
let coord = *chunk_coord;
scope.spawn(move || {
let mut min = [f32::MAX; 3];
let mut max = [f32::MIN; 3];
for pos in blocks.keys() {
min[0] = min[0].min(pos.x as f32);
min[1] = min[1].min(pos.y as f32);
min[2] = min[2].min(pos.z as f32);
max[0] = max[0].max(pos.x as f32 + 1.0);
max[1] = max[1].max(pos.y as f32 + 1.0);
max[2] = max[2].max(pos.z as f32 + 1.0);
}
let bounds = MesherBoundingBox::new(min, max);
let source = ChunkBlockSource::new(blocks.clone(), bounds);
let mesher = Mesher::with_config(pack_ref.clone(), config_ref.clone());
match mesher.mesh(&source) {
Ok(output) => Ok(mesh_output_from_mesher(output, Some(coord))),
Err(e) => Err(MeshError::Meshing(e.to_string())),
}
})
})
.collect();
for handle in batch_handles {
handles.push(handle.join().expect("Mesh thread panicked"));
}
}
handles
});
results.into_iter().collect()
}
pub fn mesh_chunks(
&self,
pack: &ResourcePackSource,
config: &MeshConfig,
chunk_size: i32,
) -> NucleationChunkIter {
let mut chunks: HashMap<(i32, i32, i32), HashMap<MesherBlockPosition, InputBlock>> =
HashMap::new();
collect_region_blocks_by_chunk(&self.default_region, &mut chunks, chunk_size);
for region in self.other_regions.values() {
collect_region_blocks_by_chunk(region, &mut chunks, chunk_size);
}
let chunk_list: Vec<_> = chunks.into_iter().filter(|(_, b)| !b.is_empty()).collect();
NucleationChunkIter {
chunks: chunk_list,
index: 0,
pack: pack.pack.clone(),
config: config.to_mesher_config(),
shared_atlas: None,
progress_callback: None,
vertices_so_far: 0,
triangles_so_far: 0,
}
}
pub fn mesh_chunks_with_atlas(
&self,
pack: &ResourcePackSource,
config: &MeshConfig,
chunk_size: i32,
atlas: TextureAtlas,
) -> NucleationChunkIter {
let mut chunks: HashMap<(i32, i32, i32), HashMap<MesherBlockPosition, InputBlock>> =
HashMap::new();
collect_region_blocks_by_chunk(&self.default_region, &mut chunks, chunk_size);
for region in self.other_regions.values() {
collect_region_blocks_by_chunk(region, &mut chunks, chunk_size);
}
let chunk_list: Vec<_> = chunks.into_iter().filter(|(_, b)| !b.is_empty()).collect();
NucleationChunkIter {
chunks: chunk_list,
index: 0,
pack: pack.pack.clone(),
config: config.to_mesher_config(),
shared_atlas: Some(atlas),
progress_callback: None,
vertices_so_far: 0,
triangles_so_far: 0,
}
}
}
pub struct NucleationChunkIter {
chunks: Vec<((i32, i32, i32), HashMap<MesherBlockPosition, InputBlock>)>,
index: usize,
pack: ResourcePack,
config: MesherConfig,
shared_atlas: Option<TextureAtlas>,
progress_callback: Option<Box<dyn Fn(MeshProgress)>>,
vertices_so_far: u64,
triangles_so_far: u64,
}
impl NucleationChunkIter {
pub fn chunk_count(&self) -> usize {
self.chunks.len()
}
pub fn chunks_yielded(&self) -> usize {
self.index
}
pub fn has_shared_atlas(&self) -> bool {
self.shared_atlas.is_some()
}
pub fn shared_atlas(&self) -> Option<&TextureAtlas> {
self.shared_atlas.as_ref()
}
pub fn set_progress_callback(&mut self, cb: Box<dyn Fn(MeshProgress)>) {
self.progress_callback = Some(cb);
}
}
impl Iterator for NucleationChunkIter {
type Item = Result<MeshOutput>;
fn next(&mut self) -> Option<Self::Item> {
if self.index >= self.chunks.len() {
return None;
}
let (chunk_coord, ref blocks) = self.chunks[self.index];
self.index += 1;
let mut min = [f32::MAX; 3];
let mut max = [f32::MIN; 3];
for pos in blocks.keys() {
min[0] = min[0].min(pos.x as f32);
min[1] = min[1].min(pos.y as f32);
min[2] = min[2].min(pos.z as f32);
max[0] = max[0].max(pos.x as f32 + 1.0);
max[1] = max[1].max(pos.y as f32 + 1.0);
max[2] = max[2].max(pos.z as f32 + 1.0);
}
let bounds = MesherBoundingBox::new(min, max);
let source = ChunkBlockSource::new(blocks.clone(), bounds);
let mut chunk_config = self.config.clone();
if let Some(ref atlas) = self.shared_atlas {
chunk_config.pre_built_atlas = Some(atlas.clone());
}
let mesher = Mesher::with_config(self.pack.clone(), chunk_config);
let output = match mesher.mesh(&source) {
Ok(o) => o,
Err(e) => return Some(Err(MeshError::Meshing(e.to_string()))),
};
let mesh = mesh_output_from_mesher(output, Some(chunk_coord));
self.vertices_so_far += mesh.total_vertices() as u64;
self.triangles_so_far += mesh.total_triangles() as u64;
if let Some(ref cb) = self.progress_callback {
cb(MeshProgress {
phase: if self.index >= self.chunks.len() {
MeshPhase::Complete
} else {
MeshPhase::MeshingChunks
},
chunks_done: self.index as u32,
chunks_total: self.chunks.len() as u32,
vertices_so_far: self.vertices_so_far,
triangles_so_far: self.triangles_so_far,
});
}
Some(Ok(mesh))
}
fn size_hint(&self) -> (usize, Option<usize>) {
let remaining = self.chunks.len() - self.index;
(remaining, Some(remaining))
}
}
fn mesh_region(
pack: &ResourcePack,
region: &Region,
config: &MesherConfig,
) -> Result<Option<MeshOutput>> {
let source = RegionBlockSource::new(region);
if source.blocks.is_empty() {
return Ok(None);
}
let mesher = Mesher::with_config(pack.clone(), config.clone());
let output = mesher
.mesh(&source)
.map_err(|e| MeshError::Meshing(e.to_string()))?;
Ok(Some(mesh_output_from_mesher(output, None)))
}
#[inline]
fn prof_now() -> Option<std::time::Instant> {
#[cfg(not(target_arch = "wasm32"))]
{
Some(std::time::Instant::now())
}
#[cfg(target_arch = "wasm32")]
{
None
}
}
fn collect_region_blocks(
region: &Region,
blocks: &mut Vec<(MesherBlockPosition, u32)>,
palette: &mut Vec<InputBlock>,
min: &mut [f32; 3],
max: &mut [f32; 3],
) {
use rayon::prelude::*;
let _prof = std::env::var("NUCLEATION_MESH_PROFILE").is_ok();
let base = palette.len() as u32;
let mut is_air: Vec<bool> = Vec::with_capacity(region.palette.len());
for state in ®ion.palette {
is_air.push(state.name == "minecraft:air");
palette.push(block_state_to_input_block(state));
}
let t_scan = prof_now();
let collected: Vec<(MesherBlockPosition, u32, i32, i32, i32)> = (0..region.volume())
.into_par_iter()
.filter_map(|index| {
let (x, y, z) = region.index_to_coords(index);
let idx = region.get_block_index(x, y, z)?;
if is_air[idx] {
return None;
}
Some((
MesherBlockPosition::new(x, y, z),
base + idx as u32,
x,
y,
z,
))
})
.collect();
if _prof {
if let Some(t) = t_scan {
eprintln!("PROFILE\t scan\t{}", t.elapsed().as_micros());
}
}
let t_ins = prof_now();
blocks.reserve(collected.len());
for (pos, gidx, x, y, z) in collected {
blocks.push((pos, gidx));
min[0] = min[0].min(x as f32);
min[1] = min[1].min(y as f32);
min[2] = min[2].min(z as f32);
max[0] = max[0].max(x as f32 + 1.0);
max[1] = max[1].max(y as f32 + 1.0);
max[2] = max[2].max(z as f32 + 1.0);
}
if _prof {
if let Some(t) = t_ins {
eprintln!("PROFILE\t vec_push\t{}", t.elapsed().as_micros());
}
}
collect_region_entities(region, blocks, palette, min, max);
}
fn collect_region_blocks_by_chunk(
region: &Region,
chunks: &mut HashMap<(i32, i32, i32), HashMap<MesherBlockPosition, InputBlock>>,
chunk_size: i32,
) {
for index in 0..region.volume() {
let (x, y, z) = region.index_to_coords(index);
if let Some(block_state) = region.get_block(x, y, z) {
if block_state.name != "minecraft:air" {
let chunk_x = x.div_euclid(chunk_size);
let chunk_y = y.div_euclid(chunk_size);
let chunk_z = z.div_euclid(chunk_size);
let chunk_blocks = chunks.entry((chunk_x, chunk_y, chunk_z)).or_default();
let pos = MesherBlockPosition::new(x, y, z);
let input_block = block_state_to_input_block(block_state);
chunk_blocks.insert(pos, input_block);
}
}
}
for entity in ®ion.entities {
let input_block = entity_to_input_block(entity);
let x = entity.position.0.floor() as i32;
let y = entity.position.1.floor() as i32;
let z = entity.position.2.floor() as i32;
let pos = MesherBlockPosition::new(x, y, z);
let chunk_x = x.div_euclid(chunk_size);
let chunk_y = y.div_euclid(chunk_size);
let chunk_z = z.div_euclid(chunk_size);
let chunk_blocks = chunks.entry((chunk_x, chunk_y, chunk_z)).or_default();
chunk_blocks.entry(pos).or_insert(input_block);
}
}
fn split_resource_name(name: &str) -> Result<(String, String)> {
match name.split_once(':') {
Some((ns, path)) => Ok((ns.to_string(), path.to_string())),
None => Ok(("minecraft".to_string(), name.to_string())),
}
}
fn blockstate_definition_to_json(def: &schematic_mesher::BlockstateDefinition) -> String {
match def {
schematic_mesher::BlockstateDefinition::Variants(variants) => {
let mut map = serde_json::Map::new();
let mut variants_map = serde_json::Map::new();
for (key, models) in variants {
if models.len() == 1 {
variants_map.insert(
key.clone(),
serde_json::to_value(&models[0]).unwrap_or_default(),
);
} else {
variants_map.insert(
key.clone(),
serde_json::to_value(models).unwrap_or_default(),
);
}
}
map.insert(
"variants".to_string(),
serde_json::Value::Object(variants_map),
);
serde_json::Value::Object(map).to_string()
}
schematic_mesher::BlockstateDefinition::Multipart(cases) => {
let mut map = serde_json::Map::new();
map.insert(
"multipart".to_string(),
serde_json::to_value(cases).unwrap_or_default(),
);
serde_json::Value::Object(map).to_string()
}
}
}
use crate::formats::manager::SchematicExporter;
pub struct MeshExporter {
pack: ResourcePackSource,
}
impl MeshExporter {
pub fn new(pack: ResourcePackSource) -> Self {
Self { pack }
}
}
impl SchematicExporter for MeshExporter {
fn name(&self) -> String {
"mesh".to_string()
}
fn extensions(&self) -> Vec<String> {
vec!["glb".into(), "usdz".into()]
}
fn available_versions(&self) -> Vec<String> {
vec!["glb".into(), "usdz".into()]
}
fn default_version(&self) -> String {
"glb".to_string()
}
fn write(
&self,
schematic: &crate::UniversalSchematic,
version: Option<&str>,
) -> crate::formats::error::Result<Vec<u8>> {
self.write_with_settings(schematic, version, None)
}
fn write_with_settings(
&self,
schematic: &crate::UniversalSchematic,
version: Option<&str>,
settings: Option<&str>,
) -> crate::formats::error::Result<Vec<u8>> {
let config: MeshConfig = match settings {
Some(s) => serde_json::from_str(s)?,
None => MeshConfig::default(),
};
let format = version.unwrap_or("glb");
match format {
"glb" => {
let mesh = schematic
.to_mesh(&self.pack, &config)
.map_err(|e| crate::formats::error::FormatError::Parse(e.to_string()))?;
mesh.to_glb()
.map_err(|e| crate::formats::error::FormatError::Parse(e.to_string()))
}
"usdz" => {
let mesh = schematic
.to_usdz(&self.pack, &config)
.map_err(|e| crate::formats::error::FormatError::Parse(e.to_string()))?;
mesh.to_usdz()
.map_err(|e| crate::formats::error::FormatError::Parse(e.to_string()))
}
_ => Err(format!("Unknown mesh format: {}. Use 'glb' or 'usdz'", format).into()),
}
}
fn export_settings_schema(&self) -> Option<String> {
serde_json::to_string_pretty(&MeshConfig::default()).ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mesh_config_builder() {
let config = MeshConfig::new()
.with_culling(false)
.with_ambient_occlusion(false)
.with_biome("swamp")
.with_ao_intensity(0.6);
assert!(!config.cull_hidden_faces);
assert!(!config.ambient_occlusion);
assert_eq!(config.biome, Some("swamp".to_string()));
assert!((config.ao_intensity - 0.6).abs() < 0.001);
}
#[test]
fn test_block_state_to_input_block() {
let block_state = BlockState::new("minecraft:oak_stairs".to_string())
.with_property("facing", "north")
.with_property("half", "bottom");
let input = block_state_to_input_block(&block_state);
assert_eq!(input.name, "minecraft:oak_stairs");
assert_eq!(input.properties.get("facing"), Some(&"north".to_string()));
assert_eq!(input.properties.get("half"), Some(&"bottom".to_string()));
}
#[test]
fn test_mesh_config_defaults() {
let config = MeshConfig::default();
assert!(config.cull_hidden_faces);
assert!(config.ambient_occlusion);
assert!((config.ao_intensity - 0.4).abs() < 0.001);
assert_eq!(config.biome, None);
assert_eq!(config.atlas_max_size, 4096);
assert!(config.cull_occluded_blocks);
assert!(!config.greedy_meshing);
}
#[test]
fn test_mesh_config_new_fields() {
let config = MeshConfig::new()
.with_cull_occluded_blocks(false)
.with_greedy_meshing(true)
.with_atlas_max_size(2048);
assert!(!config.cull_occluded_blocks);
assert!(config.greedy_meshing);
assert_eq!(config.atlas_max_size, 2048);
}
#[test]
fn test_mesh_config_full_builder_chain() {
let config = MeshConfig::new()
.with_culling(false)
.with_ambient_occlusion(false)
.with_ao_intensity(0.8)
.with_biome("jungle")
.with_atlas_max_size(1024)
.with_cull_occluded_blocks(false)
.with_greedy_meshing(true);
assert!(!config.cull_hidden_faces);
assert!(!config.ambient_occlusion);
assert!((config.ao_intensity - 0.8).abs() < 0.001);
assert_eq!(config.biome, Some("jungle".to_string()));
assert_eq!(config.atlas_max_size, 1024);
assert!(!config.cull_occluded_blocks);
assert!(config.greedy_meshing);
}
#[test]
fn test_split_resource_name_with_namespace() {
let (ns, path) = split_resource_name("minecraft:stone").unwrap();
assert_eq!(ns, "minecraft");
assert_eq!(path, "stone");
}
#[test]
fn test_split_resource_name_without_namespace() {
let (ns, path) = split_resource_name("stone").unwrap();
assert_eq!(ns, "minecraft");
assert_eq!(path, "stone");
}
#[test]
fn test_split_resource_name_custom_namespace() {
let (ns, path) = split_resource_name("mymod:block/custom_block").unwrap();
assert_eq!(ns, "mymod");
assert_eq!(path, "block/custom_block");
}
#[test]
fn test_block_state_to_input_block_no_properties() {
let block_state = BlockState::new("minecraft:stone".to_string());
let input = block_state_to_input_block(&block_state);
assert_eq!(input.name, "minecraft:stone");
assert!(input.properties.is_empty());
}
#[test]
fn test_to_mesher_config_propagates_fields() {
let config = MeshConfig::new()
.with_culling(false)
.with_ambient_occlusion(false)
.with_ao_intensity(0.7)
.with_cull_occluded_blocks(false)
.with_greedy_meshing(true);
let mesher_config = config.to_mesher_config();
assert!(!mesher_config.cull_hidden_faces);
assert!(!mesher_config.ambient_occlusion);
assert!((mesher_config.ao_intensity - 0.7).abs() < 0.001);
assert!(!mesher_config.cull_occluded_blocks);
assert!(mesher_config.greedy_meshing);
}
#[test]
fn test_empty_schematic_mesh_error() {
let _schematic = UniversalSchematic::new("Empty".to_string());
let result = ResourcePackSource::from_bytes(&[0, 1, 2, 3]);
assert!(result.is_err());
}
#[test]
fn test_resource_pack_stats() {
let result = ResourcePackSource::from_bytes(&[]);
assert!(result.is_err());
}
#[test]
fn test_from_bytes_list_empty_is_empty_pack() {
let empty: Vec<Vec<u8>> = Vec::new();
let pack = ResourcePackSource::from_bytes_list(empty).expect("empty list is valid");
let stats = pack.stats();
assert_eq!(stats.blockstate_count, 0);
assert_eq!(stats.model_count, 0);
assert_eq!(stats.texture_count, 0);
}
#[test]
fn test_from_bytes_list_rejects_garbage() {
let bad_data = vec![vec![0u8, 1, 2, 3]];
let result = ResourcePackSource::from_bytes_list(bad_data);
assert!(result.is_err());
}
#[test]
fn test_overlay_via_resource_pack_wins_on_collision() {
use schematic_mesher::resource_pack::TextureData;
let mut low = ResourcePack::new();
low.add_texture(
"minecraft",
"block/stone",
TextureData::new(1, 1, vec![10, 10, 10, 255]),
);
let mut high = ResourcePack::new();
high.add_texture(
"minecraft",
"block/stone",
TextureData::new(1, 1, vec![250, 0, 0, 255]),
);
low.overlay(high);
let source = ResourcePackSource::from_resource_pack(low);
let pixels = source
.get_texture_pixels("minecraft:block/stone")
.expect("texture exists");
assert_eq!(pixels, &[250, 0, 0, 255]);
}
#[test]
fn test_mesh_layer_type_is_accessible() {
let layer = MeshLayer {
positions: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
normals: vec![[0.0, 0.0, 1.0]; 3],
uvs: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
colors: vec![[1.0, 1.0, 1.0, 1.0]; 3],
indices: vec![0, 1, 2],
};
assert_eq!(layer.vertex_count(), 3);
assert_eq!(layer.triangle_count(), 1);
assert!(!layer.is_empty());
}
#[test]
fn test_mesh_layer_empty() {
let empty = MeshLayer::default();
assert!(empty.is_empty());
assert_eq!(empty.vertex_count(), 0);
assert_eq!(empty.triangle_count(), 0);
}
#[test]
fn test_mesh_output_is_empty() {
use schematic_mesher::TextureAtlas;
let output = MeshOutput {
opaque: MeshLayer::default(),
cutout: MeshLayer::default(),
transparent: MeshLayer::default(),
atlas: TextureAtlas::empty(),
greedy_materials: Vec::new(),
animated_textures: Vec::new(),
bounds: MesherBoundingBox::new([0.0; 3], [0.0; 3]),
chunk_coord: None,
lod_level: 0,
};
assert!(output.is_empty());
assert_eq!(output.total_vertices(), 0);
assert_eq!(output.total_triangles(), 0);
}
#[test]
fn test_mesh_result_alias_compiles() {
use schematic_mesher::TextureAtlas;
fn takes_mesh_result(_r: &MeshResult) {}
fn takes_mesh_output(_r: &MeshOutput) {}
let output = MeshOutput {
opaque: MeshLayer::default(),
cutout: MeshLayer::default(),
transparent: MeshLayer::default(),
atlas: TextureAtlas::empty(),
greedy_materials: Vec::new(),
animated_textures: Vec::new(),
bounds: MesherBoundingBox::new([0.0; 3], [0.0; 3]),
chunk_coord: None,
lod_level: 0,
};
takes_mesh_result(&output);
takes_mesh_output(&output);
}
#[test]
fn test_multi_mesh_result_alias() {
let map: MultiMeshResult = HashMap::new();
assert!(map.is_empty());
}
#[test]
fn test_chunk_mesh_result_backward_compat() {
let result = ChunkMeshResult {
meshes: HashMap::new(),
total_vertex_count: 0,
total_triangle_count: 0,
};
assert!(result.meshes.is_empty());
}
#[test]
fn test_nucleation_chunk_iter_empty() {
let _iter_type_check: fn() -> NucleationChunkIter = || unreachable!();
}
}