diff --git a/crates/re_space_view_spatial/src/eye.rs b/crates/re_space_view_spatial/src/eye.rs index 5bd58b8e2735..032a5463108e 100644 --- a/crates/re_space_view_spatial/src/eye.rs +++ b/crates/re_space_view_spatial/src/eye.rs @@ -6,7 +6,7 @@ use re_space_view::controls::{ ROTATE3D_BUTTON, SPEED_UP_3D_MODIFIER, }; -use crate::space_camera_3d::SpaceCamera3D; +use crate::{scene_bounding_boxes::SceneBoundingBoxes, space_camera_3d::SpaceCamera3D}; /// An eye in a 3D view. /// @@ -149,14 +149,39 @@ impl Eye { // ---------------------------------------------------------------------------- +/// The mode of an [`ViewEye`]. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] +pub enum EyeMode { + FirstPerson, + + #[default] + Orbital, +} + +/// An eye (camera) in 3D space, controlled by the user. +/// +/// This is either a first person camera or an orbital camera, +/// controlled by [`EyeMode`]. +/// We combine these two modes in one struct because they share a lot of state and logic. +/// /// Note: we use "eye" so we don't confuse this with logged camera. #[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize, serde::Serialize)] -pub struct OrbitEye { - pub orbit_center: Vec3, - pub orbit_radius: f32, +pub struct ViewEye { + /// First person or orbital? + mode: EyeMode, + + /// Center of orbit, or camera position in first person mode. + center: Vec3, - pub world_from_view_rot: Quat, - pub fov_y: f32, + /// Ignored for [`EyeMode::FirstPerson`], + /// but kept for if/when the user switches to orbital mode. + orbit_radius: f32, + + /// Rotate to world-space from view-space (RUB). + world_from_view_rot: Quat, + + /// Vertical field of view in radians. + fov_y: f32, /// The up-axis of the eye itself, in world-space. /// @@ -167,24 +192,25 @@ pub struct OrbitEye { /// /// A value of `Vec3::ZERO` is valid and will result in 3 degrees of freedom, although we never /// use it at the moment. - pub eye_up: Vec3, + eye_up: Vec3, /// For controlling the eye with WSAD in a smooth way. - pub velocity: Vec3, + velocity: Vec3, } -impl OrbitEye { +impl ViewEye { /// Avoids zentith/nadir singularity. const MAX_PITCH: f32 = 0.99 * 0.25 * std::f32::consts::TAU; - pub fn new( + pub fn new_orbital( orbit_center: Vec3, orbit_radius: f32, world_from_view_rot: Quat, eye_up: Vec3, ) -> Self { - OrbitEye { - orbit_center, + ViewEye { + mode: EyeMode::Orbital, + center: orbit_center, orbit_radius, world_from_view_rot, fov_y: Eye::DEFAULT_FOV_Y, @@ -193,8 +219,65 @@ impl OrbitEye { } } + pub fn mode(&self) -> EyeMode { + self.mode + } + + pub fn set_mode(&mut self, new_mode: EyeMode) { + if self.mode != new_mode { + // Keep the same position: + match new_mode { + EyeMode::FirstPerson => self.center = self.position(), + EyeMode::Orbital => { + self.center = self.position() + self.orbit_radius * self.fwd(); + } + } + + self.mode = new_mode; + } + } + + /// If in orbit mode, what are we orbiting around? + pub fn orbit_center(&self) -> Option { + match self.mode { + EyeMode::FirstPerson => None, + EyeMode::Orbital => Some(self.center), + } + } + + /// If in orbit mode, how far from the orbit center are we? + pub fn orbit_radius(&self) -> Option { + match self.mode { + EyeMode::FirstPerson => None, + EyeMode::Orbital => Some(self.orbit_radius), + } + } + + /// Set what we orbit around, and at what distance. + /// + /// If we are not in orbit mode, the state will still be set and used if the user switches to orbit mode. + pub fn set_orbit_center_and_radius(&mut self, orbit_center: Vec3, orbit_radius: f32) { + // Temporarily switch to orbital, set the values, and then switch back. + // This ensures the camera position will be set correctly, even if we + // were in first-person mode: + let old_mode = self.mode(); + self.set_mode(EyeMode::Orbital); + self.center = orbit_center; + self.orbit_radius = orbit_radius; + self.set_mode(old_mode); + } + + /// The world-space position of the eye. pub fn position(&self) -> Vec3 { - self.orbit_center + self.world_from_view_rot * vec3(0.0, 0.0, self.orbit_radius) + match self.mode { + EyeMode::FirstPerson => self.center, + EyeMode::Orbital => self.center - self.orbit_radius * self.fwd(), + } + } + + /// The local up-axis, if set + pub fn eye_up(&self) -> Option { + self.eye_up.try_normalize() } pub fn to_eye(self) -> Eye { @@ -207,15 +290,23 @@ impl OrbitEye { } } - /// Create an [`OrbitEye`] from a [`Eye`]. + /// Create an [`ViewEye`] from a [`Eye`]. pub fn copy_from_eye(&mut self, eye: &Eye) { - // The hard part is finding a good center. Let's try to keep the same, and see how that goes: - let distance = eye - .forward_in_world() - .dot(self.orbit_center - eye.pos_in_world()) - .abs(); - self.orbit_radius = distance.at_least(self.orbit_radius / 5.0); - self.orbit_center = eye.pos_in_world() + self.orbit_radius * eye.forward_in_world(); + match self.mode { + EyeMode::FirstPerson => { + self.center = eye.pos_in_world(); + } + + EyeMode::Orbital => { + // The hard part is finding a good center. Let's try to keep the same, and see how that goes: + let distance = eye + .forward_in_world() + .dot(self.center - eye.pos_in_world()) + .abs(); + self.orbit_radius = distance.at_least(self.orbit_radius / 5.0); + self.center = eye.pos_in_world() + self.orbit_radius * eye.forward_in_world(); + } + } self.world_from_view_rot = eye.world_from_rub_view.rotation(); self.fov_y = eye.fov_y.unwrap_or(Eye::DEFAULT_FOV_Y); self.velocity = Vec3::ZERO; @@ -229,7 +320,8 @@ impl OrbitEye { *other // avoid rounding errors } else { Self { - orbit_center: self.orbit_center.lerp(other.orbit_center, t), + mode: other.mode, + center: self.center.lerp(other.center, t), orbit_radius: lerp(self.orbit_radius..=other.orbit_radius, t), world_from_view_rot: self.world_from_view_rot.slerp(other.world_from_view_rot, t), fov_y: egui::lerp(self.fov_y..=other.fov_y, t), @@ -247,7 +339,7 @@ impl OrbitEye { self.world_from_view_rot * -Vec3::Z // view-coordinates are RUB } - /// Only valid if we have an up vector. + /// Only valid if we have an up-vector set. /// /// `[-tau/4, +tau/4]` fn pitch(&self) -> Option { @@ -260,7 +352,28 @@ impl OrbitEye { /// Returns `true` if interaction occurred. /// I.e. the camera changed via user input. - pub fn update(&mut self, response: &egui::Response, drag_threshold: f32) -> bool { + pub fn update( + &mut self, + response: &egui::Response, + drag_threshold: f32, + bounding_boxes: &SceneBoundingBoxes, + ) -> bool { + let mut speed = match self.mode { + EyeMode::FirstPerson => 0.1 * bounding_boxes.current.size().length(), // TODO(emilk): user controlled speed + EyeMode::Orbital => self.orbit_radius, + }; + + // Modify speed based on modifiers: + let os = response.ctx.os(); + response.ctx.input(|input| { + if input.modifiers.contains(SPEED_UP_3D_MODIFIER) { + speed *= 10.0; + } + if input.modifiers.contains(RuntimeModifiers::slow_down(&os)) { + speed *= 0.1; + } + }); + // Dragging even below the [`drag_threshold`] should be considered interaction. // Otherwise we flicker in and out of "has interacted" too quickly. let mut did_interact = response.drag_delta().length() > 0.0; @@ -278,35 +391,44 @@ impl OrbitEye { } else if response.dragged_by(ROTATE3D_BUTTON) { self.rotate(response.drag_delta()); } else if response.dragged_by(DRAG_PAN3D_BUTTON) { - self.translate(response.drag_delta()); + // The pan speed is selected to make the panning feel natural for orbit mode, + // but it should probably take FOV and screen size into account + let pan_speed = 0.001 * speed; + let delta_in_view = pan_speed * response.drag_delta(); + + self.translate(delta_in_view); } } - let (zoom_delta, scroll_delta) = if response.hovered() { - did_interact |= self.keyboard_navigation(&response.ctx); - response - .ctx - .input(|i| (i.zoom_delta(), i.smooth_scroll_delta.y)) - } else { - (1.0, 0.0) - }; - if zoom_delta != 1.0 || scroll_delta.abs() > 0.1 { - did_interact = true; + if response.hovered() { + did_interact |= self.keyboard_navigation(&response.ctx, speed); } - let zoom_factor = zoom_delta * (scroll_delta / 200.0).exp(); - if zoom_factor != 1.0 { - let new_radius = self.orbit_radius / zoom_factor; - - // The user may be scrolling to move the camera closer, but are not realizing - // the radius is now tiny. - // TODO(emilk): inform the users somehow that scrolling won't help, and that they should use WSAD instead. - // It might be tempting to start moving the camera here on scroll, but that would is bad for other reasons. + if self.mode == EyeMode::Orbital { + let (zoom_delta, scroll_delta) = if response.hovered() { + response + .ctx + .input(|i| (i.zoom_delta(), i.smooth_scroll_delta.y)) + } else { + (1.0, 0.0) + }; + + let zoom_factor = zoom_delta * (scroll_delta / 200.0).exp(); + if zoom_factor != 1.0 { + let new_radius = self.orbit_radius / zoom_factor; + + // The user may be scrolling to move the camera closer, but are not realizing + // the radius is now tiny. + // TODO(emilk): inform the users somehow that scrolling won't help, and that they should use WSAD instead. + // It might be tempting to start moving the camera here on scroll, but that would is bad for other reasons. + + // Don't let radius go too small or too big because this might cause infinity/nan in some calculations. + // Max value is chosen with some generous margin of an observed crash due to infinity. + if f32::MIN_POSITIVE < new_radius && new_radius < 1.0e17 { + self.orbit_radius = new_radius; + } - // Don't let radius go too small or too big because this might cause infinity/nan in some calculations. - // Max value is chosen with some generous margin of an observed crash due to infinity. - if f32::MIN_POSITIVE < new_radius && new_radius < 1.0e17 { - self.orbit_radius = new_radius; + did_interact = true; } } @@ -316,14 +438,12 @@ impl OrbitEye { /// Listen to WSAD and QE to move the eye. /// /// Returns `true` if we did anything. - fn keyboard_navigation(&mut self, egui_ctx: &egui::Context) -> bool { + fn keyboard_navigation(&mut self, egui_ctx: &egui::Context, speed: f32) -> bool { let anything_has_focus = egui_ctx.memory(|mem| mem.focus().is_some()); if anything_has_focus { return false; // e.g. we're typing in a TextField } - let os = egui_ctx.os(); - let mut did_interact = false; let mut requires_repaint = false; @@ -340,24 +460,13 @@ impl OrbitEye { local_movement.y += input.key_down(egui::Key::E) as i32 as f32; local_movement = local_movement.normalize_or_zero(); - let speed = self.orbit_radius - * (if input.modifiers.contains(SPEED_UP_3D_MODIFIER) { - 10.0 - } else { - 1.0 - }) - * (if input.modifiers.contains(RuntimeModifiers::slow_down(&os)) { - 0.1 - } else { - 1.0 - }); let world_movement = self.world_from_view_rot * (speed * local_movement); self.velocity = egui::lerp( self.velocity..=world_movement, egui::emath::exponential_smooth_factor(0.90, 0.2, dt), ); - self.orbit_center += self.velocity * dt; + self.center += self.velocity * dt; did_interact = local_movement != Vec3::ZERO; requires_repaint = @@ -418,15 +527,13 @@ impl OrbitEye { self.eye_up = self.eye_up.normalize_or_zero(); } - /// Translate based on a certain number of pixel delta. - fn translate(&mut self, delta: egui::Vec2) { - let delta = delta * self.orbit_radius * 0.001; // TODO(emilk): take fov and screen size into account? - + /// Given a delta in view-space, translate the eye. + fn translate(&mut self, delta_in_view: egui::Vec2) { let up = self.world_from_view_rot * Vec3::Y; let right = self.world_from_view_rot * -Vec3::X; // TODO(emilk): why do we need a negation here? O.o - let translate = delta.x * right + delta.y * up; + let translate = delta_in_view.x * right + delta_in_view.y * up; - self.orbit_center += translate; + self.center += translate; } } diff --git a/crates/re_space_view_spatial/src/ui.rs b/crates/re_space_view_spatial/src/ui.rs index 6cef5689a2c1..2bcc6b3cae94 100644 --- a/crates/re_space_view_spatial/src/ui.rs +++ b/crates/re_space_view_spatial/src/ui.rs @@ -16,7 +16,6 @@ use re_viewer_context::{ }; use super::{eye::Eye, ui_2d::View2DState, ui_3d::View3DState}; -use crate::heuristics::auto_size_world_heuristic; use crate::scene_bounding_boxes::SceneBoundingBoxes; use crate::{ contexts::{AnnotationSceneContext, NonInteractiveEntities}, @@ -24,6 +23,7 @@ use crate::{ view_kind::SpatialSpaceViewKind, visualizers::{CamerasVisualizer, ImageVisualizer, UiLabel, UiLabelTarget}, }; +use crate::{eye::EyeMode, heuristics::auto_size_world_heuristic}; /// Default auto point radius in UI points. const AUTO_POINT_RADIUS: f32 = 1.5; @@ -103,110 +103,157 @@ impl SpatialSpaceViewState { .query_latest_component::(space_origin, &ctx.current_query()) .map(|c| c.value); - ctx.re_ui.selection_grid(ui, "spatial_settings_ui") + ctx.re_ui + .selection_grid(ui, "spatial_settings_ui") .show(ui, |ui| { - let auto_size_world = auto_size_world_heuristic(&self.bounding_boxes.accumulated, self.scene_num_primitives); + let auto_size_world = auto_size_world_heuristic( + &self.bounding_boxes.accumulated, + self.scene_num_primitives, + ); - ctx.re_ui.grid_left_hand_label(ui, "Default size"); - ui.vertical(|ui| { - ui.horizontal(|ui| { - ui.push_id("points", |ui| { - size_ui( - ui, - 2.0, - auto_size_world, - &mut self.auto_size_config.point_radius, - ); + ctx.re_ui.grid_left_hand_label(ui, "Default size"); + ui.vertical(|ui| { + ui.horizontal(|ui| { + ui.push_id("points", |ui| { + size_ui( + ui, + 2.0, + auto_size_world, + &mut self.auto_size_config.point_radius, + ); + }); + ui.label("Point radius") + .on_hover_text("Point radius used whenever not explicitly specified"); }); - ui.label("Point radius") - .on_hover_text("Point radius used whenever not explicitly specified"); - }); - ui.horizontal(|ui| { - ui.push_id("lines", |ui| { - size_ui( - ui, - 1.5, - auto_size_world, - &mut self.auto_size_config.line_radius, - ); - ui.label("Line radius") - .on_hover_text("Line radius used whenever not explicitly specified"); + ui.horizontal(|ui| { + ui.push_id("lines", |ui| { + size_ui( + ui, + 1.5, + auto_size_world, + &mut self.auto_size_config.line_radius, + ); + ui.label("Line radius").on_hover_text( + "Line radius used whenever not explicitly specified", + ); + }); }); }); - }); - ui.end_row(); + ui.end_row(); - ctx.re_ui.grid_left_hand_label(ui, "Camera") - .on_hover_text("The virtual camera which controls what is shown on screen"); - ui.vertical(|ui| { - if spatial_kind == SpatialSpaceViewKind::ThreeD { - if ui.button("Reset").on_hover_text( - "Resets camera position & orientation.\nYou can also double-click the 3D view.") - .clicked() - { - self.bounding_boxes.accumulated = self.bounding_boxes.current; - self.state_3d.reset_camera(&self.bounding_boxes, scene_view_coordinates); + ctx.re_ui + .grid_left_hand_label(ui, "Camera") + .on_hover_text("The virtual camera which controls what is shown on screen"); + ui.vertical(|ui| { + if spatial_kind == SpatialSpaceViewKind::ThreeD { + self.view_eye_ui(re_ui, ui, scene_view_coordinates); } - let mut spin = self.state_3d.spin(); - if re_ui.checkbox(ui, &mut spin, "Spin") - .on_hover_text("Spin camera around the orbit center").changed() { - self.state_3d.set_spin(spin); - } - } - }); - ui.end_row(); - - if spatial_kind == SpatialSpaceViewKind::ThreeD { - ctx.re_ui.grid_left_hand_label(ui, "Coordinates") - .on_hover_text("The world coordinate system used for this view"); - ui.vertical(|ui|{ - let up_description = if let Some(scene_up) = scene_view_coordinates.and_then(|vc| vc.up()) { - format!("Scene up is {scene_up}") - } else { - "Scene up is unspecified".to_owned() - }; - ui.label(up_description).on_hover_ui(|ui| { - re_ui::markdown_ui(ui, egui::Id::new("view_coordinates_tooltip"), "Set with `rerun.ViewCoordinates`."); - }); + }); + ui.end_row(); - if let Some(eye) = &self.state_3d.orbit_eye { - if eye.eye_up != glam::Vec3::ZERO { - ui.label(format!("Current camera-eye up-axis is {}", format_vector(eye.eye_up))); + if spatial_kind == SpatialSpaceViewKind::ThreeD { + ctx.re_ui + .grid_left_hand_label(ui, "Coordinates") + .on_hover_text("The world coordinate system used for this view"); + ui.vertical(|ui| { + let up_description = + if let Some(scene_up) = scene_view_coordinates.and_then(|vc| vc.up()) { + format!("Scene up is {scene_up}") + } else { + "Scene up is unspecified".to_owned() + }; + ui.label(up_description).on_hover_ui(|ui| { + re_ui::markdown_ui( + ui, + egui::Id::new("view_coordinates_tooltip"), + "Set with `rerun.ViewCoordinates`.", + ); + }); + + if let Some(eye) = &self.state_3d.view_eye { + if let Some(eye_up) = eye.eye_up() { + ui.label(format!( + "Current camera-eye up-axis is {}", + format_vector(eye_up) + )); + } } - } - re_ui.checkbox(ui, &mut self.state_3d.show_axes, "Show origin axes").on_hover_text("Show X-Y-Z axes"); - re_ui.checkbox(ui, &mut self.state_3d.show_bbox, "Show bounding box").on_hover_text("Show the current scene bounding box"); - re_ui.checkbox(ui, &mut self.state_3d.show_accumulated_bbox, "Show accumulated bounding box").on_hover_text("Show bounding box accumulated over all rendered frames"); + re_ui + .checkbox(ui, &mut self.state_3d.show_axes, "Show origin axes") + .on_hover_text("Show X-Y-Z axes"); + re_ui + .checkbox(ui, &mut self.state_3d.show_bbox, "Show bounding box") + .on_hover_text("Show the current scene bounding box"); + re_ui + .checkbox( + ui, + &mut self.state_3d.show_accumulated_bbox, + "Show accumulated bounding box", + ) + .on_hover_text( + "Show bounding box accumulated over all rendered frames", + ); + }); + ui.end_row(); + } + + ctx.re_ui + .grid_left_hand_label(ui, "Bounding box") + .on_hover_text( + "The bounding box encompassing all Entities in the view right now", + ); + ui.vertical(|ui| { + ui.style_mut().wrap = Some(false); + let BoundingBox { min, max } = self.bounding_boxes.current; + ui.label(format!("x [{} - {}]", format_f32(min.x), format_f32(max.x),)); + ui.label(format!("y [{} - {}]", format_f32(min.y), format_f32(max.y),)); + if spatial_kind == SpatialSpaceViewKind::ThreeD { + ui.label(format!("z [{} - {}]", format_f32(min.z), format_f32(max.z),)); + } }); ui.end_row(); + }); + } + + // Say the name out loud. It is fun! + fn view_eye_ui( + &mut self, + re_ui: &re_ui::ReUi, + ui: &mut egui::Ui, + scene_view_coordinates: Option, + ) { + if ui + .button("Reset") + .on_hover_text( + "Resets camera position & orientation.\nYou can also double-click the 3D view.", + ) + .clicked() + { + self.bounding_boxes.accumulated = self.bounding_boxes.current; + self.state_3d + .reset_camera(&self.bounding_boxes, scene_view_coordinates); + } + + { + let mut spin = self.state_3d.spin(); + if re_ui + .checkbox(ui, &mut spin, "Spin") + .on_hover_text("Spin camera around the orbit center") + .changed() + { + self.state_3d.set_spin(spin); } + } - ctx.re_ui.grid_left_hand_label(ui, "Bounding box") - .on_hover_text("The bounding box encompassing all Entities in the view right now"); - ui.vertical(|ui| { - ui.style_mut().wrap = Some(false); - let BoundingBox { min, max } = self.bounding_boxes.current; - ui.label(format!( - "x [{} - {}]", - format_f32(min.x), - format_f32(max.x), - )); - ui.label(format!( - "y [{} - {}]", - format_f32(min.y), - format_f32(max.y), - )); - if spatial_kind == SpatialSpaceViewKind::ThreeD { - ui.label(format!( - "z [{} - {}]", - format_f32(min.z), - format_f32(max.z), - )); - } + if let Some(eye) = &mut self.state_3d.view_eye { + ui.horizontal(|ui| { + let mut mode = eye.mode(); + ui.selectable_value(&mut mode, EyeMode::FirstPerson, "First Person"); + ui.selectable_value(&mut mode, EyeMode::Orbital, "Orbital"); + eye.set_mode(mode); }); - ui.end_row(); - }); + } } } diff --git a/crates/re_space_view_spatial/src/ui_3d.rs b/crates/re_space_view_spatial/src/ui_3d.rs index cbe35b97c5b0..df310bc77ac9 100644 --- a/crates/re_space_view_spatial/src/ui_3d.rs +++ b/crates/re_space_view_spatial/src/ui_3d.rs @@ -29,13 +29,13 @@ use crate::{ }, }; -use super::eye::{Eye, OrbitEye}; +use super::eye::{Eye, ViewEye}; // --- #[derive(Clone)] pub struct View3DState { - pub orbit_eye: Option, + pub view_eye: Option, /// Used to show the orbit center of the eye-camera when the user interacts. /// None: user has never interacted with the eye-camera. @@ -68,7 +68,7 @@ pub struct View3DState { impl Default for View3DState { fn default() -> Self { Self { - orbit_eye: Default::default(), + view_eye: Default::default(), last_eye_interaction: None, tracked_entity: None, camera_before_tracked_entity: None, @@ -97,7 +97,7 @@ impl View3DState { // Mark as interaction since we want to stop doing any automatic interpolations, // even if this is caused by a full reset. self.last_eye_interaction = Some(Instant::now()); - self.interpolate_to_orbit_eye(default_eye(&scene_bbox.current, scene_view_coordinates)); + self.interpolate_to_view_eye(default_eye(&scene_bbox.current, scene_view_coordinates)); self.tracked_entity = None; self.camera_before_tracked_entity = None; } @@ -108,12 +108,12 @@ impl View3DState { bounding_boxes: &SceneBoundingBoxes, space_cameras: &[SpaceCamera3D], scene_view_coordinates: Option, - ) -> OrbitEye { + ) -> ViewEye { // If the user has not interacted with the eye-camera yet, continue to // interpolate to the new default eye. This gives much better robustness // with scenes that grow over time. if self.last_eye_interaction.is_none() { - self.interpolate_to_orbit_eye(default_eye( + self.interpolate_to_view_eye(default_eye( &bounding_boxes.accumulated, scene_view_coordinates, )); @@ -121,7 +121,7 @@ impl View3DState { // Detect live changes to view coordinates, and interpolate to the new up axis as needed. if scene_view_coordinates != self.scene_view_coordinates { - self.interpolate_to_orbit_eye(default_eye( + self.interpolate_to_view_eye(default_eye( &bounding_boxes.accumulated, scene_view_coordinates, )); @@ -133,10 +133,10 @@ impl View3DState { if let Some(target_eye) = find_camera(space_cameras, &tracked_entity) { // For cameras, we want to exactly track the camera pose once we're done interpolating. if let Some(eye_interpolation) = &mut self.eye_interpolation { - eye_interpolation.target_orbit = None; + eye_interpolation.target_view_eye = None; eye_interpolation.target_eye = Some(target_eye); - } else if let Some(orbit_eye) = &mut self.orbit_eye { - orbit_eye.copy_from_eye(&target_eye); + } else if let Some(view_eye) = &mut self.view_eye { + view_eye.copy_from_eye(&target_eye); } } else { // For other entities we keep interpolating, so when the entity jumps, we follow smoothly. @@ -144,12 +144,12 @@ impl View3DState { } } - let orbit_eye = self.orbit_eye.get_or_insert_with(|| { + let view_eye = self.view_eye.get_or_insert_with(|| { default_eye(&bounding_boxes.accumulated, scene_view_coordinates) }); if self.spin { - orbit_eye.rotate(egui::vec2( + view_eye.rotate(egui::vec2( -response.ctx.input(|i| i.stable_dt).at_most(0.1) * 150.0, 0.0, )); @@ -163,11 +163,11 @@ impl View3DState { let t = t.clamp(0.0, 1.0); let t = ease_out(t); - if let Some(target_orbit) = &cam_interpolation.target_orbit { - *orbit_eye = cam_interpolation.start.lerp(target_orbit, t); + if let Some(target_orbit) = &cam_interpolation.target_view_eye { + *view_eye = cam_interpolation.start.lerp(target_orbit, t); } else if let Some(target_eye) = &cam_interpolation.target_eye { let camera = cam_interpolation.start.to_eye().lerp(target_eye, t); - orbit_eye.copy_from_eye(&camera); + view_eye.copy_from_eye(&camera); } else { self.eye_interpolation = None; } @@ -184,24 +184,24 @@ impl View3DState { // If we're tracking a camera right now, we want to make it slightly sticky, // so that a click on some entity doesn't immediately break the tracked state. // (Threshold is in amount of ui points the mouse was moved.) - let orbit_eye_drag_threshold = if self.tracked_entity.is_some() { + let view_eye_drag_threshold = if self.tracked_entity.is_some() { 4.0 } else { 0.0 }; - if orbit_eye.update(response, orbit_eye_drag_threshold) { + if view_eye.update(response, view_eye_drag_threshold, bounding_boxes) { self.last_eye_interaction = Some(Instant::now()); self.eye_interpolation = None; self.tracked_entity = None; self.camera_before_tracked_entity = None; } - *orbit_eye + *view_eye } fn interpolate_to_eye(&mut self, target: Eye) { - if let Some(start) = self.orbit_eye.as_mut() { + if let Some(start) = self.view_eye.as_mut() { // the user wants to move the camera somewhere, so stop spinning self.spin = false; @@ -210,14 +210,14 @@ impl View3DState { elapsed_time: 0.0, target_time, start: *start, - target_orbit: None, + target_view_eye: None, target_eye: Some(target), }); } else { start.copy_from_eye(&target); } } else { - // shouldn't really happen (`self.orbit_eye` is only `None` for the first frame). + // shouldn't really happen (`self.view_eye` is only `None` for the first frame). } } @@ -244,42 +244,46 @@ impl View3DState { if let Some(tracked_camera) = find_camera(space_cameras, entity_path) { self.interpolate_to_eye(tracked_camera); } else if let Some(entity_bbox) = bounding_boxes.per_entity.get(&entity_path.hash()) { - let Some(mut new_orbit_eye) = self.orbit_eye else { + let Some(mut new_view_eye) = self.view_eye else { // Happens only the first frame when there's no eye set up yet. return; }; let radius = entity_bbox.centered_bounding_sphere_radius() * 1.5; - if radius < 0.0001 { - // Bounding box may be zero size. - new_orbit_eye.orbit_radius = - (bounding_boxes.accumulated.centered_bounding_sphere_radius() * 1.5) - .at_least(0.01); + let orbit_radius = if radius < 0.0001 { + // Handle zero-sized bounding boxes: + (bounding_boxes.accumulated.centered_bounding_sphere_radius() * 1.5).at_least(0.01) } else { - new_orbit_eye.orbit_radius = radius; - } - new_orbit_eye.orbit_center = entity_bbox.center(); + radius + }; - self.interpolate_to_orbit_eye(new_orbit_eye); + new_view_eye.set_orbit_center_and_radius(entity_bbox.center(), orbit_radius); + + self.interpolate_to_view_eye(new_view_eye); } } - fn interpolate_to_orbit_eye(&mut self, target: OrbitEye) { + /// The taregt mode will be ignored, and the mode of the current eye will be kept unchanged. + fn interpolate_to_view_eye(&mut self, mut target: ViewEye) { + if let Some(view_eye) = &self.view_eye { + target.set_mode(view_eye.mode()); + } + // the user wants to move the camera somewhere, so stop spinning self.spin = false; - if self.orbit_eye == Some(target) { + if self.view_eye == Some(target) { return; // We're already there. } // Don't restart interpolation if we're already on it. if let Some(eye_interpolation) = &self.eye_interpolation { - if eye_interpolation.target_orbit == Some(target) { + if eye_interpolation.target_view_eye == Some(target) { return; } } - if let Some(start) = self.orbit_eye { + if let Some(start) = self.view_eye { if let Some(target_time) = EyeInterpolation::target_time(&start.to_eye(), &target.to_eye()) { @@ -287,14 +291,14 @@ impl View3DState { elapsed_time: 0.0, target_time, start, - target_orbit: Some(target), + target_view_eye: Some(target), target_eye: None, }); } else { - self.orbit_eye = Some(target); + self.view_eye = Some(target); } } else { - self.orbit_eye = Some(target); + self.view_eye = Some(target); } } @@ -310,7 +314,7 @@ impl View3DState { re_log::debug!("3D view tracks now {:?}", entity_path); self.tracked_entity = Some(entity_path.clone()); - self.camera_before_tracked_entity = self.orbit_eye.map(|eye| eye.to_eye()); + self.camera_before_tracked_entity = self.view_eye.map(|eye| eye.to_eye()); self.interpolate_eye_to_entity(entity_path, bounding_boxes, space_cameras); } @@ -331,8 +335,8 @@ impl View3DState { struct EyeInterpolation { elapsed_time: f32, target_time: f32, - start: OrbitEye, - target_orbit: Option, + start: ViewEye, + target_view_eye: Option, target_eye: Option, } @@ -452,13 +456,13 @@ pub fn view_3d( return Ok(()); // protect against problems with zero-sized views } - let orbit_eye = state.state_3d.update_eye( + let view_eye = state.state_3d.update_eye( &response, &state.bounding_boxes, space_cameras, scene_view_coordinates, ); - let eye = orbit_eye.to_eye(); + let eye = view_eye.to_eye(); // Various ui interactions draw additional lines. let mut line_builder = LineDrawableBuilder::new(ctx.render_ctx); @@ -644,97 +648,13 @@ pub fn view_3d( }); } - // Show center of orbit camera when interacting with camera (it's quite helpful). - { - const FADE_DURATION: f32 = 0.1; - - let ui_time = ui.input(|i| i.time); - let any_mouse_button_down = ui.input(|i| i.pointer.any_down()); - - let should_show_center_of_orbit_camera = state - .state_3d - .last_eye_interaction - .map_or(false, |d| d.elapsed().as_secs_f32() < 0.35); - - if !state.state_3d.eye_interact_fade_in && should_show_center_of_orbit_camera { - // Any interaction immediately causes fade in to start if it's not already on. - state.state_3d.eye_interact_fade_change_time = ui_time; - state.state_3d.eye_interact_fade_in = true; - } - if state.state_3d.eye_interact_fade_in - && !should_show_center_of_orbit_camera - // Don't start fade-out while dragging, even if mouse is still - && !any_mouse_button_down - { - state.state_3d.eye_interact_fade_change_time = ui_time; - state.state_3d.eye_interact_fade_in = false; - } - - pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 { - let t = f32::clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); - t * t * (3.0 - t * 2.0) - } - - // Compute smooth fade. - let time_since_fade_change = - (ui_time - state.state_3d.eye_interact_fade_change_time) as f32; - let orbit_center_fade = if state.state_3d.eye_interact_fade_in { - // Fade in. - smoothstep(0.0, FADE_DURATION, time_since_fade_change) - } else { - // Fade out. - smoothstep(FADE_DURATION, 0.0, time_since_fade_change) - }; - - if orbit_center_fade > 0.001 { - let half_line_length = orbit_eye.orbit_radius * 0.03; - let half_line_length = half_line_length * orbit_center_fade; - - // We distinguish the eye up-axis from the other two axes: - // Default to RFU - let up = orbit_eye.eye_up.try_normalize().unwrap_or(glam::Vec3::Z); - - // For the other two axes, try to use the scene view coordinates if available: - let right = scene_view_coordinates - .and_then(|vc| vc.right()) - .map_or(glam::Vec3::X, Vec3::from); - let forward = up - .cross(right) - .try_normalize() - .unwrap_or_else(|| up.any_orthogonal_vector()); - let right = forward.cross(up); - - line_builder - .batch("center orbit orientation help") - .add_segments( - [ - ( - orbit_eye.orbit_center, - orbit_eye.orbit_center + 0.5 * up * half_line_length, - ), - ( - orbit_eye.orbit_center - right * half_line_length, - orbit_eye.orbit_center + right * half_line_length, - ), - ( - orbit_eye.orbit_center - forward * half_line_length, - orbit_eye.orbit_center + forward * half_line_length, - ), - ] - .into_iter(), - ) - .radius(Size::new_points(0.75)) - // TODO(andreas): Fade this out. - .color(re_renderer::Color32::WHITE); - - // TODO(andreas): Idea for nice depth perception: - // Render the lines once with additive blending and depth test enabled - // and another time without depth test. In both cases it needs to be rendered last, - // something re_renderer doesn't support yet for primitives within renderers. - - ui.ctx().request_repaint(); // show it for a bit longer. - } - } + show_orbit_eye_center( + ui.ctx(), + &mut state.state_3d, + &mut line_builder, + &view_eye, + scene_view_coordinates, + ); for draw_data in draw_data { view_builder.queue_draw(draw_data); @@ -760,6 +680,106 @@ pub fn view_3d( Ok(()) } +/// Show center of orbit camera when interacting with camera (it's quite helpful). +fn show_orbit_eye_center( + egui_ctx: &egui::Context, + state_3d: &mut View3DState, + line_builder: &mut LineDrawableBuilder<'_>, + view_eye: &ViewEye, + scene_view_coordinates: Option, +) { + let Some(orbit_center) = view_eye.orbit_center() else { + return; + }; + let Some(orbit_radius) = view_eye.orbit_radius() else { + return; + }; + + const FADE_DURATION: f32 = 0.1; + + let ui_time = egui_ctx.input(|i| i.time); + let any_mouse_button_down = egui_ctx.input(|i| i.pointer.any_down()); + + let should_show_center_of_orbit_camera = state_3d + .last_eye_interaction + .map_or(false, |d| d.elapsed().as_secs_f32() < 0.35); + + if !state_3d.eye_interact_fade_in && should_show_center_of_orbit_camera { + // Any interaction immediately causes fade in to start if it's not already on. + state_3d.eye_interact_fade_change_time = ui_time; + state_3d.eye_interact_fade_in = true; + } + if state_3d.eye_interact_fade_in + && !should_show_center_of_orbit_camera + // Don't start fade-out while dragging, even if mouse is still + && !any_mouse_button_down + { + state_3d.eye_interact_fade_change_time = ui_time; + state_3d.eye_interact_fade_in = false; + } + + pub fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 { + let t = f32::clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); + t * t * (3.0 - t * 2.0) + } + + // Compute smooth fade. + let time_since_fade_change = (ui_time - state_3d.eye_interact_fade_change_time) as f32; + let orbit_center_fade = if state_3d.eye_interact_fade_in { + // Fade in. + smoothstep(0.0, FADE_DURATION, time_since_fade_change) + } else { + // Fade out. + smoothstep(FADE_DURATION, 0.0, time_since_fade_change) + }; + + if orbit_center_fade > 0.001 { + let half_line_length = orbit_radius * 0.03; + let half_line_length = half_line_length * orbit_center_fade; + + // We distinguish the eye up-axis from the other two axes: + // Default to RFU + let up = view_eye.eye_up().unwrap_or(glam::Vec3::Z); + + // For the other two axes, try to use the scene view coordinates if available: + let right = scene_view_coordinates + .and_then(|vc| vc.right()) + .map_or(glam::Vec3::X, Vec3::from); + let forward = up + .cross(right) + .try_normalize() + .unwrap_or_else(|| up.any_orthogonal_vector()); + let right = forward.cross(up); + + line_builder + .batch("center orbit orientation help") + .add_segments( + [ + (orbit_center, orbit_center + 0.5 * up * half_line_length), + ( + orbit_center - right * half_line_length, + orbit_center + right * half_line_length, + ), + ( + orbit_center - forward * half_line_length, + orbit_center + forward * half_line_length, + ), + ] + .into_iter(), + ) + .radius(Size::new_points(0.75)) + // TODO(andreas): Fade this out. + .color(re_renderer::Color32::WHITE); + + // TODO(andreas): Idea for nice depth perception: + // Render the lines once with additive blending and depth test enabled + // and another time without depth test. In both cases it needs to be rendered last, + // something re_renderer doesn't support yet for primitives within renderers. + + egui_ctx.request_repaint(); // show it for a bit longer. + } +} + fn show_projections_from_2d_space( line_builder: &mut re_renderer::LineDrawableBuilder<'_>, space_cameras: &[SpaceCamera3D], @@ -863,7 +883,7 @@ fn add_picking_ray( fn default_eye( bounding_box: &macaw::BoundingBox, scene_view_coordinates: Option, -) -> OrbitEye { +) -> ViewEye { // Defaults to RFU. let scene_right = scene_view_coordinates .and_then(|vc| vc.right()) @@ -897,7 +917,7 @@ fn default_eye( let eye_pos = center - radius * eye_dir; - OrbitEye::new( + ViewEye::new_orbital( center, radius, Quat::from_affine3(&Affine3A::look_at_rh(eye_pos, center, eye_up).inverse()),