mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-31 08:56:34 +00:00
wgpu backend: CPU frustum cull + per-mesh draw compaction
Stage 6 of the wgpu port. Replaces the one-draw-per-(mesh, instance) loop
with a CPU cull pass that survives one drawIndexed per non-empty mesh
with packed instanceCount.
Adds to WgpuModelGpuData:
- visible_buffer: u32[] storage SSBO, pre-sized to instance_count at
applyCachedModel so the bind group reference never invalidates.
Re-uploaded each frame via wgpuQueueWriteBuffer.
- mesh_draws: per-mesh schedule (first_instance, instance_count,
first_index, base_vertex, index_count). instance_count==0 means the
mesh contributed nothing this frame and the draw is elided entirely.
cullModelCpu per-frame:
- Extract 6 frustum planes from the same VP we write into the uniform.
WebGPU clip-space z is [0, 1], so near plane = matrix row 2 (not
row 3 + row 2 as in GL); rest of the derivation is standard.
- Per-instance AABB-vs-frustum test using the p-vertex shortcut
(cheapest correct early-out for AABBs).
- Bucket survivors by mesh_id; flatten into a contiguous u32 list;
upload via wgpuQueueWriteBuffer. Per-mesh slice is [first_instance,
first_instance + instance_count).
WGSL adds @group(1) @binding(3) var<storage, read> visible: array<u32>
and an extra indirection: instance_idx = visible[iid]; the rest of the
shader is unchanged. firstInstance on each drawIndexed offsets into
visible[], so each mesh reads its own slice.
Verified two ways:
1. basic.ifc (3 instances, all on-screen) renders pixel-identically
to pre-stage-6 — proves cull keeps everything it should.
2. basic.ifc + a synthetic instance placed at (100, 100, 100) is
culled cleanly: only the cube renders, the far quad is rejected
by the frustum test. Proves cull actually rejects out-of-frustum
geometry rather than passing everything through.
Contribution culling, HiZ, and LOD selection arrive in stages 7 and 8;
they all hook into the same cullModelCpu seam.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -51,10 +51,32 @@ struct WgpuModelGpuData {
|
||||
// recompose from placement_transformation when stage matrices change.
|
||||
WGPUBuffer instance_storage = nullptr;
|
||||
|
||||
// Bind group binding the three storage buffers above (group=1 in the
|
||||
// u32[] of visible instance indices, repacked per frame by the CPU cull
|
||||
// into per-mesh contiguous slices. Sized for the worst case
|
||||
// (instance_count entries) at applyCachedModel time so we never have to
|
||||
// recreate it (and the bind group that references it) mid-frame.
|
||||
WGPUBuffer visible_buffer = nullptr;
|
||||
size_t visible_buffer_capacity = 0; // entries (u32 count), not bytes
|
||||
|
||||
// Bind group binding the four storage buffers above (group=1 in the
|
||||
// main pipeline). Built in applyCachedModel after the buffers exist.
|
||||
WGPUBindGroup bind_group = nullptr;
|
||||
|
||||
// One per mesh, populated per frame by cullAndCompact. instance_count==0
|
||||
// means the mesh contributes nothing this frame and the draw is skipped.
|
||||
struct MeshDraw {
|
||||
uint32_t first_instance = 0; // offset into visible_buffer
|
||||
uint32_t instance_count = 0;
|
||||
uint32_t first_index = 0; // index buffer offset (in u32 indices)
|
||||
int32_t base_vertex = 0; // added to every fetched vertex_index
|
||||
uint32_t index_count = 0;
|
||||
};
|
||||
std::vector<MeshDraw> mesh_draws; // size = meshes.size() after first frame
|
||||
|
||||
// Scratch reused each frame so per-frame cull doesn't allocate. Sized
|
||||
// to instance_count entries on first use; never shrunk.
|
||||
std::vector<uint32_t> visible_flat_scratch;
|
||||
|
||||
// Size mirrors for stats / range checks.
|
||||
size_t vertex_bytes = 0;
|
||||
uint32_t index_count = 0;
|
||||
|
||||
Reference in New Issue
Block a user