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

Implement outlines for points 2d/3d/depth & use them for select & hover in Viewer #1568

Merged
merged 9 commits into from
Mar 13, 2023
1 change: 1 addition & 0 deletions crates/re_renderer/examples/depth_cloud.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ impl RenderDepthClouds {
depth_dimensions: depth.dimensions,
depth_data: depth.data.clone(),
colormap: re_renderer::ColorMap::ColorMapTurbo,
outline_mask_id: Default::default(),
}],
)
.unwrap();
Expand Down
36 changes: 17 additions & 19 deletions crates/re_renderer/shader/depth_cloud.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,12 @@ struct DepthCloudInfo {

/// The scale to apply to the radii of the backprojected points.
radius_scale: f32,
radius_scale_row_pad0: f32,
radius_scale_row_pad1: f32,
radius_scale_row_pad2: f32,

/// Configures color mapping mode, see `colormap.wgsl`.
colormap: u32,

/// Outline mask id for the outline mask pass.
outline_mask_id: UVec2,
};
@group(1) @binding(0)
var<uniform> depth_cloud_info: DepthCloudInfo;
Expand Down Expand Up @@ -106,23 +106,21 @@ fn vs_main(@builtin(vertex_index) vertex_idx: u32) -> VertexOut {

@fragment
fn fs_main(in: VertexOut) -> @location(0) Vec4 {
// There's easier ways to compute anti-aliasing for when we are in ortho mode since it's
// just circles.
// But it's very nice to have mostly the same code path and this gives us the sphere world
// position along the way.
let ray_in_world = camera_ray_to_world_pos(in.pos_in_world);

// Sphere intersection with anti-aliasing as described by Iq here
// https://www.shadertoy.com/view/MsSSWV
// (but rearranged and labeled to it's easier to understand!)
let d = ray_sphere_distance(ray_in_world, in.point_pos_in_world, in.point_radius);
let smallest_distance_to_sphere = d.x;
let closest_ray_dist = d.y;
let pixel_world_size = approx_pixel_world_size_at(closest_ray_dist);
if smallest_distance_to_sphere > pixel_world_size {
let coverage = sphere_quad_coverage(in.pos_in_world, in.point_radius, in.point_pos_in_world);
if coverage < 0.001 {
discard;
}
let coverage = 1.0 - saturate(smallest_distance_to_sphere / pixel_world_size);

return vec4(in.point_color.rgb, coverage);
}

@fragment
fn fs_main_outline_mask(in: VertexOut) -> @location(0) UVec2 {
// Output is an integer target, can't use coverage therefore.
// But we still want to discard fragments where coverage is low.
// Since the outline extends a bit, a very low cut off tends to look better.
let coverage = sphere_quad_coverage(in.pos_in_world, in.point_radius, in.point_pos_in_world);
if coverage < 1.0 {
discard;
}
return depth_cloud_info.outline_mask_id;
}
4 changes: 2 additions & 2 deletions crates/re_renderer/shader/lines.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ fn vs_main(@builtin(vertex_index) vertex_idx: u32) -> VertexOut {
return out;
}

fn compute_coverage(in: VertexOut) -> f32{
fn compute_coverage(in: VertexOut) -> f32 {
var coverage = 1.0;
if has_any_flag(in.currently_active_flags, CAP_START_ROUND | CAP_END_ROUND) {
let distance_to_skeleton = length(in.position_world - in.closest_strip_position);
Expand All @@ -241,7 +241,7 @@ fn compute_coverage(in: VertexOut) -> f32{
@fragment
fn fs_main(in: VertexOut) -> @location(0) Vec4 {
var coverage = compute_coverage(in);
if coverage < 0.00001 {
if coverage < 0.001 {
discard;
}

Expand Down
30 changes: 16 additions & 14 deletions crates/re_renderer/shader/point_cloud.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ var color_texture: texture_2d<f32>;
struct BatchUniformBuffer {
world_from_obj: Mat4,
flags: u32,
_padding: UVec2, // UVec3 would take its own 4xf32 row, UVec2 is on the same as flags
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we need to pad at all?

Copy link
Member Author

@Wumpf Wumpf Mar 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's either here or in Rust. If we put nothing here they get on the same "row" since alignment for UVec2 is 8. https://www.w3.org/TR/WGSL/#alignment-and-size

This then translates to

pub struct BatchUniformBuffer {
    pub world_from_obj: wgpu_buffer_types::Mat4,
    pub flags: u32,                   // PointCloudBatchFlags
    pub outline_mask_ids: glam::Vec2,

    pub end_padding: [wgpu_buffer_types::PaddingRow; 16 - 5],
}

But bytemuck doesn't like this because that implies 32bit padding after outline_mask_ids and it forces all padding to be explicit. So we end up with

pub struct BatchUniformBuffer {
  pub world_from_obj: wgpu_buffer_types::Mat4,
  pub flags: u32,                   // PointCloudBatchFlags
  pub outline_mask_ids: glam::Vec2,
  pub _padding: u32,

  pub end_padding: [wgpu_buffer_types::PaddingRow; 16 - 5],
}

outline_mask: UVec2,
};
@group(2) @binding(0)
var<uniform> batch: BatchUniformBuffer;
Expand Down Expand Up @@ -76,24 +78,12 @@ fn vs_main(@builtin(vertex_index) vertex_idx: u32) -> VertexOut {

@fragment
fn fs_main(in: VertexOut) -> @location(0) Vec4 {
// There's easier ways to compute anti-aliasing for when we are in ortho mode since it's just circles.
// But it's very nice to have mostly the same code path and this gives us the sphere world position along the way.
let ray = camera_ray_to_world_pos(in.world_position);

// Sphere intersection with anti-aliasing as described by Iq here
// https://www.shadertoy.com/view/MsSSWV
// (but rearranged and labeled to it's easier to understand!)
let d = ray_sphere_distance(ray, in.point_center, in.radius);
let smallest_distance_to_sphere = d.x;
let closest_ray_dist = d.y;
let pixel_world_size = approx_pixel_world_size_at(closest_ray_dist);
if smallest_distance_to_sphere > pixel_world_size {
let coverage = sphere_quad_coverage(in.world_position, in.radius, in.point_center);
if coverage < 0.001 {
discard;
}
let coverage = 1.0 - saturate(smallest_distance_to_sphere / pixel_world_size);

// TODO(andreas): Do we want manipulate the depth buffer depth to actually render spheres?

// TODO(andreas): Proper shading
// TODO(andreas): This doesn't even use the sphere's world position for shading, the world position used here is flat!
var shading = 1.0;
Expand All @@ -102,3 +92,15 @@ fn fs_main(in: VertexOut) -> @location(0) Vec4 {
}
return vec4(in.color.rgb * shading, coverage);
}

@fragment
fn fs_main_outline_mask(in: VertexOut) -> @location(0) UVec2 {
// Output is an integer target, can't use coverage therefore.
// But we still want to discard fragments where coverage is low.
// Since the outline extends a bit, a very low cut off tends to look better.
let coverage = sphere_quad_coverage(in.world_position, in.radius, in.point_center);
if coverage < 1.0 {
discard;
}
return batch.outline_mask;
}
16 changes: 16 additions & 0 deletions crates/re_renderer/shader/utils/sphere_quad.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,19 @@ fn sphere_quad_span(vertex_idx: u32, point_pos: Vec3, point_unresolved_radius: f

return SphereQuadData(pos, radius);
}

fn sphere_quad_coverage(world_position: Vec3, radius: f32, point_center: Vec3) -> f32 {
// There's easier ways to compute anti-aliasing for when we are in ortho mode since it's just circles.
// But it's very nice to have mostly the same code path and this gives us the sphere world position along the way.
let ray = camera_ray_to_world_pos(world_position);

// Sphere intersection with anti-aliasing as described by Iq here
// https://www.shadertoy.com/view/MsSSWV
// (but rearranged and labeled to it's easier to understand!)
let d = ray_sphere_distance(ray, point_center, radius);
let smallest_distance_to_sphere = d.x;
let closest_ray_dist = d.y;
let pixel_world_size = approx_pixel_world_size_at(closest_ray_dist);

return 1.0 - saturate(smallest_distance_to_sphere / pixel_world_size);
}
74 changes: 69 additions & 5 deletions crates/re_renderer/src/point_cloud_builder.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use crate::{
allocator::CpuWriteGpuReadBuffer,
renderer::{
PointCloudBatchFlags, PointCloudBatchInfo, PointCloudDrawData, PointCloudDrawDataError,
PointCloudVertex,
OutlineMaskPreference, PointCloudBatchFlags, PointCloudBatchInfo, PointCloudDrawData,
PointCloudDrawDataError, PointCloudVertex,
},
Color32, DebugLabel, RenderContext, Size,
};
Expand Down Expand Up @@ -51,6 +51,8 @@ where
world_from_obj: glam::Mat4::IDENTITY,
flags: PointCloudBatchFlags::ENABLE_SHADING,
point_count: 0,
overall_outline_mask_ids: OutlineMaskPreference::NONE,
additional_outline_mask_ids_vertex_ranges: Vec::new(),
});

PointCloudBatchBuilder(self)
Expand Down Expand Up @@ -150,6 +152,13 @@ where
self
}

/// Sets an outline mask for every element in the batch.
#[inline]
pub fn outline_mask_ids(mut self, outline_mask_ids: OutlineMaskPreference) -> Self {
self.batch_mut().overall_outline_mask_ids = outline_mask_ids;
self
}

/// Each time we `add_points`, or upon builder drop, make sure that we
/// fill in any additional colors and user-data to have matched vectors.
fn extend_defaults(&mut self) {
Expand Down Expand Up @@ -215,6 +224,13 @@ where
max_points,
colors: &mut self.0.color_buffer,
user_data: &mut self.0.user_data,
additional_outline_mask_ids: &mut self
.0
.batches
.last_mut()
.unwrap()
.additional_outline_mask_ids_vertex_ranges,
start_vertex_index: old_size as _,
}
}

Expand All @@ -225,6 +241,7 @@ where
debug_assert_eq!(self.0.vertices.len(), self.0.color_buffer.num_written());
debug_assert_eq!(self.0.vertices.len(), self.0.user_data.len());

let vertex_index = self.0.vertices.len() as u32;
self.0.vertices.push(PointCloudVertex {
position,
radius: Size::AUTO,
Expand All @@ -236,6 +253,14 @@ where
vertex: self.0.vertices.last_mut().unwrap(),
color: &mut self.0.color_buffer,
user_data: self.0.user_data.last_mut().unwrap(),
vertex_index,
additional_outline_mask_ids: &mut self
.0
.batches
.last_mut()
.unwrap()
.additional_outline_mask_ids_vertex_ranges,
outline_mask_id: OutlineMaskPreference::NONE,
}
}

Expand Down Expand Up @@ -267,10 +292,14 @@ where
}
}

// TODO(andreas): Should remove single-point builder, practically this never makes sense as we're almost always dealing with arrays of points.
pub struct PointBuilder<'a, PerPointUserData> {
vertex: &'a mut PointCloudVertex,
color: &'a mut CpuWriteGpuReadBuffer<Color32>,
user_data: &'a mut PerPointUserData,
vertex_index: u32,
additional_outline_mask_ids: &'a mut Vec<(std::ops::Range<u32>, OutlineMaskPreference)>,
outline_mask_id: OutlineMaskPreference,
}

impl<'a, PerPointUserData> PointBuilder<'a, PerPointUserData>
Expand All @@ -294,17 +323,35 @@ where
*self.user_data = data;
self
}

/// Pushes additional outline mask ids for this point
///
/// Prefer the `overall_outline_mask_ids` setting to set the outline mask ids for the entire batch whenever possible!
pub fn outline_mask_id(mut self, outline_mask_id: OutlineMaskPreference) -> Self {
self.outline_mask_id = outline_mask_id;
self
}
}

impl<'a, PerPointUserData> Drop for PointBuilder<'a, PerPointUserData> {
fn drop(&mut self) {
if self.outline_mask_id.is_some() {
self.additional_outline_mask_ids.push((
self.vertex_index..self.vertex_index + 1,
self.outline_mask_id,
));
}
}
}

pub struct PointsBuilder<'a, PerPointUserData> {
// Vertices is a slice, which radii will update
vertices: &'a mut [PointCloudVertex],

// Colors and user-data are the Vec we append
// the data to if provided.
max_points: usize,
colors: &'a mut CpuWriteGpuReadBuffer<Color32>,
user_data: &'a mut Vec<PerPointUserData>,
additional_outline_mask_ids: &'a mut Vec<(std::ops::Range<u32>, OutlineMaskPreference)>,
start_vertex_index: u32,
}

impl<'a, PerPointUserData> PointsBuilder<'a, PerPointUserData>
Expand Down Expand Up @@ -354,4 +401,21 @@ where
.extend(data.take(self.max_points - self.user_data.len()));
self
}

/// Pushes additional outline mask ids for a specific range of points.
/// The range is relative to this builder's range, not the entire batch.
///
/// Prefer the `overall_outline_mask_ids` setting to set the outline mask ids for the entire batch whenever possible!
#[inline]
pub fn push_additional_outline_mask_ids_for_range(
self,
range: std::ops::Range<u32>,
ids: OutlineMaskPreference,
) -> Self {
self.additional_outline_mask_ids.push((
(range.start + self.start_vertex_index)..(range.end + self.start_vertex_index),
ids,
));
self
}
}
Loading