Skip to content

Commit

Permalink
Use outlines for mesh selections instead of highlight colors (#1540)
Browse files Browse the repository at this point in the history
* mesh highlighting is now done via outlines
* eliminate need for computing "old" selection highlight on meshes
* blend outlines among each other in linear space
  • Loading branch information
Wumpf authored Mar 9, 2023
1 parent dd2a542 commit a0c4189
Show file tree
Hide file tree
Showing 22 changed files with 233 additions and 77 deletions.
14 changes: 7 additions & 7 deletions crates/re_renderer/examples/outlines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,11 @@ impl framework::Example for Outlines {
.unwrap();

let outline_mask_large_mesh = match ((seconds_since_startup * 0.5) as u64) % 5 {
0 => OutlineMaskPreference::None,
1 => Some([1, 0]), // Same as the the y spinning mesh.
2 => Some([2, 0]), // Different than both meshes, outline A.
3 => Some([0, 1]), // Same as the the x spinning mesh.
4 => Some([0, 2]), // Different than both meshes, outline B.
0 => OutlineMaskPreference::NONE,
1 => OutlineMaskPreference::some(1, 0), // Same as the the y spinning mesh.
2 => OutlineMaskPreference::some(2, 0), // Different than both meshes, outline A.
3 => OutlineMaskPreference::some(0, 1), // Same as the the x spinning mesh.
4 => OutlineMaskPreference::some(0, 2), // Different than both meshes, outline B.
_ => unreachable!(),
};

Expand All @@ -94,12 +94,12 @@ impl framework::Example for Outlines {
rotation: glam::Quat::IDENTITY,
},
MeshProperties {
outline_mask: Some([1, 0]),
outline_mask: OutlineMaskPreference::some(1, 0),
position: glam::vec3(2.0, 0.0, -3.0),
rotation: glam::Quat::from_rotation_y(seconds_since_startup),
},
MeshProperties {
outline_mask: Some([0, 1]),
outline_mask: OutlineMaskPreference::some(0, 1),
position: glam::vec3(-2.0, 1.0, 3.0),
rotation: glam::Quat::from_rotation_x(seconds_since_startup),
},
Expand Down
11 changes: 7 additions & 4 deletions crates/re_renderer/shader/outlines/outlines_from_voronoi.wgsl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#import <../types.wgsl>
#import <../global_bindings.wgsl>
#import <../screen_triangle_vertex.wgsl>
#import <../utils/srgb.wgsl>

@group(1) @binding(0)
var voronoi_texture: texture_2d<f32>;
Expand All @@ -18,9 +19,9 @@ fn main(in: FragmentInput) -> @location(0) Vec4 {
let resolution = Vec2(textureDimensions(voronoi_texture).xy);
let pixel_coordinates = resolution * in.texcoord;
let closest_positions = textureSample(voronoi_texture, nearest_sampler, in.texcoord);
let to_closest_a_and_b = (closest_positions - pixel_coordinates.xyxy);
let distance_pixel_a = length(to_closest_a_and_b.xy);
let distance_pixel_b = length(to_closest_a_and_b.zw);

let distance_pixel_a = distance(pixel_coordinates, closest_positions.xy);
let distance_pixel_b = distance(pixel_coordinates, closest_positions.zw);

let sharpness = 1.0; // Fun to play around with, but not exposed yet.
let outline_a = saturate((uniforms.outline_radius_pixel - distance_pixel_a) * sharpness);
Expand All @@ -31,7 +32,9 @@ fn main(in: FragmentInput) -> @location(0) Vec4 {

// Blend B over A.
let color = color_a * (1.0 - color_b.a) + color_b;
return color;

// We're going directly to an srgb-gamma target without automatic conversion.
return srgba_from_linear(color);

// Show only the outline. Useful for debugging.
//return Vec4(color.rgb, 1.0);
Expand Down
3 changes: 2 additions & 1 deletion crates/re_renderer/src/renderer/mesh_renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ impl Default for MeshInstance {
mesh: None,
world_from_mesh: macaw::Affine3A::IDENTITY,
additive_tint: Color32::TRANSPARENT,
outline_mask: None,
outline_mask: OutlineMaskPreference::NONE,
}
}
}
Expand Down Expand Up @@ -221,6 +221,7 @@ impl MeshDrawData {
additive_tint: instance.additive_tint,
outline_mask: instance
.outline_mask
.0
.map_or([0, 0, 0, 0], |mask| [mask[0], mask[1], 0, 0]),
});
}
Expand Down
22 changes: 21 additions & 1 deletion crates/re_renderer/src/renderer/outlines.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,27 @@ use smallvec::smallvec;
///
/// Object index 0 is special: It is the default background of each outline channel, thus rendering with it
/// is a form of "active no outline", effectively subtracting from the outline channel.
pub type OutlineMaskPreference = Option<[u8; 2]>;
#[derive(Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)]
pub struct OutlineMaskPreference(pub Option<[u8; 2]>);

