Schematic Mesher
A Rust library for generating 3D meshes from Minecraft block data. Takes blocks and a resource pack as input, outputs GLB/glTF meshes with texture atlases.
Features
- Generate triangle meshes from Minecraft blocks
- Automatic texture atlas generation
- Face culling between adjacent opaque blocks
- Transparency handling (separate opaque/transparent geometry)
- Biome-aware tinting (grass, foliage, water, redstone)
- Ambient occlusion
- Multiple output formats: GLB, OBJ, raw mesh data
- WASM support (optional)
Installation
Add to your Cargo.toml:
[dependencies]
schematic-mesher = { git = "https://github.com/your-username/schematic-mesher" }
For CLI usage:
[dependencies]
schematic-mesher = { git = "...", features = ["cli"] }
CLI Usage
Build and install with the cli feature:
cargo install --path . --features cli
Commands
Mesh a single block (for testing):
schematic-mesher block \
--resource-pack path/to/pack.zip \
--block minecraft:stone \
--output stone.glb
With block properties:
schematic-mesher block \
--resource-pack pack.zip \
--block minecraft:oak_stairs \
--properties "facing=north,half=bottom,shape=straight" \
--output stairs.glb
Mesh blocks from JSON input:
schematic-mesher mesh \
--resource-pack pack.zip \
--input blocks.json \
--output scene.glb
Input JSON format:
{
"bounds": {
"min": [0, 0, 0],
"max": [16, 16, 16]
},
"blocks": [
{
"position": [0, 0, 0],
"name": "minecraft:stone",
"properties": {}
},
{
"position": [1, 0, 0],
"name": "minecraft:grass_block",
"properties": { "snowy": "false" }
}
]
}
Show resource pack info:
schematic-mesher info --resource-pack pack.zip
CLI Options
| Option |
Description |
--format |
Output format: glb (default), obj |
--biome |
Biome for tinting: plains, forest, swamp, etc. |
--no-cull |
Disable face culling |
--no-ao |
Disable ambient occlusion |
Library Usage
Basic Example
use schematic_mesher::{
load_resource_pack, Mesher, MesherConfig,
export_glb, InputBlock, BlockPosition, BoundingBox,
};
fn main() -> schematic_mesher::Result<()> {
let pack = load_resource_pack("path/to/pack.zip")?;
let mesher = Mesher::new(pack);
let blocks = vec![
(BlockPosition::new(0, 0, 0), InputBlock::new("minecraft:stone")),
(BlockPosition::new(1, 0, 0), InputBlock::new("minecraft:dirt")),
(BlockPosition::new(0, 1, 0), InputBlock::new("minecraft:grass_block")),
];
let bounds = BoundingBox::new([0, 0, 0], [2, 2, 1]);
let output = mesher.mesh_blocks(
blocks.iter().map(|(pos, block)| (*pos, block)),
bounds,
)?;
let glb_bytes = export_glb(&output)?;
std::fs::write("output.glb", glb_bytes)?;
Ok(())
}
Using BlockSource Trait
For larger datasets, implement the BlockSource trait:
use schematic_mesher::{BlockSource, BlockPosition, BoundingBox, InputBlock};
struct MySchematic {
blocks: Vec<(BlockPosition, InputBlock)>,
bounds: BoundingBox,
}
impl BlockSource for MySchematic {
fn bounds(&self) -> BoundingBox {
self.bounds
}
fn get_block(&self, pos: BlockPosition) -> Option<&InputBlock> {
self.blocks.iter()
.find(|(p, _)| *p == pos)
.map(|(_, b)| b)
}
fn iter_blocks(&self) -> Box<dyn Iterator<Item = (BlockPosition, &InputBlock)> + '_> {
Box::new(self.blocks.iter().map(|(p, b)| (*p, b)))
}
}
Configuration
use schematic_mesher::{Mesher, MesherConfig, TintProvider};
let config = MesherConfig {
cull_hidden_faces: true, atlas_max_size: 4096, atlas_padding: 1, include_air: false, ambient_occlusion: true, ao_intensity: 0.4, tint_provider: TintProvider::for_biome("plains"),
};
let mesher = Mesher::with_config(pack, config);
Working with MesherOutput
The mesher returns separate opaque and transparent meshes for correct rendering:
let output = mesher.mesh(&source)?;
let opaque_mesh = &output.opaque_mesh; let transparent_mesh = &output.transparent_mesh;
let combined = output.mesh();
let atlas = &output.atlas;
let png_bytes = atlas.to_png()?;
println!("Vertices: {}", output.total_vertices());
println!("Triangles: {}", output.total_triangles());
println!("Has transparency: {}", output.has_transparency());
Export Formats
GLB (recommended):
use schematic_mesher::export_glb;
let glb_bytes = export_glb(&output)?;
OBJ + MTL:
use schematic_mesher::{export_obj, ObjExport};
let (obj_content, mtl_content) = export_obj(&output, "my_mesh")?;
let export = ObjExport::from_output(&output, "my_mesh")?;
std::fs::write("mesh.obj", &export.obj)?;
std::fs::write("mesh.mtl", &export.mtl)?;
std::fs::write("mesh_atlas.png", &export.texture_png)?;
Raw mesh data:
use schematic_mesher::export_raw;
let raw = export_raw(&output);
let positions: &[[f32; 3]] = &raw.positions;
let normals: &[[f32; 3]] = &raw.normals;
let uvs: &[[f32; 2]] = &raw.uvs;
let colors: &[[f32; 4]] = &raw.colors;
let indices: &[u32] = &raw.indices;
let pos_flat: Vec<f32> = raw.positions_flat();
let norm_flat: Vec<f32> = raw.normals_flat();
Integration with Nucleation
For integration with Nucleation schematic library:
use nucleation::UniversalSchematic;
use schematic_mesher::{
load_resource_pack, Mesher, InputBlock, BlockPosition, BoundingBox,
};
fn mesh_schematic(schematic: &UniversalSchematic, pack_path: &str) -> schematic_mesher::Result<Vec<u8>> {
let pack = load_resource_pack(pack_path)?;
let mesher = Mesher::new(pack);
let mut blocks = Vec::new();
for region in schematic.regions() {
for (pos, block_state) in region.iter_blocks() {
let input_block = InputBlock {
name: block_state.name.clone(),
properties: block_state.properties.clone(),
};
blocks.push((BlockPosition::new(pos.x, pos.y, pos.z), input_block));
}
}
let bounds = BoundingBox::new(
[0, 0, 0],
[schematic.width() as i32, schematic.height() as i32, schematic.length() as i32],
);
let output = mesher.mesh_blocks(
blocks.iter().map(|(p, b)| (*p, b)),
bounds,
)?;
export_glb(&output)
}
Chunk-based Meshing
For large schematics, mesh in chunks to manage memory:
const CHUNK_SIZE: i32 = 16;
fn mesh_in_chunks(schematic: &UniversalSchematic, mesher: &Mesher) -> Vec<MesherOutput> {
let mut outputs = Vec::new();
let width = schematic.width() as i32;
let height = schematic.height() as i32;
let length = schematic.length() as i32;
for cx in (0..width).step_by(CHUNK_SIZE as usize) {
for cy in (0..height).step_by(CHUNK_SIZE as usize) {
for cz in (0..length).step_by(CHUNK_SIZE as usize) {
let chunk_bounds = BoundingBox::new(
[cx, cy, cz],
[
(cx + CHUNK_SIZE).min(width),
(cy + CHUNK_SIZE).min(height),
(cz + CHUNK_SIZE).min(length),
],
);
let chunk_blocks: Vec<_> = ;
if !chunk_blocks.is_empty() {
let output = mesher.mesh_blocks(
chunk_blocks.iter().map(|(p, b)| (*p, b)),
chunk_bounds,
).unwrap();
outputs.push(output);
}
}
}
}
outputs
}
Resource Pack Requirements
The resource pack should be a standard Minecraft Java Edition resource pack containing:
assets/
minecraft/
blockstates/ # Block state JSON files
models/
block/ # Block model JSON files
textures/
block/ # Block texture PNG files
Both ZIP files and extracted directories are supported.
Supported Block Features
- Standard cube blocks
- Rotated/oriented blocks (stairs, logs, etc.)
- Multi-part blocks (fences, walls, redstone)
- Transparent blocks (glass, ice, slime)
- Tinted blocks (grass, leaves, water, redstone)
- Custom models with arbitrary elements
Limitations
- Animated textures use first frame only
- No entity models (chests, signs, banners)
- No fluid rendering (water/lava flow shapes)
- No custom block entity rendering
License
AGPL-3.0-only