From e0a504417cc263b5a3b2a2241bb13be6964938de Mon Sep 17 00:00:00 2001 From: Dion Moult Date: Tue, 19 May 2026 17:31:53 +1000 Subject: [PATCH] Preserve precise viewer placements Keep placement transformations in double precision through streaming, sidecar caching, and viewport recomposition so large coordinates can be cancelled before the final GPU float upload. Generated with the assistance of an AI coding tool. --- src/ifcviewer-full/Measurement.cpp | 2 +- src/ifcviewer/GeometryStreamer.cpp | 13 +++++++++---- src/ifcviewer/InstancedGeometry.h | 17 ++++++++++------- src/ifcviewer/SceneLoader.cpp | 9 ++++----- src/ifcviewer/SidecarBuilder.cpp | 15 +++++++-------- src/ifcviewer/SidecarCache.cpp | 4 ++-- src/ifcviewer/SidecarCache.h | 6 +++++- src/ifcviewer/ViewportWindow.cpp | 19 ++++++++++--------- src/ifcviewer/ViewportWindow.h | 2 +- src/ifcviewer/tests/test_sidecar_cache.cpp | 4 ++-- 10 files changed, 51 insertions(+), 40 deletions(-) diff --git a/src/ifcviewer-full/Measurement.cpp b/src/ifcviewer-full/Measurement.cpp index 77e62f798e..104689f497 100644 --- a/src/ifcviewer-full/Measurement.cpp +++ b/src/ifcviewer-full/Measurement.cpp @@ -55,7 +55,7 @@ double meshLocalVolume(const ViewportWindow::MeshTriangles& tris) { return std::abs(sum) / 6.0; } -double det3(const float M[16]) { +double det3(const double M[16]) { // Upper-left 3x3 of a column-major 4x4: M[col * 4 + row]. const double m00 = M[0], m10 = M[1], m20 = M[2]; const double m01 = M[4], m11 = M[5], m21 = M[6]; diff --git a/src/ifcviewer/GeometryStreamer.cpp b/src/ifcviewer/GeometryStreamer.cpp index 786a923a8d..20216d2187 100644 --- a/src/ifcviewer/GeometryStreamer.cpp +++ b/src/ifcviewer/GeometryStreamer.cpp @@ -619,8 +619,9 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { // Vertex rebasing cont.: post-multiply the per-instance // PlacementTransformation by T(+offset) so world position is - // preserved. Matrix arithmetic is in double; narrow to float - // at the end. + // preserved. Keep the emitted placement in double so later + // CoordinateOperation / false-origin composition can cancel + // large translations before the final GPU float upload. Eigen::Matrix4d mat_d = tri_elem->transformation().data()->ccomponents(); if (mesh_aabbs[local_mesh_id].has_offset) { @@ -637,11 +638,15 @@ void GeometryStreamer::run(const std::string& path, int num_threads) { inst.object_id = object_id; inst.color_override_rgba8 = 0; for (int i = 0; i < 16; ++i) { - inst.transform[i] = static_cast(mat_d.data()[i]); + inst.transform[i] = mat_d.data()[i]; } const MeshAabb& ma = mesh_aabbs[local_mesh_id]; - worldAabbFromLocal(ma.lmin, ma.lmax, inst.transform, + float mat_f[16]; + for (int i = 0; i < 16; ++i) { + mat_f[i] = static_cast(inst.transform[i]); + } + worldAabbFromLocal(ma.lmin, ma.lmax, mat_f, inst.world_aabb_min, inst.world_aabb_max); emit instanceReady(std::move(inst)); diff --git a/src/ifcviewer/InstancedGeometry.h b/src/ifcviewer/InstancedGeometry.h index 9b2eb02bba..20854f997b 100644 --- a/src/ifcviewer/InstancedGeometry.h +++ b/src/ifcviewer/InstancedGeometry.h @@ -101,17 +101,20 @@ static_assert(sizeof(InstanceGpu) == 80, "InstanceGpu must be 80 bytes"); // // `placement_transformation` is the raw streamer output (the iterator's // transform with vertex-rebasing offset folded in; pre-CoordinateOperation -// / FederatedFalseOrigin / ModelTransformation). `transform` is the -// composed FederatedFalseOrigin · ModelTransformation · CoordinateOperation -// · placement_transformation result — what gets uploaded to the SSBO and -// used to compute world_aabb_*. When ViewportWindow's stage matrices are -// all identity (default), the two are equal. +// / FederatedFalseOrigin / ModelTransformation). Keep it in double precision: +// large IFC placements must not be rounded before the federation false origin +// has a chance to cancel them. `transform` is the composed +// FederatedFalseOrigin · ModelTransformation · CoordinateOperation +// · placement_transformation result — narrowed to float only after composition, +// uploaded to the SSBO, and used to compute world_aabb_*. When ViewportWindow's +// stage matrices are all identity (default), transform is the float rendering +// copy of placement_transformation. struct InstanceCpu { uint32_t mesh_id = 0; // index into meshes array uint32_t object_id = 0; uint32_t color_override_rgba8 = 0; uint32_t model_id = 0; - float placement_transformation[16]{}; + double placement_transformation[16]{}; float transform[16]{}; float world_aabb_min[3]{}; float world_aabb_max[3]{}; @@ -139,7 +142,7 @@ struct InstanceChunk { uint32_t local_mesh_id = 0; uint32_t object_id = 0; uint32_t color_override_rgba8 = 0; - float transform[16]{}; + double transform[16]{}; float world_aabb_min[3]{}; float world_aabb_max[3]{}; }; diff --git a/src/ifcviewer/SceneLoader.cpp b/src/ifcviewer/SceneLoader.cpp index 2185438400..23379a949a 100644 --- a/src/ifcviewer/SceneLoader.cpp +++ b/src/ifcviewer/SceneLoader.cpp @@ -283,10 +283,9 @@ void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) { } if (!data.instances.empty() && !model.has_first_placement) { - using Mat4fCol = Eigen::Matrix; + using Mat4dCol = Eigen::Matrix; model.first_placement = - Eigen::Map(data.instances[0].placement_transformation) - .cast(); + Eigen::Map(data.instances[0].placement_transformation); model.has_first_placement = true; } @@ -356,9 +355,9 @@ void SceneLoader::onStreamerInstanceReady(InstanceChunk chunk) { auto it = models_.find(loading_model_id_); if (it != models_.end()) { if (!it->second.has_first_placement) { - using Mat4fCol = Eigen::Matrix; + using Mat4dCol = Eigen::Matrix; it->second.first_placement = - Eigen::Map(chunk.transform).cast(); + Eigen::Map(chunk.transform); it->second.has_first_placement = true; } if (it->second.sidecar_builder) { diff --git a/src/ifcviewer/SidecarBuilder.cpp b/src/ifcviewer/SidecarBuilder.cpp index 82687fa917..ebafab97bb 100644 --- a/src/ifcviewer/SidecarBuilder.cpp +++ b/src/ifcviewer/SidecarBuilder.cpp @@ -105,16 +105,15 @@ void SidecarBuilder::onInstanceReady(const InstanceChunk& chunk) { inst.color_override_rgba8 = chunk.color_override_rgba8; inst.model_id = chunk.model_id; - // The streamer's chunk.transform is the placement_transformation. With - // identity stage matrices (no FederatedFalseOrigin / ModelTransformation - // / CoordinateOperation applied yet), transform == placement_transformation - // and chunk.world_aabb_* is already the world AABB. ViewportWindow's - // applyCachedModel will recompose against the consumer's stage matrices - // at load time, so the cached transform/world_aabb is just a sensible - // identity-stage baseline. + // The streamer's chunk.transform is the double-precision + // placement_transformation. The cached float transform/world_aabb is only + // an identity-stage baseline; applyCachedModel recomposes from placement + // against the consumer's stage matrices at load time. std::memcpy(inst.placement_transformation, chunk.transform, sizeof(inst.placement_transformation)); - std::memcpy(inst.transform, chunk.transform, sizeof(inst.transform)); + for (int i = 0; i < 16; ++i) { + inst.transform[i] = static_cast(chunk.transform[i]); + } std::memcpy(inst.world_aabb_min, chunk.world_aabb_min, sizeof(inst.world_aabb_min)); std::memcpy(inst.world_aabb_max, chunk.world_aabb_max, sizeof(inst.world_aabb_max)); diff --git a/src/ifcviewer/SidecarCache.cpp b/src/ifcviewer/SidecarCache.cpp index 86951f7395..0b5a346e6a 100644 --- a/src/ifcviewer/SidecarCache.cpp +++ b/src/ifcviewer/SidecarCache.cpp @@ -17,7 +17,7 @@ * * ********************************************************************************/ -// v11 layout (all multi-byte fields native-endian; endianness marker in header). +// v12 layout (all multi-byte fields native-endian; endianness marker in header). // // SidecarHeader (12 bytes) // @@ -30,7 +30,7 @@ // MeshInfo[num_meshes] // // uint32_t num_instances -// InstanceCpu[num_instances] (already sorted by mesh_id; v10 layout) +// InstanceCpu[num_instances] (already sorted by mesh_id; v12 layout) // // uint32_t has_coordinate_operation (v11+) // double[16] coordinate_operation_meters (v11+; column-major) diff --git a/src/ifcviewer/SidecarCache.h b/src/ifcviewer/SidecarCache.h index ac33e200de..78b5ac2e0a 100644 --- a/src/ifcviewer/SidecarCache.h +++ b/src/ifcviewer/SidecarCache.h @@ -59,7 +59,11 @@ static constexpr uint32_t SIDECAR_MAGIC = 0x49465657; // "IFVW" // georef without re-parsing the IFC source. Edits to the IFC's // IfcMapConversion do NOT invalidate the sidecar — delete the // .ifcview manually if you change the source's georef parameters. -static constexpr uint32_t SIDECAR_VERSION = 11; +// v12 = InstanceCpu::placement_transformation is double[16], and +// InstanceChunk carries the streamer placement as double[16]. This keeps +// large IFC placements exact until CoordinateOperation / FederatedFalseOrigin +// composition has reduced them to viewport-local float-sized values. +static constexpr uint32_t SIDECAR_VERSION = 12; static constexpr uint32_t SIDECAR_ENDIAN = 0x01020304; // Fixed-size element record. Strings are stored as (offset, length) pairs diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index d3448006f3..4055d9b10c 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -1106,9 +1106,8 @@ void ViewportWindow::uploadInstanceChunk(const InstanceChunk& chunk) { sizeof(inst.placement_transformation)); // Compose against the model's current stage matrices to fill in // inst.transform + inst.world_aabb_*. When all stages are identity - // (the default until a setter is called), this reduces to - // transform == placement_transformation and the world AABB matches - // the streamer's pre-computed chunk.world_aabb_* exactly. + // (the default until a setter is called), this reduces to a float render + // copy of placement_transformation. composeInstanceFromPlacement(inst, m); m.instances.push_back(inst); m.instance_reflected.push_back(transformIsReflected(inst.transform) ? 1 : 0); @@ -3811,10 +3810,12 @@ void ViewportWindow::handleWheel(QWheelEvent* e) { void ViewportWindow::composeInstanceFromPlacement(InstanceCpu& inst, const ModelGpuData& m) const { - // Read placement_transformation as float-column-major and lift to double. + // Read placement_transformation in double so large IFC placements are + // cancelled by the stage matrices before the final GPU float upload. + using Mat4dCol = Eigen::Matrix; using Mat4fCol = Eigen::Matrix; const Eigen::Matrix4d P = - Eigen::Map(inst.placement_transformation).cast(); + Eigen::Map(inst.placement_transformation); // FederatedFalseOrigin · ModelTransformation · CoordinateOperation · P. const Eigen::Matrix4d composed = @@ -3960,9 +3961,9 @@ void ViewportWindow::printSelectedObjectCoords() { qInfo(" vertex: (no vertex data)"); } - using Mat4f = Eigen::Matrix; + using Mat4d = Eigen::Matrix; const Eigen::Matrix4d Pd = - Eigen::Map(inst.placement_transformation).cast(); + Eigen::Map(inst.placement_transformation); // global = CoordinateOperation · placement_transformation. // (FederatedFalseOrigin and ModelTransformation are user-side // tweaks; "global" here means the IFC's own georeferenced frame.) @@ -4099,9 +4100,9 @@ bool ViewportWindow::meshLocalToGlobal(uint32_t object_id, auto model_it = models_gpu_.find(inst.model_id); if (model_it == models_gpu_.end()) return false; - using Mat4fCol = Eigen::Matrix; + using Mat4dCol = Eigen::Matrix; const Eigen::Matrix4d placement = - Eigen::Map(inst.placement_transformation).cast(); + Eigen::Map(inst.placement_transformation); const Eigen::Vector4d local(mesh_local[0], mesh_local[1], mesh_local[2], 1.0); const Eigen::Vector3d global = (model_it->second.coordinate_operation_meters * placement * local).head<3>(); diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index c05ee1c493..5cb00b7447 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -203,7 +203,7 @@ public: struct InstanceLookup { uint32_t model_id = 0; uint32_t mesh_id = 0; - float placement_transformation[16]{}; + double placement_transformation[16]{}; }; bool findInstance(uint32_t object_id, InstanceLookup& out) const; diff --git a/src/ifcviewer/tests/test_sidecar_cache.cpp b/src/ifcviewer/tests/test_sidecar_cache.cpp index f592a37772..b4a96ff9a0 100644 --- a/src/ifcviewer/tests/test_sidecar_cache.cpp +++ b/src/ifcviewer/tests/test_sidecar_cache.cpp @@ -89,7 +89,7 @@ SidecarData buildFixture() { inst.color_override_rgba8 = uint32_t(0xAA000000u | (i * 0x010203u)); inst.model_id = 1; for (int k = 0; k < 16; ++k) { - inst.placement_transformation[k] = float(i) * 0.25f + float(k); + inst.placement_transformation[k] = double(i) * 0.25 + double(k); inst.transform[k] = float(i) * 0.5f + float(k); } inst.world_aabb_min[0] = float(i); @@ -155,7 +155,7 @@ bool sidecarDataEqual(const SidecarData& a, const SidecarData& b) { TEST_CASE("MeshInfo and InstanceCpu have stable layouts (sidecar wire format)", "[sidecar]") { REQUIRE(sizeof(MeshInfo) == 56); REQUIRE(sizeof(InstanceGpu) == 80); - REQUIRE(SIDECAR_VERSION == 11); + REQUIRE(SIDECAR_VERSION == 12); REQUIRE(SIDECAR_MAGIC == 0x49465657u); }