impl OutlineMaskPreference {
pub const NONE: OutlineMaskPreference = OutlineMaskPreference(None);

#[inline]
pub fn some(channel_a: u8, channel_b: u8) -> Self {
OutlineMaskPreference(Some([channel_a, channel_b]))
}

#[inline]
pub fn is_some(self) -> bool {
self.0.is_some()
}

#[inline]
pub fn is_none(self) -> bool {
self.0.is_none()
}
}

#[derive(Clone, Debug)]
pub struct OutlineConfig {
Expand Down
175 changes: 137 additions & 38 deletions crates/re_viewer/src/misc/selection_state.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use ahash::{HashMap, HashSet};
use egui::NumExt;
use nohash_hasher::IntMap;
use re_data_store::{EntityPath, LogDb};
use re_log_types::{component_types::InstanceKey, EntityPathHash};
use re_renderer::renderer::OutlineMaskPreference;

use crate::ui::{Blueprint, HistoricalSelection, SelectionHistory, SpaceView, SpaceViewId};

Expand Down Expand Up @@ -134,12 +136,41 @@ impl<'a> OptionalSpaceViewEntityHighlight<'a> {
}
}

#[derive(Default)]
pub struct SpaceViewOutlineMasks {
overall: OutlineMaskPreference,
instances: ahash::HashMap<InstanceKey, OutlineMaskPreference>,
pub any_selection_highlight: bool,
}

#[derive(Copy, Clone)]
pub struct OptionalSpaceViewOutlineMask<'a>(Option<&'a SpaceViewOutlineMasks>);

impl<'a> OptionalSpaceViewOutlineMask<'a> {
pub fn index_outline_mask(&self, instance_key: InstanceKey) -> OutlineMaskPreference {
self.0
.map_or(OutlineMaskPreference::NONE, |entity_outline_mask| {
entity_outline_mask
.instances
.get(&instance_key)
.cloned()
.unwrap_or_default()
.max(entity_outline_mask.overall)
})
}

pub fn any_selection_highlight(&self) -> bool {
self.0.map_or(false, |mask| mask.any_selection_highlight)
}
}

/// Highlights in a specific space view.
///
/// Using this in bulk on many objects is faster than querying single objects.
#[derive(Default)]
pub struct SpaceViewHighlights {
highlighted_entity_paths: IntMap<EntityPathHash, SpaceViewEntityHighlight>,
outlines_masks: IntMap<EntityPathHash, SpaceViewOutlineMasks>,
}

impl SpaceViewHighlights {
Expand All @@ -149,6 +180,13 @@ impl SpaceViewHighlights {
) -> OptionalSpaceViewEntityHighlight<'_> {
OptionalSpaceViewEntityHighlight(self.highlighted_entity_paths.get(&entity_path_hash))
}

pub fn entity_outline_mask(
&self,
entity_path_hash: EntityPathHash,
) -> OptionalSpaceViewOutlineMask<'_> {
OptionalSpaceViewOutlineMask(self.outlines_masks.get(&entity_path_hash))
}
}

