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

+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.)