diff --git a/src/serializers/ColladaSerializer.cpp b/src/serializers/ColladaSerializer.cpp index 0b1676c338..7e3ae729d9 100644 --- a/src/serializers/ColladaSerializer.cpp +++ b/src/serializers/ColladaSerializer.cpp @@ -33,6 +33,8 @@ #include #include +#include +#include #include "../ifcparse/utils.h" @@ -70,15 +72,67 @@ void ColladaSerializer::ColladaExporter::ColladaGeometries::write( const std::vector& uvs, const std::vector& material_references) { openMesh(mesh_id); - + // The normals vector can be empty for example when the WELD_VERTICES setting is used. // IfcOpenShell does not provide them with multiple face normals collapsed into a single vertex. const bool has_normals = !normals.empty(); const bool has_uvs = !uvs.empty(); - - addFloatSource(mesh_id, COLLADASW::LibraryGeometries::POSITIONS_SOURCE_ID_SUFFIX, positions); + + // When vertices are not welded (the common case when normals are requested), 'positions' + // and 'normals' hold one entry per triangle corner, so identical position/normal values are + // repeated many times. COLLADA streams can be indexed independently (VERTEX and + // NORMAL inputs each carry their own offset), so positions and normals are deduplicated here + // on their own terms and referenced through separate index streams below, rather than being + // forced to share a single combined index as before. + std::vector dedup_positions; + std::vector dedup_normals; + std::vector pos_index; + std::vector norm_index; + if (has_normals) { - addFloatSource(mesh_id, COLLADASW::LibraryGeometries::NORMALS_SOURCE_ID_SUFFIX, normals); + typedef std::tuple Triplet; + std::map pos_map; + std::map norm_map; + const size_t n = positions.size() / 3; + pos_index.reserve(n); + norm_index.reserve(n); + for (size_t i = 0; i < n; ++i) { + const Triplet pk(positions[i * 3], positions[i * 3 + 1], positions[i * 3 + 2]); + auto pit = pos_map.find(pk); + if (pit == pos_map.end()) { + const int pidx = (int)(dedup_positions.size() / 3); + pos_map.emplace(pk, pidx); + dedup_positions.push_back(std::get<0>(pk)); + dedup_positions.push_back(std::get<1>(pk)); + dedup_positions.push_back(std::get<2>(pk)); + pos_index.push_back(pidx); + } else { + pos_index.push_back(pit->second); + } + + const Triplet nk(normals[i * 3], normals[i * 3 + 1], normals[i * 3 + 2]); + auto nit = norm_map.find(nk); + if (nit == norm_map.end()) { + const int nidx = (int)(dedup_normals.size() / 3); + norm_map.emplace(nk, nidx); + dedup_normals.push_back(std::get<0>(nk)); + dedup_normals.push_back(std::get<1>(nk)); + dedup_normals.push_back(std::get<2>(nk)); + norm_index.push_back(nidx); + } else { + norm_index.push_back(nit->second); + } + } + } + + // Positions referenced by VERTEX inputs (both triangles and lines below) are remapped + // through pos_index when deduplication above ran; otherwise (no normals) the incoming + // positions are already unique per WeldVertices and are used as-is. + auto vertex_index = [&](int idx) { return has_normals ? pos_index[idx] : idx; }; + + addFloatSource(mesh_id, COLLADASW::LibraryGeometries::POSITIONS_SOURCE_ID_SUFFIX, has_normals ? dedup_positions : positions); + if (has_normals) { + addFloatSource(mesh_id, COLLADASW::LibraryGeometries::NORMALS_SOURCE_ID_SUFFIX, dedup_normals); if (has_uvs) { addFloatSource(mesh_id, COLLADASW::LibraryGeometries::TEXCOORDS_SOURCE_ID_SUFFIX, uvs, "UV"); } @@ -120,9 +174,12 @@ void ColladaSerializer::ColladaExporter::ColladaGeometries::write( for (std::vector::const_iterator jt = index_range_start; jt != it; ++jt) { const int idx = *jt; if (has_normals && has_uvs) { - triangles.appendValues(idx, idx, idx); + // UV index is left as the original per-corner index (not deduplicated): a UV + // is a function of both position and normal (box projection), so it cannot be + // safely indexed through either the position or the normal dedup map alone. + triangles.appendValues(pos_index[idx], norm_index[idx], idx); } else if(has_normals) { - triangles.appendValues(idx, idx); + triangles.appendValues(pos_index[idx], norm_index[idx]); } else { triangles.appendValues(idx); } @@ -154,8 +211,8 @@ void ColladaSerializer::ColladaExporter::ColladaGeometries::write( linelist.resize(linelist.size() + 1); } - linelist.rbegin()->second.push_back(i1); - linelist.rbegin()->second.push_back(i2); + linelist.rbegin()->second.push_back(vertex_index(i1)); + linelist.rbegin()->second.push_back(vertex_index(i2)); } for (linelist_t::const_iterator it = linelist.begin(); it != linelist.end(); ++it) { diff --git a/src/serializers/WavefrontObjSerializer.cpp b/src/serializers/WavefrontObjSerializer.cpp index 5d178329b3..4578820474 100644 --- a/src/serializers/WavefrontObjSerializer.cpp +++ b/src/serializers/WavefrontObjSerializer.cpp @@ -26,6 +26,8 @@ #include #include +#include +#include WaveFrontOBJSerializer::WaveFrontOBJSerializer(const stream_or_filename& obj_filename, const stream_or_filename& mtl_filename, const ifcopenshell::geometry::Settings& geometry_settings, const ifcopenshell::geometry::SerializerSettings& settings, Logger* logger) : WriteOnlyGeometrySerializer(geometry_settings, settings, logger_or_root(logger)) @@ -33,6 +35,7 @@ WaveFrontOBJSerializer::WaveFrontOBJSerializer(const stream_or_filename& obj_fil , mtl_stream(mtl_filename) , vcount_total(1) , ncount_total(1) + , uvcount_total(1) { obj_stream.stream << std::setprecision(settings.get().get()); mtl_stream.stream << std::setprecision(settings.get().get()); @@ -94,14 +97,68 @@ void WaveFrontOBJSerializer::write(const IfcGeom::TriangulationElement* o) const IfcGeom::Representation::Triangulation& mesh = o->geometry(); - size_t vcount = mesh.verts().size() / 3; - size_t ncount = mesh.normals().size() / 3; + const bool has_uvs = !mesh.uvs().empty(); + const bool has_normals = !mesh.normals().empty(); - for (auto it = mesh.verts().begin(); it != mesh.verts().end();) { + // When vertices are not welded (the common case when normals are requested), mesh.verts() + // and mesh.normals() hold one entry per triangle corner, so identical position/normal values + // are repeated many times. OBJ's "f v/vt/vn" syntax indexes positions, texture coordinates + // and normals independently, so positions and normals are deduplicated here on their own + // terms and referenced through separate index streams below, rather than being forced to + // share a single combined index as before. + std::vector dedup_verts; + std::vector dedup_normals; + std::vector pos_index; + std::vector norm_index; + + if (has_normals) { + typedef std::tuple Triplet; + std::map pos_map; + std::map norm_map; + const std::vector& verts = mesh.verts(); + const std::vector& normals = mesh.normals(); + const size_t n = verts.size() / 3; + pos_index.reserve(n); + norm_index.reserve(n); + for (size_t i = 0; i < n; ++i) { + const Triplet pk(verts[i * 3], verts[i * 3 + 1], verts[i * 3 + 2]); + auto pit = pos_map.find(pk); + if (pit == pos_map.end()) { + const int pidx = (int)(dedup_verts.size() / 3); + pos_map.emplace(pk, pidx); + dedup_verts.push_back(std::get<0>(pk)); + dedup_verts.push_back(std::get<1>(pk)); + dedup_verts.push_back(std::get<2>(pk)); + pos_index.push_back(pidx); + } else { + pos_index.push_back(pit->second); + } + + const Triplet nk(normals[i * 3], normals[i * 3 + 1], normals[i * 3 + 2]); + auto nit = norm_map.find(nk); + if (nit == norm_map.end()) { + const int nidx = (int)(dedup_normals.size() / 3); + norm_map.emplace(nk, nidx); + dedup_normals.push_back(std::get<0>(nk)); + dedup_normals.push_back(std::get<1>(nk)); + dedup_normals.push_back(std::get<2>(nk)); + norm_index.push_back(nidx); + } else { + norm_index.push_back(nit->second); + } + } + } + + const std::vector& verts_to_write = has_normals ? dedup_verts : mesh.verts(); + size_t vcount = verts_to_write.size() / 3; + size_t ncount = dedup_normals.size() / 3; + size_t uvcount = mesh.uvs().size() / 2; + + for (auto it = verts_to_write.begin(); it != verts_to_write.end();) { const double x = *(it++); const double y = *(it++); const double z = *(it++); - + if (isyup) { obj_stream.stream << "v " << x << " " << z << " " << -y << "\n"; } else { @@ -109,7 +166,7 @@ void WaveFrontOBJSerializer::write(const IfcGeom::TriangulationElement* o) } } - for (auto it = mesh.normals().begin(); it != mesh.normals().end();) { + for (auto it = dedup_normals.begin(); it != dedup_normals.end();) { const double x = *(it++); const double y = *(it++); const double z = *(it++); @@ -124,9 +181,6 @@ void WaveFrontOBJSerializer::write(const IfcGeom::TriangulationElement* o) int previous_material_id = -2; std::vector::const_iterator material_it = mesh.material_ids().begin(); - - const bool has_uvs = !mesh.uvs().empty(); - const bool has_normals = !mesh.normals().empty(); for ( std::vector::const_iterator it = mesh.faces().begin(); it != mesh.faces().end(); ) { const int material_id = *(material_it++); @@ -142,18 +196,29 @@ void WaveFrontOBJSerializer::write(const IfcGeom::TriangulationElement* o) previous_material_id = material_id; } - const int v1 = *(it++) + vcount_total; - const int v2 = *(it++) + vcount_total; - const int v3 = *(it++) + vcount_total; + const int idx1 = *(it++); + const int idx2 = *(it++); + const int idx3 = *(it++); - const int n1 = v1 - vcount_total + ncount_total; - const int n2 = v2 - vcount_total + ncount_total; - const int n3 = v3 - vcount_total + ncount_total; + const int v1 = (has_normals ? pos_index[idx1] : idx1) + (int)vcount_total; + const int v2 = (has_normals ? pos_index[idx2] : idx2) + (int)vcount_total; + const int v3 = (has_normals ? pos_index[idx3] : idx3) + (int)vcount_total; + + const int n1 = has_normals ? norm_index[idx1] + (int)ncount_total : 0; + const int n2 = has_normals ? norm_index[idx2] + (int)ncount_total : 0; + const int n3 = has_normals ? norm_index[idx3] + (int)ncount_total : 0; + + // UV indices are left as the original per-corner indices (not deduplicated): a UV is a + // function of both position and normal (box projection), so it cannot be safely indexed + // through either the position or the normal dedup map alone. + const int t1 = idx1 + (int)uvcount_total; + const int t2 = idx2 + (int)uvcount_total; + const int t3 = idx3 + (int)uvcount_total; if (has_normals && has_uvs) { - obj_stream.stream << "f " << v1 << "/" << n1 << "/" << n1 << " " - << v2 << "/" << n2 << "/" << n2 << " " - << v3 << "/" << n3 << "/" << n3 << "\n"; + obj_stream.stream << "f " << v1 << "/" << t1 << "/" << n1 << " " + << v2 << "/" << t2 << "/" << n2 << " " + << v3 << "/" << t3 << "/" << n3 << "\n"; } else if (has_normals) { obj_stream.stream << "f " << v1 << "//" << n1 << " " << v2 << "//" << n2 << " " @@ -188,12 +253,13 @@ void WaveFrontOBJSerializer::write(const IfcGeom::TriangulationElement* o) previous_material_id = material_id; } - const int v1 = i1 + vcount_total; - const int v2 = i2 + vcount_total; + const int v1 = (has_normals ? pos_index[i1] : i1) + (int)vcount_total; + const int v2 = (has_normals ? pos_index[i2] : i2) + (int)vcount_total; obj_stream.stream << "l " << v1 << " " << v2 << "\n"; } vcount_total += vcount; ncount_total += ncount; + uvcount_total += uvcount; } diff --git a/src/serializers/WavefrontObjSerializer.h b/src/serializers/WavefrontObjSerializer.h index ceac640661..562a01ecfc 100644 --- a/src/serializers/WavefrontObjSerializer.h +++ b/src/serializers/WavefrontObjSerializer.h @@ -32,7 +32,7 @@ class SERIALIZERS_API WaveFrontOBJSerializer : public WriteOnlyGeometrySerialize private: stream_or_filename obj_stream; stream_or_filename mtl_stream; - size_t vcount_total, ncount_total; + size_t vcount_total, ncount_total, uvcount_total; std::set materials; public: WaveFrontOBJSerializer(const stream_or_filename& obj_filename, const stream_or_filename& mtl_filename, const ifcopenshell::geometry::Settings& geometry_settings, const ifcopenshell::geometry::SerializerSettings& settings, Logger* logger = nullptr);