From a6c40586444dc0ada4d1109759bb4a8158ca2f39 Mon Sep 17 00:00:00 2001 From: nokonoko1203 Date: Thu, 22 Aug 2024 21:13:33 +0900 Subject: [PATCH 1/3] calc atlas size for obj --- nusamai/src/sink/obj/mod.rs | 44 +++++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/nusamai/src/sink/obj/mod.rs b/nusamai/src/sink/obj/mod.rs index 1c63ef87..7dbf7430 100644 --- a/nusamai/src/sink/obj/mod.rs +++ b/nusamai/src/sink/obj/mod.rs @@ -390,27 +390,49 @@ impl DataSink for ObjSink { let texture_cache = TextureCache::new(100_000_000); let texture_size_cache = TextureSizeCache::new(); - // file output destination - let mut folder_path = self.output_path.clone(); - let base_folder_name = typename.replace(':', "_").to_string(); - folder_path.push(&base_folder_name); - - let texture_folder_name = "textures"; - let atlas_dir = folder_path.join(texture_folder_name); - std::fs::create_dir_all(&atlas_dir)?; + // Check the size of all the textures and calculate the power of 2 of the largest size + let mut max_width = 0; + let mut max_height = 0; + for feature in features.features.iter() { + for (_, orig_mat_id) in feature + .polygons + .iter() + .zip_eq(feature.polygon_material_ids.iter()) + { + let mat = feature.materials[*orig_mat_id as usize].clone(); + let t = mat.base_texture.clone(); + if let Some(base_texture) = t { + let texture_uri = base_texture.uri.to_file_path().unwrap(); + let texture_size = texture_size_cache.get_or_insert(&texture_uri); + max_width = max_width.max(texture_size.0); + max_height = max_height.max(texture_size.1); + } + } + } + let max_width = max_width.next_power_of_two(); + let max_height = max_height.next_power_of_two(); // initialize texture packer - // In rare cases, large textures also exist, so the maximum texture size is set to 8096x8096. let config = TexturePlacerConfig { - width: 8096, - height: 8096, + width: max_width, + height: max_height, padding: 0, }; + let placer = GuillotineTexturePlacer::new(config.clone()); let exporter = JpegAtlasExporter::default(); let ext = exporter.clone().get_extension().to_string(); let packer = Mutex::new(TexturePacker::new(placer, exporter)); + // file output destination + let mut folder_path = self.output_path.clone(); + let base_folder_name = typename.replace(':', "_").to_string(); + folder_path.push(&base_folder_name); + + let texture_folder_name = "textures"; + let atlas_dir = folder_path.join(texture_folder_name); + std::fs::create_dir_all(&atlas_dir)?; + let atlas_packing_start = Instant::now(); // Coordinate transformation From 2e548862e33e98e4bbd7f79c09f3cbeacd9a8200 Mon Sep 17 00:00:00 2001 From: nokonoko1203 Date: Thu, 22 Aug 2024 21:18:10 +0900 Subject: [PATCH 2/3] remove Instant --- nusamai/src/sink/cesiumtiles/mod.rs | 1 - nusamai/src/sink/obj/mod.rs | 21 --------------------- 2 files changed, 22 deletions(-) diff --git a/nusamai/src/sink/cesiumtiles/mod.rs b/nusamai/src/sink/cesiumtiles/mod.rs index 8a016b76..c733b6d0 100644 --- a/nusamai/src/sink/cesiumtiles/mod.rs +++ b/nusamai/src/sink/cesiumtiles/mod.rs @@ -423,7 +423,6 @@ fn tile_writing_stage( let max_height = max_height.next_power_of_two(); // initialize texture packer - // In rare cases, large textures also exist, so the maximum texture size is set to 8096x8096. let config = TexturePlacerConfig { width: max_width, height: max_height, diff --git a/nusamai/src/sink/obj/mod.rs b/nusamai/src/sink/obj/mod.rs index 7dbf7430..6e8f5b53 100644 --- a/nusamai/src/sink/obj/mod.rs +++ b/nusamai/src/sink/obj/mod.rs @@ -6,7 +6,6 @@ use std::{ f64::consts::FRAC_PI_2, path::PathBuf, sync::{mpsc, Mutex}, - time::Instant, }; use ahash::{HashMap, HashMapExt}; @@ -216,8 +215,6 @@ impl DataSink for ObjSink { } fn run(&mut self, upstream: Receiver, feedback: &Feedback, _schema: &Schema) -> Result<()> { - let preprocessing_start = Instant::now(); - let ellipsoid = nusamai_projection::ellipsoid::wgs84(); let classified_features: Mutex = Default::default(); @@ -376,9 +373,6 @@ impl DataSink for ObjSink { }; let _ = transform_matrix.inverse(); - let duration = preprocessing_start.elapsed(); - feedback.info(format!("preprocessing {:?}", duration)); - // Create the information needed to output an OBJ file and write it to a file classified_features .into_par_iter() @@ -433,8 +427,6 @@ impl DataSink for ObjSink { let atlas_dir = folder_path.join(texture_folder_name); std::fs::create_dir_all(&atlas_dir)?; - let atlas_packing_start = Instant::now(); - // Coordinate transformation { for feature in features.features.iter_mut() { @@ -646,20 +638,10 @@ impl DataSink for ObjSink { all_materials.insert(material_key, feature_material); } - let duration = atlas_packing_start.elapsed(); - feedback.info(format!("atlas packing process {:?}", duration)); - - let atlas_export_start = Instant::now(); - packer.export(&atlas_dir, &texture_cache, config.width, config.height); - let duration = atlas_export_start.elapsed(); - feedback.info(format!("atlas export process {:?}", duration)); - feedback.ensure_not_canceled()?; - let obj_export_start = Instant::now(); - // Write OBJ file write( all_meshes, @@ -668,9 +650,6 @@ impl DataSink for ObjSink { self.obj_options.is_split, )?; - let duration = obj_export_start.elapsed(); - feedback.info(format!("obj export process {:?}", duration)); - Ok::<(), PipelineError>(()) })?; From 067a24695f72ea2ab2b7593e6bdac6fb57b6b927 Mon Sep 17 00:00:00 2001 From: nokonoko1203 Date: Thu, 22 Aug 2024 21:49:29 +0900 Subject: [PATCH 3/3] atlas --- nusamai/src/sink/gltf/mod.rs | 157 +++++++++++++++++++++++++++++++---- 1 file changed, 143 insertions(+), 14 deletions(-) diff --git a/nusamai/src/sink/gltf/mod.rs b/nusamai/src/sink/gltf/mod.rs index f1bb670c..a7931714 100644 --- a/nusamai/src/sink/gltf/mod.rs +++ b/nusamai/src/sink/gltf/mod.rs @@ -6,6 +6,12 @@ use std::{f64::consts::FRAC_PI_2, fs::File, io::BufWriter, path::PathBuf, sync:: use crate::sink::cesiumtiles::utils::calculate_normal; use ahash::{HashMap, HashSet, RandomState}; +use atlas_packer::{ + export::{AtlasExporter as _, JpegAtlasExporter}, + pack::TexturePacker, + place::{GuillotineTexturePlacer, PlacedTextureInfo, TexturePlacerConfig}, + texture::{CroppedTexture, DownsampleFactor, TextureCache, TextureSizeCache}, +}; use earcut::{utils3d::project3d_to_2d, Earcut}; use flatgeom::MultiPolygon; use glam::{DMat4, DVec3, DVec4}; @@ -18,6 +24,8 @@ use nusamai_plateau::appearance; use nusamai_projection::cartesian::geodetic_to_geocentric; use rayon::iter::{IntoParallelIterator, ParallelBridge, ParallelIterator}; use serde::{Deserialize, Serialize}; +use tempfile::tempdir; +use url::Url; use crate::{ get_parameter_value, @@ -333,16 +341,57 @@ impl DataSink for GltfSink { }; let _ = transform_matrix.inverse(); + // Texture cache + // use default cache size + let texture_cache = TextureCache::new(100_000_000); + let texture_size_cache = TextureSizeCache::new(); + classified_features .into_par_iter() .try_for_each(|(typename, mut features)| { feedback.ensure_not_canceled()?; - // Triangulation - let mut earcutter: Earcut = Earcut::new(); - let mut buf3d: Vec<[f64; 3]> = Vec::new(); - let mut buf2d: Vec<[f64; 2]> = Vec::new(); // 2d-projected [x, y] - let mut index_buf: Vec = Vec::new(); + // Use a temporary directory for embedding in glb. + let binding = tempdir().unwrap(); + let folder_path = binding.path(); + let texture_folder_name = "textures"; + let atlas_dir = folder_path.join(texture_folder_name); + std::fs::create_dir_all(&atlas_dir)?; + + // Check the size of all the textures and calculate the power of 2 of the largest size + let mut max_width = 0; + let mut max_height = 0; + for feature in features.features.iter() { + feedback.ensure_not_canceled()?; + + for (_, orig_mat_id) in feature + .polygons + .iter() + .zip_eq(feature.polygon_material_ids.iter()) + { + let mat = feature.materials[*orig_mat_id as usize].clone(); + let t = mat.base_texture.clone(); + if let Some(base_texture) = t { + let texture_uri = base_texture.uri.to_file_path().unwrap(); + let texture_size = texture_size_cache.get_or_insert(&texture_uri); + max_width = max_width.max(texture_size.0); + max_height = max_height.max(texture_size.1); + } + } + } + let max_width = max_width.next_power_of_two(); + let max_height = max_height.next_power_of_two(); + + // initialize texture packer + let config = TexturePlacerConfig { + width: max_width, + height: max_height, + padding: 0, + }; + let placer = GuillotineTexturePlacer::new(config.clone()); + let exporter = JpegAtlasExporter::default(); + let ext = exporter.clone().get_extension().to_string(); + let packer = Mutex::new(TexturePacker::new(placer, exporter)); let mut vertices: IndexSet<[u32; 9], RandomState> = IndexSet::default(); // [x, y, z, nx, ny, nz, u, v, feature_id] let mut primitives: Primitives = Default::default(); @@ -366,8 +415,7 @@ impl DataSink for GltfSink { let v_enu = transform_matrix * v_xyz; // println!("enu: {:?}", v_enu); - // flip the texture v-coordinate - [v_enu[0], v_enu[1], v_enu[2], u, 1.0 - v] + [v_enu[0], v_enu[1], v_enu[2], u, v] }); // Encode properties @@ -379,23 +427,98 @@ impl DataSink for GltfSink { continue; } - for (poly, orig_mat_id) in feature + for (poly_count, (mut poly, orig_mat_id)) in feature .polygons .iter() .zip_eq(feature.polygon_material_ids.iter()) + .enumerate() { - let num_outer = match poly.hole_indices().first() { - Some(&v) => v as usize, - None => poly.raw_coords().len(), - }; + let mut mat = feature.materials[*orig_mat_id as usize].clone(); + let t = mat.base_texture.clone(); + if let Some(base_texture) = t { + // texture packing + let original_vertices = poly + .raw_coords() + .iter() + .map(|[x, y, z, u, v]| (*x, *y, *z, *u, *v)) + .collect::>(); + let uv_coords = original_vertices + .iter() + .map(|(_, _, _, u, v)| (*u, *v)) + .collect::>(); + + let texture_uri = base_texture.uri.to_file_path().unwrap(); + let texture_size = texture_size_cache.get_or_insert(&texture_uri); + let downsample_factor = DownsampleFactor::new(&1.0); + let cropped_texture = CroppedTexture::new( + &texture_uri, + texture_size, + &uv_coords, + downsample_factor, + ); - let mat = feature.materials[*orig_mat_id as usize].clone(); + // Unique id required for placement in atlas + let base_name = typename.replace(':', "_"); + let texture_id = format!("{}_{}_{}", base_name, feature_id, poly_count); + let info: PlacedTextureInfo = packer + .lock() + .unwrap() + .add_texture(texture_id, cropped_texture); + + let atlas_placed_uv_coords = info + .placed_uv_coords + .iter() + .map(|(u, v)| ({ *u }, { *v })) + .collect::>(); + let updated_vertices = original_vertices + .iter() + .zip(atlas_placed_uv_coords.iter()) + .map(|((x, y, z, _, _), (u, v))| (*x, *y, *z, *u, *v)) + .collect::>(); + + // Apply the UV coordinates placed in the atlas to the original polygon + poly.transform_inplace(|&[x, y, z, _, _]| { + let (u, v) = updated_vertices + .iter() + .find(|(x_, y_, z_, _, _)| { + (*x_ - x).abs() < 1e-6 + && (*y_ - y).abs() < 1e-6 + && (*z_ - z).abs() < 1e-6 + }) + .map(|(_, _, _, u, v)| (*u, *v)) + .unwrap(); + [x, y, z, u, v] + }); + + let atlas_file_name = info.atlas_id.to_string(); + + let atlas_uri = + atlas_dir.join(atlas_file_name).with_extension(ext.clone()); + + // update material + mat = material::Material { + base_color: mat.base_color, + base_texture: Some(material::Texture { + uri: Url::from_file_path(atlas_uri).unwrap(), + }), + }; + } let primitive = primitives.entry(mat).or_default(); primitive.feature_ids.insert(feature_id as u32); if let Some((nx, ny, nz)) = calculate_normal(poly.exterior().iter().map(|v| [v[0], v[1], v[2]])) { + // Triangulation + let num_outer = match poly.hole_indices().first() { + Some(&v) => v as usize, + None => poly.raw_coords().len(), + }; + let mut earcutter: Earcut = Earcut::new(); + let mut buf3d: Vec<[f64; 3]> = Vec::new(); + let mut buf2d: Vec<[f64; 2]> = Vec::new(); // 2d-projected [x, y] + let mut index_buf: Vec = Vec::new(); + buf3d.clear(); buf3d.extend(poly.raw_coords().iter().map(|c| [c[0], c[1], c[2]])); @@ -418,7 +541,8 @@ impl DataSink for GltfSink { (ny as f32).to_bits(), (nz as f32).to_bits(), (u as f32).to_bits(), - (v as f32).to_bits(), + // flip the texture v-coordinate + (1.0 - v as f32).to_bits(), (feature_id as f32).to_bits(), // UNSIGNED_INT can't be used for vertex attribute ]; let (index, _) = vertices.insert_full(vbits); @@ -433,6 +557,11 @@ impl DataSink for GltfSink { // Ensure that the parent directory exists std::fs::create_dir_all(&self.output_path)?; + let mut packer = packer.into_inner().unwrap(); + packer.finalize(); + + packer.export(&atlas_dir, &texture_cache, config.width, config.height); + // Write glTF (.glb) let file_path = { let filename = format!("{}.glb", typename.replace(':', "_"));