diff --git a/src/ifcviewer/ViewportCore.cpp b/src/ifcviewer/ViewportCore.cpp index 314b6746c1..641f9b605a 100644 --- a/src/ifcviewer/ViewportCore.cpp +++ b/src/ifcviewer/ViewportCore.cpp @@ -3447,3 +3447,179 @@ void ViewportCore::releaseMsaaColorTexture() { if (msaa_color_texture_) { wgpuTextureRelease(msaa_color_texture_); msaa_color_texture_ = nullptr; } msaa_w_ = msaa_h_ = 0; } + +// =========================================================================== +// Edge silhouette post-process (#84-s): buildEdgePipeline + encodeEdgePass + +// releaseEdgeResources +// =========================================================================== + +namespace { + +const char* EDGE_WGSL = R"( +@group(0) @binding(0) var src_depth: texture_depth_multisampled_2d; + +const NEAR: f32 = 0.1; +const FAR: f32 = 10000.0; +const EDGE_SCALE: f32 = 6.0; +const EDGE_THRESHOLD: f32 = 0.004; + +// Depth texture stores [0,1] z (we pre-multiply a z-remap onto Qt's GL-style +// projection in the main pipeline). Convert back to GL-NDC then reverse- +// project to view-space distance. +fn linearise(z: f32) -> f32 { + let ndc = z * 2.0 - 1.0; + return (2.0 * NEAR * FAR) / (FAR + NEAR - ndc * (FAR - NEAR)); +} + +@vertex +fn vs_main(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4 { + let x = f32((vid << 1u) & 2u) * 2.0 - 1.0; + let y = f32(vid & 2u) * 2.0 - 1.0; + return vec4(x, y, 0.0, 1.0); +} + +@fragment +fn fs_main(@builtin(position) frag: vec4) -> @location(0) vec4 { + let p = vec2(i32(frag.x), i32(frag.y)); + let dim = vec2(textureDimensions(src_depth)); + + let dc_raw = textureLoad(src_depth, p, 0); + // Background pixels: nothing was drawn here. Skip so we don't draw + // edges on the void / sky. + if (dc_raw >= 0.99999) { discard; } + + let c = linearise(dc_raw); + let n = linearise(textureLoad(src_depth, vec2(p.x, max(p.y - 1, 0)), 0)); + let s = linearise(textureLoad(src_depth, vec2(p.x, min(p.y + 1, dim.y - 1)), 0)); + let e = linearise(textureLoad(src_depth, vec2(min(p.x + 1, dim.x - 1), p.y), 0)); + let w = linearise(textureLoad(src_depth, vec2(max(p.x - 1, 0), p.y), 0)); + + let lap = abs(4.0 * c - n - s - e - w); + let t = EDGE_THRESHOLD * c; + let edge = clamp((lap - t) * EDGE_SCALE, 0.0, 0.6); + + // Multiplicative blend (Dst, Zero): output rgb = (1 - edge), so the + // existing surface colour is multiplied by (1 - edge) per channel. + return vec4(vec3(1.0 - edge), 1.0); +} +)"; + +} // namespace + +bool ViewportCore::buildEdgePipeline() { + WGPUBindGroupLayoutEntry entries[1] = {}; + entries[0].binding = 0; + entries[0].visibility = WGPUShaderStage_Fragment; + entries[0].texture.sampleType = WGPUTextureSampleType_Depth; + entries[0].texture.viewDimension = WGPUTextureViewDimension_2D; + entries[0].texture.multisampled = 1; + + WGPUBindGroupLayoutDescriptor bgl_desc = {}; + bgl_desc.entryCount = 1; + bgl_desc.entries = entries; + bgl_desc.label = svFromCStr("ifcviewer-wgpu.edge_bgl"); + edge_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); + + WGPUPipelineLayoutDescriptor pl_desc = {}; + pl_desc.bindGroupLayoutCount = 1; + pl_desc.bindGroupLayouts = &edge_bgl_; + pl_desc.label = svFromCStr("ifcviewer-wgpu.edge_pipeline_layout"); + edge_pipeline_layout_ = wgpuDeviceCreatePipelineLayout(device_, &pl_desc); + + WGPUShaderSourceWGSL wgsl_src = {}; + wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; + wgsl_src.code = svFromCStr(EDGE_WGSL); + WGPUShaderModuleDescriptor sm_desc = {}; + sm_desc.nextInChain = &wgsl_src.chain; + sm_desc.label = svFromCStr("ifcviewer-wgpu.edge_wgsl"); + edge_shader_module_ = wgpuDeviceCreateShaderModule(device_, &sm_desc); + + // Multiplicative blend (Dst, Zero): out.rgb = src.rgb * dst.rgb. + // Fragment outputs (1 - edge, 1 - edge, 1 - edge) so the existing + // surface colour is scaled per-channel — strictly darkens, never + // brightens. Matches GL's renderEdgePass (GL_DST_COLOR, GL_ZERO). + WGPUBlendState blend = {}; + blend.color.srcFactor = WGPUBlendFactor_Dst; + blend.color.dstFactor = WGPUBlendFactor_Zero; + blend.color.operation = WGPUBlendOperation_Add; + blend.alpha.srcFactor = WGPUBlendFactor_Zero; + blend.alpha.dstFactor = WGPUBlendFactor_One; + blend.alpha.operation = WGPUBlendOperation_Add; + + WGPUColorTargetState target = {}; + target.format = surface_format_; + target.blend = &blend; + target.writeMask = WGPUColorWriteMask_All; + + WGPUFragmentState frag = {}; + frag.module = edge_shader_module_; + frag.entryPoint = svFromCStr("fs_main"); + frag.targetCount = 1; + frag.targets = ⌖ + + WGPURenderPipelineDescriptor rp_desc = {}; + rp_desc.layout = edge_pipeline_layout_; + rp_desc.label = svFromCStr("ifcviewer-wgpu.edge_pipeline"); + rp_desc.vertex.module = edge_shader_module_; + rp_desc.vertex.entryPoint = svFromCStr("vs_main"); + rp_desc.vertex.bufferCount = 0; + rp_desc.fragment = &frag; + rp_desc.depthStencil = nullptr; // no depth attachment + rp_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList; + rp_desc.primitive.cullMode = WGPUCullMode_None; + rp_desc.multisample.count = 1; + rp_desc.multisample.mask = 0xFFFFFFFFu; + + edge_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc); + if (!edge_pipeline_) { + Log::warn() << "wgpu edge pipeline creation failed"; + return false; + } + return true; +} + +void ViewportCore::encodeEdgePass(WGPUCommandEncoder enc, + WGPUTextureView surface_view) { + if (!edges_enabled_ || !edge_pipeline_ || !depth_view_ || !surface_view) return; + + // Rebuild lazily when the underlying depth view was replaced (on + // resize we proactively null this alongside the HiZ bind group). + if (!edge_bind_group_) { + WGPUBindGroupEntry entry = {}; + entry.binding = 0; + entry.textureView = depth_view_; + WGPUBindGroupDescriptor bg = {}; + bg.layout = edge_bgl_; + bg.entryCount = 1; + bg.entries = &entry; + bg.label = svFromCStr("ifcviewer-wgpu.edge_bind_group"); + edge_bind_group_ = wgpuDeviceCreateBindGroup(device_, &bg); + } + + WGPURenderPassColorAttachment color = {}; + color.view = surface_view; + color.loadOp = WGPULoadOp_Load; + color.storeOp = WGPUStoreOp_Store; + color.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; + + WGPURenderPassDescriptor pass_desc = {}; + pass_desc.colorAttachmentCount = 1; + pass_desc.colorAttachments = &color; + pass_desc.depthStencilAttachment = nullptr; + pass_desc.label = svFromCStr("ifcviewer-wgpu.edge_pass"); + + WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(enc, &pass_desc); + wgpuRenderPassEncoderSetPipeline(pass, edge_pipeline_); + wgpuRenderPassEncoderSetBindGroup(pass, 0, edge_bind_group_, 0, nullptr); + wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); + wgpuRenderPassEncoderEnd(pass); + wgpuRenderPassEncoderRelease(pass); +} + +void ViewportCore::releaseEdgeResources() { + if (edge_bind_group_) { wgpuBindGroupRelease(edge_bind_group_); edge_bind_group_ = nullptr; } + if (edge_pipeline_) { wgpuRenderPipelineRelease(edge_pipeline_); edge_pipeline_ = nullptr; } + if (edge_shader_module_) { wgpuShaderModuleRelease(edge_shader_module_);edge_shader_module_ = nullptr; } + if (edge_pipeline_layout_) { wgpuPipelineLayoutRelease(edge_pipeline_layout_); edge_pipeline_layout_ = nullptr; } + if (edge_bgl_) { wgpuBindGroupLayoutRelease(edge_bgl_); edge_bgl_ = nullptr; } +} diff --git a/src/ifcviewer/ViewportCore.h b/src/ifcviewer/ViewportCore.h index e326742a39..170678258f 100644 --- a/src/ifcviewer/ViewportCore.h +++ b/src/ifcviewer/ViewportCore.h @@ -375,6 +375,22 @@ public: void ensureMsaaColorTexture(int w, int h); void releaseMsaaColorTexture(); + // ---- Edge silhouette post-process (#84-s) ----------------------------- + // + // Build the edge pipeline + shader + BGL. Run after initWgpu's + // device is up. Returns false on pipeline creation failure. + bool buildEdgePipeline(); + + // Encode the edge silhouette fullscreen pass into the supplied + // command encoder. No-op when edges_enabled_ is false or the + // pipeline / depth view / surface view is null. Lazily builds the + // bind group on first call after a surface resize. + void encodeEdgePass(WGPUCommandEncoder enc, WGPUTextureView surface_view); + + // Tear down the edge pipeline + bind group + supporting state. + // Called from shutdown() before the device dies. + void releaseEdgeResources(); + // ---- Cull (#84-p) ----------------------------------------------------- // // Per-instance occlusion test, supplied by the caller. Wired by @@ -535,6 +551,8 @@ private: WGPUBindGroupLayout edge_bgl_ = nullptr; WGPUPipelineLayout edge_pipeline_layout_ = nullptr; WGPURenderPipeline edge_pipeline_ = nullptr; + WGPUBindGroup edge_bind_group_ = nullptr; + bool edges_enabled_ = true; // Pick pass. Reuses pipeline_layout_ — same set of bindings as the // main pass since the pick fragment also vertex-pulls instance data. diff --git a/src/ifcviewer/ViewportWindow.cpp b/src/ifcviewer/ViewportWindow.cpp index 7052ef9792..61012afe19 100644 --- a/src/ifcviewer/ViewportWindow.cpp +++ b/src/ifcviewer/ViewportWindow.cpp @@ -241,6 +241,8 @@ ViewportWindow::ViewportWindow(QWindow* parent) edge_bgl_ (core_.edge_bgl_), edge_pipeline_layout_ (core_.edge_pipeline_layout_), edge_pipeline_ (core_.edge_pipeline_), + edge_bind_group_ (core_.edge_bind_group_), + edges_enabled_ (core_.edges_enabled_), pick_pipeline_(core_.pick_pipeline_), pool_ (core_.pool_), streaming_thread_(core_.streaming_thread_), @@ -742,7 +744,7 @@ bool ViewportWindow::initWgpu() { // init haven't migrated yet) ------------------------------------- if (!buildPipelines()) return false; if (!core_.buildHizPipeline()) return false; - if (!buildEdgePipeline()) return false; + if (!core_.buildEdgePipeline()) return false; if (!overlays_.init(instance_, device_, queue_, surface_format_, SAMPLE_COUNT)) { Log::warn() << "OverlayRenderer init failed"; return false; @@ -999,164 +1001,11 @@ void ViewportWindow::configureSurface(int width_px, int height_px) { // camera near/far are hard-coded to the viewport defaults (0.1 / 10000). // They'll move to a small uniform when AppSettings ports over. -static const char* EDGE_WGSL = R"( -@group(0) @binding(0) var src_depth: texture_depth_multisampled_2d; +// EDGE_WGSL moved to ViewportCore.cpp anon namespace (#84-s). -const NEAR: f32 = 0.1; -const FAR: f32 = 10000.0; -const EDGE_SCALE: f32 = 6.0; -const EDGE_THRESHOLD: f32 = 0.004; +// buildEdgePipeline moved to ViewportCore (#84-s). -// Depth texture stores [0,1] z (we pre-multiply a z-remap onto Qt's GL-style -// projection in the main pipeline). Convert back to GL-NDC then reverse- -// project to view-space distance. -fn linearise(z: f32) -> f32 { - let ndc = z * 2.0 - 1.0; - return (2.0 * NEAR * FAR) / (FAR + NEAR - ndc * (FAR - NEAR)); -} - -@vertex -fn vs_main(@builtin(vertex_index) vid: u32) -> @builtin(position) vec4 { - let x = f32((vid << 1u) & 2u) * 2.0 - 1.0; - let y = f32(vid & 2u) * 2.0 - 1.0; - return vec4(x, y, 0.0, 1.0); -} - -@fragment -fn fs_main(@builtin(position) frag: vec4) -> @location(0) vec4 { - let p = vec2(i32(frag.x), i32(frag.y)); - let dim = vec2(textureDimensions(src_depth)); - - let dc_raw = textureLoad(src_depth, p, 0); - // Background pixels: nothing was drawn here. Skip so we don't draw - // edges on the void / sky. - if (dc_raw >= 0.99999) { discard; } - - let c = linearise(dc_raw); - let n = linearise(textureLoad(src_depth, vec2(p.x, max(p.y - 1, 0)), 0)); - let s = linearise(textureLoad(src_depth, vec2(p.x, min(p.y + 1, dim.y - 1)), 0)); - let e = linearise(textureLoad(src_depth, vec2(min(p.x + 1, dim.x - 1), p.y), 0)); - let w = linearise(textureLoad(src_depth, vec2(max(p.x - 1, 0), p.y), 0)); - - let lap = abs(4.0 * c - n - s - e - w); - let t = EDGE_THRESHOLD * c; - let edge = clamp((lap - t) * EDGE_SCALE, 0.0, 0.6); - - // Multiplicative blend (Dst, Zero): output rgb = (1 - edge), so the - // existing surface colour is multiplied by (1 - edge) per channel. - return vec4(vec3(1.0 - edge), 1.0); -} -)"; - -bool ViewportWindow::buildEdgePipeline() { - WGPUBindGroupLayoutEntry entries[1] = {}; - entries[0].binding = 0; - entries[0].visibility = WGPUShaderStage_Fragment; - entries[0].texture.sampleType = WGPUTextureSampleType_Depth; - entries[0].texture.viewDimension = WGPUTextureViewDimension_2D; - entries[0].texture.multisampled = 1; - - WGPUBindGroupLayoutDescriptor bgl_desc = {}; - bgl_desc.entryCount = 1; - bgl_desc.entries = entries; - bgl_desc.label = svFromCStr("ifcviewer-wgpu.edge_bgl"); - edge_bgl_ = wgpuDeviceCreateBindGroupLayout(device_, &bgl_desc); - - WGPUPipelineLayoutDescriptor pl_desc = {}; - pl_desc.bindGroupLayoutCount = 1; - pl_desc.bindGroupLayouts = &edge_bgl_; - pl_desc.label = svFromCStr("ifcviewer-wgpu.edge_pipeline_layout"); - edge_pipeline_layout_ = wgpuDeviceCreatePipelineLayout(device_, &pl_desc); - - WGPUShaderSourceWGSL wgsl_src = {}; - wgsl_src.chain.sType = WGPUSType_ShaderSourceWGSL; - wgsl_src.code = svFromCStr(EDGE_WGSL); - WGPUShaderModuleDescriptor sm_desc = {}; - sm_desc.nextInChain = &wgsl_src.chain; - sm_desc.label = svFromCStr("ifcviewer-wgpu.edge_wgsl"); - edge_shader_module_ = wgpuDeviceCreateShaderModule(device_, &sm_desc); - - // Multiplicative blend (Dst, Zero): out.rgb = src.rgb * dst.rgb. - // Fragment outputs (1 - edge, 1 - edge, 1 - edge) so the existing - // surface colour is scaled per-channel — strictly darkens, never - // brightens. Matches GL's renderEdgePass (GL_DST_COLOR, GL_ZERO). - WGPUBlendState blend = {}; - blend.color.srcFactor = WGPUBlendFactor_Dst; - blend.color.dstFactor = WGPUBlendFactor_Zero; - blend.color.operation = WGPUBlendOperation_Add; - blend.alpha.srcFactor = WGPUBlendFactor_Zero; - blend.alpha.dstFactor = WGPUBlendFactor_One; - blend.alpha.operation = WGPUBlendOperation_Add; - - WGPUColorTargetState target = {}; - target.format = surface_format_; - target.blend = &blend; - target.writeMask = WGPUColorWriteMask_All; - - WGPUFragmentState frag = {}; - frag.module = edge_shader_module_; - frag.entryPoint = svFromCStr("fs_main"); - frag.targetCount = 1; - frag.targets = ⌖ - - WGPURenderPipelineDescriptor rp_desc = {}; - rp_desc.layout = edge_pipeline_layout_; - rp_desc.label = svFromCStr("ifcviewer-wgpu.edge_pipeline"); - rp_desc.vertex.module = edge_shader_module_; - rp_desc.vertex.entryPoint = svFromCStr("vs_main"); - rp_desc.vertex.bufferCount = 0; - rp_desc.fragment = &frag; - rp_desc.depthStencil = nullptr; // no depth attachment - rp_desc.primitive.topology = WGPUPrimitiveTopology_TriangleList; - rp_desc.primitive.cullMode = WGPUCullMode_None; - rp_desc.multisample.count = 1; - rp_desc.multisample.mask = 0xFFFFFFFFu; - - edge_pipeline_ = wgpuDeviceCreateRenderPipeline(device_, &rp_desc); - if (!edge_pipeline_) { - Log::warn() << "wgpu edge pipeline creation failed"; - return false; - } - return true; -} - -void ViewportWindow::encodeEdgePass(WGPUCommandEncoder enc, - WGPUTextureView surface_view) { - if (!edges_enabled_ || !edge_pipeline_ || !depth_view_ || !surface_view) return; - - // Rebuild lazily when the underlying depth view was replaced (on resize - // we proactively null this out alongside the HiZ bind group). - if (!edge_bind_group_) { - WGPUBindGroupEntry entry = {}; - entry.binding = 0; - entry.textureView = depth_view_; - WGPUBindGroupDescriptor bg = {}; - bg.layout = edge_bgl_; - bg.entryCount = 1; - bg.entries = &entry; - bg.label = svFromCStr("ifcviewer-wgpu.edge_bind_group"); - edge_bind_group_ = wgpuDeviceCreateBindGroup(device_, &bg); - } - - WGPURenderPassColorAttachment color = {}; - color.view = surface_view; - color.loadOp = WGPULoadOp_Load; // preserve resolved main-pass colour - color.storeOp = WGPUStoreOp_Store; - color.depthSlice = WGPU_DEPTH_SLICE_UNDEFINED; - - WGPURenderPassDescriptor pass_desc = {}; - pass_desc.colorAttachmentCount = 1; - pass_desc.colorAttachments = &color; - pass_desc.depthStencilAttachment = nullptr; - pass_desc.label = svFromCStr("ifcviewer-wgpu.edge_pass"); - - WGPURenderPassEncoder pass = wgpuCommandEncoderBeginRenderPass(enc, &pass_desc); - wgpuRenderPassEncoderSetPipeline(pass, edge_pipeline_); - wgpuRenderPassEncoderSetBindGroup(pass, 0, edge_bind_group_, 0, nullptr); - wgpuRenderPassEncoderDraw(pass, 3, 1, 0, 0); - wgpuRenderPassEncoderEnd(pass); - wgpuRenderPassEncoderRelease(pass); -} +// encodeEdgePass moved to ViewportCore (#84-s). // ----------------------------------------------------------------------------- void ViewportWindow::setPivotIndicatorVisible(bool visible, int hide_after_ms) { @@ -1177,13 +1026,7 @@ void ViewportWindow::setPivotIndicatorVisible(bool visible, int hide_after_ms) { } requestUpdate(); } -void ViewportWindow::releaseEdgeResources() { - if (edge_bind_group_) { wgpuBindGroupRelease(edge_bind_group_); edge_bind_group_ = nullptr; } - if (edge_pipeline_) { wgpuRenderPipelineRelease(edge_pipeline_); edge_pipeline_ = nullptr; } - if (edge_shader_module_) { wgpuShaderModuleRelease(edge_shader_module_);edge_shader_module_ = nullptr; } - if (edge_pipeline_layout_) { wgpuPipelineLayoutRelease(edge_pipeline_layout_); edge_pipeline_layout_ = nullptr; } - if (edge_bgl_) { wgpuBindGroupLayoutRelease(edge_bgl_); edge_bgl_ = nullptr; } -} +// releaseEdgeResources moved to ViewportCore (#84-s). // ----------------------------------------------------------------------------- // Pick pipeline (stage 4) @@ -2896,7 +2739,7 @@ void ViewportWindow::render() { // lines onto the resolved surface colour. Encoded before HiZ resolve // so HiZ uses the same MSAA depth that produced the edges. if (edges_enabled_) { - encodeEdgePass(enc, view); + core_.encodeEdgePass(enc, view); } // Corner axis gizmo. Encoded after the edge pass on the resolved @@ -4453,7 +4296,7 @@ void ViewportWindow::shutdown() { core_.releaseDepthTexture(); core_.releaseMsaaColorTexture(); core_.releaseHizResources(); - releaseEdgeResources(); + core_.releaseEdgeResources(); overlays_.destroy(); releasePickResources(); diff --git a/src/ifcviewer/ViewportWindow.h b/src/ifcviewer/ViewportWindow.h index f8f2e73fa9..92b08d779f 100644 --- a/src/ifcviewer/ViewportWindow.h +++ b/src/ifcviewer/ViewportWindow.h @@ -670,8 +670,8 @@ private: WGPUBindGroupLayout& edge_bgl_; WGPUPipelineLayout& edge_pipeline_layout_; WGPURenderPipeline& edge_pipeline_; - WGPUBindGroup edge_bind_group_ = nullptr; - bool edges_enabled_ = true; + WGPUBindGroup& edge_bind_group_; + bool& edges_enabled_; // Pivot visibility state — the gizmo itself lives in overlays_. // The timer auto-hides the pivot after a wheel-zoom afterglow.