Skip to content

Commit 5045cbd

Browse files
committed
Fix orthographic projection rendering blank due to reversed depth ordering
When near > far was passed to Mat4::orthographic_rh, the positive z coefficient caused back faces to win depth testing, producing zero lighting output. Fix the original call site, add a runtime warning for reversed depth, document the near < far convention, and add a dedicated test for orthographic lit rendering.
1 parent e6e0a24 commit 5045cbd

5 files changed

Lines changed: 130 additions & 16 deletions

File tree

crates/renderling/src/bvol.rs

Lines changed: 71 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -490,8 +490,8 @@ impl BoundingSphere {
490490
}
491491
}
492492

493-
/// Returns an [`Aabb`] with x and y coordinates in viewport pixels and z coordinate
494-
/// in NDC depth.
493+
/// Returns an [`Aabb`] with x and y coordinates in viewport pixels and z
494+
/// coordinate in NDC depth.
495495
pub fn project_onto_viewport(&self, camera: &CameraDescriptor, viewport: Vec2) -> Aabb {
496496
fn ndc_to_pixel(viewport: Vec2, ndc: Vec3) -> Vec2 {
497497
let screen = Vec3::new((ndc.x + 1.0) * 0.5, 1.0 - (ndc.y + 1.0) * 0.5, ndc.z);
@@ -658,10 +658,10 @@ mod test {
658658
.with_msaa_sample_count(4)
659659
.with_lighting(true);
660660
let _camera = stage.new_camera().with_projection_and_view(
661-
// TODO: BUG - using orthographic here renderes nothing
662-
// Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, 10.0, -10.0),
663-
crate::camera::perspective(256.0, 256.0),
664-
Mat4::look_at_rh(Vec3::new(-3.0, 3.0, 5.0) * 0.5, Vec3::ZERO, Vec3::Y),
661+
// Fixed: was `near=10, far=-10` which reversed depth ordering.
662+
// Using `near=0.1, far=20` ensures correct depth testing.
663+
Mat4::orthographic_rh(-3.0, 3.0, -3.0, 3.0, 0.1, 20.0),
664+
Mat4::look_at_rh(Vec3::new(-3.0, 3.0, 5.0), Vec3::ZERO, Vec3::Y),
665665
);
666666
let _lights = crate::test::make_two_directional_light_setup(&stage);
667667

@@ -713,6 +713,71 @@ mod test {
713713
img_diff::assert_img_eq("bvol/bounding_box/get_mesh.png", img);
714714
}
715715

716+
/// Verifies that an orthographic projection with correct `near < far`
717+
/// produces visible lit output.
718+
///
719+
/// The original bug was that `orthographic_rh(-10, 10, -10, 10, 10, -10)`
720+
/// (note: `near=10 > far=-10`) reversed the depth mapping, causing back
721+
/// faces to win the depth test and produce zero lighting output.
722+
///
723+
/// The fix is to use `near < far`, matching the convention of glTF, wgpu,
724+
/// and every major rendering engine.
725+
#[test]
726+
fn orthographic_projection_lighting() {
727+
let ctx = Context::headless(256, 256).block();
728+
let view = Mat4::look_at_rh(Vec3::new(-3.0, 3.0, 5.0) * 0.5, Vec3::ZERO, Vec3::Y);
729+
730+
// Helper: render a lit unit cube and return the number of bright
731+
// pixels (any RGB channel > 10).
732+
let render = |projection: Mat4, label: &str| -> usize {
733+
let stage = ctx
734+
.new_stage()
735+
.with_background_color(Vec4::ZERO)
736+
.with_msaa_sample_count(4)
737+
.with_lighting(true);
738+
let _camera = stage
739+
.new_camera()
740+
.with_projection_and_view(projection, view);
741+
let _lights = crate::test::make_two_directional_light_setup(&stage);
742+
let white = stage.new_material().with_albedo_factor(Vec4::ONE);
743+
let _prim = stage.new_primitive().with_material(&white).with_vertices(
744+
stage.new_vertices(
745+
crate::math::unit_cube()
746+
.into_iter()
747+
.map(|(p, n)| Vertex::default().with_position(p).with_normal(n)),
748+
),
749+
);
750+
let frame = ctx.get_next_frame().unwrap();
751+
stage.render(&frame.view());
752+
let img = frame.read_image().block().unwrap();
753+
img_diff::save(&format!("bvol/ortho_lighting_{label}.png"), img.clone());
754+
let bright = img
755+
.pixels()
756+
.filter(|p| p.0[0] > 10 || p.0[1] > 10 || p.0[2] > 10)
757+
.count();
758+
bright
759+
};
760+
761+
// Correct orthographic: near=-10, far=10 (near < far)
762+
let ortho_bright = render(
763+
Mat4::orthographic_rh(-10.0, 10.0, -10.0, 10.0, -10.0, 10.0),
764+
"ortho_correct",
765+
);
766+
767+
// Perspective for reference
768+
let persp_bright = render(crate::camera::perspective(256.0, 256.0), "perspective");
769+
770+
assert!(
771+
persp_bright > 100,
772+
"Sanity: perspective should have bright pixels, got {persp_bright}"
773+
);
774+
assert!(
775+
ortho_bright > 100,
776+
"Orthographic projection (near=-10, far=10) should produce visible lit output, got \
777+
only {ortho_bright} bright pixels (perspective had {persp_bright})"
778+
);
779+
}
780+
716781
#[test]
717782
fn aabb_intersection() {
718783
let a = Aabb::new(Vec3::ZERO, Vec3::ONE);

crates/renderling/src/camera.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ pub mod shader;
2727
/// let target = Vec3::ZERO;
2828
/// let up = Vec3::Y;
2929
/// let view = Mat4::look_at_rh(eye, target, up);
30-
/// assert_eq!(renderling::camera::default_perspective(width, height), (projection, view));
30+
/// assert_eq!(
31+
/// renderling::camera::default_perspective(width, height),
32+
/// (projection, view)
33+
/// );
3134
/// ```
3235
pub fn default_perspective(width: f32, height: f32) -> (Mat4, Mat4) {
3336
let projection = perspective(width, height);
@@ -46,6 +49,12 @@ pub fn perspective(width: f32, height: f32) -> Mat4 {
4649
Mat4::perspective_rh(fovy, aspect, znear, zfar)
4750
}
4851

52+
/// Creates a right-handed orthographic projection matrix.
53+
///
54+
/// ## Note
55+
/// When constructing orthographic projections, `near` must be less than `far`
56+
/// to ensure correct depth testing. Reversed depth (`near > far`) will cause
57+
/// back faces to be drawn over front faces, producing incorrect lighting.
4958
pub fn ortho(width: f32, height: f32) -> Mat4 {
5059
let left = 0.0;
5160
let right = width;
@@ -62,6 +71,11 @@ pub fn look_at(eye: impl Into<Vec3>, target: impl Into<Vec3>, up: impl Into<Vec3
6271

6372
/// Creates a typical 2d orthographic projection with +Y extending downward
6473
/// and the +Z axis coming out towards the viewer.
74+
///
75+
/// ## Note
76+
/// When constructing orthographic projections, `near` must be less than `far`
77+
/// to ensure correct depth testing. Reversed depth (`near > far`) will cause
78+
/// back faces to be drawn over front faces, producing incorrect lighting.
6579
pub fn default_ortho2d(width: f32, height: f32) -> (Mat4, Mat4) {
6680
let left = 0.0;
6781
let right = width;
@@ -100,10 +114,8 @@ mod tests {
100114
const THRESHOLD: f32 = 1e-3;
101115
assert!(
102116
distance < THRESHOLD,
103-
"Forward vector is incorrect\n\
104-
forward: {forward}\n\
105-
expected: {expected_forward}\n\
106-
distance: {distance}, threshold: {THRESHOLD}"
117+
"Forward vector is incorrect\nforward: {forward}\nexpected: \
118+
{expected_forward}\ndistance: {distance}, threshold: {THRESHOLD}"
107119
);
108120
}
109121
}

crates/renderling/src/camera/cpu.rs

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ use super::*;
99

1010
/// A camera used for transforming the stage during rendering.
1111
///
12-
/// * Use [`Stage::new_camera`](crate::stage::Stage::new_camera) to create a new camera.
13-
/// * Use [`Stage::use_camera`](crate::stage::Stage::use_camera) to set a camera on the stage.
12+
/// * Use [`Stage::new_camera`](crate::stage::Stage::new_camera) to create a new
13+
/// camera.
14+
/// * Use [`Stage::use_camera`](crate::stage::Stage::use_camera) to set a camera
15+
/// on the stage.
1416
///
1517
/// ## Note
1618
///
@@ -63,7 +65,10 @@ impl Camera {
6365
/// let target = Vec3::ZERO;
6466
/// let up = Vec3::Y;
6567
/// let view = Mat4::look_at_rh(eye, target, up);
66-
/// assert_eq!(renderling::camera::default_perspective(width, height), (projection, view));
68+
/// assert_eq!(
69+
/// renderling::camera::default_perspective(width, height),
70+
/// (projection, view)
71+
/// );
6772
/// ```
6873
pub fn set_default_perspective(&self, width: f32, height: f32) -> &Self {
6974
self.inner
@@ -90,7 +95,10 @@ impl Camera {
9095
/// let target = Vec3::ZERO;
9196
/// let up = Vec3::Y;
9297
/// let view = Mat4::look_at_rh(eye, target, up);
93-
/// assert_eq!(renderling::camera::default_perspective(width, height), (projection, view));
98+
/// assert_eq!(
99+
/// renderling::camera::default_perspective(width, height),
100+
/// (projection, view)
101+
/// );
94102
/// ```
95103
pub fn with_default_perspective(self, width: f32, height: f32) -> Self {
96104
self.set_default_perspective(width, height);

crates/renderling/src/camera/shader.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,35 @@ impl CameraDescriptor {
4141
self.projection
4242
}
4343

44+
/// Set the projection and view matrices for this camera.
45+
///
46+
/// ## Note on depth ordering
47+
/// The rendering pipeline uses `CompareFunction::Less` for depth testing.
48+
/// This requires that the projection matrix maps closer objects to
49+
/// *smaller* z_ndc values. For orthographic projections created with
50+
/// `Mat4::orthographic_rh`, this means `near` must be less than `far`.
51+
/// If `near > far`, the depth mapping is reversed and back faces will
52+
/// be drawn over front faces, producing incorrect (typically black)
53+
/// lighting output.
4454
pub fn set_projection_and_view(&mut self, projection: Mat4, view: Mat4) {
55+
// The z column of the projection matrix determines the depth mapping
56+
// direction. A positive z coefficient (projection.z_axis.z > 0)
57+
// indicates reversed depth (closer objects get larger z_ndc), which
58+
// will cause incorrect results with the standard
59+
// CompareFunction::Less depth test. This typically happens when
60+
// `near > far` is passed to `Mat4::orthographic_rh`.
61+
#[cfg(cpu)]
62+
{
63+
let z_coeff = projection.z_axis.z;
64+
if z_coeff > 0.0 {
65+
log::warn!(
66+
"Projection matrix has a positive z coefficient ({z_coeff}), indicating \
67+
reversed depth mapping. This will cause incorrect depth testing (back faces \
68+
drawn over front faces) with the standard CompareFunction::Less. For \
69+
orthographic projections, ensure near < far."
70+
);
71+
}
72+
}
4573
self.projection = projection;
4674
self.view = view;
4775
self.position = view.inverse().transform_point3(Vec3::ZERO);
@@ -111,7 +139,8 @@ impl CameraDescriptor {
111139
(self.z_far() - self.z_near()).abs()
112140
}
113141

114-
/// Returns the normalized forward vector which points in the direction the camera is looking.
142+
/// Returns the normalized forward vector which points in the direction the
143+
/// camera is looking.
115144
pub fn forward(&self) -> Vec3 {
116145
(self.z_far_point - self.z_near_point).alt_norm_or_zero()
117146
}
-25 KB
Loading

0 commit comments

Comments
 (0)