Skip to content

Commit

Permalink
glTFのアトラス化 (#621)
Browse files Browse the repository at this point in the history
<!-- Close or Related Issues -->
Close #570

### What I did(変更内容)
<!-- Please describe the motivation behind this PR and the changes it
introduces. -->
<!-- どのような変更をしますか? 目的は? -->

- OBJや3D Tilesと似たような処理を行いました

### Notes(連絡事項)
<!-- If manual testing is required, please describe the procedure. -->
<!-- 手動での動作確認が必要なら手順を簡単に伝えてください。そのほか連絡事項など。 -->

- 以下のように変換します
```bash
cargo run -- <input_path> --sink gltf -o transform=use_texture --output <output_path> --release
```
  • Loading branch information
satoshi7190 authored Aug 26, 2024
2 parents 5931c58 + 067a246 commit 078bf4f
Show file tree
Hide file tree
Showing 3 changed files with 175 additions and 46 deletions.
1 change: 0 additions & 1 deletion nusamai/src/sink/cesiumtiles/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
157 changes: 143 additions & 14 deletions nusamai/src/sink/gltf/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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,
Expand Down Expand Up @@ -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<f64> = 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<u32> = 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();
Expand All @@ -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
Expand All @@ -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::<Vec<(f64, f64, f64, f64, f64)>>();
let uv_coords = original_vertices
.iter()
.map(|(_, _, _, u, v)| (*u, *v))
.collect::<Vec<(f64, f64)>>();

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::<Vec<(f64, f64)>>();
let updated_vertices = original_vertices
.iter()
.zip(atlas_placed_uv_coords.iter())
.map(|((x, y, z, _, _), (u, v))| (*x, *y, *z, *u, *v))
.collect::<Vec<(f64, f64, f64, f64, f64)>>();

// 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<f64> = 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<u32> = Vec::new();

buf3d.clear();
buf3d.extend(poly.raw_coords().iter().map(|c| [c[0], c[1], c[2]]));

Expand All @@ -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);
Expand All @@ -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(':', "_"));
Expand Down
63 changes: 32 additions & 31 deletions nusamai/src/sink/obj/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use std::{
f64::consts::FRAC_PI_2,
path::PathBuf,
sync::{mpsc, Mutex},
time::Instant,
};

use ahash::{HashMap, HashMapExt};
Expand Down Expand Up @@ -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<ClassifiedFeatures> = Default::default();
Expand Down Expand Up @@ -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()
Expand All @@ -390,28 +384,48 @@ 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));

let atlas_packing_start = Instant::now();
// 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)?;

// Coordinate transformation
{
Expand Down Expand Up @@ -624,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,
Expand All @@ -646,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>(())
})?;

Expand Down

0 comments on commit 078bf4f

Please sign in to comment.