ifcviewer-full: print object volume on click via lazy GPU readback

Adds neutral primitives on ViewportWindow (readbackMeshTriangles,
findInstance) so consumers can compute per-object geometry queries
without the library retaining a CPU triangle copy. Measurement.cpp in
ifcviewer-full uses them to sum signed-tetrahedra in mesh-local space,
weighted by |det(placement_3x3)| per instance for mapped-item scaling.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Dion Moult
2026-05-07 11:25:30 +10:00
parent 0bb6df6a6b
commit f2b655fcf6
5 changed files with 220 additions and 0 deletions
+6
View File
@@ -21,6 +21,7 @@
#include "AppSettings.h"
#include "Federation.h"
#include "FederationSettingsDialog.h"
#include "Measurement.h"
#include "ModelTransformationDialog.h"
#include "SettingsWindow.h"
#include "LodBuilder.h"
@@ -871,6 +872,11 @@ void MainWindow::onObjectPicked(uint32_t object_id) {
}
populateProperties(object_id);
if (object_id != 0) {
const double v = volumeOfObjects(*viewport_, {object_id});
qInfo("Volume of object %u: %.6f m^3", object_id, v);
}
}
void MainWindow::onTreeSelectionChanged() {
+89
View File
@@ -0,0 +1,89 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#include "Measurement.h"
#include "ViewportWindow.h"
#include <cmath>
#include <cstring>
#include <unordered_map>
#include <vector>
namespace {
double meshLocalVolume(const ViewportWindow::MeshTriangles& tris) {
// Signed tetrahedra from the origin: V = sum( a · (b × c) ) / 6.
// Absolute value at the end so winding convention doesn't matter.
double sum = 0.0;
const size_t n = tris.indices.size();
for (size_t i = 0; i + 2 < n; i += 3) {
const uint32_t ia = tris.indices[i + 0];
const uint32_t ib = tris.indices[i + 1];
const uint32_t ic = tris.indices[i + 2];
const float* a = &tris.positions[3 * ia];
const float* b = &tris.positions[3 * ib];
const float* c = &tris.positions[3 * ic];
const double cx = double(b[1]) * c[2] - double(b[2]) * c[1];
const double cy = double(b[2]) * c[0] - double(b[0]) * c[2];
const double cz = double(b[0]) * c[1] - double(b[1]) * c[0];
sum += double(a[0]) * cx + double(a[1]) * cy + double(a[2]) * cz;
}
return std::abs(sum) / 6.0;
}
double det3(const float 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];
const double m02 = M[8], m12 = M[9], m22 = M[10];
return m00 * (m11 * m22 - m12 * m21)
- m01 * (m10 * m22 - m12 * m20)
+ m02 * (m10 * m21 - m11 * m20);
}
} // namespace
double volumeOfObjects(ViewportWindow& vp,
const std::vector<uint32_t>& object_ids) {
if (object_ids.empty()) return 0.0;
// Group selected instances by (model_id, mesh_id) so each unique mesh
// is read back at most once per call. Each entry stores the |det| of
// every instance of that mesh in the request.
std::unordered_map<uint64_t, std::vector<double>> by_mesh;
by_mesh.reserve(object_ids.size());
for (uint32_t oid : object_ids) {
ViewportWindow::InstanceLookup lk;
if (!vp.findInstance(oid, lk)) continue;
const uint64_t key = (uint64_t(lk.model_id) << 32) | lk.mesh_id;
by_mesh[key].push_back(std::abs(det3(lk.placement_transformation)));
}
double total = 0.0;
ViewportWindow::MeshTriangles tris;
for (const auto& [key, dets] : by_mesh) {
const uint32_t model_id = uint32_t(key >> 32);
const uint32_t mesh_id = uint32_t(key & 0xffffffffu);
if (!vp.readbackMeshTriangles(model_id, mesh_id, tris)) continue;
const double v = meshLocalVolume(tris);
for (double d : dets) total += v * d;
}
return total;
}
+38
View File
@@ -0,0 +1,38 @@
/********************************************************************************
* *
* This file is part of IfcOpenShell. *
* *
* IfcOpenShell is free software: you can redistribute it and/or modify *
* it under the terms of the Lesser GNU General Public License as published by *
* the Free Software Foundation, either version 3.0 of the License, or *
* (at your option) any later version. *
* *
* IfcOpenShell is distributed in the hope that it will be useful, *
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
* Lesser GNU General Public License for more details. *
* *
* You should have received a copy of the Lesser GNU General Public License *
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
* *
********************************************************************************/
#ifndef IFCVIEWER_FULL_MEASUREMENT_H
#define IFCVIEWER_FULL_MEASUREMENT_H
#include <cstdint>
#include <vector>
class ViewportWindow;
// Sum of mesh-local volumes (m³) of every instance whose object_id is in
// `object_ids`. Groups by (model, mesh) so each unique mesh is read back
// from the GPU at most once per call; instances of the same mesh are scaled
// by |det(placement_3x3)| to pick up mapped-item scale/mirror. Volume is
// taken as the absolute value of the signed-tetrahedra sum, so winding
// convention does not matter. Returns 0.0 for empty input or when nothing
// resolves. Recomputes from scratch on every call — no cache.
double volumeOfObjects(ViewportWindow& vp,
const std::vector<uint32_t>& object_ids);
#endif // IFCVIEWER_FULL_MEASUREMENT_H
+62
View File
@@ -3747,3 +3747,65 @@ void ViewportWindow::printSelectedObjectCoords() {
qInfo("printSelectedObjectCoords: object_id %u not found in any model",
selected_object_id_);
}
bool ViewportWindow::readbackMeshTriangles(uint32_t model_id, uint32_t mesh_id,
MeshTriangles& out) {
out.positions.clear();
out.indices.clear();
if (!gl_initialized_) return false;
auto it = models_gpu_.find(model_id);
if (it == models_gpu_.end()) return false;
const ModelGpuData& m = it->second;
if (!m.finalized) return false;
if (mesh_id >= m.meshes.size()) return false;
const MeshInfo& mi = m.meshes[mesh_id];
if (mi.vertex_count == 0 || mi.index_count == 0) return false;
context_->makeCurrent(this);
// Vertices: stride is INSTANCED_VERTEX_STRIDE_BYTES (12), positions are
// the first 6 bytes (3 x uint16) of each vertex. Read the full range
// and stride through it — simpler than chasing a position-only buffer.
std::vector<uint8_t> raw(size_t(mi.vertex_count) * INSTANCED_VERTEX_STRIDE_BYTES);
gl_->glGetNamedBufferSubData(m.vbo, mi.vbo_byte_offset,
GLsizeiptr(raw.size()), raw.data());
out.positions.resize(size_t(mi.vertex_count) * 3);
const float ox = mi.local_aabb_min[0];
const float oy = mi.local_aabb_min[1];
const float oz = mi.local_aabb_min[2];
const float sx = (mi.local_aabb_max[0] - mi.local_aabb_min[0]) / 65535.0f;
const float sy = (mi.local_aabb_max[1] - mi.local_aabb_min[1]) / 65535.0f;
const float sz = (mi.local_aabb_max[2] - mi.local_aabb_min[2]) / 65535.0f;
for (uint32_t i = 0; i < mi.vertex_count; ++i) {
const uint16_t* p = reinterpret_cast<const uint16_t*>(
raw.data() + size_t(i) * INSTANCED_VERTEX_STRIDE_BYTES);
out.positions[3 * i + 0] = ox + sx * float(p[0]);
out.positions[3 * i + 1] = oy + sy * float(p[1]);
out.positions[3 * i + 2] = oz + sz * float(p[2]);
}
// Indices: LOD0 only — measurements should use the full-resolution mesh.
out.indices.resize(mi.index_count);
gl_->glGetNamedBufferSubData(m.ebo, mi.ebo_byte_offset,
GLsizeiptr(mi.index_count * sizeof(uint32_t)),
out.indices.data());
return true;
}
bool ViewportWindow::findInstance(uint32_t object_id, InstanceLookup& out) const {
if (object_id == 0) return false;
for (const auto& kv : models_gpu_) {
const ModelGpuData& m = kv.second;
for (const InstanceCpu& inst : m.instances) {
if (inst.object_id != object_id) continue;
out.model_id = inst.model_id;
out.mesh_id = inst.mesh_id;
std::memcpy(out.placement_transformation,
inst.placement_transformation,
sizeof(out.placement_transformation));
return true;
}
}
return false;
}
+25
View File
@@ -183,6 +183,31 @@ public:
// (CoordinateOperation · placement_transformation), in metres.
void printSelectedObjectCoords();
// Mesh-local CPU triangles read back from the VBO/EBO. Positions are
// dequantised against the mesh's local AABB; units match the streamer
// (metres). Indices are mesh-local (0..vertex_count-1).
struct MeshTriangles {
std::vector<float> positions; // 3 * vertex_count
std::vector<uint32_t> indices; // 3 * triangle_count
};
// Lazy GPU readback of one mesh's triangles. Fails if model_id /
// mesh_id aren't live, the model isn't finalised, or GL isn't ready.
// Stalls the GL pipeline for the readback — call from the main thread
// and not from inside render().
bool readbackMeshTriangles(uint32_t model_id, uint32_t mesh_id,
MeshTriangles& out);
// Pure CPU lookup: object_id → owning model + mesh + raw streamer
// placement matrix (column-major, pre-CoordinateOperation /
// FederatedFalseOrigin / ModelTransformation).
struct InstanceLookup {
uint32_t model_id = 0;
uint32_t mesh_id = 0;
float placement_transformation[16]{};
};
bool findInstance(uint32_t object_id, InstanceLookup& out) const;
// Federation pipeline: composed instance transform =
// FederatedFalseOrigin · ModelTransformation · CoordinateOperation
// · placement_transformation