mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-08-11 10:06:47 +00:00
ifcviewer: move buildPipelines + selection-flags wiring into ViewportCore (#84-k)
Move the main render pipeline construction + the selection flags
buffer/bind group lifecycle. Both buildPipelines and the selection
flags methods produce/consume state ViewportCore already owns
(main_pipeline_, frame_bgl_, etc.) plus a handful of "frame
infrastructure" fields this commit also brings across.
State moved (7 fields):
WGPUBuffer frame_uniform_buffer_
WGPUBindGroup frame_bind_group_
WGPUBuffer selection_flags_buffer_
uint32_t selection_flags_capacity_
std::vector<u32> selection_flags_scratch_
SelectionState selection_
VisibilityState visibility_
Methods moved:
buildPipelines (~150 lines + 320-line MAIN_WGSL string)
ensureSelectionFlagsBuffer (~60 lines)
uploadSelectionFlagsIfDirty (~10 lines)
Plus the MAIN_WGSL constant + the svFromCStr helper into
ViewportCore.cpp's anonymous namespace. ViewportWindow.cpp keeps its
own svFromCStr copy (still used by 50+ label fields in the not-yet-
moved pipeline builders + render encoders).
Shared constants extracted to ViewportCore.h:
kMaxSectionPlanes (was OverlayRenderer::kMaxSectionPlanes — assert
in VW.cpp keeps them in sync)
kViewportSampleCount (was SAMPLE_COUNT in VW; VW keeps a static
constexpr alias for the existing callsites)
struct FrameUniforms (canonical layout for the per-frame UBO,
consumed by both core's buildPipelines and
VW's still-in-flight updateFrameUniforms)
Builds: desktop / bonsai / web all green. Tests 100/100.
This commit is contained in:
@@ -28,6 +28,7 @@
|
||||
|
||||
#include "CameraMath.h"
|
||||
#include "InstanceCompose.h"
|
||||
#include "Log.h"
|
||||
|
||||
namespace {
|
||||
// Orbit camera around target_. World +Z up (BIM convention). Yaw is
|
||||
@@ -543,3 +544,571 @@ ViewportCore::volumesPerObject(
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// Pipeline construction (#84-k)
|
||||
// ===========================================================================
|
||||
|
||||
namespace {
|
||||
// WGPUStringView builder for null-terminated C strings. Used heavily by
|
||||
// label fields and shader source descriptors. Tiny but worth a name.
|
||||
WGPUStringView svFromCStr(const char* s) {
|
||||
WGPUStringView v{};
|
||||
v.data = s;
|
||||
v.length = std::strlen(s);
|
||||
return v;
|
||||
}
|
||||
|
||||
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>,
|
||||
clip_count: i32,
|
||||
// Three scalar i32 pads instead of vec3<i32>: vec3 has 16-byte
|
||||
// alignment so it would also pad the SUBSEQUENT clip_planes start
|
||||
// up to offset 160. Three i32s pad to 144 with no further nudge,
|
||||
// matching the tightly-packed C++ FrameUniforms (240 B).
|
||||
_pad_clip_0: i32,
|
||||
_pad_clip_1: i32,
|
||||
_pad_clip_2: i32,
|
||||
clip_planes: array<vec4<f32>, 6>,
|
||||
// X-ray mode cap. fs_main clamps `out.a = min(in.color.a, xray_alpha_cap)`.
|
||||
// Default 1.0 (no effect — the min returns in.color.a). Alt+X drops it
|
||||
// toward ~0.3 to translucent-everything. The cull classifier also
|
||||
// routes every instance into the transparent pass when this is < 1
|
||||
// so the blend stage actually fires (an opaque-pass fragment with
|
||||
// capped alpha would still overwrite the back buffer).
|
||||
xray_alpha_cap: f32,
|
||||
_pad_xray_0: f32,
|
||||
_pad_xray_1: f32,
|
||||
_pad_xray_2: f32,
|
||||
};
|
||||
|
||||
// Returns true if `world` lies on the positive (clipped-away) side of any
|
||||
// active section plane. Each plane is (n.xyz, d) and clips where
|
||||
// dot(n, world) + d > 0. Both the main and pick fragments discard with
|
||||
// this predicate so cuts are visible AND consistent with selection.
|
||||
fn is_section_clipped(world: vec3<f32>) -> bool {
|
||||
let n = u_frame.clip_count;
|
||||
if (n == 0) { return false; }
|
||||
for (var i = 0; i < n; i = i + 1) {
|
||||
let p = u_frame.clip_planes[i];
|
||||
if (dot(p.xyz, world) + p.w > 0.0) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
struct VisibleDraw {
|
||||
mesh_id: u32,
|
||||
instance_idx: u32,
|
||||
ebo_first_u32: u32,
|
||||
base_vertex: u32,
|
||||
};
|
||||
|
||||
struct PerModel {
|
||||
draw_count: u32,
|
||||
total_vertex_count: u32,
|
||||
_pad0: u32,
|
||||
_pad1: u32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> u_frame: FrameUniforms;
|
||||
// Selection flags indexed by object_id. bit 0 = in selection, bit 1 = active.
|
||||
// Sized to next_object_id_ on the CPU side; out-of-range reads can't happen
|
||||
// because we cap the index by arrayLength before fetching.
|
||||
@group(0) @binding(1) var<storage, read> sel_flags: 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(2) var<storage, read> instances: array<InstanceRecord>;
|
||||
@group(1) @binding(3) var<storage, read> indices: array<u32>;
|
||||
@group(1) @binding(4) var<storage, read> visible_draws: array<VisibleDraw>;
|
||||
@group(1) @binding(5) var<storage, read> prefix_sums: array<u32>;
|
||||
@group(1) @binding(6) var<uniform> u_model: PerModel;
|
||||
|
||||
struct VsOut {
|
||||
@builtin(position) clip_pos: vec4<f32>,
|
||||
@location(0) normal: vec3<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
@location(2) world_pos: vec3<f32>,
|
||||
@location(3) @interpolate(flat) object_id: u32,
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Binary search for the largest i in [0, draw_count) with prefix_sums[i] <= vid.
|
||||
// prefix_sums is monotonic non-decreasing and contains draw_count+1 entries
|
||||
// (prefix_sums[draw_count] == total_vertex_count).
|
||||
fn find_draw(vid: u32) -> u32 {
|
||||
var lo: u32 = 0u;
|
||||
var hi: u32 = u_model.draw_count;
|
||||
while (lo + 1u < hi) {
|
||||
let mid = (lo + hi) >> 1u;
|
||||
if (prefix_sums[mid] <= vid) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
return lo;
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vid: u32) -> VsOut {
|
||||
// Saturate past the end (shouldn't happen given draw() count, but safe).
|
||||
if (vid >= u_model.total_vertex_count) {
|
||||
var degen: VsOut;
|
||||
degen.clip_pos = vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
||||
return degen;
|
||||
}
|
||||
|
||||
let draw_idx = find_draw(vid);
|
||||
let local_v = vid - prefix_sums[draw_idx];
|
||||
let item = visible_draws[draw_idx];
|
||||
|
||||
// Fetch the mesh-local index then the global vertex index.
|
||||
let mesh_local_index = indices[item.ebo_first_u32 + local_v];
|
||||
let v_global = item.base_vertex + mesh_local_index;
|
||||
|
||||
let inst = instances[item.instance_idx];
|
||||
let mq = meshes[item.mesh_id];
|
||||
|
||||
let w0 = vertices[v_global * 3u + 0u];
|
||||
let w1 = vertices[v_global * 3u + 1u];
|
||||
let w2 = vertices[v_global * 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;
|
||||
out.object_id = inst.object_id;
|
||||
return out;
|
||||
}
|
||||
|
||||
// sRGB decode — used to undo wgpu's automatic linear→sRGB write encoding
|
||||
// on swap-chain BGRA8Unorm so the final bytes match what the GL backend
|
||||
// writes directly. The GL pipeline outputs to a non-sRGB FB and treats
|
||||
// every colour input as already-linear, so its bytes are exactly its
|
||||
// shader outputs. wgpu on the same swap chain auto-encodes, which makes
|
||||
// everything appear ~3× brighter unless we pre-decode once.
|
||||
fn srgbToLinear(s: vec3<f32>) -> vec3<f32> {
|
||||
let lo = s / 12.92;
|
||||
let hi = pow((s + 0.055) / 1.055, vec3<f32>(2.4));
|
||||
return select(hi, lo, s <= vec3<f32>(0.04045));
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
||||
if (is_section_clipped(in.world_pos)) { discard; }
|
||||
|
||||
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;
|
||||
|
||||
var color = in.color.xyz * (ambient + (key + fill) * 0.7);
|
||||
|
||||
// Cavity shading: where adjacent fragments have a sharp normal change
|
||||
// (concave creases, edges where two faces meet), darken slightly so
|
||||
// shape boundaries read on flat-colour models. Matches the GL shader.
|
||||
let cavity = clamp(length(fwidth(n)) * 1.5, 0.0, 0.35);
|
||||
color = color * (1.0 - cavity);
|
||||
|
||||
// Selection tint. bit 0 = in selection (cool blue mix), bit 1 = active
|
||||
// (slightly stronger blue mix). Matches the GL main shader.
|
||||
if (in.object_id < arrayLength(&sel_flags)) {
|
||||
let flags = sel_flags[in.object_id];
|
||||
if ((flags & 1u) != 0u) { color = mix(color, vec3<f32>(0.2, 0.6, 1.0), 0.45); }
|
||||
if ((flags & 2u) != 0u) { color = mix(color, vec3<f32>(0.4, 0.8, 1.0), 0.40); }
|
||||
}
|
||||
|
||||
// Cancel the swap chain's implicit linear→sRGB encoding so the final
|
||||
// bytes match the GL backend (see srgbToLinear above). Alpha is
|
||||
// clamped to `xray_alpha_cap` (default 1.0 = no effect; X-ray sets
|
||||
// it to ~0.3) so a global translucency override lands without
|
||||
// touching any per-instance state.
|
||||
let alpha_out = min(in.color.a, u_frame.xray_alpha_cap);
|
||||
return vec4<f32>(srgbToLinear(color), alpha_out);
|
||||
}
|
||||
|
||||
// --------------------------- Pick pipeline ---------------------------------
|
||||
// Same vertex pulling as vs_main, but VsOutPick carries only the object_id
|
||||
// (flat-interpolated). Fragment writes the object_id to an R32UInt target.
|
||||
// Background (no draw) reads 0 because the pick attachment is cleared to 0.
|
||||
|
||||
struct VsOutPick {
|
||||
@builtin(position) clip_pos: vec4<f32>,
|
||||
@location(0) @interpolate(flat) object_id: u32,
|
||||
@location(1) world_pos: vec3<f32>,
|
||||
@location(2) normal: vec3<f32>,
|
||||
};
|
||||
|
||||
// Section tool needs the actual per-fragment normal (the AABB face was
|
||||
// too coarse for diagonal geometry). Two color attachments — R32UInt
|
||||
// object_id at @location(0), RGBA16F packed normal at @location(1).
|
||||
// We multiply-by-0.5+0.5 so unsigned half-floats keep the sign without
|
||||
// extra channel allocation.
|
||||
struct FsOutPick {
|
||||
@location(0) object_id: u32,
|
||||
@location(1) normal: vec4<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_pick(@builtin(vertex_index) vid: u32) -> VsOutPick {
|
||||
var out: VsOutPick;
|
||||
if (vid >= u_model.total_vertex_count) {
|
||||
out.clip_pos = vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
||||
out.object_id = 0u;
|
||||
out.world_pos = vec3<f32>(0.0, 0.0, 0.0);
|
||||
out.normal = vec3<f32>(0.0, 0.0, 1.0);
|
||||
return out;
|
||||
}
|
||||
|
||||
let draw_idx = find_draw(vid);
|
||||
let local_v = vid - prefix_sums[draw_idx];
|
||||
let item = visible_draws[draw_idx];
|
||||
let mesh_local_index = indices[item.ebo_first_u32 + local_v];
|
||||
let v_global = item.base_vertex + mesh_local_index;
|
||||
let inst = instances[item.instance_idx];
|
||||
let mq = meshes[item.mesh_id];
|
||||
|
||||
let w0 = vertices[v_global * 3u + 0u];
|
||||
let w1 = vertices[v_global * 3u + 1u];
|
||||
let pos_norm = vec3<f32>(
|
||||
f32(w0 & 0xFFFFu) / 65535.0,
|
||||
f32((w0 >> 16u) & 0xFFFFu) / 65535.0,
|
||||
f32(w1 & 0xFFFFu) / 65535.0,
|
||||
);
|
||||
let pos_local = mix(mq.aabb_min.xyz, mq.aabb_max.xyz, pos_norm);
|
||||
let world4 = inst.transform * vec4<f32>(pos_local, 1.0);
|
||||
|
||||
// Decode the same octahedral normal as vs_main — pick needs it so
|
||||
// the section tool can drop perpendicular cuts.
|
||||
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 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);
|
||||
|
||||
out.clip_pos = u_frame.view_proj * world4;
|
||||
out.object_id = inst.object_id;
|
||||
out.world_pos = world4.xyz;
|
||||
out.normal = n_final;
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_pick(in: VsOutPick) -> FsOutPick {
|
||||
if (is_section_clipped(in.world_pos)) { discard; }
|
||||
var out: FsOutPick;
|
||||
out.object_id = in.object_id;
|
||||
// Pack signed normal into RGBA16F (unsigned-ish half range) as ×0.5+0.5.
|
||||
out.normal = vec4<f32>(normalize(in.normal) * 0.5 + vec3<f32>(0.5), 1.0);
|
||||
return out;
|
||||
}
|
||||
)";
|
||||
} // namespace
|
||||
|
||||
bool ViewportCore::buildPipelines() {
|
||||
// ---- Bind group layouts ----------------------------------------------
|
||||
WGPUBindGroupLayoutEntry frame_entries[2] = {};
|
||||
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);
|
||||
frame_entries[1].binding = 1;
|
||||
frame_entries[1].visibility = WGPUShaderStage_Fragment;
|
||||
frame_entries[1].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
|
||||
|
||||
WGPUBindGroupLayoutDescriptor frame_bgl_desc = {};
|
||||
frame_bgl_desc.entryCount = 2;
|
||||
frame_bgl_desc.entries = frame_entries;
|
||||
frame_bgl_desc.label = svFromCStr("ifcviewer-wgpu.frame_bgl");
|
||||
frame_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &frame_bgl_desc);
|
||||
|
||||
// 6 read-only storage buffers (vertices, meshes, instances, indices,
|
||||
// visible_draws, prefix_sums) + 1 uniform (per-model count). All read
|
||||
// in the vertex shader. WebGPU's mandatory min is 8 storage / 12 uniform
|
||||
// per stage, so we're comfortably under the cap.
|
||||
WGPUBindGroupLayoutEntry model_entries[7] = {};
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
model_entries[i].binding = uint32_t(i);
|
||||
model_entries[i].visibility = WGPUShaderStage_Vertex;
|
||||
model_entries[i].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
|
||||
}
|
||||
model_entries[6].binding = 6;
|
||||
model_entries[6].visibility = WGPUShaderStage_Vertex;
|
||||
model_entries[6].buffer.type = WGPUBufferBindingType_Uniform;
|
||||
model_entries[6].buffer.minBindingSize = 16;
|
||||
WGPUBindGroupLayoutDescriptor model_bgl_desc = {};
|
||||
model_bgl_desc.entryCount = 7;
|
||||
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 = kViewportSampleCount;
|
||||
rp_desc.multisample.mask = 0xFFFFFFFFu;
|
||||
|
||||
main_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc);
|
||||
if (!main_pipeline_) {
|
||||
Log::warn() << "wgpu main render pipeline creation failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Transparent variant of the main pipeline ----------------------
|
||||
// Same shader, same layout, same vertex pulling, same depth test —
|
||||
// differs only in:
|
||||
// * depth.depthWriteEnabled = False (we still depth-test against
|
||||
// the opaque pass's z-buffer, but the transparent fragment's z
|
||||
// doesn't write, so further-back geometry behind the glass still
|
||||
// paints over)
|
||||
// * color_target.blend = SrcAlpha / OneMinusSrcAlpha (standard
|
||||
// porter-duff "over" — premultiplied wouldn't help because our
|
||||
// vertex colours come in straight-alpha from the IFC iterator)
|
||||
// No sort, no OIT — overlapping transparent surfaces of the same
|
||||
// kind will produce order-dependent artefacts but for typical IFC
|
||||
// glazing (panes that don't overlap much in screen space) the
|
||||
// result is "good enough".
|
||||
WGPUBlendState main_blend = {};
|
||||
main_blend.color.srcFactor = WGPUBlendFactor_SrcAlpha;
|
||||
main_blend.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
|
||||
main_blend.color.operation = WGPUBlendOperation_Add;
|
||||
main_blend.alpha.srcFactor = WGPUBlendFactor_One;
|
||||
main_blend.alpha.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
|
||||
main_blend.alpha.operation = WGPUBlendOperation_Add;
|
||||
|
||||
WGPUColorTargetState color_target_transparent = color_target;
|
||||
color_target_transparent.blend = &main_blend;
|
||||
|
||||
WGPUFragmentState frag_transparent = frag;
|
||||
frag_transparent.targets = &color_target_transparent;
|
||||
|
||||
// depthWriteEnabled stays True so the edge-detect pass (which samples
|
||||
// depth_view_ to find silhouette discontinuities) can see window
|
||||
// panes — leaving it False made transparent surfaces invisible to
|
||||
// the edge detector, so windows ended up as edge-less "framed holes"
|
||||
// and the edges of opaque geometry behind the glass painted through
|
||||
// at full intensity. Trade-off: overlapping transparent surfaces
|
||||
// become depth-test-occluded by the closer one, increasing order
|
||||
// sensitivity. For BIM glass (panes that don't overlap in screen
|
||||
// space) this is invisible; for scenes where it matters, the right
|
||||
// fix is OIT or sort-by-distance, not turning depth write off.
|
||||
WGPUDepthStencilState depth_transparent = depth;
|
||||
|
||||
WGPURenderPipelineDescriptor rp_desc_t = rp_desc;
|
||||
rp_desc_t.label = svFromCStr("ifcviewer-wgpu.main_pipeline_transparent");
|
||||
rp_desc_t.fragment = &frag_transparent;
|
||||
rp_desc_t.depthStencil = &depth_transparent;
|
||||
|
||||
main_pipeline_transparent_ =
|
||||
wgpuDeviceCreateRenderPipeline(device_, &rp_desc_t);
|
||||
if (!main_pipeline_transparent_) {
|
||||
Log::warn() << "wgpu main transparent render pipeline creation failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Per-frame uniform buffer ---------------------------------------
|
||||
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);
|
||||
|
||||
// frame_bind_group_ is built lazily once we have a selection_flags_
|
||||
// buffer to bind alongside the uniform — ensureSelectionFlagsBuffer
|
||||
// handles both the first creation and any subsequent resize.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ViewportCore::ensureSelectionFlagsBuffer() {
|
||||
// Round up to at least 64 entries (256 B — minimum useful storage) and
|
||||
// grow geometrically when next_object_id_ outruns the current capacity.
|
||||
const uint32_t needed = std::max<uint32_t>(next_object_id_, 64);
|
||||
if (selection_flags_buffer_ && selection_flags_capacity_ >= needed) {
|
||||
if (!frame_bind_group_) {
|
||||
// First-time bind group creation after the buffer exists.
|
||||
// (Should always be true here.)
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// (Re)allocate. Geometric grow so we don't recreate every frame as a
|
||||
// big scene streams in.
|
||||
uint32_t new_cap = selection_flags_capacity_;
|
||||
if (new_cap < 64) new_cap = 64;
|
||||
while (new_cap < needed) new_cap *= 2;
|
||||
|
||||
if (!selection_flags_buffer_ || selection_flags_capacity_ < new_cap) {
|
||||
if (selection_flags_buffer_) {
|
||||
wgpuBufferRelease(selection_flags_buffer_);
|
||||
selection_flags_buffer_ = nullptr;
|
||||
}
|
||||
WGPUBufferDescriptor sb = {};
|
||||
sb.size = uint64_t(new_cap) * sizeof(uint32_t);
|
||||
sb.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst;
|
||||
sb.label = svFromCStr("ifcviewer-wgpu.selection_flags");
|
||||
selection_flags_buffer_ = wgpuDeviceCreateBuffer(device_, &sb);
|
||||
selection_flags_capacity_ = new_cap;
|
||||
// Initialise to zero so any unused range reads as "not selected".
|
||||
// wgpuQueueWriteBuffer with a small zero block is enough; the rest
|
||||
// is created as zero-initialised by wgpu per the spec.
|
||||
}
|
||||
|
||||
// Rebuild the frame bind group against the (possibly new) buffer.
|
||||
if (frame_bind_group_) {
|
||||
wgpuBindGroupRelease(frame_bind_group_);
|
||||
frame_bind_group_ = nullptr;
|
||||
}
|
||||
WGPUBindGroupEntry fbg_entries[2] = {};
|
||||
fbg_entries[0].binding = 0;
|
||||
fbg_entries[0].buffer = frame_uniform_buffer_;
|
||||
fbg_entries[0].size = sizeof(FrameUniforms);
|
||||
fbg_entries[1].binding = 1;
|
||||
fbg_entries[1].buffer = selection_flags_buffer_;
|
||||
fbg_entries[1].size = WGPU_WHOLE_SIZE;
|
||||
WGPUBindGroupDescriptor fbg_desc = {};
|
||||
fbg_desc.layout = frame_bgl_;
|
||||
fbg_desc.entryCount = 2;
|
||||
fbg_desc.entries = fbg_entries;
|
||||
fbg_desc.label = svFromCStr("ifcviewer-wgpu.frame_bind_group");
|
||||
frame_bind_group_ = wgpuDeviceCreateBindGroup(device_, &fbg_desc);
|
||||
|
||||
// Force a re-upload of the flags into the (possibly new) buffer.
|
||||
selection_flags_scratch_.assign(selection_flags_capacity_, 0);
|
||||
selection_.fillFlagsArray(selection_flags_scratch_, selection_flags_capacity_);
|
||||
wgpuQueueWriteBuffer(queue_, selection_flags_buffer_, 0,
|
||||
selection_flags_scratch_.data(),
|
||||
selection_flags_scratch_.size() * sizeof(uint32_t));
|
||||
selection_.markClean();
|
||||
}
|
||||
|
||||
void ViewportCore::uploadSelectionFlagsIfDirty() {
|
||||
if (!selection_.dirty() || !selection_flags_buffer_) return;
|
||||
selection_flags_scratch_.assign(selection_flags_capacity_, 0);
|
||||
selection_.fillFlagsArray(selection_flags_scratch_, selection_flags_capacity_);
|
||||
wgpuQueueWriteBuffer(queue_, selection_flags_buffer_, 0,
|
||||
selection_flags_scratch_.data(),
|
||||
selection_flags_scratch_.size() * sizeof(uint32_t));
|
||||
selection_.markClean();
|
||||
}
|
||||
|
||||
@@ -47,8 +47,43 @@
|
||||
#include "InstanceCompose.h"
|
||||
#include "InstancedGeometry.h"
|
||||
#include "ModelGpuData.h"
|
||||
#include "SelectionState.h"
|
||||
#include "StreamingThread.h"
|
||||
#include "ViewportHost.h"
|
||||
#include "VisibilityState.h"
|
||||
|
||||
// Render-loop constants shared between ViewportCore and ViewportWindow.
|
||||
// Kept here (not in OverlayRenderer.h) so IfcViewerCore stays Qt-free.
|
||||
// ViewportWindow.cpp asserts the section-plane cap matches
|
||||
// OverlayRenderer's so the WGSL clip-plane array and the section-tool
|
||||
// state vector agree by construction.
|
||||
constexpr int kMaxSectionPlanes = 6;
|
||||
constexpr uint32_t kViewportSampleCount = 4;
|
||||
|
||||
// Per-frame uniform layout. Matches the WGSL struct the main pipeline
|
||||
// declares (see ViewportCore.cpp MAIN_WGSL). Both buildPipelines (in
|
||||
// ViewportCore) and updateFrameUniforms (currently in ViewportWindow)
|
||||
// allocate / write this; keeping the type here makes the layout the
|
||||
// single source of truth.
|
||||
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
|
||||
int clip_count; // active section-plane count (≤ kMaxSectionPlanes)
|
||||
int _pad_clip[3]; // pad to 16-byte alignment for the array below
|
||||
float clip_planes[kMaxSectionPlanes][4]; // xyz = world-space unit normal, w = plane offset
|
||||
float xray_alpha_cap; // X-ray mode: fragment alpha clamped to min(in.color.a, cap)
|
||||
float _pad_xray[3]; // pad to 16-byte alignment so the struct stays vec4-aligned
|
||||
};
|
||||
static_assert(sizeof(FrameUniforms)
|
||||
== 16 * sizeof(float)
|
||||
+ 4 * 4 * sizeof(float)
|
||||
+ 4 * sizeof(int)
|
||||
+ kMaxSectionPlanes * 4 * sizeof(float)
|
||||
+ 4 * sizeof(float),
|
||||
"FrameUniforms must match WGSL layout");
|
||||
|
||||
class ViewportCore {
|
||||
public:
|
||||
@@ -171,6 +206,23 @@ public:
|
||||
std::vector<std::pair<uint32_t, double>>
|
||||
volumesPerObject(const std::vector<uint32_t>& object_ids) const;
|
||||
|
||||
// ---- Pipeline construction --------------------------------------------
|
||||
//
|
||||
// buildPipelines creates the main render pipelines (opaque +
|
||||
// transparent variants), bind group layouts, the per-frame UBO,
|
||||
// and the WGSL shader module. Called once after the device + queue
|
||||
// come up + the surface format is picked.
|
||||
//
|
||||
// ensureSelectionFlagsBuffer (re)allocates the selection flags
|
||||
// storage buffer geometrically as next_object_id_ grows, and
|
||||
// (re)builds the frame bind group when its referenced buffers
|
||||
// change. uploadSelectionFlagsIfDirty flushes
|
||||
// selection_.fillFlagsArray() into the GPU when selection_'s dirty
|
||||
// flag is set — called once per frame at render time.
|
||||
bool buildPipelines();
|
||||
void ensureSelectionFlagsBuffer();
|
||||
void uploadSelectionFlagsIfDirty();
|
||||
|
||||
// Friend access for ViewportWindow's reference proxies. As each
|
||||
// render method moves into ViewportCore it stops needing these
|
||||
// (it touches the fields directly); once everything has migrated
|
||||
@@ -227,6 +279,28 @@ private:
|
||||
// main pass since the pick fragment also vertex-pulls instance data.
|
||||
WGPURenderPipeline pick_pipeline_ = nullptr;
|
||||
|
||||
// ---- Frame uniforms + selection bind ----------------------------------
|
||||
//
|
||||
// The per-frame UBO (view-proj + lighting + section planes + xray
|
||||
// params) and the frame bind group it lives in alongside the
|
||||
// selection flags storage buffer at group=0 binding=1.
|
||||
// ensureSelectionFlagsBuffer is the only writer for the buffer +
|
||||
// bind group; uploadSelectionFlagsIfDirty repopulates the flags
|
||||
// from selection_ when it changes.
|
||||
WGPUBuffer frame_uniform_buffer_ = nullptr;
|
||||
WGPUBindGroup frame_bind_group_ = nullptr;
|
||||
WGPUBuffer selection_flags_buffer_ = nullptr;
|
||||
uint32_t selection_flags_capacity_ = 0; // u32 entries
|
||||
std::vector<uint32_t> selection_flags_scratch_;
|
||||
|
||||
// Selection + per-element visibility state machines. Pure CPU
|
||||
// bookkeeping today (no GPU touch beyond the readback uploaded via
|
||||
// selection_flags_buffer_). Mutated on the main thread between
|
||||
// renders; cull workers read concurrently which is safe as long
|
||||
// as no concurrent writes.
|
||||
SelectionState selection_;
|
||||
VisibilityState visibility_;
|
||||
|
||||
// ---- Scene state ---------------------------------------------------------
|
||||
//
|
||||
// Sub-allocator for chunk vertex + index buffers. All per-chunk
|
||||
|
||||
@@ -53,29 +53,12 @@
|
||||
// std140-ish layout: every member naturally 16-aligned, struct stride = 96.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
// Section-cutting cap. Single source of truth lives in OverlayRenderer
|
||||
// so the visualizer and the WGSL clip array agree by construction.
|
||||
static constexpr int kMaxSectionPlanes = OverlayRenderer::kMaxSectionPlanes;
|
||||
|
||||
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
|
||||
int clip_count; // active section-plane count (≤ kMaxSectionPlanes)
|
||||
int _pad_clip[3]; // pad to 16-byte alignment for the array below
|
||||
float clip_planes[kMaxSectionPlanes][4]; // xyz = world-space unit normal, w = plane offset
|
||||
float xray_alpha_cap; // X-ray mode: fragment alpha clamped to min(in.color.a, cap)
|
||||
float _pad_xray[3]; // pad to 16-byte alignment so the struct stays vec4-aligned
|
||||
};
|
||||
static_assert(sizeof(FrameUniforms)
|
||||
== 16 * sizeof(float)
|
||||
+ 4 * 4 * sizeof(float)
|
||||
+ 4 * sizeof(int)
|
||||
+ kMaxSectionPlanes * 4 * sizeof(float)
|
||||
+ 4 * sizeof(float),
|
||||
"FrameUniforms must match WGSL layout");
|
||||
// kMaxSectionPlanes + FrameUniforms moved to ViewportCore.h (#84-k).
|
||||
// Keep this assert so OverlayRenderer's kMaxSectionPlanes (the section
|
||||
// visualizer's per-plane uniform slot count) stays in sync with the
|
||||
// WGSL clip array size.
|
||||
static_assert(kMaxSectionPlanes == OverlayRenderer::kMaxSectionPlanes,
|
||||
"section-plane cap must match OverlayRenderer's");
|
||||
|
||||
// Inverse of sRGB encoding. wgpu-native's Vulkan swap chain on X11 treats
|
||||
// BGRA8Unorm as sRGB-output (encodes shader output linear→sRGB on write,
|
||||
@@ -240,326 +223,7 @@ static WGPUBuffer createBufferWithData(WGPUDevice device, WGPUQueue queue,
|
||||
// GPU.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
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>,
|
||||
clip_count: i32,
|
||||
// Three scalar i32 pads instead of vec3<i32>: vec3 has 16-byte
|
||||
// alignment so it would also pad the SUBSEQUENT clip_planes start
|
||||
// up to offset 160. Three i32s pad to 144 with no further nudge,
|
||||
// matching the tightly-packed C++ FrameUniforms (240 B).
|
||||
_pad_clip_0: i32,
|
||||
_pad_clip_1: i32,
|
||||
_pad_clip_2: i32,
|
||||
clip_planes: array<vec4<f32>, 6>,
|
||||
// X-ray mode cap. fs_main clamps `out.a = min(in.color.a, xray_alpha_cap)`.
|
||||
// Default 1.0 (no effect — the min returns in.color.a). Alt+X drops it
|
||||
// toward ~0.3 to translucent-everything. The cull classifier also
|
||||
// routes every instance into the transparent pass when this is < 1
|
||||
// so the blend stage actually fires (an opaque-pass fragment with
|
||||
// capped alpha would still overwrite the back buffer).
|
||||
xray_alpha_cap: f32,
|
||||
_pad_xray_0: f32,
|
||||
_pad_xray_1: f32,
|
||||
_pad_xray_2: f32,
|
||||
};
|
||||
|
||||
// Returns true if `world` lies on the positive (clipped-away) side of any
|
||||
// active section plane. Each plane is (n.xyz, d) and clips where
|
||||
// dot(n, world) + d > 0. Both the main and pick fragments discard with
|
||||
// this predicate so cuts are visible AND consistent with selection.
|
||||
fn is_section_clipped(world: vec3<f32>) -> bool {
|
||||
let n = u_frame.clip_count;
|
||||
if (n == 0) { return false; }
|
||||
for (var i = 0; i < n; i = i + 1) {
|
||||
let p = u_frame.clip_planes[i];
|
||||
if (dot(p.xyz, world) + p.w > 0.0) { return true; }
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
struct VisibleDraw {
|
||||
mesh_id: u32,
|
||||
instance_idx: u32,
|
||||
ebo_first_u32: u32,
|
||||
base_vertex: u32,
|
||||
};
|
||||
|
||||
struct PerModel {
|
||||
draw_count: u32,
|
||||
total_vertex_count: u32,
|
||||
_pad0: u32,
|
||||
_pad1: u32,
|
||||
};
|
||||
|
||||
@group(0) @binding(0) var<uniform> u_frame: FrameUniforms;
|
||||
// Selection flags indexed by object_id. bit 0 = in selection, bit 1 = active.
|
||||
// Sized to next_object_id_ on the CPU side; out-of-range reads can't happen
|
||||
// because we cap the index by arrayLength before fetching.
|
||||
@group(0) @binding(1) var<storage, read> sel_flags: 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(2) var<storage, read> instances: array<InstanceRecord>;
|
||||
@group(1) @binding(3) var<storage, read> indices: array<u32>;
|
||||
@group(1) @binding(4) var<storage, read> visible_draws: array<VisibleDraw>;
|
||||
@group(1) @binding(5) var<storage, read> prefix_sums: array<u32>;
|
||||
@group(1) @binding(6) var<uniform> u_model: PerModel;
|
||||
|
||||
struct VsOut {
|
||||
@builtin(position) clip_pos: vec4<f32>,
|
||||
@location(0) normal: vec3<f32>,
|
||||
@location(1) color: vec4<f32>,
|
||||
@location(2) world_pos: vec3<f32>,
|
||||
@location(3) @interpolate(flat) object_id: u32,
|
||||
};
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Binary search for the largest i in [0, draw_count) with prefix_sums[i] <= vid.
|
||||
// prefix_sums is monotonic non-decreasing and contains draw_count+1 entries
|
||||
// (prefix_sums[draw_count] == total_vertex_count).
|
||||
fn find_draw(vid: u32) -> u32 {
|
||||
var lo: u32 = 0u;
|
||||
var hi: u32 = u_model.draw_count;
|
||||
while (lo + 1u < hi) {
|
||||
let mid = (lo + hi) >> 1u;
|
||||
if (prefix_sums[mid] <= vid) {
|
||||
lo = mid;
|
||||
} else {
|
||||
hi = mid;
|
||||
}
|
||||
}
|
||||
return lo;
|
||||
}
|
||||
|
||||
@vertex
|
||||
fn vs_main(@builtin(vertex_index) vid: u32) -> VsOut {
|
||||
// Saturate past the end (shouldn't happen given draw() count, but safe).
|
||||
if (vid >= u_model.total_vertex_count) {
|
||||
var degen: VsOut;
|
||||
degen.clip_pos = vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
||||
return degen;
|
||||
}
|
||||
|
||||
let draw_idx = find_draw(vid);
|
||||
let local_v = vid - prefix_sums[draw_idx];
|
||||
let item = visible_draws[draw_idx];
|
||||
|
||||
// Fetch the mesh-local index then the global vertex index.
|
||||
let mesh_local_index = indices[item.ebo_first_u32 + local_v];
|
||||
let v_global = item.base_vertex + mesh_local_index;
|
||||
|
||||
let inst = instances[item.instance_idx];
|
||||
let mq = meshes[item.mesh_id];
|
||||
|
||||
let w0 = vertices[v_global * 3u + 0u];
|
||||
let w1 = vertices[v_global * 3u + 1u];
|
||||
let w2 = vertices[v_global * 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;
|
||||
out.object_id = inst.object_id;
|
||||
return out;
|
||||
}
|
||||
|
||||
// sRGB decode — used to undo wgpu's automatic linear→sRGB write encoding
|
||||
// on swap-chain BGRA8Unorm so the final bytes match what the GL backend
|
||||
// writes directly. The GL pipeline outputs to a non-sRGB FB and treats
|
||||
// every colour input as already-linear, so its bytes are exactly its
|
||||
// shader outputs. wgpu on the same swap chain auto-encodes, which makes
|
||||
// everything appear ~3× brighter unless we pre-decode once.
|
||||
fn srgbToLinear(s: vec3<f32>) -> vec3<f32> {
|
||||
let lo = s / 12.92;
|
||||
let hi = pow((s + 0.055) / 1.055, vec3<f32>(2.4));
|
||||
return select(hi, lo, s <= vec3<f32>(0.04045));
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_main(in: VsOut) -> @location(0) vec4<f32> {
|
||||
if (is_section_clipped(in.world_pos)) { discard; }
|
||||
|
||||
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;
|
||||
|
||||
var color = in.color.xyz * (ambient + (key + fill) * 0.7);
|
||||
|
||||
// Cavity shading: where adjacent fragments have a sharp normal change
|
||||
// (concave creases, edges where two faces meet), darken slightly so
|
||||
// shape boundaries read on flat-colour models. Matches the GL shader.
|
||||
let cavity = clamp(length(fwidth(n)) * 1.5, 0.0, 0.35);
|
||||
color = color * (1.0 - cavity);
|
||||
|
||||
// Selection tint. bit 0 = in selection (cool blue mix), bit 1 = active
|
||||
// (slightly stronger blue mix). Matches the GL main shader.
|
||||
if (in.object_id < arrayLength(&sel_flags)) {
|
||||
let flags = sel_flags[in.object_id];
|
||||
if ((flags & 1u) != 0u) { color = mix(color, vec3<f32>(0.2, 0.6, 1.0), 0.45); }
|
||||
if ((flags & 2u) != 0u) { color = mix(color, vec3<f32>(0.4, 0.8, 1.0), 0.40); }
|
||||
}
|
||||
|
||||
// Cancel the swap chain's implicit linear→sRGB encoding so the final
|
||||
// bytes match the GL backend (see srgbToLinear above). Alpha is
|
||||
// clamped to `xray_alpha_cap` (default 1.0 = no effect; X-ray sets
|
||||
// it to ~0.3) so a global translucency override lands without
|
||||
// touching any per-instance state.
|
||||
let alpha_out = min(in.color.a, u_frame.xray_alpha_cap);
|
||||
return vec4<f32>(srgbToLinear(color), alpha_out);
|
||||
}
|
||||
|
||||
// --------------------------- Pick pipeline ---------------------------------
|
||||
// Same vertex pulling as vs_main, but VsOutPick carries only the object_id
|
||||
// (flat-interpolated). Fragment writes the object_id to an R32UInt target.
|
||||
// Background (no draw) reads 0 because the pick attachment is cleared to 0.
|
||||
|
||||
struct VsOutPick {
|
||||
@builtin(position) clip_pos: vec4<f32>,
|
||||
@location(0) @interpolate(flat) object_id: u32,
|
||||
@location(1) world_pos: vec3<f32>,
|
||||
@location(2) normal: vec3<f32>,
|
||||
};
|
||||
|
||||
// Section tool needs the actual per-fragment normal (the AABB face was
|
||||
// too coarse for diagonal geometry). Two color attachments — R32UInt
|
||||
// object_id at @location(0), RGBA16F packed normal at @location(1).
|
||||
// We multiply-by-0.5+0.5 so unsigned half-floats keep the sign without
|
||||
// extra channel allocation.
|
||||
struct FsOutPick {
|
||||
@location(0) object_id: u32,
|
||||
@location(1) normal: vec4<f32>,
|
||||
};
|
||||
|
||||
@vertex
|
||||
fn vs_pick(@builtin(vertex_index) vid: u32) -> VsOutPick {
|
||||
var out: VsOutPick;
|
||||
if (vid >= u_model.total_vertex_count) {
|
||||
out.clip_pos = vec4<f32>(0.0, 0.0, 0.0, 0.0);
|
||||
out.object_id = 0u;
|
||||
out.world_pos = vec3<f32>(0.0, 0.0, 0.0);
|
||||
out.normal = vec3<f32>(0.0, 0.0, 1.0);
|
||||
return out;
|
||||
}
|
||||
|
||||
let draw_idx = find_draw(vid);
|
||||
let local_v = vid - prefix_sums[draw_idx];
|
||||
let item = visible_draws[draw_idx];
|
||||
let mesh_local_index = indices[item.ebo_first_u32 + local_v];
|
||||
let v_global = item.base_vertex + mesh_local_index;
|
||||
let inst = instances[item.instance_idx];
|
||||
let mq = meshes[item.mesh_id];
|
||||
|
||||
let w0 = vertices[v_global * 3u + 0u];
|
||||
let w1 = vertices[v_global * 3u + 1u];
|
||||
let pos_norm = vec3<f32>(
|
||||
f32(w0 & 0xFFFFu) / 65535.0,
|
||||
f32((w0 >> 16u) & 0xFFFFu) / 65535.0,
|
||||
f32(w1 & 0xFFFFu) / 65535.0,
|
||||
);
|
||||
let pos_local = mix(mq.aabb_min.xyz, mq.aabb_max.xyz, pos_norm);
|
||||
let world4 = inst.transform * vec4<f32>(pos_local, 1.0);
|
||||
|
||||
// Decode the same octahedral normal as vs_main — pick needs it so
|
||||
// the section tool can drop perpendicular cuts.
|
||||
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 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);
|
||||
|
||||
out.clip_pos = u_frame.view_proj * world4;
|
||||
out.object_id = inst.object_id;
|
||||
out.world_pos = world4.xyz;
|
||||
out.normal = n_final;
|
||||
return out;
|
||||
}
|
||||
|
||||
@fragment
|
||||
fn fs_pick(in: VsOutPick) -> FsOutPick {
|
||||
if (is_section_clipped(in.world_pos)) { discard; }
|
||||
var out: FsOutPick;
|
||||
out.object_id = in.object_id;
|
||||
// Pack signed normal into RGBA16F (unsigned-ish half range) as ×0.5+0.5.
|
||||
out.normal = vec4<f32>(normalize(in.normal) * 0.5 + vec3<f32>(0.5), 1.0);
|
||||
return out;
|
||||
}
|
||||
)";
|
||||
// MAIN_WGSL moved to ViewportCore.cpp (#84-k).
|
||||
|
||||
// Helper: build a WGPUStringView from a null-terminated C string literal.
|
||||
static WGPUStringView svFromCStr(const char* s) {
|
||||
@@ -619,7 +283,14 @@ ViewportWindow::ViewportWindow(QWindow* parent)
|
||||
camera_fov_y_deg_(core_.camera_fov_y_deg_),
|
||||
camera_near_ (core_.camera_near_),
|
||||
camera_far_ (core_.camera_far_),
|
||||
background_color_(core_.background_color_) {
|
||||
background_color_(core_.background_color_),
|
||||
frame_uniform_buffer_(core_.frame_uniform_buffer_),
|
||||
frame_bind_group_ (core_.frame_bind_group_),
|
||||
selection_flags_buffer_ (core_.selection_flags_buffer_),
|
||||
selection_flags_capacity_(core_.selection_flags_capacity_),
|
||||
selection_flags_scratch_ (core_.selection_flags_scratch_),
|
||||
selection_ (core_.selection_),
|
||||
visibility_ (core_.visibility_) {
|
||||
// wgpu doesn't need a GL context; we just need a real native window
|
||||
// whose backing layer matches the GPU API wgpu will drive.
|
||||
//
|
||||
@@ -5516,237 +5187,14 @@ void ViewportWindow::render() {
|
||||
// Pipeline + bind-group layouts (built once after init)
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
bool ViewportWindow::buildPipelines() {
|
||||
// ---- Bind group layouts ----------------------------------------------
|
||||
WGPUBindGroupLayoutEntry frame_entries[2] = {};
|
||||
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);
|
||||
frame_entries[1].binding = 1;
|
||||
frame_entries[1].visibility = WGPUShaderStage_Fragment;
|
||||
frame_entries[1].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
|
||||
// buildPipelines moved to ViewportCore (#84-k).
|
||||
bool ViewportWindow::buildPipelines() { return core_.buildPipelines(); }
|
||||
|
||||
WGPUBindGroupLayoutDescriptor frame_bgl_desc = {};
|
||||
frame_bgl_desc.entryCount = 2;
|
||||
frame_bgl_desc.entries = frame_entries;
|
||||
frame_bgl_desc.label = svFromCStr("ifcviewer-wgpu.frame_bgl");
|
||||
frame_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &frame_bgl_desc);
|
||||
// ensureSelectionFlagsBuffer moved to ViewportCore (#84-k).
|
||||
void ViewportWindow::ensureSelectionFlagsBuffer() { core_.ensureSelectionFlagsBuffer(); }
|
||||
|
||||
// 6 read-only storage buffers (vertices, meshes, instances, indices,
|
||||
// visible_draws, prefix_sums) + 1 uniform (per-model count). All read
|
||||
// in the vertex shader. WebGPU's mandatory min is 8 storage / 12 uniform
|
||||
// per stage, so we're comfortably under the cap.
|
||||
WGPUBindGroupLayoutEntry model_entries[7] = {};
|
||||
for (int i = 0; i < 6; ++i) {
|
||||
model_entries[i].binding = uint32_t(i);
|
||||
model_entries[i].visibility = WGPUShaderStage_Vertex;
|
||||
model_entries[i].buffer.type = WGPUBufferBindingType_ReadOnlyStorage;
|
||||
}
|
||||
model_entries[6].binding = 6;
|
||||
model_entries[6].visibility = WGPUShaderStage_Vertex;
|
||||
model_entries[6].buffer.type = WGPUBufferBindingType_Uniform;
|
||||
model_entries[6].buffer.minBindingSize = 16;
|
||||
WGPUBindGroupLayoutDescriptor model_bgl_desc = {};
|
||||
model_bgl_desc.entryCount = 7;
|
||||
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 = SAMPLE_COUNT;
|
||||
rp_desc.multisample.mask = 0xFFFFFFFFu;
|
||||
|
||||
main_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc);
|
||||
if (!main_pipeline_) {
|
||||
Log::warn() << "wgpu main render pipeline creation failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Transparent variant of the main pipeline ----------------------
|
||||
// Same shader, same layout, same vertex pulling, same depth test —
|
||||
// differs only in:
|
||||
// * depth.depthWriteEnabled = False (we still depth-test against
|
||||
// the opaque pass's z-buffer, but the transparent fragment's z
|
||||
// doesn't write, so further-back geometry behind the glass still
|
||||
// paints over)
|
||||
// * color_target.blend = SrcAlpha / OneMinusSrcAlpha (standard
|
||||
// porter-duff "over" — premultiplied wouldn't help because our
|
||||
// vertex colours come in straight-alpha from the IFC iterator)
|
||||
// No sort, no OIT — overlapping transparent surfaces of the same
|
||||
// kind will produce order-dependent artefacts but for typical IFC
|
||||
// glazing (panes that don't overlap much in screen space) the
|
||||
// result is "good enough".
|
||||
WGPUBlendState main_blend = {};
|
||||
main_blend.color.srcFactor = WGPUBlendFactor_SrcAlpha;
|
||||
main_blend.color.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
|
||||
main_blend.color.operation = WGPUBlendOperation_Add;
|
||||
main_blend.alpha.srcFactor = WGPUBlendFactor_One;
|
||||
main_blend.alpha.dstFactor = WGPUBlendFactor_OneMinusSrcAlpha;
|
||||
main_blend.alpha.operation = WGPUBlendOperation_Add;
|
||||
|
||||
WGPUColorTargetState color_target_transparent = color_target;
|
||||
color_target_transparent.blend = &main_blend;
|
||||
|
||||
WGPUFragmentState frag_transparent = frag;
|
||||
frag_transparent.targets = &color_target_transparent;
|
||||
|
||||
// depthWriteEnabled stays True so the edge-detect pass (which samples
|
||||
// depth_view_ to find silhouette discontinuities) can see window
|
||||
// panes — leaving it False made transparent surfaces invisible to
|
||||
// the edge detector, so windows ended up as edge-less "framed holes"
|
||||
// and the edges of opaque geometry behind the glass painted through
|
||||
// at full intensity. Trade-off: overlapping transparent surfaces
|
||||
// become depth-test-occluded by the closer one, increasing order
|
||||
// sensitivity. For BIM glass (panes that don't overlap in screen
|
||||
// space) this is invisible; for scenes where it matters, the right
|
||||
// fix is OIT or sort-by-distance, not turning depth write off.
|
||||
WGPUDepthStencilState depth_transparent = depth;
|
||||
|
||||
WGPURenderPipelineDescriptor rp_desc_t = rp_desc;
|
||||
rp_desc_t.label = svFromCStr("ifcviewer-wgpu.main_pipeline_transparent");
|
||||
rp_desc_t.fragment = &frag_transparent;
|
||||
rp_desc_t.depthStencil = &depth_transparent;
|
||||
|
||||
main_pipeline_transparent_ =
|
||||
wgpuDeviceCreateRenderPipeline(device_, &rp_desc_t);
|
||||
if (!main_pipeline_transparent_) {
|
||||
Log::warn() << "wgpu main transparent render pipeline creation failed";
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Per-frame uniform buffer ---------------------------------------
|
||||
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);
|
||||
|
||||
// frame_bind_group_ is built lazily once we have a selection_flags_
|
||||
// buffer to bind alongside the uniform — ensureSelectionFlagsBuffer
|
||||
// handles both the first creation and any subsequent resize.
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void ViewportWindow::ensureSelectionFlagsBuffer() {
|
||||
// Round up to at least 64 entries (256 B — minimum useful storage) and
|
||||
// grow geometrically when next_object_id_ outruns the current capacity.
|
||||
const uint32_t needed = std::max<uint32_t>(next_object_id_, 64);
|
||||
if (selection_flags_buffer_ && selection_flags_capacity_ >= needed) {
|
||||
if (!frame_bind_group_) {
|
||||
// First-time bind group creation after the buffer exists.
|
||||
// (Should always be true here.)
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// (Re)allocate. Geometric grow so we don't recreate every frame as a
|
||||
// big scene streams in.
|
||||
uint32_t new_cap = selection_flags_capacity_;
|
||||
if (new_cap < 64) new_cap = 64;
|
||||
while (new_cap < needed) new_cap *= 2;
|
||||
|
||||
if (!selection_flags_buffer_ || selection_flags_capacity_ < new_cap) {
|
||||
if (selection_flags_buffer_) {
|
||||
wgpuBufferRelease(selection_flags_buffer_);
|
||||
selection_flags_buffer_ = nullptr;
|
||||
}
|
||||
WGPUBufferDescriptor sb = {};
|
||||
sb.size = uint64_t(new_cap) * sizeof(uint32_t);
|
||||
sb.usage = WGPUBufferUsage_Storage | WGPUBufferUsage_CopyDst;
|
||||
sb.label = svFromCStr("ifcviewer-wgpu.selection_flags");
|
||||
selection_flags_buffer_ = wgpuDeviceCreateBuffer(device_, &sb);
|
||||
selection_flags_capacity_ = new_cap;
|
||||
// Initialise to zero so any unused range reads as "not selected".
|
||||
// wgpuQueueWriteBuffer with a small zero block is enough; the rest
|
||||
// is created as zero-initialised by wgpu per the spec.
|
||||
}
|
||||
|
||||
// Rebuild the frame bind group against the (possibly new) buffer.
|
||||
if (frame_bind_group_) {
|
||||
wgpuBindGroupRelease(frame_bind_group_);
|
||||
frame_bind_group_ = nullptr;
|
||||
}
|
||||
WGPUBindGroupEntry fbg_entries[2] = {};
|
||||
fbg_entries[0].binding = 0;
|
||||
fbg_entries[0].buffer = frame_uniform_buffer_;
|
||||
fbg_entries[0].size = sizeof(FrameUniforms);
|
||||
fbg_entries[1].binding = 1;
|
||||
fbg_entries[1].buffer = selection_flags_buffer_;
|
||||
fbg_entries[1].size = WGPU_WHOLE_SIZE;
|
||||
WGPUBindGroupDescriptor fbg_desc = {};
|
||||
fbg_desc.layout = frame_bgl_;
|
||||
fbg_desc.entryCount = 2;
|
||||
fbg_desc.entries = fbg_entries;
|
||||
fbg_desc.label = svFromCStr("ifcviewer-wgpu.frame_bind_group");
|
||||
frame_bind_group_ = wgpuDeviceCreateBindGroup(device_, &fbg_desc);
|
||||
|
||||
// Force a re-upload of the flags into the (possibly new) buffer.
|
||||
selection_flags_scratch_.assign(selection_flags_capacity_, 0);
|
||||
selection_.fillFlagsArray(selection_flags_scratch_, selection_flags_capacity_);
|
||||
wgpuQueueWriteBuffer(queue_, selection_flags_buffer_, 0,
|
||||
selection_flags_scratch_.data(),
|
||||
selection_flags_scratch_.size() * sizeof(uint32_t));
|
||||
selection_.markClean();
|
||||
}
|
||||
|
||||
void ViewportWindow::uploadSelectionFlagsIfDirty() {
|
||||
if (!selection_.dirty() || !selection_flags_buffer_) return;
|
||||
selection_flags_scratch_.assign(selection_flags_capacity_, 0);
|
||||
selection_.fillFlagsArray(selection_flags_scratch_, selection_flags_capacity_);
|
||||
wgpuQueueWriteBuffer(queue_, selection_flags_buffer_, 0,
|
||||
selection_flags_scratch_.data(),
|
||||
selection_flags_scratch_.size() * sizeof(uint32_t));
|
||||
selection_.markClean();
|
||||
}
|
||||
// uploadSelectionFlagsIfDirty moved to ViewportCore (#84-k).
|
||||
void ViewportWindow::uploadSelectionFlagsIfDirty() { core_.uploadSelectionFlagsIfDirty(); }
|
||||
|
||||
void ViewportWindow::buildModelBindGroup(ModelGpuData& m) {
|
||||
if (!m.mesh_storage || !m.instance_storage) {
|
||||
|
||||
@@ -658,25 +658,14 @@ private:
|
||||
WGPURenderPipeline& main_pipeline_;
|
||||
WGPURenderPipeline& main_pipeline_transparent_;
|
||||
|
||||
// Per-frame uniform (view-proj + lighting), bound at group 0.
|
||||
WGPUBuffer frame_uniform_buffer_ = nullptr;
|
||||
WGPUBindGroup frame_bind_group_ = nullptr;
|
||||
|
||||
// Selection flags storage buffer at group=0 binding=1. u32-per-object_id,
|
||||
// bit 0 = selected, bit 1 = active. Sized to next_object_id_ rounded up;
|
||||
// grows when a load pushes past the current capacity. Bound in the
|
||||
// frame bind group because object_ids are globally unique across models.
|
||||
WGPUBuffer selection_flags_buffer_ = nullptr;
|
||||
uint32_t selection_flags_capacity_ = 0; // number of u32 entries
|
||||
SelectionState selection_;
|
||||
std::vector<uint32_t> selection_flags_scratch_;
|
||||
|
||||
// Per-element visibility. Consulted in cullModelCpuCompute to drop
|
||||
// hidden instances before they're added to visible_draws — keeps
|
||||
// hidden geometry out of cost on every axis (no draw, no depth, no
|
||||
// pick). Mutated on the main thread between renders; cull workers
|
||||
// read concurrently which is safe as long as no concurrent writes.
|
||||
VisibilityState visibility_;
|
||||
// Frame uniforms + selection flags aliases (storage in core_).
|
||||
WGPUBuffer& frame_uniform_buffer_;
|
||||
WGPUBindGroup& frame_bind_group_;
|
||||
WGPUBuffer& selection_flags_buffer_;
|
||||
uint32_t& selection_flags_capacity_;
|
||||
std::vector<uint32_t>& selection_flags_scratch_;
|
||||
SelectionState& selection_;
|
||||
VisibilityState& visibility_;
|
||||
|
||||
// Depth attachment (4× MSAA), recreated on surface resize.
|
||||
WGPUTexture depth_texture_ = nullptr;
|
||||
@@ -690,7 +679,8 @@ private:
|
||||
WGPUTextureView msaa_color_view_ = nullptr;
|
||||
int msaa_w_ = 0;
|
||||
int msaa_h_ = 0;
|
||||
static constexpr uint32_t SAMPLE_COUNT = 4;
|
||||
// SAMPLE_COUNT moved to ViewportCore.h as kViewportSampleCount (#84-k).
|
||||
static constexpr uint32_t SAMPLE_COUNT = kViewportSampleCount;
|
||||
|
||||
// HiZ occlusion culling. After each frame's main render pass we
|
||||
// downsample MSAA depth into a small single-sample Depth32Float texture
|
||||
|
||||
Reference in New Issue
Block a user