Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix web depth/projection regression, causing incorrect rendering on all 3D scenes #2170

Merged
merged 4 commits into from
May 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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