From 05a63e37055ce57e57d3ea3b363356699ed97682 Mon Sep 17 00:00:00 2001 From: Aras Pranckevicius Date: Mon, 27 Mar 2023 21:38:52 +0300 Subject: [PATCH] OBJ: improve UV values merging in the exporter Previous code was using BKE_mesh_uv_vert_map_get_vert to somewhat detect identical UV values on mesh vertices. But this was not handling the case when separate mesh vertices still use the same UV values (e.g. cube with all six faces mapped to whole image). Replace usage of BKE_mesh_uv_vert_map_get_vert with a simple "unique UV value" map, very similar to how OBJ normals are calculated for export. Measurements on a somewhat extreme case: exporting "Rungholt" Minecraft level from https://casual-effects.com/data time (Win10, Ryzen 5950X) goes 2.9sec -> 2.2sec, and resulting file size 486MB -> 231MB. This particular model has a lot of mesh faces mapping to the same places in UV map. On less extreme cases the timings are similar between old and new code, with a tiny speedup in most tests I've tried. --- .../wavefront_obj/exporter/obj_export_mesh.cc | 64 ++++++------------- .../wavefront_obj/exporter/obj_export_mesh.hh | 9 +-- .../wavefront_obj/tests/obj_exporter_tests.cc | 13 ++++ 3 files changed, 36 insertions(+), 50 deletions(-) diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc index 9aeb0b7ab58..3fed4547cf2 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc +++ b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.cc @@ -89,7 +89,7 @@ void OBJMesh::clear() owned_export_mesh_ = nullptr; } export_mesh_ = nullptr; - uv_indices_.clear_and_shrink(); + loop_to_uv_index_.clear_and_shrink(); uv_coords_.clear_and_shrink(); loop_to_normal_index_.clear_and_shrink(); normal_coords_.clear_and_shrink(); @@ -162,7 +162,7 @@ int OBJMesh::tot_polygons() const int OBJMesh::tot_uv_vertices() const { - return tot_uv_vertices_; + return int(uv_coords_.size()); } int OBJMesh::tot_edges() const @@ -290,71 +290,47 @@ Span OBJMesh::calc_poly_vertex_indices(const int poly_index) const void OBJMesh::store_uv_coords_and_indices() { - const int totvert = export_mesh_->totvert; const StringRef active_uv_name = CustomData_get_active_layer_name(&export_mesh_->ldata, CD_PROP_FLOAT2); if (active_uv_name.is_empty()) { - tot_uv_vertices_ = 0; + uv_coords_.clear(); return; } const bke::AttributeAccessor attributes = export_mesh_->attributes(); const VArraySpan uv_map = attributes.lookup(active_uv_name, ATTR_DOMAIN_CORNER); if (uv_map.is_empty()) { - tot_uv_vertices_ = 0; + uv_coords_.clear(); return; } - const float limit[2] = {STD_UV_CONNECT_LIMIT, STD_UV_CONNECT_LIMIT}; + Map uv_to_index; - UvVertMap *uv_vert_map = BKE_mesh_uv_vert_map_create( - mesh_polys_.data(), - nullptr, - nullptr, - mesh_corner_verts_.data(), - reinterpret_cast(uv_map.data()), - mesh_polys_.size(), - totvert, - limit, - false, - false); + /* We don't know how many unique UVs there will be, but this is a guess. */ + uv_to_index.reserve(export_mesh_->totvert); + uv_coords_.reserve(export_mesh_->totvert); - uv_indices_.resize(mesh_polys_.size()); - /* At least total vertices of a mesh will be present in its texture map. So - * reserve minimum space early. */ - uv_coords_.reserve(totvert); + loop_to_uv_index_.resize(uv_map.size()); - tot_uv_vertices_ = 0; - for (int vertex_index = 0; vertex_index < totvert; vertex_index++) { - const UvMapVert *uv_vert = BKE_mesh_uv_vert_map_get_vert(uv_vert_map, vertex_index); - for (; uv_vert; uv_vert = uv_vert->next) { - if (uv_vert->separate) { - tot_uv_vertices_ += 1; - } - const int verts_in_poly = mesh_polys_[uv_vert->poly_index].totloop; - - /* Store UV vertex coordinates. */ - uv_coords_.resize(tot_uv_vertices_); - const int loopstart = mesh_polys_[uv_vert->poly_index].loopstart; - Span vert_uv_coords(uv_map[loopstart + uv_vert->loop_of_poly_index], 2); - uv_coords_[tot_uv_vertices_ - 1] = float2(vert_uv_coords[0], vert_uv_coords[1]); - - /* Store UV vertex indices. */ - uv_indices_[uv_vert->poly_index].resize(verts_in_poly); - /* Keep indices zero-based and let the writer handle the "+ 1" as per OBJ spec. */ - uv_indices_[uv_vert->poly_index][uv_vert->loop_of_poly_index] = tot_uv_vertices_ - 1; + for (int index = 0; index < int(uv_map.size()); index++) { + float2 uv = uv_map[index]; + int uv_index = uv_to_index.lookup_default(uv, -1); + if (uv_index == -1) { + uv_index = uv_to_index.size(); + uv_to_index.add(uv, uv_index); + uv_coords_.append(uv); } + loop_to_uv_index_[index] = uv_index; } - BKE_mesh_uv_vert_map_free(uv_vert_map); } Span OBJMesh::calc_poly_uv_indices(const int poly_index) const { - if (uv_indices_.size() <= 0) { + if (uv_coords_.is_empty()) { return {}; } BLI_assert(poly_index < export_mesh_->totpoly); - BLI_assert(poly_index < uv_indices_.size()); - return uv_indices_[poly_index]; + const MPoly &poly = mesh_polys_[poly_index]; + return loop_to_uv_index_.as_span().slice(poly.loopstart, poly.totloop); } float3 OBJMesh::calc_poly_normal(const int poly_index) const diff --git a/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh index a90f6e3c3fd..b19c5df740f 100644 --- a/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh +++ b/source/blender/io/wavefront_obj/exporter/obj_export_mesh.hh @@ -52,17 +52,14 @@ class OBJMesh : NonCopyable { bool mirrored_transform_; /** - * Total UV vertices in a mesh's texture map. + * Per-loop UV index. */ - int tot_uv_vertices_ = 0; - /** - * Per-polygon-per-vertex UV vertex indices. - */ - Vector> uv_indices_; + Vector loop_to_uv_index_; /* * UV vertices. */ Vector uv_coords_; + /** * Per-loop normal index. */ diff --git a/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc index 5be3bf781ac..8308250510b 100644 --- a/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc +++ b/source/blender/io/wavefront_obj/tests/obj_exporter_tests.cc @@ -340,6 +340,19 @@ TEST_F(obj_exporter_regression_test, edges) } TEST_F(obj_exporter_regression_test, vertices) +{ + OBJExportParamsDefault _export; + _export.params.forward_axis = IO_AXIS_Y; + _export.params.up_axis = IO_AXIS_Z; + _export.params.export_materials = false; + compare_obj_export_to_golden("io_tests" SEP_STR "blend_geometry" SEP_STR + "cube_loose_edges_verts.blend", + "io_tests" SEP_STR "obj" SEP_STR "cube_loose_edges_verts.obj", + "", + _export.params); +} + +TEST_F(obj_exporter_regression_test, cube_loose_edges) { OBJExportParamsDefault _export; _export.params.forward_axis = IO_AXIS_Y;