diff --git a/crates/store/re_query/src/latest_at.rs b/crates/store/re_query/src/latest_at.rs index eb0afd78425d..922cd3a648b1 100644 --- a/crates/store/re_query/src/latest_at.rs +++ b/crates/store/re_query/src/latest_at.rs @@ -508,7 +508,7 @@ impl LatestAtResults { /// Returns the deserialized data for the specified component, assuming a mono-batch. /// - /// Returns an error if the data cannot be deserialized, or if the underlying batch is not of unit length. + /// Logs an error if the data cannot be deserialized, or if the underlying batch is not of unit length. #[inline] pub fn component_mono(&self) -> Option { self.component_mono_with_log_level(re_log::Level::Error) @@ -516,7 +516,7 @@ impl LatestAtResults { /// Returns the deserialized data for the specified component, assuming a mono-batch. /// - /// Returns an error if the data cannot be deserialized, or if the underlying batch is not of unit length. + /// Returns none if the data cannot be deserialized, or if the underlying batch is not of unit length. #[inline] pub fn component_mono_quiet(&self) -> Option { self.components diff --git a/crates/store/re_types/src/archetypes/pinhole_ext.rs b/crates/store/re_types/src/archetypes/pinhole_ext.rs index 787e2b932837..dba2076ef46b 100644 --- a/crates/store/re_types/src/archetypes/pinhole_ext.rs +++ b/crates/store/re_types/src/archetypes/pinhole_ext.rs @@ -1,8 +1,16 @@ -use crate::datatypes::Vec2D; +use crate::{components::ViewCoordinates, datatypes::Vec2D}; use super::Pinhole; impl Pinhole { + /// Camera orientation used when there's no camera orientation explicitly logged. + /// + /// - x pointing right + /// - y pointing down + /// - z pointing into the image plane + /// (this is convenient for reading out a depth image which has typically positive z values) + pub const DEFAULT_CAMERA_XYZ: ViewCoordinates = ViewCoordinates::RDF; + /// Creates a pinhole from the camera focal length and resolution, both specified in pixels. /// /// The focal length is the diagonal of the projection matrix. diff --git a/crates/viewer/re_view/src/query.rs b/crates/viewer/re_view/src/query.rs index 7365c2324cc8..d997c2a94e35 100644 --- a/crates/viewer/re_view/src/query.rs +++ b/crates/viewer/re_view/src/query.rs @@ -215,6 +215,12 @@ pub trait DataResultQuery { latest_at_query: &'a LatestAtQuery, ) -> HybridLatestAtResults<'a>; + fn latest_at_with_blueprint_resolved_data_for_component<'a, C: re_types_core::Component>( + &'a self, + ctx: &'a ViewContext<'a>, + latest_at_query: &'a LatestAtQuery, + ) -> HybridLatestAtResults<'a>; + fn query_archetype_with_history<'a, A: re_types_core::Archetype>( &'a self, ctx: &'a ViewContext<'a>, @@ -235,14 +241,30 @@ impl DataResultQuery for DataResult { ctx: &'a ViewContext<'a>, latest_at_query: &'a LatestAtQuery, ) -> HybridLatestAtResults<'a> { - let query_shadowed_defaults = false; + let query_shadowed_components = false; latest_at_with_blueprint_resolved_data( ctx, None, latest_at_query, self, A::all_components().iter().map(|descr| descr.component_name), - query_shadowed_defaults, + query_shadowed_components, + ) + } + + fn latest_at_with_blueprint_resolved_data_for_component<'a, C: re_types_core::Component>( + &'a self, + ctx: &'a ViewContext<'a>, + latest_at_query: &'a LatestAtQuery, + ) -> HybridLatestAtResults<'a> { + let query_shadowed_components = false; + latest_at_with_blueprint_resolved_data( + ctx, + None, + latest_at_query, + self, + std::iter::once(C::name()), + query_shadowed_components, ) } diff --git a/crates/viewer/re_view_spatial/src/contexts/mod.rs b/crates/viewer/re_view_spatial/src/contexts/mod.rs index 7bab048b990c..2bb6a7ac3877 100644 --- a/crates/viewer/re_view_spatial/src/contexts/mod.rs +++ b/crates/viewer/re_view_spatial/src/contexts/mod.rs @@ -1,10 +1,10 @@ mod depth_offsets; -mod transform_context; +mod transform_tree_context; pub use depth_offsets::EntityDepthOffsets; use re_types::ViewClassIdentifier; use re_view::AnnotationSceneContext; -pub use transform_context::{TransformContext, TransformInfo, TwoDInThreeDTransformInfo}; +pub use transform_tree_context::{TransformInfo, TransformTreeContext, TwoDInThreeDTransformInfo}; // ----------------------------------------------------------------------------- @@ -24,7 +24,7 @@ pub struct SpatialSceneEntityContext<'a> { pub fn register_spatial_contexts( system_registry: &mut re_viewer_context::ViewSystemRegistrator<'_>, ) -> Result<(), ViewClassRegistryError> { - system_registry.register_context_system::()?; + system_registry.register_context_system::()?; system_registry.register_context_system::()?; system_registry.register_context_system::()?; Ok(()) diff --git a/crates/viewer/re_view_spatial/src/contexts/transform_context.rs b/crates/viewer/re_view_spatial/src/contexts/transform_tree_context.rs similarity index 50% rename from crates/viewer/re_view_spatial/src/contexts/transform_context.rs rename to crates/viewer/re_view_spatial/src/contexts/transform_tree_context.rs index 4e7cbc27eb5c..6d474bd6b525 100644 --- a/crates/viewer/re_view_spatial/src/contexts/transform_context.rs +++ b/crates/viewer/re_view_spatial/src/contexts/transform_tree_context.rs @@ -1,23 +1,23 @@ -use itertools::Either; use nohash_hasher::IntMap; use re_chunk_store::LatestAtQuery; -use re_entity_db::{EntityDb, EntityPath, EntityTree}; +use re_entity_db::{EntityPath, EntityTree}; +use re_log_types::EntityPathHash; use re_types::{ - archetypes::{InstancePoses3D, Pinhole, Transform3D}, - components::{ - ImagePlaneDistance, PinholeProjection, PoseRotationAxisAngle, PoseRotationQuat, - PoseScale3D, PoseTransformMat3x3, PoseTranslation3D, RotationAxisAngle, RotationQuat, - Scale3D, TransformMat3x3, TransformRelation, Translation3D, ViewCoordinates, - }, + archetypes::{InstancePoses3D, Transform3D}, + components::{ImagePlaneDistance, PinholeProjection}, Archetype, Component as _, ComponentNameSet, }; use re_view::DataResultQuery as _; -use re_viewer_context::{IdentifiedViewSystem, ViewContext, ViewContextSystem}; +use re_viewer_context::{ + DataResultNode, DataResultTree, IdentifiedViewSystem, ViewContext, ViewContextSystem, +}; use vec1::smallvec_v1::SmallVec1; use crate::{ - transform_component_tracker::TransformComponentTrackerStoreSubscriber, + transform_cache::{ + CachedTransformsPerTimeline, ResolvedPinholeProjection, TransformCacheStoreSubscriber, + }, visualizers::image_view_coordinates, }; @@ -107,54 +107,50 @@ impl TransformInfo { } } -#[derive(Clone, Copy)] -enum UnreachableTransformReason { - /// More than one pinhole camera between this and the reference space. - NestedPinholeCameras, -} - /// Provides transforms from an entity to a chosen reference space for all elements in the scene /// for the currently selected time & timeline. /// +/// The resulting transforms are dependent on: +/// * tree, pose, pinhole and view-coordinates transforms components as logged to the data store +/// * TODO(#6743): blueprint overrides aren't respected yet +/// * the view' spatial origin +/// * the query time +/// * TODO(#723): ranges aren't taken into account yet +/// * TODO(andreas): the queried entities. Right now we determine transforms for ALL entities in the scene. +/// since 3D views tend to display almost everything that's mostly fine, but it's very wasteful when they don't. +/// /// The renderer then uses this reference space as its world space, /// making world and reference space equivalent for a given view. /// -/// Should be recomputed every frame. -/// -/// TODO(#7025): Alternative proposal to not have to deal with tree upwards walking & per-origin tree walking. +/// TODO(#7025): Right now we also do full tree traversal in here to resolve transforms to the root. +/// However, for views that share the same query, we can easily make all entities relative to the respective origin in a linear pass over all matrices. +/// (Note that right now the query IS always the same across all views for a given frame since it's just latest-at controlled by the timeline, +/// but once we support range queries it may be not or only partially the case) #[derive(Clone)] -pub struct TransformContext { +pub struct TransformTreeContext { /// All transforms provided are relative to this reference path. space_origin: EntityPath, /// All reachable entities. - transform_per_entity: IntMap, - - /// All unreachable descendant paths of `reference_path`. - unreachable_descendants: Vec<(EntityPath, UnreachableTransformReason)>, - - /// The first parent of `reference_path` that is no longer reachable. - first_unreachable_parent: Option<(EntityPath, UnreachableTransformReason)>, + transform_per_entity: IntMap, } -impl IdentifiedViewSystem for TransformContext { +impl IdentifiedViewSystem for TransformTreeContext { fn identifier() -> re_viewer_context::ViewSystemIdentifier { "TransformContext".into() } } -impl Default for TransformContext { +impl Default for TransformTreeContext { fn default() -> Self { Self { space_origin: EntityPath::root(), transform_per_entity: Default::default(), - unreachable_descendants: Default::default(), - first_unreachable_parent: None, } } } -impl ViewContextSystem for TransformContext { +impl ViewContextSystem for TransformTreeContext { fn compatible_component_sets(&self) -> Vec { vec![ Transform3D::all_components() @@ -180,10 +176,19 @@ impl ViewContextSystem for TransformContext { query: &re_viewer_context::ViewQuery<'_>, ) { re_tracing::profile_function!(); - debug_assert_transform_field_order(ctx.viewer_ctx.reflection); + // Make sure transform cache is up to date. + // TODO(andreas): This is a rather annoying sync point between different views. + // We could alleviate this by introducing a per view class (not instance) method that is called + // before system execution. + TransformCacheStoreSubscriber::access_mut(&ctx.recording().store_id(), |cache| { + cache.apply_all_updates(ctx.recording()); + }); + let entity_tree = ctx.recording().tree(); + let query_result = ctx.viewer_ctx.lookup_query_result(query.view_id); + let data_result_tree = &query_result.tree; self.space_origin = query.space_origin.clone(); @@ -197,19 +202,44 @@ impl ViewContextSystem for TransformContext { let time_query = ctx.current_query(); - // Child transforms of this space - self.gather_descendants_transforms( - ctx, - query, - current_tree, - ctx.recording(), - &time_query, - // Ignore potential pinhole camera at the root of the view, since it regarded as being "above" this root. - TransformInfo::default(), - ); + TransformCacheStoreSubscriber::access(&ctx.recording().store_id(), |cache| { + let Some(transforms_per_timeline) = cache.transforms_per_timeline(query.timeline) + else { + // No transforms on this timeline at all. In other words, everything is identity! + query_result.tree.visit(&mut |node: &DataResultNode| { + self.transform_per_entity.insert( + node.data_result.entity_path.hash(), + TransformInfo::default(), + ); + true + }); + return; + }; + + // Child transforms of this space + { + re_tracing::profile_scope!("gather_descendants_transforms"); + + self.gather_descendants_transforms( + ctx, + data_result_tree, + current_tree, + &time_query, + // Ignore potential pinhole camera at the root of the view, since it is regarded as being "above" this root. + TransformInfo::default(), + transforms_per_timeline, + ); + } - // Walk up from the reference to the highest reachable parent. - self.gather_parent_transforms(ctx, query, current_tree, &time_query); + // Walk up from the reference to the highest reachable parent. + self.gather_parent_transforms( + ctx, + data_result_tree, + current_tree, + &time_query, + transforms_per_timeline, + ); + }); // Note that this can return None if no event has happened for this timeline yet. } fn as_any(&self) -> &dyn std::any::Any { @@ -217,64 +247,56 @@ impl ViewContextSystem for TransformContext { } } -impl TransformContext { +impl TransformTreeContext { /// Gather transforms for everything _above_ the root. fn gather_parent_transforms<'a>( &mut self, ctx: &'a ViewContext<'a>, - query: &re_viewer_context::ViewQuery<'_>, + data_result_tree: &DataResultTree, mut current_tree: &'a EntityTree, time_query: &LatestAtQuery, + transforms_per_timeline: &CachedTransformsPerTimeline, ) { re_tracing::profile_function!(); let entity_tree = ctx.recording().tree(); - let mut encountered_pinhole = None; let mut reference_from_ancestor = glam::Affine3A::IDENTITY; while let Some(parent_path) = current_tree.path.parent() { let Some(parent_tree) = entity_tree.subtree(&parent_path) else { // Unlike not having the space path in the hierarchy, this should be impossible. re_log::error_once!( - "Path {} is not part of the global entity tree whereas its child {} is", - parent_path, - query.space_origin + "Path {parent_path} is not part of the global entity tree whereas its child is" ); return; }; // Note that the transform at the reference is the first that needs to be inverted to "break out" of its hierarchy. // Generally, the transform _at_ a node isn't relevant to it's children, but only to get to its parent in turn! - let new_transform = match transforms_at( + let transforms_at_entity = transforms_at( ¤t_tree.path, - ctx.recording(), time_query, // TODO(#1025): See comment in transform_at. This is a workaround for precision issues // and the fact that there is no meaningful image plane distance for 3D->2D views. |_| 500.0, - &mut encountered_pinhole, - ) { - Err(unreachable_reason) => { - self.first_unreachable_parent = - Some((parent_tree.path.clone(), unreachable_reason)); - break; - } - Ok(transforms_at_entity) => transform_info_for_upward_propagation( - reference_from_ancestor, - transforms_at_entity, - ), - }; + &mut None, // Don't care about pinhole encounters. + transforms_per_timeline, + ); + let new_transform = transform_info_for_upward_propagation( + reference_from_ancestor, + &transforms_at_entity, + ); reference_from_ancestor = new_transform.reference_from_entity; // (this skips over everything at and under `current_tree` automatically) self.gather_descendants_transforms( ctx, - query, + data_result_tree, parent_tree, - ctx.recording(), time_query, new_transform, + transforms_per_timeline, ); current_tree = parent_tree; @@ -285,15 +307,15 @@ impl TransformContext { fn gather_descendants_transforms( &mut self, ctx: &ViewContext<'_>, - view_query: &re_viewer_context::ViewQuery<'_>, + data_result_tree: &DataResultTree, subtree: &EntityTree, - entity_db: &EntityDb, query: &LatestAtQuery, transform: TransformInfo, + transforms_per_timeline: &CachedTransformsPerTimeline, ) { let twod_in_threed_info = transform.twod_in_threed_info.clone(); let reference_from_parent = transform.reference_from_entity; - match self.transform_per_entity.entry(subtree.path.clone()) { + match self.transform_per_entity.entry(subtree.path.hash()) { std::collections::hash_map::Entry::Occupied(_) => { return; } @@ -305,54 +327,34 @@ impl TransformContext { for child_tree in subtree.children.values() { let child_path = &child_tree.path; - let lookup_image_plane = |p: &_| { - let query_result = ctx.viewer_ctx.lookup_query_result(view_query.view_id); - - query_result - .tree - .lookup_result_by_path(p) - .cloned() - .map(|data_result| { - let results = data_result - .latest_at_with_blueprint_resolved_data::(ctx, query); - - results.get_mono_with_fallback::() - }) - .unwrap_or_default() - .into() - }; + let lookup_image_plane = + |p: &_| lookup_image_plane_distance(ctx, data_result_tree, p, query); let mut encountered_pinhole = twod_in_threed_info .as_ref() .map(|info| info.parent_pinhole.clone()); - let new_transform = match transforms_at( + + let transforms_at_entity = transforms_at( child_path, - entity_db, query, lookup_image_plane, &mut encountered_pinhole, - ) { - Err(unreachable_reason) => { - self.unreachable_descendants - .push((child_path.clone(), unreachable_reason)); - continue; - } - - Ok(transforms_at_entity) => transform_info_for_downward_propagation( - child_path, - reference_from_parent, - twod_in_threed_info.clone(), - transforms_at_entity, - ), - }; + transforms_per_timeline, + ); + let new_transform = transform_info_for_downward_propagation( + child_path, + reference_from_parent, + twod_in_threed_info.clone(), + &transforms_at_entity, + ); self.gather_descendants_transforms( ctx, - view_query, + data_result_tree, child_tree, - entity_db, query, new_transform, + transforms_per_timeline, ); } } @@ -364,15 +366,35 @@ impl TransformContext { /// Retrieves transform information for a given entity. /// /// Returns `None` if it's not reachable from the view's origin. - pub fn transform_info_for_entity(&self, ent_path: &EntityPath) -> Option<&TransformInfo> { - self.transform_per_entity.get(ent_path) + pub fn transform_info_for_entity(&self, ent_path: EntityPathHash) -> Option<&TransformInfo> { + self.transform_per_entity.get(&ent_path) } } +fn lookup_image_plane_distance( + ctx: &ViewContext<'_>, + data_result_tree: &DataResultTree, + entity_path: &EntityPath, + query: &LatestAtQuery, +) -> f32 { + data_result_tree + .lookup_result_by_path(entity_path) + .cloned() + .map(|data_result| { + data_result + .latest_at_with_blueprint_resolved_data_for_component::( + ctx, query, + ) + .get_mono_with_fallback::() + }) + .unwrap_or_default() + .into() +} + /// Compute transform info for when we walk up the tree from the reference. fn transform_info_for_upward_propagation( reference_from_ancestor: glam::Affine3A, - transforms_at_entity: TransformsAtEntity, + transforms_at_entity: &TransformsAtEntity<'_>, ) -> TransformInfo { let mut reference_from_entity = reference_from_ancestor; @@ -390,7 +412,7 @@ fn transform_info_for_upward_propagation( // Collect & compute poses. let (mut reference_from_instances, has_instance_transforms) = - if let Ok(mut entity_from_instances) = SmallVec1::<[glam::Affine3A; 1]>::try_from_vec( + if let Ok(mut entity_from_instances) = SmallVec1::<[glam::Affine3A; 1]>::try_from_slice( transforms_at_entity.entity_from_instance_poses, ) { for entity_from_instance in &mut entity_from_instances { @@ -402,18 +424,16 @@ fn transform_info_for_upward_propagation( (SmallVec1::new(reference_from_entity), false) }; - // Apply tree transform if any. - if let Some(parent_from_entity_tree_transform) = - transforms_at_entity.parent_from_entity_tree_transform - { - reference_from_entity *= parent_from_entity_tree_transform.inverse(); - if has_instance_transforms { - for reference_from_instance in &mut reference_from_instances { - *reference_from_instance = reference_from_entity * (*reference_from_instance); - } - } else { - *reference_from_instances.first_mut() = reference_from_entity; + // Apply tree transform. + reference_from_entity *= transforms_at_entity + .parent_from_entity_tree_transform + .inverse(); + if has_instance_transforms { + for reference_from_instance in &mut reference_from_instances { + *reference_from_instance = reference_from_entity * (*reference_from_instance); } + } else { + *reference_from_instances.first_mut() = reference_from_entity; } TransformInfo { @@ -431,21 +451,18 @@ fn transform_info_for_downward_propagation( current_path: &EntityPath, reference_from_parent: glam::Affine3A, mut twod_in_threed_info: Option, - transforms_at_entity: TransformsAtEntity, + transforms_at_entity: &TransformsAtEntity<'_>, ) -> TransformInfo { let mut reference_from_entity = reference_from_parent; // Apply tree transform. - if let Some(parent_from_entity_tree_transform) = - transforms_at_entity.parent_from_entity_tree_transform - { - reference_from_entity *= parent_from_entity_tree_transform; - } + + reference_from_entity *= transforms_at_entity.parent_from_entity_tree_transform; // Collect & compute poses. let (mut reference_from_instances, has_instance_transforms) = if let Ok(mut entity_from_instances) = - SmallVec1::try_from_vec(transforms_at_entity.entity_from_instance_poses) + SmallVec1::try_from_slice(transforms_at_entity.entity_from_instance_poses) { for entity_from_instance in &mut entity_from_instances { *entity_from_instance = reference_from_entity * (*entity_from_instance); @@ -491,15 +508,16 @@ fn transform_info_for_downward_propagation( #[cfg(debug_assertions)] fn debug_assert_transform_field_order(reflection: &re_types::reflection::Reflection) { + use re_types::{components, Archetype as _}; + let expected_order = vec![ - Translation3D::name(), - RotationAxisAngle::name(), - RotationQuat::name(), - Scale3D::name(), - TransformMat3x3::name(), + components::Translation3D::name(), + components::RotationAxisAngle::name(), + components::RotationQuat::name(), + components::Scale3D::name(), + components::TransformMat3x3::name(), ]; - use re_types::Archetype as _; let transform3d_reflection = reflection .archetypes .get(&re_types::archetypes::Transform3D::name()) @@ -528,276 +546,87 @@ But they are instead ordered like this:\n{actual_order:?}" #[cfg(not(debug_assertions))] fn debug_assert_transform_field_order(_: &re_types::reflection::Reflection) {} -fn query_and_resolve_tree_transform_at_entity( +fn transform_from_pinhole_with_image_plane( entity_path: &EntityPath, - entity_db: &EntityDb, - query: &LatestAtQuery, - transform3d_components: impl Iterator, -) -> Option { - // TODO(#6743): Doesn't take into account overrides. - let result = entity_db.latest_at(query, entity_path, transform3d_components); - if result.components.is_empty() { - return None; - } - - let mut transform = glam::Affine3A::IDENTITY; - - // Order see `debug_assert_transform_field_order` - if let Some(translation) = result.component_instance::(0) { - transform = glam::Affine3A::from(translation); - } - if let Some(axis_angle) = result.component_instance::(0) { - if let Ok(axis_angle) = glam::Affine3A::try_from(axis_angle) { - transform *= axis_angle; - } else { - // Invalid transform. - return None; - } - } - if let Some(quaternion) = result.component_instance::(0) { - if let Ok(quaternion) = glam::Affine3A::try_from(quaternion) { - transform *= quaternion; - } else { - // Invalid transform. - return None; - } - } - if let Some(scale) = result.component_instance::(0) { - if scale.x() == 0.0 && scale.y() == 0.0 && scale.z() == 0.0 { - // Invalid scale. - return None; - } - transform *= glam::Affine3A::from(scale); - } - if let Some(mat3x3) = result.component_instance::(0) { - let affine_transform = glam::Affine3A::from(mat3x3); - if affine_transform.matrix3.determinant() == 0.0 { - // Invalid transform. - return None; - } - transform *= affine_transform; - } - - if result.component_instance::(0) == Some(TransformRelation::ChildFromParent) - // TODO(andreas): Should we warn? This might be intentionally caused by zero scale. - && transform.matrix3.determinant() != 0.0 - { - transform = transform.inverse(); - } - - Some(transform) -} - -fn query_and_resolve_instance_poses_at_entity( - entity_path: &EntityPath, - entity_db: &EntityDb, - query: &LatestAtQuery, - pose3d_components: impl Iterator, -) -> Vec { - // TODO(#6743): Doesn't take into account overrides. - let result = entity_db.latest_at(query, entity_path, pose3d_components); - - let max_count = result - .components - .iter() - .map(|(name, row)| row.num_instances(name)) - .max() - .unwrap_or(0) as usize; - - if max_count == 0 { - return Vec::new(); - } - - #[inline] - pub fn clamped_or_nothing( - values: Vec, - clamped_len: usize, - ) -> impl Iterator { - let Some(last) = values.last() else { - return Either::Left(std::iter::empty()); - }; - let last = last.clone(); - Either::Right( - values - .into_iter() - .chain(std::iter::repeat(last)) - .take(clamped_len), - ) - } - - let mut iter_translation = clamped_or_nothing( - result - .component_batch::() - .unwrap_or_default(), - max_count, - ); - let mut iter_rotation_quat = clamped_or_nothing( - result - .component_batch::() - .unwrap_or_default(), - max_count, - ); - let mut iter_rotation_axis_angle = clamped_or_nothing( - result - .component_batch::() - .unwrap_or_default(), - max_count, - ); - let mut iter_scale = clamped_or_nothing( - result.component_batch::().unwrap_or_default(), - max_count, - ); - let mut iter_mat3x3 = clamped_or_nothing( - result - .component_batch::() - .unwrap_or_default(), - max_count, - ); - - let mut transforms = Vec::with_capacity(max_count); - for _ in 0..max_count { - // Order see `debug_assert_transform_field_order` - let mut transform = glam::Affine3A::IDENTITY; - if let Some(translation) = iter_translation.next() { - transform = glam::Affine3A::from(translation); - } - if let Some(rotation_quat) = iter_rotation_quat.next() { - if let Ok(rotation_quat) = glam::Affine3A::try_from(rotation_quat) { - transform *= rotation_quat; - } else { - transform = glam::Affine3A::ZERO; - } - } - if let Some(rotation_axis_angle) = iter_rotation_axis_angle.next() { - if let Ok(axis_angle) = glam::Affine3A::try_from(rotation_axis_angle) { - transform *= axis_angle; - } else { - transform = glam::Affine3A::ZERO; - } - } - if let Some(scale) = iter_scale.next() { - transform *= glam::Affine3A::from(scale); - } - if let Some(mat3x3) = iter_mat3x3.next() { - transform *= glam::Affine3A::from(mat3x3); - } - - transforms.push(transform); - } - - transforms -} - -fn query_and_resolve_obj_from_pinhole_image_plane( - entity_path: &EntityPath, - entity_db: &EntityDb, - query: &LatestAtQuery, + resolved_pinhole_projection: &ResolvedPinholeProjection, pinhole_image_plane_distance: impl Fn(&EntityPath) -> f32, -) -> Option { - entity_db - .latest_at_component::(entity_path, query) - .map(|(_index, image_from_camera)| { - ( - image_from_camera, - entity_db - .latest_at_component::(entity_path, query) - .map_or(ViewCoordinates::RDF, |(_index, res)| res), - ) - }) - .map(|(image_from_camera, view_coordinates)| { - // Everything under a pinhole camera is a 2D projection, thus doesn't actually have a proper 3D representation. - // Our visualization interprets this as looking at a 2D image plane from a single point (the pinhole). - - // Center the image plane and move it along z, scaling the further the image plane is. - let distance = pinhole_image_plane_distance(entity_path); - let focal_length = image_from_camera.focal_length_in_pixels(); - let focal_length = glam::vec2(focal_length.x(), focal_length.y()); - let scale = distance / focal_length; - let translation = (-image_from_camera.principal_point() * scale).extend(distance); - - let image_plane3d_from_2d_content = glam::Affine3A::from_translation(translation) +) -> glam::Affine3A { + let ResolvedPinholeProjection { + image_from_camera, + view_coordinates, + } = resolved_pinhole_projection; + + // Everything under a pinhole camera is a 2D projection, thus doesn't actually have a proper 3D representation. + // Our visualization interprets this as looking at a 2D image plane from a single point (the pinhole). + + // Center the image plane and move it along z, scaling the further the image plane is. + let distance = pinhole_image_plane_distance(entity_path); + let focal_length = image_from_camera.focal_length_in_pixels(); + let focal_length = glam::vec2(focal_length.x(), focal_length.y()); + let scale = distance / focal_length; + let translation = (-image_from_camera.principal_point() * scale).extend(distance); + + let image_plane3d_from_2d_content = glam::Affine3A::from_translation(translation) // We want to preserve any depth that might be on the pinhole image. // Use harmonic mean of x/y scale for those. * glam::Affine3A::from_scale( scale.extend(2.0 / (1.0 / scale.x + 1.0 / scale.y)), ); - // Our interpretation of the pinhole camera implies that the axis semantics, i.e. ViewCoordinates, - // determine how the image plane is oriented. - // (see also `CamerasPart` where the frustum lines are set up) - let obj_from_image_plane3d = view_coordinates.from_other(&image_view_coordinates()); + // Our interpretation of the pinhole camera implies that the axis semantics, i.e. ViewCoordinates, + // determine how the image plane is oriented. + // (see also `CamerasPart` where the frustum lines are set up) + let obj_from_image_plane3d = view_coordinates.from_other(&image_view_coordinates()); - glam::Affine3A::from_mat3(obj_from_image_plane3d) * image_plane3d_from_2d_content + glam::Affine3A::from_mat3(obj_from_image_plane3d) * image_plane3d_from_2d_content - // Above calculation is nice for a certain kind of visualizing a projected image plane, - // but the image plane distance is arbitrary and there might be other, better visualizations! + // Above calculation is nice for a certain kind of visualizing a projected image plane, + // but the image plane distance is arbitrary and there might be other, better visualizations! - // TODO(#1025): - // As such we don't ever want to invert this matrix! - // However, currently our 2D views require do to exactly that since we're forced to - // build a relationship between the 2D plane and the 3D world, when actually the 2D plane - // should have infinite depth! - // The inverse of this matrix *is* working for this, but quickly runs into precision issues. - // See also `ui_2d.rs#setup_target_config` - }) + // TODO(#1025): + // As such we don't ever want to invert this matrix! + // However, currently our 2D views require do to exactly that since we're forced to + // build a relationship between the 2D plane and the 3D world, when actually the 2D plane + // should have infinite depth! + // The inverse of this matrix *is* working for this, but quickly runs into precision issues. + // See also `ui_2d.rs#setup_target_config` } /// Resolved transforms at an entity. #[derive(Default)] -struct TransformsAtEntity { - parent_from_entity_tree_transform: Option, - entity_from_instance_poses: Vec, +struct TransformsAtEntity<'a> { + parent_from_entity_tree_transform: glam::Affine3A, + entity_from_instance_poses: &'a [glam::Affine3A], instance_from_pinhole_image_plane: Option, } -fn transforms_at( +fn transforms_at<'a>( entity_path: &EntityPath, - entity_db: &EntityDb, query: &LatestAtQuery, pinhole_image_plane_distance: impl Fn(&EntityPath) -> f32, encountered_pinhole: &mut Option, -) -> Result { + transforms_per_timeline: &'a CachedTransformsPerTimeline, +) -> TransformsAtEntity<'a> { // This is called very frequently, don't put a profile scope here. - let potential_transform_components = - TransformComponentTrackerStoreSubscriber::access(&entity_db.store_id(), |tracker| { - tracker.potential_transform_components(entity_path).cloned() - }) - .flatten() - .unwrap_or_default(); - - let parent_from_entity_tree_transform = if potential_transform_components.transform3d.is_empty() - { - None - } else { - query_and_resolve_tree_transform_at_entity( - entity_path, - entity_db, - query, - potential_transform_components.transform3d.iter().copied(), - ) - }; - let entity_from_instance_poses = if potential_transform_components.pose3d.is_empty() { - Vec::new() - } else { - query_and_resolve_instance_poses_at_entity( - entity_path, - entity_db, - query, - potential_transform_components.pose3d.iter().copied(), - ) - }; - let instance_from_pinhole_image_plane = if potential_transform_components.pinhole { - query_and_resolve_obj_from_pinhole_image_plane( - entity_path, - entity_db, - query, - pinhole_image_plane_distance, - ) - } else { - None + let Some(entity_transforms) = transforms_per_timeline.entity_transforms(entity_path.hash()) + else { + return TransformsAtEntity::default(); }; + let parent_from_entity_tree_transform = entity_transforms.latest_at_tree_transform(query); + let entity_from_instance_poses = entity_transforms.latest_at_instance_poses(query); + let instance_from_pinhole_image_plane = + entity_transforms + .latest_at_pinhole(query) + .map(|resolved_pinhole_projection| { + transform_from_pinhole_with_image_plane( + entity_path, + resolved_pinhole_projection, + pinhole_image_plane_distance, + ) + }); + let transforms_at_entity = TransformsAtEntity { parent_from_entity_tree_transform, entity_from_instance_poses, @@ -809,12 +638,8 @@ fn transforms_at( .instance_from_pinhole_image_plane .is_some() { - if encountered_pinhole.is_some() { - return Err(UnreachableTransformReason::NestedPinholeCameras); - } else { - *encountered_pinhole = Some(entity_path.clone()); - } + *encountered_pinhole = Some(entity_path.clone()); } - Ok(transforms_at_entity) + transforms_at_entity } diff --git a/crates/viewer/re_view_spatial/src/lib.rs b/crates/viewer/re_view_spatial/src/lib.rs index 62cf77783d4f..1ce80145f306 100644 --- a/crates/viewer/re_view_spatial/src/lib.rs +++ b/crates/viewer/re_view_spatial/src/lib.rs @@ -19,7 +19,6 @@ mod proc_mesh; mod scene_bounding_boxes; mod space_camera_3d; mod spatial_topology; -mod transform_component_tracker; mod ui; mod ui_2d; mod ui_3d; @@ -29,6 +28,8 @@ mod view_3d; mod view_3d_properties; mod visualizers; +mod transform_cache; + pub use view_2d::SpatialView2D; pub use view_3d::SpatialView3D; diff --git a/crates/viewer/re_view_spatial/src/transform_cache.rs b/crates/viewer/re_view_spatial/src/transform_cache.rs new file mode 100644 index 000000000000..87922ecb7eef --- /dev/null +++ b/crates/viewer/re_view_spatial/src/transform_cache.rs @@ -0,0 +1,1093 @@ +use std::collections::BTreeMap; + +use ahash::{HashMap, HashSet}; +use glam::Affine3A; +use itertools::Either; +use nohash_hasher::{IntMap, IntSet}; + +use once_cell::sync::OnceCell; +use re_chunk_store::{ + ChunkStore, ChunkStoreSubscriberHandle, LatestAtQuery, PerStoreChunkSubscriber, +}; +use re_entity_db::EntityDb; +use re_log_types::{EntityPath, EntityPathHash, StoreId, TimeInt, Timeline}; +use re_types::{ + archetypes::{self}, + components::{self}, + Archetype as _, Component, ComponentName, +}; + +/// Store subscriber that resolves all transform components at a given entity to an affine transform. +/// +/// It only handles resulting transforms individually to each entity, not how these transforms propagate in the tree. +/// For transform tree propagation see [`crate::contexts::TransformTreeContext`]. +/// +/// There are different kinds of transforms handled here: +/// * [`archetypes::Transform3D`] +/// Tree transforms that should propagate in the tree (via [`crate::contexts::TransformTreeContext`]). +/// * [`archetypes::InstancePoses3D`] +/// Instance poses that should be applied to the tree transforms (via [`crate::contexts::TransformTreeContext`]) but not propagate. +/// * [`components::PinholeProjection`] and [`components::ViewCoordinates`] +/// Pinhole projections & associated view coordinates used for visualizing cameras in 3D and embedding 2D in 3D +pub struct TransformCacheStoreSubscriber { + /// All components of [`archetypes::Transform3D`] + transform_components: IntSet, + + /// All components of [`archetypes::InstancePoses3D`] + pose_components: IntSet, + + /// All components related to pinholes (i.e. [`components::PinholeProjection`] and [`components::ViewCoordinates`]). + pinhole_components: IntSet, + + per_timeline: HashMap, +} + +impl Default for TransformCacheStoreSubscriber { + #[inline] + fn default() -> Self { + use re_types::Archetype as _; + + Self { + transform_components: archetypes::Transform3D::all_components() + .iter() + .map(|descr| descr.component_name) + .collect(), + pose_components: archetypes::InstancePoses3D::all_components() + .iter() + .map(|descr| descr.component_name) + .collect(), + pinhole_components: [ + components::PinholeProjection::name(), + components::ViewCoordinates::name(), + ] + .into_iter() + .collect(), + + per_timeline: Default::default(), + } + } +} + +bitflags::bitflags! { + /// Flags for the different kinds of independent transforms that the transform cache handles. + #[derive(Debug, Clone, Copy)] + pub struct TransformAspect: u8 { + /// The entity has a tree transform, i.e. any non-style component of [`archetypes::Transform3D`]. + const Tree = 1 << 0; + + /// The entity has instance poses, i.e. any non-style component of [`archetypes::InstancePoses3D`]. + const Pose = 1 << 1; + + /// The entity has a pinhole projection or view coordinates, i.e. either [`components::PinholeProjection`] or [`components::ViewCoordinates`]. + const PinholeOrViewCoordinates = 1 << 2; + } +} + +/// Points in time that have changed for a given entity, +/// i.e. the cache is invalid for these times. +#[derive(Debug)] +struct InvalidatedTransforms { + entity_path: EntityPath, + times: Vec, + aspects: TransformAspect, +} + +#[derive(Default)] +pub struct CachedTransformsPerTimeline { + /// Updates that should be applied to the cache. + /// I.e. times & entities at which the cache is invalid right now. + invalidated_transforms: Vec, + + per_entity: IntMap, +} + +type PoseTransformMap = BTreeMap>; + +/// Maps from time to pinhole projection. +/// +/// Unlike with tree & pose transforms, there's identity value that we can insert upon clears. +/// (clears here meaning that the user first logs a pinhole and then later either logs a clear or an empty pinhole array) +/// Therefore, we instead store those events as `None` values to ensure that everything after a clear +/// is properly marked as having no pinhole projection. +type PinholeProjectionMap = BTreeMap>; + +pub struct PerTimelinePerEntityTransforms { + timeline: Timeline, + + tree_transforms: BTreeMap, + + // Pose transforms and pinhole projections are typically more rare, which is why we store them as optional boxes. + pose_transforms: Option>, + pinhole_projections: Option>, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct ResolvedPinholeProjection { + pub image_from_camera: components::PinholeProjection, + + /// View coordinates at this pinhole camera. + /// + /// This is needed to orient 2D in 3D and 3D in 2D the right way around + /// (answering questions like which axis is distance to viewer increasing). + /// If no view coordinates were logged, this is set to [`archetypes::Pinhole::DEFAULT_CAMERA_XYZ`]. + pub view_coordinates: components::ViewCoordinates, +} + +impl CachedTransformsPerTimeline { + #[inline] + pub fn entity_transforms( + &self, + entity_path: EntityPathHash, + ) -> Option<&PerTimelinePerEntityTransforms> { + self.per_entity.get(&entity_path) + } +} + +impl PerTimelinePerEntityTransforms { + #[inline] + pub fn latest_at_tree_transform(&self, query: &LatestAtQuery) -> Affine3A { + debug_assert_eq!(query.timeline(), self.timeline); + self.tree_transforms + .range(..query.at().inc()) + .next_back() + .map(|(_time, transform)| *transform) + .unwrap_or(Affine3A::IDENTITY) + } + + #[inline] + pub fn latest_at_instance_poses(&self, query: &LatestAtQuery) -> &[Affine3A] { + debug_assert_eq!(query.timeline(), self.timeline); + self.pose_transforms + .as_ref() + .and_then(|pose_transforms| pose_transforms.range(..query.at().inc()).next_back()) + .map(|(_time, pose_transforms)| pose_transforms.as_slice()) + .unwrap_or(&[]) + } + + #[inline] + pub fn latest_at_pinhole(&self, query: &LatestAtQuery) -> Option<&ResolvedPinholeProjection> { + debug_assert_eq!(query.timeline(), self.timeline); + self.pinhole_projections + .as_ref() + .and_then(|pinhole_projections| { + pinhole_projections.range(..query.at().inc()).next_back() + }) + .and_then(|(_time, projection)| projection.as_ref()) + } +} + +impl TransformCacheStoreSubscriber { + /// Accesses the global store subscriber. + /// + /// Lazily registers the subscriber if it hasn't been registered yet. + pub fn subscription_handle() -> ChunkStoreSubscriberHandle { + static SUBSCRIPTION: OnceCell = OnceCell::new(); + *SUBSCRIPTION.get_or_init(ChunkStore::register_per_store_subscriber::) + } + + /// Accesses the transform component tracking data for a given store. + #[inline] + pub fn access(store_id: &StoreId, f: impl FnMut(&Self) -> T) -> Option { + ChunkStore::with_per_store_subscriber(Self::subscription_handle(), store_id, f) + } + + /// Accesses the transform component tracking data for a given store exclusively. + #[inline] + pub fn access_mut(store_id: &StoreId, f: impl FnMut(&mut Self) -> T) -> Option { + ChunkStore::with_per_store_subscriber_mut(Self::subscription_handle(), store_id, f) + } + + /// Accesses the transform component tracking data for a given timeline. + /// + /// Returns `None` if the timeline doesn't have any transforms at all. + #[inline] + pub fn transforms_per_timeline( + &self, + timeline: Timeline, + ) -> Option<&CachedTransformsPerTimeline> { + self.per_timeline.get(&timeline) + } + + /// Makes sure the transform cache is up to date with the latest data. + /// + /// This needs to be called once per frame prior to any transform propagation. + /// (which is done by [`crate::contexts::TransformTreeContext`]) + pub fn apply_all_updates(&mut self, entity_db: &EntityDb) { + re_tracing::profile_function!(); + + for (timeline, per_timeline) in &mut self.per_timeline { + for invalidated_transform in per_timeline.invalidated_transforms.drain(..) { + let entity_path = &invalidated_transform.entity_path; + let entity_entry = per_timeline + .per_entity + .entry(entity_path.hash()) + .or_insert_with(|| PerTimelinePerEntityTransforms { + timeline: *timeline, + tree_transforms: Default::default(), + pose_transforms: Default::default(), + pinhole_projections: Default::default(), + }); + + for time in invalidated_transform.times { + let query = LatestAtQuery::new(*timeline, time); + + if invalidated_transform + .aspects + .contains(TransformAspect::Tree) + { + let transform = query_and_resolve_tree_transform_at_entity( + entity_path, + entity_db, + &query, + ) + .unwrap_or(Affine3A::IDENTITY); + // If there's *no* transform, we have to put identity in, otherwise we'd miss clears! + entity_entry.tree_transforms.insert(time, transform); + } + if invalidated_transform + .aspects + .contains(TransformAspect::Pose) + { + let poses = query_and_resolve_instance_poses_at_entity( + entity_path, + entity_db, + &query, + ); + // *do* also insert empty ones, otherwise it's not possible to clear previous state. + entity_entry + .pose_transforms + .get_or_insert_with(Box::default) + .insert(time, poses); + } + if invalidated_transform + .aspects + .contains(TransformAspect::PinholeOrViewCoordinates) + { + let pinhole_projection = query_and_resolve_pinhole_projection_at_entity( + entity_path, + entity_db, + &query, + ); + // `None` values need to be inserted as well to clear out previous state. + // See also doc string on `PinholeProjectionMap`. + entity_entry + .pinhole_projections + .get_or_insert_with(Box::default) + .insert(time, pinhole_projection); + } + } + } + } + } + + fn add_chunk(&mut self, event: &re_chunk_store::ChunkStoreEvent, aspects: TransformAspect) { + let entity_path = event.chunk.entity_path(); + + for (timeline, time_column) in event.diff.chunk.timelines() { + let per_timeline = self.per_timeline.entry(*timeline).or_default(); + + // All of these require complex latest-at queries that would require a lot more context, + // are fairly expensive, and may depend on other components that may come in at the same time. + // (we could inject that here, but it's not entirely straight forward). + // So instead, we note down that the caches is invalidated for the given entity & times. + + // This invalidates any time _after_ the first event in this chunk. + // (e.g. if a rotation is added prior to translations later on, + // then the resulting transforms at those translations changes as well for latest-at queries) + let mut invalidated_times = Vec::new(); + let Some(min_time) = time_column.times().min() else { + continue; + }; + if let Some(entity_entry) = per_timeline.per_entity.get_mut(&entity_path.hash()) { + if aspects.contains(TransformAspect::Tree) { + let invalidated_tree_transforms = + entity_entry.tree_transforms.split_off(&min_time); + invalidated_times.extend(invalidated_tree_transforms.into_keys()); + } + if aspects.contains(TransformAspect::Pose) { + if let Some(pose_transforms) = &mut entity_entry.pose_transforms { + let invalidated_pose_transforms = pose_transforms.split_off(&min_time); + invalidated_times.extend(invalidated_pose_transforms.into_keys()); + } + } + if aspects.contains(TransformAspect::PinholeOrViewCoordinates) { + if let Some(pinhole_projections) = &mut entity_entry.pinhole_projections { + let invalidated_pinhole_projections = + pinhole_projections.split_off(&min_time); + invalidated_times.extend(invalidated_pinhole_projections.into_keys()); + } + } + } + + per_timeline + .invalidated_transforms + .push(InvalidatedTransforms { + entity_path: entity_path.clone(), + times: time_column + .times() + .chain(invalidated_times.into_iter()) + .collect(), + aspects, + }); + } + } + + fn remove_chunk(&mut self, event: &re_chunk_store::ChunkStoreEvent, aspects: TransformAspect) { + let entity_path = event.chunk.entity_path(); + + for (timeline, time_column) in event.diff.chunk.timelines() { + let Some(per_timeline) = self.per_timeline.get_mut(timeline) else { + continue; + }; + + // Remove incoming data. + for invalidated_transform in per_timeline + .invalidated_transforms + .iter_mut() + .filter(|invalidated_transform| &invalidated_transform.entity_path == entity_path) + { + let times = time_column.times().collect::>(); + invalidated_transform + .times + .retain(|time| !times.contains(time)); + } + per_timeline + .invalidated_transforms + .retain(|invalidated_transform| !invalidated_transform.times.is_empty()); + + // Remove existing data. + if let Some(per_entity) = per_timeline.per_entity.get_mut(&entity_path.hash()) { + for time in time_column.times() { + if aspects.contains(TransformAspect::Tree) { + per_entity.tree_transforms.remove(&time); + } + if aspects.contains(TransformAspect::Pose) { + if let Some(pose_transforms) = &mut per_entity.pose_transforms { + pose_transforms.remove(&time); + } + } + if aspects.contains(TransformAspect::PinholeOrViewCoordinates) { + if let Some(pinhole_projections) = &mut per_entity.pinhole_projections { + pinhole_projections.remove(&time); + } + } + } + + if per_entity.tree_transforms.is_empty() + && per_entity + .pose_transforms + .as_ref() + .map_or(true, |pose_transforms| pose_transforms.is_empty()) + && per_entity + .pinhole_projections + .as_ref() + .map_or(true, |pinhole_projections| pinhole_projections.is_empty()) + { + per_timeline.per_entity.remove(&entity_path.hash()); + } + } + + if per_timeline.per_entity.is_empty() && per_timeline.invalidated_transforms.is_empty() + { + self.per_timeline.remove(timeline); + } + } + } +} + +impl PerStoreChunkSubscriber for TransformCacheStoreSubscriber { + fn name() -> String { + "rerun.TransformCacheStoreSubscriber".to_owned() + } + + fn on_events<'a>(&mut self, events: impl Iterator) { + re_tracing::profile_function!(); + + for event in events { + // The components we are interested in may only show up on some of the timelines + // within this chunk, so strictly speaking the affected "aspects" we compute here are conservative. + // But that's fairly rare, so a few false positive entries here are fine. + let mut aspects = TransformAspect::empty(); + for component_name in event.chunk.component_names() { + if self.transform_components.contains(&component_name) { + aspects |= TransformAspect::Tree; + } + if self.pose_components.contains(&component_name) { + aspects |= TransformAspect::Pose; + } + if self.pinhole_components.contains(&component_name) { + aspects |= TransformAspect::PinholeOrViewCoordinates; + } + } + if aspects.is_empty() { + continue; + } + + if event.kind == re_chunk_store::ChunkStoreDiffKind::Deletion { + self.remove_chunk(event, aspects); + } else { + self.add_chunk(event, aspects); + } + } + } +} + +/// Queries all components that are part of pose transforms, returning the transform from child to parent. +/// +/// If any of the components yields an invalid transform, returns a `glam::Affine3A::ZERO`. +/// (this effectively disconnects a subtree from the transform hierarchy!) +// TODO(#3849): There's no way to discover invalid transforms right now (they can be intentional but often aren't). +fn query_and_resolve_tree_transform_at_entity( + entity_path: &EntityPath, + entity_db: &EntityDb, + query: &LatestAtQuery, +) -> Option { + // TODO(andreas): Filter out styling components. + let components = archetypes::Transform3D::all_components(); + let component_names = components.iter().map(|descr| descr.component_name); + let results = entity_db.latest_at(query, entity_path, component_names); + if results.components.is_empty() { + return None; + } + + let mut transform = Affine3A::IDENTITY; + + // It's an error if there's more than one component. Warn in that case. + let mono_log_level = re_log::Level::Warn; + + // The order of the components here is important, and checked by `debug_assert_transform_field_order` + if let Some(translation) = + results.component_mono_with_log_level::(mono_log_level) + { + transform = Affine3A::from(translation); + } + if let Some(axis_angle) = + results.component_mono_with_log_level::(mono_log_level) + { + if let Ok(axis_angle) = Affine3A::try_from(axis_angle) { + transform *= axis_angle; + } else { + return Some(Affine3A::ZERO); + } + } + if let Some(quaternion) = + results.component_mono_with_log_level::(mono_log_level) + { + if let Ok(quaternion) = Affine3A::try_from(quaternion) { + transform *= quaternion; + } else { + return Some(Affine3A::ZERO); + } + } + if let Some(scale) = + results.component_mono_with_log_level::(mono_log_level) + { + if scale.x() == 0.0 && scale.y() == 0.0 && scale.z() == 0.0 { + return Some(Affine3A::ZERO); + } + transform *= Affine3A::from(scale); + } + if let Some(mat3x3) = + results.component_mono_with_log_level::(mono_log_level) + { + let affine_transform = Affine3A::from(mat3x3); + if affine_transform.matrix3.determinant() == 0.0 { + return Some(Affine3A::ZERO); + } + transform *= affine_transform; + } + + if results.component_mono_with_log_level::(mono_log_level) + == Some(components::TransformRelation::ChildFromParent) + { + let determinant = transform.matrix3.determinant(); + if determinant != 0.0 && determinant.is_finite() { + transform = transform.inverse(); + } else { + // All "regular invalid" transforms should have been caught. + // So ending up here means something else went wrong? + re_log::warn_once!( + "Failed to express child-from-parent transform at {} since it wasn't invertible", + entity_path, + ); + } + } + + Some(transform) +} + +/// Queries all components that are part of pose transforms, returning the transform from child to parent. +/// +/// If any of the components yields an invalid transform, returns a `glam::Affine3A::ZERO` for that instance. +/// (this effectively ignores the instance for most visualizations!) +// TODO(#3849): There's no way to discover invalid transforms right now (they can be intentional but often aren't). +fn query_and_resolve_instance_poses_at_entity( + entity_path: &EntityPath, + entity_db: &EntityDb, + query: &LatestAtQuery, +) -> Vec { + // TODO(andreas): Filter out styling components. + let components = archetypes::InstancePoses3D::all_components(); + let component_names = components.iter().map(|descr| descr.component_name); + let result = entity_db.latest_at(query, entity_path, component_names); + + let max_num_instances = result + .components + .iter() + .map(|(name, row)| row.num_instances(name)) + .max() + .unwrap_or(0) as usize; + + if max_num_instances == 0 { + return Vec::new(); + } + + #[inline] + pub fn clamped_or_nothing( + values: Vec, + clamped_len: usize, + ) -> impl Iterator { + let Some(last) = values.last() else { + return Either::Left(std::iter::empty()); + }; + let last = last.clone(); + Either::Right( + values + .into_iter() + .chain(std::iter::repeat(last)) + .take(clamped_len), + ) + } + + let batch_translation = result + .component_batch::() + .unwrap_or_default(); + let batch_rotation_quat = result + .component_batch::() + .unwrap_or_default(); + let batch_rotation_axis_angle = result + .component_batch::() + .unwrap_or_default(); + let batch_scale = result + .component_batch::() + .unwrap_or_default(); + let batch_mat3x3 = result + .component_batch::() + .unwrap_or_default(); + + if batch_translation.is_empty() + && batch_rotation_quat.is_empty() + && batch_rotation_axis_angle.is_empty() + && batch_scale.is_empty() + && batch_mat3x3.is_empty() + { + return Vec::new(); + } + let mut iter_translation = clamped_or_nothing(batch_translation, max_num_instances); + let mut iter_rotation_quat = clamped_or_nothing(batch_rotation_quat, max_num_instances); + let mut iter_rotation_axis_angle = + clamped_or_nothing(batch_rotation_axis_angle, max_num_instances); + let mut iter_scale = clamped_or_nothing(batch_scale, max_num_instances); + let mut iter_mat3x3 = clamped_or_nothing(batch_mat3x3, max_num_instances); + + (0..max_num_instances) + .map(|_| { + // We apply these in a specific order - see `debug_assert_transform_field_order` + let mut transform = Affine3A::IDENTITY; + if let Some(translation) = iter_translation.next() { + transform = Affine3A::from(translation); + } + if let Some(rotation_quat) = iter_rotation_quat.next() { + if let Ok(rotation_quat) = Affine3A::try_from(rotation_quat) { + transform *= rotation_quat; + } else { + transform = Affine3A::ZERO; + } + } + if let Some(rotation_axis_angle) = iter_rotation_axis_angle.next() { + if let Ok(axis_angle) = Affine3A::try_from(rotation_axis_angle) { + transform *= axis_angle; + } else { + transform = Affine3A::ZERO; + } + } + if let Some(scale) = iter_scale.next() { + transform *= Affine3A::from(scale); + } + if let Some(mat3x3) = iter_mat3x3.next() { + transform *= Affine3A::from(mat3x3); + } + transform + }) + .collect() +} + +fn query_and_resolve_pinhole_projection_at_entity( + entity_path: &EntityPath, + entity_db: &EntityDb, + query: &LatestAtQuery, +) -> Option { + entity_db + .latest_at_component::(entity_path, query) + .map(|(_index, image_from_camera)| ResolvedPinholeProjection { + image_from_camera, + view_coordinates: entity_db + .latest_at_component::(entity_path, query) + .map_or(archetypes::Pinhole::DEFAULT_CAMERA_XYZ, |(_index, res)| res), + }) +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use re_chunk_store::{ + external::re_chunk::ChunkBuilder, ChunkId, GarbageCollectionOptions, RowId, + }; + use re_types::{archetypes, Loggable, SerializedComponentBatch}; + + use super::*; + + fn ensure_subscriber_registered(entity_db: &EntityDb) { + TransformCacheStoreSubscriber::access(&entity_db.store_id(), |_| { + // Make sure the subscriber is registered. + }); + } + + #[test] + fn test_transforms_per_timeline_access() { + let mut entity_db = EntityDb::new(StoreId::random(re_log_types::StoreKind::Recording)); + ensure_subscriber_registered(&entity_db); + + // Log a few tree transforms at different times. + let timeline = Timeline::new_sequence("t"); + let chunk0 = ChunkBuilder::new(ChunkId::new(), EntityPath::from("with_transform")) + .with_archetype( + RowId::new(), + [(timeline, 1)], + &archetypes::Transform3D::from_translation([1.0, 2.0, 3.0]), + ) + .build() + .unwrap(); + let chunk1 = ChunkBuilder::new(ChunkId::new(), EntityPath::from("without_transform")) + .with_archetype( + RowId::new(), + [(timeline, 1)], + // Anything that doesn't have components the transform cache is interested in. + &archetypes::Points3D::new([[1.0, 2.0, 3.0]]), + ) + .build() + .unwrap(); + entity_db.add_chunk(&Arc::new(chunk0)).unwrap(); + entity_db.add_chunk(&Arc::new(chunk1)).unwrap(); + + TransformCacheStoreSubscriber::access_mut(&entity_db.store_id(), |cache| { + cache.apply_all_updates(&entity_db); + let transforms_per_timeline = cache.transforms_per_timeline(timeline).unwrap(); + assert!(transforms_per_timeline + .entity_transforms(EntityPath::from("without_transform").hash()) + .is_none()); + assert!(transforms_per_timeline + .entity_transforms(EntityPath::from("rando").hash()) + .is_none()); + let transforms = transforms_per_timeline + .entity_transforms(EntityPath::from("with_transform").hash()) + .unwrap(); + assert_eq!(transforms.timeline, timeline); + assert_eq!(transforms.tree_transforms.len(), 1); + assert_eq!(transforms.pose_transforms, None); + assert_eq!(transforms.pinhole_projections, None); + }); + } + + #[test] + fn test_tree_transforms() { + let mut entity_db = EntityDb::new(StoreId::random(re_log_types::StoreKind::Recording)); + ensure_subscriber_registered(&entity_db); + + // Log a few tree transforms at different times. + let timeline = Timeline::new_sequence("t"); + let chunk = ChunkBuilder::new(ChunkId::new(), EntityPath::from("my_entity")) + .with_archetype( + RowId::new(), + [(timeline, 1)], + &archetypes::Transform3D::from_translation([1.0, 2.0, 3.0]), + ) + .with_archetype( + RowId::new(), + [(timeline, 3)], + &archetypes::Transform3D::update_fields().with_scale([1.0, 2.0, 3.0]), + ) + .with_archetype( + RowId::new(), + [(timeline, 4)], + &archetypes::Transform3D::from_rotation(glam::Quat::from_rotation_x(1.0)), + ) + .with_archetype( + RowId::new(), + [(timeline, 5)], + &archetypes::Transform3D::clear_fields(), + ) + .build() + .unwrap(); + entity_db.add_chunk(&Arc::new(chunk)).unwrap(); + + // Check that the transform cache has the expected transforms. + TransformCacheStoreSubscriber::access_mut(&entity_db.store_id(), |cache| { + cache.apply_all_updates(&entity_db); + let transforms_per_timeline = cache.transforms_per_timeline(timeline).unwrap(); + let transforms = transforms_per_timeline + .entity_transforms(EntityPath::from("my_entity").hash()) + .unwrap(); + + assert_eq!( + transforms.latest_at_tree_transform(&LatestAtQuery::new(timeline, 0)), + glam::Affine3A::IDENTITY + ); + assert_eq!( + transforms.latest_at_tree_transform(&LatestAtQuery::new(timeline, 1)), + glam::Affine3A::from_translation(glam::Vec3::new(1.0, 2.0, 3.0)) + ); + assert_eq!( + transforms.latest_at_tree_transform(&LatestAtQuery::new(timeline, 2)), + glam::Affine3A::from_translation(glam::Vec3::new(1.0, 2.0, 3.0)) + ); + assert_eq!( + transforms.latest_at_tree_transform(&LatestAtQuery::new(timeline, 3)), + glam::Affine3A::from_scale_rotation_translation( + glam::Vec3::new(1.0, 2.0, 3.0), + glam::Quat::IDENTITY, + glam::Vec3::new(1.0, 2.0, 3.0), + ) + ); + assert_eq!( + transforms.latest_at_tree_transform(&LatestAtQuery::new(timeline, 4)), + glam::Affine3A::from_quat(glam::Quat::from_rotation_x(1.0)) + ); + assert_eq!( + transforms.latest_at_tree_transform(&LatestAtQuery::new(timeline, 5)), + glam::Affine3A::IDENTITY + ); + assert_eq!( + transforms.latest_at_tree_transform(&LatestAtQuery::new(timeline, 123)), + glam::Affine3A::IDENTITY + ); + }); + } + + #[test] + fn test_pose_transforms() { + let mut entity_db = EntityDb::new(StoreId::random(re_log_types::StoreKind::Recording)); + ensure_subscriber_registered(&entity_db); + + // Log a few tree transforms at different times. + let timeline = Timeline::new_sequence("t"); + let chunk = ChunkBuilder::new(ChunkId::new(), EntityPath::from("my_entity")) + .with_archetype( + RowId::new(), + [(timeline, 1)], + &archetypes::InstancePoses3D::new().with_translations([ + [1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0], + ]), + ) + .with_archetype( + RowId::new(), + [(timeline, 3)], + // Less instances, and a splatted scale. + &archetypes::InstancePoses3D::new() + .with_translations([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]) + .with_scales([[2.0, 3.0, 4.0]]), + ) + .with_serialized_batches( + RowId::new(), + [(timeline, 4)], + [ + SerializedComponentBatch::new( + arrow::array::new_empty_array(&components::Translation3D::arrow_datatype()), + archetypes::InstancePoses3D::descriptor_translations(), + ), + SerializedComponentBatch::new( + arrow::array::new_empty_array(&components::Scale3D::arrow_datatype()), + archetypes::InstancePoses3D::descriptor_scales(), + ), + ], + ) + // TODO(#7245): Use this instead of the above + // .with_archetype( + // RowId::new(), + // [(timeline, 4)], + // &archetypes::InstancePoses3D::clear_fields(), + // ) + .build() + .unwrap(); + entity_db.add_chunk(&Arc::new(chunk)).unwrap(); + + // Check that the transform cache has the expected transforms. + TransformCacheStoreSubscriber::access_mut(&entity_db.store_id(), |cache| { + cache.apply_all_updates(&entity_db); + let transforms_per_timeline = cache.transforms_per_timeline(timeline).unwrap(); + let transforms = transforms_per_timeline + .entity_transforms(EntityPath::from("my_entity").hash()) + .unwrap(); + + assert_eq!( + transforms.latest_at_instance_poses(&LatestAtQuery::new(timeline, 0)), + &[] + ); + assert_eq!( + transforms.latest_at_instance_poses(&LatestAtQuery::new(timeline, 1)), + &[ + glam::Affine3A::from_translation(glam::Vec3::new(1.0, 2.0, 3.0)), + glam::Affine3A::from_translation(glam::Vec3::new(4.0, 5.0, 6.0)), + glam::Affine3A::from_translation(glam::Vec3::new(7.0, 8.0, 9.0)), + ] + ); + assert_eq!( + transforms.latest_at_instance_poses(&LatestAtQuery::new(timeline, 2)), + &[ + glam::Affine3A::from_translation(glam::Vec3::new(1.0, 2.0, 3.0)), + glam::Affine3A::from_translation(glam::Vec3::new(4.0, 5.0, 6.0)), + glam::Affine3A::from_translation(glam::Vec3::new(7.0, 8.0, 9.0)), + ] + ); + assert_eq!( + transforms.latest_at_instance_poses(&LatestAtQuery::new(timeline, 3)), + &[ + glam::Affine3A::from_scale_rotation_translation( + glam::Vec3::new(2.0, 3.0, 4.0), + glam::Quat::IDENTITY, + glam::Vec3::new(1.0, 2.0, 3.0), + ), + glam::Affine3A::from_scale_rotation_translation( + glam::Vec3::new(2.0, 3.0, 4.0), + glam::Quat::IDENTITY, + glam::Vec3::new(4.0, 5.0, 6.0), + ), + ] + ); + assert_eq!( + transforms.latest_at_instance_poses(&LatestAtQuery::new(timeline, 4)), + &[] + ); + assert_eq!( + transforms.latest_at_instance_poses(&LatestAtQuery::new(timeline, 123)), + &[] + ); + }); + } + + #[test] + fn test_pinhole_projections() { + let mut entity_db = EntityDb::new(StoreId::random(re_log_types::StoreKind::Recording)); + ensure_subscriber_registered(&entity_db); + + let image_from_camera = + components::PinholeProjection::from_focal_length_and_principal_point( + [1.0, 2.0], + [1.0, 2.0], + ); + + // Log a few tree transforms at different times. + let timeline = Timeline::new_sequence("t"); + let chunk = ChunkBuilder::new(ChunkId::new(), EntityPath::from("my_entity")) + .with_archetype( + RowId::new(), + [(timeline, 1)], + &archetypes::Pinhole::new(image_from_camera), + ) + .with_archetype( + RowId::new(), + [(timeline, 3)], + &archetypes::ViewCoordinates::BLU, + ) + // Clear out the pinhole projection (this should yield nothing then for the remaining view coordinates.) + .with_serialized_batch( + RowId::new(), + [(timeline, 4)], + SerializedComponentBatch::new( + arrow::array::new_empty_array(&components::PinholeProjection::arrow_datatype()), + archetypes::Pinhole::descriptor_image_from_camera(), + ), + ) + // TODO(#7245): Use this instead + // .with_archetype( + // RowId::new(), + // [(timeline, 4)], + // &archetypes::Pinhole::clear_fields(), + // ) + .build() + .unwrap(); + entity_db.add_chunk(&Arc::new(chunk)).unwrap(); + + // Check that the transform cache has the expected transforms. + TransformCacheStoreSubscriber::access_mut(&entity_db.store_id(), |cache| { + cache.apply_all_updates(&entity_db); + let transforms_per_timeline = cache.transforms_per_timeline(timeline).unwrap(); + let transforms = transforms_per_timeline + .entity_transforms(EntityPath::from("my_entity").hash()) + .unwrap(); + + assert_eq!( + transforms.latest_at_pinhole(&LatestAtQuery::new(timeline, 0)), + None + ); + assert_eq!( + transforms.latest_at_pinhole(&LatestAtQuery::new(timeline, 1)), + Some(&ResolvedPinholeProjection { + image_from_camera, + view_coordinates: archetypes::Pinhole::DEFAULT_CAMERA_XYZ, + }) + ); + assert_eq!( + transforms.latest_at_pinhole(&LatestAtQuery::new(timeline, 2)), + Some(&ResolvedPinholeProjection { + image_from_camera, + view_coordinates: archetypes::Pinhole::DEFAULT_CAMERA_XYZ, + }) + ); + assert_eq!( + transforms.latest_at_pinhole(&LatestAtQuery::new(timeline, 3)), + Some(&ResolvedPinholeProjection { + image_from_camera, + view_coordinates: components::ViewCoordinates::BLU, + }) + ); + assert_eq!( + transforms.latest_at_pinhole(&LatestAtQuery::new(timeline, 4)), + None // View coordinates alone doesn't give us a pinhole projection from the transform cache. + ); + assert_eq!( + transforms.latest_at_pinhole(&LatestAtQuery::new(timeline, 123)), + None + ); + }); + } + + #[test] + fn test_out_of_order_updates() { + let mut entity_db = EntityDb::new(StoreId::random(re_log_types::StoreKind::Recording)); + ensure_subscriber_registered(&entity_db); + + // Log a few tree transforms at different times. + let timeline = Timeline::new_sequence("t"); + let chunk = ChunkBuilder::new(ChunkId::new(), EntityPath::from("my_entity")) + .with_archetype( + RowId::new(), + [(timeline, 1)], + &archetypes::Transform3D::from_translation([1.0, 2.0, 3.0]), + ) + .with_archetype( + RowId::new(), + [(timeline, 3)], + // Note that this doesn't clear anything that could be inserted at time 2. + &archetypes::Transform3D::update_fields().with_translation([2.0, 3.0, 4.0]), + ) + .build() + .unwrap(); + entity_db.add_chunk(&Arc::new(chunk)).unwrap(); + + // Check that the transform cache has the expected transforms. + TransformCacheStoreSubscriber::access_mut(&entity_db.store_id(), |cache| { + cache.apply_all_updates(&entity_db); + let transforms_per_timeline = cache.transforms_per_timeline(timeline).unwrap(); + let transforms = transforms_per_timeline + .entity_transforms(EntityPath::from("my_entity").hash()) + .unwrap(); + + // Check that the transform cache has the expected transforms. + assert_eq!( + transforms.latest_at_tree_transform(&LatestAtQuery::new(timeline, 1)), + glam::Affine3A::from_translation(glam::Vec3::new(1.0, 2.0, 3.0)) + ); + assert_eq!( + transforms.latest_at_tree_transform(&LatestAtQuery::new(timeline, 3)), + glam::Affine3A::from_translation(glam::Vec3::new(2.0, 3.0, 4.0)) + ); + }); + + // Add a transform between the two that invalidates the one at time stamp 3. + let timeline = Timeline::new_sequence("t"); + let chunk = ChunkBuilder::new(ChunkId::new(), EntityPath::from("my_entity")) + .with_archetype( + RowId::new(), + [(timeline, 2)], + &archetypes::Transform3D::update_fields().with_scale([-1.0, -2.0, -3.0]), + ) + .build() + .unwrap(); + entity_db.add_chunk(&Arc::new(chunk)).unwrap(); + + // Check that the transform cache has the expected changed transforms. + TransformCacheStoreSubscriber::access_mut(&entity_db.store_id(), |cache| { + cache.apply_all_updates(&entity_db); + let transforms_per_timeline = cache.transforms_per_timeline(timeline).unwrap(); + let transforms = transforms_per_timeline + .entity_transforms(EntityPath::from("my_entity").hash()) + .unwrap(); + + // Check that the transform cache has the expected transforms. + assert_eq!( + transforms.latest_at_tree_transform(&LatestAtQuery::new(timeline, 1)), + glam::Affine3A::from_translation(glam::Vec3::new(1.0, 2.0, 3.0)) + ); + assert_eq!( + transforms.latest_at_tree_transform(&LatestAtQuery::new(timeline, 2)), + glam::Affine3A::from_scale_rotation_translation( + glam::Vec3::new(-1.0, -2.0, -3.0), + glam::Quat::IDENTITY, + glam::Vec3::new(1.0, 2.0, 3.0), + ) + ); + assert_eq!( + transforms.latest_at_tree_transform(&LatestAtQuery::new(timeline, 3)), + glam::Affine3A::from_scale_rotation_translation( + glam::Vec3::new(-1.0, -2.0, -3.0), + glam::Quat::IDENTITY, + glam::Vec3::new(2.0, 3.0, 4.0), + ) + ); + }); + } + + #[test] + fn test_gc() { + let mut entity_db = EntityDb::new(StoreId::random(re_log_types::StoreKind::Recording)); + ensure_subscriber_registered(&entity_db); + + let timeline = Timeline::new_sequence("t"); + let chunk = ChunkBuilder::new(ChunkId::new(), EntityPath::from("my_entity0")) + .with_archetype( + RowId::new(), + [(timeline, 1)], + &archetypes::Transform3D::from_translation([1.0, 2.0, 3.0]), + ) + .build() + .unwrap(); + entity_db.add_chunk(&Arc::new(chunk)).unwrap(); + + // Apply some updates to the transform before GC pass. + TransformCacheStoreSubscriber::access_mut(&entity_db.store_id(), |cache| { + cache.apply_all_updates(&entity_db); + }); + + let chunk = ChunkBuilder::new(ChunkId::new(), EntityPath::from("my_entity1")) + .with_archetype( + RowId::new(), + [(timeline, 2)], + &archetypes::Transform3D::from_translation([4.0, 5.0, 6.0]), + ) + .build() + .unwrap(); + entity_db.add_chunk(&Arc::new(chunk)).unwrap(); + + // Don't apply updates for this chunk. + + entity_db.gc(&GarbageCollectionOptions::gc_everything()); + + TransformCacheStoreSubscriber::access_mut(&entity_db.store_id(), |cache| { + assert!(cache.transforms_per_timeline(timeline).is_none()); + }); + } +} diff --git a/crates/viewer/re_view_spatial/src/transform_component_tracker.rs b/crates/viewer/re_view_spatial/src/transform_component_tracker.rs deleted file mode 100644 index 7f6b564f10f6..000000000000 --- a/crates/viewer/re_view_spatial/src/transform_component_tracker.rs +++ /dev/null @@ -1,139 +0,0 @@ -use once_cell::sync::OnceCell; - -use nohash_hasher::{IntMap, IntSet}; -use re_chunk_store::{ - ChunkStore, ChunkStoreDiffKind, ChunkStoreEvent, ChunkStoreSubscriberHandle, - PerStoreChunkSubscriber, -}; -use re_log_types::{EntityPath, EntityPathHash, StoreId}; -use re_types::{Component as _, ComponentName}; - -// --- - -/// Set of components that an entity ever had over its known lifetime. -#[derive(Default, Clone)] -pub struct PotentialTransformComponentSet { - /// All transform components ever present. - pub transform3d: IntSet, - - /// All pose transform components ever present. - pub pose3d: IntSet, - - /// Whether the entity ever had a pinhole camera. - pub pinhole: bool, -} - -/// Keeps track of which entities have had any `Transform3D`-related data on any timeline at any -/// point in time. -/// -/// This is used to optimize queries in the `TransformContext`, so that we don't unnecessarily pay -/// for the fixed overhead of all the query layers when we know for a fact that there won't be any -/// data there. -/// This is a huge performance improvement in practice, especially in recordings with many entities. -pub struct TransformComponentTrackerStoreSubscriber { - /// The components of interest. - transform_components: IntSet, - pose_components: IntSet, - - components_per_entity: IntMap, -} - -impl Default for TransformComponentTrackerStoreSubscriber { - #[inline] - fn default() -> Self { - use re_types::Archetype as _; - Self { - transform_components: re_types::archetypes::Transform3D::all_components() - .iter() - .map(|descr| descr.component_name) - .collect(), - pose_components: re_types::archetypes::InstancePoses3D::all_components() - .iter() - .map(|descr| descr.component_name) - .collect(), - components_per_entity: Default::default(), - } - } -} - -impl TransformComponentTrackerStoreSubscriber { - /// Accesses the global store subscriber. - /// - /// Lazily registers the subscriber if it hasn't been registered yet. - pub fn subscription_handle() -> ChunkStoreSubscriberHandle { - static SUBSCRIPTION: OnceCell = OnceCell::new(); - *SUBSCRIPTION.get_or_init(ChunkStore::register_per_store_subscriber::) - } - - /// Accesses the transform component tracking data for a given store. - #[inline] - pub fn access(store_id: &StoreId, f: impl FnOnce(&Self) -> T) -> Option { - ChunkStore::with_per_store_subscriber_once(Self::subscription_handle(), store_id, f) - } - - pub fn potential_transform_components( - &self, - entity_path: &EntityPath, - ) -> Option<&PotentialTransformComponentSet> { - self.components_per_entity.get(&entity_path.hash()) - } -} - -impl PerStoreChunkSubscriber for TransformComponentTrackerStoreSubscriber { - #[inline] - fn name() -> String { - "rerun.store_subscriber.TransformComponentTracker".into() - } - - fn on_events<'a>(&mut self, events: impl Iterator) { - re_tracing::profile_function!(); - - for event in events - // This is only additive, don't care about removals. - .filter(|e| e.kind == ChunkStoreDiffKind::Addition) - { - let entity_path_hash = event.chunk.entity_path().hash(); - - let contains_non_zero_component_array = |component_name| { - event - .chunk - .components() - .get(&component_name) - .is_some_and(|per_desc| { - per_desc - .values() - .any(|list_array| list_array.offsets().lengths().any(|len| len > 0)) - }) - }; - - for component_name in event.chunk.component_names() { - if self.transform_components.contains(&component_name) - && contains_non_zero_component_array(component_name) - { - self.components_per_entity - .entry(entity_path_hash) - .or_default() - .transform3d - .insert(component_name); - } - if self.pose_components.contains(&component_name) - && contains_non_zero_component_array(component_name) - { - self.components_per_entity - .entry(entity_path_hash) - .or_default() - .pose3d - .insert(component_name); - } - if component_name == re_types::components::PinholeProjection::name() - && contains_non_zero_component_array(component_name) - { - self.components_per_entity - .entry(entity_path_hash) - .or_default() - .pinhole = true; - } - } - } - } -} diff --git a/crates/viewer/re_view_spatial/src/ui_2d.rs b/crates/viewer/re_view_spatial/src/ui_2d.rs index 3104e685c88b..a77dc8c6df91 100644 --- a/crates/viewer/re_view_spatial/src/ui_2d.rs +++ b/crates/viewer/re_view_spatial/src/ui_2d.rs @@ -10,7 +10,6 @@ use re_types::{ archetypes::{Background, NearClipPlane, VisualBounds2D}, components as blueprint_components, }, - components::ViewCoordinates, }; use re_ui::{ContextExt as _, ModifiersMarkdown, MouseButtonMarkdown}; use re_view::controls::{DRAG_PAN2D_BUTTON, ZOOM_SCROLL_MODIFIER}; @@ -353,7 +352,7 @@ fn setup_target_config( ) .into(), resolution: Some([resolution.x, resolution.y].into()), - camera_xyz: Some(ViewCoordinates::RDF), + camera_xyz: Some(Pinhole::DEFAULT_CAMERA_XYZ), image_plane_distance: None, }; } diff --git a/crates/viewer/re_view_spatial/src/view_2d.rs b/crates/viewer/re_view_spatial/src/view_2d.rs index 1ed42c0201d7..1949cfa043cb 100644 --- a/crates/viewer/re_view_spatial/src/view_2d.rs +++ b/crates/viewer/re_view_spatial/src/view_2d.rs @@ -68,7 +68,7 @@ impl ViewClass for SpatialView2D { ) -> Result<(), ViewClassRegistryError> { // Ensure spatial topology & max image dimension is registered. crate::spatial_topology::SpatialTopologyStoreSubscriber::subscription_handle(); - crate::transform_component_tracker::TransformComponentTrackerStoreSubscriber::subscription_handle(); + crate::transform_cache::TransformCacheStoreSubscriber::subscription_handle(); crate::max_image_dimension_subscriber::MaxImageDimensionsStoreSubscriber::subscription_handle(); register_spatial_contexts(system_registry)?; diff --git a/crates/viewer/re_view_spatial/src/view_3d.rs b/crates/viewer/re_view_spatial/src/view_3d.rs index 2af66bb20404..c83faf828c5a 100644 --- a/crates/viewer/re_view_spatial/src/view_3d.rs +++ b/crates/viewer/re_view_spatial/src/view_3d.rs @@ -74,7 +74,7 @@ impl ViewClass for SpatialView3D { ) -> Result<(), ViewClassRegistryError> { // Ensure spatial topology is registered. crate::spatial_topology::SpatialTopologyStoreSubscriber::subscription_handle(); - crate::transform_component_tracker::TransformComponentTrackerStoreSubscriber::subscription_handle(); + crate::transform_cache::TransformCacheStoreSubscriber::subscription_handle(); register_spatial_contexts(system_registry)?; register_3d_spatial_visualizers(system_registry)?; diff --git a/crates/viewer/re_view_spatial/src/visualizers/cameras.rs b/crates/viewer/re_view_spatial/src/visualizers/cameras.rs index 709ccae686c4..834d90f90daf 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/cameras.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/cameras.rs @@ -14,7 +14,8 @@ use re_viewer_context::{ use super::{filter_visualizable_3d_entities, SpatialViewVisualizerData}; use crate::{ - contexts::TransformContext, query_pinhole, space_camera_3d::SpaceCamera3D, ui::SpatialViewState, + contexts::TransformTreeContext, query_pinhole, space_camera_3d::SpaceCamera3D, + ui::SpatialViewState, }; const CAMERA_COLOR: re_renderer::Color32 = re_renderer::Color32::from_rgb(150, 150, 150); @@ -46,7 +47,7 @@ impl CamerasVisualizer { fn visit_instance( &mut self, line_builder: &mut re_renderer::LineDrawableBuilder<'_>, - transforms: &TransformContext, + transforms: &TransformTreeContext, data_result: &DataResult, pinhole: &Pinhole, pinhole_view_coordinates: ViewCoordinates, @@ -71,7 +72,7 @@ impl CamerasVisualizer { } // The camera transform does not include the pinhole transform. - let Some(transform_info) = transforms.transform_info_for_entity(ent_path) else { + let Some(transform_info) = transforms.transform_info_for_entity(ent_path.hash()) else { return; }; let Some(twod_in_threed_info) = &transform_info.twod_in_threed_info else { @@ -214,7 +215,7 @@ impl VisualizerSystem for CamerasVisualizer { query: &ViewQuery<'_>, context_systems: &ViewContextCollection, ) -> Result, ViewSystemExecutionError> { - let transforms = context_systems.get::()?; + let transforms = context_systems.get::()?; // Counting all cameras ahead of time is a bit wasteful, but we also don't expect a huge amount, // so let re_renderer's allocator internally decide what buffer sizes to pick & grow them as we go. @@ -236,7 +237,7 @@ impl VisualizerSystem for CamerasVisualizer { transforms, data_result, &pinhole, - pinhole.camera_xyz.unwrap_or(ViewCoordinates::RDF), // TODO(#2641): This should come from archetype + pinhole.camera_xyz.unwrap_or(Pinhole::DEFAULT_CAMERA_XYZ), entity_highlight, ); } @@ -279,4 +280,10 @@ impl TypedComponentFallbackProvider for CamerasVisualizer { } } -re_viewer_context::impl_component_fallback_provider!(CamerasVisualizer => [ImagePlaneDistance]); +impl TypedComponentFallbackProvider for CamerasVisualizer { + fn fallback_for(&self, _ctx: &QueryContext<'_>) -> ViewCoordinates { + Pinhole::DEFAULT_CAMERA_XYZ + } +} + +re_viewer_context::impl_component_fallback_provider!(CamerasVisualizer => [ImagePlaneDistance, ViewCoordinates]); diff --git a/crates/viewer/re_view_spatial/src/visualizers/depth_images.rs b/crates/viewer/re_view_spatial/src/visualizers/depth_images.rs index 652eafbdb38f..3b946caefd14 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/depth_images.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/depth_images.rs @@ -7,7 +7,6 @@ use re_types::{ archetypes::DepthImage, components::{ self, Colormap, DepthMeter, DrawOrder, FillRatio, ImageBuffer, ImageFormat, ValueRange, - ViewCoordinates, }, image::ImageKind, Component as _, @@ -190,7 +189,7 @@ impl DepthImageVisualizer { * glam::Affine3A::from_mat3( intrinsics .camera_xyz - .unwrap_or(ViewCoordinates::RDF) // TODO(#2641): This should come from archetype + .unwrap_or(re_types::archetypes::Pinhole::DEFAULT_CAMERA_XYZ) .from_rdf(), ); diff --git a/crates/viewer/re_view_spatial/src/visualizers/mod.rs b/crates/viewer/re_view_spatial/src/visualizers/mod.rs index b1b5ea34d38f..0bf2cb4b8297 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/mod.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/mod.rs @@ -267,11 +267,7 @@ pub fn load_keypoint_connections( /// /// TODO(#1387): Image coordinate space should be configurable. pub fn image_view_coordinates() -> re_types::components::ViewCoordinates { - // Typical image spaces have - // - x pointing right - // - y pointing down - // - z pointing into the image plane (this is convenient for reading out a depth image which has typically positive z values) - re_types::components::ViewCoordinates::RDF + re_types::archetypes::Pinhole::DEFAULT_CAMERA_XYZ } fn filter_visualizable_2d_entities( diff --git a/crates/viewer/re_view_spatial/src/visualizers/transform3d_arrows.rs b/crates/viewer/re_view_spatial/src/visualizers/transform3d_arrows.rs index a2340f88fd85..1ecb0dc73177 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/transform3d_arrows.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/transform3d_arrows.rs @@ -13,7 +13,7 @@ use re_viewer_context::{ VisualizableEntities, VisualizableFilterContext, VisualizerQueryInfo, VisualizerSystem, }; -use crate::{contexts::TransformContext, ui::SpatialViewState, view_kind::SpatialViewKind}; +use crate::{contexts::TransformTreeContext, ui::SpatialViewState, view_kind::SpatialViewKind}; use super::{filter_visualizable_3d_entities, CamerasVisualizer, SpatialViewVisualizerData}; @@ -81,7 +81,7 @@ impl VisualizerSystem for Transform3DArrowsVisualizer { query: &ViewQuery<'_>, context_systems: &ViewContextCollection, ) -> Result, ViewSystemExecutionError> { - let transforms = context_systems.get::()?; + let transforms = context_systems.get::()?; let latest_at_query = re_chunk_store::LatestAtQuery::new(query.timeline, query.latest_at); @@ -95,7 +95,7 @@ impl VisualizerSystem for Transform3DArrowsVisualizer { for data_result in query.iter_visible_data_results(ctx, Self::identifier()) { // Use transform without potential pinhole, since we don't want to visualize image-space coordinates. let Some(transform_info) = - transforms.transform_info_for_entity(&data_result.entity_path) + transforms.transform_info_for_entity(data_result.entity_path.hash()) else { continue; }; diff --git a/crates/viewer/re_view_spatial/src/visualizers/utilities/entity_iterator.rs b/crates/viewer/re_view_spatial/src/visualizers/utilities/entity_iterator.rs index 2b5718a26e92..2ca627bf1430 100644 --- a/crates/viewer/re_view_spatial/src/visualizers/utilities/entity_iterator.rs +++ b/crates/viewer/re_view_spatial/src/visualizers/utilities/entity_iterator.rs @@ -6,7 +6,7 @@ use re_viewer_context::{ ViewSystemExecutionError, }; -use crate::contexts::{EntityDepthOffsets, SpatialSceneEntityContext, TransformContext}; +use crate::contexts::{EntityDepthOffsets, SpatialSceneEntityContext, TransformTreeContext}; // --- @@ -84,7 +84,7 @@ where &HybridResults<'_>, ) -> Result<(), ViewSystemExecutionError>, { - let transforms = view_ctx.get::()?; + let transforms = view_ctx.get::()?; let depth_offsets = view_ctx.get::()?; let annotations = view_ctx.get::()?; @@ -93,7 +93,8 @@ where let system_identifier = System::identifier(); for data_result in query.iter_visible_data_results(ctx, system_identifier) { - let Some(transform_info) = transforms.transform_info_for_entity(&data_result.entity_path) + let Some(transform_info) = + transforms.transform_info_for_entity(data_result.entity_path.hash()) else { continue; };