diff --git a/nusamai/Cargo.toml b/nusamai/Cargo.toml index b5cba674..907c305e 100644 --- a/nusamai/Cargo.toml +++ b/nusamai/Cargo.toml @@ -52,6 +52,7 @@ kv-extsort = { git = "https://github.com/MIERUNE/kv-extsort-rs.git" } bytemuck = { version = "1.16.0", features = ["derive"] } dda-voxelize = "0.2.0-alpha.1" atlas-packer = { git = "https://github.com/MIERUNE/atlas-packer.git" } +# atlas-packer = { path = "../atlas_packer" }; tempfile = "3.10.1" glam = "0.29.0" diff --git a/nusamai/src/sink/cesiumtiles/mod.rs b/nusamai/src/sink/cesiumtiles/mod.rs index d8fde01f..c8684a97 100644 --- a/nusamai/src/sink/cesiumtiles/mod.rs +++ b/nusamai/src/sink/cesiumtiles/mod.rs @@ -17,7 +17,7 @@ use std::{ use ahash::RandomState; use atlas_packer::{ - export::{AtlasExporter as _, JpegAtlasExporter}, + export::{AtlasExporter as _, WebpAtlasExporter}, pack::AtlasPacker, place::{GuillotineTexturePlacer, TexturePlacerConfig}, texture::{ @@ -49,8 +49,11 @@ use crate::{ }; use utils::calculate_normal; -use super::option::{limit_texture_resolution_parameter, output_parameter}; use super::texture_resolution::get_texture_downsample_scale_of_polygon; +use super::{ + option::{limit_texture_resolution_parameter, output_parameter}, + texture_resolution::apply_downsample_factor, +}; pub struct CesiumTilesSinkProvider {} @@ -65,6 +68,32 @@ impl DataSinkProvider for CesiumTilesSinkProvider { fn sink_options(&self) -> Parameters { let mut params = Parameters::new(); params.define(output_parameter()); + params.define(ParameterDefinition { + key: "min_z".into(), + entry: ParameterEntry { + description: "Minumum zoom level".into(), + required: true, + parameter: ParameterType::Integer(IntegerParameter { + value: Some(15), + min: Some(0), + max: Some(20), + }), + label: Some("最小ズームレベル".into()), + }, + }); + params.define(ParameterDefinition { + key: "max_z".into(), + entry: ParameterEntry { + description: "Maximum zoom level".into(), + required: true, + parameter: ParameterType::Integer(IntegerParameter { + value: Some(18), + min: Some(0), + max: Some(20), + }), + label: Some("最大ズームレベル".into()), + }, + }); params.define(limit_texture_resolution_parameter(false)); params @@ -79,6 +108,8 @@ impl DataSinkProvider for CesiumTilesSinkProvider { fn create(&self, params: &Parameters) -> Box { let output_path = get_parameter_value!(params, "@output", FileSystemPath); + let min_z = get_parameter_value!(params, "min_z", Integer).unwrap() as u8; + let max_z = get_parameter_value!(params, "max_z", Integer).unwrap() as u8; let limit_texture_resolution = *get_parameter_value!(params, "limit_texture_resolution", Boolean); let transform_settings = self.transformer_options(); @@ -87,6 +118,8 @@ impl DataSinkProvider for CesiumTilesSinkProvider { output_path: output_path.as_ref().unwrap().into(), transform_settings, limit_texture_resolution, + min_z, + max_z, }) } } @@ -95,6 +128,8 @@ struct CesiumTilesSink { output_path: PathBuf, transform_settings: TransformerRegistry, limit_texture_resolution: Option, + min_z: u8, + max_z: u8, } impl DataSink for CesiumTilesSink { @@ -118,9 +153,8 @@ impl DataSink for CesiumTilesSink { let tile_id_conv = TileIdMethod::Hilbert; - // TODO: configurable - let min_zoom = 15; - let max_zoom = 18; + let min_zoom = self.min_z; + let max_zoom = self.max_z; let limit_texture_resolution = self.limit_texture_resolution; @@ -300,7 +334,7 @@ fn tile_writing_stage( // Texture cache // use default cache size - let texture_cache = TextureCache::new(100_000_000); + let texture_cache = TextureCache::new(200_000_000); let texture_size_cache = TextureSizeCache::new(); // Use a temporary directory for embedding in glb. @@ -344,7 +378,7 @@ fn tile_writing_stage( let geom_error = tiling::geometric_error(tile_zoom, tile_y); feedback.info(format!( "tile: z={tile_zoom}, x={tile_x}, y={tile_y} (lng: [{min_lng} => {max_lng}], \ - lat: [{min_lat} => {max_lat}) geometricError: {geom_error}" + lat: [{min_lat} => {max_lat}] geometricError: {geom_error}" )); let content_path = { let normalized_typename = typename.replace(':', "_"); @@ -369,51 +403,6 @@ fn tile_writing_stage( let mut metadata_encoder = metadata::MetadataEncoder::new(schema); - // 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 serialized_feat in feats.iter() { - feedback.ensure_not_canceled()?; - - let feature = { - let (feature, _): (SlicedFeature, _) = - bincode::serde::decode_from_slice(serialized_feat, bincode_config) - .map_err(|err| { - PipelineError::Other(format!( - "Failed to deserialize a sliced feature: {:?}", - err - )) - })?; - - feature - }; - - 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 - // To reduce unnecessary draw calls, set the lower limit for max_width and max_height to 4096 - let config = TexturePlacerConfig { - width: max_width.max(2048), - height: max_height.max(2048), - padding: 0, - }; - let packer = Mutex::new(AtlasPacker::default()); // transform features @@ -488,6 +477,10 @@ fn tile_writing_stage( format!("{}_{}_{}_{}_{}", z, x, y, feature_id, poly_count) }; + // 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; + // Load all textures into the Packer for (feature_id, feature) in features.iter().enumerate() { for (poly_count, (mat, poly)) in feature @@ -516,13 +509,17 @@ fn tile_writing_stage( let texture_uri = base_texture.uri.to_file_path().unwrap(); let texture_size = texture_size_cache.get_or_insert(&texture_uri); - let downsample_scale = get_texture_downsample_scale_of_polygon( - &original_vertices, - texture_size, - limit_texture_resolution, - ); - let factor = apply_downsample_factor(tile_zoom, downsample_scale as f32); + let downsample_scale = if limit_texture_resolution.unwrap_or(false) { + get_texture_downsample_scale_of_polygon( + &original_vertices, + texture_size, + ) as f32 + } else { + 1.0 + }; + let geom_error = tiling::geometric_error(tile_zoom, tile_y); + let factor = apply_downsample_factor(geom_error, downsample_scale as f32); let downsample_factor = DownsampleFactor::new(&factor); let cropped_texture = PolygonMappedTexture::new( &texture_uri, @@ -531,6 +528,12 @@ fn tile_writing_stage( downsample_factor, ); + let scaled_width = (texture_size.0 as f32 * factor) as u32; + let scaled_height = (texture_size.1 as f32 * factor) as u32; + + max_width = max_width.max(scaled_width); + max_height = max_height.max(scaled_height); + // Unique id required for placement in atlas let (z, x, y) = tile_id_conv.id_to_zxy(tile_id); let texture_id = generate_texture_id(z, x, y, feature_id, poly_count); @@ -543,13 +546,24 @@ fn tile_writing_stage( } } + let max_width = max_width.next_power_of_two(); + let max_height = max_height.next_power_of_two(); + + // initialize texture packer + // To reduce unnecessary draw calls, set the lower limit for max_width and max_height to 1024 + let config = TexturePlacerConfig { + width: max_width.max(1024), + height: max_height.max(1024), + padding: 0, + }; + let placer = GuillotineTexturePlacer::new(config.clone()); let packer = packer.into_inner().unwrap(); // Packing the loaded textures into an atlas let packed = packer.pack(placer); - let exporter = JpegAtlasExporter::default(); + let exporter = WebpAtlasExporter::default(); let ext = exporter.clone().get_extension().to_string(); // Obtain the UV coordinates placed in the atlas by specifying the ID @@ -725,13 +739,3 @@ fn tile_writing_stage( Ok(()) } - -fn apply_downsample_factor(z: u8, downsample_scale: f32) -> f32 { - let f = match z { - 0..=14 => 0.0, - 15..=16 => 0.25, - 17 => 0.5, - _ => 1.0, - }; - f * downsample_scale -} diff --git a/nusamai/src/sink/cesiumtiles/slice.rs b/nusamai/src/sink/cesiumtiles/slice.rs index 898838e2..1172564f 100644 --- a/nusamai/src/sink/cesiumtiles/slice.rs +++ b/nusamai/src/sink/cesiumtiles/slice.rs @@ -138,7 +138,7 @@ pub fn slice_to_tiles( tiling::scheme::zxy_from_lng_lat(zoom, lng_center, lat_center); tiling::scheme::geometric_error(zoom, y) }; - let threshold = geom_error * 2.0; // TODO: adjustable + let threshold = geom_error / 0.9; // TODO: adjustable if approx_dx < threshold && approx_dy < threshold && approx_dh < threshold diff --git a/nusamai/src/sink/gltf/mod.rs b/nusamai/src/sink/gltf/mod.rs index e99a7a93..cb353b36 100644 --- a/nusamai/src/sink/gltf/mod.rs +++ b/nusamai/src/sink/gltf/mod.rs @@ -451,11 +451,16 @@ impl DataSink for GltfSink { let texture_uri = base_texture.uri.to_file_path().unwrap(); let texture_size = texture_size_cache.get_or_insert(&texture_uri); - let downsample_scale = get_texture_downsample_scale_of_polygon( - &original_vertices, - texture_size, - self.limit_texture_resolution, - ) as f32; + + let downsample_scale = if self.limit_texture_resolution.unwrap_or(false) + { + get_texture_downsample_scale_of_polygon( + &original_vertices, + texture_size, + ) as f32 + } else { + 1.0 + }; let downsample_factor = DownsampleFactor::new(&downsample_scale); diff --git a/nusamai/src/sink/obj/mod.rs b/nusamai/src/sink/obj/mod.rs index d4be06c3..e9254e82 100644 --- a/nusamai/src/sink/obj/mod.rs +++ b/nusamai/src/sink/obj/mod.rs @@ -455,11 +455,16 @@ impl DataSink for ObjSink { let texture_uri = base_texture.uri.to_file_path().unwrap(); let texture_size = texture_size_cache.get_or_insert(&texture_uri); - let downsample_scale = get_texture_downsample_scale_of_polygon( - &original_vertices, - texture_size, - self.limit_texture_resolution, - ) as f32; + + let downsample_scale = if self.limit_texture_resolution.unwrap_or(false) + { + get_texture_downsample_scale_of_polygon( + &original_vertices, + texture_size, + ) as f32 + } else { + 1.0 + }; let downsample_factor = DownsampleFactor::new(&downsample_scale); diff --git a/nusamai/src/sink/texture_resolution.rs b/nusamai/src/sink/texture_resolution.rs index 1e988b14..b26171ae 100644 --- a/nusamai/src/sink/texture_resolution.rs +++ b/nusamai/src/sink/texture_resolution.rs @@ -1,5 +1,8 @@ -/// Limits the texture resolution based on the distance (meters) between the vertices of a polygon. -const MAX_TEXTURE_PIXELS_PER_METER: f64 = 30.0; +/// Limits the texture resolution based on the distance (in meters) between the vertices of the polygon. +/// The resolution of aerial photographs is usually between 10cm and 20cm. +/// The pixel resolution should be limited to around 10cm (0.1m), +/// but this means that signs and other objects will not be visible, so adjustments are necessary. +const MIN_METER_PER_PIXEL: f64 = 0.025; // WARN: This function has an equivalent in `atlas-packer/src/texture.rs`. fn uv_to_pixel_coords(uv_coords: &[(f64, f64)], width: u32, height: u32) -> Vec<(u32, u32)> { @@ -14,40 +17,70 @@ fn uv_to_pixel_coords(uv_coords: &[(f64, f64)], width: u32, height: u32) -> Vec< .collect() } +fn get_distance_par_pixel(vertices: &[(f64, f64, f64)], pixel_coords: &[(u32, u32)]) -> f64 { + let mut valid_scales = Vec::new(); + let epsilon = 1e-6; + + for i in 0..vertices.len() { + let j = (i + 1) % vertices.len(); + let (euc0, txl0) = (vertices[i], pixel_coords[i]); + let (euc1, txl1) = (vertices[j], pixel_coords[j]); + + // 3D Euclidean distance + let euc_dist = + ((euc0.0 - euc1.0).powi(2) + (euc0.1 - euc1.1).powi(2) + (euc0.2 - euc1.2).powi(2)) + .sqrt(); + + // 2D pixel distance + let txl_dist = ((txl0.0 as f64 - txl1.0 as f64).powi(2) + + (txl0.1 as f64 - txl1.1 as f64).powi(2)) + .sqrt(); + + if txl_dist > epsilon && euc_dist.is_finite() { + let scale = euc_dist / txl_dist; + if scale.is_finite() && scale > 0.0 { + valid_scales.push(scale); + } + } + } + + let avg_scale = valid_scales.iter().sum::() / valid_scales.len() as f64; + avg_scale +} + +/// Obtain the downsample scale to limit the distance per pixel to a specific value or less. pub fn get_texture_downsample_scale_of_polygon( vertices: &[(f64, f64, f64, f64, f64)], // (x, y, z, u, v) texture_size: (u32, u32), - limit_texture_resolution: Option, ) -> f64 { let uv_coords = vertices.iter().map(|v| (v.3, v.4)).collect::>(); - let pixel_coords = uv_to_pixel_coords(&uv_coords, texture_size.0, texture_size.1); + let vertices = vertices.iter().map(|v| (v.0, v.1, v.2)).collect::>(); + let pixel_per_distance = get_distance_par_pixel(&vertices, &pixel_coords); - let pixel_per_distance = (0..vertices.len()) - .map(|i| { - let j = (i + 1) % vertices.len(); - let (euc0, txl0) = ( - (vertices[i].0, vertices[i].1, vertices[i].2), - pixel_coords[i], - ); - let (euc1, txl1) = ( - (vertices[j].0, vertices[j].1, vertices[j].2), - pixel_coords[j], - ); - let euc_dist = - ((euc0.0 - euc1.0).powi(2) + (euc0.1 - euc1.1).powi(2) + (euc0.2 - euc1.2).powi(2)) - .sqrt(); - let txl_dist = ((txl0.0 as f64 - txl1.0 as f64).powi(2) - + (txl0.1 as f64 - txl1.1 as f64).powi(2)) - .sqrt(); - txl_dist / euc_dist - }) - .min_by(|a, b| a.total_cmp(b)) - .unwrap_or(1.0); - - if limit_texture_resolution.unwrap_or(false) { - 1.0_f64.min(MAX_TEXTURE_PIXELS_PER_METER / pixel_per_distance) + if pixel_per_distance < MIN_METER_PER_PIXEL { + 1.0 / (MIN_METER_PER_PIXEL / pixel_per_distance) } else { 1.0 } } + +/// A downsample scale is applied, taking into account geometric error and distance per pixel. +/// The downsample scale is a value between 0.0 and 1.0. +pub fn apply_downsample_factor(geometric_error: f64, downsample_scale: f32) -> f32 { + let error_factor = if geometric_error == 0.0 { + 1.0 + } else { + // Applying a scale factor increases the distance per pixel and decreases the texture resolution. + // The resolution of textures generated from aerial photographs is typically 10~20 cm. + // When the geometric error is 0, the scale approaches 1. + let error_factor = 1.0 / (1.0 + (geometric_error / 20.0).powf(2.5)); + if error_factor.is_nan() { + 1.0 + } else { + error_factor + } + }; + + (error_factor * downsample_scale as f64).clamp(0.0, 1.0) as f32 +}