Skip to main content

rusterix/camera/
d3orbit.rs

1use vek::{Mat4, Vec2, Vec3};
2
3use super::{D3Camera, Ray};
4
5#[derive(Clone)]
6pub struct D3OrbitCamera {
7    pub center: Vec3<f32>,
8    pub distance: f32,
9    pub azimuth: f32,
10    pub elevation: f32,
11    pub up: Vec3<f32>,
12
13    pub fov: f32,
14    pub near: f32,
15    pub far: f32,
16}
17
18impl D3Camera for D3OrbitCamera {
19    fn position(&self) -> Vec3<f32> {
20        self.eye_position()
21    }
22
23    fn new() -> Self {
24        Self {
25            center: Vec3::zero(),
26            distance: 20.0,
27            azimuth: std::f32::consts::PI / 2.0,
28            elevation: 0.698,
29            up: Vec3::unit_y(),
30
31            fov: 75.0,
32            near: 0.01,
33            far: 100.0,
34        }
35    }
36
37    fn id(&self) -> String {
38        "orbit".to_string()
39    }
40
41    fn fov(&self) -> f32 {
42        self.fov
43    }
44
45    fn distance(&self) -> f32 {
46        self.distance
47    }
48
49    fn view_matrix(&self) -> Mat4<f32> {
50        let position = self.eye_position();
51        Mat4::look_at_rh(position, self.center, self.up)
52    }
53
54    fn projection_matrix(&self, width: f32, height: f32) -> Mat4<f32> {
55        vek::Mat4::perspective_fov_rh_zo(self.fov.to_radians(), width, height, self.near, self.far)
56    }
57
58    fn set_parameter_f32(&mut self, key: &str, value: f32) {
59        #[allow(clippy::single_match)]
60        match key {
61            "distance" => {
62                self.distance = value;
63            }
64            _ => {}
65        }
66    }
67
68    fn set_parameter_vec2(&mut self, key: &str, value: Vec2<f32>) {
69        #[allow(clippy::single_match)]
70        match key {
71            "from_normalized" => {
72                self.azimuth = std::f32::consts::PI * value.x;
73                self.elevation = std::f32::consts::PI * (value.y - 0.5);
74            }
75            _ => {}
76        }
77    }
78
79    fn set_parameter_vec3(&mut self, key: &str, value: Vec3<f32>) {
80        #[allow(clippy::single_match)]
81        match key {
82            "center" => {
83                self.center = value;
84            }
85            _ => {}
86        }
87    }
88
89    /// Rotate the camera around its center point using mouse delta in screen space.
90    /// delta: mouse delta in pixels (x, y)
91    fn rotate(&mut self, delta: Vec2<f32>) {
92        // Sensitivity values (tweak as needed)
93        let sensitivity = 0.005;
94
95        self.azimuth -= delta.x * sensitivity;
96        self.elevation += delta.y * sensitivity;
97
98        // Clamp elevation to avoid flipping (just below ±90°)
99        let epsilon = 0.01;
100        let max_elevation = std::f32::consts::FRAC_PI_2 - epsilon;
101        self.elevation = self.elevation.clamp(-max_elevation, max_elevation);
102    }
103
104    /// Zoom the camera in or out based on vertical mouse delta
105    fn zoom(&mut self, delta: f32) {
106        let zoom_sensitivity = 0.05;
107
108        // Compute zoom factor (make sure it's always > 0)
109        let zoom_factor = (1.0 - delta * zoom_sensitivity).clamp(0.5, 2.0);
110
111        self.distance *= zoom_factor;
112
113        self.distance = self.distance.clamp(0.1, 100.0);
114    }
115
116    /// Create a ray from a screen-space UV coordinate and offset.
117    fn create_ray(&self, uv: Vec2<f32>, screen: Vec2<f32>, offset: Vec2<f32>) -> Ray {
118        let aspect = screen.x / screen.y;
119        let pixel_size = Vec2::new(1.0 / screen.x, 1.0 / screen.y);
120
121        let mut uv = uv;
122        uv.y = 1.0 - uv.y;
123
124        // Orbit camera position
125        let position = self.eye_position();
126
127        // Compute correct basis
128        let forward = (self.center - position).normalized(); // from eye to center
129        let mut right = forward.cross(self.up);
130        if right.magnitude_squared() < 1e-12 {
131            right = Vec3::unit_x();
132        }
133        right = right.normalized();
134        let up = right.cross(forward).normalized();
135
136        // Screen plane height/width
137        let half_height = (self.fov.to_radians() * 0.5).tan();
138        let half_width = half_height * aspect;
139
140        // Now build the ray
141        let pixel_ndc = Vec2::new(
142            (pixel_size.x * offset.x + uv.x) * 2.0 - 1.0, // [-1..1]
143            (pixel_size.y * offset.y + uv.y) * 2.0 - 1.0,
144        );
145
146        let dir = (forward + right * pixel_ndc.x * half_width - up * pixel_ndc.y * half_height) // ← minus Y because screen Y usually goes down
147            .normalized();
148
149        Ray {
150            origin: position,
151            dir,
152        }
153    }
154
155    fn basis_vectors(&self) -> (Vec3<f32>, Vec3<f32>, Vec3<f32>) {
156        let position = self.eye_position();
157
158        let forward = (self.center - position).normalized();
159        let mut right = forward.cross(self.up);
160        if right.magnitude_squared() < 1e-12 {
161            right = Vec3::unit_x();
162        }
163        right = right.normalized();
164        let up = right.cross(forward).normalized();
165        (forward, right, up)
166    }
167
168    /// Generate a SceneVM camera
169    fn as_scenevm_camera(&self) -> scenevm::Camera3D {
170        let pos = self.eye_position();
171        let (forward, right, up) = self.basis_vectors();
172        scenevm::Camera3D {
173            kind: scenevm::CameraKind::OrbitPersp,
174            pos,
175            forward,
176            right,
177            up,
178            vfov_deg: self.fov,
179            near: self.near,
180            far: self.far,
181            ..Default::default()
182        }
183    }
184}
185
186impl D3OrbitCamera {
187    #[inline]
188    fn eye_position(&self) -> Vec3<f32> {
189        // Convert spherical coordinates to cartesian coordinates
190        let x = self.distance * self.azimuth.cos() * self.elevation.cos();
191        let y = self.distance * self.elevation.sin();
192        let z = self.distance * self.azimuth.sin() * self.elevation.cos();
193
194        Vec3::new(x, y, z) + self.center
195    }
196}