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.
This commit is contained in:
Aras Pranckevicius
2023-03-27 21:38:52 +03:00
parent 8ad0935ed6
commit 05a63e3705
3 changed files with 36 additions and 50 deletions

View File

@@ -89,7 +89,7 @@ void OBJMesh::clear()
owned_export_mesh_ = nullptr; owned_export_mesh_ = nullptr;
} }
export_mesh_ = nullptr; export_mesh_ = nullptr;
uv_indices_.clear_and_shrink(); loop_to_uv_index_.clear_and_shrink();
uv_coords_.clear_and_shrink(); uv_coords_.clear_and_shrink();
loop_to_normal_index_.clear_and_shrink(); loop_to_normal_index_.clear_and_shrink();
normal_coords_.clear_and_shrink(); normal_coords_.clear_and_shrink();
@@ -162,7 +162,7 @@ int OBJMesh::tot_polygons() const
int OBJMesh::tot_uv_vertices() const int OBJMesh::tot_uv_vertices() const
{ {
return tot_uv_vertices_; return int(uv_coords_.size());
} }
int OBJMesh::tot_edges() const int OBJMesh::tot_edges() const
@@ -290,71 +290,47 @@ Span<int> OBJMesh::calc_poly_vertex_indices(const int poly_index) const
void OBJMesh::store_uv_coords_and_indices() 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, const StringRef active_uv_name = CustomData_get_active_layer_name(&export_mesh_->ldata,
CD_PROP_FLOAT2); CD_PROP_FLOAT2);
if (active_uv_name.is_empty()) { if (active_uv_name.is_empty()) {
tot_uv_vertices_ = 0; uv_coords_.clear();
return; return;
} }
const bke::AttributeAccessor attributes = export_mesh_->attributes(); const bke::AttributeAccessor attributes = export_mesh_->attributes();
const VArraySpan<float2> uv_map = attributes.lookup<float2>(active_uv_name, ATTR_DOMAIN_CORNER); const VArraySpan<float2> uv_map = attributes.lookup<float2>(active_uv_name, ATTR_DOMAIN_CORNER);
if (uv_map.is_empty()) { if (uv_map.is_empty()) {
tot_uv_vertices_ = 0; uv_coords_.clear();
return; return;
} }
const float limit[2] = {STD_UV_CONNECT_LIMIT, STD_UV_CONNECT_LIMIT}; Map<float2, int> uv_to_index;
UvVertMap *uv_vert_map = BKE_mesh_uv_vert_map_create( /* We don't know how many unique UVs there will be, but this is a guess. */
mesh_polys_.data(), uv_to_index.reserve(export_mesh_->totvert);
nullptr, uv_coords_.reserve(export_mesh_->totvert);
nullptr,
mesh_corner_verts_.data(),
reinterpret_cast<const float(*)[2]>(uv_map.data()),
mesh_polys_.size(),
totvert,
limit,
false,
false);
uv_indices_.resize(mesh_polys_.size()); loop_to_uv_index_.resize(uv_map.size());
/* At least total vertices of a mesh will be present in its texture map. So
* reserve minimum space early. */
uv_coords_.reserve(totvert);
tot_uv_vertices_ = 0; for (int index = 0; index < int(uv_map.size()); index++) {
for (int vertex_index = 0; vertex_index < totvert; vertex_index++) { float2 uv = uv_map[index];
const UvMapVert *uv_vert = BKE_mesh_uv_vert_map_get_vert(uv_vert_map, vertex_index); int uv_index = uv_to_index.lookup_default(uv, -1);
for (; uv_vert; uv_vert = uv_vert->next) { if (uv_index == -1) {
if (uv_vert->separate) { uv_index = uv_to_index.size();
tot_uv_vertices_ += 1; uv_to_index.add(uv, uv_index);
} uv_coords_.append(uv);
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<float> 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;
} }
loop_to_uv_index_[index] = uv_index;
} }
BKE_mesh_uv_vert_map_free(uv_vert_map);
} }
Span<int> OBJMesh::calc_poly_uv_indices(const int poly_index) const Span<int> OBJMesh::calc_poly_uv_indices(const int poly_index) const
{ {
if (uv_indices_.size() <= 0) { if (uv_coords_.is_empty()) {
return {}; return {};
} }
BLI_assert(poly_index < export_mesh_->totpoly); BLI_assert(poly_index < export_mesh_->totpoly);
BLI_assert(poly_index < uv_indices_.size()); const MPoly &poly = mesh_polys_[poly_index];
return uv_indices_[poly_index]; return loop_to_uv_index_.as_span().slice(poly.loopstart, poly.totloop);
} }
float3 OBJMesh::calc_poly_normal(const int poly_index) const float3 OBJMesh::calc_poly_normal(const int poly_index) const

View File

@@ -52,17 +52,14 @@ class OBJMesh : NonCopyable {
bool mirrored_transform_; bool mirrored_transform_;
/** /**
* Total UV vertices in a mesh's texture map. * Per-loop UV index.
*/ */
int tot_uv_vertices_ = 0; Vector<int> loop_to_uv_index_;
/**
* Per-polygon-per-vertex UV vertex indices.
*/
Vector<Vector<int>> uv_indices_;
/* /*
* UV vertices. * UV vertices.
*/ */
Vector<float2> uv_coords_; Vector<float2> uv_coords_;
/** /**
* Per-loop normal index. * Per-loop normal index.
*/ */

View File

@@ -340,6 +340,19 @@ TEST_F(obj_exporter_regression_test, edges)
} }
TEST_F(obj_exporter_regression_test, vertices) 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; OBJExportParamsDefault _export;
_export.params.forward_axis = IO_AXIS_Y; _export.params.forward_axis = IO_AXIS_Y;