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

Mitigate depth offset precision issues on web #2187

Merged
merged 5 commits into from
May 24, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
157 changes: 157 additions & 0 deletions crates/re_renderer/examples/depth_offset.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//! Depth offset and depth precision comparison test scene.
//!
//! Rectangles are at close distance to each other in the z==0 plane.
//! Rects on the left use "real" depth values, rects on the right use depth offset.
//! You should see the least saturated rects in front of more saturated rects.
//!
//! Press arrow up/down to increase/decrease the distance of the camera to the z==0 plane in tandem with the scale of the rectangles.
//! Press arrow left/right to increase/decrease the near plane distance.

use ecolor::Hsva;
use re_renderer::{
renderer::{ColormappedTexture, RectangleDrawData, RectangleOptions, TexturedRect},
view_builder::{self, Projection, ViewBuilder},
};

mod framework;

struct Render2D {
distance_scale: f32,
near_plane: f32,
}

impl framework::Example for Render2D {
fn title() -> &'static str {
"Depth Offset"
}

fn new(_re_ctx: &mut re_renderer::RenderContext) -> Self {
Render2D {
distance_scale: 100.0,
near_plane: 0.1,
}
}

fn draw(
&mut self,
re_ctx: &mut re_renderer::RenderContext,
resolution: [u32; 2],
_time: &framework::Time,
pixels_from_point: f32,
) -> Vec<framework::ViewDrawResult> {
let mut rectangles = Vec::new();

let extent_u = glam::vec3(1.0, 0.0, 0.0) * self.distance_scale;
let extent_v = glam::vec3(0.0, 1.0, 0.0) * self.distance_scale;

// Rectangles on the left from near to far, using z.
let base_top_left = glam::vec2(-0.8, -0.5) * self.distance_scale
- (extent_u.truncate() + extent_v.truncate()) * 0.5;
let xy_step = glam::vec2(-0.1, 0.1) * self.distance_scale;
let z_values = [0.1, 0.01, 0.001, 0.0001, 0.00001, 0.0]; // Make sure to go from near to far so that painter's algorithm would fail if depth values are no longer distinct.
for (i, z) in z_values.into_iter().enumerate() {
let saturation = 0.1 + i as f32 / z_values.len() as f32 * 0.9;
rectangles.push(TexturedRect {
top_left_corner_position: (base_top_left + i as f32 * xy_step).extend(z),
extent_u,
extent_v,
colormapped_texture: ColormappedTexture::from_unorm_srgba(
re_ctx
.texture_manager_2d
.white_texture_unorm_handle()
.clone(),
),
options: RectangleOptions {
multiplicative_tint: Hsva::new(0.0, saturation, 0.5, 1.0).into(),
..Default::default()
},
});
}

// Rectangles on the right from near to far, using depth offset.
let base_top_left = glam::vec2(0.8, -0.5) * self.distance_scale
- (extent_u.truncate() + extent_v.truncate()) * 0.5;
let xy_step = glam::vec2(0.1, 0.1) * self.distance_scale;
let depth_offsets = [1000, 100, 10, 1, 0, -1]; // Make sure to go from near to far so that painter's algorithm would fail if depth values are no longer distinct.
for (i, depth_offset) in depth_offsets.into_iter().enumerate() {
let saturation = 0.1 + i as f32 / depth_offsets.len() as f32 * 0.9;
rectangles.push(TexturedRect {
top_left_corner_position: (base_top_left + i as f32 * xy_step).extend(0.0),
extent_u,
extent_v,
colormapped_texture: ColormappedTexture::from_unorm_srgba(
re_ctx
.texture_manager_2d
.white_texture_unorm_handle()
.clone(),
),
options: RectangleOptions {
multiplicative_tint: Hsva::new(0.68, saturation, 0.5, 1.0).into(),
depth_offset,
..Default::default()
},
});
}

let mut view_builder = ViewBuilder::new(
re_ctx,
view_builder::TargetConfiguration {
name: "3D".into(),
resolution_in_pixel: resolution,
view_from_world: macaw::IsoTransform::look_at_rh(
glam::Vec3::Z * 2.0 * self.distance_scale,
glam::Vec3::ZERO,
glam::Vec3::Y,
)
.unwrap(),
projection_from_view: Projection::Perspective {
vertical_fov: 70.0 * std::f32::consts::TAU / 360.0,
near_plane_distance: self.near_plane,
aspect_ratio: resolution[0] as f32 / resolution[1] as f32,
},
pixels_from_point,
..Default::default()
},
);
let command_buffer = view_builder
.queue_draw(&RectangleDrawData::new(re_ctx, &rectangles).unwrap())
.draw(re_ctx, ecolor::Rgba::TRANSPARENT)
.unwrap();

vec![{
framework::ViewDrawResult {
view_builder,
command_buffer,
target_location: glam::Vec2::ZERO,
}
}]
}

