From 972f553c7c96c11d804fc1cde4cfd453f5beb8a1 Mon Sep 17 00:00:00 2001 From: Taku Fukada Date: Thu, 13 Jun 2024 09:22:48 +0900 Subject: [PATCH] minecraft: Update voxelizer + thematic surfaces (#564) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Description(変更内容) - ボクセルに任意の値を持たせられるようになった https://github.com/MIERUNE/dda-voxelize-rs/pull/6 ので、それを利用する。 - → 地物ごとにボクセライザを作る必要がなくなる。 - 上記のデモも兼ねて、Thematic surfaces の類(壁とか屋根とか窓とか)で塗り分けられるように変更する。 - ボクセライズアルゴリズムの都合で、窓より外側に壁が塗られてしまうこともある、などの問題はありますが、これの回避は難しそう。 ![Untitled](https://github.com/MIERUNE/plateau-gis-converter/assets/5351911/150016c4-955e-4c8c-a55d-6ae798b8bbeb) --- nusamai/Cargo.toml | 2 +- nusamai/src/sink/minecraft/block_colors.rs | 193 +++++++---- nusamai/src/sink/minecraft/mod.rs | 355 ++++++++++----------- 3 files changed, 295 insertions(+), 255 deletions(-) diff --git a/nusamai/Cargo.toml b/nusamai/Cargo.toml index 960dbe694..2ab6bcc0b 100644 --- a/nusamai/Cargo.toml +++ b/nusamai/Cargo.toml @@ -50,7 +50,7 @@ flate2 = "1.0.28" chrono = "0.4.35" kv-extsort = { git = "https://github.com/MIERUNE/kv-extsort-rs.git" } bytemuck = { version = "1.16.0", features = ["derive"] } -dda-voxelize = "0.1.0" +dda-voxelize = "0.2.0-alpha.1" [dev-dependencies] rand = "0.8.5" diff --git a/nusamai/src/sink/minecraft/block_colors.rs b/nusamai/src/sink/minecraft/block_colors.rs index 923246793..8280ca811 100644 --- a/nusamai/src/sink/minecraft/block_colors.rs +++ b/nusamai/src/sink/minecraft/block_colors.rs @@ -1,78 +1,139 @@ use std::collections::HashMap; -pub fn get_block_for_typename() -> HashMap<&'static str, &'static str> { - let mut typename_blocks: HashMap<&'static str, &'static str> = HashMap::new(); +#[derive(Clone)] +pub struct Voxel { + pub block_name: &'static str, + pub priority: u8, +} - typename_blocks.insert("bldg:Building", "iron_block"); - typename_blocks.insert("tran:Road", "gray_wool"); - typename_blocks.insert("tran:Railway", "granite"); - typename_blocks.insert("tran:Track", "stone_bricks"); - typename_blocks.insert("tran:Square", "smooth_stone"); - typename_blocks.insert("uro:Waterway", "cyan_stained_glass"); - typename_blocks.insert("luse:LandUse", "coarse_dirt"); - typename_blocks.insert("frn:CityFurniture", "quartz_block"); - typename_blocks.insert("veg:PlantCover", "moss_block"); - typename_blocks.insert("veg:SolitaryVegetationObject", "oak_leaves"); - typename_blocks.insert("wtr:WaterBody", "water"); - typename_blocks.insert("dem:ReliefFeature", "stone"); - typename_blocks.insert("brid:Bridge", "polished_andesite"); - typename_blocks.insert("tun:Tunnel", "cobblestone"); - typename_blocks.insert("urf:UseDistrict", "green_stained_glass"); - typename_blocks.insert("urf:FirePreventionDistrict", "red_stained_glass"); - typename_blocks.insert("urf:SedimentDisasterProneArea", "yellow_stained_glass"); - typename_blocks.insert("urf:Zone", "magenta_stained_glass"); +impl Voxel { + fn new(block_name: &'static str, priority: u8) -> Voxel { + Voxel { + block_name, + priority, + } + } +} - typename_blocks +pub struct DefaultBlockResolver { + typename_blocks: HashMap<&'static str, Voxel>, } -#[cfg(test)] -mod tests { - use super::*; +impl DefaultBlockResolver { + pub fn new() -> DefaultBlockResolver { + let mut typename_blocks: HashMap<&'static str, Voxel> = HashMap::new(); - #[test] - fn test_get_typename_block() { - let typename_blocks = get_block_for_typename(); - assert_eq!(typename_blocks.get("bldg:Building"), Some(&"iron_block")); - assert_eq!(typename_blocks.get("tran:Road"), Some(&"gray_wool")); - assert_eq!(typename_blocks.get("tran:Railway"), Some(&"granite")); - assert_eq!(typename_blocks.get("tran:Track"), Some(&"stone_bricks")); - assert_eq!(typename_blocks.get("tran:Square"), Some(&"smooth_stone")); - assert_eq!( - typename_blocks.get("uro:Waterway"), - Some(&"cyan_stained_glass") - ); - assert_eq!(typename_blocks.get("luse:LandUse"), Some(&"coarse_dirt")); - assert_eq!( - typename_blocks.get("frn:CityFurniture"), - Some(&"quartz_block") - ); - assert_eq!(typename_blocks.get("veg:PlantCover"), Some(&"moss_block")); - assert_eq!( - typename_blocks.get("veg:SolitaryVegetationObject"), - Some(&"oak_leaves") + typename_blocks.insert( + "bldg:BuildingInstallation", + Voxel::new("light_gray_concrete", 40), ); - assert_eq!(typename_blocks.get("wtr:WaterBody"), Some(&"water")); - assert_eq!(typename_blocks.get("dem:ReliefFeature"), Some(&"stone")); - assert_eq!( - typename_blocks.get("brid:Bridge"), - Some(&"polished_andesite") - ); - assert_eq!(typename_blocks.get("tun:Tunnel"), Some(&"cobblestone")); - assert_eq!( - typename_blocks.get("urf:UseDistrict"), - Some(&"green_stained_glass") - ); - assert_eq!( - typename_blocks.get("urf:FirePreventionDistrict"), - Some(&"red_stained_glass") - ); - assert_eq!( - typename_blocks.get("urf:SedimentDisasterProneArea"), - Some(&"yellow_stained_glass") + typename_blocks.insert("bldg:OuterFloorSurface", Voxel::new("white_wool", 40)); + typename_blocks.insert("bldg:OuterCeilingSurface", Voxel::new("white_wool", 40)); + typename_blocks.insert("bldg:CeilingSurface", Voxel::new("white_wool", 20)); + typename_blocks.insert("bldg:FloorSurface", Voxel::new("white_wool", 20)); + typename_blocks.insert("bldg:WallSurface", Voxel::new("iron_block", 20)); + + typename_blocks.insert("tran:Road", Voxel::new("gray_wool", 10)); + typename_blocks.insert("tran:Railway", Voxel::new("granite", 10)); + typename_blocks.insert("tran:Track", Voxel::new("stone_bricks", 10)); + typename_blocks.insert("tran:Square", Voxel::new("smooth_stone", 10)); + + typename_blocks.insert("veg:PlantCover", Voxel::new("moss_block", 10)); + typename_blocks.insert("veg:SolitaryVegetationObject", Voxel::new("oak_leaves", 10)); + + typename_blocks.insert("uro:Waterway", Voxel::new("cyan_stained_glass", 10)); + typename_blocks.insert("luse:LandUse", Voxel::new("coarse_dirt", 10)); + typename_blocks.insert("dem:ReliefFeature", Voxel::new("stone", 10)); + typename_blocks.insert("urf:UseDistrict", Voxel::new("green_stained_glass", 1)); + typename_blocks.insert( + "urf:FirePreventionDistrict", + Voxel::new("red_stained_glass", 1), ); - assert_eq!( - typename_blocks.get("urf:Zone"), - Some(&"magenta_stained_glass") + typename_blocks.insert( + "urf:SedimentDisasterProneArea", + Voxel::new("yellow_stained_glass", 1), ); + typename_blocks.insert("urf:Zone", Voxel::new("magenta_stained_glass", 1)); + + DefaultBlockResolver { typename_blocks } + } + + pub fn resolve(&self, typename: &str) -> Option { + let (prefix, local_name) = typename.split_once(':')?; + + match local_name { + "ClosureSurface" => return None, + "InteriorWallSurface" => return None, + "Window" | "Door" => return Some(Voxel::new("cyan_stained_glass", 100)), + _ => {} + }; + + if let Some(voxel) = self.typename_blocks.get(typename) { + return Some(voxel.clone()); + } + + match prefix { + "bldg" => return Some(Voxel::new("iron_block", 30)), + "brid" => return Some(Voxel::new("polished_andesite", 30)), + "tun" => return Some(Voxel::new("cobblestone", 20)), + "frn" => return Some(Voxel::new("quartz_block", 20)), + "tran" => return Some(Voxel::new("gray_wool", 10)), + "wtr" => return Some(Voxel::new("water", 10)), + _ => {} + } + + Some(Voxel::new("white_wool", 0)) } } + +// #[cfg(test)] +// mod tests { +// use super::*; +// +// #[test] +// fn test_get_typename_block() { +// let typename_blocks = get_block_for_typename(); +// assert_eq!(typename_blocks.get("bldg:Building"), Some(&"iron_block")); +// assert_eq!(typename_blocks.get("tran:Road"), Some(&"gray_wool")); +// assert_eq!(typename_blocks.get("tran:Railway"), Some(&"granite")); +// assert_eq!(typename_blocks.get("tran:Track"), Some(&"stone_bricks")); +// assert_eq!(typename_blocks.get("tran:Square"), Some(&"smooth_stone")); +// assert_eq!( +// typename_blocks.get("uro:Waterway"), +// Some(&"cyan_stained_glass") +// ); +// assert_eq!(typename_blocks.get("luse:LandUse"), Some(&"coarse_dirt")); +// assert_eq!( +// typename_blocks.get("frn:CityFurniture"), +// Some(&"quartz_block") +// ); +// assert_eq!(typename_blocks.get("veg:PlantCover"), Some(&"moss_block")); +// assert_eq!( +// typename_blocks.get("veg:SolitaryVegetationObject"), +// Some(&"oak_leaves") +// ); +// assert_eq!(typename_blocks.get("wtr:WaterBody"), Some(&"water")); +// assert_eq!(typename_blocks.get("dem:ReliefFeature"), Some(&"stone")); +// assert_eq!( +// typename_blocks.get("brid:Bridge"), +// Some(&"polished_andesite") +// ); +// assert_eq!(typename_blocks.get("tun:Tunnel"), Some(&"cobblestone")); +// assert_eq!( +// typename_blocks.get("urf:UseDistrict"), +// Some(&"green_stained_glass") +// ); +// assert_eq!( +// typename_blocks.get("urf:FirePreventionDistrict"), +// Some(&"red_stained_glass") +// ); +// assert_eq!( +// typename_blocks.get("urf:SedimentDisasterProneArea"), +// Some(&"yellow_stained_glass") +// ); +// assert_eq!( +// typename_blocks.get("urf:Zone"), +// Some(&"magenta_stained_glass") +// ); +// } +// } diff --git a/nusamai/src/sink/minecraft/mod.rs b/nusamai/src/sink/minecraft/mod.rs index eda4b8302..19fdcc254 100644 --- a/nusamai/src/sink/minecraft/mod.rs +++ b/nusamai/src/sink/minecraft/mod.rs @@ -4,9 +4,9 @@ mod level; mod region; use log::error; -use std::{io::Write, path::PathBuf}; +use std::{io::Write, path::PathBuf, sync::Mutex}; -use dda_voxelize::{DdaVoxelizer, MeshVoxelizer}; +use dda_voxelize::DdaVoxelizer; use earcut::{utils3d::project3d_to_2d, Earcut}; use flate2::{write::GzEncoder, Compression}; use hashbrown::HashMap; @@ -22,11 +22,12 @@ use nusamai_projection::etmerc::ExtendedTransverseMercatorProjection; use crate::{ get_parameter_value, parameters::*, - pipeline::{Feedback, PipelineError, Receiver, Result}, + pipeline::{Feedback, Receiver, Result}, sink::{DataRequirements, DataSink, DataSinkProvider, SinkInfo}, + transformer::{self, TreeFlatteningSpec}, }; -use block_colors::get_block_for_typename; +use block_colors::{DefaultBlockResolver, Voxel}; use level::{Data, Level}; use region::{write_anvil, BlockData, ChunkData, Position2D, RegionData, SectionData, WorldData}; @@ -105,13 +106,16 @@ impl Default for BoundingVolume { impl DataSink for MinecraftSink { fn make_requirements(&self) -> DataRequirements { DataRequirements { + tree_flattening: TreeFlatteningSpec::Flatten { + feature: transformer::FeatureFlatteningOption::All, + data: transformer::DataFlatteningOption::None, + object: transformer::ObjectFlatteningOption::None, + }, ..Default::default() } } fn run(&mut self, upstream: Receiver, feedback: &Feedback, _schema: &Schema) -> Result<()> { - let (sender, receiver) = std::sync::mpsc::sync_channel(1000); - let mut world_data = WorldData::new(); let mut region_map: HashMap = HashMap::new(); let mut chunk_map: HashMap<(Position2D, Position2D), usize> = HashMap::new(); @@ -141,6 +145,8 @@ impl DataSink for MinecraftSink { global_bvol.update(&local_bvol); }); + log::info!("start voxelizing..."); + // Calculation of centre coordinates let center_lng = (global_bvol.min_lng + global_bvol.max_lng) / 2.0; let center_lat = (global_bvol.min_lat + global_bvol.max_lat) / 2.0; @@ -152,223 +158,196 @@ impl DataSink for MinecraftSink { &nusamai_projection::ellipsoid::grs80(), ); - let typename_block = get_block_for_typename(); + let typename_block = DefaultBlockResolver::new(); - let (ra, rb) = rayon::join( - || { - parcels - .into_par_iter() - .try_for_each_with(sender, |sender, parcel| { - feedback.ensure_not_canceled()?; + let voxelizer = Mutex::new(DdaVoxelizer::::new()); - let entity = parcel.entity; + // TODO: Scalable par-region processing with external sorting (map-reduce). - let Value::Object(obj) = &entity.root else { - error!("The root value is not an object"); - return Ok(()); - }; + parcels.into_par_iter().try_for_each(|parcel| { + feedback.ensure_not_canceled()?; - let ObjectStereotype::Feature { geometries, .. } = &obj.stereotype else { - return Ok(()); - }; + let entity = parcel.entity; + + let Value::Object(obj) = &entity.root else { + error!("The root value is not an object"); + return Result::<()>::Ok(()); + }; - let geom_store = entity.geometry_store.read().unwrap(); + let Some(voxel) = typename_block.resolve(&obj.typename) else { + return Ok(()); + }; - let mut earcutter = Earcut::::new(); - let mut buf3d: Vec<[f32; 3]> = Vec::new(); - let mut buf2d: Vec<[f32; 2]> = Vec::new(); - let mut index_buf: Vec = Vec::new(); + let ObjectStereotype::Feature { geometries, .. } = &obj.stereotype else { + return Ok(()); + }; - let vertices: Vec<_> = geom_store - .vertices - .iter() - .map(|v| match projection.project_forward(v[0], v[1], v[2]) { + let geom_store = entity.geometry_store.read().unwrap(); + + geometries.par_iter().for_each(|entry| match entry.ty { + GeometryType::Solid | GeometryType::Surface | GeometryType::Triangle => { + let mut earcutter = Earcut::::new(); + let mut buf3d: Vec<[f32; 3]> = Vec::new(); + let mut buf2d: Vec<[f32; 2]> = Vec::new(); + let mut index_buf: Vec = Vec::new(); + + for idx_poly in geom_store + .multipolygon + .iter_range(entry.pos as usize..(entry.pos + entry.len) as usize) + { + let poly = idx_poly.transform(|idx| geom_store.vertices[*idx as usize]); + let num_outer = match poly.hole_indices().first() { + Some(&v) => v as usize, + None => poly.raw_coords().len(), + }; + + buf3d.clear(); + buf3d.extend(poly.raw_coords().iter().map(|v| { + match projection.project_forward(v[0], v[1], v[2]) { // To match the Minecraft coordinate system, the y-coordinate is multiplied by -1 and replaced with z Ok((x, y, mut z)) => { // Set the minimum altitude to 0 by subtracting the minimum altitude of the bounding volume. z -= global_bvol.min_height.min(v[2]); - [x, z, -y] + [x as f32, z as f32, -y as f32] } Err(e) => { println!("conversion error: {:?}", e); - [f64::NAN, f64::NAN, f64::NAN] + [f32::NAN, f32::NAN, f32::NAN] } - }) - .collect(); - - let mut typename_map: HashMap = HashMap::new(); - - let mut voxelizer = DdaVoxelizer { - voxels: HashMap::new(), - }; + } + })); - geometries.iter().for_each(|entry| match entry.ty { - GeometryType::Solid - | GeometryType::Surface - | GeometryType::Triangle => { - for idx_poly in geom_store.multipolygon.iter_range( - entry.pos as usize..(entry.pos + entry.len) as usize, - ) { - let poly = idx_poly.transform(|idx| vertices[*idx as usize]); - let num_outer = match poly.hole_indices().first() { - Some(&v) => v as usize, - None => poly.raw_coords().len(), - }; - - buf3d.clear(); - buf3d.extend( - poly.raw_coords() - .iter() - .map(|v| [v[0] as f32, v[1] as f32, v[2] as f32]), + if project3d_to_2d(&buf3d, num_outer, &mut buf2d) { + earcutter.earcut( + buf2d.iter().cloned(), + poly.hole_indices(), + &mut index_buf, + ); + { + let mut voxelizer = voxelizer.lock().unwrap(); + for indx in index_buf.chunks_exact(3) { + voxelizer.add_triangle( + &[ + buf3d[indx[0] as usize], + buf3d[indx[1] as usize], + buf3d[indx[2] as usize], + ], + &|previous_value, _, _| match previous_value { + None => voxel.clone(), + Some(prev) => { + if voxel.priority > prev.priority { + voxel.clone() + } else { + prev.clone() + } + } + }, ); - - if project3d_to_2d(&buf3d, num_outer, &mut buf2d) { - earcutter.earcut( - buf2d.iter().cloned(), - poly.hole_indices(), - &mut index_buf, - ); - for indx in index_buf.chunks_exact(3) { - voxelizer.add_triangle(&[ - buf3d[indx[0] as usize], - buf3d[indx[1] as usize], - buf3d[indx[2] as usize], - ]); - } - } } } - GeometryType::Curve => { - // TODO: implement - } - GeometryType::Point => { - // TODO: implement - } - }); + } + } + } + GeometryType::Curve => { + // TODO: implement + } + GeometryType::Point => { + // TODO: implement + } + }); - typename_map.insert(obj.typename.to_string(), voxelizer); + Ok(()) + })?; - if sender.send(typename_map).is_err() { - return Err(PipelineError::Canceled); - }; + let voxels = voxelizer.into_inner().unwrap().finalize(); - Ok(()) - }) - }, - || { - receiver.into_iter().for_each(|feature| { - feature.iter().for_each(|(typename, voxelizer)| { - voxelizer.voxels.iter().for_each(|(pos, voxel)| { - // If the voxel color is white, the block id is determined by reference to the geographical type name. - let block_name = match voxel.color { - [255, 255, 255] => typename_block - .get(typename.as_str()) - .unwrap_or(&"white_wool"), - _ => "white_wool", - }; - - // TODO:Process for determining blocks from voxel colors. - - let [x, y, z] = pos; - - // Calculate region coordinates from x,y coordinates - let region_x = x.div_euclid(512); - let region_z = z.div_euclid(512); - - // Calculate chunk coordinates from x,y coordinates - let chunk_x = x.div_euclid(16); - let chunk_z = z.div_euclid(16); - - // Calculate the y-level of the section from the y-coordinate. - let section_y = (y + 64) / 16 - 4; - - // Create BlockData - // Coordinates relative to the blocks within a section (0-15) - let block_data = BlockData::new( - x.rem_euclid(16) as u8, - y.rem_euclid(16) as u8, - z.rem_euclid(16) as u8, - block_name.to_string(), - ); + voxels.iter().for_each(|(pos, voxel)| { + // TODO:Process for determining blocks from voxel colors. - let region_pos = [region_x, region_z]; - let chunk_pos = [chunk_x, chunk_z]; - - let region_index = *region_map.entry(region_pos).or_insert_with(|| { - world_data.push(RegionData { - position: region_pos, - chunks: Vec::new(), - }); - world_data.len() - 1 - }); - - let chunk_index = - *chunk_map.entry((region_pos, chunk_pos)).or_insert_with(|| { - world_data[region_index].chunks.push(ChunkData { - position: chunk_pos, - sections: Vec::new(), - }); - world_data[region_index].chunks.len() - 1 - }); - - let section_index = *section_map - .entry((region_pos, chunk_pos, section_y)) - .or_insert_with(|| { - world_data[region_index].chunks[chunk_index].sections.push( - SectionData { - y: section_y, - blocks: Vec::new(), - }, - ); - world_data[region_index].chunks[chunk_index].sections.len() - 1 - }); + let [x, y, z] = pos; - world_data[region_index].chunks[chunk_index].sections[section_index] - .blocks - .push(block_data); - }); - }); + // Calculate region coordinates from x,y coordinates + let region_x = x.div_euclid(512); + let region_z = z.div_euclid(512); + + // Calculate chunk coordinates from x,y coordinates + let chunk_x = x.div_euclid(16); + let chunk_z = z.div_euclid(16); + + // Calculate the y-level of the section from the y-coordinate. + let section_y = (y + 64) / 16 - 4; + + // Create BlockData + // Coordinates relative to the blocks within a section (0-15) + let block_data = BlockData::new( + x.rem_euclid(16) as u8, + y.rem_euclid(16) as u8, + z.rem_euclid(16) as u8, + voxel.block_name.to_string(), + ); + + let region_pos = [region_x, region_z]; + let chunk_pos = [chunk_x, chunk_z]; + + let region_index = *region_map.entry(region_pos).or_insert_with(|| { + world_data.push(RegionData { + position: region_pos, + chunks: Vec::new(), + }); + world_data.len() - 1 + }); + + let chunk_index = *chunk_map.entry((region_pos, chunk_pos)).or_insert_with(|| { + world_data[region_index].chunks.push(ChunkData { + position: chunk_pos, + sections: Vec::new(), }); + world_data[region_index].chunks.len() - 1 + }); - let mut file_path = self.output_path.clone(); - file_path.push("region"); - std::fs::create_dir_all(&file_path)?; + let section_index = *section_map + .entry((region_pos, chunk_pos, section_y)) + .or_insert_with(|| { + world_data[region_index].chunks[chunk_index] + .sections + .push(SectionData { + y: section_y, + blocks: Vec::new(), + }); + world_data[region_index].chunks[chunk_index].sections.len() - 1 + }); - world_data.iter().try_for_each(|region| -> Result<()> { - feedback.ensure_not_canceled()?; + world_data[region_index].chunks[chunk_index].sections[section_index] + .blocks + .push(block_data); + }); - write_anvil(region, &file_path)?; + let mut file_path = self.output_path.clone(); + file_path.push("region"); + std::fs::create_dir_all(&file_path)?; - Ok(()) - })?; + world_data.iter().try_for_each(|region| -> Result<()> { + feedback.ensure_not_canceled()?; - // write level.dat - let dir_name = self.output_path.file_name().unwrap().to_string_lossy(); + write_anvil(region, &file_path)?; - // Set the entered directory name as the level name - let data = Data { - level_name: Some(dir_name.to_string()), - ..Default::default() - }; + Ok(()) + })?; - let level_dat_file = std::fs::File::create(self.output_path.join("level.dat"))?; - let mut encoder = GzEncoder::new(level_dat_file, Compression::fast()); + // write level.dat + let dir_name = self.output_path.file_name().unwrap().to_string_lossy(); - let bytes = fastnbt::to_bytes(&Level { data }).unwrap(); - encoder.write_all(&bytes)?; + // Set the entered directory name as the level name + let data = Data { + level_name: Some(dir_name.to_string()), + ..Default::default() + }; - Ok(()) - }, - ); + let level_dat_file = std::fs::File::create(self.output_path.join("level.dat"))?; + let mut encoder = GzEncoder::new(level_dat_file, Compression::fast()); - match ra { - Ok(_) | Err(PipelineError::Canceled) => {} - Err(error) => feedback.fatal_error(error), - } - match rb { - Ok(_) | Err(PipelineError::Canceled) => {} - Err(error) => feedback.fatal_error(error), - } + let bytes = fastnbt::to_bytes(&Level { data }).unwrap(); + encoder.write_all(&bytes)?; Ok(()) }