Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OBJ sinkの作成 ジオメトリ出力のみ #607

Merged
merged 8 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions app/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@
sink::{
cesiumtiles::CesiumTilesSinkProvider, czml::CzmlSinkProvider, geojson::GeoJsonSinkProvider,
gltf::GltfSinkProvider, gpkg::GpkgSinkProvider, kml::KmlSinkProvider,
minecraft::MinecraftSinkProvider, mvt::MvtSinkProvider, ply::StanfordPlySinkProvider,
serde::SerdeSinkProvider, shapefile::ShapefileSinkProvider, DataSinkProvider,
minecraft::MinecraftSinkProvider, mvt::MvtSinkProvider, obj::ObjSinkProvider,
ply::StanfordPlySinkProvider, serde::SerdeSinkProvider, shapefile::ShapefileSinkProvider,
DataSinkProvider,
},
source::{citygml::CityGmlSourceProvider, DataSourceProvider},
transformer::{
Expand Down Expand Up @@ -123,6 +124,7 @@
"ply" => Some(Box::new(StanfordPlySinkProvider {})),
"cesiumtiles" => Some(Box::new(CesiumTilesSinkProvider {})),
"minecraft" => Some(Box::new(MinecraftSinkProvider {})),
"obj" => Some(Box::new(ObjSinkProvider {})),

Check warning on line 127 in app/src-tauri/src/main.rs

View check run for this annotation

Codecov / codecov/patch

app/src-tauri/src/main.rs#L127

Added line #L127 was not covered by tests
_ => None,
}
}
Expand Down
1 change: 1 addition & 0 deletions nusamai/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ pub static BUILTIN_SINKS: &[&dyn sink::DataSinkProvider] = &[
&sink::shapefile::ShapefileSinkProvider {},
&sink::noop::NoopSinkProvider {},
&sink::minecraft::MinecraftSinkProvider {},
&sink::obj::ObjSinkProvider {},
];
1 change: 1 addition & 0 deletions nusamai/src/sink/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pub mod gltf;
pub mod gpkg;
pub mod kml;
pub mod minecraft;
pub mod obj;
pub mod mvt;
pub mod noop;
pub mod ply;
Expand Down
355 changes: 355 additions & 0 deletions nusamai/src/sink/obj/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,355 @@
//! obj sink

use std::{f64::consts::FRAC_PI_2, io::Write, path::PathBuf, sync::Mutex};

use ahash::{HashMap, RandomState};
use earcut::{utils3d::project3d_to_2d, Earcut};
use flatgeom::MultiPolygon;
use indexmap::IndexSet;
use nusamai_citygml::{
object::{ObjectStereotype, Value},
schema::Schema,
GeometryType,
};

use serde::{Deserialize, Serialize};

use glam::{DMat4, DVec3, DVec4};

use crate::{
get_parameter_value,
parameters::*,
pipeline::{Feedback, PipelineError, Receiver, Result},
sink::{DataRequirements, DataSink, DataSinkProvider, SinkInfo},
transformer::{TransformerOption, TransformerRegistry},
};

use itertools::Itertools;
use nusamai_projection::cartesian::geodetic_to_geocentric;
use rayon::iter::{IntoParallelIterator, ParallelBridge, ParallelIterator};

pub struct ObjSinkProvider {}

impl DataSinkProvider for ObjSinkProvider {
fn info(&self) -> SinkInfo {
SinkInfo {
id_name: "obj".to_string(),
name: "OBJ".to_string(),
}
}

fn parameters(&self) -> Parameters {
let mut params = Parameters::new();
params.define(
"@output".into(),
ParameterEntry {
description: "Output file path".into(),
required: true,
parameter: ParameterType::FileSystemPath(FileSystemPathParameter {
value: None,
must_exist: false,
}),
label: None,
},
);
params
}

Check warning on line 56 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L41-L56

Added lines #L41 - L56 were not covered by tests

fn available_transformer(&self) -> TransformerRegistry {
let settings: TransformerRegistry = TransformerRegistry::new();

settings
}
fn create(&self, params: &Parameters) -> Box<dyn DataSink> {
let output_path = get_parameter_value!(params, "@output", FileSystemPath);
let transform_options = self.available_transformer();

Box::<ObjSink>::new(ObjSink {
output_path: output_path.as_ref().unwrap().into(),
transform_settings: transform_options,
})
}

Check warning on line 71 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L58-L71

Added lines #L58 - L71 were not covered by tests
}

