1
1
use egui:: { lerp, NumExt as _, Rect } ;
2
- use glam:: Affine3A ;
3
2
use macaw:: { vec3, IsoTransform , Mat4 , Quat , Vec3 } ;
4
3
5
4
use re_space_view:: controls:: {
@@ -96,7 +95,7 @@ impl Eye {
96
95
let ray_dir = self
97
96
. world_from_rub_view
98
97
. transform_vector3 ( glam:: vec3 ( px, py, -1.0 ) ) ;
99
- macaw:: Ray3 :: from_origin_dir ( self . pos_in_world ( ) , ray_dir. normalize ( ) )
98
+ macaw:: Ray3 :: from_origin_dir ( self . pos_in_world ( ) , ray_dir. normalize_or_zero ( ) )
100
99
} else {
101
100
// The ray originates on the camera plane, not from the camera position
102
101
let ray_dir = self . world_from_rub_view . rotation ( ) . mul_vec3 ( glam:: Vec3 :: Z ) ;
@@ -159,23 +158,37 @@ pub struct OrbitEye {
159
158
pub world_from_view_rot : Quat ,
160
159
pub fov_y : f32 ,
161
160
162
- /// Zero = no up (3dof rotation)
163
- pub up : Vec3 ,
161
+ /// The up-axis of the eye itself, in world-space.
162
+ ///
163
+ /// Initially, the up-axis of the eye will be the same as the up-axis of the scene (or +Z if
164
+ /// the scene has no up axis defined).
165
+ /// Rolling the camera (e.g. middle-click) will permanently modify the eye's up axis, until the
166
+ /// next reset.
167
+ ///
168
+ /// A value of `Vec3::ZERO` is valid and will result in 3 degrees of freedom, although we never
169
+ /// use it at the moment.
170
+ pub eye_up : Vec3 ,
164
171
165
172
/// For controlling the eye with WSAD in a smooth way.
166
173
pub velocity : Vec3 ,
167
174
}
168
175
169
176
impl OrbitEye {
170
- const MAX_PITCH : f32 = 0.999 * 0.25 * std:: f32:: consts:: TAU ;
171
-
172
- pub fn new ( orbit_center : Vec3 , orbit_radius : f32 , world_from_view_rot : Quat , up : Vec3 ) -> Self {
177
+ /// Avoids zentith/nadir singularity.
178
+ const MAX_PITCH : f32 = 0.99 * 0.25 * std:: f32:: consts:: TAU ;
179
+
180
+ pub fn new (
181
+ orbit_center : Vec3 ,
182
+ orbit_radius : f32 ,
183
+ world_from_view_rot : Quat ,
184
+ eye_up : Vec3 ,
185
+ ) -> Self {
173
186
OrbitEye {
174
187
orbit_center,
175
188
orbit_radius,
176
189
world_from_view_rot,
177
190
fov_y : Eye :: DEFAULT_FOV_Y ,
178
- up ,
191
+ eye_up ,
179
192
velocity : Vec3 :: ZERO ,
180
193
}
181
194
}
@@ -206,6 +219,7 @@ impl OrbitEye {
206
219
self . world_from_view_rot = eye. world_from_rub_view . rotation ( ) ;
207
220
self . fov_y = eye. fov_y . unwrap_or ( Eye :: DEFAULT_FOV_Y ) ;
208
221
self . velocity = Vec3 :: ZERO ;
222
+ self . eye_up = eye. world_from_rub_view . rotation ( ) * glam:: Vec3 :: Y ;
209
223
}
210
224
211
225
pub fn lerp ( & self , other : & Self , t : f32 ) -> Self {
@@ -219,54 +233,28 @@ impl OrbitEye {
219
233
orbit_radius : lerp ( self . orbit_radius ..=other. orbit_radius , t) ,
220
234
world_from_view_rot : self . world_from_view_rot . slerp ( other. world_from_view_rot , t) ,
221
235
fov_y : egui:: lerp ( self . fov_y ..=other. fov_y , t) ,
222
- up : self . up . lerp ( other. up , t) . normalize_or_zero ( ) ,
236
+ // A slerp would technically be nicer for eye_up, but it only really
237
+ // matters if the user starts interacting half-way through the lerp,
238
+ // and even then it's not a big deal.
239
+ eye_up : self . eye_up . lerp ( other. eye_up , t) . normalize_or_zero ( ) ,
223
240
velocity : self . velocity . lerp ( other. velocity , t) ,
224
241
}
225
242
}
226
243
}
227
244
228
- /// Direction we are looking at
245
+ /// World-direction we are looking at
229
246
fn fwd ( & self ) -> Vec3 {
230
- self . world_from_view_rot * -Vec3 :: Z
247
+ self . world_from_view_rot * -Vec3 :: Z // view-coordinates are RUB
231
248
}
232
249
233
250
/// Only valid if we have an up vector.
234
251
///
235
252
/// `[-tau/4, +tau/4]`
236
253
fn pitch ( & self ) -> Option < f32 > {
237
- if self . up == Vec3 :: ZERO {
254
+ if self . eye_up == Vec3 :: ZERO {
238
255
None
239
256
} else {
240
- Some ( self . fwd ( ) . dot ( self . up ) . clamp ( -1.0 , 1.0 ) . asin ( ) )
241
- }
242
- }
243
-
244
- fn set_fwd ( & mut self , fwd : Vec3 ) {
245
- if let Some ( pitch) = self . pitch ( ) {
246
- let pitch = pitch. clamp ( -Self :: MAX_PITCH , Self :: MAX_PITCH ) ;
247
-
248
- let fwd = project_onto ( fwd, self . up ) . normalize ( ) ; // Remove pitch
249
- let right = fwd. cross ( self . up ) . normalize ( ) ;
250
- let fwd = Quat :: from_axis_angle ( right, pitch) * fwd; // Tilt up/down
251
- let fwd = fwd. normalize ( ) ; // Prevent drift
252
-
253
- let world_from_view_rot =
254
- Quat :: from_affine3 ( & Affine3A :: look_at_rh ( Vec3 :: ZERO , fwd, self . up ) . inverse ( ) ) ;
255
-
256
- if world_from_view_rot. is_finite ( ) {
257
- self . world_from_view_rot = world_from_view_rot;
258
- }
259
- } else {
260
- self . world_from_view_rot = Quat :: from_rotation_arc ( -Vec3 :: Z , fwd) ;
261
- }
262
- }
263
-
264
- #[ allow( unused) ]
265
- pub fn set_up ( & mut self , up : Vec3 ) {
266
- self . up = up. normalize_or_zero ( ) ;
267
-
268
- if self . up != Vec3 :: ZERO {
269
- self . set_fwd ( self . fwd ( ) ) ; // this will clamp the rotation
257
+ Some ( self . fwd ( ) . dot ( self . eye_up ) . clamp ( -1.0 , 1.0 ) . asin ( ) )
270
258
}
271
259
}
272
260
@@ -278,12 +266,12 @@ impl OrbitEye {
278
266
let mut did_interact = response. drag_delta ( ) . length ( ) > 0.0 ;
279
267
280
268
if response. drag_delta ( ) . length ( ) > drag_threshold {
281
- if response. dragged_by ( ROLL_MOUSE )
269
+ let roll = response. dragged_by ( ROLL_MOUSE )
282
270
|| ( response. dragged_by ( ROLL_MOUSE_ALT )
283
271
&& response
284
272
. ctx
285
- . input ( |i| i. modifiers . contains ( ROLL_MOUSE_MODIFIER ) ) )
286
- {
273
+ . input ( |i| i. modifiers . contains ( ROLL_MOUSE_MODIFIER ) ) ) ;
274
+ if roll {
287
275
if let Some ( pointer_pos) = response. ctx . pointer_latest_pos ( ) {
288
276
self . roll ( & response. rect , pointer_pos, response. drag_delta ( ) ) ;
289
277
}
@@ -388,31 +376,26 @@ impl OrbitEye {
388
376
let sensitivity = 0.004 ; // radians-per-point. TODO(emilk): take fov_y and canvas size into account
389
377
let delta = sensitivity * delta;
390
378
391
- if self . up == Vec3 :: ZERO {
392
- // 3-dof rotation
393
- let rot_delta = Quat :: from_rotation_y ( -delta. x ) * Quat :: from_rotation_x ( -delta. y ) ;
394
- self . world_from_view_rot *= rot_delta;
395
- } else {
379
+ if let Some ( old_pitch) = self . pitch ( ) {
396
380
// 2-dof rotation
397
- let fwd = Quat :: from_axis_angle ( self . up , -delta. x ) * self . fwd ( ) ;
398
- let fwd = fwd. normalize ( ) ; // Prevent drift
399
381
400
- let pitch = self . pitch ( ) . unwrap ( ) - delta. y ;
401
- let pitch = pitch. clamp ( -Self :: MAX_PITCH , Self :: MAX_PITCH ) ;
382
+ // Apply change in heading:
383
+ self . world_from_view_rot =
384
+ Quat :: from_axis_angle ( self . eye_up , -delta. x ) * self . world_from_view_rot ;
402
385
403
- let fwd = project_onto ( fwd, self . up ) . normalize ( ) ; // Remove pitch
404
- let right = fwd. cross ( self . up ) . normalize ( ) ;
405
- let fwd = Quat :: from_axis_angle ( right, pitch) * fwd; // Tilt up/down
406
- let fwd = fwd. normalize ( ) ; // Prevent drift
386
+ // We need to clamp pitch to avoid nadir/zenith singularity:
387
+ let new_pitch = ( old_pitch - delta. y ) . clamp ( -Self :: MAX_PITCH , Self :: MAX_PITCH ) ;
388
+ let pitch_delta = new_pitch - old_pitch;
407
389
408
- let new_world_from_view_rot =
409
- Quat :: from_affine3 ( & Affine3A :: look_at_rh ( Vec3 :: ZERO , fwd , self . up ) . inverse ( ) ) ;
390
+ // Apply change in pitch:
391
+ self . world_from_view_rot *= Quat :: from_rotation_x ( pitch_delta ) ;
410
392
411
- if new_world_from_view_rot. is_finite ( ) {
412
- self . world_from_view_rot = new_world_from_view_rot;
413
- } else {
414
- re_log:: debug_once!( "Failed to rotate camera: got non-finites" ) ;
415
- }
393
+ // Avoid numeric drift:
394
+ self . world_from_view_rot = self . world_from_view_rot . normalize ( ) ;
395
+ } else {
396
+ // no up-axis -> no pitch -> 3-dof rotation
397
+ let rot_delta = Quat :: from_rotation_y ( -delta. x ) * Quat :: from_rotation_x ( -delta. y ) ;
398
+ self . world_from_view_rot *= rot_delta;
416
399
}
417
400
}
418
401
@@ -422,9 +405,17 @@ impl OrbitEye {
422
405
let rel = pointer_pos - rect. center ( ) ;
423
406
let delta_angle = delta. rot90 ( ) . dot ( rel) / rel. length_sq ( ) ;
424
407
let rot_delta = Quat :: from_rotation_z ( delta_angle) ;
408
+
409
+ let up_in_view = self . world_from_view_rot . inverse ( ) * self . eye_up ;
410
+
425
411
self . world_from_view_rot *= rot_delta;
426
412
427
- self . up = Vec3 :: ZERO ; // forget about this until user resets the eye
413
+ // Permanently change our up-axis, at least until the user resets the view:
414
+ self . eye_up = self . world_from_view_rot * up_in_view;
415
+
416
+ // Prevent numeric drift:
417
+ self . world_from_view_rot = self . world_from_view_rot . normalize ( ) ;
418
+ self . eye_up = self . eye_up . normalize_or_zero ( ) ;
428
419
}
429
420
430
421
/// Translate based on a certain number of pixel delta.
@@ -439,8 +430,3 @@ impl OrbitEye {
439
430
self . orbit_center += translate;
440
431
}
441
432
}
442
-
443
- /// e.g. up is `[0,0,1]`, we return things like `[x,y,0]`
444
- fn project_onto ( v : Vec3 , up : Vec3 ) -> Vec3 {
445
- v - up * v. dot ( up)
446
- }
0 commit comments