use crate::core::scene3d::camera::{Camera, CameraExtrinsic};
use crate::core::scene3d::mat4::{Vec3, mat4_rotate};
pub fn window_to_ndc(win: (f32, f32), size: (f32, f32)) -> (f32, f32) {
let (x, y) = win;
let (w, h) = size;
(2.0 * x / w - 1.0, 1.0 - 2.0 * y / h)
}
#[derive(Clone, Copy, Debug)]
pub struct OrbitDrag {
origin: (f32, f32),
start: CameraExtrinsic,
center: Vec3,
}
impl OrbitDrag {
pub fn begin(camera: &Camera, origin: (f32, f32), center: Vec3) -> Self {
OrbitDrag {
origin,
start: camera.extrinsic,
center,
}
}
pub fn update(&self, camera: &mut Camera, win: (f32, f32), size: (f32, f32)) {
let dx = self.origin.0 - win.0;
let dy = self.origin.1 - win.1;
let (direction, up, position) = if dx == 0.0 && dy == 0.0 {
(
self.start.direction(),
self.start.up(),
self.start.position(),
)
} else {
let minsize = size.0.min(size.1);
let distance = (dx * dx + dy * dy).sqrt();
let angle = distance / minsize * std::f32::consts::PI;
let drag = (self.start.side() * dx - self.start.up() * dy).normalized();
let axis = drag.cross(self.start.direction()).normalized();
let rotation = mat4_rotate(angle, axis.x, axis.y, axis.z);
let direction = rotation.transform_dir(self.start.direction());
let up = rotation.transform_dir(self.start.up());
let position =
self.center + rotation.transform_dir(self.start.position() - self.center);
(direction, up, position)
};
camera.extrinsic.set_orientation(Some(direction), Some(up));
camera.extrinsic.set_position(position);
}
}
#[derive(Clone, Copy, Debug)]
pub struct PanDrag {
last: Vec3,
}
impl PanDrag {
pub fn begin(win: (f32, f32), size: (f32, f32), plane_ndc_z: f32) -> Self {
let (nx, ny) = window_to_ndc(win, size);
PanDrag {
last: Vec3::new(nx, ny, plane_ndc_z),
}
}
pub fn update(&mut self, camera: &mut Camera, win: (f32, f32), size: (f32, f32)) {
let (nx, ny) = window_to_ndc(win, size);
let cur = Vec3::new(nx, ny, self.last.z);
camera.pan(self.last, cur);
self.last = cur;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::scene3d::camera::Camera;
fn approx(a: f32, b: f32, eps: f32) {
assert!((a - b).abs() < eps, "{a} != {b}");
}
fn test_camera() -> Camera {
Camera::new(
30.0,
0.1,
100.0,
(300.0, 300.0),
Vec3::new(0.0, 0.0, 5.0),
Vec3::new(0.0, 0.0, -1.0),
Vec3::new(0.0, 1.0, 0.0),
)
}
#[test]
fn window_to_ndc_maps_centre_and_corners() {
let size = (200.0, 100.0);
let (cx, cy) = window_to_ndc((100.0, 50.0), size);
approx(cx, 0.0, 1e-6);
approx(cy, 0.0, 1e-6);
let (lx, ly) = window_to_ndc((0.0, 0.0), size);
approx(lx, -1.0, 1e-6);
approx(ly, 1.0, 1e-6);
let (rx, ry) = window_to_ndc((200.0, 100.0), size);
approx(rx, 1.0, 1e-6);
approx(ry, -1.0, 1e-6);
}
#[test]
fn orbit_preserves_radius_and_keeps_looking_at_centre() {
let mut camera = test_camera();
let size = (300.0, 300.0);
let center = Vec3::ZERO;
let start_radius = camera.extrinsic.position().length();
let drag = OrbitDrag::begin(&camera, (150.0, 150.0), center);
drag.update(&mut camera, (225.0, 150.0), size);
let pos = camera.extrinsic.position();
let dir = camera.extrinsic.direction();
approx((pos - center).length(), start_radius, 1e-3);
let looked_at = pos + dir * start_radius;
approx(looked_at.x, center.x, 1e-2);
approx(looked_at.y, center.y, 1e-2);
approx(looked_at.z, center.z, 1e-2);
assert!(pos.x.abs() > 0.1, "horizontal orbit should move x: {pos:?}");
approx(dir.length(), 1.0, 1e-4);
approx(camera.extrinsic.up().length(), 1.0, 1e-4);
}
#[test]
fn orbit_zero_drag_is_identity() {
let mut camera = test_camera();
let before = camera.extrinsic.position();
let drag = OrbitDrag::begin(&camera, (150.0, 150.0), Vec3::ZERO);
drag.update(&mut camera, (150.0, 150.0), (300.0, 300.0));
let after = camera.extrinsic.position();
approx(after.x, before.x, 1e-6);
approx(after.y, before.y, 1e-6);
approx(after.z, before.z, 1e-6);
}
#[test]
fn pan_keeps_grabbed_point_under_the_cursor() {
let mut camera = test_camera();
let size = (300.0, 300.0);
let plane_z = camera.matrix().transform_point(Vec3::ZERO, true).z;
let a = (120.0, 160.0);
let b = (180.0, 130.0);
let (nax, nay) = window_to_ndc(a, size);
let (nbx, nby) = window_to_ndc(b, size);
let inv0 = camera.matrix().inverse().expect("invertible");
let p_a = inv0.transform_point(Vec3::new(nax, nay, plane_z), true);
let mut pan = PanDrag::begin(a, size, plane_z);
pan.update(&mut camera, b, size);
let projected = camera.matrix().transform_point(p_a, true);
approx(projected.x, nbx, 1e-3);
approx(projected.y, nby, 1e-3);
}
#[test]
fn pan_anchored_at_picked_depth_tracks_the_picked_point() {
let mut camera = test_camera();
let size = (300.0, 300.0);
let picked = Vec3::new(0.4, -0.3, 2.0);
let picked_ndc = camera.matrix().transform_point(picked, true);
let a_ndc = (picked_ndc.x, picked_ndc.y);
let a_win = (
(a_ndc.0 + 1.0) * 0.5 * size.0,
(1.0 - a_ndc.1) * 0.5 * size.1,
);
let b_win = (a_win.0 + 60.0, a_win.1 - 45.0);
let (nbx, nby) = window_to_ndc(b_win, size);
let mut pan = PanDrag::begin(a_win, size, picked_ndc.z);
pan.update(&mut camera, b_win, size);
let projected = camera.matrix().transform_point(picked, true);
approx(projected.x, nbx, 1e-3);
approx(projected.y, nby, 1e-3);
}
#[test]
fn orbit_pivots_on_the_picked_point() {
let mut camera = test_camera();
let size = (300.0, 300.0);
let pivot = Vec3::new(1.0, 0.5, 1.0); let start_radius = (camera.extrinsic.position() - pivot).length();
let drag = OrbitDrag::begin(&camera, (150.0, 150.0), pivot);
drag.update(&mut camera, (210.0, 170.0), size);
let pos = camera.extrinsic.position();
approx((pos - pivot).length(), start_radius, 1e-3);
assert!(
(pos - Vec3::new(0.0, 0.0, 5.0)).length() > 0.1,
"orbit should move the camera: {pos:?}"
);
let centre_radius = pos.length();
assert!(
(centre_radius - 5.0).abs() > 1e-3,
"off-centre pivot must not orbit the origin (radius stayed {centre_radius})"
);
}
}