GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: per-draw wire tint + depth-biased wireframe pipeline
SceneDraw gains wire_tint (rgb + mix, zero = untouched), carried
through the per-draw uniform block into scene3d's fragment mix — a
wireframe pass overlaid on its own filled mesh needs it, since the
lines inherit the mesh colors and would vanish into the identical
fill. The wireframe pipeline gets a small negative depth bias so those
lines win the LESS depth test against the coplanar fill instead of
z-fighting it.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/vk/scene.rs | 15 +++++++++++++++
src/vk/scene3d.wgsl | 7 ++++++-
2 files changed, 21 insertions(+), 1 deletion(-)
diff --git a/src/vk/scene.rs b/src/vk/scene.rs
index c0a60e0..078246b 100644
--- a/src/vk/scene.rs
+++ b/src/vk/scene.rs
@@ -35,6 +35,12 @@ pub struct SceneDraw {
/// Rasterize as lines (PolygonMode::LINE) instead of filled triangles.
/// Falls back to filled when the device lacks fillModeNonSolid.
pub wireframe: bool,
+ /// rgb + mix: the fragment color is mixed toward `wire_tint.rgb` by
+ /// `wire_tint[3]`. Zero = vertex colors untouched (the default draw).
+ /// A wireframe pass overlaid on its own filled mesh needs this — the
+ /// lines inherit the mesh's colors and would otherwise vanish into the
+ /// identical fill beneath.
+ pub wire_tint: [f32; 4],
}
/// shader_3d.wgsl's uniform block.
@@ -45,6 +51,7 @@ struct SceneUniforms {
window_size: [f32; 2],
window_radius: f32,
corner_shape: f32,
+ wire_tint: [f32; 4],
}
const UNIFORM_SIZE: vk::DeviceSize = std::mem::size_of::<SceneUniforms>() as vk::DeviceSize;
@@ -282,11 +289,18 @@ impl SceneStage {
// The wireframe twin: identical but rasterized as lines. Culling
// stays on so the wire view matches the fill's visible surface.
+ // A small negative depth bias pulls the lines toward the viewer,
+ // so a wire pass drawn over its own filled mesh (the overlay
+ // mode) wins the LESS depth test instead of z-fighting the
+ // coplanar fill.
let wireframe_pipeline = wireframe_supported.then(|| {
let rasterization_lines = vk::PipelineRasterizationStateCreateInfo::default()
.polygon_mode(vk::PolygonMode::LINE)
.cull_mode(vk::CullModeFlags::BACK)
.front_face(vk::FrontFace::COUNTER_CLOCKWISE)
+ .depth_bias_enable(true)
+ .depth_bias_constant_factor(-2.0)
+ .depth_bias_slope_factor(-1.0)
.line_width(1.0);
device
.create_graphics_pipelines(
@@ -652,6 +666,7 @@ impl SceneStage {
window_size,
window_radius: corner_radius_px,
corner_shape,
+ wire_tint: draw.wire_tint,
};
let offset = (self.uniform_stride as usize) * i;
mapped[offset..offset + UNIFORM_SIZE as usize]
diff --git a/src/vk/scene3d.wgsl b/src/vk/scene3d.wgsl
index 8bd7e14..d06179b 100644
--- a/src/vk/scene3d.wgsl
+++ b/src/vk/scene3d.wgsl
@@ -5,6 +5,10 @@ struct Uniforms {
// Corner-shape exponent shared with shader2d: circular arc at 2,
// superellipse squircle above.
corner_shape: f32,
+ // rgb + mix: fragment color mixed toward .rgb by .a. Zero = vertex
+ // colors untouched; a wireframe pass overlaid on its own filled mesh
+ // sets it so the lines separate from the identical fill beneath.
+ wire_tint: vec4<f32>,
}
@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@@ -67,5 +71,6 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4f {
if (cov <= 0.0) {
discard;
}
- return vec4f(in.color, cov);
+ let rgb = mix(in.color, uniforms.wire_tint.rgb, uniforms.wire_tint.a);
+ return vec4f(rgb, cov);
}