Skip to content

Commit

Permalink
Fix web depth/projection regression, causing incorrect rendering on a…
Browse files Browse the repository at this point in the history
…ll 3D scenes (#2170)

* Revert "Fix depth precision issues on WebGL due to different NDC space (#2123)"

This reverts commit 4f60fd7.

* fudge depth offsets on Webgl

* hardware_tier global constant, better depth handling for gles

* Improve the depth offset for all platforms

---------

Co-authored-by: Emil Ernerfeldt <emil.ernerfeldt@gmail.com>
  • Loading branch information
Wumpf and emilk committed May 25, 2023
1 parent c00b08d commit 79388b1
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 77 deletions.
8 changes: 8 additions & 0 deletions crates/re_renderer/shader/global_bindings.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ struct FrameUniformBuffer {

// Size used for all line radii given with Size::AUTO.
auto_size_lines: f32,

/// re_renderer defined hardware tier.
hardware_tier: u32,
};

@group(0) @binding(0)
var<uniform> frame: FrameUniformBuffer;

Expand All @@ -36,3 +40,7 @@ var nearest_sampler: sampler;

@group(0) @binding(2)
var trilinear_sampler: sampler;

// See config.rs#HardwareTier
const HARDWARE_TIER_GLES = 0u;
const HARDWARE_TIER_WEBGPU = 1u;
84 changes: 54 additions & 30 deletions crates/re_renderer/shader/utils/depth_offset.wgsl
Original file line number Diff line number Diff line change
@@ -1,35 +1,59 @@
#import <../global_bindings.wgsl>
#import <../types.wgsl>

/*
We use reverse infinite depth, as promoted by https://developer.nvidia.com/content/depth-precision-visualized

The projection matrix (from `glam::Mat4::perspective_infinite_reverse_rh`) looks like this:

f / aspect_ratio 0 0 0
0 f 0 0
0 0 0 z_near
0 0 -1 0

This means after multiplication with xyzw (with w=1) we end up with:

x_proj: x * f / aspect_ratio,
y_proj: y * f,
z_proj: w * z_near,
w_proj: -z

This is then projected by dividing with w, giving:

x_ndc: x_proj / w_proj
y_ndc: y_proj / w_proj
z_ndc: z_proj / w_proj

Without z offset, we get this:

x_ndc: x * f / aspect_ratio / -z
y_ndc: y * f / -z
z_ndc: w * z_near / -z

The negative -z axis is away from the camera, so with w=1 we get
z_near mapping to z_ndc=1, and infinity mapping to z_ndc=0.

The code below act on the *_proj values by adding a scale multiplier on `w_proj` resulting in:
x_ndc: x_proj / (-z * w_scale)
y_ndc: y_proj / (-z * w_scale)
z_ndc: z_proj / (-z * w_scale)
*/

fn apply_depth_offset(position: Vec4, offset: f32) -> Vec4 {
// We're using inverse z, i.e. 0.0 is far, 1.0 is near.
// We want a positive offset to move towards the viewer, so offset needs to be added.
//
// With this in place we still may cross over to 0.0 (the far plane) too early,
// making objects disappear into the far when they'd be otherwise still rendered.
// Since we're actually supposed to have an *infinite* far plane this should never happen!
// Therefore we simply dictacte a minimum z value.
// This ofc wrecks the depth offset and may cause z fighting with all very far away objects, but it's better than having things disappear!

if true {
// This path assumes a `f32` depth buffer!

// 1.0 * eps _should_ be enough, but in practice it causes Z-fighting for unknown reasons.
// Maybe because of GPU interpolation of vertex coordinates?
let eps = 5.0 * f32eps;

return Vec4(
position.xy,
max(position.z * (1.0 + eps * offset), f32eps),
position.w
);
} else {
// Causes Z-collision at far distances
let eps = f32eps;
return Vec4(
position.xy,
max(position.z + eps * offset * position.w, f32eps),
position.w
);
}
// On GLES/WebGL, the NDC clipspace range for depth is from -1 to 1 and y is flipped.
// wgpu/Naga counteracts this by patching all vertex shaders with:
// "gl_Position.yz = vec2(-gl_Position.y, gl_Position.z * 2.0 - gl_Position.w);",
// This doesn't matter for us though.

// This path assumes a `f32` depth buffer.

// We set up the depth comparison to Greater, so that large z means closer (overdraw).
// We want a greater offset to win over a smaller offset,
// so a great depth offset should result in a large z_ndc.
// How do we get there? We let large depth offset lead to a smaller divisor (w_proj):

return Vec4(
position.xyz,
position.w * (1.0 - f32eps * offset),
);
}
6 changes: 4 additions & 2 deletions crates/re_renderer/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,18 @@
/// but choosing lower tiers is always possible.
/// Tiers may loosely relate to quality settings, but their primary function is an easier way to
/// do bundle feature *support* checks.
///
/// See also `global_bindings.wgsl`
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum HardwareTier {
/// Limited feature support as provided by WebGL and native GLES2/OpenGL3(ish).
Gles,
Gles = 0,

/// Full support of WebGPU spec without additional feature requirements.
///
/// Expecting to run either in a stable WebGPU implementation.
/// I.e. either natively with Vulkan/Metal or in a browser with WebGPU support.
FullWebGpuSupport,
FullWebGpuSupport = 1,
// Run natively with Vulkan/Metal and require additional features.
//HighEnd
}
Expand Down
4 changes: 2 additions & 2 deletions crates/re_renderer/src/global_bindings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ pub struct FrameUniformBuffer {
// Size used for all line radii given with Size::AUTO.
pub auto_size_lines: f32,

/// Factor used to compute depth offsets, see `depth_offset.wgsl`.
pub end_padding: wgpu_buffer_types::PaddingRow,
/// re_renderer defined hardware tier.
pub hardware_tier: wgpu_buffer_types::U32RowPadded,
}

pub(crate) struct GlobalBindings {
Expand Down
61 changes: 18 additions & 43 deletions crates/re_renderer/src/view_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ use std::sync::Arc;

use crate::{
allocator::{create_and_fill_uniform_buffer, GpuReadbackIdentifier},
config::HardwareTier,
context::RenderContext,
draw_phases::{
DrawPhase, OutlineConfig, OutlineMaskProcessor, PickingLayerError, PickingLayerProcessor,
Expand Down Expand Up @@ -331,30 +330,11 @@ impl ViewBuilder {
// We use infinite reverse-z projection matrix
// * great precision both with floating point and integer: https://developer.nvidia.com/content/depth-precision-visualized
// * no need to worry about far plane
//
// There's a pretty big catch though:
// When we're on GLES, the NDC coordinates go from -1 to 1 and are then mapped to 0 to 1 as part of the viewport transformation.
// This means, that if we don't care about this, we'll not only throw away "half" of the space,
// we also have the most precise region around 0 in the wrong spot initially 😱
// Therefore, we pretty much *have* to use a different projection in this case.
// Modern OpenGL has a way to change this to the more common 0 to 1 z-range for NDC
// which is the standard in Metal/Vulkan/DirectX/WebGPU, but WebGL is lacking this.
// see https://registry.khronos.org/OpenGL-Refpages/gl4/html/glClipControl.xhtml
let projection_from_view =
if ctx.shared_renderer_data.config.hardware_tier == HardwareTier::Gles {
glam::Mat4::perspective_rh_gl(
vertical_fov,
aspect_ratio,
near_plane_distance,
100000.0,
)
} else {
glam::Mat4::perspective_infinite_reverse_rh(
vertical_fov,
aspect_ratio,
near_plane_distance,
)
};
let projection_from_view = glam::Mat4::perspective_infinite_reverse_rh(
vertical_fov,
aspect_ratio,
near_plane_distance,
);

// Calculate ratio between screen size and screen distance.
// Great for getting directions from normalized device coordinates.
Expand Down Expand Up @@ -391,31 +371,26 @@ impl ViewBuilder {
config.resolution_in_pixel[0] as f32 / config.resolution_in_pixel[1] as f32;
let horizontal_world_size = vertical_world_size * aspect_ratio;
// Note that we inverse z (by swapping near and far plane) to be consistent with our perspective projection.
let (left, right, bottom, top, near, far) = match camera_mode {
OrthographicCameraMode::NearPlaneCenter => (
let projection_from_view = match camera_mode {
OrthographicCameraMode::NearPlaneCenter => glam::Mat4::orthographic_rh(
-0.5 * horizontal_world_size,
0.5 * horizontal_world_size,
-0.5 * vertical_world_size,
0.5 * vertical_world_size,
far_plane_distance,
0.0,
),
OrthographicCameraMode::TopLeftCornerAndExtendZ => (
0.0,
horizontal_world_size,
vertical_world_size,
0.0,
far_plane_distance,
-far_plane_distance,
),
OrthographicCameraMode::TopLeftCornerAndExtendZ => {
glam::Mat4::orthographic_rh(
0.0,
horizontal_world_size,
vertical_world_size,
0.0,
far_plane_distance,
-far_plane_distance,
)
}
};
let projection_from_view =
if ctx.shared_renderer_data.config.hardware_tier == HardwareTier::Gles {
// See comment on perspective projection for why we need to care about Gles vs non-Gles.
glam::Mat4::orthographic_rh_gl(left, right, bottom, top, near, far)
} else {
glam::Mat4::orthographic_rh(left, right, bottom, top, near, far)
};

let tan_half_fov = glam::vec2(f32::MAX, f32::MAX);
let pixel_world_size_from_camera_distance = vertical_world_size
Expand Down Expand Up @@ -482,7 +457,7 @@ impl ViewBuilder {
auto_size_points: auto_size_points.0,
auto_size_lines: auto_size_lines.0,

end_padding: Default::default(),
hardware_tier: (ctx.shared_renderer_data.config.hardware_tier as u32).into(),
};
let frame_uniform_buffer = create_and_fill_uniform_buffer(
ctx,
Expand Down

0 comments on commit 79388b1

Please sign in to comment.