mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-19 11:43:53 +00:00
GPU instancing: streamer, viewport, shaders rewritten
Commit A of the instancing migration (Phase 3a). The streamer now runs the iterator with use-world-coords=false and dedupes by the geometry's representation id, emitting a MeshChunk once per unique geometry and an InstanceChunk per placement. The viewport keeps geometry in local coordinates (28 B/vertex, down from 32) and applies the per-instance transform in the vertex shader via an std430 SSBO indexed by gl_InstanceID + a per-draw uniform offset. After streaming finishes finalizeModel() stable-sorts instances by mesh_id, assigns each mesh a contiguous range, and uploads the SSBO; render then issues one glDrawElementsInstancedBaseVertex per mesh. BvhAccel is reshaped to operate on a generic BvhItem (world AABB + model_id) so it can drive instance-level culling, but the path is not wired in yet -- every instance is drawn every frame in this commit. Progressive-during-streaming rendering is likewise disabled: a model appears when its SSBO is uploaded, not incrementally. Sidecar cache is stubbed (reads miss, writes are no-ops); the v4 on-disk format with MeshInfo + InstanceGpu sections lands in Commit B. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+42
-129
@@ -23,7 +23,6 @@
|
||||
#include <cassert>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <numeric>
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -31,38 +30,36 @@ struct Centroid {
|
||||
float x, y, z;
|
||||
};
|
||||
|
||||
Centroid computeCentroid(const ObjectDrawInfo& obj) {
|
||||
Centroid computeCentroid(const BvhItem& it) {
|
||||
return {
|
||||
(obj.aabb_min[0] + obj.aabb_max[0]) * 0.5f,
|
||||
(obj.aabb_min[1] + obj.aabb_max[1]) * 0.5f,
|
||||
(obj.aabb_min[2] + obj.aabb_max[2]) * 0.5f
|
||||
(it.aabb_min[0] + it.aabb_max[0]) * 0.5f,
|
||||
(it.aabb_min[1] + it.aabb_max[1]) * 0.5f,
|
||||
(it.aabb_min[2] + it.aabb_max[2]) * 0.5f
|
||||
};
|
||||
}
|
||||
|
||||
void computeAABB(const std::vector<ObjectDrawInfo>& draw_info,
|
||||
void computeAABB(const std::vector<BvhItem>& items,
|
||||
const uint32_t* indices, uint32_t count,
|
||||
float out_min[3], float out_max[3]) {
|
||||
out_min[0] = out_min[1] = out_min[2] = std::numeric_limits<float>::max();
|
||||
out_max[0] = out_max[1] = out_max[2] = -std::numeric_limits<float>::max();
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
const auto& obj = draw_info[indices[i]];
|
||||
const auto& it = items[indices[i]];
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
if (obj.aabb_min[a] < out_min[a]) out_min[a] = obj.aabb_min[a];
|
||||
if (obj.aabb_max[a] > out_max[a]) out_max[a] = obj.aabb_max[a];
|
||||
if (it.aabb_min[a] < out_min[a]) out_min[a] = it.aabb_min[a];
|
||||
if (it.aabb_max[a] > out_max[a]) out_max[a] = it.aabb_max[a];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recursive BVH builder. Writes nodes in pre-order DFS into mbvh.nodes.
|
||||
// object_indices[start..start+count) are the indices to partition.
|
||||
void buildRecursive(ModelBvh& mbvh,
|
||||
const std::vector<ObjectDrawInfo>& draw_info,
|
||||
const std::vector<BvhItem>& items,
|
||||
uint32_t start, uint32_t count) {
|
||||
uint32_t node_idx = static_cast<uint32_t>(mbvh.nodes.size());
|
||||
mbvh.nodes.emplace_back();
|
||||
BvhNode& node = mbvh.nodes[node_idx];
|
||||
|
||||
computeAABB(draw_info, &mbvh.object_indices[start], count,
|
||||
computeAABB(items, &mbvh.item_indices[start], count,
|
||||
node.aabb_min, node.aabb_max);
|
||||
|
||||
if (count <= BVH_MAX_LEAF_SIZE) {
|
||||
@@ -72,7 +69,6 @@ void buildRecursive(ModelBvh& mbvh,
|
||||
return;
|
||||
}
|
||||
|
||||
// Find longest axis of node AABB.
|
||||
float extent[3] = {
|
||||
node.aabb_max[0] - node.aabb_min[0],
|
||||
node.aabb_max[1] - node.aabb_min[1],
|
||||
@@ -82,145 +78,62 @@ void buildRecursive(ModelBvh& mbvh,
|
||||
if (extent[1] > extent[axis]) axis = 1;
|
||||
if (extent[2] > extent[axis]) axis = 2;
|
||||
|
||||
// Partition at median centroid on the chosen axis.
|
||||
uint32_t mid = count / 2;
|
||||
std::nth_element(
|
||||
mbvh.object_indices.begin() + start,
|
||||
mbvh.object_indices.begin() + start + mid,
|
||||
mbvh.object_indices.begin() + start + count,
|
||||
mbvh.item_indices.begin() + start,
|
||||
mbvh.item_indices.begin() + start + mid,
|
||||
mbvh.item_indices.begin() + start + count,
|
||||
[&](uint32_t a, uint32_t b) {
|
||||
Centroid ca = computeCentroid(draw_info[a]);
|
||||
Centroid cb = computeCentroid(draw_info[b]);
|
||||
Centroid ca = computeCentroid(items[a]);
|
||||
Centroid cb = computeCentroid(items[b]);
|
||||
return (&ca.x)[axis] < (&cb.x)[axis];
|
||||
});
|
||||
|
||||
node.count = 0; // interior
|
||||
node.count = 0;
|
||||
node.axis = static_cast<uint16_t>(axis);
|
||||
|
||||
// Left child is always node_idx + 1 (implicit in pre-order DFS).
|
||||
// Build left subtree first. Note: &node is invalidated after this call
|
||||
// because the vector may reallocate.
|
||||
buildRecursive(mbvh, draw_info, start, mid);
|
||||
buildRecursive(mbvh, items, start, mid);
|
||||
|
||||
// Right child is the next node written after the entire left subtree.
|
||||
uint32_t right_child_idx = static_cast<uint32_t>(mbvh.nodes.size());
|
||||
buildRecursive(mbvh, draw_info, start + mid, count - mid);
|
||||
buildRecursive(mbvh, items, start + mid, count - mid);
|
||||
|
||||
// Patch the right child index (left is implicit = node_idx + 1).
|
||||
mbvh.nodes[node_idx].right_or_first = right_child_idx;
|
||||
}
|
||||
|
||||
ModelBvh buildModelBvh(const std::vector<BvhItem>& items,
|
||||
const std::vector<uint32_t>& model_item_indices,
|
||||
uint32_t model_id) {
|
||||
ModelBvh mbvh;
|
||||
mbvh.model_id = model_id;
|
||||
mbvh.item_indices = model_item_indices;
|
||||
|
||||
uint32_t count = static_cast<uint32_t>(model_item_indices.size());
|
||||
if (count == 0) return mbvh;
|
||||
|
||||
mbvh.nodes.reserve(count * 2);
|
||||
buildRecursive(mbvh, items, 0, count);
|
||||
|
||||
assert(!mbvh.nodes.empty());
|
||||
return mbvh;
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
ModelBvh buildModelBvh(const std::vector<ObjectDrawInfo>& draw_info,
|
||||
const std::vector<uint32_t>& model_object_indices,
|
||||
uint32_t model_id) {
|
||||
ModelBvh mbvh;
|
||||
mbvh.model_id = model_id;
|
||||
mbvh.object_indices = model_object_indices;
|
||||
|
||||
uint32_t count = static_cast<uint32_t>(model_object_indices.size());
|
||||
if (count == 0) return mbvh;
|
||||
|
||||
// Reserve a rough estimate: ~2*n nodes for a balanced binary tree.
|
||||
mbvh.nodes.reserve(count * 2);
|
||||
|
||||
buildRecursive(mbvh, draw_info, 0, count);
|
||||
|
||||
// Verify: every object appears exactly once in the leaves.
|
||||
assert(!mbvh.nodes.empty());
|
||||
|
||||
return mbvh;
|
||||
}
|
||||
|
||||
std::shared_ptr<BvhSet> buildBvhSet(const std::vector<ObjectDrawInfo>& draw_info) {
|
||||
std::shared_ptr<BvhSet> buildBvhSet(const std::vector<BvhItem>& items) {
|
||||
auto bvh_set = std::make_shared<BvhSet>();
|
||||
|
||||
// Group object indices by model_id.
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> model_objects;
|
||||
for (uint32_t i = 0; i < static_cast<uint32_t>(draw_info.size()); ++i) {
|
||||
model_objects[draw_info[i].model_id].push_back(i);
|
||||
std::unordered_map<uint32_t, std::vector<uint32_t>> model_items;
|
||||
for (uint32_t i = 0; i < static_cast<uint32_t>(items.size()); ++i) {
|
||||
model_items[items[i].model_id].push_back(i);
|
||||
}
|
||||
|
||||
// Build per-model BVHs.
|
||||
for (auto& [model_id, obj_indices] : model_objects) {
|
||||
if (obj_indices.size() < BVH_MIN_OBJECTS) continue;
|
||||
for (auto& [model_id, idxs] : model_items) {
|
||||
if (idxs.size() < BVH_MIN_OBJECTS) continue;
|
||||
|
||||
ModelBvh mbvh = buildModelBvh(draw_info, obj_indices, model_id);
|
||||
ModelBvh mbvh = buildModelBvh(items, idxs, model_id);
|
||||
bvh_set->bvh_model_ids.insert(model_id);
|
||||
bvh_set->models[model_id] = std::move(mbvh);
|
||||
}
|
||||
|
||||
return bvh_set;
|
||||
}
|
||||
|
||||
EboReorderResult reorderEbo(const BvhSet& bvh_set,
|
||||
const std::vector<ObjectDrawInfo>& draw_info,
|
||||
const std::vector<uint32_t>& original_ebo) {
|
||||
EboReorderResult result;
|
||||
result.reordered_draw_info = draw_info; // copy; we'll update offsets
|
||||
result.reordered_ebo.reserve(original_ebo.size());
|
||||
|
||||
// Track which draw_info entries have been placed.
|
||||
std::vector<bool> placed(draw_info.size(), false);
|
||||
|
||||
for (const auto& [model_id, mbvh] : bvh_set.models) {
|
||||
// DFS traversal of BVH to visit leaves in order.
|
||||
uint32_t stack[64];
|
||||
int sp = 0;
|
||||
stack[sp++] = 0;
|
||||
|
||||
while (sp > 0) {
|
||||
uint32_t ni = stack[--sp];
|
||||
const BvhNode& node = mbvh.nodes[ni];
|
||||
|
||||
if (node.count > 0) {
|
||||
// Leaf: emit objects in order.
|
||||
for (uint32_t i = 0; i < node.count; ++i) {
|
||||
uint32_t oi = mbvh.object_indices[node.right_or_first + i];
|
||||
if (placed[oi]) continue;
|
||||
placed[oi] = true;
|
||||
|
||||
const auto& old_info = draw_info[oi];
|
||||
uint32_t new_offset = static_cast<uint32_t>(
|
||||
result.reordered_ebo.size() * sizeof(uint32_t));
|
||||
|
||||
// Copy indices from original EBO.
|
||||
uint32_t idx_start = old_info.index_offset / sizeof(uint32_t);
|
||||
uint32_t idx_count = old_info.index_count;
|
||||
for (uint32_t j = 0; j < idx_count; ++j) {
|
||||
result.reordered_ebo.push_back(original_ebo[idx_start + j]);
|
||||
}
|
||||
|
||||
result.reordered_draw_info[oi].index_offset = new_offset;
|
||||
}
|
||||
} else {
|
||||
// Interior: push left (=ni+1) last so it's processed first.
|
||||
stack[sp++] = node.right_or_first; // right child
|
||||
stack[sp++] = ni + 1; // left child
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Append non-BVH objects (models too small for BVH).
|
||||
for (uint32_t oi = 0; oi < static_cast<uint32_t>(draw_info.size()); ++oi) {
|
||||
if (placed[oi]) continue;
|
||||
placed[oi] = true;
|
||||
|
||||
const auto& old_info = draw_info[oi];
|
||||
uint32_t new_offset = static_cast<uint32_t>(
|
||||
result.reordered_ebo.size() * sizeof(uint32_t));
|
||||
|
||||
uint32_t idx_start = old_info.index_offset / sizeof(uint32_t);
|
||||
uint32_t idx_count = old_info.index_count;
|
||||
for (uint32_t j = 0; j < idx_count; ++j) {
|
||||
result.reordered_ebo.push_back(original_ebo[idx_start + j]);
|
||||
}
|
||||
|
||||
result.reordered_draw_info[oi].index_offset = new_offset;
|
||||
}
|
||||
|
||||
assert(result.reordered_ebo.size() == original_ebo.size());
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
+17
-26
@@ -26,22 +26,22 @@
|
||||
#include <unordered_set>
|
||||
#include <memory>
|
||||
|
||||
struct ObjectDrawInfo {
|
||||
uint32_t index_offset; // byte offset into EBO
|
||||
uint32_t index_count; // number of indices
|
||||
uint32_t model_id; // which model this object belongs to
|
||||
float aabb_min[3]; // world-space AABB
|
||||
float aabb_max[3];
|
||||
// Generic BVH item — anything with a world AABB and a model_id.
|
||||
// For the instanced renderer each item represents one InstanceCpu.
|
||||
struct BvhItem {
|
||||
float aabb_min[3];
|
||||
float aabb_max[3];
|
||||
uint32_t model_id;
|
||||
};
|
||||
|
||||
static constexpr uint32_t BVH_MAX_LEAF_SIZE = 8;
|
||||
static constexpr uint32_t BVH_MIN_OBJECTS = 32;
|
||||
static constexpr uint32_t BVH_MIN_OBJECTS = 32;
|
||||
|
||||
struct BvhNode {
|
||||
float aabb_min[3];
|
||||
float aabb_max[3];
|
||||
uint32_t right_or_first; // interior: right child index (left is always this_index+1); leaf: first object index
|
||||
uint16_t count; // 0 = interior; >0 = leaf with this many objects
|
||||
float aabb_min[3];
|
||||
float aabb_max[3];
|
||||
uint32_t right_or_first; // interior: right child index (left is always this_index+1); leaf: first item index
|
||||
uint16_t count; // 0 = interior; >0 = leaf with this many items
|
||||
uint16_t axis; // split axis (0/1/2) for interior; unused for leaf
|
||||
};
|
||||
static_assert(sizeof(BvhNode) == 32, "BvhNode must be 32 bytes for cache alignment and sidecar format");
|
||||
@@ -49,7 +49,7 @@ static_assert(sizeof(BvhNode) == 32, "BvhNode must be 32 bytes for cache alignme
|
||||
struct ModelBvh {
|
||||
uint32_t model_id = 0;
|
||||
std::vector<BvhNode> nodes;
|
||||
std::vector<uint32_t> object_indices; // indices into object_draw_info_
|
||||
std::vector<uint32_t> item_indices; // indices into the model's InstanceCpu array
|
||||
};
|
||||
|
||||
struct BvhSet {
|
||||
@@ -57,19 +57,10 @@ struct BvhSet {
|
||||
std::unordered_set<uint32_t> bvh_model_ids;
|
||||
};
|
||||
|
||||
struct EboReorderResult {
|
||||
std::vector<uint32_t> reordered_ebo;
|
||||
std::vector<ObjectDrawInfo> reordered_draw_info;
|
||||
};
|
||||
|
||||
// Build BVH trees for all models in the given draw info snapshot.
|
||||
// Only builds the tree structure; does not touch EBO data.
|
||||
std::shared_ptr<BvhSet> buildBvhSet(const std::vector<ObjectDrawInfo>& draw_info);
|
||||
|
||||
// Reorder the EBO so objects within each BVH leaf are contiguous.
|
||||
// Must be called with the CURRENT run's EBO and draw_info (not cached).
|
||||
EboReorderResult reorderEbo(const BvhSet& bvh_set,
|
||||
const std::vector<ObjectDrawInfo>& draw_info,
|
||||
const std::vector<uint32_t>& original_ebo);
|
||||
// Build BVH trees for all models in the given item snapshot.
|
||||
// Items are expected to already be grouped/filtered by caller if needed.
|
||||
// item_indices in the result reference positions within the full `items`
|
||||
// vector — callers providing a single model's items will see 0..N-1.
|
||||
std::shared_ptr<BvhSet> buildBvhSet(const std::vector<BvhItem>& items);
|
||||
|
||||
#endif // BVHACCEL_H
|
||||
|
||||
+228
-219
@@ -20,16 +20,52 @@
|
||||
#include "GeometryStreamer.h"
|
||||
#include "AppSettings.h"
|
||||
#include "../ifcgeom/hybrid_kernel.h"
|
||||
#include "../ifcgeom/taxonomy.h"
|
||||
|
||||
#include <Eigen/Dense>
|
||||
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
|
||||
#include <QDebug>
|
||||
#include <QElapsedTimer>
|
||||
|
||||
struct MaterialInfo {
|
||||
float r = 0.75f, g = 0.75f, b = 0.78f, a = 1.0f;
|
||||
};
|
||||
|
||||
static MaterialInfo materialFromStyle(const ifcopenshell::geometry::taxonomy::style::ptr& style) {
|
||||
MaterialInfo m;
|
||||
if (!style) return m;
|
||||
const auto& color = style->get_color();
|
||||
if (color) {
|
||||
m.r = static_cast<float>(color.r());
|
||||
m.g = static_cast<float>(color.g());
|
||||
m.b = static_cast<float>(color.b());
|
||||
}
|
||||
if (!std::isnan(style->transparency)) {
|
||||
m.a = 1.0f - static_cast<float>(style->transparency);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
static inline uint32_t packRGBA8(const MaterialInfo& m) {
|
||||
auto to_byte = [](float v) -> uint32_t {
|
||||
float c = std::clamp(v, 0.0f, 1.0f);
|
||||
return static_cast<uint32_t>(c * 255.0f + 0.5f);
|
||||
};
|
||||
uint32_t r = to_byte(m.r);
|
||||
uint32_t g = to_byte(m.g);
|
||||
uint32_t b = to_byte(m.b);
|
||||
uint32_t a = to_byte(m.a);
|
||||
// Little-endian byte layout [r,g,b,a] for GL_UNSIGNED_BYTE * 4 normalized.
|
||||
return r | (g << 8) | (b << 16) | (a << 24);
|
||||
}
|
||||
|
||||
GeometryStreamer::GeometryStreamer(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
@@ -96,6 +132,130 @@ std::vector<ElementInfo> GeometryStreamer::drainElements() {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Build a mesh chunk (local coords, 28-byte interleaved vertices) from a
|
||||
// TriangulationElement. Per-vertex color is baked from material_ids so that
|
||||
// triangulations with per-face materials still render correctly.
|
||||
static MeshChunk buildMeshChunk(uint32_t model_id,
|
||||
uint32_t local_mesh_id,
|
||||
const IfcGeom::TriangulationElement* elem) {
|
||||
MeshChunk chunk;
|
||||
chunk.model_id = model_id;
|
||||
chunk.local_mesh_id = local_mesh_id;
|
||||
|
||||
const auto& geom = elem->geometry();
|
||||
const auto& verts = geom.verts();
|
||||
const auto& faces = geom.faces();
|
||||
const auto& normals = geom.normals();
|
||||
const auto& materials = geom.materials();
|
||||
const auto& material_ids = geom.material_ids();
|
||||
|
||||
if (verts.empty() || faces.empty()) return chunk;
|
||||
|
||||
const size_t num_verts_src = verts.size() / 3;
|
||||
const size_t num_tris = faces.size() / 3;
|
||||
const bool have_per_tri_material = (material_ids.size() == num_tris);
|
||||
|
||||
// Dedupe (original vertex index, material id) so vertices shared across
|
||||
// triangles of the same material stay shared; vertices spanning multiple
|
||||
// materials are split (per-face color demands it).
|
||||
auto make_key = [](uint32_t orig_idx, int mat_id) -> uint64_t {
|
||||
return (static_cast<uint64_t>(orig_idx) << 32) | static_cast<uint32_t>(mat_id);
|
||||
};
|
||||
|
||||
std::unordered_map<uint64_t, uint32_t> remap;
|
||||
remap.reserve(num_verts_src);
|
||||
|
||||
chunk.vertices.reserve(num_verts_src * INSTANCED_VERTEX_STRIDE_FLOATS);
|
||||
chunk.indices.reserve(faces.size());
|
||||
|
||||
// Track local AABB as we emit vertices.
|
||||
float amin[3] = { std::numeric_limits<float>::max(),
|
||||
std::numeric_limits<float>::max(),
|
||||
std::numeric_limits<float>::max() };
|
||||
float amax[3] = { -std::numeric_limits<float>::max(),
|
||||
-std::numeric_limits<float>::max(),
|
||||
-std::numeric_limits<float>::max() };
|
||||
|
||||
auto emit_vertex = [&](uint32_t orig_idx, int mat_id) -> uint32_t {
|
||||
const uint64_t key = make_key(orig_idx, mat_id);
|
||||
auto it = remap.find(key);
|
||||
if (it != remap.end()) return it->second;
|
||||
|
||||
const uint32_t new_idx = static_cast<uint32_t>(
|
||||
chunk.vertices.size() / INSTANCED_VERTEX_STRIDE_FLOATS);
|
||||
|
||||
float px = static_cast<float>(verts[orig_idx * 3 + 0]);
|
||||
float py = static_cast<float>(verts[orig_idx * 3 + 1]);
|
||||
float pz = static_cast<float>(verts[orig_idx * 3 + 2]);
|
||||
chunk.vertices.push_back(px);
|
||||
chunk.vertices.push_back(py);
|
||||
chunk.vertices.push_back(pz);
|
||||
if (px < amin[0]) amin[0] = px; if (px > amax[0]) amax[0] = px;
|
||||
if (py < amin[1]) amin[1] = py; if (py > amax[1]) amax[1] = py;
|
||||
if (pz < amin[2]) amin[2] = pz; if (pz > amax[2]) amax[2] = pz;
|
||||
|
||||
if (orig_idx * 3 + 2 < normals.size()) {
|
||||
chunk.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 0]));
|
||||
chunk.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 1]));
|
||||
chunk.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 2]));
|
||||
} else {
|
||||
chunk.vertices.push_back(0.0f);
|
||||
chunk.vertices.push_back(1.0f);
|
||||
chunk.vertices.push_back(0.0f);
|
||||
}
|
||||
|
||||
MaterialInfo m;
|
||||
if (mat_id >= 0 && mat_id < static_cast<int>(materials.size())) {
|
||||
m = materialFromStyle(materials[mat_id]);
|
||||
}
|
||||
uint32_t packed = packRGBA8(m);
|
||||
float packed_as_float;
|
||||
std::memcpy(&packed_as_float, &packed, sizeof(float));
|
||||
chunk.vertices.push_back(packed_as_float);
|
||||
|
||||
remap.emplace(key, new_idx);
|
||||
return new_idx;
|
||||
};
|
||||
|
||||
for (size_t t = 0; t < num_tris; ++t) {
|
||||
const int mat_id = have_per_tri_material ? material_ids[t] : -1;
|
||||
chunk.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 0]), mat_id));
|
||||
chunk.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 1]), mat_id));
|
||||
chunk.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 2]), mat_id));
|
||||
}
|
||||
|
||||
if (chunk.vertices.empty()) {
|
||||
for (int a = 0; a < 3; ++a) amin[a] = amax[a] = 0.0f;
|
||||
}
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
chunk.local_aabb_min[a] = amin[a];
|
||||
chunk.local_aabb_max[a] = amax[a];
|
||||
}
|
||||
return chunk;
|
||||
}
|
||||
|
||||
// Compute the world-space AABB by transforming the 8 corners of the local
|
||||
// AABB through the column-major 4x4 transform.
|
||||
static void worldAabbFromLocal(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<float>::max();
|
||||
out_max[0] = out_max[1] = out_max[2] = -std::numeric_limits<float>::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];
|
||||
// Column-major: world = M * [x,y,z,1].
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
try {
|
||||
ifc_file_ = std::make_unique<ifcopenshell::file>(path);
|
||||
@@ -105,7 +265,9 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
}
|
||||
|
||||
ifcopenshell::geometry::Settings settings;
|
||||
settings.set("use-world-coords", true);
|
||||
// Instancing path: geometry stays in local coords; the transform is
|
||||
// applied on the GPU per instance.
|
||||
settings.set("use-world-coords", false);
|
||||
settings.set("weld-vertices", false);
|
||||
settings.set("apply-default-materials", true);
|
||||
|
||||
@@ -129,17 +291,14 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
|
||||
int last_progress = 0;
|
||||
|
||||
// Instancing analysis: count shapes grouped by representation id.
|
||||
struct GeomStat {
|
||||
uint32_t count = 0;
|
||||
size_t vertex_count = 0;
|
||||
size_t index_count = 0;
|
||||
std::string example_type;
|
||||
};
|
||||
std::unordered_map<std::string, GeomStat> geom_stats;
|
||||
// geom.id() → local_mesh_id within this model.
|
||||
std::unordered_map<std::string, uint32_t> geom_to_local_mesh_id;
|
||||
// local_mesh_id → (local AABB) so we can derive world AABBs for later instances.
|
||||
struct MeshAabb { float lmin[3], lmax[3]; };
|
||||
std::vector<MeshAabb> mesh_aabbs;
|
||||
|
||||
uint32_t total_shapes = 0;
|
||||
size_t total_vertices = 0;
|
||||
size_t total_indices = 0;
|
||||
uint32_t total_meshes = 0;
|
||||
QElapsedTimer stream_timer;
|
||||
stream_timer.start();
|
||||
|
||||
@@ -152,9 +311,12 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
const auto* tri_elem = dynamic_cast<const IfcGeom::TriangulationElement*>(elem);
|
||||
if (!tri_elem) continue;
|
||||
|
||||
const auto& geom = tri_elem->geometry();
|
||||
if (geom.verts().empty() || geom.faces().empty()) continue;
|
||||
|
||||
uint32_t object_id = next_object_id_++;
|
||||
|
||||
// Record element metadata
|
||||
// Element metadata.
|
||||
ElementInfo info;
|
||||
info.object_id = object_id;
|
||||
info.model_id = model_id_;
|
||||
@@ -163,36 +325,62 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
info.name = tri_elem->name();
|
||||
info.type = tri_elem->type();
|
||||
info.parent_id = tri_elem->parent_id();
|
||||
|
||||
// Instancing stats: key by representation id, count unique vs repeated.
|
||||
const auto& geom = tri_elem->geometry();
|
||||
const std::string& geom_id = geom.id();
|
||||
size_t nv = geom.verts().size() / 3;
|
||||
size_t ni = geom.faces().size();
|
||||
if (!geom_id.empty()) {
|
||||
auto& gs = geom_stats[geom_id];
|
||||
gs.count++;
|
||||
if (gs.count == 1) {
|
||||
gs.vertex_count = nv;
|
||||
gs.index_count = ni;
|
||||
gs.example_type = info.type;
|
||||
}
|
||||
}
|
||||
total_shapes++;
|
||||
total_vertices += nv;
|
||||
total_indices += ni;
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(elements_mutex_);
|
||||
pending_elements_.push_back(std::move(info));
|
||||
}
|
||||
|
||||
// Convert geometry to upload chunk
|
||||
UploadChunk chunk = convertElement(tri_elem, object_id);
|
||||
if (!chunk.indices.empty()) {
|
||||
emit elementReady(std::move(chunk));
|
||||
// Representation dedup.
|
||||
const std::string& geom_id = geom.id();
|
||||
uint32_t local_mesh_id;
|
||||
bool first_sight = false;
|
||||
if (geom_id.empty()) {
|
||||
// No representation key — treat as unique.
|
||||
local_mesh_id = total_meshes++;
|
||||
first_sight = true;
|
||||
} else {
|
||||
auto it = geom_to_local_mesh_id.find(geom_id);
|
||||
if (it == geom_to_local_mesh_id.end()) {
|
||||
local_mesh_id = total_meshes++;
|
||||
geom_to_local_mesh_id.emplace(geom_id, local_mesh_id);
|
||||
first_sight = true;
|
||||
} else {
|
||||
local_mesh_id = it->second;
|
||||
}
|
||||
}
|
||||
|
||||
if (first_sight) {
|
||||
MeshChunk mesh_chunk = buildMeshChunk(model_id_, local_mesh_id, tri_elem);
|
||||
MeshAabb ma;
|
||||
for (int a = 0; a < 3; ++a) {
|
||||
ma.lmin[a] = mesh_chunk.local_aabb_min[a];
|
||||
ma.lmax[a] = mesh_chunk.local_aabb_max[a];
|
||||
}
|
||||
if (mesh_aabbs.size() <= local_mesh_id) mesh_aabbs.resize(local_mesh_id + 1);
|
||||
mesh_aabbs[local_mesh_id] = ma;
|
||||
if (!mesh_chunk.indices.empty()) {
|
||||
emit meshReady(std::move(mesh_chunk));
|
||||
}
|
||||
}
|
||||
|
||||
// Transform (column-major 4x4, cast to float).
|
||||
const Eigen::Matrix4d& mat_d = tri_elem->transformation().data()->ccomponents();
|
||||
InstanceChunk inst;
|
||||
inst.model_id = model_id_;
|
||||
inst.local_mesh_id = local_mesh_id;
|
||||
inst.object_id = object_id;
|
||||
inst.color_override_rgba8 = 0; // 0 = use baked vertex color
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
inst.transform[i] = static_cast<float>(mat_d.data()[i]);
|
||||
}
|
||||
|
||||
const MeshAabb& ma = mesh_aabbs[local_mesh_id];
|
||||
worldAabbFromLocal(ma.lmin, ma.lmax, inst.transform,
|
||||
inst.world_aabb_min, inst.world_aabb_max);
|
||||
|
||||
emit instanceReady(std::move(inst));
|
||||
total_shapes++;
|
||||
|
||||
int p = iterator->progress();
|
||||
if (p != last_progress) {
|
||||
last_progress = p;
|
||||
@@ -204,188 +392,9 @@ void GeometryStreamer::run(const std::string& path, int num_threads) {
|
||||
progress_ = 100;
|
||||
emit progressChanged(100);
|
||||
|
||||
// === Instancing report ===
|
||||
{
|
||||
size_t unique_geoms = geom_stats.size();
|
||||
size_t unique_vertices = 0;
|
||||
size_t unique_indices = 0;
|
||||
size_t repeated_shapes = 0; // total shapes that share a repr with another
|
||||
for (const auto& [gid, gs] : geom_stats) {
|
||||
unique_vertices += gs.vertex_count;
|
||||
unique_indices += gs.index_count;
|
||||
if (gs.count > 1) repeated_shapes += gs.count;
|
||||
}
|
||||
|
||||
// Bytes assuming current layout (32 B/vertex, 4 B/index).
|
||||
size_t baked_vbo_bytes = total_vertices * 32;
|
||||
size_t baked_ebo_bytes = total_indices * 4;
|
||||
size_t instanced_vbo_bytes = unique_vertices * 32;
|
||||
size_t instanced_ebo_bytes = unique_indices * 4;
|
||||
// Per-instance data: 64 B transform + 8 B (object_id + color).
|
||||
size_t per_instance_bytes = 72;
|
||||
size_t instance_ssbo_bytes = total_shapes * per_instance_bytes;
|
||||
|
||||
double dedup_ratio = unique_geoms > 0
|
||||
? static_cast<double>(total_shapes) / static_cast<double>(unique_geoms)
|
||||
: 1.0;
|
||||
|
||||
qDebug("=== Instancing analysis: %s ===", path.c_str());
|
||||
qDebug(" Stream time: %.2f s", stream_timer.elapsed() / 1000.0);
|
||||
qDebug(" Total shapes: %u", total_shapes);
|
||||
qDebug(" Unique geometries: %zu (dedup ratio %.2fx)",
|
||||
unique_geoms, dedup_ratio);
|
||||
qDebug(" Repeated shapes: %zu (%.1f%% of total)",
|
||||
repeated_shapes,
|
||||
total_shapes > 0 ? 100.0 * repeated_shapes / total_shapes : 0.0);
|
||||
qDebug(" Baked geometry: VBO %.1f MB + EBO %.1f MB = %.1f MB",
|
||||
baked_vbo_bytes / (1024.0*1024.0),
|
||||
baked_ebo_bytes / (1024.0*1024.0),
|
||||
(baked_vbo_bytes + baked_ebo_bytes) / (1024.0*1024.0));
|
||||
qDebug(" If instanced: VBO %.1f MB + EBO %.1f MB + SSBO %.1f MB = %.1f MB",
|
||||
instanced_vbo_bytes / (1024.0*1024.0),
|
||||
instanced_ebo_bytes / (1024.0*1024.0),
|
||||
instance_ssbo_bytes / (1024.0*1024.0),
|
||||
(instanced_vbo_bytes + instanced_ebo_bytes + instance_ssbo_bytes)
|
||||
/ (1024.0*1024.0));
|
||||
size_t baked_total = baked_vbo_bytes + baked_ebo_bytes;
|
||||
size_t inst_total = instanced_vbo_bytes + instanced_ebo_bytes + instance_ssbo_bytes;
|
||||
if (inst_total > 0 && baked_total > inst_total) {
|
||||
qDebug(" Potential savings: %.1f MB (%.1f%%)",
|
||||
(baked_total - inst_total) / (1024.0*1024.0),
|
||||
100.0 * (baked_total - inst_total) / baked_total);
|
||||
} else {
|
||||
qDebug(" Potential savings: none (instance overhead exceeds dedup win)");
|
||||
}
|
||||
|
||||
// Top-5 most duplicated representations.
|
||||
std::vector<std::pair<std::string, GeomStat>> sorted(geom_stats.begin(), geom_stats.end());
|
||||
std::partial_sort(sorted.begin(),
|
||||
sorted.begin() + std::min<size_t>(5, sorted.size()),
|
||||
sorted.end(),
|
||||
[](const auto& a, const auto& b) { return a.second.count > b.second.count; });
|
||||
qDebug(" Top duplicated representations:");
|
||||
for (size_t i = 0; i < std::min<size_t>(5, sorted.size()); ++i) {
|
||||
const auto& [gid, gs] = sorted[i];
|
||||
qDebug(" [%zu] count=%u verts=%zu type=%s repr_id=%s",
|
||||
i + 1, gs.count, gs.vertex_count,
|
||||
gs.example_type.c_str(), gid.c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static MaterialInfo materialFromStyle(const ifcopenshell::geometry::taxonomy::style::ptr& style) {
|
||||
MaterialInfo m;
|
||||
if (!style) return m;
|
||||
|
||||
const auto& color = style->get_color();
|
||||
if (color) {
|
||||
m.r = static_cast<float>(color.r());
|
||||
m.g = static_cast<float>(color.g());
|
||||
m.b = static_cast<float>(color.b());
|
||||
}
|
||||
if (!std::isnan(style->transparency)) {
|
||||
m.a = 1.0f - static_cast<float>(style->transparency);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
static inline uint32_t packRGBA8(const MaterialInfo& m) {
|
||||
auto to_byte = [](float v) -> uint32_t {
|
||||
float c = std::clamp(v, 0.0f, 1.0f);
|
||||
return static_cast<uint32_t>(c * 255.0f + 0.5f);
|
||||
};
|
||||
uint32_t r = to_byte(m.r);
|
||||
uint32_t g = to_byte(m.g);
|
||||
uint32_t b = to_byte(m.b);
|
||||
uint32_t a = to_byte(m.a);
|
||||
// Layout in memory (little-endian) reads as bytes [r, g, b, a] which is
|
||||
// what the GL_UNSIGNED_BYTE * 4 normalized vertex attribute expects.
|
||||
return r | (g << 8) | (b << 16) | (a << 24);
|
||||
}
|
||||
|
||||
UploadChunk GeometryStreamer::convertElement(const IfcGeom::TriangulationElement* elem, uint32_t object_id) {
|
||||
UploadChunk chunk;
|
||||
chunk.object_id = object_id;
|
||||
chunk.model_id = model_id_;
|
||||
|
||||
const auto& geom = elem->geometry();
|
||||
const auto& verts = geom.verts();
|
||||
const auto& faces = geom.faces();
|
||||
const auto& normals = geom.normals();
|
||||
const auto& materials = geom.materials();
|
||||
const auto& material_ids = geom.material_ids();
|
||||
|
||||
if (verts.empty() || faces.empty()) return chunk;
|
||||
|
||||
// Encode object_id as float bits for the vertex attribute
|
||||
float id_as_float;
|
||||
static_assert(sizeof(float) == sizeof(uint32_t));
|
||||
std::memcpy(&id_as_float, &object_id, sizeof(float));
|
||||
|
||||
const size_t num_verts = verts.size() / 3;
|
||||
const size_t num_tris = faces.size() / 3;
|
||||
const bool have_per_tri_material = (material_ids.size() == num_tris);
|
||||
|
||||
// Per-vertex color requires that any vertex shared between triangles with
|
||||
// *different* materials be split. We dedupe (orig_vert_idx, mat_id) pairs
|
||||
// so vertices that are only ever used by one material stay shared.
|
||||
auto make_key = [](uint32_t orig_idx, int mat_id) -> uint64_t {
|
||||
return (static_cast<uint64_t>(orig_idx) << 32) |
|
||||
static_cast<uint32_t>(mat_id);
|
||||
};
|
||||
|
||||
std::unordered_map<uint64_t, uint32_t> remap;
|
||||
remap.reserve(num_verts);
|
||||
|
||||
chunk.vertices.reserve(num_verts * 8);
|
||||
chunk.indices.reserve(faces.size());
|
||||
|
||||
auto emit_vertex = [&](uint32_t orig_idx, int mat_id) -> uint32_t {
|
||||
const uint64_t key = make_key(orig_idx, mat_id);
|
||||
auto it = remap.find(key);
|
||||
if (it != remap.end()) return it->second;
|
||||
|
||||
const uint32_t new_idx = static_cast<uint32_t>(chunk.vertices.size() / 8);
|
||||
|
||||
// pos
|
||||
chunk.vertices.push_back(static_cast<float>(verts[orig_idx * 3 + 0]));
|
||||
chunk.vertices.push_back(static_cast<float>(verts[orig_idx * 3 + 1]));
|
||||
chunk.vertices.push_back(static_cast<float>(verts[orig_idx * 3 + 2]));
|
||||
|
||||
// normal
|
||||
if (orig_idx * 3 + 2 < normals.size()) {
|
||||
chunk.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 0]));
|
||||
chunk.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 1]));
|
||||
chunk.vertices.push_back(static_cast<float>(normals[orig_idx * 3 + 2]));
|
||||
} else {
|
||||
chunk.vertices.push_back(0.0f);
|
||||
chunk.vertices.push_back(1.0f);
|
||||
chunk.vertices.push_back(0.0f);
|
||||
}
|
||||
|
||||
// object_id (float bits)
|
||||
chunk.vertices.push_back(id_as_float);
|
||||
|
||||
// color (packed RGBA8 reinterpreted as float)
|
||||
MaterialInfo m;
|
||||
if (mat_id >= 0 && mat_id < static_cast<int>(materials.size())) {
|
||||
m = materialFromStyle(materials[mat_id]);
|
||||
}
|
||||
uint32_t packed = packRGBA8(m);
|
||||
float packed_as_float;
|
||||
std::memcpy(&packed_as_float, &packed, sizeof(float));
|
||||
chunk.vertices.push_back(packed_as_float);
|
||||
|
||||
remap.emplace(key, new_idx);
|
||||
return new_idx;
|
||||
};
|
||||
|
||||
for (size_t t = 0; t < num_tris; ++t) {
|
||||
const int mat_id = have_per_tri_material ? material_ids[t] : -1;
|
||||
chunk.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 0]), mat_id));
|
||||
chunk.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 1]), mat_id));
|
||||
chunk.indices.push_back(emit_vertex(static_cast<uint32_t>(faces[t * 3 + 2]), mat_id));
|
||||
}
|
||||
|
||||
return chunk;
|
||||
double dedup_ratio = total_meshes > 0
|
||||
? static_cast<double>(total_shapes) / static_cast<double>(total_meshes) : 1.0;
|
||||
qDebug("Streamer done: %s %.2fs shapes=%u unique_meshes=%u dedup=%.2fx",
|
||||
path.c_str(), stream_timer.elapsed() / 1000.0,
|
||||
total_shapes, total_meshes, dedup_ratio);
|
||||
}
|
||||
|
||||
@@ -26,15 +26,13 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <deque>
|
||||
|
||||
#include "../ifcparse/file.h"
|
||||
#include "../ifcgeom/Iterator.h"
|
||||
|
||||
#include "ViewportWindow.h"
|
||||
#include "InstancedGeometry.h"
|
||||
|
||||
struct ElementInfo {
|
||||
uint32_t object_id;
|
||||
@@ -67,15 +65,14 @@ public:
|
||||
|
||||
signals:
|
||||
void progressChanged(int percent);
|
||||
void elementReady(UploadChunk chunk);
|
||||
void meshReady(MeshChunk chunk);
|
||||
void instanceReady(InstanceChunk chunk);
|
||||
void finished();
|
||||
void errorOccurred(const QString& message);
|
||||
|
||||
private:
|
||||
void run(const std::string& path, int num_threads);
|
||||
|
||||
UploadChunk convertElement(const IfcGeom::TriangulationElement* elem, uint32_t object_id);
|
||||
|
||||
std::unique_ptr<ifcopenshell::file> ifc_file_;
|
||||
std::unique_ptr<QThread> worker_thread_;
|
||||
std::atomic<bool> running_{false};
|
||||
@@ -85,7 +82,7 @@ private:
|
||||
std::mutex elements_mutex_;
|
||||
std::vector<ElementInfo> pending_elements_;
|
||||
|
||||
uint32_t next_object_id_ = 1; // 0 = no object
|
||||
uint32_t next_object_id_ = 1;
|
||||
uint32_t model_id_ = 0;
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
/********************************************************************************
|
||||
* *
|
||||
* 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 INSTANCEDGEOMETRY_H
|
||||
#define INSTANCEDGEOMETRY_H
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// Per-vertex layout for instanced meshes, stored in local coordinates.
|
||||
// 28 bytes per vertex:
|
||||
// pos(3 float) -- 12 B
|
||||
// normal(3 float) -- 12 B
|
||||
// color(4 bytes RGBA8, read as GL_UNSIGNED_BYTE*4 normalized) -- 4 B
|
||||
static constexpr int INSTANCED_VERTEX_STRIDE_BYTES = 28;
|
||||
static constexpr int INSTANCED_VERTEX_STRIDE_FLOATS = 7;
|
||||
|
||||
// Per-mesh metadata on the CPU side. Meshes own a slice of the model's
|
||||
// VBO and EBO (both local-coords/mesh-local indices).
|
||||
struct MeshInfo {
|
||||
uint32_t vbo_byte_offset = 0; // where this mesh's vertices start
|
||||
uint32_t vertex_count = 0;
|
||||
uint32_t ebo_byte_offset = 0; // where this mesh's indices start
|
||||
uint32_t index_count = 0;
|
||||
float local_aabb_min[3]{};
|
||||
float local_aabb_max[3]{};
|
||||
uint32_t first_instance = 0; // index into per-model instances array
|
||||
uint32_t instance_count = 0;
|
||||
};
|
||||
static_assert(sizeof(MeshInfo) == 48, "MeshInfo must be 48 bytes");
|
||||
|
||||
// Per-instance record uploaded to an SSBO and read by the vertex shader.
|
||||
// Layout deliberately matches std430 expectations:
|
||||
// mat4 transform (64 B column-major)
|
||||
// uint object_id
|
||||
// uint color_override_rgba8 -- 0 = use baked vertex color, else override
|
||||
// uint _pad0, _pad1 -- align to 16 for std430
|
||||
struct alignas(16) InstanceGpu {
|
||||
float transform[16];
|
||||
uint32_t object_id = 0;
|
||||
uint32_t color_override_rgba8 = 0;
|
||||
uint32_t _pad0 = 0;
|
||||
uint32_t _pad1 = 0;
|
||||
};
|
||||
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.
|
||||
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 transform[16]{};
|
||||
float world_aabb_min[3]{};
|
||||
float world_aabb_max[3]{};
|
||||
};
|
||||
|
||||
// Chunks emitted by the streamer to the viewport (main thread).
|
||||
|
||||
// Emitted the first time a representation id is seen. Carries the mesh
|
||||
// geometry in local coords. `local_mesh_id` is the streamer-assigned id
|
||||
// within this model.
|
||||
struct MeshChunk {
|
||||
uint32_t model_id = 0;
|
||||
uint32_t local_mesh_id = 0;
|
||||
std::vector<float> vertices; // 7 floats * N_verts (pos3+norm3+color1_packed)
|
||||
std::vector<uint32_t> indices;
|
||||
float local_aabb_min[3]{};
|
||||
float local_aabb_max[3]{};
|
||||
};
|
||||
|
||||
// Emitted for every placement (every triangulation element from the
|
||||
// iterator). For the first instance of a mesh, the MeshChunk is emitted
|
||||
// just before this.
|
||||
struct InstanceChunk {
|
||||
uint32_t model_id = 0;
|
||||
uint32_t local_mesh_id = 0;
|
||||
uint32_t object_id = 0;
|
||||
uint32_t color_override_rgba8 = 0;
|
||||
float transform[16]{};
|
||||
float world_aabb_min[3]{};
|
||||
float world_aabb_max[3]{};
|
||||
};
|
||||
|
||||
#endif // INSTANCEDGEOMETRY_H
|
||||
@@ -173,8 +173,10 @@ void MainWindow::addFiles(const QStringList& paths) {
|
||||
void MainWindow::connectStreamer(GeometryStreamer* streamer) {
|
||||
connect(streamer, &GeometryStreamer::progressChanged,
|
||||
this, &MainWindow::onProgressChanged, Qt::QueuedConnection);
|
||||
connect(streamer, &GeometryStreamer::elementReady,
|
||||
this, &MainWindow::onElementReady, Qt::QueuedConnection);
|
||||
connect(streamer, &GeometryStreamer::meshReady,
|
||||
this, &MainWindow::onMeshReady, Qt::QueuedConnection);
|
||||
connect(streamer, &GeometryStreamer::instanceReady,
|
||||
this, &MainWindow::onInstanceReady, Qt::QueuedConnection);
|
||||
connect(streamer, &GeometryStreamer::finished,
|
||||
this, &MainWindow::onStreamingFinished, Qt::QueuedConnection);
|
||||
connect(streamer, &GeometryStreamer::errorOccurred, this, [this](const QString& msg) {
|
||||
@@ -208,7 +210,7 @@ void MainWindow::startNextLoad() {
|
||||
qDebug(" Sidecar read: %lld ms (%s)", rt.elapsed(), ifc_path.c_str());
|
||||
auto result = std::make_shared<std::optional<SidecarData>>(std::move(cached));
|
||||
QMetaObject::invokeMethod(this, [this, mid, result]() {
|
||||
if (*result && !(*result)->draw_info.empty()) {
|
||||
if (*result && !(*result)->meshes.empty()) {
|
||||
applySidecarData(mid, std::move(**result));
|
||||
} else {
|
||||
// No sidecar — fall back to streaming from IFC.
|
||||
@@ -227,51 +229,10 @@ void MainWindow::startNextLoad() {
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::applySidecarData(ModelId mid, SidecarData data) {
|
||||
auto it = models_.find(mid);
|
||||
if (it == models_.end()) return;
|
||||
auto& model = it->second;
|
||||
|
||||
QElapsedTimer t;
|
||||
|
||||
qDebug("Sidecar hit: %s (%zu objects, %zu verts, %zu indices, %.1f MB)",
|
||||
model.file_path.toStdString().c_str(), data.draw_info.size(),
|
||||
data.vertices.size() / 8, data.indices.size(),
|
||||
(data.vertices.size() * 4 + data.indices.size() * 4) / (1024.0 * 1024.0));
|
||||
|
||||
// GL upload — fast, single buffer copy.
|
||||
t.start();
|
||||
viewport_->uploadBulk(mid, data.vertices, data.indices,
|
||||
data.draw_info, std::move(data.bvh_set));
|
||||
qDebug(" GL upload: %lld ms", t.elapsed());
|
||||
|
||||
// Update next_object_id_ past all objects in this model.
|
||||
for (const auto& elem : data.elements) {
|
||||
if (elem.object_id >= next_object_id_)
|
||||
next_object_id_ = elem.object_id + 1;
|
||||
}
|
||||
|
||||
// Suppress per-item layout recalcs while building the tree.
|
||||
t.restart();
|
||||
element_tree_->setUpdatesEnabled(false);
|
||||
populateTreeFromSidecar(model, data.elements, data.string_table);
|
||||
element_tree_->setUpdatesEnabled(true);
|
||||
qDebug(" Tree build: %lld ms (%zu elements)", t.elapsed(), data.elements.size());
|
||||
|
||||
progress_bar_->setVisible(false);
|
||||
|
||||
qint64 ms = load_timer_.elapsed();
|
||||
QString elapsed = (ms >= 1000)
|
||||
? QString::number(ms / 1000.0, 'f', 2) + " s"
|
||||
: QString::number(ms) + " ms";
|
||||
|
||||
status_label_->setText(QString("%1 elements across %2 model(s) — loaded from cache in %3")
|
||||
.arg(element_map_.size())
|
||||
.arg(models_.size())
|
||||
.arg(elapsed));
|
||||
|
||||
loading_model_id_ = 0;
|
||||
QTimer::singleShot(0, this, &MainWindow::startNextLoad);
|
||||
void MainWindow::applySidecarData(ModelId /*mid*/, SidecarData /*data*/) {
|
||||
// Commit A: readSidecar() always returns nullopt, so this is unreachable.
|
||||
// Restored in Commit B along with the v4 on-disk format.
|
||||
qWarning("applySidecarData called but sidecar is disabled in Commit A");
|
||||
}
|
||||
|
||||
void MainWindow::populateTreeFromSidecar(ModelHandle& model,
|
||||
@@ -325,8 +286,12 @@ void MainWindow::onProgressChanged(int percent) {
|
||||
progress_bar_->setValue(percent);
|
||||
}
|
||||
|
||||
void MainWindow::onElementReady(UploadChunk chunk) {
|
||||
viewport_->uploadChunk(chunk);
|
||||
void MainWindow::onMeshReady(MeshChunk chunk) {
|
||||
viewport_->uploadMeshChunk(chunk);
|
||||
}
|
||||
|
||||
void MainWindow::onInstanceReady(InstanceChunk chunk) {
|
||||
viewport_->uploadInstanceChunk(chunk);
|
||||
}
|
||||
|
||||
void MainWindow::onStreamingFinished() {
|
||||
@@ -355,39 +320,10 @@ void MainWindow::onStreamingFinished() {
|
||||
.arg(num_models)
|
||||
.arg(elapsed));
|
||||
|
||||
// Build BVH and write sidecar (geometry + metadata + BVH).
|
||||
// Sort instances by mesh and upload the per-model instance SSBO.
|
||||
// Sidecar write is stubbed in Commit A.
|
||||
if (loading_model_id_ != 0) {
|
||||
auto it = models_.find(loading_model_id_);
|
||||
if (it != models_.end()) {
|
||||
std::string ifc_path = it->second.file_path.toStdString();
|
||||
QFileInfo fi(it->second.file_path);
|
||||
uint64_t file_size = static_cast<uint64_t>(fi.size());
|
||||
|
||||
// Pack element info for the sidecar (only this model's elements).
|
||||
std::vector<PackedElementInfo> packed;
|
||||
std::string stbl;
|
||||
for (const auto& [oid, info] : element_map_) {
|
||||
if (info.model_id != loading_model_id_) continue;
|
||||
PackedElementInfo pe;
|
||||
pe.object_id = info.object_id;
|
||||
pe.model_id = info.model_id;
|
||||
pe.ifc_id = info.ifc_id;
|
||||
pe.parent_id = info.parent_id;
|
||||
pe.guid_offset = static_cast<uint32_t>(stbl.size());
|
||||
pe.guid_length = static_cast<uint32_t>(info.guid.size());
|
||||
stbl += info.guid;
|
||||
pe.name_offset = static_cast<uint32_t>(stbl.size());
|
||||
pe.name_length = static_cast<uint32_t>(info.name.size());
|
||||
stbl += info.name;
|
||||
pe.type_offset = static_cast<uint32_t>(stbl.size());
|
||||
pe.type_length = static_cast<uint32_t>(info.type.size());
|
||||
stbl += info.type;
|
||||
packed.push_back(pe);
|
||||
}
|
||||
|
||||
viewport_->buildBvhAsync(loading_model_id_, ifc_path, file_size,
|
||||
std::move(packed), std::move(stbl));
|
||||
}
|
||||
viewport_->finalizeModel(loading_model_id_);
|
||||
}
|
||||
|
||||
// Start next model if queued.
|
||||
|
||||
@@ -62,7 +62,8 @@ private slots:
|
||||
void onFileOpen();
|
||||
void onFileSettings();
|
||||
void onProgressChanged(int percent);
|
||||
void onElementReady(UploadChunk chunk);
|
||||
void onMeshReady(MeshChunk chunk);
|
||||
void onInstanceReady(InstanceChunk chunk);
|
||||
void onStreamingFinished();
|
||||
void onObjectPicked(uint32_t object_id);
|
||||
void onTreeSelectionChanged();
|
||||
|
||||
+11
-171
@@ -17,180 +17,20 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
// Commit A: sidecar cache is temporarily disabled. The on-disk format is
|
||||
// being rewritten from v3 (monolithic world-coord geometry) to v4 (instanced
|
||||
// meshes + per-instance records). Until v4 is finalised, loads always go
|
||||
// through the streaming path and writes are no-ops.
|
||||
|
||||
#include "SidecarCache.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
// Binary layout (all multi-byte fields native-endian):
|
||||
//
|
||||
// SidecarHeader (16 bytes)
|
||||
// uint64_t source_file_size
|
||||
//
|
||||
// uint32_t num_vertices (count of floats)
|
||||
// float[num_vertices] vertex data
|
||||
//
|
||||
// uint32_t num_indices
|
||||
// uint32_t[num_indices] index data
|
||||
//
|
||||
// uint32_t num_draw_infos
|
||||
// ObjectDrawInfo[N] draw info array
|
||||
//
|
||||
// uint32_t num_elements
|
||||
// PackedElementInfo[N] element records
|
||||
// uint32_t string_table_bytes
|
||||
// char[string_table_bytes]
|
||||
//
|
||||
// uint32_t num_bvh_models
|
||||
// for each model:
|
||||
// uint32_t model_id
|
||||
// uint32_t num_nodes
|
||||
// BvhNode[num_nodes]
|
||||
// uint32_t num_object_indices
|
||||
// uint32_t[num_object_indices]
|
||||
|
||||
struct SidecarHeader {
|
||||
uint32_t magic;
|
||||
uint32_t version;
|
||||
uint32_t endian;
|
||||
uint32_t reserved;
|
||||
};
|
||||
|
||||
static std::string sidecarPath(const std::string& ifc_path) {
|
||||
return ifc_path + ".ifcview";
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static bool writeVec(FILE* f, const std::vector<T>& v) {
|
||||
uint32_t n = static_cast<uint32_t>(v.size());
|
||||
if (fwrite(&n, 4, 1, f) != 1) return false;
|
||||
if (n > 0 && fwrite(v.data(), sizeof(T), n, f) != n) return false;
|
||||
bool writeSidecar(const std::string& /*ifc_path*/,
|
||||
const SidecarData& /*data*/,
|
||||
uint64_t /*ifc_file_size*/) {
|
||||
return true;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
static bool readVec(FILE* f, std::vector<T>& v) {
|
||||
uint32_t n;
|
||||
if (fread(&n, 4, 1, f) != 1) return false;
|
||||
v.resize(n);
|
||||
if (n > 0 && fread(v.data(), sizeof(T), n, f) != n) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool writeSidecar(const std::string& ifc_path,
|
||||
const SidecarData& data,
|
||||
uint64_t ifc_file_size) {
|
||||
std::string path = sidecarPath(ifc_path);
|
||||
FILE* f = fopen(path.c_str(), "wb");
|
||||
if (!f) return false;
|
||||
|
||||
// Header
|
||||
SidecarHeader hdr = { SIDECAR_MAGIC, SIDECAR_VERSION, SIDECAR_ENDIAN, 0 };
|
||||
fwrite(&hdr, sizeof(hdr), 1, f);
|
||||
fwrite(&ifc_file_size, 8, 1, f);
|
||||
|
||||
// Geometry
|
||||
if (!writeVec(f, data.vertices)) { fclose(f); return false; }
|
||||
if (!writeVec(f, data.indices)) { fclose(f); return false; }
|
||||
|
||||
// Draw info
|
||||
if (!writeVec(f, data.draw_info)) { fclose(f); return false; }
|
||||
|
||||
// Elements + string table
|
||||
if (!writeVec(f, data.elements)) { fclose(f); return false; }
|
||||
uint32_t stbl_len = static_cast<uint32_t>(data.string_table.size());
|
||||
fwrite(&stbl_len, 4, 1, f);
|
||||
if (stbl_len > 0) fwrite(data.string_table.data(), 1, stbl_len, f);
|
||||
|
||||
// BVH
|
||||
uint32_t num_bvh_models = data.bvh_set
|
||||
? static_cast<uint32_t>(data.bvh_set->models.size()) : 0;
|
||||
fwrite(&num_bvh_models, 4, 1, f);
|
||||
|
||||
if (data.bvh_set) {
|
||||
for (const auto& [model_id, mbvh] : data.bvh_set->models) {
|
||||
fwrite(&model_id, 4, 1, f);
|
||||
|
||||
uint32_t nn = static_cast<uint32_t>(mbvh.nodes.size());
|
||||
fwrite(&nn, 4, 1, f);
|
||||
if (nn > 0) fwrite(mbvh.nodes.data(), sizeof(BvhNode), nn, f);
|
||||
|
||||
uint32_t no = static_cast<uint32_t>(mbvh.object_indices.size());
|
||||
fwrite(&no, 4, 1, f);
|
||||
if (no > 0) fwrite(mbvh.object_indices.data(), 4, no, f);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::optional<SidecarData> readSidecar(const std::string& ifc_path,
|
||||
uint64_t ifc_file_size) {
|
||||
std::string path = sidecarPath(ifc_path);
|
||||
FILE* f = fopen(path.c_str(), "rb");
|
||||
if (!f) return std::nullopt;
|
||||
|
||||
auto fail = [&]() -> std::optional<SidecarData> { fclose(f); return std::nullopt; };
|
||||
|
||||
// Header
|
||||
SidecarHeader hdr;
|
||||
if (fread(&hdr, sizeof(hdr), 1, f) != 1) return fail();
|
||||
if (hdr.magic != SIDECAR_MAGIC ||
|
||||
hdr.version != SIDECAR_VERSION ||
|
||||
hdr.endian != SIDECAR_ENDIAN) return fail();
|
||||
|
||||
uint64_t stored_size;
|
||||
if (fread(&stored_size, 8, 1, f) != 1) return fail();
|
||||
if (stored_size != ifc_file_size) return fail();
|
||||
|
||||
SidecarData data;
|
||||
|
||||
// Geometry
|
||||
if (!readVec(f, data.vertices)) return fail();
|
||||
if (!readVec(f, data.indices)) return fail();
|
||||
|
||||
// Draw info
|
||||
if (!readVec(f, data.draw_info)) return fail();
|
||||
|
||||
// Elements + string table
|
||||
if (!readVec(f, data.elements)) return fail();
|
||||
uint32_t stbl_len;
|
||||
if (fread(&stbl_len, 4, 1, f) != 1) return fail();
|
||||
data.string_table.resize(stbl_len);
|
||||
if (stbl_len > 0 && fread(data.string_table.data(), 1, stbl_len, f) != stbl_len)
|
||||
return fail();
|
||||
|
||||
// BVH
|
||||
uint32_t num_bvh_models;
|
||||
if (fread(&num_bvh_models, 4, 1, f) != 1) return fail();
|
||||
|
||||
if (num_bvh_models > 0) {
|
||||
data.bvh_set = std::make_shared<BvhSet>();
|
||||
for (uint32_t m = 0; m < num_bvh_models; ++m) {
|
||||
uint32_t model_id;
|
||||
if (fread(&model_id, 4, 1, f) != 1) return fail();
|
||||
|
||||
ModelBvh mbvh;
|
||||
mbvh.model_id = model_id;
|
||||
|
||||
uint32_t nn;
|
||||
if (fread(&nn, 4, 1, f) != 1) return fail();
|
||||
mbvh.nodes.resize(nn);
|
||||
if (nn > 0 && fread(mbvh.nodes.data(), sizeof(BvhNode), nn, f) != nn)
|
||||
return fail();
|
||||
|
||||
uint32_t no;
|
||||
if (fread(&no, 4, 1, f) != 1) return fail();
|
||||
mbvh.object_indices.resize(no);
|
||||
if (no > 0 && fread(mbvh.object_indices.data(), 4, no, f) != no)
|
||||
return fail();
|
||||
|
||||
data.bvh_set->bvh_model_ids.insert(model_id);
|
||||
data.bvh_set->models[model_id] = std::move(mbvh);
|
||||
}
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
return data;
|
||||
std::optional<SidecarData> readSidecar(const std::string& /*ifc_path*/,
|
||||
uint64_t /*ifc_file_size*/) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
@@ -17,22 +17,28 @@
|
||||
* *
|
||||
********************************************************************************/
|
||||
|
||||
// NOTE: Sidecar format v3 is being rewritten to v4 (instanced geometry layout).
|
||||
// During the instancing rewrite (Commit A) the cache is a no-op: reads always
|
||||
// miss and writes always succeed without producing a file. Commit B will
|
||||
// re-introduce the on-disk format with MeshInfo + InstanceGpu sections.
|
||||
|
||||
#ifndef SIDECARCACHE_H
|
||||
#define SIDECARCACHE_H
|
||||
|
||||
#include "BvhAccel.h"
|
||||
#include "InstancedGeometry.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
static constexpr uint32_t SIDECAR_MAGIC = 0x49465657; // "IFVW"
|
||||
static constexpr uint32_t SIDECAR_VERSION = 3;
|
||||
static constexpr uint32_t SIDECAR_VERSION = 4;
|
||||
static constexpr uint32_t SIDECAR_ENDIAN = 0x01020304;
|
||||
|
||||
// Fixed-size element record for the sidecar. Strings are stored as
|
||||
// (offset, length) pairs into a separate string table.
|
||||
// Fixed-size element record. Strings are stored as (offset, length) pairs
|
||||
// into a separate string table.
|
||||
struct PackedElementInfo {
|
||||
uint32_t object_id;
|
||||
uint32_t model_id;
|
||||
@@ -46,30 +52,27 @@ struct PackedElementInfo {
|
||||
uint32_t type_length;
|
||||
};
|
||||
|
||||
// Everything the viewer needs to display a model without tessellating.
|
||||
// Everything needed to display an already-tessellated model without
|
||||
// re-running the iterator. v4 schema: instanced geometry.
|
||||
struct SidecarData {
|
||||
// GPU geometry (ready to upload as-is)
|
||||
std::vector<float> vertices; // interleaved, 8 floats per vertex
|
||||
std::vector<uint32_t> indices; // global (already remapped)
|
||||
// Per-model GPU geometry (local coords). 28 bytes/vertex.
|
||||
std::vector<float> vertices;
|
||||
std::vector<uint32_t> indices;
|
||||
|
||||
// Per-object metadata
|
||||
std::vector<ObjectDrawInfo> draw_info;
|
||||
// Mesh dictionary and per-instance data.
|
||||
std::vector<MeshInfo> meshes; // indexed by local_mesh_id
|
||||
std::vector<InstanceCpu> instances; // sorted by mesh_id
|
||||
|
||||
// Element tree metadata
|
||||
// Element tree metadata.
|
||||
std::vector<PackedElementInfo> elements;
|
||||
std::string string_table; // concatenated UTF-8
|
||||
|
||||
// BVH acceleration
|
||||
std::shared_ptr<BvhSet> bvh_set;
|
||||
std::string string_table;
|
||||
};
|
||||
|
||||
// Write a full sidecar next to the IFC file.
|
||||
// Returns true on success.
|
||||
// v4 writer/reader are stubbed for Commit A — no disk I/O happens.
|
||||
bool writeSidecar(const std::string& ifc_path,
|
||||
const SidecarData& data,
|
||||
uint64_t ifc_file_size);
|
||||
|
||||
// Read a sidecar. Returns nullopt on any failure (missing, stale, corrupt).
|
||||
std::optional<SidecarData> readSidecar(const std::string& ifc_path,
|
||||
uint64_t ifc_file_size);
|
||||
|
||||
|
||||
+352
-687
File diff suppressed because it is too large
Load Diff
@@ -28,58 +28,43 @@
|
||||
#include <QMatrix4x4>
|
||||
#include <QVector3D>
|
||||
|
||||
#include <deque>
|
||||
#include <vector>
|
||||
#include <unordered_set>
|
||||
#include <unordered_map>
|
||||
#include <cstdint>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
#include <atomic>
|
||||
|
||||
#include "BvhAccel.h"
|
||||
#include "InstancedGeometry.h"
|
||||
#include "SidecarCache.h"
|
||||
|
||||
struct MaterialInfo {
|
||||
float r = 0.75f, g = 0.75f, b = 0.78f, a = 1.0f;
|
||||
};
|
||||
|
||||
struct UploadChunk {
|
||||
// Interleaved per-vertex layout (8 floats / 32 bytes per vertex):
|
||||
// pos(3 float) + normal(3 float) + object_id(1 float bitcast from uint)
|
||||
// + color(1 float holding RGBA8 packed bytes, read on the GPU as
|
||||
// GL_UNSIGNED_BYTE * 4 normalized).
|
||||
std::vector<float> vertices;
|
||||
std::vector<uint32_t> indices; // local to this chunk's vertices
|
||||
uint32_t object_id = 0;
|
||||
uint32_t model_id = 0;
|
||||
};
|
||||
|
||||
// Per-model GPU state: own VAO, VBO, EBO, draw info, BVH.
|
||||
// Per-model GPU state for the instanced render path.
|
||||
//
|
||||
// VBO: local-coord interleaved verts (pos3 + normal3 + color1_packed) — 28 B.
|
||||
// EBO: mesh-local indices (uint32).
|
||||
// meshes[]: per-unique-representation metadata; indexed by local_mesh_id.
|
||||
// instances[]: CPU-side per-instance records; sorted by mesh_id at finalize.
|
||||
// ssbo: InstanceGpu[]; populated at finalize.
|
||||
//
|
||||
// A model is drawable once `finalized == true`.
|
||||
struct ModelGpuData {
|
||||
GLuint vao = 0;
|
||||
GLuint vbo = 0;
|
||||
GLuint ebo = 0;
|
||||
GLuint ssbo = 0;
|
||||
|
||||
size_t vbo_capacity = 0;
|
||||
size_t ebo_capacity = 0;
|
||||
size_t vbo_used = 0; // bytes
|
||||
size_t ebo_used = 0; // bytes
|
||||
uint32_t vertex_count = 0;
|
||||
size_t vbo_used = 0;
|
||||
size_t ebo_used = 0;
|
||||
uint32_t vertex_count = 0; // total (across all meshes)
|
||||
uint32_t total_triangles = 0;
|
||||
std::vector<ObjectDrawInfo> draw_info;
|
||||
uint32_t active_draw_count = 0; // how many objects are drawable (progressive upload)
|
||||
bool hidden = false;
|
||||
};
|
||||
|
||||
// Pending progressive upload — VBO first, then EBO.
|
||||
struct PendingUpload {
|
||||
uint32_t model_id = 0;
|
||||
std::vector<float> vertices;
|
||||
std::vector<uint32_t> indices;
|
||||
std::shared_ptr<BvhSet> bvh_set;
|
||||
size_t vbo_uploaded = 0; // bytes
|
||||
size_t ebo_uploaded = 0; // bytes
|
||||
std::vector<MeshInfo> meshes;
|
||||
std::vector<InstanceCpu> instances; // unsorted until finalize
|
||||
uint32_t ssbo_instance_count = 0;
|
||||
|
||||
bool finalized = false;
|
||||
bool hidden = false;
|
||||
};
|
||||
|
||||
class ViewportWindow : public QWindow {
|
||||
@@ -88,32 +73,21 @@ public:
|
||||
explicit ViewportWindow(QWindow* parent = nullptr);
|
||||
~ViewportWindow();
|
||||
|
||||
void uploadChunk(const UploadChunk& chunk);
|
||||
void resetScene();
|
||||
// Streaming ingress.
|
||||
void uploadMeshChunk(const MeshChunk& chunk);
|
||||
void uploadInstanceChunk(const InstanceChunk& chunk);
|
||||
|
||||
// Bulk upload pre-built geometry from a sidecar cache.
|
||||
// Creates a perfectly-sized per-model buffer set. No copy.
|
||||
void uploadBulk(uint32_t model_id,
|
||||
std::vector<float> vertices,
|
||||
std::vector<uint32_t> indices,
|
||||
const std::vector<ObjectDrawInfo>& draw_info,
|
||||
std::shared_ptr<BvhSet> bvh_set);
|
||||
// Called once all chunks for a model have arrived: sorts instances by
|
||||
// mesh_id, assigns each mesh its contiguous range, and uploads the
|
||||
// instance SSBO. The model becomes drawable.
|
||||
void finalizeModel(uint32_t model_id);
|
||||
|
||||
void resetScene();
|
||||
|
||||
void hideModel(uint32_t model_id);
|
||||
void showModel(uint32_t model_id);
|
||||
void removeModel(uint32_t model_id);
|
||||
|
||||
// Build BVH and optionally write a sidecar cache.
|
||||
void buildBvhAsync(uint32_t model_id,
|
||||
const std::string& ifc_path = "",
|
||||
uint64_t ifc_file_size = 0,
|
||||
std::vector<PackedElementInfo> sidecar_elements = {},
|
||||
std::string sidecar_string_table = {});
|
||||
|
||||
// Read snapshots of a model's GPU buffers into CPU vectors.
|
||||
std::vector<uint32_t> readbackEbo(uint32_t model_id) const;
|
||||
std::vector<float> readbackVbo(uint32_t model_id) const;
|
||||
|
||||
void setSelectedObjectId(uint32_t id);
|
||||
uint32_t pickObjectAt(int x, int y);
|
||||
|
||||
@@ -124,6 +98,8 @@ public:
|
||||
uint32_t visible_objects;
|
||||
uint32_t total_triangles;
|
||||
uint32_t visible_triangles;
|
||||
uint32_t unique_meshes;
|
||||
uint32_t instanced_draws;
|
||||
};
|
||||
|
||||
signals:
|
||||
@@ -147,13 +123,7 @@ private:
|
||||
void setupVaoLayout(GLuint vao, GLuint vbo, GLuint ebo);
|
||||
bool growModelVbo(ModelGpuData& m, size_t needed_total);
|
||||
bool growModelEbo(ModelGpuData& m, size_t needed_total);
|
||||
void buildVisibleList(const QMatrix4x4& vp);
|
||||
void traverseBvh(const ModelBvh& mbvh, const ModelGpuData& mgpu,
|
||||
const float planes[6][4]);
|
||||
static bool aabbInFrustum(const float aabb_min[3], const float aabb_max[3],
|
||||
const float planes[6][4]);
|
||||
void applyBvhResult();
|
||||
void processPendingUploads();
|
||||
ModelGpuData& getOrCreateModel(uint32_t model_id);
|
||||
|
||||
// Mouse interaction
|
||||
void handleMousePress(QMouseEvent* event);
|
||||
@@ -172,13 +142,12 @@ private:
|
||||
GLuint pick_program_ = 0;
|
||||
GLuint axis_program_ = 0;
|
||||
|
||||
// Axis gizmo (separate VAO/VBO since vertex layout differs from scene)
|
||||
// Axis gizmo
|
||||
GLuint axis_vao_ = 0;
|
||||
GLuint axis_vbo_ = 0;
|
||||
|
||||
// Per-model GPU data
|
||||
std::unordered_map<uint32_t, ModelGpuData> models_gpu_;
|
||||
std::mutex models_mutex_;
|
||||
|
||||
// Pick framebuffer
|
||||
GLuint pick_fbo_ = 0;
|
||||
@@ -187,21 +156,10 @@ private:
|
||||
int pick_width_ = 0;
|
||||
int pick_height_ = 0;
|
||||
|
||||
// Per-model BVH
|
||||
std::unordered_map<uint32_t, std::shared_ptr<const BvhSet>> model_bvhs_;
|
||||
|
||||
// Progressive upload queue
|
||||
std::deque<PendingUpload> pending_uploads_;
|
||||
|
||||
// Scratch buffers reused each frame to avoid allocation.
|
||||
struct ModelDrawCmd {
|
||||
GLuint vao;
|
||||
std::vector<GLsizei> counts;
|
||||
std::vector<const void*> offsets;
|
||||
};
|
||||
std::vector<ModelDrawCmd> frame_draw_cmds_;
|
||||
// Per-frame stats
|
||||
uint32_t visible_triangles_ = 0;
|
||||
uint32_t visible_objects_ = 0;
|
||||
uint32_t instanced_draws_ = 0;
|
||||
|
||||
// Camera
|
||||
QVector3D camera_target_{0, 0, 0};
|
||||
@@ -211,26 +169,14 @@ private:
|
||||
QMatrix4x4 view_matrix_;
|
||||
QMatrix4x4 proj_matrix_;
|
||||
|
||||
// Mouse state
|
||||
// Mouse
|
||||
Qt::MouseButton active_button_ = Qt::NoButton;
|
||||
QPoint last_mouse_pos_;
|
||||
|
||||
// Selection
|
||||
uint32_t selected_object_id_ = 0;
|
||||
bool pick_requested_ = false;
|
||||
int pick_x_ = 0, pick_y_ = 0;
|
||||
|
||||
// BVH build (phase 2)
|
||||
struct PendingBvh {
|
||||
uint32_t model_id;
|
||||
std::shared_ptr<BvhSet> bvh_set;
|
||||
EboReorderResult ebo_reorder;
|
||||
};
|
||||
std::unique_ptr<PendingBvh> pending_bvh_;
|
||||
std::mutex bvh_result_mutex_;
|
||||
std::thread bvh_build_thread_;
|
||||
|
||||
// Stats
|
||||
// FPS smoothing
|
||||
int frame_count_ = 0;
|
||||
float accumulated_time_ = 0.0f;
|
||||
float last_fps_ = 0.0f;
|
||||
|
||||
Reference in New Issue
Block a user