fix(renderer): keep draw-order offsets within clipping planes

This commit is contained in:
Christopher Overbeck committed 2026-09-15 12:36:05 -05:00
1 parent 0e42e3bf28
commit e52e565b1f
26 files changed
+525 -90

No files matched your search

+2 -2
View File
@@ -113,7 +113,7 @@ pub fn create_pipelines(
) -> (wgpu::RenderPipeline, wgpu::RenderPipeline) {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("circle.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/circle.wgsl").into()),
source: wgpu::ShaderSource::Wgsl(draw_order_shader!("circle.wgsl").into()),
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("circle.pipeline.layout"),
@@ -349,7 +349,7 @@ mod tests {
#[test]
fn circle_shader_validates_with_naga() {
let source = include_str!("../../shaders/circle.wgsl");
let source = draw_order_shader!("circle.wgsl");
let module = naga::front::wgsl::parse_str(source).expect("circle.wgsl parses cleanly");
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
+370
View File
@@ -0,0 +1,370 @@
//! Pixel regressions for projection and draw-order clipping, using the actual
//! wire pipelines in both storage and WebGL-compatible packed modes.
use super::*;
use crate::scene::view::camera::{Camera, Projection};
use iced::futures::executor::block_on;
const SIZE: u32 = 512;
fn pixels(
device: &wgpu::Device,
queue: &wgpu::Queue,
pipeline: &mut Pipeline,
uniforms: &Uniforms,
wires: &[WireModel],
depths: &rustc_hash::FxHashMap<u64, [f32; 2]>,
) -> Vec<u8> {
let size = wgpu::Extent3d {
width: SIZE,
height: SIZE,
depth_or_array_layers: 1,
};
let target = device.create_texture(&wgpu::TextureDescriptor {
label: Some("draw-order regression"),
size,
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
view_formats: &[],
});
let view = target.create_view(&Default::default());
let readback = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("draw-order readback"),
size: (SIZE * SIZE * 4) as u64,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
pipeline.ensure_depth_texture(device, Size::new(SIZE, SIZE));
pipeline.upload_uniforms(device, queue, uniforms);
pipeline.gpu_wires = std::sync::Arc::new(WireGpu::from_batch(
device,
queue,
wires,
depths,
pipeline.wire_const_bgl.as_ref(),
));
let mut encoder = device.create_command_encoder(&Default::default());
let rect = Rectangle {
x: 0,
y: 0,
width: SIZE,
height: SIZE,
};
pipeline.render(
&mut encoder,
&view,
Rectangle::with_size(Size::new(SIZE as f32, SIZE as f32)),
rect,
rect,
[0., 0., 0., 1.],
false,
false,
false,
);
encoder.copy_texture_to_buffer(
wgpu::TexelCopyTextureInfo {
texture: &target,
mip_level: 0,
origin: wgpu::Origin3d::ZERO,
aspect: wgpu::TextureAspect::All,
},
wgpu::TexelCopyBufferInfo {
buffer: &readback,
layout: wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(SIZE * 4),
rows_per_image: Some(SIZE),
},
},
size,
);
queue.submit([encoder.finish()]);
let (tx, rx) = std::sync::mpsc::channel();
readback.slice(..).map_async(wgpu::MapMode::Read, move |r| {
tx.send(r).unwrap();
});
device.poll(wgpu::PollType::wait_indefinitely()).unwrap();
rx.recv().unwrap().unwrap();
let result = readback.slice(..).get_mapped_range().to_vec();
readback.unmap();
result
}
fn color_at(bytes: &[u8], ndc_x: f32) -> [u32; 3] {
let x = ((ndc_x * 0.5 + 0.5) * SIZE as f32) as usize;
let mut color = [0; 3];
for col in x - 2..=x + 2 {
let offset = (SIZE as usize / 2 * SIZE as usize + col) * 4;
for c in 0..3 {
color[c] += bytes[offset + c] as u32;
}
}
color
}
#[test]
#[ignore = "requires a GPU adapter"]
fn perspective_draw_order_keeps_flat_lines_visible() {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = block_on(instance.request_adapter(&Default::default())).expect("GPU adapter");
let bounds = Rectangle::with_size(Size::new(SIZE as f32, SIZE as f32));
let mut failures = Vec::new();
for packed in [false, true] {
let mut limits = adapter.limits();
if packed {
limits.max_storage_buffers_per_shader_stage = 0;
}
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_limits: limits,
..Default::default()
}))
.expect("GPU device");
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
let mut pipeline = Pipeline::new(&device, &queue, wgpu::TextureFormat::Rgba8Unorm);
for projection in [Projection::Orthographic, Projection::Perspective] {
let camera = Camera {
distance: 102.03651,
fov_y: 0.47108996,
rotation: glam::Quat::IDENTITY,
target: glam::DVec3::ZERO,
projection,
..Camera::default()
};
// Same 17-entity draw-order labels as the reported drawing. Its
// missing line (45EB) occupies index 1 with depth -0.5794896.
let depths = (0..17)
.map(|i| {
(
i + 1,
[
(-800_000_000_i64 + 88_888_888 * (i + 1) as i64) as f32
/ 1_073_741_824.,
0.,
],
)
})
.collect();
let wires: Vec<_> = (0..17)
.map(|i| {
let x = (-0.8 + i as f32 * 0.1) * camera.ortho_size();
let h = camera.ortho_size() * 0.65;
WireModel::solid(
(i + 1).to_string(),
vec![[x, -h, 0.], [x, h, 0.]],
if i < 2 { [1., 0.3, 0.2, 1.] } else { [1.; 4] },
false,
)
})
.collect();
let bytes = pixels(
&device,
&queue,
&mut pipeline,
&Uniforms::new(&camera, bounds, false),
&wires,
&depths,
);
if let Ok(path) = std::env::var("OCS_DEPTH_TEST_IMAGES") {
let path = std::path::Path::new(&path);
std::fs::create_dir_all(path).unwrap();
image::save_buffer(
path.join(format!(
"{projection:?}-{}.png",
if packed { "packed" } else { "storage" }
)),
&bytes,
SIZE,
SIZE,
image::ColorType::Rgba8,
)
.unwrap();
}
for i in 0..17 {
if color_at(&bytes, -0.8 + i as f32 * 0.1) == [0; 3] {
failures.push(format!("{projection:?}, packed={packed}, line {i}"));
}
}
}
assert!(
block_on(validation.pop()).is_none(),
"GPU validation failed"
);
}
assert!(
failures.is_empty(),
"visible lines were clipped: {failures:?}"
);
}
#[test]
fn all_draw_order_shaders_validate() {
for source in [
draw_order_shader!("wire.wgsl"),
draw_order_shader!("wire_indexed.wgsl"),
draw_order_shader!("block_wire.wgsl"),
draw_order_shader!("block_wire_storage.wgsl"),
draw_order_shader!("circle.wgsl"),
draw_order_shader!("ellipse.wgsl"),
draw_order_shader!("text.wgsl"),
draw_order_shader!("block_text.wgsl"),
draw_order_shader!("face3d.wgsl"),
draw_order_shader!("block_face3d.wgsl"),
draw_order_shader!("hatch.wgsl"),
draw_order_shader!("hatch_texture.wgsl"),
draw_order_shader!("image.wgsl"),
draw_order_shader!("wipeout.wgsl"),
] {
let module = naga::front::wgsl::parse_str(source).expect("composed shader parses");
naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
naga::valid::Capabilities::all(),
)
.validate(&module)
.expect("composed shader validates");
}
}
#[test]
#[ignore = "requires a GPU adapter"]
fn draw_order_preserves_clip_boundaries_sorting_and_mesh_occlusion() {
use crate::scene::model::mesh_model::{MeshLodSet, MeshModel};
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
let adapter = block_on(instance.request_adapter(&Default::default())).expect("GPU adapter");
for packed in [false, true] {
let mut limits = adapter.limits();
if packed {
limits.max_storage_buffers_per_shader_stage = 0;
}
let (device, queue) = block_on(adapter.request_device(&wgpu::DeviceDescriptor {
required_limits: limits,
..Default::default()
}))
.expect("GPU device");
let validation = device.push_error_scope(wgpu::ErrorFilter::Validation);
let mut pipeline = Pipeline::new(&device, &queue, wgpu::TextureFormat::Rgba8Unorm);
let bounds = Rectangle::with_size(Size::new(SIZE as f32, SIZE as f32));
let mut uniforms = Uniforms::new(&Camera::default(), bounds, false);
uniforms.view_rot = glam::Mat4::IDENTITY;
uniforms.eye_high = [0.; 3];
uniforms.eye_low = [0.; 3];
let line = |id: u64, x: f32, z: f32, color: [f32; 4]| {
WireModel::solid(
id.to_string(),
vec![[x, -0.65, z], [x, 0.65, z]],
color,
false,
)
};
// Both bias signs, the paper's -2 order, and already-clipped geometry.
let cases = [
(0.00001, 2., true),
(0.00001, -2., true),
(0.99999, 2., true),
(0.99999, -2., true),
(-0.00001, -2., false),
(1.00001, 2., false),
(0., 2., true),
(1., -2., true),
(0.5, 0., true),
];
let wires: Vec<_> = cases
.iter()
.enumerate()
.map(|(i, (z, _, _))| line(i as u64 + 1, -0.8 + i as f32 * 0.2, *z, [1.; 4]))
.collect();
let depths = cases
.iter()
.enumerate()
.map(|(i, (_, order, _))| (i as u64 + 1, [*order, 0.]))
.collect();
let bytes = pixels(&device, &queue, &mut pipeline, &uniforms, &wires, &depths);
for (i, (z, order, visible)) in cases.iter().enumerate() {
assert_eq!(
color_at(&bytes, -0.8 + i as f32 * 0.2) != [0; 3],
*visible,
"packed={packed}, depth={z}, order={order}"
);
}
// Coplanar draw order must work close to both planes, for negative
// ranks too, regardless of the order in which the GPU sees the wires.
for z in [0.0001, 0.5, 0.9999] {
for (back, front) in [(-0.7, 0.7), (-2., -0.9), (0.3, 0.8)] {
let depths = rustc_hash::FxHashMap::from_iter([(1, [back, 0.]), (2, [front, 0.])]);
let mut wires = [
line(1, 0., z, [1., 0., 0., 1.]),
line(2, 0., z, [0., 1., 0., 1.]),
];
for _ in 0..2 {
let bytes = pixels(&device, &queue, &mut pipeline, &uniforms, &wires, &depths);
let [red, green, _] = color_at(&bytes, 0.);
assert!(
green > 0 && red == 0,
"draw order reversed at z={z}, packed={packed}"
);
wires.reverse();
}
}
}
// A real triangle mesh still occludes a wire behind it. A wire in front
// stays visible: neutral draw order must retain geometric depth.
pipeline.upload_mesh_batch(
&device,
&queue,
&[MeshLodSet::from_single(MeshModel {
name: "100".into(),
verts: vec![
[-0.6, -0.8, 0.5],
[0.6, -0.8, 0.5],
[0.6, 0.8, 0.5],
[-0.6, 0.8, 0.5],
],
verts_low: Vec::new(),
normals: vec![[0., 0., 1.]; 4],
indices: vec![0, 1, 2, 0, 2, 3],
triangle_material_handles: Vec::new(),
triangle_colors: Vec::new(),
color: [0., 1., 0., 1.],
selected: false,
})],
);
let mesh_only = pixels(
&device,
&queue,
&mut pipeline,
&uniforms,
&[],
&Default::default(),
);
assert_ne!(
color_at(&mesh_only, -0.3),
[0; 3],
"mesh must produce pixels"
);
let bytes = pixels(
&device,
&queue,
&mut pipeline,
&uniforms,
&[
line(1, -0.3, 0.6, [1., 0., 0., 1.]),
line(2, 0.3, 0.4, [0., 0., 1., 1.]),
],
&Default::default(),
);
assert_eq!(
color_at(&bytes, -0.3),
color_at(&mesh_only, -0.3),
"mesh failed to occlude the rear wire, packed={packed}"
);
let [red, _, blue] = color_at(&bytes, 0.3);
assert!(blue > red, "mesh hid the front wire, packed={packed}");
assert!(
block_on(validation.pop()).is_none(),
"GPU validation failed"
);
}
}
+2 -2
View File
@@ -113,7 +113,7 @@ pub fn create_pipelines(
) -> (wgpu::RenderPipeline, wgpu::RenderPipeline) {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("ellipse.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/ellipse.wgsl").into()),
source: wgpu::ShaderSource::Wgsl(draw_order_shader!("ellipse.wgsl").into()),
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("ellipse.pipeline.layout"),
@@ -305,7 +305,7 @@ mod tests {
#[test]
fn ellipse_shader_validates_with_naga() {
let source = include_str!("../../shaders/ellipse.wgsl");
let source = draw_order_shader!("ellipse.wgsl");
let module = naga::front::wgsl::parse_str(source).expect("ellipse.wgsl parses cleanly");
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
+2 -2
View File
@@ -78,9 +78,9 @@ impl HatchGpu {
"hatch.texture.shader"
}),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(if uses_storage {
include_str!("../../../shaders/hatch.wgsl")
draw_order_shader!("hatch.wgsl")
} else {
include_str!("../../../shaders/hatch_texture.wgsl")
draw_order_shader!("hatch_texture.wgsl")
})),
});
let vertex_layouts = if uses_storage {
+29 -12
View File
@@ -1,5 +1,22 @@
// WGSL has no includes. Compose one shared draw-order function into each 2D
// shader at compile time, including the sources used by shader validation tests.
macro_rules! draw_order_shader {
($file:literal) => {
concat!(
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/src/shaders/draw_order.wgsl"
)),
"\n",
include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/src/shaders/", $file))
)
};
}
#[cfg(test)]
mod gpu_tests;
#[cfg(test)]
mod depth_tests;
mod device_capabilities;
pub mod circle_gpu;
pub mod ellipse_gpu;
@@ -614,18 +631,18 @@ impl Pipeline {
label: Some("wire.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(match wire_mode {
wire_gpu::WirePipelineMode::IndexedStorage => {
include_str!("../../shaders/wire_indexed.wgsl")
draw_order_shader!("wire_indexed.wgsl")
}
wire_gpu::WirePipelineMode::Packed => include_str!("../../shaders/wire.wgsl"),
wire_gpu::WirePipelineMode::Packed => draw_order_shader!("wire.wgsl"),
})),
});
let block_wire_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("block_wire.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(
if wire_mode.uses_storage() {
include_str!("../../shaders/block_wire_storage.wgsl")
draw_order_shader!("block_wire_storage.wgsl")
} else {
include_str!("../../shaders/block_wire.wgsl")
draw_order_shader!("block_wire.wgsl")
},
)),
});
@@ -1011,8 +1028,8 @@ impl Pipeline {
let wipeout_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("wipeout.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
"../../shaders/wipeout.wgsl"
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(draw_order_shader!(
"wipeout.wgsl"
))),
});
@@ -1826,14 +1843,14 @@ impl Pipeline {
// ── Face3D pipeline ────────────────────────────────────────────────
let face3d_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("face3d.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
"../../shaders/face3d.wgsl"
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(draw_order_shader!(
"face3d.wgsl"
))),
});
let block_face3d_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("block_face3d.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
"../../shaders/block_face3d.wgsl"
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(draw_order_shader!(
"block_face3d.wgsl"
))),
});
@@ -2038,8 +2055,8 @@ impl Pipeline {
let image_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("image.shader"),
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(include_str!(
"../../shaders/image.wgsl"
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(draw_order_shader!(
"image.wgsl"
))),
});
+2 -2
View File
@@ -341,7 +341,7 @@ pub fn create_pipelines(
) {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("text.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/text.wgsl").into()),
source: wgpu::ShaderSource::Wgsl(draw_order_shader!("text.wgsl").into()),
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("text.pipeline.layout"),
@@ -390,7 +390,7 @@ pub fn create_pipelines(
};
let block_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("block_text.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/block_text.wgsl").into()),
source: wgpu::ShaderSource::Wgsl(draw_order_shader!("block_text.wgsl").into()),
});
let create_block = |label, depth_write_enabled, depth_compare| {
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
+7 -7
View File
@@ -1460,7 +1460,7 @@ mod block_wire_storage_tests {
/// real device, so the stride is checked against the Rust record instead.
#[test]
fn segment_struct_matches_the_rust_record() {
let module = module(include_str!("../../shaders/block_wire_storage.wgsl"));
let module = module(draw_order_shader!("block_wire_storage.wgsl"));
validate(&module);
let mut layouter = naga::proc::Layouter::default();
layouter
@@ -1484,8 +1484,8 @@ mod block_wire_storage_tests {
/// fragment behaviour. Only the geometry source differs.
#[test]
fn both_block_wire_shaders_expose_the_same_entry_points() {
let packed = module(include_str!("../../shaders/block_wire.wgsl"));
let storage = module(include_str!("../../shaders/block_wire_storage.wgsl"));
let packed = module(draw_order_shader!("block_wire.wgsl"));
let storage = module(draw_order_shader!("block_wire_storage.wgsl"));
validate(&packed);
let names = |m: &naga::Module| {
let mut n: Vec<String> = m.entry_points.iter().map(|e| e.name.clone()).collect();
@@ -1498,8 +1498,8 @@ mod block_wire_storage_tests {
#[test]
fn both_block_wire_shaders_honor_fixed_screen_width() {
for source in [
include_str!("../../shaders/block_wire.wgsl"),
include_str!("../../shaders/block_wire_storage.wgsl"),
draw_order_shader!("block_wire.wgsl"),
draw_order_shader!("block_wire_storage.wgsl"),
] {
assert!(source.contains("if world_hw < 0.0"));
assert!(source.contains(
@@ -1510,7 +1510,7 @@ mod block_wire_storage_tests {
#[test]
fn wire_shader_validates_with_naga() {
let source = include_str!("../../shaders/wire.wgsl");
let source = draw_order_shader!("wire.wgsl");
let module = naga::front::wgsl::parse_str(source).expect("wire.wgsl parses cleanly");
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
@@ -1523,7 +1523,7 @@ mod block_wire_storage_tests {
#[test]
fn wire_indexed_shader_validates_with_naga() {
let source = include_str!("../../shaders/wire_indexed.wgsl");
let source = draw_order_shader!("wire_indexed.wgsl");
let module = naga::front::wgsl::parse_str(source).expect("wire_indexed.wgsl parses cleanly");
let mut validator = naga::valid::Validator::new(
naga::valid::ValidationFlags::all(),
+40 -15
View File
@@ -84,13 +84,6 @@ impl Default for Camera {
}
}
pub const OPENGL_TO_WGPU: Mat4 = glam::mat4(
glam::vec4(1.0, 0.0, 0.0, 0.0),
glam::vec4(0.0, 1.0, 0.0, 0.0),
glam::vec4(0.0, 0.0, 0.5, 0.0),
glam::vec4(0.0, 0.0, 0.5, 1.0),
);
impl Camera {
// ── Eye position ───────────────────────────────────────────────────────
@@ -238,13 +231,9 @@ impl Camera {
/// Orthographic near/far that CENTRE the target plane at ndc-z ≈ 0.5.
///
/// The draw-order depth bias shifts clip-z by ±`DRAW_ORDER_BIAS` (0.001).
/// The old range (`near = distance*0.001`, `far = distance*1000`) parked the
/// geometry at ndc-z ≈ 0.001 — right on the near plane — so a front-biased
/// entity landed exactly at z = 0 and got clipped the moment f32 rounding
/// (worse at high zoom) tipped it past the plane, making the drawing vanish.
/// A symmetric range gives the bias half the depth buffer of headroom on
/// each side; ortho permits a negative near.
/// A symmetric range gives draw-order offsets headroom on each side and
/// keeps the target away from both clipping planes. The shared shader
/// bounds those offsets near either plane; ortho permits a negative near.
fn ortho_depth_range(&self) -> (f32, f32) {
// Generous headroom based on the current screen size so that rotating any
// geometry visible on screen in 3D never penetrates the near/far planes.
@@ -296,7 +285,9 @@ impl Camera {
orthographic(-w, w, -h, h, near, far)
}
};
OPENGL_TO_WGPU * proj * view
// The DirectX projection functions already produce WebGPU's [0, 1]
// depth range. An OpenGL remap here would compress it to [0.5, 1].
proj * view
}
/// Project a world point to screen pixels with full f64 precision: the
@@ -798,6 +789,40 @@ pub fn yaw_pitch_to_quat(yaw: f32, pitch: f32, roll: f32) -> Quat {
mod tests {
use super::*;
#[test]
fn projection_uses_the_webgpu_depth_range_once() {
let bounds = Rectangle::with_size(iced::Size::new(800., 600.));
for projection in [Projection::Orthographic, Projection::Perspective] {
let camera = Camera {
projection,
rotation: Quat::IDENTITY,
..Camera::default()
};
let (near, far) = match projection {
Projection::Perspective => (camera.distance * 0.001, camera.distance * 1000.),
Projection::Orthographic => camera.ortho_depth_range(),
};
let matrix = camera.view_proj_rte(bounds);
let depth = |distance: f32| {
let clip = matrix * glam::vec4(0., 0., -distance, 1.);
clip.z / clip.w
};
assert!(depth(near).abs() < 1e-5, "{projection:?}: near must map to 0");
assert!(
(depth(far) - 1.).abs() < 1e-5,
"{projection:?}: far must map to 1"
);
assert!(
depth(near - (far - near) * 1e-7) < 0.,
"near clipping boundary moved"
);
assert!(depth(far * 2.) > 1., "far clipping boundary moved");
if projection == Projection::Orthographic {
assert!((depth(camera.distance) - 0.5).abs() < 1e-5);
}
}
}
/// A drawing 140 units wide carrying one entity 800 km below its plane must
/// still zoom to the 140 units — the outlier belongs to the depth range, not
/// to the zoom. (Sizing both from the 3-D diagonal made the drawing a dot.)
+1 -4
View File
@@ -28,16 +28,13 @@ struct VertexOut {
@location(0) color: vec4<f32>,
}
const DRAW_ORDER_BIAS: f32 = 0.001;
@vertex
fn vs_main(v: VertexIn) -> VertexOut {
var out: VertexOut;
let rel = (v.position + v.translation - u.eye_high)
+ (v.position_low + v.translation_low - u.eye_low);
out.clip_pos = u.view_rot * vec4<f32>(rel, 1.0);
out.clip_pos.z = out.clip_pos.z
- (v.source_depth + v.instance_depth) * DRAW_ORDER_BIAS * out.clip_pos.w;
out.clip_pos = apply_draw_order(out.clip_pos, v.source_depth + v.instance_depth);
out.color = v.color;
return out;
}
+1 -4
View File
@@ -32,16 +32,13 @@ struct VertOut {
@location(1) color: vec4<f32>,
}
const DRAW_ORDER_BIAS: f32 = 0.001;
@vertex
fn vs_main(v: VertIn) -> VertOut {
var out: VertOut;
let rel = (v.pos + v.translation - u.eye_high)
+ (v.pos_low + v.translation_low - u.eye_low);
out.clip_pos = u.view_rot * vec4<f32>(rel, 1.0);
out.clip_pos.z = out.clip_pos.z
- (v.source_depth + v.instance_depth) * DRAW_ORDER_BIAS * out.clip_pos.w;
out.clip_pos = apply_draw_order(out.clip_pos, v.source_depth + v.instance_depth);
out.uv = v.uv;
out.color = v.color;
return out;
+2 -3
View File
@@ -44,7 +44,6 @@ struct VertexIn {
@location(8) depth: vec2<f32>,
}
const DRAW_ORDER_BIAS: f32 = 0.001;
const MODEL_LINEWEIGHT_BOOST: f32 = 2.0;
const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
@@ -165,7 +164,7 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut
let pos_rel = mix(rel_a, rel_b, which_end);
let world_pos = pos_rel + perp_world * (eff_hw * side);
var clip_pos = u.view_rot * vec4<f32>(world_pos, 1.0);
clip_pos.z = clip_pos.z - in.depth.x * DRAW_ORDER_BIAS * clip_pos.w;
clip_pos = apply_draw_order(clip_pos, in.depth.x);
final_clip = clip_pos;
out_dist = mix(in.distances.x, in.distances.y, which_end);
@@ -192,7 +191,7 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut
+ direction * half_width * extension;
let ndc_offset = offset_px / (u.viewport_size * 0.5);
var clip_pos_out = clip_position + vec4<f32>(ndc_offset * clip_position.w, 0.0, 0.0);
clip_pos_out.z = clip_pos_out.z - in.depth.x * DRAW_ORDER_BIAS * clip_pos_out.w;
clip_pos_out = apply_draw_order(clip_pos_out, in.depth.x);
final_clip = clip_pos_out;
out_dist = mix(in.distances.x, in.distances.y, which_end)
+2 -3
View File
@@ -68,7 +68,6 @@ struct VertexIn {
@location(8) depth: vec2<f32>,
}
const DRAW_ORDER_BIAS: f32 = 0.001;
const MODEL_LINEWEIGHT_BOOST: f32 = 2.0;
const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
@@ -196,7 +195,7 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut
let pos_rel = mix(rel_a, rel_b, which_end);
let world_pos = pos_rel + perp_world * (eff_hw * side);
var clip_pos = u.view_rot * vec4<f32>(world_pos, 1.0);
clip_pos.z = clip_pos.z - in.depth.x * DRAW_ORDER_BIAS * clip_pos.w;
clip_pos = apply_draw_order(clip_pos, in.depth.x);
final_clip = clip_pos;
out_dist = mix(seg_distances.x, seg_distances.y, which_end);
@@ -223,7 +222,7 @@ fn vs_main(@builtin(vertex_index) vertex_index: u32, in: VertexIn) -> VertexOut
+ direction * half_width * extension;
let ndc_offset = offset_px / (u.viewport_size * 0.5);
var clip_pos_out = clip_position + vec4<f32>(ndc_offset * clip_position.w, 0.0, 0.0);
clip_pos_out.z = clip_pos_out.z - in.depth.x * DRAW_ORDER_BIAS * clip_pos_out.w;
clip_pos_out = apply_draw_order(clip_pos_out, in.depth.x);
final_clip = clip_pos_out;
out_dist = mix(seg_distances.x, seg_distances.y, which_end)
+1 -2
View File
@@ -26,7 +26,6 @@ struct Uniforms {
}
@group(0) @binding(0) var<uniform> u: Uniforms;
const DRAW_ORDER_BIAS: f32 = 0.001;
const MODEL_LINEWEIGHT_BOOST: f32 = 2.0;
const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
const TAU: f32 = 6.283185307179586;
@@ -116,7 +115,7 @@ fn resolve_px_hw(px_hw: f32) -> f32 {
let world_pos_rel = center_rel + (u_val * axis_x + v_val * axis_y) * ext;
var clip_pos = u.view_rot * vec4<f32>(world_pos_rel, 1.0);
clip_pos.z = clip_pos.z - draw_depth * DRAW_ORDER_BIAS * clip_pos.w;
clip_pos = apply_draw_order(clip_pos, draw_depth);
var out: VertexOut;
out.clip_pos = clip_pos;
+13
View File
@@ -0,0 +1,13 @@
// Positive order draws in front; zero retains geometric depth. Keep the usual
// 0.001 NDC offset away from the clipping planes, but reduce it near either
// plane: perspective depth can be much closer to 1 than this fixed offset.
//
// Using order / (1 + abs(order)) for the limited region keeps ordering monotonic
// (including the paper background's -2). Less than half the remaining depth is
// consumed, so valid vertices stay valid. Out-of-range/behind-eye vertices get
// no offset and remain subject to the real near/far clipping planes.
fn apply_draw_order(clip: vec4<f32>, order: f32) -> vec4<f32> {
let room = 0.5 * max(0.0, min(clip.z, clip.w - clip.z));
let scale = min(0.001 * max(clip.w, 0.0), room / (1.0 + abs(order)));
return vec4<f32>(clip.xy, clip.z - order * scale, clip.w);
}
+1 -2
View File
@@ -21,7 +21,6 @@ struct Uniforms {
}
@group(0) @binding(0) var<uniform> u: Uniforms;
const DRAW_ORDER_BIAS: f32 = 0.001;
const MODEL_LINEWEIGHT_BOOST: f32 = 2.0;
const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
const TAU: f32 = 6.283185307179586;
@@ -98,7 +97,7 @@ fn resolve_hw(px_hw: f32) -> f32 {
let world_pos_rel = center_rel + (u_val * ext_u) * u_axis + (v_val * ext_v) * v_axis;
var clip_pos = u.view_rot * vec4<f32>(world_pos_rel, 1.0);
clip_pos.z = clip_pos.z - draw_depth * DRAW_ORDER_BIAS * clip_pos.w;
clip_pos = apply_draw_order(clip_pos, draw_depth);
var out: VertexOut;
out.clip_pos = clip_pos;
+2 -3
View File
@@ -34,17 +34,16 @@ struct VertexOut {
@location(0) color: vec4<f32>,
};
// Draw-order depth bias (see wire.wgsl). Signed draw_depth; 0.0 (all 3D
// Draw-order depth bias (see draw_order.wgsl). Signed draw_depth; 0.0 (all 3D
// surface faces — 3DFACE, PolyfaceMesh, PolygonMesh) leaves real depth
// untouched so they occlude against solids; only 2D fills order by rank.
const DRAW_ORDER_BIAS: f32 = 0.001;
@vertex
fn vs_main(v: VertexIn) -> VertexOut {
var out: VertexOut;
let rel = (v.position - u.eye_high) + (v.position_low - u.eye_low);
out.clip_pos = u.view_rot * vec4<f32>(rel, 1.0);
out.clip_pos.z = out.clip_pos.z - v.draw_depth * DRAW_ORDER_BIAS * out.clip_pos.w;
out.clip_pos = apply_draw_order(out.clip_pos, v.draw_depth);
out.color = v.color;
return out;
}
+1 -5
View File
@@ -47,9 +47,6 @@ struct HatchInstance {
grad_kind: u32, // shape (0=linear,1=cyl,2=sph,3=hemi,4=curved), bit4=invert
}
// Draw-order depth bias (see wire.wgsl). Higher draw_depth → smaller z →
// drawn on top, ordering this fill against other entity types.
const DRAW_ORDER_BIAS: f32 = 0.001;
const MODEL_LINEWEIGHT_BOOST: f32 = 2.0;
const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
@@ -122,8 +119,7 @@ struct VOut {
local.y + inst.world_origin_low.y + v.translation_low.y - u.eye_low.y,
-u.eye_low.z);
o.clip = u.view_rot * vec4<f32>(hi + lo, 1.0);
o.clip.z = o.clip.z
- (inst.draw_depth + v.draw_depth) * DRAW_ORDER_BIAS * o.clip.w;
o.clip = apply_draw_order(o.clip, inst.draw_depth + v.draw_depth);
o.xz = local;
o.instance_index = v.instance_index;
return o;
+1 -1
View File
@@ -110,7 +110,7 @@ struct VOut {
v.pos.y + h.origin_low.y + v.translation_low.y - u.eye_low.y,
v.pos.z - u.eye_low.z);
o.clip = u.view_rot * vec4<f32>(hi + lo, 1.0);
o.clip.z = o.clip.z - v.draw_depth * 0.001 * o.clip.w;
o.clip = apply_draw_order(o.clip, v.draw_depth);
o.xz = vec2<f32>(v.pos.x, v.pos.y);
return o;
}
+1 -5
View File
@@ -33,9 +33,6 @@ struct ImageParams {
};
@group(1) @binding(2) var<uniform> img_params: ImageParams;
// Draw-order depth bias (see wire.wgsl).
const DRAW_ORDER_BIAS: f32 = 0.001;
// ── Vertex stage ──────────────────────────────────────────────────────────────
struct VertIn {
@location(0) pos: vec3<f32>,
@@ -57,8 +54,7 @@ fn vs_main(in: VertIn) -> VertOut {
let rel = (in.pos + in.translation - u.eye_high)
+ (in.pos_low + in.translation_low - u.eye_low);
out.clip_pos = u.view_rot * vec4<f32>(rel, 1.0);
out.clip_pos.z = out.clip_pos.z
- (img_params.draw_depth + in.draw_depth) * DRAW_ORDER_BIAS * out.clip_pos.w;
out.clip_pos = apply_draw_order(out.clip_pos, img_params.draw_depth + in.draw_depth);
out.uv = in.uv;
return out;
}
+1 -4
View File
@@ -26,9 +26,6 @@ struct Uniforms {
@group(1) @binding(0) var atlas_tex: texture_2d<f32>;
@group(1) @binding(1) var atlas_samp: sampler;
// Draw-order depth bias (see wire.wgsl / image.wgsl).
const DRAW_ORDER_BIAS: f32 = 0.001;
// ── Vertex stage ──────────────────────────────────────────────────────────────
struct VertIn {
@location(0) pos: vec3<f32>,
@@ -50,7 +47,7 @@ fn vs_main(in: VertIn) -> VertOut {
// Double-single relative-to-eye, then rotation-only projection.
let rel = (in.pos - u.eye_high) + (in.pos_low - u.eye_low);
out.clip_pos = u.view_rot * vec4<f32>(rel, 1.0);
out.clip_pos.z = out.clip_pos.z - in.draw_depth * DRAW_ORDER_BIAS * out.clip_pos.w;
out.clip_pos = apply_draw_order(out.clip_pos, in.draw_depth);
out.uv = in.uv;
out.color = in.color;
return out;
+2 -2
View File
@@ -106,10 +106,10 @@ struct VOut {
let hi = h.plane_origin_high.xyz + v.translation - u.eye_high;
let lo = local_world + h.plane_origin_low.xyz + v.translation_low - u.eye_low;
o.clip = u.view_rot * vec4<f32>(hi + lo, 1.0);
// Draw-order bias (see wire.wgsl): the mask erases what draws BELOW its
// Draw-order bias (see draw_order.wgsl): the mask erases what draws BELOW its
// entity and loses to what draws above — including its own frame wire,
// which rides a +half-rank override.
o.clip.z = o.clip.z - (h.draw_depth + v.draw_depth) * 0.001 * o.clip.w;
o.clip = apply_draw_order(o.clip, h.draw_depth + v.draw_depth);
o.xz = vec2<f32>(v.pos.x, v.pos.y);
return o;
}
+2 -7
View File
@@ -67,11 +67,6 @@ struct InstanceIn {
@location(12) marker_normal_scale: vec4<f32>,
}
// Draw-order depth bias: shifts clip-space z so 2D entities of different
// types order against each other through the shared LessEqual depth test.
// draw_depth is signed (-1,1): front → positive → smaller z → drawn on top;
// 0.0 = neutral (real depth). Depth32Float gives ample precision.
const DRAW_ORDER_BIAS: f32 = 0.001;
const MODEL_LINEWEIGHT_BOOST: f32 = 2.0;
const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
@@ -199,7 +194,7 @@ fn marker_relative(position_high: vec3<f32>, position_low: vec3<f32>, instance:
let pos_rel = mix(rel_a, rel_b, which_end);
let world_pos = pos_rel + perp_world * (eff_hw * side);
var clip_pos = u.view_rot * vec4<f32>(world_pos, 1.0);
clip_pos.z = clip_pos.z - in.misc.x * DRAW_ORDER_BIAS * clip_pos.w;
clip_pos = apply_draw_order(clip_pos, in.misc.x);
final_clip = clip_pos;
out_dist = mix(in.dists.x, in.dists.y, which_end);
@@ -233,7 +228,7 @@ fn marker_relative(position_high: vec3<f32>, position_low: vec3<f32>, instance:
let offset_px = perp * hw * side + dir * hw * ext;
let ndc_offset = offset_px / (u.viewport_size * 0.5);
var clip_pos_out = clip_pos + vec4<f32>(ndc_offset * clip_pos.w, 0.0, 0.0);
clip_pos_out.z = clip_pos_out.z - in.misc.x * DRAW_ORDER_BIAS * clip_pos_out.w;
clip_pos_out = apply_draw_order(clip_pos_out, in.misc.x);
final_clip = clip_pos_out;
out_dist = mix(in.dists.x, in.dists.y, which_end) + ext * hw * u.world_per_pixel;
+2 -3
View File
@@ -53,7 +53,6 @@ struct InstanceIn {
@location(7) taper_ratio: vec2<f32>,
}
const DRAW_ORDER_BIAS: f32 = 0.001;
const MODEL_LINEWEIGHT_BOOST: f32 = 2.0;
const MODEL_LINEWEIGHT_MAX_PX: f32 = 10.0;
@@ -172,7 +171,7 @@ fn marker_relative(position_high: vec3<f32>, position_low: vec3<f32>, c: WireCon
let pos_rel = mix(rel_a, rel_b, which_end);
let world_pos = pos_rel + perp_world * (eff_hw * side);
var clip_pos = u.view_rot * vec4<f32>(world_pos, 1.0);
clip_pos.z = clip_pos.z - c.draw_depth * DRAW_ORDER_BIAS * clip_pos.w;
clip_pos = apply_draw_order(clip_pos, c.draw_depth);
final_clip = clip_pos;
out_dist = mix(in.distance_a, in.distance_b, which_end);
@@ -206,7 +205,7 @@ fn marker_relative(position_high: vec3<f32>, position_low: vec3<f32>, c: WireCon
let offset_px = perp * hw * side + dir * hw * ext;
let ndc_offset = offset_px / (u.viewport_size * 0.5);
var clip_pos_out = clip_pos + vec4<f32>(ndc_offset * clip_pos.w, 0.0, 0.0);
clip_pos_out.z = clip_pos_out.z - c.draw_depth * DRAW_ORDER_BIAS * clip_pos_out.w;
clip_pos_out = apply_draw_order(clip_pos_out, c.draw_depth);
final_clip = clip_pos_out;
out_dist = mix(in.distance_a, in.distance_b, which_end) + ext * hw * u.world_per_pixel;