fn on_keyboard_input(&mut self, input: winit::event::KeyboardInput) {
if input.state == winit::event::ElementState::Pressed {
match input.virtual_keycode {
Some(winit::event::VirtualKeyCode::Up) => {
self.distance_scale *= 1.1;
re_log::info!(self.distance_scale);
}
Some(winit::event::VirtualKeyCode::Down) => {
self.distance_scale /= 1.1;
re_log::info!(self.distance_scale);
}
Some(winit::event::VirtualKeyCode::Right) => {
self.near_plane *= 1.1;
re_log::info!(self.near_plane);
}
Some(winit::event::VirtualKeyCode::Left) => {
self.near_plane /= 1.1;
re_log::info!(self.near_plane);
}
_ => {}
}
}
}
}

fn main() {
framework::start::<Render2D>();
}
42 changes: 40 additions & 2 deletions crates/re_renderer/shader/utils/depth_offset.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,41 @@ Without z offset, we get this:
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:
The code in apply_depth_offset acts 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)


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);"
Meaning projected coordinates (without any offset) become:

x_proj_gl: x_proj,
y_proj_gl: -y_proj,
z_proj_gl: z_proj * 2 - w_proj,
w_proj_gl: w_proj

For NDC follows:

x_ndc: x_proj / w_proj = x * f / aspect_ratio / -z
y_ndc: -y_proj / w_proj = -y * f / -z
z_ndc: (z_proj * 2 - w_proj) / w_proj = (w * z_near * 2 + z) / -z

Which means depth precision is greatly reduced before hitting the depth buffer
and then further by shifting back to the [0, 1] range in which depth is stored.

This is a general issue, not specific to our depth offset implementation, affecting precision for all depth values.

Note that for convenience we still use inverse depth (otherwise we'd have to flip all depth tests),
but this does actually neither improve not worsen precision, in any case most of the precision is
somewhere in the middle of the depth range (see also https://developer.nvidia.com/content/depth-precision-visualized).

The only reliable ways to mitigate this we found so far are:
* higher near plane distance
* larger depth offset

*/

fn apply_depth_offset(position: Vec4, offset: f32) -> Vec4 {
Expand All @@ -52,8 +83,15 @@ fn apply_depth_offset(position: Vec4, offset: f32) -> Vec4 {
// 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):

var w_scale_bias = f32eps * offset;
if frame.hardware_tier == HARDWARE_TIER_GLES {
// Empirically determined, see section on GLES above.
w_scale_bias *= 1000.0;
}
let w_scale = 1.0 - w_scale_bias;

return Vec4(
position.xyz,
position.w * (1.0 - f32eps * offset),
position.w * w_scale,
);
}
2 changes: 1 addition & 1 deletion crates/re_viewer/src/ui/view_spatial/ui_2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -444,7 +444,7 @@ fn setup_target_config(

let projection_from_view = re_renderer::view_builder::Projection::Perspective {
vertical_fov: pinhole.fov_y().unwrap_or(Eye::DEFAULT_FOV_Y),
near_plane_distance: 0.01,
near_plane_distance: 0.1,
aspect_ratio: pinhole
.aspect_ratio()
.unwrap_or(canvas_size.x / canvas_size.y),
Expand Down
22 changes: 13 additions & 9 deletions run_wasm/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,21 +99,25 @@ fn main() {
let host = host.as_deref().unwrap_or("localhost");
let port = port.as_deref().unwrap_or("8000");

std::thread::Builder::new()
let thread = std::thread::Builder::new()
.name("cargo_run_wasm".into())
.spawn(|| {
cargo_run_wasm::run_wasm_with_css(CSS);
})
.expect("Failed to spawn thread");

// It would be nice to start a webbrowser, but we can't really know when the server is ready.
// So we just sleep for a while and hope it works.
std::thread::sleep(Duration::from_millis(500));
if !args.contains("--build-only") {
Wumpf marked this conversation as resolved.
Show resolved Hide resolved
// It would be nice to start a web-browser, but we can't really know when the server is ready.
// So we just sleep for a while and hope it works.
std::thread::sleep(Duration::from_millis(500));

// Open browser tab.
let viewer_url = format!("http://{host}:{port}",);
webbrowser::open(&viewer_url).ok();
println!("Opening browser at {viewer_url}");
// Open browser tab.
let viewer_url = format!("http://{host}:{port}",);
webbrowser::open(&viewer_url).ok();
println!("Opening browser at {viewer_url}");

std::thread::sleep(Duration::from_secs(u64::MAX));
std::thread::sleep(Duration::from_secs(u64::MAX));
} else {
thread.join().unwrap();
}
}