pub struct ObjSink {
output_path: PathBuf,
transform_settings: TransformerRegistry,
}

#[derive(Debug)]
pub struct BoundingVolume {
pub min_lng: f64,
pub max_lng: f64,
pub min_lat: f64,
pub max_lat: f64,
pub min_height: f64,
pub max_height: f64,
}

impl BoundingVolume {
fn update(&mut self, other: &Self) {
self.min_lng = self.min_lng.min(other.min_lng);
self.max_lng = self.max_lng.max(other.max_lng);
self.min_lat = self.min_lat.min(other.min_lat);
self.max_lat = self.max_lat.max(other.max_lat);
self.min_height = self.min_height.min(other.min_height);
self.max_height = self.max_height.max(other.max_height);
}

Check warning on line 97 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L90-L97

Added lines #L90 - L97 were not covered by tests
}

impl Default for BoundingVolume {
fn default() -> Self {
Self {
min_lng: f64::MAX,
max_lng: f64::MIN,
min_lat: f64::MAX,
max_lat: f64::MIN,
min_height: f64::MAX,
max_height: f64::MIN,
}
}

Check warning on line 110 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L101-L110

Added lines #L101 - L110 were not covered by tests
}

#[derive(Serialize, Deserialize, Debug, Clone)]

Check warning on line 113 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L113

Added line #L113 was not covered by tests
pub struct Feature {
// polygons [x, y, z, u, v]
pub polygons: MultiPolygon<'static, [f64; 5]>,
// feature_id
pub feature_id: Option<u32>,
}

type ClassifiedFeatures = HashMap<String, ClassFeatures>;

#[derive(Default)]
struct ClassFeatures {
features: Vec<Feature>,
bounding_volume: BoundingVolume,
}

