From 6232e1bf9df7f584c700e5be665ca452a258f9c9 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 22 Feb 2024 08:06:55 +0100 Subject: [PATCH 01/12] Add a first-person camera mode --- crates/re_space_view_spatial/src/eye.rs | 145 +++++++++----- crates/re_space_view_spatial/src/ui.rs | 226 +++++++++++++--------- crates/re_space_view_spatial/src/ui_3d.rs | 22 ++- 3 files changed, 240 insertions(+), 153 deletions(-) diff --git a/crates/re_space_view_spatial/src/eye.rs b/crates/re_space_view_spatial/src/eye.rs index 5bd58b8e2735..d9d7b77f0a93 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,13 +149,33 @@ impl Eye { // ---------------------------------------------------------------------------- +/// The mode of an [`OrbitEye`]. +#[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. +/// /// 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, + /// First person or orbital? + pub mode: EyeMode, + + /// Center of orbit, or camera position in first person mode. + pub center: Vec3, + + /// Ignored for [`EyeControl::FirstPerson`]. pub orbit_radius: f32, + /// Rotate to world-space from view-space (RUB). pub world_from_view_rot: Quat, + + /// Vertical field of view in radians. pub fov_y: f32, /// The up-axis of the eye itself, in world-space. @@ -177,14 +197,15 @@ impl OrbitEye { /// 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, + mode: EyeMode::Orbital, + center: orbit_center, orbit_radius, world_from_view_rot, fov_y: Eye::DEFAULT_FOV_Y, @@ -194,7 +215,12 @@ impl OrbitEye { } 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.world_from_view_rot * Vec3::Z) + } + } } pub fn to_eye(self) -> Eye { @@ -212,10 +238,10 @@ impl OrbitEye { // 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()) + .dot(self.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(); + 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 +255,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), @@ -260,7 +287,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.accumulated.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 +326,41 @@ 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()); + let delta_in_view = 0.001 * speed * response.drag_delta(); // TODO(emilk): take fov and screen size into account? + + 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 +370,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 +392,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 +459,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..117dd108eae3 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,156 @@ 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.eye_3d_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.orbit_eye { + if eye.eye_up != glam::Vec3::ZERO { + ui.label(format!( + "Current camera-eye up-axis is {}", + format_vector(eye.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(); + }); + } + + fn eye_3d_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.orbit_eye { + ui.horizontal(|ui| { + let mode = &mut eye.mode; + ui.label("Mode:"); + ui.selectable_value(mode, EyeMode::FirstPerson, "First Person"); + ui.selectable_value(mode, EyeMode::Orbital, "Orbital"); }); - 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..465c10901c09 100644 --- a/crates/re_space_view_spatial/src/ui_3d.rs +++ b/crates/re_space_view_spatial/src/ui_3d.rs @@ -19,6 +19,7 @@ use re_viewer_context::{ }; use crate::{ + eye::EyeMode, scene_bounding_boxes::SceneBoundingBoxes, space_camera_3d::SpaceCamera3D, ui::{create_labels, outline_config, picking, screenshot_context_menu, SpatialSpaceViewState}, @@ -190,7 +191,7 @@ impl View3DState { 0.0 }; - if orbit_eye.update(response, orbit_eye_drag_threshold) { + if orbit_eye.update(response, orbit_eye_drag_threshold, bounding_boxes) { self.last_eye_interaction = Some(Instant::now()); self.eye_interpolation = None; self.tracked_entity = None; @@ -258,7 +259,7 @@ impl View3DState { } else { new_orbit_eye.orbit_radius = radius; } - new_orbit_eye.orbit_center = entity_bbox.center(); + new_orbit_eye.center = entity_bbox.center(); self.interpolate_to_orbit_eye(new_orbit_eye); } @@ -645,7 +646,8 @@ pub fn view_3d( } // Show center of orbit camera when interacting with camera (it's quite helpful). - { + let is_orbital = orbit_eye.mode == EyeMode::Orbital; + if is_orbital { const FADE_DURATION: f32 = 0.1; let ui_time = ui.input(|i| i.time); @@ -709,16 +711,16 @@ pub fn view_3d( .add_segments( [ ( - orbit_eye.orbit_center, - orbit_eye.orbit_center + 0.5 * up * half_line_length, + orbit_eye.center, + orbit_eye.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.center - right * half_line_length, + orbit_eye.center + right * half_line_length, ), ( - orbit_eye.orbit_center - forward * half_line_length, - orbit_eye.orbit_center + forward * half_line_length, + orbit_eye.center - forward * half_line_length, + orbit_eye.center + forward * half_line_length, ), ] .into_iter(), @@ -897,7 +899,7 @@ fn default_eye( let eye_pos = center - radius * eye_dir; - OrbitEye::new( + OrbitEye::new_orbital( center, radius, Quat::from_affine3(&Affine3A::look_at_rh(eye_pos, center, eye_up).inverse()), From fdafe8080f6e88d3ad9a2822880f4bab5a41b176 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 22 Feb 2024 08:13:44 +0100 Subject: [PATCH 02/12] Move out orbital center visualization code --- crates/re_space_view_spatial/src/ui_3d.rs | 199 ++++++++++++---------- 1 file changed, 107 insertions(+), 92 deletions(-) diff --git a/crates/re_space_view_spatial/src/ui_3d.rs b/crates/re_space_view_spatial/src/ui_3d.rs index 465c10901c09..88fc570f47fb 100644 --- a/crates/re_space_view_spatial/src/ui_3d.rs +++ b/crates/re_space_view_spatial/src/ui_3d.rs @@ -645,98 +645,13 @@ pub fn view_3d( }); } - // Show center of orbit camera when interacting with camera (it's quite helpful). - let is_orbital = orbit_eye.mode == EyeMode::Orbital; - if is_orbital { - 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.center, - orbit_eye.center + 0.5 * up * half_line_length, - ), - ( - orbit_eye.center - right * half_line_length, - orbit_eye.center + right * half_line_length, - ), - ( - orbit_eye.center - forward * half_line_length, - orbit_eye.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, + &orbit_eye, + scene_view_coordinates, + ); for draw_data in draw_data { view_builder.queue_draw(draw_data); @@ -762,6 +677,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<'_>, + orbit_eye: &OrbitEye, + scene_view_coordinates: Option, +) { + if orbit_eye.mode != EyeMode::Orbital { + 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_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.center, + orbit_eye.center + 0.5 * up * half_line_length, + ), + ( + orbit_eye.center - right * half_line_length, + orbit_eye.center + right * half_line_length, + ), + ( + orbit_eye.center - forward * half_line_length, + orbit_eye.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], From 89617a3aead6268c4bdcac027891dd17d72727be Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 22 Feb 2024 08:18:23 +0100 Subject: [PATCH 03/12] Some cleanup --- crates/re_space_view_spatial/src/eye.rs | 29 ++++++++++++++++++------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/re_space_view_spatial/src/eye.rs b/crates/re_space_view_spatial/src/eye.rs index d9d7b77f0a93..4f50a3455f8a 100644 --- a/crates/re_space_view_spatial/src/eye.rs +++ b/crates/re_space_view_spatial/src/eye.rs @@ -160,6 +160,10 @@ pub enum EyeMode { /// 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 { @@ -214,6 +218,7 @@ impl OrbitEye { } } + /// The world-space position of the eye. pub fn position(&self) -> Vec3 { match self.mode { EyeMode::FirstPerson => self.center, @@ -235,13 +240,21 @@ impl OrbitEye { /// Create an [`OrbitEye`] 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.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(); + 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; @@ -274,7 +287,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 { From 046382fbf7c1762af61712a0f7d66bdd25d8ef63 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 22 Feb 2024 09:25:57 +0100 Subject: [PATCH 04/12] Rename OrbitEye to ViewEye --- crates/re_space_view_spatial/src/eye.rs | 10 +-- crates/re_space_view_spatial/src/ui.rs | 4 +- crates/re_space_view_spatial/src/ui_3d.rs | 96 +++++++++++------------ 3 files changed, 55 insertions(+), 55 deletions(-) diff --git a/crates/re_space_view_spatial/src/eye.rs b/crates/re_space_view_spatial/src/eye.rs index 4f50a3455f8a..f1bd5cddac58 100644 --- a/crates/re_space_view_spatial/src/eye.rs +++ b/crates/re_space_view_spatial/src/eye.rs @@ -149,7 +149,7 @@ impl Eye { // ---------------------------------------------------------------------------- -/// The mode of an [`OrbitEye`]. +/// The mode of an [`ViewEye`]. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)] pub enum EyeMode { FirstPerson, @@ -166,7 +166,7 @@ pub enum EyeMode { /// /// 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 struct ViewEye { /// First person or orbital? pub mode: EyeMode, @@ -197,7 +197,7 @@ pub struct OrbitEye { pub velocity: Vec3, } -impl OrbitEye { +impl ViewEye { /// Avoids zentith/nadir singularity. const MAX_PITCH: f32 = 0.99 * 0.25 * std::f32::consts::TAU; @@ -207,7 +207,7 @@ impl OrbitEye { world_from_view_rot: Quat, eye_up: Vec3, ) -> Self { - OrbitEye { + ViewEye { mode: EyeMode::Orbital, center: orbit_center, orbit_radius, @@ -238,7 +238,7 @@ impl OrbitEye { } } - /// Create an [`OrbitEye`] from a [`Eye`]. + /// Create an [`ViewEye`] from a [`Eye`]. pub fn copy_from_eye(&mut self, eye: &Eye) { match self.mode { EyeMode::FirstPerson => { diff --git a/crates/re_space_view_spatial/src/ui.rs b/crates/re_space_view_spatial/src/ui.rs index 117dd108eae3..74d59790a925 100644 --- a/crates/re_space_view_spatial/src/ui.rs +++ b/crates/re_space_view_spatial/src/ui.rs @@ -170,7 +170,7 @@ impl SpatialSpaceViewState { ); }); - if let Some(eye) = &self.state_3d.orbit_eye { + if let Some(eye) = &self.state_3d.view_eye { if eye.eye_up != glam::Vec3::ZERO { ui.label(format!( "Current camera-eye up-axis is {}", @@ -245,7 +245,7 @@ impl SpatialSpaceViewState { } } - if let Some(eye) = &mut self.state_3d.orbit_eye { + if let Some(eye) = &mut self.state_3d.view_eye { ui.horizontal(|ui| { let mode = &mut eye.mode; ui.label("Mode:"); diff --git a/crates/re_space_view_spatial/src/ui_3d.rs b/crates/re_space_view_spatial/src/ui_3d.rs index 88fc570f47fb..b07dd9c745f0 100644 --- a/crates/re_space_view_spatial/src/ui_3d.rs +++ b/crates/re_space_view_spatial/src/ui_3d.rs @@ -30,13 +30,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. @@ -69,7 +69,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, @@ -98,7 +98,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; } @@ -109,12 +109,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, )); @@ -122,7 +122,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, )); @@ -136,8 +136,8 @@ impl View3DState { if let Some(eye_interpolation) = &mut self.eye_interpolation { eye_interpolation.target_orbit = 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. @@ -145,12 +145,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, )); @@ -165,10 +165,10 @@ impl View3DState { let t = ease_out(t); if let Some(target_orbit) = &cam_interpolation.target_orbit { - *orbit_eye = cam_interpolation.start.lerp(target_orbit, t); + *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; } @@ -185,24 +185,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, bounding_boxes) { + 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; @@ -218,7 +218,7 @@ impl View3DState { 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). } } @@ -245,7 +245,7 @@ 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; }; @@ -253,23 +253,23 @@ impl View3DState { 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 = + new_view_eye.orbit_radius = (bounding_boxes.accumulated.centered_bounding_sphere_radius() * 1.5) .at_least(0.01); } else { - new_orbit_eye.orbit_radius = radius; + new_view_eye.orbit_radius = radius; } - new_orbit_eye.center = entity_bbox.center(); + new_view_eye.center = entity_bbox.center(); - self.interpolate_to_orbit_eye(new_orbit_eye); + self.interpolate_to_view_eye(new_view_eye); } } - fn interpolate_to_orbit_eye(&mut self, target: OrbitEye) { + fn interpolate_to_view_eye(&mut self, target: ViewEye) { // 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. } @@ -280,7 +280,7 @@ impl View3DState { } } - 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()) { @@ -292,10 +292,10 @@ impl View3DState { 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); } } @@ -311,7 +311,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); } @@ -332,8 +332,8 @@ impl View3DState { struct EyeInterpolation { elapsed_time: f32, target_time: f32, - start: OrbitEye, - target_orbit: Option, + start: ViewEye, + target_orbit: Option, target_eye: Option, } @@ -453,13 +453,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); @@ -645,11 +645,11 @@ pub fn view_3d( }); } - show_orbit_eye_center( + show_view_eye_center( ui.ctx(), &mut state.state_3d, &mut line_builder, - &orbit_eye, + &view_eye, scene_view_coordinates, ); @@ -678,14 +678,14 @@ pub fn view_3d( } /// Show center of orbit camera when interacting with camera (it's quite helpful). -fn show_orbit_eye_center( +fn show_view_eye_center( egui_ctx: &egui::Context, state_3d: &mut View3DState, line_builder: &mut LineDrawableBuilder<'_>, - orbit_eye: &OrbitEye, + view_eye: &ViewEye, scene_view_coordinates: Option, ) { - if orbit_eye.mode != EyeMode::Orbital { + if view_eye.mode != EyeMode::Orbital { return; } @@ -728,12 +728,12 @@ fn show_orbit_eye_center( }; if orbit_center_fade > 0.001 { - let half_line_length = orbit_eye.orbit_radius * 0.03; + let half_line_length = view_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); + let up = view_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 @@ -750,16 +750,16 @@ fn show_orbit_eye_center( .add_segments( [ ( - orbit_eye.center, - orbit_eye.center + 0.5 * up * half_line_length, + view_eye.center, + view_eye.center + 0.5 * up * half_line_length, ), ( - orbit_eye.center - right * half_line_length, - orbit_eye.center + right * half_line_length, + view_eye.center - right * half_line_length, + view_eye.center + right * half_line_length, ), ( - orbit_eye.center - forward * half_line_length, - orbit_eye.center + forward * half_line_length, + view_eye.center - forward * half_line_length, + view_eye.center + forward * half_line_length, ), ] .into_iter(), @@ -880,7 +880,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()) @@ -914,7 +914,7 @@ fn default_eye( let eye_pos = center - radius * eye_dir; - OrbitEye::new_orbital( + ViewEye::new_orbital( center, radius, Quat::from_affine3(&Affine3A::look_at_rh(eye_pos, center, eye_up).inverse()), From 89c844a2b947b38bf31ea0b669ee2ca3346b5e30 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 22 Feb 2024 09:26:42 +0100 Subject: [PATCH 05/12] Better naming --- crates/re_space_view_spatial/src/ui.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/re_space_view_spatial/src/ui.rs b/crates/re_space_view_spatial/src/ui.rs index 74d59790a925..f80f3ee6b6a2 100644 --- a/crates/re_space_view_spatial/src/ui.rs +++ b/crates/re_space_view_spatial/src/ui.rs @@ -146,7 +146,7 @@ impl SpatialSpaceViewState { .on_hover_text("The virtual camera which controls what is shown on screen"); ui.vertical(|ui| { if spatial_kind == SpatialSpaceViewKind::ThreeD { - self.eye_3d_ui(re_ui, ui, scene_view_coordinates); + self.view_eye_ui(re_ui, ui, scene_view_coordinates); } }); ui.end_row(); @@ -216,7 +216,8 @@ impl SpatialSpaceViewState { }); } - fn eye_3d_ui( + // Say the name out loud. It is fun! + fn view_eye_ui( &mut self, re_ui: &re_ui::ReUi, ui: &mut egui::Ui, From 8df7d9a869629005caab98e8e0e0c03edb9435ec Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 22 Feb 2024 10:03:00 +0100 Subject: [PATCH 06/12] Make the memers of ViewEye private --- crates/re_space_view_spatial/src/eye.rs | 74 +++++++++++++++++++---- crates/re_space_view_spatial/src/ui.rs | 11 ++-- crates/re_space_view_spatial/src/ui_3d.rs | 40 ++++++------ 3 files changed, 88 insertions(+), 37 deletions(-) diff --git a/crates/re_space_view_spatial/src/eye.rs b/crates/re_space_view_spatial/src/eye.rs index f1bd5cddac58..c2e6b84ceadf 100644 --- a/crates/re_space_view_spatial/src/eye.rs +++ b/crates/re_space_view_spatial/src/eye.rs @@ -168,19 +168,20 @@ pub enum EyeMode { #[derive(Clone, Copy, Debug, PartialEq, serde::Deserialize, serde::Serialize)] pub struct ViewEye { /// First person or orbital? - pub mode: EyeMode, + mode: EyeMode, /// Center of orbit, or camera position in first person mode. - pub center: Vec3, + center: Vec3, - /// Ignored for [`EyeControl::FirstPerson`]. - pub orbit_radius: f32, + /// Ignored for [`EyeControl::FirstPerson`], + /// but kept for if/when the user switches to orbital mode. + orbit_radius: f32, /// Rotate to world-space from view-space (RUB). - pub world_from_view_rot: Quat, + world_from_view_rot: Quat, /// Vertical field of view in radians. - pub fov_y: f32, + fov_y: f32, /// The up-axis of the eye itself, in world-space. /// @@ -191,10 +192,10 @@ pub struct ViewEye { /// /// 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 ViewEye { @@ -218,16 +219,67 @@ impl ViewEye { } } + 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 { match self.mode { EyeMode::FirstPerson => self.center, - EyeMode::Orbital => { - self.center + self.orbit_radius * (self.world_from_view_rot * Vec3::Z) - } + 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 { Eye { world_from_rub_view: IsoTransform::from_rotation_translation( diff --git a/crates/re_space_view_spatial/src/ui.rs b/crates/re_space_view_spatial/src/ui.rs index f80f3ee6b6a2..75ef2718a741 100644 --- a/crates/re_space_view_spatial/src/ui.rs +++ b/crates/re_space_view_spatial/src/ui.rs @@ -171,10 +171,10 @@ impl SpatialSpaceViewState { }); if let Some(eye) = &self.state_3d.view_eye { - if eye.eye_up != glam::Vec3::ZERO { + if let Some(eye_up) = eye.eye_up() { ui.label(format!( "Current camera-eye up-axis is {}", - format_vector(eye.eye_up) + format_vector(eye_up) )); } } @@ -248,10 +248,11 @@ impl SpatialSpaceViewState { if let Some(eye) = &mut self.state_3d.view_eye { ui.horizontal(|ui| { - let mode = &mut eye.mode; + let mut mode = eye.mode(); ui.label("Mode:"); - ui.selectable_value(mode, EyeMode::FirstPerson, "First Person"); - ui.selectable_value(mode, EyeMode::Orbital, "Orbital"); + ui.selectable_value(&mut mode, EyeMode::FirstPerson, "First Person"); + ui.selectable_value(&mut mode, EyeMode::Orbital, "Orbital"); + eye.set_mode(mode); }); } } diff --git a/crates/re_space_view_spatial/src/ui_3d.rs b/crates/re_space_view_spatial/src/ui_3d.rs index b07dd9c745f0..5e4af2e0fdd7 100644 --- a/crates/re_space_view_spatial/src/ui_3d.rs +++ b/crates/re_space_view_spatial/src/ui_3d.rs @@ -19,7 +19,6 @@ use re_viewer_context::{ }; use crate::{ - eye::EyeMode, scene_bounding_boxes::SceneBoundingBoxes, space_camera_3d::SpaceCamera3D, ui::{create_labels, outline_config, picking, screenshot_context_menu, SpatialSpaceViewState}, @@ -251,15 +250,14 @@ impl View3DState { }; let radius = entity_bbox.centered_bounding_sphere_radius() * 1.5; - if radius < 0.0001 { - // Bounding box may be zero size. - new_view_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_view_eye.orbit_radius = radius; - } - new_view_eye.center = entity_bbox.center(); + radius + }; + + new_view_eye.set_orbit_center_and_radius(entity_bbox.center(), orbit_radius); self.interpolate_to_view_eye(new_view_eye); } @@ -685,9 +683,12 @@ fn show_view_eye_center( view_eye: &ViewEye, scene_view_coordinates: Option, ) { - if view_eye.mode != EyeMode::Orbital { + 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; @@ -728,12 +729,12 @@ fn show_view_eye_center( }; if orbit_center_fade > 0.001 { - let half_line_length = view_eye.orbit_radius * 0.03; + 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.try_normalize().unwrap_or(glam::Vec3::Z); + 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 @@ -749,17 +750,14 @@ fn show_view_eye_center( .batch("center orbit orientation help") .add_segments( [ + (orbit_center, orbit_center + 0.5 * up * half_line_length), ( - view_eye.center, - view_eye.center + 0.5 * up * half_line_length, - ), - ( - view_eye.center - right * half_line_length, - view_eye.center + right * half_line_length, + orbit_center - right * half_line_length, + orbit_center + right * half_line_length, ), ( - view_eye.center - forward * half_line_length, - view_eye.center + forward * half_line_length, + orbit_center - forward * half_line_length, + orbit_center + forward * half_line_length, ), ] .into_iter(), From e19d337cb144ca96d296f1795f6367428b945411 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 22 Feb 2024 10:08:48 +0100 Subject: [PATCH 07/12] Keep eye-mode when interpolating to a camera --- crates/re_space_view_spatial/src/ui_3d.rs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/crates/re_space_view_spatial/src/ui_3d.rs b/crates/re_space_view_spatial/src/ui_3d.rs index 5e4af2e0fdd7..b01d99a8cdef 100644 --- a/crates/re_space_view_spatial/src/ui_3d.rs +++ b/crates/re_space_view_spatial/src/ui_3d.rs @@ -133,7 +133,7 @@ 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(view_eye) = &mut self.view_eye { view_eye.copy_from_eye(&target_eye); @@ -163,7 +163,7 @@ impl View3DState { let t = t.clamp(0.0, 1.0); let t = ease_out(t); - if let Some(target_orbit) = &cam_interpolation.target_orbit { + 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); @@ -210,7 +210,7 @@ impl View3DState { elapsed_time: 0.0, target_time, start: *start, - target_orbit: None, + target_view_eye: None, target_eye: Some(target), }); } else { @@ -263,7 +263,12 @@ impl View3DState { } } - fn interpolate_to_view_eye(&mut self, target: ViewEye) { + /// 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; @@ -273,7 +278,7 @@ impl View3DState { // 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; } } @@ -286,7 +291,7 @@ impl View3DState { elapsed_time: 0.0, target_time, start, - target_orbit: Some(target), + target_view_eye: Some(target), target_eye: None, }); } else { @@ -331,7 +336,7 @@ struct EyeInterpolation { elapsed_time: f32, target_time: f32, start: ViewEye, - target_orbit: Option, + target_view_eye: Option, target_eye: Option, } From e0d0a8871f6bd55e17dc2ffe5b07deefeff48a1a Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Thu, 22 Feb 2024 12:02:02 +0100 Subject: [PATCH 08/12] Fix doclink --- crates/re_space_view_spatial/src/eye.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/re_space_view_spatial/src/eye.rs b/crates/re_space_view_spatial/src/eye.rs index c2e6b84ceadf..da6cfec8fce9 100644 --- a/crates/re_space_view_spatial/src/eye.rs +++ b/crates/re_space_view_spatial/src/eye.rs @@ -173,7 +173,7 @@ pub struct ViewEye { /// Center of orbit, or camera position in first person mode. center: Vec3, - /// Ignored for [`EyeControl::FirstPerson`], + /// Ignored for [`EyeMode::FirstPerson`], /// but kept for if/when the user switches to orbital mode. orbit_radius: f32, From 2fac3c56cd6a2d69edd7ac26dc2a30b1eff81912 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 26 Feb 2024 11:47:41 +0100 Subject: [PATCH 09/12] Remove "Mode" to make selection panel ui more narrow --- crates/re_space_view_spatial/src/ui.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/re_space_view_spatial/src/ui.rs b/crates/re_space_view_spatial/src/ui.rs index 75ef2718a741..2bcc6b3cae94 100644 --- a/crates/re_space_view_spatial/src/ui.rs +++ b/crates/re_space_view_spatial/src/ui.rs @@ -249,7 +249,6 @@ impl SpatialSpaceViewState { if let Some(eye) = &mut self.state_3d.view_eye { ui.horizontal(|ui| { let mut mode = eye.mode(); - ui.label("Mode:"); ui.selectable_value(&mut mode, EyeMode::FirstPerson, "First Person"); ui.selectable_value(&mut mode, EyeMode::Orbital, "Orbital"); eye.set_mode(mode); From e9c2632827f431e2ecb6fc3ce6ad1b5cecde32e9 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 26 Feb 2024 11:48:42 +0100 Subject: [PATCH 10/12] use current bbox instead --- crates/re_space_view_spatial/src/eye.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/re_space_view_spatial/src/eye.rs b/crates/re_space_view_spatial/src/eye.rs index da6cfec8fce9..28bead24dc34 100644 --- a/crates/re_space_view_spatial/src/eye.rs +++ b/crates/re_space_view_spatial/src/eye.rs @@ -359,7 +359,7 @@ impl ViewEye { bounding_boxes: &SceneBoundingBoxes, ) -> bool { let mut speed = match self.mode { - EyeMode::FirstPerson => 0.1 * bounding_boxes.accumulated.size().length(), // TODO(emilk): user controlled speed + EyeMode::FirstPerson => 0.1 * bounding_boxes.current.size().length(), // TODO(emilk): user controlled speed EyeMode::Orbital => self.orbit_radius, }; From 89cbf5ecbade669e9635f9e6e8e10b3c446c58b2 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 26 Feb 2024 11:48:50 +0100 Subject: [PATCH 11/12] show_orbit_eye_center was a better name --- crates/re_space_view_spatial/src/ui_3d.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/re_space_view_spatial/src/ui_3d.rs b/crates/re_space_view_spatial/src/ui_3d.rs index b01d99a8cdef..df310bc77ac9 100644 --- a/crates/re_space_view_spatial/src/ui_3d.rs +++ b/crates/re_space_view_spatial/src/ui_3d.rs @@ -648,7 +648,7 @@ pub fn view_3d( }); } - show_view_eye_center( + show_orbit_eye_center( ui.ctx(), &mut state.state_3d, &mut line_builder, @@ -681,7 +681,7 @@ pub fn view_3d( } /// Show center of orbit camera when interacting with camera (it's quite helpful). -fn show_view_eye_center( +fn show_orbit_eye_center( egui_ctx: &egui::Context, state_3d: &mut View3DState, line_builder: &mut LineDrawableBuilder<'_>, From bac481f20acaca68b9701efa59ab1628dccab35f Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Mon, 26 Feb 2024 11:51:41 +0100 Subject: [PATCH 12/12] Add a comment explaining the pan speed --- crates/re_space_view_spatial/src/eye.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/re_space_view_spatial/src/eye.rs b/crates/re_space_view_spatial/src/eye.rs index 28bead24dc34..032a5463108e 100644 --- a/crates/re_space_view_spatial/src/eye.rs +++ b/crates/re_space_view_spatial/src/eye.rs @@ -391,7 +391,10 @@ impl ViewEye { } else if response.dragged_by(ROTATE3D_BUTTON) { self.rotate(response.drag_delta()); } else if response.dragged_by(DRAG_PAN3D_BUTTON) { - let delta_in_view = 0.001 * speed * response.drag_delta(); // TODO(emilk): take fov and screen size into account? + // 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); }