mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-10 09:48:32 +00:00
wgpu backend: vertex-pulling main render pass
Stage 3 of the wgpu port. Replaces the clear-only render loop with the
full main shading pass:
- WGSL port of the GL main shader. Vertex-pulling: the vertex storage
buffer is read as array<u32> in the shader, with pos/normal/color
decoded manually per vertex. baseVertex (set per draw to mesh's
vertex offset) folds into @builtin(vertex_index) automatically;
firstInstance carries the instance slot for @builtin(instance_index).
No vertex-input layout — vertex pulling means no IA bindings.
- Render pipeline bound to depth-32-float (write-on, less compare),
back-face cull, CCW front face. Pre-multiplies a [-1,1]→[0,1] z-remap
matrix onto Qt's projection so WebGPU's clip-z convention is met.
- Two bind groups: group=0 per-frame (uniform with view-proj + key/fill
light + hemisphere ambient), group=1 per-model (three read-only
storage buffers: vertices, mesh quant, instances).
- Depth texture is created lazily and recreated on surface resize.
- Orbit camera state on WgpuViewportWindow with viewAll() that frames
the union of all loaded models' world AABBs after the first load.
Mouse navigation lands later.
- Draw loop: one drawIndexed per (mesh, instance) pair per model. This
is correct but CPU-heavy on dense scenes; stage 6 introduces the cull
+ compacted visible list that lets multiple instances of one mesh
collapse to a single call, and the eventual GPU-driven cull (post
sunset of the GL backend) goes further.
Verified on /tmp/quad_v13.ifcview (1 mesh, 1 instance) and on a real v13
sidecar baked from basic.ifc via the GL minimal viewer (3 meshes,
3 instances, 864 B verts). No wgpu validation errors fire across pipeline
creation, depth attachment, bind groups, or the draw loop on either.
Visual confirmation deferred until --screenshot lands (task #10) which
is being pulled forward next so we don't keep flying blind.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -51,6 +51,10 @@ 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
|
||||
// main pipeline). Built in applyCachedModel after the buffers exist.
|
||||
WGPUBindGroup bind_group = nullptr;
|
||||
|
||||
// Size mirrors for stats / range checks.
|
||||
size_t vertex_bytes = 0;
|
||||
uint32_t index_count = 0;
|
||||
|
||||
@@ -26,9 +26,27 @@
|
||||
|
||||
#include <webgpu/wgpu.h> // wgpu-native extensions (logging, MULTI_DRAW_INDIRECT, …)
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Frame uniforms (CPU mirror of group=0 binding=0 in the WGSL).
|
||||
// std140-ish layout: every member naturally 16-aligned, struct stride = 96.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
struct FrameUniforms {
|
||||
float view_proj[16];
|
||||
float light_dir[4]; // xyz = unit dir toward light, w unused
|
||||
float fill_dir[4]; // xyz = secondary fill dir
|
||||
float sky_color[4]; // xyz = sky-tint ambient, w unused
|
||||
float ground_color[4]; // xyz = ground-tint ambient, w unused
|
||||
};
|
||||
static_assert(sizeof(FrameUniforms) == 16 * sizeof(float) + 4 * 4 * sizeof(float),
|
||||
"FrameUniforms must match WGSL layout (mat4 + 4xvec4)");
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Small helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -85,6 +103,7 @@ static WGPUBuffer createBufferWithData(WGPUDevice device, WGPUQueue queue,
|
||||
}
|
||||
|
||||
void releaseWgpuModelGpuData(WgpuModelGpuData& m) {
|
||||
if (m.bind_group) { wgpuBindGroupRelease(m.bind_group); m.bind_group = nullptr; }
|
||||
if (m.vertex_storage) { wgpuBufferRelease(m.vertex_storage); m.vertex_storage = 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; }
|
||||
@@ -97,6 +116,143 @@ void releaseWgpuModelGpuData(WgpuModelGpuData& m) {
|
||||
m.instances.clear();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// WGSL main pipeline — vertex pulling.
|
||||
//
|
||||
// Vertex bytes are read manually from a u32 storage buffer (3 u32 per vertex,
|
||||
// matching INSTANCED_VERTEX_STRIDE_BYTES=12). gl_BaseVertex is folded into
|
||||
// vertex_index by the WebGPU IA, so per-mesh draws set baseVertex =
|
||||
// mesh.vbo_byte_offset / 12 and the shader indexes the global storage array
|
||||
// directly. firstInstance carries the global instance slot, fetched as
|
||||
// @builtin(instance_index).
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
static const char* MAIN_WGSL = R"(
|
||||
struct InstanceRecord {
|
||||
transform: mat4x4<f32>,
|
||||
object_id: u32,
|
||||
color_override: u32,
|
||||
mesh_id: u32,
|
||||
_pad1: u32,
|
||||
};
|
||||
|
||||
struct MeshQuant {
|
||||
aabb_min: vec4<f32>,
|
||||
aabb_max: vec4<f32>,
|
||||
};
|
||||
|
||||
struct FrameUniforms {
|
||||
view_proj: mat4x4<f32>,
|
||||
light_dir: vec4<f32>,
|
||||
fill_dir: vec4<f32>,
|
||||
sky_color: vec4<f32>,
|
||||
ground_color: vec4<f32>,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> u_frame: FrameUniforms;
|
||||
|
||||
@group(1) @binding(0) var<storage, read> vertices: array<u32>;
|
||||
@group(1) @binding(1) var<storage, read> meshes: array<MeshQuant>;
|
||||
@group(1) @binding(2) var<storage, read> instances: array<InstanceRecord>;
|
||||
|
||||
struct VsOut {
|
||||
@builtin(position) clip_pos: vec4<f32>,
|
||||
@location(0) normal: vec3<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
@location(2) world_pos: vec3<f32>,
|
||||
};
|
||||
|
||||
// Sign-extend an i8 packed into the byte_idx'th byte of `packed`.
|
||||
fn extractI8(packed: u32, byte_idx: u32) -> i32 {
|
||||
let raw = i32((packed >> (byte_idx * 8u)) & 0xFFu);
|
||||
return select(raw, raw - 256, raw >= 128);
|
||||
}
|
||||
|
||||
// Meyer et al. octahedral normal decode. Input in [-1,1]^2.
|
||||
fn octDecode(e: vec2<f32>) -> vec3<f32> {
|
||||
var n = vec3<f32>(e.x, e.y, 1.0 - abs(e.x) - abs(e.y));
|
||||
if (n.z < 0.0) {
|
||||
let tx = select(-1.0, 1.0, n.x >= 0.0);
|
||||
let ty = select(-1.0, 1.0, n.y >= 0.0);
|
||||
n = vec3<f32>((1.0 - abs(n.y)) * tx, (1.0 - abs(n.x)) * ty, n.z);
|
||||
}
|
||||
return normalize(n);
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vid: u32,
|
||||
@builtin(instance_index) iid: u32) -> VsOut {
|
||||
let inst = instances[iid];
|
||||
let mq = meshes[inst.mesh_id];
|
||||
|
||||
let w0 = vertices[vid * 3u + 0u];
|
||||
let w1 = vertices[vid * 3u + 1u];
|
||||
let w2 = vertices[vid * 3u + 2u];
|
||||
|
||||
let px = f32(w0 & 0xFFFFu) / 65535.0;
|
||||
let py = f32((w0 >> 16u) & 0xFFFFu) / 65535.0;
|
||||
let pz = f32(w1 & 0xFFFFu) / 65535.0;
|
||||
let pos_local = mix(mq.aabb_min.xyz, mq.aabb_max.xyz, vec3<f32>(px, py, pz));
|
||||
|
||||
let nx = f32(extractI8(w1, 2u)) / 127.0;
|
||||
let ny = f32(extractI8(w1, 3u)) / 127.0;
|
||||
let n_local = octDecode(vec2<f32>(nx, ny));
|
||||
|
||||
let r = f32(w2 & 0xFFu) / 255.0;
|
||||
let g = f32((w2 >> 8u) & 0xFFu) / 255.0;
|
||||
let b = f32((w2 >> 16u) & 0xFFu) / 255.0;
|
||||
let a = f32((w2 >> 24u) & 0xFFu) / 255.0;
|
||||
|
||||
let world4 = inst.transform * vec4<f32>(pos_local, 1.0);
|
||||
let rot = mat3x3<f32>(inst.transform[0].xyz,
|
||||
inst.transform[1].xyz,
|
||||
inst.transform[2].xyz);
|
||||
let n_world = normalize(rot * n_local);
|
||||
let det = determinant(rot);
|
||||
let n_final = select(n_world, -n_world, det < 0.0);
|
||||
|
||||
var color = vec4<f32>(r, g, b, a);
|
||||
if (inst.color_override != 0u) {
|
||||
let cr = f32(inst.color_override & 0xFFu) / 255.0;
|
||||
let cg = f32((inst.color_override >> 8u) & 0xFFu) / 255.0;
|
||||
let cb = f32((inst.color_override >> 16u) & 0xFFu) / 255.0;
|
||||
let ca = f32((inst.color_override >> 24u) & 0xFFu) / 255.0;
|
||||
if (ca > 0.0) { color = vec4<f32>(cr, cg, cb, ca); }
|
||||
}
|
||||
|
||||
var out: VsOut;
|
||||
out.clip_pos = u_frame.view_proj * world4;
|
||||
out.normal = n_final;
|
||||
out.color = color;
|
||||
out.world_pos = world4.xyz;
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
||||
var n = normalize(in.normal);
|
||||
|
||||
// World +Z is up (BIM convention). Hemisphere ambient: faces pointing
|
||||
// up read sky, faces pointing down read ground, lerp by n.z.
|
||||
let hemi_t = 0.5 + 0.5 * n.z;
|
||||
let ambient = mix(u_frame.ground_color.xyz, u_frame.sky_color.xyz, hemi_t);
|
||||
|
||||
let key = max(dot(n, u_frame.light_dir.xyz), 0.0);
|
||||
let fill = max(dot(n, u_frame.fill_dir.xyz), 0.0) * 0.35;
|
||||
|
||||
let color = in.color.xyz * (ambient + (key + fill) * 0.7);
|
||||
return vec4<f32>(color, in.color.a);
|
||||
}
|
||||
)";
|
||||
|
||||
// Helper: build a WGPUStringView from a null-terminated C string literal.
|
||||
static WGPUStringView svFromCStr(const char* s) {
|
||||
WGPUStringView v{};
|
||||
v.data = s;
|
||||
v.length = std::strlen(s);
|
||||
return v;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Construction / destruction
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -228,14 +384,22 @@ void WgpuViewportWindow::applyCachedModel(uint32_t model_id, SidecarData data) {
|
||||
m.meshes = std::move(data.meshes);
|
||||
m.instances = std::move(data.instances);
|
||||
|
||||
models_gpu_.emplace(model_id, std::move(m));
|
||||
auto [inserted, _] = models_gpu_.emplace(model_id, std::move(m));
|
||||
WgpuModelGpuData& mref = inserted->second;
|
||||
buildModelBindGroup(mref);
|
||||
|
||||
qInfo().noquote().nospace()
|
||||
<< "[wgpu] applyCachedModel mid=" << model_id
|
||||
<< " verts=" << m.vertex_bytes << "B"
|
||||
<< " idx=" << m.index_count
|
||||
<< " meshes=" << m.mesh_count
|
||||
<< " instances=" << m.instance_count;
|
||||
<< " verts=" << mref.vertex_bytes << "B"
|
||||
<< " idx=" << mref.index_count
|
||||
<< " meshes=" << mref.mesh_count
|
||||
<< " instances=" << mref.instance_count;
|
||||
|
||||
if (!initial_view_applied_) {
|
||||
viewAll();
|
||||
initial_view_applied_ = true;
|
||||
}
|
||||
if (isExposed()) requestUpdate();
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::removeModel(uint32_t model_id) {
|
||||
@@ -392,6 +556,8 @@ bool WgpuViewportWindow::initWgpu() {
|
||||
surface_format_ = caps.formats[0]; // preferred format per wgpu docs
|
||||
wgpuSurfaceCapabilitiesFreeMembers(caps);
|
||||
|
||||
if (!buildPipelines()) return false;
|
||||
|
||||
qInfo() << "wgpu init OK; surface format =" << int(surface_format_);
|
||||
return true;
|
||||
}
|
||||
@@ -489,6 +655,7 @@ void WgpuViewportWindow::configureSurface(int width_px, int height_px) {
|
||||
configured_w_ = width_px;
|
||||
configured_h_ = height_px;
|
||||
surface_configured_ = true;
|
||||
ensureDepthTexture(width_px, height_px);
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::render() {
|
||||
@@ -516,6 +683,8 @@ void WgpuViewportWindow::render() {
|
||||
|
||||
WGPUTextureView view = wgpuTextureCreateView(surf_tex.texture, nullptr);
|
||||
|
||||
updateFrameUniforms();
|
||||
|
||||
WGPUCommandEncoder enc = wgpuDeviceCreateCommandEncoder(device_, nullptr);
|
||||
|
||||
WGPURenderPassColorAttachment color = {};
|
||||
@@ -530,11 +699,60 @@ void WgpuViewportWindow::render() {
|
||||
};
|
||||
color.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED;
|
||||
|
||||
WGPURenderPassDepthStencilAttachment depth = {};
|
||||
depth.view = depth_view_;
|
||||
depth.depthLoadOp = WGPULoadOp_Clear;
|
||||
depth.depthStoreOp = WGPUStoreOp_Store;
|
||||
depth.depthClearValue = 1.0f;
|
||||
depth.stencilLoadOp = WGPULoadOp_Undefined;
|
||||
depth.stencilStoreOp = WGPUStoreOp_Undefined;
|
||||
depth.depthReadOnly = false;
|
||||
depth.stencilReadOnly = true;
|
||||
|
||||
WGPURenderPassDescriptor pass_desc = {};
|
||||
pass_desc.colorAttachmentCount = 1;
|
||||
pass_desc.colorAttachments = &color;
|
||||
pass_desc.depthStencilAttachment = depth_view_ ? &depth : nullptr;
|
||||
|
||||
WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(enc, &pass_desc);
|
||||
|
||||
if (main_pipeline_ && frame_bind_group_ && !models_gpu_.empty()) {
|
||||
wgpuRenderPassEncoderSetPipeline(pass, main_pipeline_);
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 0, frame_bind_group_, 0, nullptr);
|
||||
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden || !m.bind_group || !m.index_buffer || m.index_count == 0) continue;
|
||||
|
||||
wgpuRenderPassEncoderSetBindGroup(pass, 1, m.bind_group, 0, nullptr);
|
||||
wgpuRenderPassEncoderSetIndexBuffer(pass, m.index_buffer,
|
||||
WGPUIndexFormat_Uint32,
|
||||
0, WGPU_WHOLE_SIZE);
|
||||
|
||||
// One draw per (mesh, instance) pair. firstInstance carries the
|
||||
// instance's absolute index into instance_storage so the vertex
|
||||
// shader can fetch its transform; baseVertex offsets vertex pulling
|
||||
// into the mesh's slice of the model's vertex storage.
|
||||
for (uint32_t mi = 0; mi < m.meshes.size(); ++mi) {
|
||||
const MeshInfo& mesh = m.meshes[mi];
|
||||
if (mesh.index_count == 0 || mesh.instance_count == 0) continue;
|
||||
|
||||
const uint32_t first_index = mesh.ebo_byte_offset / uint32_t(sizeof(uint32_t));
|
||||
const int32_t base_vertex = int32_t(mesh.vbo_byte_offset / INSTANCED_VERTEX_STRIDE_BYTES);
|
||||
|
||||
for (uint32_t i = 0; i < mesh.instance_count; ++i) {
|
||||
const uint32_t inst_idx = mesh.first_instance + i;
|
||||
wgpuRenderPassEncoderDrawIndexed(
|
||||
pass,
|
||||
mesh.index_count,
|
||||
1, // instanceCount
|
||||
first_index,
|
||||
base_vertex,
|
||||
inst_idx); // firstInstance == @builtin(instance_index)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
wgpuRenderPassEncoderEnd(pass);
|
||||
wgpuRenderPassEncoderRelease(pass);
|
||||
|
||||
@@ -549,11 +767,289 @@ void WgpuViewportWindow::render() {
|
||||
wgpuTextureRelease(surf_tex.texture);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Pipeline + bind-group layouts (built once after init)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
bool WgpuViewportWindow::buildPipelines() {
|
||||
// ---- Bind group layouts ----------------------------------------------
|
||||
WGPUBindGroupLayoutEntry frame_entries[1] = {};
|
||||
frame_entries[0].binding = 0;
|
||||
frame_entries[0].visibility = WGPUShaderStage_Vertex | WGPUShaderStage_Fragment;
|
||||
frame_entries[0].buffer.type = WGPUBufferBindingType_Uniform;
|
||||
frame_entries[0].buffer.minBindingSize = sizeof(FrameUniforms);
|
||||
|
||||
WGPUBindGroupLayoutDescriptor frame_bgl_desc = {};
|
||||
frame_bgl_desc.entryCount = 1;
|
||||
frame_bgl_desc.entries = frame_entries;
|
||||
frame_bgl_desc.label = svFromCStr("ifcviewer-wgpu.frame_bgl");
|
||||
frame_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &frame_bgl_desc);
|
||||
|
||||
WGPUBindGroupLayoutEntry model_entries[3] = {};
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
model_entries[i].binding = uint32_t(i);
|
||||
model_entries[i].visibility = WGPUShaderStage_Vertex;
|
||||
model_entries[i].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
|
||||
}
|
||||
WGPUBindGroupLayoutDescriptor model_bgl_desc = {};
|
||||
model_bgl_desc.entryCount = 3;
|
||||
model_bgl_desc.entries = model_entries;
|
||||
model_bgl_desc.label = svFromCStr("ifcviewer-wgpu.model_bgl");
|
||||
model_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &model_bgl_desc);
|
||||
|
||||
// ---- Pipeline layout -------------------------------------------------
|
||||
WGPUBindGroupLayout bgls[2] = { frame_bgl_, model_bgl_ };
|
||||
WGPUPipelineLayoutDescriptor pl_desc = {};
|
||||
pl_desc.bindGroupLayoutCount = 2;
|
||||
pl_desc.bindGroupLayouts = bgls;
|
||||
pl_desc.label = svFromCStr("ifcviewer-wgpu.pipeline_layout");
|
||||
pipeline_layout_ = wgpuDeviceCreatePipelineLayout(device_, &pl_desc);
|
||||
|
||||
// ---- Shader module ---------------------------------------------------
|
||||
WGPUShaderSourceWGSL wgsl_src = {};
|
||||
wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL;
|
||||
wgsl_src.code = svFromCStr(MAIN_WGSL);
|
||||
|
||||
WGPUShaderModuleDescriptor sm_desc = {};
|
||||
sm_desc.nextInChain = &wgsl_src.chain;
|
||||
sm_desc.label = svFromCStr("ifcviewer-wgpu.main_wgsl");
|
||||
main_shader_module_ = wgpuDeviceCreateShaderModule(device_, &sm_desc);
|
||||
|
||||
// ---- Render pipeline -------------------------------------------------
|
||||
WGPUColorTargetState color_target = {};
|
||||
color_target.format = surface_format_;
|
||||
color_target.writeMask = WGPUColorWriteMask_All;
|
||||
|
||||
WGPUFragmentState frag = {};
|
||||
frag.module = main_shader_module_;
|
||||
frag.entryPoint = svFromCStr("fs_main");
|
||||
frag.targetCount = 1;
|
||||
frag.targets = &color_target;
|
||||
|
||||
WGPUDepthStencilState depth = {};
|
||||
depth.format = WGPUTextureFormat_Depth32Float;
|
||||
depth.depthWriteEnabled = WGPUOptionalBool_True;
|
||||
depth.depthCompare = WGPUCompareFunction_Less;
|
||||
depth.stencilFront.compare = WGPUCompareFunction_Always;
|
||||
depth.stencilBack.compare = WGPUCompareFunction_Always;
|
||||
|
||||
WGPURenderPipelineDescriptor rp_desc = {};
|
||||
rp_desc.layout = pipeline_layout_;
|
||||
rp_desc.label = svFromCStr("ifcviewer-wgpu.main_pipeline");
|
||||
rp_desc.vertex.module = main_shader_module_;
|
||||
rp_desc.vertex.entryPoint = svFromCStr("vs_main");
|
||||
rp_desc.vertex.bufferCount = 0; // vertex pulling: no IA bindings
|
||||
rp_desc.fragment = &frag;
|
||||
rp_desc.depthStencil = &depth;
|
||||
rp_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList;
|
||||
rp_desc.primitive.cullMode = WGPUCullMode_Back;
|
||||
rp_desc.primitive.frontFace = WGPUFrontFace_CCW;
|
||||
rp_desc.multisample.count = 1;
|
||||
rp_desc.multisample.mask = 0xFFFFFFFFu;
|
||||
|
||||
main_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc);
|
||||
if (!main_pipeline_) {
|
||||
qWarning() << "wgpu main render pipeline creation failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Per-frame uniform buffer + bind group ---------------------------
|
||||
WGPUBufferDescriptor fb_desc = {};
|
||||
fb_desc.size = sizeof(FrameUniforms);
|
||||
fb_desc.usage = WGPUBufferUsage_Uniform | WGPUBufferUsage_CopyDst;
|
||||
fb_desc.label = svFromCStr("ifcviewer-wgpu.frame_uniform");
|
||||
frame_uniform_buffer_ = wgpuDeviceCreateBuffer(device_, &fb_desc);
|
||||
|
||||
WGPUBindGroupEntry fbg_entries[1] = {};
|
||||
fbg_entries[0].binding = 0;
|
||||
fbg_entries[0].buffer = frame_uniform_buffer_;
|
||||
fbg_entries[0].offset = 0;
|
||||
fbg_entries[0].size = sizeof(FrameUniforms);
|
||||
WGPUBindGroupDescriptor fbg_desc = {};
|
||||
fbg_desc.layout = frame_bgl_;
|
||||
fbg_desc.entryCount = 1;
|
||||
fbg_desc.entries = fbg_entries;
|
||||
fbg_desc.label = svFromCStr("ifcviewer-wgpu.frame_bind_group");
|
||||
frame_bind_group_ = wgpuDeviceCreateBindGroup(device_, &fbg_desc);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::buildModelBindGroup(WgpuModelGpuData& m) {
|
||||
if (m.bind_group) {
|
||||
wgpuBindGroupRelease(m.bind_group);
|
||||
m.bind_group = nullptr;
|
||||
}
|
||||
if (!m.vertex_storage || !m.mesh_storage || !m.instance_storage) {
|
||||
// Empty model — no bind group needed; the draw loop will skip it.
|
||||
return;
|
||||
}
|
||||
|
||||
WGPUBindGroupEntry entries[3] = {};
|
||||
entries[0].binding = 0;
|
||||
entries[0].buffer = m.vertex_storage;
|
||||
entries[0].size = WGPU_WHOLE_SIZE;
|
||||
entries[1].binding = 1;
|
||||
entries[1].buffer = m.mesh_storage;
|
||||
entries[1].size = WGPU_WHOLE_SIZE;
|
||||
entries[2].binding = 2;
|
||||
entries[2].buffer = m.instance_storage;
|
||||
entries[2].size = WGPU_WHOLE_SIZE;
|
||||
|
||||
WGPUBindGroupDescriptor desc = {};
|
||||
desc.layout = model_bgl_;
|
||||
desc.entryCount = 3;
|
||||
desc.entries = entries;
|
||||
desc.label = svFromCStr("ifcviewer-wgpu.model_bind_group");
|
||||
m.bind_group = wgpuDeviceCreateBindGroup(device_, &desc);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Depth attachment
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
void WgpuViewportWindow::ensureDepthTexture(int w, int h) {
|
||||
if (w == depth_w_ && h == depth_h_ && depth_view_) return;
|
||||
releaseDepthTexture();
|
||||
|
||||
WGPUTextureDescriptor desc = {};
|
||||
desc.usage = WGPUTextureUsage_RenderAttachment;
|
||||
desc.dimension = WGPUTextureDimension_2D;
|
||||
desc.size.width = uint32_t(w);
|
||||
desc.size.height = uint32_t(h);
|
||||
desc.size.depthOrArrayLayers = 1;
|
||||
desc.format = WGPUTextureFormat_Depth32Float;
|
||||
desc.mipLevelCount = 1;
|
||||
desc.sampleCount = 1;
|
||||
desc.label = svFromCStr("ifcviewer-wgpu.depth");
|
||||
depth_texture_ = wgpuDeviceCreateTexture(device_, &desc);
|
||||
|
||||
WGPUTextureViewDescriptor vdesc = {};
|
||||
vdesc.format = WGPUTextureFormat_Depth32Float;
|
||||
vdesc.dimension = WGPUTextureViewDimension_2D;
|
||||
vdesc.mipLevelCount = 1;
|
||||
vdesc.arrayLayerCount = 1;
|
||||
vdesc.aspect = WGPUTextureAspect_DepthOnly;
|
||||
depth_view_ = wgpuTextureCreateView(depth_texture_, &vdesc);
|
||||
|
||||
depth_w_ = w;
|
||||
depth_h_ = h;
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::releaseDepthTexture() {
|
||||
if (depth_view_) { wgpuTextureViewRelease(depth_view_); depth_view_ = nullptr; }
|
||||
if (depth_texture_) { wgpuTextureRelease(depth_texture_); depth_texture_ = nullptr; }
|
||||
depth_w_ = depth_h_ = 0;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Camera + frame uniforms
|
||||
// -----------------------------------------------------------------------------
|
||||
//
|
||||
// Orbit camera around `camera_target_`. World +Z up (BIM convention). Yaw is
|
||||
// rotation about Z (positive = anticlockwise looking down +Z); pitch is
|
||||
// elevation above the XY plane.
|
||||
|
||||
#include <QMatrix4x4>
|
||||
#include <QVector3D>
|
||||
#include <QtMath>
|
||||
|
||||
static QVector3D orbitEye(const float target[3], float dist,
|
||||
float yaw_deg, float pitch_deg) {
|
||||
const float yaw = qDegreesToRadians(yaw_deg);
|
||||
const float pit = qDegreesToRadians(pitch_deg);
|
||||
const float cp = std::cos(pit), sp = std::sin(pit);
|
||||
const float cy = std::cos(yaw), sy = std::sin(yaw);
|
||||
return QVector3D(target[0] + dist * cp * sy,
|
||||
target[1] - dist * cp * cy,
|
||||
target[2] + dist * sp);
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::updateFrameUniforms() {
|
||||
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 view;
|
||||
view.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 proj;
|
||||
proj.perspective(camera_fov_y_deg_, aspect, camera_near_, camera_far_);
|
||||
|
||||
// Qt builds a GL-style projection (clip-z in [-1, 1]); WebGPU expects
|
||||
// clip-z in [0, 1]. Pre-multiply by a remap matrix that maps [-1,1] → [0,1].
|
||||
QMatrix4x4 z_remap; // identity
|
||||
z_remap(2, 2) = 0.5f;
|
||||
z_remap(2, 3) = 0.5f;
|
||||
|
||||
const QMatrix4x4 view_proj = z_remap * proj * view;
|
||||
|
||||
FrameUniforms u = {};
|
||||
std::memcpy(u.view_proj, view_proj.constData(), 16 * sizeof(float));
|
||||
|
||||
QVector3D L(0.4f, -0.6f, 0.9f); L.normalize();
|
||||
QVector3D F(-0.3f, 0.5f, 0.4f); F.normalize();
|
||||
u.light_dir[0] = L.x(); u.light_dir[1] = L.y(); u.light_dir[2] = L.z(); u.light_dir[3] = 0;
|
||||
u.fill_dir [0] = F.x(); u.fill_dir [1] = F.y(); u.fill_dir [2] = F.z(); u.fill_dir [3] = 0;
|
||||
u.sky_color [0] = 0.85f; u.sky_color [1] = 0.88f; u.sky_color [2] = 0.95f;
|
||||
u.ground_color[0] = 0.35f; u.ground_color[1] = 0.32f; u.ground_color[2] = 0.30f;
|
||||
|
||||
wgpuQueueWriteBuffer(queue_, frame_uniform_buffer_, 0, &u, sizeof(u));
|
||||
}
|
||||
|
||||
bool WgpuViewportWindow::computeSceneAabb(float mn[3], float mx[3]) const {
|
||||
bool any = false;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
mn[i] = std::numeric_limits<float>::infinity();
|
||||
mx[i] = -std::numeric_limits<float>::infinity();
|
||||
}
|
||||
for (const auto& [mid, m] : models_gpu_) {
|
||||
if (m.hidden) continue;
|
||||
for (const auto& inst : m.instances) {
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
mn[i] = std::min(mn[i], inst.world_aabb_min[i]);
|
||||
mx[i] = std::max(mx[i], inst.world_aabb_max[i]);
|
||||
}
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
return any;
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::viewAll() {
|
||||
float mn[3], mx[3];
|
||||
if (!computeSceneAabb(mn, mx)) return;
|
||||
camera_target_[0] = 0.5f * (mn[0] + mx[0]);
|
||||
camera_target_[1] = 0.5f * (mn[1] + mx[1]);
|
||||
camera_target_[2] = 0.5f * (mn[2] + mx[2]);
|
||||
const float diag = std::sqrt(
|
||||
(mx[0] - mn[0]) * (mx[0] - mn[0]) +
|
||||
(mx[1] - mn[1]) * (mx[1] - mn[1]) +
|
||||
(mx[2] - mn[2]) * (mx[2] - mn[2]));
|
||||
// Pull back enough so the bounding sphere fits at half-FOV.
|
||||
const float half_fov = qDegreesToRadians(camera_fov_y_deg_) * 0.5f;
|
||||
camera_distance_ = std::max(1.0f, 0.6f * diag / std::tan(half_fov));
|
||||
if (isExposed()) requestUpdate();
|
||||
}
|
||||
|
||||
void WgpuViewportWindow::shutdown() {
|
||||
// Release per-model buffers before the device they were created from.
|
||||
for (auto& [mid, m] : models_gpu_) releaseWgpuModelGpuData(m);
|
||||
models_gpu_.clear();
|
||||
|
||||
releaseDepthTexture();
|
||||
|
||||
if (frame_bind_group_) { wgpuBindGroupRelease(frame_bind_group_); frame_bind_group_ = nullptr; }
|
||||
if (frame_uniform_buffer_) { wgpuBufferRelease(frame_uniform_buffer_); frame_uniform_buffer_ = nullptr; }
|
||||
if (main_pipeline_) { wgpuRenderPipelineRelease(main_pipeline_); main_pipeline_ = nullptr; }
|
||||
if (main_shader_module_) { wgpuShaderModuleRelease(main_shader_module_); main_shader_module_ = nullptr; }
|
||||
if (pipeline_layout_) { wgpuPipelineLayoutRelease(pipeline_layout_); pipeline_layout_ = nullptr; }
|
||||
if (model_bgl_) { wgpuBindGroupLayoutRelease(model_bgl_); model_bgl_ = nullptr; }
|
||||
if (frame_bgl_) { wgpuBindGroupLayoutRelease(frame_bgl_); frame_bgl_ = nullptr; }
|
||||
|
||||
if (queue_) { wgpuQueueRelease(queue_); queue_ = nullptr; }
|
||||
if (device_) { wgpuDeviceRelease(device_); device_ = nullptr; }
|
||||
if (adapter_) { wgpuAdapterRelease(adapter_); adapter_ = nullptr; }
|
||||
|
||||
@@ -70,6 +70,11 @@ public:
|
||||
|
||||
size_t modelCount() const { return models_gpu_.size(); }
|
||||
|
||||
// Frame the union of all loaded models' world AABBs. No-op on empty
|
||||
// scenes. Called automatically after the first model loads; clients
|
||||
// can re-invoke to re-frame.
|
||||
void viewAll();
|
||||
|
||||
protected:
|
||||
void exposeEvent(QExposeEvent* event) override;
|
||||
void resizeEvent(QResizeEvent* event) override;
|
||||
@@ -82,7 +87,13 @@ private:
|
||||
void render();
|
||||
void shutdown();
|
||||
|
||||
void flushPendingSidecarQueue();
|
||||
bool buildPipelines();
|
||||
void buildModelBindGroup(WgpuModelGpuData& m);
|
||||
void ensureDepthTexture(int w, int h);
|
||||
void releaseDepthTexture();
|
||||
void updateFrameUniforms();
|
||||
void flushPendingSidecarQueue();
|
||||
bool computeSceneAabb(float mn[3], float mx[3]) const;
|
||||
|
||||
bool wgpu_initialized_ = false;
|
||||
bool surface_configured_ = false;
|
||||
@@ -96,14 +107,46 @@ private:
|
||||
WGPUSurface surface_ = nullptr;
|
||||
WGPUTextureFormat surface_format_ = WGPUTextureFormat_Undefined;
|
||||
|
||||
// Render pipeline + bind group layouts (built once after init).
|
||||
WGPUShaderModule main_shader_module_ = nullptr;
|
||||
WGPUBindGroupLayout frame_bgl_ = nullptr; // group 0
|
||||
WGPUBindGroupLayout model_bgl_ = nullptr; // group 1
|
||||
WGPUPipelineLayout pipeline_layout_ = nullptr;
|
||||
WGPURenderPipeline main_pipeline_ = nullptr;
|
||||
|
||||
// Per-frame uniform (view-proj + lighting), bound at group 0.
|
||||
WGPUBuffer frame_uniform_buffer_ = nullptr;
|
||||
WGPUBindGroup frame_bind_group_ = nullptr;
|
||||
|
||||
// Depth attachment, recreated on surface resize.
|
||||
WGPUTexture depth_texture_ = nullptr;
|
||||
WGPUTextureView depth_view_ = nullptr;
|
||||
int depth_w_ = 0;
|
||||
int depth_h_ = 0;
|
||||
|
||||
QColor background_color_ = QColor("#202329");
|
||||
|
||||
// Camera (orbit, right-handed Y-up world → wait, BIM is +Z up).
|
||||
// Mirrors the GL viewport's defaults; mouse navigation lands later.
|
||||
float camera_target_[3] = { 0.0f, 0.0f, 0.0f };
|
||||
float camera_distance_ = 50.0f;
|
||||
float camera_yaw_deg_ = 45.0f;
|
||||
float camera_pitch_deg_ = 30.0f;
|
||||
float camera_fov_y_deg_ = 45.0f;
|
||||
float camera_near_ = 0.1f;
|
||||
float camera_far_ = 10000.0f;
|
||||
|
||||
// Per-model state, keyed by viewport-assigned model_id.
|
||||
std::unordered_map<uint32_t, WgpuModelGpuData> models_gpu_;
|
||||
uint32_t next_model_id_ = 1;
|
||||
|
||||
// Sidecar paths queued before init completes.
|
||||
std::deque<QString> pending_sidecars_;
|
||||
|
||||
// Set after the first model load triggers a viewAll(); prevents
|
||||
// subsequent loads from snapping the camera away from where the
|
||||
// user pointed it.
|
||||
bool initial_view_applied_ = false;
|
||||
};
|
||||
|
||||
#endif // WGPUVIEWPORTWINDOW_H
|
||||
|
||||
Reference in New Issue
Block a user