mirror of
https://github.com/IfcOpenShell/IfcOpenShell.git
synced 2026-09-27 10:51:13 +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 "CameraMath.h"
|
||||||
#include "InstanceCompose.h"
|
#include "InstanceCompose.h"
|
||||||
|
#include "Log.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
// Orbit camera around target_. World +Z up (BIM convention). Yaw is
|
// Orbit camera around target_. World +Z up (BIM convention). Yaw is
|
||||||
@@ -543,3 +544,571 @@ ViewportCore::volumesPerObject(
|
|||||||
}
|
}
|
||||||
return out;
|
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 "InstanceCompose.h"
|
||||||
#include "InstancedGeometry.h"
|
#include "InstancedGeometry.h"
|
||||||
#include "ModelGpuData.h"
|
#include "ModelGpuData.h"
|
||||||
|
#include "SelectionState.h"
|
||||||
#include "StreamingThread.h"
|
#include "StreamingThread.h"
|
||||||
#include "ViewportHost.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 {
|
class ViewportCore {
|
||||||
public:
|
public:
|
||||||
@@ -171,6 +206,23 @@ public:
|
|||||||
std::vector<std::pair<uint32_t, double>>
|
std::vector<std::pair<uint32_t, double>>
|
||||||
volumesPerObject(const std::vector<uint32_t>& object_ids) const;
|
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
|
// Friend access for ViewportWindow's reference proxies. As each
|
||||||
// render method moves into ViewportCore it stops needing these
|
// render method moves into ViewportCore it stops needing these
|
||||||
// (it touches the fields directly); once everything has migrated
|
// (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.
|
// main pass since the pick fragment also vertex-pulls instance data.
|
||||||
WGPURenderPipeline pick_pipeline_ = nullptr;
|
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 ---------------------------------------------------------
|
// ---- Scene state ---------------------------------------------------------
|
||||||
//
|
//
|
||||||
// Sub-allocator for chunk vertex + index buffers. All per-chunk
|
// 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.
|
// std140-ish layout: every member naturally 16-aligned, struct stride = 96.
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
// Section-cutting cap. Single source of truth lives in OverlayRenderer
|
// kMaxSectionPlanes + FrameUniforms moved to ViewportCore.h (#84-k).
|
||||||
// so the visualizer and the WGSL clip array agree by construction.
|
// Keep this assert so OverlayRenderer's kMaxSectionPlanes (the section
|
||||||
static constexpr int kMaxSectionPlanes = OverlayRenderer::kMaxSectionPlanes;
|
// visualizer's per-plane uniform slot count) stays in sync with the
|
||||||
|
// WGSL clip array size.
|
||||||
struct FrameUniforms {
|
static_assert(kMaxSectionPlanes == OverlayRenderer::kMaxSectionPlanes,
|
||||||
float view_proj[16];
|
"section-plane cap must match OverlayRenderer's");
|
||||||
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");
|
|
||||||
|
|
||||||
// Inverse of sRGB encoding. wgpu-native's Vulkan swap chain on X11 treats
|
// Inverse of sRGB encoding. wgpu-native's Vulkan swap chain on X11 treats
|
||||||
// BGRA8Unorm as sRGB-output (encodes shader output linear→sRGB on write,
|
// BGRA8Unorm as sRGB-output (encodes shader output linear→sRGB on write,
|
||||||
@@ -240,326 +223,7 @@ static WGPUBuffer createBufferWithData(WGPUDevice device, WGPUQueue queue,
|
|||||||
// GPU.
|
// GPU.
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
static const char* MAIN_WGSL = R"(
|
// MAIN_WGSL moved to ViewportCore.cpp (#84-k).
|
||||||
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;
|
|
||||||
}
|
|
||||||
)";
|
|
||||||
|
|
||||||
// Helper: build a WGPUStringView from a null-terminated C string literal.
|
// Helper: build a WGPUStringView from a null-terminated C string literal.
|
||||||
static WGPUStringView svFromCStr(const char* s) {
|
static WGPUStringView svFromCStr(const char* s) {
|
||||||
@@ -619,7 +283,14 @@ ViewportWindow::ViewportWindow(QWindow* parent)
|
|||||||
camera_fov_y_deg_(core_.camera_fov_y_deg_),
|
camera_fov_y_deg_(core_.camera_fov_y_deg_),
|
||||||
camera_near_ (core_.camera_near_),
|
camera_near_ (core_.camera_near_),
|
||||||
camera_far_ (core_.camera_far_),
|
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
|
// wgpu doesn't need a GL context; we just need a real native window
|
||||||
// whose backing layer matches the GPU API wgpu will drive.
|
// 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)
|
// Pipeline + bind-group layouts (built once after init)
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
bool ViewportWindow::buildPipelines() {
|
// buildPipelines moved to ViewportCore (#84-k).
|
||||||
// ---- Bind group layouts ----------------------------------------------
|
bool ViewportWindow::buildPipelines() { return core_.buildPipelines(); }
|
||||||
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 = {};
|
// ensureSelectionFlagsBuffer moved to ViewportCore (#84-k).
|
||||||
frame_bgl_desc.entryCount = 2;
|
void ViewportWindow::ensureSelectionFlagsBuffer() { core_.ensureSelectionFlagsBuffer(); }
|
||||||
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,
|
// uploadSelectionFlagsIfDirty moved to ViewportCore (#84-k).
|
||||||
// visible_draws, prefix_sums) + 1 uniform (per-model count). All read
|
void ViewportWindow::uploadSelectionFlagsIfDirty() { core_.uploadSelectionFlagsIfDirty(); }
|
||||||
// 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();
|
|
||||||
}
|
|
||||||
|
|
||||||
void ViewportWindow::buildModelBindGroup(ModelGpuData& m) {
|
void ViewportWindow::buildModelBindGroup(ModelGpuData& m) {
|
||||||
if (!m.mesh_storage || !m.instance_storage) {
|
if (!m.mesh_storage || !m.instance_storage) {
|
||||||
|
|||||||
@@ -658,25 +658,14 @@ private:
|
|||||||
WGPURenderPipeline& main_pipeline_;
|
WGPURenderPipeline& main_pipeline_;
|
||||||
WGPURenderPipeline& main_pipeline_transparent_;
|
WGPURenderPipeline& main_pipeline_transparent_;
|
||||||
|
|
||||||
// Per-frame uniform (view-proj + lighting), bound at group 0.
|
// Frame uniforms + selection flags aliases (storage in core_).
|
||||||
WGPUBuffer frame_uniform_buffer_ = nullptr;
|
WGPUBuffer& frame_uniform_buffer_;
|
||||||
WGPUBindGroup frame_bind_group_ = nullptr;
|
WGPUBindGroup& frame_bind_group_;
|
||||||
|
WGPUBuffer& selection_flags_buffer_;
|
||||||
// Selection flags storage buffer at group=0 binding=1. u32-per-object_id,
|
uint32_t& selection_flags_capacity_;
|
||||||
// bit 0 = selected, bit 1 = active. Sized to next_object_id_ rounded up;
|
std::vector<uint32_t>& selection_flags_scratch_;
|
||||||
// grows when a load pushes past the current capacity. Bound in the
|
SelectionState& selection_;
|
||||||
// frame bind group because object_ids are globally unique across models.
|
VisibilityState& visibility_;
|
||||||
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_;
|
|
||||||
|
|
||||||
// Depth attachment (4× MSAA), recreated on surface resize.
|
// Depth attachment (4× MSAA), recreated on surface resize.
|
||||||
WGPUTexture depth_texture_ = nullptr;
|
WGPUTexture depth_texture_ = nullptr;
|
||||||
@@ -690,7 +679,8 @@ private:
|
|||||||
WGPUTextureView msaa_color_view_ = nullptr;
|
WGPUTextureView msaa_color_view_ = nullptr;
|
||||||
int msaa_w_ = 0;
|
int msaa_w_ = 0;
|
||||||
int msaa_h_ = 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
|
// HiZ occlusion culling. After each frame's main render pass we
|
||||||
// downsample MSAA depth into a small single-sample Depth32Float texture
|
// downsample MSAA depth into a small single-sample Depth32Float texture
|
||||||
|
|||||||
Reference in New Issue
Block a user