diff --git a/src/ifcviewer/InstancedGeometry.h b/src/ifcviewer/InstancedGeometry.h index accf155bf9..9b2eb02bba 100644 --- a/src/ifcviewer/InstancedGeometry.h +++ b/src/ifcviewer/InstancedGeometry.h @@ -98,11 +98,20 @@ static_assert(sizeof(InstanceGpu) == 80, "InstanceGpu must be 80 bytes"); // CPU-side per-instance data. The GPU record above is derived from this; // we also retain the world AABB for BVH construction and the mesh_id. +// +// `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. 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; + 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]{}; float transform[16]{}; float world_aabb_min[3]{}; float world_aabb_max[3]{}; diff --git a/src/ifcviewer/SidecarCache.h b/src/ifcviewer/SidecarCache.h index 95c8a51c38..916a42acb1 100644 --- a/src/ifcviewer/SidecarCache.h +++ b/src/ifcviewer/SidecarCache.h @@ -46,7 +46,14 @@ static constexpr uint32_t SIDECAR_MAGIC = 0x49465657; // "IFVW" // same cache serves either source format. Staleness is user-managed // (delete the sidecar to force a rebuild). // v9 = unused `reserved` field dropped from header (16 B -> 12 B). -static constexpr uint32_t SIDECAR_VERSION = 9; +// v10 = InstanceCpu gains placement_transformation[16] alongside transform[16] +// — record grew from 104 B to 168 B. placement_transformation is the +// raw streamer output; transform is the composed FederatedFalseOrigin · +// ModelTransformation · CoordinateOperation · placement_transformation +// result. Sidecar serialises both; on load the transform is recomputed +// from placement_transformation + the ViewportWindow's current stage +// matrices, so v10 sidecars are reusable across .ifcfeds. +static constexpr uint32_t SIDECAR_VERSION = 10; 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 d9aaa1f733..8aeb3300f3 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -567,6 +567,29 @@ static void extractFrustumPlanes(const QMatrix4x4& vp, float planes[6][4]) { } } +// Compute world AABB by transforming the 8 corners of `local_min..local_max` +// through the column-major 4x4 `M` and bounding the result. Same maths as +// GeometryStreamer's worldAabbFromLocal; duplicated here so the viewport +// can recompute world AABBs independently when stage matrices change. +static void worldAabbFromLocalVp(const float local_min[3], + const float local_max[3], + const float M[16], + float out_min[3], float out_max[3]) { + out_min[0] = out_min[1] = out_min[2] = std::numeric_limits::max(); + out_max[0] = out_max[1] = out_max[2] = -std::numeric_limits::max(); + for (int c = 0; c < 8; ++c) { + float x = (c & 1) ? local_max[0] : local_min[0]; + float y = (c & 2) ? local_max[1] : local_min[1]; + float z = (c & 4) ? local_max[2] : local_min[2]; + float wx = M[0]*x + M[4]*y + M[8]*z + M[12]; + float wy = M[1]*x + M[5]*y + M[9]*z + M[13]; + float wz = M[2]*x + M[6]*y + M[10]*z + M[14]; + if (wx < out_min[0]) out_min[0] = wx; if (wx > out_max[0]) out_max[0] = wx; + if (wy < out_min[1]) out_min[1] = wy; if (wy > out_max[1]) out_max[1] = wy; + if (wz < out_min[2]) out_min[2] = wz; if (wz > out_max[2]) out_max[2] = wz; + } +} + // Build bvh_items (one per instance, 1:1 ordering) and a per-model BVH. // Items with instances.size() < BVH_MIN_OBJECTS leave bvh empty — the // render path falls back to drawing every instance. @@ -1054,9 +1077,16 @@ void ViewportWindow::uploadInstanceChunk(const InstanceChunk& chunk) { inst.object_id = chunk.object_id; inst.color_override_rgba8 = chunk.color_override_rgba8; inst.model_id = chunk.model_id; - std::memcpy(inst.transform, chunk.transform, sizeof(inst.transform)); - 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)); + // Streamer's `chunk.transform` is the placement_transformation (the + // iterator's per-shape transform with vertex-rebasing offset folded in). + std::memcpy(inst.placement_transformation, chunk.transform, + 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. + composeInstanceFromPlacement(inst, m); m.instances.push_back(inst); m.instance_reflected.push_back(transformIsReflected(inst.transform) ? 1 : 0); @@ -1209,6 +1239,14 @@ void ViewportWindow::applyCachedModel(uint32_t model_id, SidecarData data) { } m.total_triangles = total_tri; + // Recompose every instance against the model's current stage matrices. + // The cached InstanceCpu::transform / world_aabb_* may have been frozen + // against a different .ifcfed's stages — placement_transformation is the + // authoritative input, so we always rebuild transform + world_aabb here. + for (auto& inst : m.instances) { + composeInstanceFromPlacement(inst, m); + } + // Build and upload the instance SSBO. std::vector gpu(m.instances.size()); for (size_t i = 0; i < m.instances.size(); ++i) { @@ -3523,3 +3561,106 @@ void ViewportWindow::handleWheel(QWheelEvent* e) { setPivotIndicatorVisible(true, 750); requestUpdate(); } + +// === Federation pipeline composition === + +void ViewportWindow::composeInstanceFromPlacement(InstanceCpu& inst, + const ModelGpuData& m) const { + // Read placement_transformation as float-column-major and lift to double. + using Mat4fCol = Eigen::Matrix; + const Eigen::Matrix4d P = + Eigen::Map(inst.placement_transformation).cast(); + + // FederatedFalseOrigin · ModelTransformation · CoordinateOperation · P. + const Eigen::Matrix4d composed = + federated_false_origin_meters_ * + m.model_transformation_meters * + m.coordinate_operation_meters * + P; + + Eigen::Map T_f(inst.transform); + T_f = composed.cast(); + + // World AABB from the composed transform + the mesh's local AABB. + if (inst.mesh_id < m.meshes.size()) { + const MeshInfo& mi = m.meshes[inst.mesh_id]; + worldAabbFromLocalVp(mi.local_aabb_min, mi.local_aabb_max, + inst.transform, + inst.world_aabb_min, inst.world_aabb_max); + } else { + // Mesh not yet uploaded — leave AABB at zero. The streamer's + // contract emits MeshChunk before InstanceChunk so this branch + // shouldn't fire under normal flow. + for (int a = 0; a < 3; ++a) { + inst.world_aabb_min[a] = 0.0f; + inst.world_aabb_max[a] = 0.0f; + } + } +} + +void ViewportWindow::recomposeAndUploadModel(uint32_t model_id) { + if (!gl_initialized_) return; + auto it = models_gpu_.find(model_id); + if (it == models_gpu_.end()) return; + ModelGpuData& m = it->second; + if (m.instances.empty()) return; + context_->makeCurrent(this); + + // Recompose every instance + refresh reflection flag. + m.instance_reflected.resize(m.instances.size()); + std::vector gpu(m.instances.size()); + for (size_t i = 0; i < m.instances.size(); ++i) { + InstanceCpu& inst = m.instances[i]; + composeInstanceFromPlacement(inst, m); + m.instance_reflected[i] = transformIsReflected(inst.transform) ? 1 : 0; + + InstanceGpu& dst = gpu[i]; + std::memcpy(dst.transform, inst.transform, sizeof(dst.transform)); + dst.object_id = inst.object_id; + dst.color_override_rgba8 = inst.color_override_rgba8; + dst.mesh_id = inst.mesh_id; + dst._pad1 = 0; + } + + // Re-upload the SSBO in one shot — its capacity already matches + // m.instances.size() since the SSBO grew incrementally during stream. + const size_t bytes = gpu.size() * sizeof(InstanceGpu); + if (bytes > m.ssbo_capacity) { + if (!growModelSsbo(m, bytes)) return; + } + if (bytes > 0) { + gl_->glNamedBufferSubData(m.ssbo, 0, bytes, gpu.data()); + } + m.ssbo_instance_count = static_cast(gpu.size()); + + // World AABBs changed, so the BVH is stale. + buildBvhForModel(m, model_id); + have_cached_cull_ = false; + requestUpdate(); +} + +void ViewportWindow::setFederatedFalseOrigin(const Eigen::Matrix4d& matrix_meters) { + if (federated_false_origin_meters_ == matrix_meters) return; + federated_false_origin_meters_ = matrix_meters; + for (auto& kv : models_gpu_) { + recomposeAndUploadModel(kv.first); + } +} + +void ViewportWindow::setModelCoordinateOperation(uint32_t model_id, + const Eigen::Matrix4d& matrix_meters) { + auto it = models_gpu_.find(model_id); + if (it == models_gpu_.end()) return; + if (it->second.coordinate_operation_meters == matrix_meters) return; + it->second.coordinate_operation_meters = matrix_meters; + recomposeAndUploadModel(model_id); +} + +void ViewportWindow::setModelTransformation(uint32_t model_id, + const Eigen::Matrix4d& matrix_meters) { + auto it = models_gpu_.find(model_id); + if (it == models_gpu_.end()) return; + if (it->second.model_transformation_meters == matrix_meters) return; + it->second.model_transformation_meters = matrix_meters; + recomposeAndUploadModel(model_id); +} diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index a64657fbc2..9c49d1ef98 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -41,6 +41,8 @@ QT_END_NAMESPACE #include #include +#include + #include "BvhAccel.h" #include "InstancedGeometry.h" #include "SidecarCache.h" @@ -124,6 +126,12 @@ struct ModelGpuData { bool finalized = false; bool hidden = false; + + // Per-model federation-pipeline matrices in metres. Default identity + // → no per-model contribution to the composed transform. See + // Federation.h for the full pipeline composition order. + Eigen::Matrix4d coordinate_operation_meters = Eigen::Matrix4d::Identity(); + Eigen::Matrix4d model_transformation_meters = Eigen::Matrix4d::Identity(); }; // Rendering is event-driven: render() runs only when QEvent::UpdateRequest @@ -169,6 +177,18 @@ public: void showModel(uint32_t model_id); void removeModel(uint32_t model_id); + // Federation pipeline: composed instance transform = + // FederatedFalseOrigin · ModelTransformation · CoordinateOperation + // · placement_transformation + // setFederatedFalseOrigin affects every model; the per-model setters + // affect a single model. Each setter rewrites the SSBO, recomputes + // world AABBs, rebuilds the BVH, and posts an update. Defaults are + // identity, so until a setter is called the composed transform equals + // placement_transformation. + void setFederatedFalseOrigin(const Eigen::Matrix4d& matrix_meters); + void setModelCoordinateOperation(uint32_t model_id, const Eigen::Matrix4d& matrix_meters); + void setModelTransformation(uint32_t model_id, const Eigen::Matrix4d& matrix_meters); + void setSelectedObjectId(uint32_t id); uint32_t pickObjectAt(int x, int y); @@ -364,6 +384,21 @@ private: // model's SSBO + indirect buffer, growing them if needed. void uploadCullResults(ModelGpuData& m); + // Compose + // FederatedFalseOrigin · ModelTransformation · CoordinateOperation + // · placement_transformation + // for one instance: writes inst.transform (composed, float) and + // recomputes inst.world_aabb_* from the composed transform + the + // mesh's local AABB. Maths runs in double; narrow to float at the end. + void composeInstanceFromPlacement(InstanceCpu& inst, const ModelGpuData& m) const; + + // Walk every instance of `model_id`, recompose `transform` from + // `placement_transformation` + current stage matrices, recompute world + // AABBs, re-upload the InstanceGpu SSBO, refresh instance_reflected, + // and rebuild the BVH. Posts an update. No-op if model_id is unknown + // or GL isn't initialised yet. + void recomposeAndUploadModel(uint32_t model_id); + // Mouse interaction void handleMousePress(QMouseEvent* event); void handleMouseRelease(QMouseEvent* event); @@ -413,6 +448,10 @@ private: // Per-model GPU data std::unordered_map models_gpu_; + // FederatedFalseOrigin matrix, in metres. Default identity → no + // contribution to the composed transform. See Federation.h. + Eigen::Matrix4d federated_false_origin_meters_ = Eigen::Matrix4d::Identity(); + // Pick framebuffer. Three color attachments: // 0: R32UI — object_id // 1: RGB32F — world position at hit