impl DataSink for ObjSink {
fn make_requirements(&mut self, properties: Vec<TransformerOption>) -> DataRequirements {
let default_requirements: DataRequirements = DataRequirements {
resolve_appearance: true,
key_value: crate::transformer::KeyValueSpec::JsonifyObjectsAndArrays,
..Default::default()
};

Check warning on line 135 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L130-L135

Added lines #L130 - L135 were not covered by tests

for prop in properties {
let _ = &self
.transform_settings
.update_transformer(&prop.key, prop.is_enabled);
}

Check warning on line 141 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L137-L141

Added lines #L137 - L141 were not covered by tests

self.transform_settings.build(default_requirements)
}

Check warning on line 144 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L143-L144

Added lines #L143 - L144 were not covered by tests

fn run(&mut self, upstream: Receiver, feedback: &Feedback, schema: &Schema) -> Result<()> {
let ellipsoid = nusamai_projection::ellipsoid::wgs84();

let classified_features: Mutex<ClassifiedFeatures> = Default::default();

// Construct a Feature classified by typename from Entity
// The coordinates of polygon store the actual coordinate values (WGS84) and UV coordinates, not the index.
let _ = upstream.into_iter().par_bridge().try_for_each(|parcel| {
feedback.ensure_not_canceled()?;

Check warning on line 154 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L146-L154

Added lines #L146 - L154 were not covered by tests

let entity = parcel.entity;

Check warning on line 156 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L156

Added line #L156 was not covered by tests

// entity must be a Feature
let Value::Object(obj) = &entity.root else {
return Ok(());

Check warning on line 160 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L159-L160

Added lines #L159 - L160 were not covered by tests
};
let ObjectStereotype::Feature { geometries, .. } = &obj.stereotype else {
return Ok(());

Check warning on line 163 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L162-L163

Added lines #L162 - L163 were not covered by tests
};

let geom_store = entity.geometry_store.read().unwrap();
if geom_store.multipolygon.is_empty() {
return Ok(());
}

let mut feature = Feature {
polygons: MultiPolygon::new(),
feature_id: None,
};

let mut local_bvol = BoundingVolume::default();

geometries.iter().for_each(|entry| {
match entry.ty {

Check warning on line 179 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L166-L179

Added lines #L166 - L179 were not covered by tests
GeometryType::Solid | GeometryType::Surface | GeometryType::Triangle => {
// extract the polygon and UV
for (idx_poly, poly_uv) in
geom_store
.multipolygon
.iter_range(entry.pos as usize..(entry.pos + entry.len) as usize)
.zip_eq(geom_store.polygon_uvs.iter_range(
entry.pos as usize..(entry.pos + entry.len) as usize,
))
{
// convert to idx_poly to polygon
let poly = idx_poly.transform(|c| geom_store.vertices[*c as usize]);

let mut ring_buffer: Vec<[f64; 5]> = Vec::new();

poly.rings()
.zip_eq(poly_uv.rings())
.for_each(|(ring, uv_ring)| {
ring.iter_closed().zip_eq(uv_ring.iter_closed()).for_each(
|(c, uv)| {
let [lng, lat, height] = c;
ring_buffer.push([lng, lat, height, uv[0], uv[1]]);

local_bvol.min_lng = local_bvol.min_lng.min(lng);
local_bvol.max_lng = local_bvol.max_lng.max(lng);
local_bvol.min_lat = local_bvol.min_lat.min(lat);
local_bvol.max_lat = local_bvol.max_lat.max(lat);
local_bvol.min_height =
local_bvol.min_height.min(height);
local_bvol.max_height =
local_bvol.max_height.max(height);
},
);

feature.polygons.add_exterior(ring_buffer.drain(..));
});
}

Check warning on line 216 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L182-L216

Added lines #L182 - L216 were not covered by tests
}
GeometryType::Curve => {
// TODO: implement
}
GeometryType::Point => {
// TODO: implement
}

Check warning on line 223 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L218-L223

Added lines #L218 - L223 were not covered by tests
}
});

{
let mut locked_features = classified_features.lock().unwrap();
let feats = locked_features.entry(obj.typename.to_string()).or_default();
feats.features.push(feature);
feats.bounding_volume.update(&local_bvol);
}

Ok::<(), PipelineError>(())
});

let classified_features = classified_features.into_inner().unwrap();

Check warning on line 237 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L225-L237

Added lines #L225 - L237 were not covered by tests

// Bounding volume for the entire dataset
let global_bvol = {
let mut global_bvol = BoundingVolume::default();
for features in classified_features.values() {
global_bvol.update(&features.bounding_volume);
}
global_bvol
};

let transform_matrix = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nits: glTFのSinkでは書かれてなかったと思いますが、なんのために変換行列を計算しているのか、というコメントはあっても良いかもしれないですね。

let bounds = &global_bvol;
let center_lng = (bounds.min_lng + bounds.max_lng) / 2.0;
let center_lat = (bounds.min_lat + bounds.max_lat) / 2.0;

let psi = ((1. - ellipsoid.e_sq()) * center_lat.to_radians().tan()).atan();

let (tx, ty, tz) = geodetic_to_geocentric(&ellipsoid, center_lng, center_lat, 0.);
let h = (tx * tx + ty * ty + tz * tz).sqrt();

DMat4::from_translation(DVec3::new(0., -h, 0.))
* DMat4::from_rotation_x(-(FRAC_PI_2 - psi))
* DMat4::from_rotation_y((-center_lng - 90.).to_radians())
};
let _ = transform_matrix.inverse();

classified_features
.into_par_iter()
.try_for_each(|(typename, mut features)| {
feedback.ensure_not_canceled()?;

Check warning on line 267 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L240-L267

Added lines #L240 - L267 were not covered by tests

// Triangulation
let mut earcutter = Earcut::new();
let mut buf3d: Vec<[f64; 3]> = Vec::new();
let mut buf2d: Vec<[f64; 2]> = Vec::new();
let mut index_buf: Vec<u32> = Vec::new();
let mut triangles = Vec::new();

Check warning on line 274 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L270-L274

Added lines #L270 - L274 were not covered by tests

for feature in features.features.iter_mut() {
feedback.ensure_not_canceled()?;

Check warning on line 277 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L276-L277

Added lines #L276 - L277 were not covered by tests

feature
.polygons
.transform_inplace(|&[lng, lat, height, u, v]| {
let (x, y, z) = geodetic_to_geocentric(&ellipsoid, lng, lat, height);
let v_xyz = DVec4::new(x, z, -y, 1.0);
let v_enu = transform_matrix * v_xyz;
[v_enu[0], v_enu[1], v_enu[2], u, 1.0 - v]
});

Check warning on line 286 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L279-L286

Added lines #L279 - L286 were not covered by tests

for poly in feature.polygons.iter() {
let num_outer = match poly.hole_indices().first() {
Some(&v) => v as usize,
None => poly.raw_coords().len(),

Check warning on line 291 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L288-L291

Added lines #L288 - L291 were not covered by tests
};

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

if project3d_to_2d(&buf3d, num_outer, &mut buf2d) {
// earcut
earcutter.earcut(
buf2d.iter().cloned(),
poly.hole_indices(),
&mut index_buf,
);
triangles.extend(index_buf.iter().map(|&idx| buf3d[idx as usize]));
}

Check warning on line 305 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L294-L305

Added lines #L294 - L305 were not covered by tests
}
}

// make vertices and indices
let mut vertices: IndexSet<[u64; 3], RandomState> = IndexSet::default();
let indices: Vec<_> = triangles
.iter()
.map(|[x, y, z]| {
let vbits = [(x).to_bits(), (y).to_bits(), (z).to_bits()];
let (index, _) = vertices.insert_full(vbits);
index as u32
})
.collect();

println!("{:?} {:?}", vertices.len(), indices.len());

feedback.ensure_not_canceled()?;

Check warning on line 322 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L310-L322

Added lines #L310 - L322 were not covered by tests

// Write to file
let file_path = {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fiy: マテリアル処理の場合は当然そうするだろうとは思っているのですが、OBJフォーマットに関する知識や具体的なファイル書き出し手法に関するコードはmod.rsに紛れ込ませたくありません。
多少は堅牢なプログラムにしたいので、gltf sinkのようにファイルを分ける、nusamai-objのように分離するなどを検討してください!
特別な構造体も必要になってくるだろうと思います!

let filename = format!("{}.obj", typename.replace(':', "_"));
self.output_path.join(filename)

Check warning on line 327 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L325-L327

Added lines #L325 - L327 were not covered by tests
};
let file = std::fs::File::create(file_path)?;
let mut writer = std::io::BufWriter::new(file);

Check warning on line 330 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L329-L330

Added lines #L329 - L330 were not covered by tests

// Writing vertex data
for vertex in &vertices {
let [vx, vy, vz] = vertex;
let vx = f64::from_bits(*vx);
let vy = f64::from_bits(*vy);
let vz = f64::from_bits(*vz);
writeln!(writer, "v {} {} {}", vx, vy, vz)?;

Check warning on line 338 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L333-L338

Added lines #L333 - L338 were not covered by tests
}

// Writing of surface data (index starts at 1, so +1 is used)
for face in indices.chunks(3) {
if let [i1, i2, i3] = face {
writeln!(writer, "f {} {} {}", i1 + 1, i2 + 1, i3 + 1)?;
}

Check warning on line 345 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L342-L345

Added lines #L342 - L345 were not covered by tests
}

writer.flush()?;

Check warning on line 348 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L348

Added line #L348 was not covered by tests

Ok::<(), PipelineError>(())
})?;

Check warning on line 351 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L350-L351

Added lines #L350 - L351 were not covered by tests

Ok(())
}

Check warning on line 354 in nusamai/src/sink/obj/mod.rs

View check run for this annotation

Codecov / codecov/patch

nusamai/src/sink/obj/mod.rs#L353-L354

Added lines #L353 - L354 were not covered by tests
satoshi7190 marked this conversation as resolved.
Show resolved Hide resolved
}
Loading