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;
}
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];
+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
// 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<float>(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<float>(inst.transform[i]);
}
worldAabbFromLocal(ma.lmin, ma.lmax, mat_f,
inst.world_aabb_min, inst.world_aabb_max);
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
// 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]{};
};
+4 -5
View File
@@ -283,10 +283,9 @@ void SceneLoader::applySidecarData(uint32_t mid, SidecarData data) {
}
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 =
Eigen::Map<const Mat4fCol>(data.instances[0].placement_transformation)
.cast<double>();
Eigen::Map<const Mat4dCol>(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<float, 4, 4, Eigen::ColMajor>;
using Mat4dCol = Eigen::Matrix<double, 4, 4, Eigen::ColMajor>;
it->second.first_placement =
Eigen::Map<const Mat4fCol>(chunk.transform).cast<double>();
Eigen::Map<const Mat4dCol>(chunk.transform);
it->second.has_first_placement = true;
}
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.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<float>(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));
+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)
//
@@ -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)
+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
// 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
+10 -9
View File
@@ -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<double, 4, 4, Eigen::ColMajor>;
using Mat4fCol = Eigen::Matrix<float, 4, 4, Eigen::ColMajor>;
const Eigen::Matrix4d P =
Eigen::Map<const Mat4fCol>(inst.placement_transformation).cast<double>();
Eigen::Map<const Mat4dCol>(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<float, 4, 4, Eigen::ColMajor>;
using Mat4d = Eigen::Matrix<double, 4, 4, Eigen::ColMajor>;
const Eigen::Matrix4d Pd =
Eigen::Map<const Mat4f>(inst.placement_transformation).cast<double>();
Eigen::Map<const Mat4d>(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<float, 4, 4, Eigen::ColMajor>;
using Mat4dCol = Eigen::Matrix<double, 4, 4, Eigen::ColMajor>;
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::Vector3d global =
(model_it->second.coordinate_operation_meters * placement * local).head<3>();
+1 -1
View File
@@ -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;
+2 -2
View File
@@ -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);
}