Position now u16x3 normalized against each mesh's local AABB; normal
oct-encoded to i16x2; RGBA8 colour unchanged. Per-mesh dequant basis
lives in a new MeshGpu SSBO at binding 2; both main and pick shaders
mix() against it before applying the instance transform.
Drops VBO and sidecar size by ~43 % (28 -> 16 B/vert), which matters
mostly for warm-load downloads of precomputed sidecars and steady-state
VRAM. LodBuilder dequantizes positions into a scratch buffer before
calling meshopt, since meshoptimizer needs float positions.
Also fixes a streaming-time crash in cullAndUploadVisible: bvh_items
was only populated at finalize, but the linear fallback indexes it
during streaming. Mirror BvhItem appends in uploadInstanceChunk so the
hot path stays valid before the BVH is built.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Decimate each unique mesh once at sidecar-build time and swap to the
reduced index slice per-instance per-frame when projected sphere radius
drops below IFC_LOD1_PX (default 30). Same VBO, same SSBO, just a
different firstIndex/count in the indirect command.
Extends MeshInfo (48→56 B) with lod1_ebo_byte_offset + lod1_index_count
and bumps the sidecar to v5. buildLods() runs inside
onStreamingFinished, appends decimated indices to sd.indices,
applyLodExtension pushes the EBO suffix to the live GPU state, and the
sidecar is written with LOD1 baked in.
simplifySloppy (voxel clustering) is used instead of the default
edge-collapse meshopt_simplify because BIM brep output is per-triangle-
unwelded and non-manifold after welding — simplify returned the input
unchanged for every mesh tested. Sloppy ignores topology. Knobs
(IFC_LOD_SLOPPY, IFC_LOD_ERROR, IFC_LOD_RATIO, IFC_LOD_MIN_SAVINGS,
IFC_LOD_LOCK_BORDER, IFC_LOD_DEBUG) are available for A/B tuning.
Result on the 128M-tri 10-model test scene (GTX 1650, 2px contribution
cull): 20.2 → 43.2 fps, 40M → 14M visible triangles, no change in
object count. LOD build adds 100–600 ms per model on first open,
cached thereafter.
README Phase 3B section is now a full writeup of pipeline, selection,
decimator-choice rationale, env vars, and measured numbers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two bugs conflated as "weird colors":
1. Two-sided lighting. IFC placements often embed reflection
matrices (mirrored families). Transforming a_normal by
mat3(inst.transform) produces a normal pointing the wrong way
on those instances, and max(n·L, 0) then clamps the surface to
pure ambient — reads as dark / washed out. Use gl_FrontFacing
to flip n in the fragment shader so both winding orientations
shade correctly. The proper fix (ship an inverse-transpose
normal matrix or a det-sign bit per instance) is still owed;
that would unlock re-enabling GL_CULL_FACE for a big fragment-
work win on closed solids.
2. Stats label "inst_draws" was counting indirect sub-draws, not
actual GL draw calls — misleading since MDI collapses N sub-
draws into one glMultiDrawElementsIndirect. Split into
gl_draw_calls (real GL calls, = drawn-model count) and
indirect_sub_draws (packed sub-commands). For a BIM model
with 47k unique meshes at full view this now correctly reads
"1 gl_draws (47092 sub)" rather than suggesting 47k driver
dispatches.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Commit B of the instancing migration. The sidecar on-disk format is
reintroduced at version 4 with MeshInfo + InstanceCpu sections in place
of v3's flat per-object draw-info array.
After streaming finishes, MainWindow asks the viewport for a post-
finalise snapshot (VBO + EBO are read back from the GPU, meshes and
instances come from the CPU-side arrays) and writes it alongside
PackedElementInfo + the string table. On a subsequent load,
readSidecar rehydrates the whole struct and ViewportWindow::
applyCachedModel uploads VBO/EBO/SSBO in a single step, bypassing the
iterator entirely.
Staleness check is still by source file size.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
Phase 2 performance: BVH acceleration with median-split build, per-model
trees, and EBO re-sorting for GPU cache coherence. Raw binary .ifcview
sidecar stores full geometry + BVH for instant subsequent loads (skip
tessellation entirely).
Per-model GPU buffers (VAO/VBO/EBO per model) eliminate cross-model buffer
copies on growth. Sidecar reads happen on a background thread. Bulk GPU
uploads are progressive (48 MB/frame chunks) so the viewport stays
interactive while multi-GB models stream in.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Introduce ModelHandle and per-model GeometryStreamers so multiple IFC
files can be loaded simultaneously. Object IDs are globally unique
(monotonically increasing across models). File picker is now multiselect.
Each model gets a top-level tree node. Property lookup uses the correct
model's ifcopenshell::file. ViewportWindow supports hide/show/remove
per model via model_id filtering in the frustum cull pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Show FPS, frame time, visible/total objects, and visible/total
triangles in the status bar. Toggled via Settings > Show Performance
Stats, persisted in app settings.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>