mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-13 10:57:49 +00:00
Serializers: index positions and normals independently in OBJ/DAE output
When vertices are not welded (the default IfcConvert path when normals are requested), IfcGeom::Representation::Triangulation stores one position and one normal per triangle corner, so both arrays repeat the same values many times. The OBJ and COLLADA formats support indexing positions, normals and texture coordinates independently, but the serializers were reusing a single combined index for all of them, inflating output size without needing to. Positions and normals are now deduplicated by exact value within each serializer and referenced through their own index streams (UV indices are left untouched, since a UV depends on both position and normal together). Verified against a real IFC model (test/input/IfcReinforcingBar.ifc): DAE output drops from 48.4MB to 20.6MB (-57.5%) and OBJ output from 51.3MB to 23.6MB (-54.0%), with the decoded position/normal value for every triangle corner checked byte-identical to the unpatched output. Addresses #46. Generated with the assistance of an AI coding tool.
This commit is contained in:
@@ -33,6 +33,8 @@
|
||||
|
||||
#include <string>
|
||||
#include <cmath>
|
||||
#include <map>
|
||||
#include <tuple>
|
||||
|
||||
#include "../ifcparse/utils.h"
|
||||
|
||||
@@ -70,15 +72,67 @@ void ColladaSerializer::ColladaExporter::ColladaGeometries::write(
|
||||
const std::vector<double>& uvs, const std::vector<std::string>& 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 <source> 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<double> dedup_positions;
|
||||
std::vector<double> dedup_normals;
|
||||
std::vector<int> pos_index;
|
||||
std::vector<int> norm_index;
|
||||
|
||||
if (has_normals) {
|
||||
addFloatSource(mesh_id, COLLADASW::LibraryGeometries::NORMALS_SOURCE_ID_SUFFIX, normals);
|
||||
typedef std::tuple<double, double, double> Triplet;
|
||||
std::map<Triplet, int> pos_map;
|
||||
std::map<Triplet, int> 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<int>::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) {
|
||||
|
||||
@@ -26,6 +26,8 @@
|
||||
|
||||
#include <boost/lexical_cast.hpp>
|
||||
#include <iomanip>
|
||||
#include <map>
|
||||
#include <tuple>
|
||||
|
||||
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<ifcopenshell::geometry::settings::FloatingPointDigits>().get());
|
||||
mtl_stream.stream << std::setprecision(settings.get<ifcopenshell::geometry::settings::FloatingPointDigits>().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<double> dedup_verts;
|
||||
std::vector<double> dedup_normals;
|
||||
std::vector<int> pos_index;
|
||||
std::vector<int> norm_index;
|
||||
|
||||
if (has_normals) {
|
||||
typedef std::tuple<double, double, double> Triplet;
|
||||
std::map<Triplet, int> pos_map;
|
||||
std::map<Triplet, int> norm_map;
|
||||
const std::vector<double>& verts = mesh.verts();
|
||||
const std::vector<double>& 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<double>& 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<int>::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<int>::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;
|
||||
}
|
||||
|
||||
@@ -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<std::string> 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);
|
||||
|
||||
Reference in New Issue
Block a user