/// Selection and hover state
Expand Down Expand Up @@ -325,6 +363,20 @@ impl SelectionState {

let mut highlighted_entity_paths =
IntMap::<EntityPathHash, SpaceViewEntityHighlight>::default();
let mut outlines_masks = IntMap::<EntityPathHash, SpaceViewOutlineMasks>::default();

let mut selection_mask_index: u8 = 0;
let mut hover_mask_index: u8 = 0;
let mut next_selection_mask_index = || {
// We don't expect to overflow u8, but if we do, don't use the "background mask".
selection_mask_index = selection_mask_index.wrapping_add(1).at_least(1);
selection_mask_index
};
let mut next_hover_mask_index = || {
// We don't expect to overflow u8, but if we do, don't use the "background mask".
hover_mask_index = hover_mask_index.wrapping_add(1).at_least(1);
hover_mask_index
};

for current_selection in self.selection.iter() {
match current_selection {
Expand All @@ -333,6 +385,11 @@ impl SelectionState {
Item::DataBlueprintGroup(group_space_view_id, group_handle) => {
if *group_space_view_id == space_view_id {
if let Some(space_view) = space_views.get(group_space_view_id) {
// Everything in the same group should receive the same selection outline.
// (Due to the way outline masks work in re_renderer, we can't leave the hover channel empty)
let selection_mask =
OutlineMaskPreference::some(next_selection_mask_index(), 0);

space_view.data_blueprint.visit_group_entities_recursively(
*group_handle,
&mut |entity_path: &EntityPath| {
Expand All @@ -341,36 +398,55 @@ impl SelectionState {
.or_default()
.overall
.selection = SelectionHighlight::SiblingSelection;
let outline_mask =
outlines_masks.entry(entity_path.hash()).or_default();
outline_mask.overall = outline_mask.overall.max(selection_mask);
outline_mask.any_selection_highlight = true;
},
);
}
}
}

Item::InstancePath(selected_space_view_context, selected_instance) => {
let highlight = if *selected_space_view_context == Some(space_view_id) {
SelectionHighlight::Selection
} else {
SelectionHighlight::SiblingSelection
};

let highlighted_entity = highlighted_entity_paths
.entry(selected_instance.entity_path.hash())
.or_default();

let highlight_target = if let Some(selected_index) =
selected_instance.instance_key.specific_index()
{
&mut highlighted_entity
.instances
.entry(selected_index)
.or_default()
.selection
} else {
&mut highlighted_entity.overall.selection
};

*highlight_target = (*highlight_target).max(highlight);
let highlight = if *selected_space_view_context == Some(space_view_id) {
SelectionHighlight::Selection
} else {
SelectionHighlight::SiblingSelection
};

let highlighted_entity = highlighted_entity_paths
.entry(selected_instance.entity_path.hash())
.or_default();
let highlight_target = if let Some(selected_index) =
selected_instance.instance_key.specific_index()
{
&mut highlighted_entity
.instances
.entry(selected_index)
.or_default()
.selection
} else {
&mut highlighted_entity.overall.selection
};
*highlight_target = (*highlight_target).max(highlight);
}
{
let outline_mask = outlines_masks
.entry(selected_instance.entity_path.hash())
.or_default();
outline_mask.any_selection_highlight = true;
let outline_mask_target = if let Some(selected_index) =
selected_instance.instance_key.specific_index()
{
outline_mask.instances.entry(selected_index).or_default()
} else {
&mut outline_mask.overall
};
*outline_mask_target = (*outline_mask_target)
.max(OutlineMaskPreference::some(next_selection_mask_index(), 0));
}
}
};
}
Expand All @@ -384,6 +460,10 @@ impl SelectionState {
// since they are truly local to a space view.
if *group_space_view_id == space_view_id {
if let Some(space_view) = space_views.get(group_space_view_id) {
// Everything in the same group should receive the same selection outline.
let hover_mask =
OutlineMaskPreference::some(0, next_hover_mask_index());

space_view.data_blueprint.visit_group_entities_recursively(
*group_handle,
&mut |entity_path: &EntityPath| {
Expand All @@ -392,36 +472,55 @@ impl SelectionState {
.or_default()
.overall
.hover = HoverHighlight::Hovered;
let mask =
outlines_masks.entry(entity_path.hash()).or_default();
mask.overall = mask.overall.max(hover_mask);
},
);
}
}
}

Item::InstancePath(_, selected_instance) => {
let highlighted_entity = highlighted_entity_paths
.entry(selected_instance.entity_path.hash())
.or_default();

let highlight_target = if let Some(selected_index) =
selected_instance.instance_key.specific_index()
{
&mut highlighted_entity
.instances
.entry(selected_index)
.or_default()
.hover
} else {
&mut highlighted_entity.overall.hover
};

*highlight_target = HoverHighlight::Hovered;
let highlighted_entity = highlighted_entity_paths
.entry(selected_instance.entity_path.hash())
.or_default();

let highlight_target = if let Some(selected_index) =
selected_instance.instance_key.specific_index()
{
&mut highlighted_entity
.instances
.entry(selected_index)
.or_default()
.hover
} else {
&mut highlighted_entity.overall.hover
};
*highlight_target = HoverHighlight::Hovered;
}
{
let outlined_entity = outlines_masks
.entry(selected_instance.entity_path.hash())
.or_default();
let outline_mask_target = if let Some(selected_index) =
selected_instance.instance_key.specific_index()
{
outlined_entity.instances.entry(selected_index).or_default()
} else {
&mut outlined_entity.overall
};
*outline_mask_target = (*outline_mask_target)
.max(OutlineMaskPreference::some(0, next_selection_mask_index()));
}
}
};
}

