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 <noreply@anthropic.com>
This commit is contained in:
Hakan SevenandClaude Sonnet 4.6 committed 2026-04-27 03:01:42 +03:00
1 parent 392f4503d8
commit c27927c2b3
2 files changed
+108 -1

No files matched your search

+95
View File
@@ -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<WireGpu>,
/// Ghost copies (25% alpha) of selected wires for the X-ray depth pass.
gpu_selected_wires: Vec<WireGpu>,
gpu_hatches: Vec<HatchGpu>,
/// Wipeout fills — rendered after wires in a separate pass.
gpu_wipeouts: Vec<HatchGpu>,
@@ -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.
+13 -1
View File
@@ -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,