mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-19 03:33:48 +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.
|
// recompose from placement_transformation when stage matrices change.
|
||||||
WGPUBuffer instance_storage = nullptr;
|
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.
|
// main pipeline). Built in applyCachedModel after the buffers exist.
|
||||||
WGPUBindGroup bind_group = nullptr;
|
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 mirrors for stats / range checks.
|
||||||
size_t vertex_bytes = 0;
|
size_t vertex_bytes = 0;
|
||||||
uint32_t index_count = 0;
|
uint32_t index_count = 0;
|
||||||
|
|||||||
@@ -23,6 +23,9 @@
|
|||||||
#include <QResizeEvent>
|
#include <QResizeEvent>
|
||||||
#include <QDebug>
|
#include <QDebug>
|
||||||
#include <QFileInfo>
|
#include <QFileInfo>
|
||||||
|
#include <QMatrix4x4>
|
||||||
|
#include <QVector3D>
|
||||||
|
#include <QtMath>
|
||||||
|
|
||||||
#include <webgpu/wgpu.h> // wgpu-native extensions (logging, MULTI_DRAW_INDIRECT, …)
|
#include <webgpu/wgpu.h> // wgpu-native extensions (logging, MULTI_DRAW_INDIRECT, …)
|
||||||
|
|
||||||
@@ -53,6 +56,11 @@ static_assert(sizeof(FrameUniforms) == 16 * sizeof(float) + 4 * 4 * sizeof(float
|
|||||||
// stride in the capture path.
|
// stride in the capture path.
|
||||||
static constexpr uint64_t WGPU_BYTES_PER_ROW_ALIGN = 256;
|
static constexpr uint64_t WGPU_BYTES_PER_ROW_ALIGN = 256;
|
||||||
|
|
||||||
|
// Forward declaration — defined below alongside updateFrameUniforms. Used
|
||||||
|
// by render() to extract camera/frustum state without duplicating the math.
|
||||||
|
static QVector3D orbitEye(const float target[3], float dist,
|
||||||
|
float yaw_deg, float pitch_deg);
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Small helpers
|
// Small helpers
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -114,12 +122,16 @@ void releaseWgpuModelGpuData(WgpuModelGpuData& m) {
|
|||||||
if (m.index_buffer) { wgpuBufferRelease(m.index_buffer); m.index_buffer = nullptr; }
|
if (m.index_buffer) { wgpuBufferRelease(m.index_buffer); m.index_buffer = nullptr; }
|
||||||
if (m.mesh_storage) { wgpuBufferRelease(m.mesh_storage); m.mesh_storage = nullptr; }
|
if (m.mesh_storage) { wgpuBufferRelease(m.mesh_storage); m.mesh_storage = nullptr; }
|
||||||
if (m.instance_storage) { wgpuBufferRelease(m.instance_storage); m.instance_storage = nullptr; }
|
if (m.instance_storage) { wgpuBufferRelease(m.instance_storage); m.instance_storage = nullptr; }
|
||||||
|
if (m.visible_buffer) { wgpuBufferRelease(m.visible_buffer); m.visible_buffer = nullptr; }
|
||||||
m.vertex_bytes = 0;
|
m.vertex_bytes = 0;
|
||||||
m.index_count = 0;
|
m.index_count = 0;
|
||||||
m.mesh_count = 0;
|
m.mesh_count = 0;
|
||||||
m.instance_count = 0;
|
m.instance_count = 0;
|
||||||
|
m.visible_buffer_capacity = 0;
|
||||||
m.meshes.clear();
|
m.meshes.clear();
|
||||||
m.instances.clear();
|
m.instances.clear();
|
||||||
|
m.mesh_draws.clear();
|
||||||
|
m.visible_flat_scratch.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -160,6 +172,10 @@ struct FrameUniforms {
|
|||||||
@group(1) @binding(0) var<storage, read> vertices: array<u32>;
|
@group(1) @binding(0) var<storage, read> vertices: array<u32>;
|
||||||
@group(1) @binding(1) var<storage, read> meshes: array<MeshQuant>;
|
@group(1) @binding(1) var<storage, read> meshes: array<MeshQuant>;
|
||||||
@group(1) @binding(2) var<storage, read> instances: array<InstanceRecord>;
|
@group(1) @binding(2) var<storage, read> instances: array<InstanceRecord>;
|
||||||
|
// Per-frame compacted visible list: visible[firstInstance + i] picks the
|
||||||
|
// real instance for this draw slot. firstInstance is set per drawIndexed
|
||||||
|
// call so each mesh reads its own contiguous slice of `visible`.
|
||||||
|
@group(1) @binding(3) var<storage, read> visible: array<u32>;
|
||||||
|
|
||||||
struct VsOut {
|
struct VsOut {
|
||||||
@builtin(position) clip_pos: vec4<f32>,
|
@builtin(position) clip_pos: vec4<f32>,
|
||||||
@@ -188,7 +204,8 @@ fn octDecode(e: vec2<f32>) -> vec3<f32> {
|
|||||||
@vertex
|
@vertex
|
||||||
fn vs_main(@builtin(vertex_index) vid: u32,
|
fn vs_main(@builtin(vertex_index) vid: u32,
|
||||||
@builtin(instance_index) iid: u32) -> VsOut {
|
@builtin(instance_index) iid: u32) -> VsOut {
|
||||||
let inst = instances[iid];
|
let inst_idx = visible[iid];
|
||||||
|
let inst = instances[inst_idx];
|
||||||
let mq = meshes[inst.mesh_id];
|
let mq = meshes[inst.mesh_id];
|
||||||
|
|
||||||
let w0 = vertices[vid * 3u + 0u];
|
let w0 = vertices[vid * 3u + 0u];
|
||||||
@@ -386,9 +403,24 @@ void WgpuViewportWindow::applyCachedModel(uint32_t model_id, SidecarData data) {
|
|||||||
WGPUBufferUsage_Storage,
|
WGPUBufferUsage_Storage,
|
||||||
"model.instance_storage");
|
"model.instance_storage");
|
||||||
|
|
||||||
|
// Pre-size the visible buffer for the worst case (every instance visible).
|
||||||
|
// Per-frame cull writes a u32 list into it via wgpuQueueWriteBuffer; we
|
||||||
|
// never grow it so the bind group reference stays valid for the model's
|
||||||
|
// lifetime. Minimum size 4 bytes — wgpu rejects zero-sized buffers.
|
||||||
|
const size_t visible_bytes = std::max<size_t>(
|
||||||
|
size_t(data.instances.size()) * sizeof(uint32_t), 4u);
|
||||||
|
WGPUBufferDescriptor vb_desc = {};
|
||||||
|
vb_desc.size = visible_bytes;
|
||||||
|
vb_desc.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst;
|
||||||
|
vb_desc.label = svFromCStr("model.visible_buffer");
|
||||||
|
m.visible_buffer = wgpuDeviceCreateBuffer(device_, &vb_desc);
|
||||||
|
m.visible_buffer_capacity = std::max<size_t>(data.instances.size(), 1u);
|
||||||
|
|
||||||
// Hand off CPU mirrors (cull / picking will need them later).
|
// Hand off CPU mirrors (cull / picking will need them later).
|
||||||
m.meshes = std::move(data.meshes);
|
m.meshes = std::move(data.meshes);
|
||||||
m.instances = std::move(data.instances);
|
m.instances = std::move(data.instances);
|
||||||
|
m.mesh_draws.assign(m.meshes.size(), WgpuModelGpuData::MeshDraw{});
|
||||||
|
m.visible_flat_scratch.reserve(m.instances.size());
|
||||||
|
|
||||||
auto [inserted, _] = models_gpu_.emplace(model_id, std::move(m));
|
auto [inserted, _] = models_gpu_.emplace(model_id, std::move(m));
|
||||||
WgpuModelGpuData& mref = inserted->second;
|
WgpuModelGpuData& mref = inserted->second;
|
||||||
@@ -666,6 +698,120 @@ void WgpuViewportWindow::configureSurface(int width_px, int height_px) {
|
|||||||
ensureDepthTexture(width_px, height_px);
|
ensureDepthTexture(width_px, height_px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// CPU frustum cull + per-mesh compaction
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
//
|
||||||
|
// Plane extraction follows the standard "rows of the VP matrix" derivation,
|
||||||
|
// adjusted for WebGPU's [0, 1] clip-space z (near plane = row 2, not row 3
|
||||||
|
// + row 2 as in GL). Planes are stored as (a, b, c, d) with the convention
|
||||||
|
// a*x + b*y + c*z + d >= 0 meaning the point is inside.
|
||||||
|
//
|
||||||
|
// VP is column-major float[16] (Qt convention): element [c*4 + r] is column
|
||||||
|
// c, row r. row(i) = (vp[0*4+i], vp[1*4+i], vp[2*4+i], vp[3*4+i]).
|
||||||
|
|
||||||
|
static inline void rowVec(const float vp[16], int row, float out[4]) {
|
||||||
|
out[0] = vp[0 * 4 + row];
|
||||||
|
out[1] = vp[1 * 4 + row];
|
||||||
|
out[2] = vp[2 * 4 + row];
|
||||||
|
out[3] = vp[3 * 4 + row];
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void planeNormalize(float p[4]) {
|
||||||
|
const float len = std::sqrt(p[0] * p[0] + p[1] * p[1] + p[2] * p[2]);
|
||||||
|
if (len > 0.0f) {
|
||||||
|
const float inv = 1.0f / len;
|
||||||
|
p[0] *= inv; p[1] *= inv; p[2] *= inv; p[3] *= inv;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void extractFrustumPlanes(const float vp[16], float planes[6][4]) {
|
||||||
|
float r0[4], r1[4], r2[4], r3[4];
|
||||||
|
rowVec(vp, 0, r0);
|
||||||
|
rowVec(vp, 1, r1);
|
||||||
|
rowVec(vp, 2, r2);
|
||||||
|
rowVec(vp, 3, r3);
|
||||||
|
|
||||||
|
// left = r3 + r0
|
||||||
|
// right = r3 - r0
|
||||||
|
// bottom = r3 + r1
|
||||||
|
// top = r3 - r1
|
||||||
|
// near = r2 (WebGPU clip z >= 0)
|
||||||
|
// far = r3 - r2
|
||||||
|
for (int i = 0; i < 4; ++i) {
|
||||||
|
planes[0][i] = r3[i] + r0[i];
|
||||||
|
planes[1][i] = r3[i] - r0[i];
|
||||||
|
planes[2][i] = r3[i] + r1[i];
|
||||||
|
planes[3][i] = r3[i] - r1[i];
|
||||||
|
planes[4][i] = r2[i];
|
||||||
|
planes[5][i] = r3[i] - r2[i];
|
||||||
|
}
|
||||||
|
for (int p = 0; p < 6; ++p) planeNormalize(planes[p]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns false iff the AABB is fully outside any one plane (early-rejects
|
||||||
|
// trivially-invisible instances). May return true for boxes that straddle
|
||||||
|
// the frustum — that's fine, those still need to draw.
|
||||||
|
static bool aabbInFrustum(const float mn[3], const float mx[3],
|
||||||
|
const float planes[6][4]) {
|
||||||
|
for (int p = 0; p < 6; ++p) {
|
||||||
|
const float a = planes[p][0], b = planes[p][1], c = planes[p][2], d = planes[p][3];
|
||||||
|
// p-vertex: the AABB corner furthest along the plane normal.
|
||||||
|
const float px = (a >= 0.0f) ? mx[0] : mn[0];
|
||||||
|
const float py = (b >= 0.0f) ? mx[1] : mn[1];
|
||||||
|
const float pz = (c >= 0.0f) ? mx[2] : mn[2];
|
||||||
|
if (a * px + b * py + c * pz + d < 0.0f) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WgpuViewportWindow::cullModelCpu(WgpuModelGpuData& m, const float planes[6][4]) {
|
||||||
|
if (m.instances.empty() || m.meshes.empty() || !m.visible_buffer) {
|
||||||
|
for (auto& d : m.mesh_draws) d.instance_count = 0;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-mesh visible-instance buckets. Allocated once per cull from scratch
|
||||||
|
// vectors held on the model (no fresh heap on the per-frame path).
|
||||||
|
static thread_local std::vector<std::vector<uint32_t>> per_mesh_visible;
|
||||||
|
if (per_mesh_visible.size() < m.meshes.size()) per_mesh_visible.resize(m.meshes.size());
|
||||||
|
for (size_t mi = 0; mi < m.meshes.size(); ++mi) per_mesh_visible[mi].clear();
|
||||||
|
|
||||||
|
for (uint32_t i = 0; i < uint32_t(m.instances.size()); ++i) {
|
||||||
|
const auto& inst = m.instances[i];
|
||||||
|
if (inst.mesh_id >= m.meshes.size()) continue;
|
||||||
|
if (!aabbInFrustum(inst.world_aabb_min, inst.world_aabb_max, planes)) continue;
|
||||||
|
per_mesh_visible[inst.mesh_id].push_back(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flatten into m.visible_flat_scratch and populate per-mesh draws.
|
||||||
|
m.visible_flat_scratch.clear();
|
||||||
|
m.mesh_draws.assign(m.meshes.size(), WgpuModelGpuData::MeshDraw{});
|
||||||
|
for (uint32_t mi = 0; mi < m.meshes.size(); ++mi) {
|
||||||
|
const MeshInfo& mesh = m.meshes[mi];
|
||||||
|
WgpuModelGpuData::MeshDraw& d = m.mesh_draws[mi];
|
||||||
|
d.first_instance = uint32_t(m.visible_flat_scratch.size());
|
||||||
|
d.instance_count = uint32_t(per_mesh_visible[mi].size());
|
||||||
|
d.first_index = mesh.ebo_byte_offset / uint32_t(sizeof(uint32_t));
|
||||||
|
d.base_vertex = int32_t(mesh.vbo_byte_offset / INSTANCED_VERTEX_STRIDE_BYTES);
|
||||||
|
d.index_count = mesh.index_count;
|
||||||
|
for (uint32_t inst_idx : per_mesh_visible[mi]) {
|
||||||
|
m.visible_flat_scratch.push_back(inst_idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload the visible list. Round up to 4-byte multiple (always true for
|
||||||
|
// u32 arrays). Empty visible list still uploads one zero so the bind
|
||||||
|
// group has something well-defined; the per-mesh loop will skip the draw.
|
||||||
|
const size_t bytes_to_upload = std::max<size_t>(
|
||||||
|
m.visible_flat_scratch.size() * sizeof(uint32_t), sizeof(uint32_t));
|
||||||
|
static const uint32_t zero = 0;
|
||||||
|
const void* src = m.visible_flat_scratch.empty()
|
||||||
|
? static_cast<const void*>(&zero)
|
||||||
|
: static_cast<const void*>(m.visible_flat_scratch.data());
|
||||||
|
wgpuQueueWriteBuffer(queue_, m.visible_buffer, 0, src, bytes_to_upload);
|
||||||
|
}
|
||||||
|
|
||||||
void WgpuViewportWindow::render() {
|
void WgpuViewportWindow::render() {
|
||||||
WGPUSurfaceTexture surf_tex = {};
|
WGPUSurfaceTexture surf_tex = {};
|
||||||
wgpuSurfaceGetCurrentTexture(surface_, &surf_tex);
|
wgpuSurfaceGetCurrentTexture(surface_, &surf_tex);
|
||||||
@@ -693,6 +839,29 @@ void WgpuViewportWindow::render() {
|
|||||||
|
|
||||||
updateFrameUniforms();
|
updateFrameUniforms();
|
||||||
|
|
||||||
|
// Per-frame cull: extract frustum planes from the same VP we just wrote
|
||||||
|
// into the uniform, then run cullModelCpu on every visible model. The
|
||||||
|
// cull writes its results directly into each model's visible_buffer via
|
||||||
|
// wgpuQueueWriteBuffer — these writes are sequenced before the draw
|
||||||
|
// commands we encode next.
|
||||||
|
{
|
||||||
|
const QVector3D target(camera_target_[0], camera_target_[1], camera_target_[2]);
|
||||||
|
const QVector3D eye = orbitEye(camera_target_, camera_distance_,
|
||||||
|
camera_yaw_deg_, camera_pitch_deg_);
|
||||||
|
QMatrix4x4 v; v.lookAt(eye, target, QVector3D(0.0f, 0.0f, 1.0f));
|
||||||
|
const float aspect = (configured_h_ > 0)
|
||||||
|
? float(configured_w_) / float(configured_h_) : 1.0f;
|
||||||
|
QMatrix4x4 p; p.perspective(camera_fov_y_deg_, aspect, camera_near_, camera_far_);
|
||||||
|
QMatrix4x4 z; z(2, 2) = 0.5f; z(2, 3) = 0.5f;
|
||||||
|
const QMatrix4x4 vp = z * p * v;
|
||||||
|
float planes[6][4];
|
||||||
|
extractFrustumPlanes(vp.constData(), planes);
|
||||||
|
for (auto& [mid, m] : models_gpu_) {
|
||||||
|
if (m.hidden) continue;
|
||||||
|
cullModelCpu(m, planes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
WGPUCommandEncoder enc = wgpuDeviceCreateCommandEncoder(device_, nullptr);
|
WGPUCommandEncoder enc = wgpuDeviceCreateCommandEncoder(device_, nullptr);
|
||||||
|
|
||||||
WGPURenderPassColorAttachment color = {};
|
WGPURenderPassColorAttachment color = {};
|
||||||
@@ -736,27 +905,20 @@ void WgpuViewportWindow::render() {
|
|||||||
WGPUIndexFormat_Uint32,
|
WGPUIndexFormat_Uint32,
|
||||||
0, WGPU_WHOLE_SIZE);
|
0, WGPU_WHOLE_SIZE);
|
||||||
|
|
||||||
// One draw per (mesh, instance) pair. firstInstance carries the
|
// One drawIndexed per non-empty mesh. instanceCount is the number
|
||||||
// instance's absolute index into instance_storage so the vertex
|
// of frustum-surviving instances of this mesh; firstInstance is
|
||||||
// shader can fetch its transform; baseVertex offsets vertex pulling
|
// their offset into m.visible_buffer (consumed by the WGSL
|
||||||
// into the mesh's slice of the model's vertex storage.
|
// `visible[iid]` indirection). All-instances-culled meshes are
|
||||||
for (uint32_t mi = 0; mi < m.meshes.size(); ++mi) {
|
// skipped — no draw call issued at all.
|
||||||
const MeshInfo& mesh = m.meshes[mi];
|
for (const auto& d : m.mesh_draws) {
|
||||||
if (mesh.index_count == 0 || mesh.instance_count == 0) continue;
|
if (d.instance_count == 0 || d.index_count == 0) continue;
|
||||||
|
wgpuRenderPassEncoderDrawIndexed(
|
||||||
const uint32_t first_index = mesh.ebo_byte_offset / uint32_t(sizeof(uint32_t));
|
pass,
|
||||||
const int32_t base_vertex = int32_t(mesh.vbo_byte_offset / INSTANCED_VERTEX_STRIDE_BYTES);
|
d.index_count,
|
||||||
|
d.instance_count,
|
||||||
for (uint32_t i = 0; i < mesh.instance_count; ++i) {
|
d.first_index,
|
||||||
const uint32_t inst_idx = mesh.first_instance + i;
|
d.base_vertex,
|
||||||
wgpuRenderPassEncoderDrawIndexed(
|
d.first_instance);
|
||||||
pass,
|
|
||||||
mesh.index_count,
|
|
||||||
1, // instanceCount
|
|
||||||
first_index,
|
|
||||||
base_vertex,
|
|
||||||
inst_idx); // firstInstance == @builtin(instance_index)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -894,14 +1056,14 @@ bool WgpuViewportWindow::buildPipelines() {
|
|||||||
frame_bgl_desc.label = svFromCStr("ifcviewer-wgpu.frame_bgl");
|
frame_bgl_desc.label = svFromCStr("ifcviewer-wgpu.frame_bgl");
|
||||||
frame_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &frame_bgl_desc);
|
frame_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &frame_bgl_desc);
|
||||||
|
|
||||||
WGPUBindGroupLayoutEntry model_entries[3] = {};
|
WGPUBindGroupLayoutEntry model_entries[4] = {};
|
||||||
for (int i = 0; i < 3; ++i) {
|
for (int i = 0; i < 4; ++i) {
|
||||||
model_entries[i].binding = uint32_t(i);
|
model_entries[i].binding = uint32_t(i);
|
||||||
model_entries[i].visibility = WGPUShaderStage_Vertex;
|
model_entries[i].visibility = WGPUShaderStage_Vertex;
|
||||||
model_entries[i].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
|
model_entries[i].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
|
||||||
}
|
}
|
||||||
WGPUBindGroupLayoutDescriptor model_bgl_desc = {};
|
WGPUBindGroupLayoutDescriptor model_bgl_desc = {};
|
||||||
model_bgl_desc.entryCount = 3;
|
model_bgl_desc.entryCount = 4;
|
||||||
model_bgl_desc.entries = model_entries;
|
model_bgl_desc.entries = model_entries;
|
||||||
model_bgl_desc.label = svFromCStr("ifcviewer-wgpu.model_bgl");
|
model_bgl_desc.label = svFromCStr("ifcviewer-wgpu.model_bgl");
|
||||||
model_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &model_bgl_desc);
|
model_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &model_bgl_desc);
|
||||||
@@ -989,12 +1151,12 @@ void WgpuViewportWindow::buildModelBindGroup(WgpuModelGpuData& m) {
|
|||||||
wgpuBindGroupRelease(m.bind_group);
|
wgpuBindGroupRelease(m.bind_group);
|
||||||
m.bind_group = nullptr;
|
m.bind_group = nullptr;
|
||||||
}
|
}
|
||||||
if (!m.vertex_storage || !m.mesh_storage || !m.instance_storage) {
|
if (!m.vertex_storage || !m.mesh_storage || !m.instance_storage || !m.visible_buffer) {
|
||||||
// Empty model — no bind group needed; the draw loop will skip it.
|
// Empty model — no bind group needed; the draw loop will skip it.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
WGPUBindGroupEntry entries[3] = {};
|
WGPUBindGroupEntry entries[4] = {};
|
||||||
entries[0].binding = 0;
|
entries[0].binding = 0;
|
||||||
entries[0].buffer = m.vertex_storage;
|
entries[0].buffer = m.vertex_storage;
|
||||||
entries[0].size = WGPU_WHOLE_SIZE;
|
entries[0].size = WGPU_WHOLE_SIZE;
|
||||||
@@ -1004,10 +1166,13 @@ void WgpuViewportWindow::buildModelBindGroup(WgpuModelGpuData& m) {
|
|||||||
entries[2].binding = 2;
|
entries[2].binding = 2;
|
||||||
entries[2].buffer = m.instance_storage;
|
entries[2].buffer = m.instance_storage;
|
||||||
entries[2].size = WGPU_WHOLE_SIZE;
|
entries[2].size = WGPU_WHOLE_SIZE;
|
||||||
|
entries[3].binding = 3;
|
||||||
|
entries[3].buffer = m.visible_buffer;
|
||||||
|
entries[3].size = WGPU_WHOLE_SIZE;
|
||||||
|
|
||||||
WGPUBindGroupDescriptor desc = {};
|
WGPUBindGroupDescriptor desc = {};
|
||||||
desc.layout = model_bgl_;
|
desc.layout = model_bgl_;
|
||||||
desc.entryCount = 3;
|
desc.entryCount = 4;
|
||||||
desc.entries = entries;
|
desc.entries = entries;
|
||||||
desc.label = svFromCStr("ifcviewer-wgpu.model_bind_group");
|
desc.label = svFromCStr("ifcviewer-wgpu.model_bind_group");
|
||||||
m.bind_group = wgpuDeviceCreateBindGroup(device_, &desc);
|
m.bind_group = wgpuDeviceCreateBindGroup(device_, &desc);
|
||||||
@@ -1059,10 +1224,6 @@ void WgpuViewportWindow::releaseDepthTexture() {
|
|||||||
// rotation about Z (positive = anticlockwise looking down +Z); pitch is
|
// rotation about Z (positive = anticlockwise looking down +Z); pitch is
|
||||||
// elevation above the XY plane.
|
// elevation above the XY plane.
|
||||||
|
|
||||||
#include <QMatrix4x4>
|
|
||||||
#include <QVector3D>
|
|
||||||
#include <QtMath>
|
|
||||||
|
|
||||||
static QVector3D orbitEye(const float target[3], float dist,
|
static QVector3D orbitEye(const float target[3], float dist,
|
||||||
float yaw_deg, float pitch_deg) {
|
float yaw_deg, float pitch_deg) {
|
||||||
const float yaw = qDegreesToRadians(yaw_deg);
|
const float yaw = qDegreesToRadians(yaw_deg);
|
||||||
|
|||||||
@@ -102,6 +102,12 @@ private:
|
|||||||
void flushPendingSidecarQueue();
|
void flushPendingSidecarQueue();
|
||||||
bool computeSceneAabb(float mn[3], float mx[3]) const;
|
bool computeSceneAabb(float mn[3], float mx[3]) const;
|
||||||
|
|
||||||
|
// Cull `m`'s instances against the supplied frustum planes (world-space,
|
||||||
|
// ax+by+cz+d >= 0 means inside), bucket survivors by mesh_id, and write
|
||||||
|
// the flat visible-index list into m.visible_buffer via wgpuQueueWriteBuffer.
|
||||||
|
// After return, m.mesh_draws is the per-mesh draw schedule for the frame.
|
||||||
|
void cullModelCpu(WgpuModelGpuData& m, const float planes[6][4]);
|
||||||
|
|
||||||
bool wgpu_initialized_ = false;
|
bool wgpu_initialized_ = false;
|
||||||
bool surface_configured_ = false;
|
bool surface_configured_ = false;
|
||||||
int configured_w_ = 0;
|
int configured_w_ = 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user