SpaceViewHighlights {
highlighted_entity_paths,
outlines_masks,
}
}
}
4 changes: 2 additions & 2 deletions crates/re_viewer/src/ui/view_spatial/scene/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use re_log_types::{
component_types::{ClassId, KeypointId, Tensor},
MeshId,
};
use re_renderer::{Color32, Size};
use re_renderer::{renderer::OutlineMaskPreference, Color32, Size};

use super::{eye::Eye, SpaceCamera3D, SpatialNavigationMode};
use crate::{
Expand Down Expand Up @@ -55,7 +55,7 @@ pub struct MeshSource {
// TODO(andreas): Make this Conformal3 once glow is gone?
pub world_from_mesh: macaw::Affine3A,
pub mesh: Arc<LoadedMesh>,
pub additive_tint: Color32,
pub outline_mask: OutlineMaskPreference,
}

pub struct Image {
Expand Down
1 change: 1 addition & 0 deletions crates/re_viewer/src/ui/view_spatial/scene/picking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ pub fn picking(
points,
meshes,
depth_clouds: _, // no picking for depth clouds yet
any_outlines: _,
} = primitives;

picking_points(&context, &mut state, points);
Expand Down
Loading

1 comment on commit a0c4189

@github-actions
Copy link

Choose a reason for hiding this comment

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

Rust Benchmark

Benchmark suite Current: a0c4189 Previous: dd2a542 Ratio
datastore/insert/batch/rects/insert 544261 ns/iter (± 7746) 570651 ns/iter (± 3533) 0.95
datastore/latest_at/batch/rects/query 1777 ns/iter (± 21) 1801 ns/iter (± 8) 0.99
datastore/latest_at/missing_components/primary 351 ns/iter (± 4) 363 ns/iter (± 0) 0.97
datastore/latest_at/missing_components/secondaries 410 ns/iter (± 5) 425 ns/iter (± 0) 0.96
datastore/range/batch/rects/query 145353 ns/iter (± 2072) 149518 ns/iter (± 251) 0.97
mono_points_arrow/generate_message_bundles 44933418 ns/iter (± 1628532) 47416340 ns/iter (± 734945) 0.95
mono_points_arrow/generate_messages 122775580 ns/iter (± 1385766) 125466343 ns/iter (± 6485212) 0.98
mono_points_arrow/encode_log_msg 148283913 ns/iter (± 1577545) 150750510 ns/iter (± 1405395) 0.98
mono_points_arrow/encode_total 315642136 ns/iter (± 2493831) 325110979 ns/iter (± 2677415) 0.97
mono_points_arrow/decode_log_msg 173220112 ns/iter (± 1714685) 176547565 ns/iter (± 1348562) 0.98
mono_points_arrow/decode_message_bundles 63035296 ns/iter (± 844766) 63845690 ns/iter (± 777029) 0.99
mono_points_arrow/decode_total 234241203 ns/iter (± 2478412) 239380655 ns/iter (± 2386117) 0.98
batch_points_arrow/generate_message_bundles 321353 ns/iter (± 5079) 328465 ns/iter (± 3781) 0.98
batch_points_arrow/generate_messages 5982 ns/iter (± 90) 6110 ns/iter (± 94) 0.98
batch_points_arrow/encode_log_msg 359440 ns/iter (± 4053) 372431 ns/iter (± 2194) 0.97
batch_points_arrow/encode_total 696682 ns/iter (± 8564) 718603 ns/iter (± 43942) 0.97
batch_points_arrow/decode_log_msg 341016 ns/iter (± 3062) 347345 ns/iter (± 1881) 0.98
batch_points_arrow/decode_message_bundles 2024 ns/iter (± 31) 2086 ns/iter (± 22) 0.97
batch_points_arrow/decode_total 349163 ns/iter (± 3289) 352211 ns/iter (± 1735) 0.99
arrow_mono_points/insert 5968647020 ns/iter (± 17782098) 6072775620 ns/iter (± 14109546) 0.98
arrow_mono_points/query 1646371 ns/iter (± 22599) 1705040 ns/iter (± 18823) 0.97
arrow_batch_points/insert 2573089 ns/iter (± 30807) 2586681 ns/iter (± 19981) 0.99
arrow_batch_points/query 16294 ns/iter (± 269) 16738 ns/iter (± 187) 0.97
arrow_batch_vecs/insert 41311 ns/iter (± 586) 42837 ns/iter (± 385) 0.96
arrow_batch_vecs/query 486457 ns/iter (± 6856) 502694 ns/iter (± 8265) 0.97
tuid/Tuid::random 33 ns/iter (± 0) 34 ns/iter (± 0) 0.97

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.