Skip to content

Commit

Permalink
Smooth out scroll wheel input for camera zooming (#1920)
Browse files Browse the repository at this point in the history
  • Loading branch information
Wumpf committed Apr 19, 2023
1 parent be58f9a commit 2316076
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 26 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"texcoords",
"Tonemapper",
"tonemapping",
"unsmoothed",
"voronoi",
"vram",
"Wgsl"
Expand Down
84 changes: 69 additions & 15 deletions crates/re_viewer/src/ui/view_spatial/eye.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ impl Eye {
pub struct OrbitEye {
pub orbit_center: Vec3,
pub orbit_radius: f32,

pub world_from_view_rot: Quat,
pub fov_y: f32,

Expand All @@ -173,11 +174,38 @@ pub struct OrbitEye {

/// For controlling the eye with WSAD in a smooth way.
pub velocity: Vec3,

/// Left over scroll delta that still needs to be applied (smoothed out over several frames)
#[serde(skip)]
unprocessed_scroll_delta: f32,
}

impl OrbitEye {
const MAX_PITCH: f32 = 0.999 * 0.25 * std::f32::consts::TAU;

/// Scroll wheels delta are capped out at this value per second. Anything above is smoothed out over several frames.
///
/// We generally only want this to only kick in when the user scrolls fast while we maintain very high framerate,
/// so don't go too low!
///
/// To give a sense of ballpark:
/// * measured 14.0 as the value of a single notch on a logitech mouse wheel connected to a Macbook returns in a single frame (!)
/// (so scrolling 10 notches in a tenth of a second gives a per second scroll delta of 1400)
/// * macbook trackpad is typically at max 1.0 in every given frame
const MAX_SCROLL_DELTA_PER_SECOND: f32 = 1000.0;

pub fn new(orbit_center: Vec3, orbit_radius: f32, world_from_view_rot: Quat, up: Vec3) -> Self {
OrbitEye {
orbit_center,
orbit_radius,
world_from_view_rot,
fov_y: Eye::DEFAULT_FOV_Y,
up,
velocity: Vec3::ZERO,
unprocessed_scroll_delta: 0.0,
}
}

pub fn position(&self) -> Vec3 {
self.orbit_center + self.world_from_view_rot * vec3(0.0, 0.0, self.orbit_radius)
}
Expand Down Expand Up @@ -213,6 +241,10 @@ impl OrbitEye {
fov_y: egui::lerp(self.fov_y..=other.fov_y, t),
up: self.up.lerp(other.up, t).normalize_or_zero(),
velocity: self.velocity.lerp(other.velocity, t),
unprocessed_scroll_delta: lerp(
self.unprocessed_scroll_delta..=other.unprocessed_scroll_delta,
t,
),
}
}

Expand Down Expand Up @@ -261,8 +293,9 @@ impl OrbitEye {
}
}

/// Returns `true` if any change
pub fn interact(&mut self, response: &egui::Response, drag_threshold: f32) -> bool {
/// 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 {
let mut did_interact = false;

if response.drag_delta().length() > drag_threshold {
Expand All @@ -283,21 +316,42 @@ impl OrbitEye {
}
}

if response.hovered() {
let (zoom_delta, raw_scroll_delta) = if response.hovered() {
self.keyboard_navigation(&response.ctx);
let factor = response
.ctx
.input(|i| i.zoom_delta() * (i.scroll_delta.y / 200.0).exp());
if factor != 1.0 {
let new_radius = self.orbit_radius / factor;

// 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;
}
response.ctx.input(|i| (i.zoom_delta(), i.scroll_delta.y))
} else {
(1.0, 0.0)
};
if zoom_delta != 1.0 || raw_scroll_delta != 0.0 {
did_interact = true;
}

did_interact = true;
// Mouse wheels often go very large steps!
// This makes the zoom speed feel clunky, so we smooth it out over several frames.
let frame_delta = response.ctx.input(|i| i.stable_dt).at_most(0.1);
let accumulated_scroll_delta = raw_scroll_delta + self.unprocessed_scroll_delta;
let unsmoothed_scroll_per_second = accumulated_scroll_delta / frame_delta;
let scroll_dir = unsmoothed_scroll_per_second.signum();
let scroll_delta = scroll_dir
* unsmoothed_scroll_per_second
.abs()
.at_most(Self::MAX_SCROLL_DELTA_PER_SECOND)
* frame_delta;
self.unprocessed_scroll_delta = accumulated_scroll_delta - scroll_delta;

if self.unprocessed_scroll_delta.abs() > 0.1 {
// We have a lot of unprocessed scroll delta, so we need to keep calling this function.
response.ctx.request_repaint();
}

let zoom_factor = zoom_delta * (scroll_delta / 200.0).exp();
if zoom_factor != 1.0 {
let new_radius = self.orbit_radius / zoom_factor;

// 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;
}
}

Expand Down
18 changes: 7 additions & 11 deletions crates/re_viewer/src/ui/view_spatial/ui_3d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ pub fn view_3d(
state
.state_3d
.update_eye(&response, &state.scene_bbox_accum, &scene.space_cameras);
let did_interact_with_eye = orbit_eye.interact(&response, orbit_eye_drag_threshold);
let did_interact_with_eye = orbit_eye.update(&response, orbit_eye_drag_threshold);

let orbit_eye = *orbit_eye;
let eye = orbit_eye.to_eye();
Expand Down Expand Up @@ -633,14 +633,10 @@ fn default_eye(scene_bbox: &macaw::BoundingBox, space_specs: &SpaceSpecs) -> Orb

let eye_pos = center - radius * look_dir;

OrbitEye {
orbit_center: center,
orbit_radius: radius,
world_from_view_rot: Quat::from_affine3(
&Affine3A::look_at_rh(eye_pos, center, look_up).inverse(),
),
fov_y: Eye::DEFAULT_FOV_Y,
up: space_specs.up.unwrap_or(Vec3::ZERO),
velocity: Vec3::ZERO,
}
OrbitEye::new(
center,
radius,
Quat::from_affine3(&Affine3A::look_at_rh(eye_pos, center, look_up).inverse()),
space_specs.up.unwrap_or(Vec3::ZERO),
)
}

0 comments on commit 2316076

Please sign in to comment.