Skip to content

Commit

Permalink
Mitigate depth offset precision issues on web (#2187)
Browse files Browse the repository at this point in the history
* run-wasm no longer pops up browser when running with --build-only

* add depth offset example

* mitigate depth issues on web

* deterministic order of depth offset determination

* inverte negative if
  • Loading branch information
Wumpf authored and emilk committed May 25, 2023
1 parent 68dab42 commit a189055
Show file tree
Hide file tree
Showing 7 changed files with 226 additions and 24 deletions.
2 changes: 1 addition & 1 deletion crates/re_log_types/src/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use std::hash::BuildHasher;
/// 10^-9 collision risk with 190k values.
/// 10^-6 collision risk with 6M values.
/// 10^-3 collision risk with 200M values.
#[derive(Copy, Clone, Eq)]
#[derive(Copy, Clone, Eq, PartialOrd, Ord)]
pub struct Hash64(u64);

impl Hash64 {
Expand Down
2 changes: 1 addition & 1 deletion crates/re_log_types/src/path/entity_path.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::{
// ----------------------------------------------------------------------------

/// A 64 bit hash of [`EntityPath`] with very small risk of collision.
#[derive(Copy, Clone, Eq)]
#[derive(Copy, Clone, Eq, PartialOrd, Ord)]
pub struct EntityPathHash(Hash64);

impl EntityPathHash {
Expand Down
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,
);
}
23 changes: 13 additions & 10 deletions crates/re_viewer/src/ui/view_spatial/scene/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
use std::{collections::BTreeMap, sync::Arc};
use std::{
collections::{BTreeMap, BTreeSet},
sync::Arc,
};

use ahash::HashMap;

Expand All @@ -10,8 +13,6 @@ use re_log_types::{
};
use re_renderer::{renderer::TexturedRect, Color32, OutlineMaskPreference, Size};
use re_viewer_context::{auto_color, AnnotationMap, Annotations, SceneQuery, ViewerContext};
use smallvec::smallvec;
use smallvec::SmallVec;

use crate::misc::{mesh_loader::LoadedMesh, SpaceViewHighlights, TransformCache};

Expand Down Expand Up @@ -130,6 +131,7 @@ impl SceneSpatial {
) -> EntityDepthOffsets {
crate::profile_function!();

#[derive(PartialEq, PartialOrd, Eq, Ord)]
enum DrawOrderTarget {
Entity(EntityPathHash),
DefaultBox2D,
Expand All @@ -138,7 +140,8 @@ impl SceneSpatial {
DefaultPoints,
}

let mut entities_per_draw_order = BTreeMap::<DrawOrder, SmallVec<[_; 4]>>::new();
// Use a BTreeSet for entity hashes to get a stable order.
let mut entities_per_draw_order = BTreeMap::<DrawOrder, BTreeSet<DrawOrderTarget>>::new();
for (ent_path, _) in query.iter_entities() {
if let Some(draw_order) = query_latest_single::<DrawOrder>(
&ctx.log_db.entity_db.data_store,
Expand All @@ -148,26 +151,26 @@ impl SceneSpatial {
entities_per_draw_order
.entry(draw_order)
.or_default()
.push(DrawOrderTarget::Entity(ent_path.hash()));
.insert(DrawOrderTarget::Entity(ent_path.hash()));
}
}

// Push in default draw orders. All of them using the none hash.
entities_per_draw_order.insert(
DrawOrder::DEFAULT_BOX2D,
smallvec![DrawOrderTarget::DefaultBox2D],
[DrawOrderTarget::DefaultBox2D].into(),
);
entities_per_draw_order.insert(
DrawOrder::DEFAULT_IMAGE,
smallvec![DrawOrderTarget::DefaultImage],
[DrawOrderTarget::DefaultImage].into(),
);
entities_per_draw_order.insert(
DrawOrder::DEFAULT_LINES2D,
smallvec![DrawOrderTarget::DefaultLines2D],
[DrawOrderTarget::DefaultLines2D].into(),
);
entities_per_draw_order.insert(
DrawOrder::DEFAULT_POINTS2D,
smallvec![DrawOrderTarget::DefaultPoints],
[DrawOrderTarget::DefaultPoints].into(),
);

// Determine re_renderer draw order from this.
Expand Down Expand Up @@ -210,7 +213,7 @@ impl SceneSpatial {
}
}
})
.collect::<SmallVec<[_; 4]>>()
.collect::<Vec<_>>()
})
.collect();

Expand Down
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 @@ -445,7 +445,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") {
thread.join().unwrap();
} else {
// 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));
}
}

0 comments on commit a189055

Please sign in to comment.