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.
This commit is contained in:
Dion Moult
2026-05-19 17:31:53 +10:00
parent 8b8fafa698
commit e0a504417c
10 changed files with 51 additions and 40 deletions
+1 -1
View File
@@ -55,7 +55,7 @@ double meshLocalVolume(const ViewportWindow::MeshTriangles& tris) {
return std::abs(sum) / 6.0; 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]. // 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 m00 = M[0], m10 = M[1], m20 = M[2];
const double m01 = M[4], m11 = M[5], m21 = M[6]; const double m01 = M[4], m11 = M[5], m21 = M[6];
+9 -4
View File
@@ -619,8 +619,9 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
// Vertex rebasing cont.: post-multiply the per-instance // Vertex rebasing cont.: post-multiply the per-instance
// PlacementTransformation by T(+offset) so world position is // PlacementTransformation by T(+offset) so world position is
// preserved. Matrix arithmetic is in double; narrow to float // preserved. Keep the emitted placement in double so later
// at the end. // CoordinateOperation / false-origin composition can cancel
// large translations before the final GPU float upload.
Eigen::Matrix4d mat_d = Eigen::Matrix4d mat_d =
tri_elem->transformation().data()->ccomponents(); tri_elem->transformation().data()->ccomponents();
if (mesh_aabbs[local_mesh_id].has_offset) { 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.object_id = object_id;
inst.color_override_rgba8 = 0; inst.color_override_rgba8 = 0;
for (int i = 0; i < 16; ++i) { for (int i = 0; i < 16; ++i) {
inst.transform[i] = static_cast<float>(mat_d.data()[i]); inst.transform[i] = mat_d.data()[i];
} }
const MeshAabb& ma = mesh_aabbs[local_mesh_id]; 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<float>(inst.transform[i]);
}
worldAabbFromLocal(ma.lmin, ma.lmax, mat_f,
inst.world_aabb_min, inst.world_aabb_max); inst.world_aabb_min, inst.world_aabb_max);
emit instanceReady(std::move(inst)); emit instanceReady(std::move(inst));
+10 -7
View File
@@ -101,17 +101,20 @@ static_assert(sizeof(InstanceGpu) == 80, "InstanceGpu must be 80 bytes");
// //
// `placement_transformation` is the raw streamer output (the iterator's // `placement_transformation` is the raw streamer output (the iterator's
// transform with vertex-rebasing offset folded in; pre-CoordinateOperation // transform with vertex-rebasing offset folded in; pre-CoordinateOperation
// / FederatedFalseOrigin / ModelTransformation). `transform` is the // / FederatedFalseOrigin / ModelTransformation). Keep it in double precision:
// composed FederatedFalseOrigin · ModelTransformation · CoordinateOperation // large IFC placements must not be rounded before the federation false origin
// · placement_transformation result — what gets uploaded to the SSBO and // has a chance to cancel them. `transform` is the composed
// used to compute world_aabb_*. When ViewportWindow's stage matrices are // FederatedFalseOrigin · ModelTransformation · CoordinateOperation
// all identity (default), the two are equal. // · 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 { struct InstanceCpu {
uint32_t mesh_id = 0; // index into meshes array uint32_t mesh_id = 0; // index into meshes array
uint32_t object_id = 0; uint32_t object_id = 0;
uint32_t color_override_rgba8 = 0; uint32_t color_override_rgba8 = 0;
uint32_t model_id = 0; uint32_t model_id = 0;
float placement_transformation[16]{}; double placement_transformation[16]{};
float transform[16]{}; float transform[16]{};
float world_aabb_min[3]{}; float world_aabb_min[3]{};
float world_aabb_max[3]{}; float world_aabb_max[3]{};
@@ -139,7 +142,7 @@ struct InstanceChunk {
uint32_t local_mesh_id = 0; uint32_t local_mesh_id = 0;
uint32_t object_id = 0; uint32_t object_id = 0;
uint32_t color_override_rgba8 = 0; uint32_t color_override_rgba8 = 0;
float transform[16]{}; double transform[16]{};
float world_aabb_min[3]{}; float world_aabb_min[3]{};
float world_aabb_max[3]{}; float world_aabb_max[3]{};
}; };
+4 -5
View File
@@ -283,10 +283,9 @@ void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) {
} }
if (!data.instances.empty() && !model.has_first_placement) { if (!data.instances.empty() && !model.has_first_placement) {
using Mat4fCol = Eigen::Matrix<float, 4, 4, Eigen::ColMajor>; using Mat4dCol = Eigen::Matrix<double, 4, 4, Eigen::ColMajor>;
model.first_placement = model.first_placement =
Eigen::Map<const Mat4fCol>(data.instances[0].placement_transformation) Eigen::Map<const Mat4dCol>(data.instances[0].placement_transformation);
.cast<double>();
model.has_first_placement = true; model.has_first_placement = true;
} }
@@ -356,9 +355,9 @@ void SceneLoader::onStreamerInstanceReady(InstanceChunk chunk) {
auto it = models_.find(loading_model_id_); auto it = models_.find(loading_model_id_);
if (it != models_.end()) { if (it != models_.end()) {
if (!it->second.has_first_placement) { if (!it->second.has_first_placement) {
using Mat4fCol = Eigen::Matrix<float, 4, 4, Eigen::ColMajor>; using Mat4dCol = Eigen::Matrix<double, 4, 4, Eigen::ColMajor>;
it->second.first_placement = it->second.first_placement =
Eigen::Map<const Mat4fCol>(chunk.transform).cast<double>(); Eigen::Map<const Mat4dCol>(chunk.transform);
it->second.has_first_placement = true; it->second.has_first_placement = true;
} }
if (it->second.sidecar_builder) { if (it->second.sidecar_builder) {
+7 -8
View File
@@ -105,16 +105,15 @@ void SidecarBuilder::onInstanceReady(const InstanceChunk& chunk) {
inst.color_override_rgba8 = chunk.color_override_rgba8; inst.color_override_rgba8 = chunk.color_override_rgba8;
inst.model_id = chunk.model_id; inst.model_id = chunk.model_id;
// The streamer's chunk.transform is the placement_transformation. With // The streamer's chunk.transform is the double-precision
// identity stage matrices (no FederatedFalseOrigin / ModelTransformation // placement_transformation. The cached float transform/world_aabb is only
// / CoordinateOperation applied yet), transform == placement_transformation // an identity-stage baseline; applyCachedModel recomposes from placement
// and chunk.world_aabb_* is already the world AABB. ViewportWindow's // against the consumer's stage matrices at load time.
// 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.
std::memcpy(inst.placement_transformation, chunk.transform, std::memcpy(inst.placement_transformation, chunk.transform,
sizeof(inst.placement_transformation)); 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<float>(chunk.transform[i]);
}
std::memcpy(inst.world_aabb_min, chunk.world_aabb_min, sizeof(inst.world_aabb_min)); 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)); std::memcpy(inst.world_aabb_max, chunk.world_aabb_max, sizeof(inst.world_aabb_max));
+2 -2
View File
@@ -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) // SidecarHeader (12 bytes)
// //
@@ -30,7 +30,7 @@
// MeshInfo[num_meshes] // MeshInfo[num_meshes]
// //
// uint32_t num_instances // 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+) // uint32_t has_coordinate_operation (v11+)
// double[16] coordinate_operation_meters (v11+; column-major) // double[16] coordinate_operation_meters (v11+; column-major)
+5 -1
View File
@@ -59,7 +59,11 @@ static constexpr uint32_t SIDECAR_MAGIC = 0x49465657; // "IFVW"
// georef without re-parsing the IFC source. Edits to the IFC's // georef without re-parsing the IFC source. Edits to the IFC's
// IfcMapConversion do NOT invalidate the sidecar — delete the // IfcMapConversion do NOT invalidate the sidecar — delete the
// .ifcview manually if you change the source's georef parameters. // .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; static constexpr uint32_t SIDECAR_ENDIAN = 0x01020304;
// Fixed-size element record. Strings are stored as (offset, length) pairs // Fixed-size element record. Strings are stored as (offset, length) pairs
+10 -9
View File
@@ -1106,9 +1106,8 @@ void ViewportWindow::uploadInstanceChunk(const InstanceChunk& chunk) {
sizeof(inst.placement_transformation)); sizeof(inst.placement_transformation));
// Compose against the model's current stage matrices to fill in // Compose against the model's current stage matrices to fill in
// inst.transform + inst.world_aabb_*. When all stages are identity // inst.transform + inst.world_aabb_*. When all stages are identity
// (the default until a setter is called), this reduces to // (the default until a setter is called), this reduces to a float render
// transform == placement_transformation and the world AABB matches // copy of placement_transformation.
// the streamer's pre-computed chunk.world_aabb_* exactly.
composeInstanceFromPlacement(inst, m); composeInstanceFromPlacement(inst, m);
m.instances.push_back(inst); m.instances.push_back(inst);
m.instance_reflected.push_back(transformIsReflected(inst.transform) ? 1 : 0); m.instance_reflected.push_back(transformIsReflected(inst.transform) ? 1 : 0);
@@ -3811,10 +3810,12 @@ void ViewportWindow::handleWheel(QWheelEvent* e) {
void ViewportWindow::composeInstanceFromPlacement(InstanceCpu& inst, void ViewportWindow::composeInstanceFromPlacement(InstanceCpu& inst,
const ModelGpuData& m) const { 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<double, 4, 4, Eigen::ColMajor>;
using Mat4fCol = Eigen::Matrix<float, 4, 4, Eigen::ColMajor>; using Mat4fCol = Eigen::Matrix<float, 4, 4, Eigen::ColMajor>;
const Eigen::Matrix4d P = const Eigen::Matrix4d P =
Eigen::Map<const Mat4fCol>(inst.placement_transformation).cast<double>(); Eigen::Map<const Mat4dCol>(inst.placement_transformation);
// FederatedFalseOrigin · ModelTransformation · CoordinateOperation · P. // FederatedFalseOrigin · ModelTransformation · CoordinateOperation · P.
const Eigen::Matrix4d composed = const Eigen::Matrix4d composed =
@@ -3960,9 +3961,9 @@ void ViewportWindow::printSelectedObjectCoords() {
qInfo(" vertex: (no vertex data)"); qInfo(" vertex: (no vertex data)");
} }
using Mat4f = Eigen::Matrix<float, 4, 4, Eigen::ColMajor>; using Mat4d = Eigen::Matrix<double, 4, 4, Eigen::ColMajor>;
const Eigen::Matrix4d Pd = const Eigen::Matrix4d Pd =
Eigen::Map<const Mat4f>(inst.placement_transformation).cast<double>(); Eigen::Map<const Mat4d>(inst.placement_transformation);
// global = CoordinateOperation · placement_transformation. // global = CoordinateOperation · placement_transformation.
// (FederatedFalseOrigin and ModelTransformation are user-side // (FederatedFalseOrigin and ModelTransformation are user-side
// tweaks; "global" here means the IFC's own georeferenced frame.) // 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); auto model_it = models_gpu_.find(inst.model_id);
if (model_it == models_gpu_.end()) return false; if (model_it == models_gpu_.end()) return false;
using Mat4fCol = Eigen::Matrix<float, 4, 4, Eigen::ColMajor>; using Mat4dCol = Eigen::Matrix<double, 4, 4, Eigen::ColMajor>;
const Eigen::Matrix4d placement = const Eigen::Matrix4d placement =
Eigen::Map<const Mat4fCol>(inst.placement_transformation).cast<double>(); Eigen::Map<const Mat4dCol>(inst.placement_transformation);
const Eigen::Vector4d local(mesh_local[0], mesh_local[1], mesh_local[2], 1.0); const Eigen::Vector4d local(mesh_local[0], mesh_local[1], mesh_local[2], 1.0);
const Eigen::Vector3d global = const Eigen::Vector3d global =
(model_it->second.coordinate_operation_meters * placement * local).head<3>(); (model_it->second.coordinate_operation_meters * placement * local).head<3>();
+1 -1
View File
@@ -203,7 +203,7 @@ public:
struct InstanceLookup { struct InstanceLookup {
uint32_t model_id = 0; uint32_t model_id = 0;
uint32_t mesh_id = 0; uint32_t mesh_id = 0;
float placement_transformation[16]{}; double placement_transformation[16]{};
}; };
bool findInstance(uint32_t object_id, InstanceLookup& out) const; bool findInstance(uint32_t object_id, InstanceLookup& out) const;
+2 -2
View File
@@ -89,7 +89,7 @@ SidecarData buildFixture() {
inst.color_override_rgba8 = uint32_t(0xAA000000u | (i * 0x010203u)); inst.color_override_rgba8 = uint32_t(0xAA000000u | (i * 0x010203u));
inst.model_id = 1; inst.model_id = 1;
for (int k = 0; k < 16; ++k) { 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.transform[k] = float(i) * 0.5f + float(k);
} }
inst.world_aabb_min[0] = float(i); 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]") { TEST_CASE("MeshInfo and InstanceCpu have stable layouts (sidecar wire format)", "[sidecar]") {
REQUIRE(sizeof(MeshInfo) == 56); REQUIRE(sizeof(MeshInfo) == 56);
REQUIRE(sizeof(InstanceGpu) == 80); REQUIRE(sizeof(InstanceGpu) == 80);
REQUIRE(SIDECAR_VERSION == 11); REQUIRE(SIDECAR_VERSION == 12);
REQUIRE(SIDECAR_MAGIC == 0x49465657u); REQUIRE(SIDECAR_MAGIC == 0x49465657u);
} }