From c27927c2b39bad5d648b21320060ffa5e528f149 Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Mon, 27 Apr 2026 03:01:42 +0300 Subject: [PATCH] fix: X-ray ghost pass for selected wires occluded by other geometry Selected wires are now visible through occluding objects. A second wire_xray pipeline (depth_compare=Greater, depth_write_enabled=false) renders 25%-alpha ghost copies of selected wires only where they are behind the depth buffer, leaving fully-visible portions at full glow brightness unchanged. Co-Authored-By: Claude Sonnet 4.6 --- src/scene/pipeline/mod.rs | 95 ++++++++++++++++++++++++++++++++++ src/scene/pipeline/wire_gpu.rs | 14 ++++- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/src/scene/pipeline/mod.rs b/src/scene/pipeline/mod.rs index 410871a08..1d7456654 100644 --- a/src/scene/pipeline/mod.rs +++ b/src/scene/pipeline/mod.rs @@ -25,6 +25,9 @@ const MSAA_SAMPLES: u32 = 4; pub struct Pipeline { wire_pipeline: wgpu::RenderPipeline, + /// Same shader as wire_pipeline but depth_compare=Greater, depth_write_enabled=false. + /// Used to draw ghost copies of selected wires through occluding geometry. + wire_xray_pipeline: wgpu::RenderPipeline, hatch_pipeline: wgpu::RenderPipeline, image_pipeline: wgpu::RenderPipeline, mesh_pipeline: wgpu::RenderPipeline, @@ -46,6 +49,8 @@ pub struct Pipeline { /// Cached texture format (needed to recreate MSAA / depth textures on resize). surface_format: wgpu::TextureFormat, gpu_wires: Vec, + /// Ghost copies (25% alpha) of selected wires for the X-ray depth pass. + gpu_selected_wires: Vec, gpu_hatches: Vec, /// Wipeout fills — rendered after wires in a separate pass. gpu_wipeouts: Vec, @@ -148,6 +153,49 @@ impl Pipeline { cache: None, }); + // Xray variant: only draws where fragments are BEHIND the depth buffer + // (depth_compare=Greater), without writing depth. Used to ghost selected + // wires through occluding geometry at reduced alpha. + let wire_xray_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("wire_xray.pipeline"), + layout: Some(&wire_layout), + vertex: wgpu::VertexState { + module: &wire_shader, + entry_point: Some("vs_main"), + buffers: &[wire_gpu::WireVertex::layout()], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }, + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + cull_mode: None, + ..Default::default() + }, + depth_stencil: Some(wgpu::DepthStencilState { + format: wgpu::TextureFormat::Depth32Float, + depth_write_enabled: false, + depth_compare: wgpu::CompareFunction::Greater, + stencil: wgpu::StencilState::default(), + bias: wgpu::DepthBiasState::default(), + }), + multisample: wgpu::MultisampleState { + count: MSAA_SAMPLES, + mask: !0, + alpha_to_coverage_enabled: false, + }, + fragment: Some(wgpu::FragmentState { + module: &wire_shader, + entry_point: Some("fs_main"), + targets: &[Some(wgpu::ColorTargetState { + format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + compilation_options: wgpu::PipelineCompilationOptions::default(), + }), + multiview: None, + cache: None, + }); + // ── Hatch pipeline ───────────────────────────────────────────────── let hatch_entry = |binding: u32| wgpu::BindGroupLayoutEntry { binding, @@ -457,6 +505,7 @@ impl Pipeline { Self { wire_pipeline, + wire_xray_pipeline, hatch_pipeline, image_pipeline, mesh_pipeline, @@ -474,6 +523,7 @@ impl Pipeline { blit_bind_group, surface_format: format, gpu_wires: vec![], + gpu_selected_wires: vec![], gpu_hatches: vec![], gpu_wipeouts: vec![], gpu_images: vec![], @@ -485,6 +535,13 @@ impl Pipeline { pub fn upload_wires(&mut self, device: &wgpu::Device, wires: &[WireModel]) { self.gpu_wires = wires.iter().map(|w| WireGpu::new(device, w)).collect(); + // Ghost copies of selected wires at 25% alpha, drawn through occluding + // geometry in the xray pass to keep selected entities fully visible. + self.gpu_selected_wires = wires + .iter() + .filter(|w| w.selected) + .map(|w| WireGpu::new_ghost(device, w, 0.25)) + .collect(); } pub fn upload_meshes(&mut self, device: &wgpu::Device, meshes: &[MeshModel]) { @@ -716,6 +773,44 @@ impl Pipeline { } } + // ── Pass 7: selected wire X-ray ghost pass ──────────────────────── + // Draws ghost copies of selected wires only where they are occluded + // (depth_compare=Greater). Visible parts are already drawn at full + // brightness by the wire pass; this only reveals hidden portions. + if !self.gpu_selected_wires.is_empty() { + let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("wire_xray.render_pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: msaa, + depth_slice: None, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }, + })], + depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment { + view: &self.depth_view, + depth_ops: Some(wgpu::Operations { + load: wgpu::LoadOp::Load, + store: wgpu::StoreOp::Store, + }), + stencil_ops: None, + }), + timestamp_writes: None, + occlusion_query_set: None, + }); + pass.set_viewport(0.0, 0.0, vp.width as f32, vp.height as f32, 0.0, 1.0); + pass.set_pipeline(&self.wire_xray_pipeline); + pass.set_bind_group(0, &self.uniform_bind_group, &[]); + for wire in &self.gpu_selected_wires { + if wire.vertex_count >= 6 { + pass.set_vertex_buffer(0, wire.vertex_buffer.slice(..)); + pass.draw(0..wire.vertex_count, 0..1); + } + } + } + // ── Resolve MSAA → clip-sized resolve texture ───────────────────── // Both msaa_view and resolve_view are sized to clip_bounds, so the // resolve does NOT touch any pixels outside the shader widget's area. diff --git a/src/scene/pipeline/wire_gpu.rs b/src/scene/pipeline/wire_gpu.rs index 9d9cbf47d..54718d0c8 100644 --- a/src/scene/pipeline/wire_gpu.rs +++ b/src/scene/pipeline/wire_gpu.rs @@ -115,6 +115,18 @@ pub struct WireGpu { impl WireGpu { pub fn new(device: &wgpu::Device, wire: &WireModel) -> Self { + Self::build(device, wire, wire.color) + } + + /// Creates a ghost copy with `alpha` applied on top of the wire's own alpha. + /// Used for the X-ray pass: selected wires rendered at reduced opacity through + /// occluding geometry. + pub fn new_ghost(device: &wgpu::Device, wire: &WireModel, alpha: f32) -> Self { + let [r, g, b, a] = wire.color; + Self::build(device, wire, [r, g, b, a * alpha]) + } + + fn build(device: &wgpu::Device, wire: &WireModel, color: [f32; 4]) -> Self { let pat0 = [ wire.pattern[0], wire.pattern[1], @@ -178,7 +190,7 @@ impl WireGpu { pos_b: b, which_end, side, - color: wire.color, + color, distance: dist, half_width, pattern_length: wire.pattern_length,