forked from 1b-agent/opencadstudio-upstream
fix(renderer): keep draw-order offsets within clipping planes
This commit is contained in:
1 parent
0e42e3bf28
commit
e52e565b1f
26 files changed
+525
-90
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.3 KiB |
@@ -0,0 +1,38 @@
|
||||
# Perspective draw-order clipping regression
|
||||
|
||||
Switching a flat model view from orthographic to perspective could hide otherwise
|
||||
visible lines. The camera applied an OpenGL-to-WebGPU depth conversion to a
|
||||
projection that already used WebGPU's depth range. The shaders then applied a
|
||||
fixed draw-order offset that could push geometry past the far clipping boundary.
|
||||
|
||||
The camera now uses the DirectX/WebGPU projection directly. The shared
|
||||
`draw_order.wgsl` function keeps the existing offset away from clipping planes
|
||||
and reduces it near either plane. Its adjustment is monotonic in draw order,
|
||||
leaves neutral geometric depth unchanged, and does not move clipped vertices
|
||||
back into range. It is shared by the native and compatibility shader paths.
|
||||
|
||||
## GPU reproduction
|
||||
|
||||
These are offscreen renders from the application's wire pipelines. All 17 lines
|
||||
are on the same plane; only their draw-order values differ. The first two lines
|
||||
(red when visible) were clipped in perspective. Both projections now show all
|
||||
17 lines. This synthetic drawing contains no user CAD geometry.
|
||||
|
||||
| Before | After |
|
||||
| --- | --- |
|
||||
|  |  |
|
||||
|
||||
## Validation
|
||||
|
||||
```sh
|
||||
cargo test --locked --lib
|
||||
cargo test --locked --lib scene::pipeline::depth_tests -- --ignored --nocapture
|
||||
```
|
||||
|
||||
The GPU tests exercise storage and packed wire pipelines, near/far clipping,
|
||||
coplanar draw order in both submission orders, and wire occlusion by a triangle
|
||||
mesh. Normal tests check camera depth endpoints, picking, and validation of all
|
||||
14 composed draw-order shaders.
|
||||
|
||||
Set `OCS_DEPTH_TEST_IMAGES` to an output directory to save the orthographic and
|
||||
perspective reproduction images while running the GPU tests.
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
|
||||
@@ -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
@@ -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"
|
||||
))),
|
||||
});
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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.)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user