1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
use common::{terrain::TerrainGrid, vol::ReadVol};
use common_base::span;
use core::{f32::consts::PI, fmt::Debug, ops::Range};
use num::traits::{real::Real, FloatConst};
use treeculler::Frustum;
use vek::*;

pub const NEAR_PLANE: f32 = 0.0625;
pub const FAR_PLANE: f32 = 524288.06; // excessive precision: 524288.0625

const FIRST_PERSON_INTERP_TIME: f32 = 0.1;
const THIRD_PERSON_INTERP_TIME: f32 = 0.1;
const FREEFLY_INTERP_TIME: f32 = 0.0;
const LERP_ORI_RATE: f32 = 15.0;
const CLIPPING_MODE_RANGE: Range<f32> = 2.0..20.0;
pub const MIN_ZOOM: f32 = 0.1;

// Possible TODO: Add more modes
#[derive(PartialEq, Debug, Clone, Copy, Eq, Hash)]
pub enum CameraMode {
    FirstPerson = 0,
    ThirdPerson = 1,
    Freefly = 2,
}

impl Default for CameraMode {
    fn default() -> Self { Self::ThirdPerson }
}

#[derive(Clone, Copy)]
pub struct Dependents {
    pub view_mat: Mat4<f32>,
    pub view_mat_inv: Mat4<f32>,
    pub proj_mat: Mat4<f32>,
    pub proj_mat_inv: Mat4<f32>,
    /// Specifically there for satisfying our treeculler dependency, which can't
    /// handle inverted depth planes.
    pub proj_mat_treeculler: Mat4<f32>,
    pub cam_pos: Vec3<f32>,
    pub cam_dir: Vec3<f32>,
}

pub struct Camera {
    tgt_focus: Vec3<f32>,
    focus: Vec3<f32>,
    tgt_ori: Vec3<f32>,
    ori: Vec3<f32>,
    tgt_dist: f32,
    dist: f32,
    tgt_fov: f32,
    fov: f32,
    tgt_fixate: f32,
    fixate: f32,
    aspect: f32,
    mode: CameraMode,

    last_time: Option<f64>,

    dependents: Dependents,
    frustum: Frustum<f32>,
}

fn clamp_and_modulate(ori: Vec3<f32>) -> Vec3<f32> {
    Vec3 {
        // Wrap camera yaw
        x: ori.x.rem_euclid(2.0 * PI),
        // Clamp camera pitch to the vertical limits
        y: ori.y.clamp(-PI / 2.0 + 0.0001, PI / 2.0 - 0.0001),
        // Wrap camera roll
        z: ori.z.rem_euclid(2.0 * PI),
    }
}

/// Generalized method to construct a perspective projection with x ∈ [-1,1], y
/// ∈ [-1,1], z ∈ [0,1] given fov_y_radians, aspect_ratio, 1/n, and 1/f.  Note
/// that you pass in *1/n* and *1/f*, not n and f like you normally would for a
/// perspective projection; this is done to enable uniform handling of both
/// finite and infinite far planes.
///
/// The only requirements on n and f are: 1/n ≠ 1/f, and 0 ≤ 1/n * 1/f.
///
/// This ensures that the near and far plane are not identical (or else your
/// projection would not cover any distance), and that they have the same sign
/// (or else we cannot rely on clipping to properly fix your scene).  This also
/// ensures that at least one of 1/n and 1/f is not 0, and by construction it
/// guarantees that neither n nor f are 0; these are required in order to make
/// sense of the definition of near and far planes, and avoid collapsing all
/// depths to a single point.
///
/// For "typical" projections (matching perspective_lh_no), you would satisfy
/// the stronger requirements.  We give the typical conditions for each bullet
/// point, and then explain the consequences of not satisfying these conditions:
///
/// * 1/n < 1/f (0 to 1 depth planes, meaning n = near and f = far; if f < n,
///   depth planes go from 1 to 0, meaning f = near and n = far, aka "reverse
///   depth").
///
///     This is by far the most
///     likely thing to want to change; inverted depth coordinates have *far*
/// better accuracy for     DirectX / Metal / WGPU-like APIs, when using
/// floating point depth, while not being *worse*     than the alternative
/// (OpenGL-like depth, or when using fixed-point / integer depth).  For
///     maximum benefit, make sure you are using Depth32F, as on most platforms
/// this is the only     depth buffer size where floating point can be used.
///
///     It is a bit unintuitive to prove this, but it turns out that when using
/// 1 to 0 depth planes,     the point where the depth buffer has its worst
/// precision is not at the far plane (as with 0     to 1 depth planes) nor at
/// the near plane, as you might expect, but exactly at far/2 (the
///     near plane setting does not affect the point of minimum accuracy at
/// all!).  However, don't     let this fool you into believing the point of
/// worst precision has simply been moved     around--for *any* fixed Δz that is
/// the minimum amount of depth precision you want over the     whole range, and
/// any near plane, you can set the far plane farther (generally much much
///     farther!) with reversed clip space than you can with standard clip space
/// while still     getting at least that much depth precision in the worst
/// case.  Nor is this a small     worst-case; for many desirable near and far
/// plane combinations, more than half the visible     space will have
/// completely unusable precision under 0 to 1 depth, while having much better
///     than needed precision under 1 to 0 depth.
///
///     To compute the exact (at least "roughly exact") worst-case accuracy for
/// floating     point depth and a given precision target Δz, for reverse clip
/// planes (this can be computed     for the non-reversed case too, but it's
/// painful and the values are horrible, so don't     bother), we compute
/// (assuming a finite far plane--see below for details on the infinite
///     case) the change in the integer representation of the mantissa at z=n/2:
///
///     ```ignore
///     e = floor(ln(near/(far - near))/ln(2))
///     db/dz = 2^(2-e) / ((1 / far - 1 / near) * (far)^2)
///     ```
///
///     Then the maximum precision you can safely use to get a change in the
/// integer representation     of the mantissa (assuming 32-bit floating points)
/// is around:
///
///     ```ignore
///     abs(2^(-23) / (db/dz)).
///     ```
///
///     In particular, if your worst-case target accuracy over the depth range
/// is Δz, you should     be okay if:
///
///     ```ignore
///     abs(Δz * (db/dz)) * 2^(23) ≥ 1.
///     ```
///
///     This only accounts for precision of the final floating-point value, so
/// it's     possible that artifacts may be introduced elsewhere during the
/// computation that reduce     precision further; the most famous example of
/// this is that OpenGL wipes out most of the     precision gains by going from
/// [-1,1] to [0,1] by letting
///
///     ```ignore
///     clip space depth = depth * 0.5 + 0.5
///     ```
///
///     which results in huge precision errors by removing nearly all the
/// floating point values     with the most precision (those close to 0).
/// Fortunately, most such artifacts are absent     under the wgpu/DirectX/Metal
/// depth clip space model, so with any luck remaining depth     errors due to
/// the perspective warp itself should be minimal.
///
/// * 0 ≠ 1/far (finite far plane).  When this is false, the far plane is at
///   infinity; this removes the restriction of having a far plane at all, often
///   with minimal reduction in accuracy for most values in the scene.  In fact,
///   in almost all cases with non-reversed depth planes, it *improves* accuracy
///   over the finite case for the vast majority of the range; however, you
///   should be using reversed depth planes, and if you are then there is a
///   quite natural accuracy vs. distance tradeoff in the infinite case.
///
///     When using an infinite far plane, the worst-case accuracy is *always* at
/// infinity, and gets     progressively worse as you get farther away from the
/// near plane.  However, there is a     second advantage that may not be
/// immediately apparent: the perspective warp becomes much     simpler,
/// potentially removing artifacts!  Specifically, in the 0 to 1 depth plane
/// case, the     assigned depth value (after perspective division) becomes:
///
///     ```ignore
///     depth = 1 - near/z
///     ```
///
///     while in the 1 to 0 depth plane case (which you should be using), the
/// equation is even     simpler:
///
///     ```ignore
///     depth = near/z
///     ```
///
///     In the 1 to 0 case, in particular, you can see that the depth value is
/// *linear in z in     log space.*  This lets us compute, for any given target
/// precision, a *very* simple     worst-case upper bound on the maximum
/// absolute z value for which that precision can     be achieved (the upper
/// bound is tight in some cases, but in others may be conservative):
///
///     ```ignore
///     db/dz ≥ 1/z
///     ```
///
///     Plugging that into our old formula, we find that we attain the required
/// precision at least     in the range (again, this is for the 1 to 0 infinite
/// case only!):
///
///     ```ignore
///     abs(z) ≤ Δz * 2^23
///     ```
///
///     One thing you may notice is that this worst-case bound *does not depend
/// on the near plane.*     This means that (within reason) you can put the near
/// plane as close as you like and still     attain this bound.  Of course, the
/// bound is not completely tight, but it should not be off     by more than a
/// factor of 2 or so (informally proven, not made rigorous yet), so for most
///     practical purposes you can set the near plane as low as you like in this
/// case.
///
/// * 0 < 1/near (positive near plane--best used when moving *to* left-handed
///   spaces, as we normally do in OpenGL and DirectX).  A use case for *not*
///   doing this is that it allows moving *from* a left-handed space *to* a
///   right-handed space in WGPU / DirectX / Metal coordinates; this means that
///   if matrices were already set up for OpenGL using functions like look_at_rh
///   that assume right-handed coordinates, we can simply switch these to
///   look_at_lh and use a right-handed perspective projection with a negative
///   near plane, to get correct rendering behavior.  Details are out of scope
///   for this comment.
///
/// Note that there is one final, very important thing that affects possible
/// precision--the actual underlying precision of the floating point format at a
/// particular value!  As your z values go up, their precision will shrink, so
/// if at all possible try to shrink your z values down to the lowest range in
/// which they can be.  Unfortunately, this cannot be part of the perspective
/// projection itself, because by the time z gets to the projection it is
/// usually too late for values to still be integers (or coarse-grained powers
/// of 2).  Instead, try to scale down x, y, and z as soon as possible before
/// submitting them to the GPU, ideally by as large as possible of a power of 2
/// that works for your use case.  Not only will this improve depth precision
/// and recall, it will also help address other artifacts caused by values far
/// from z (such as improperly rounded rotations, or improper line equations due
/// to greedy meshing).
///
/// TODO: Consider passing fractions rather than 1/n and 1/f directly, even
/// though the logic for why it should be okay to pass them directly is probably
/// sound (they are both valid z values in the range, so gl_FragCoord.w will be
/// assigned to this, meaning if they are imprecise enough then the whole
/// calculation will be similarly imprecise).
///
/// TODO: Since it's a bit confusing that n and f are not always near and far,
/// and a negative near plane can (probably) be emulated with simple actions on
/// the perspective matrix, consider removing this functionality and replacing
/// our assertion with a single condition: `(1/far) * (1/near) < (1/near)²`.
pub fn perspective_lh_zo_general<T>(
    fov_y_radians: T,
    aspect_ratio: T,
    inv_n: T,
    inv_f: T,
) -> Mat4<T>
where
    T: Real + FloatConst + Debug,
{
    // Per comments, we only need these two assertions to make sure our calculations
    // make sense.
    debug_assert_ne!(
        inv_n, inv_f,
        "The near and far plane distances cannot be equal, found: {:?} = {:?}",
        inv_n, inv_f
    );
    debug_assert!(
        T::zero() <= inv_n * inv_f,
        "The near and far plane distances must have the same sign, found: {:?} * {:?} < 0",
        inv_n,
        inv_f
    );

    // TODO: Would be nice to separate out the aspect ratio computations.
    let two = T::one() + T::one();
    let tan_half_fovy = (fov_y_radians / two).tan();
    let m00 = T::one() / (aspect_ratio * tan_half_fovy);
    let m11 = T::one() / tan_half_fovy;
    let m23 = -T::one() / (inv_n - inv_f);
    let m22 = inv_n * (-m23);
    Mat4::new(
        m00,
        T::zero(),
        T::zero(),
        T::zero(),
        T::zero(),
        m11,
        T::zero(),
        T::zero(),
        T::zero(),
        T::zero(),
        m22,
        m23,
        T::zero(),
        T::zero(),
        T::one(),
        T::zero(),
    )
}

/// Same as perspective_lh_zo_general, but for right-handed source spaces.
pub fn perspective_rh_zo_general<T>(
    fov_y_radians: T,
    aspect_ratio: T,
    inv_n: T,
    inv_f: T,
) -> Mat4<T>
where
    T: Real + FloatConst + Debug,
{
    let mut m = perspective_lh_zo_general(fov_y_radians, aspect_ratio, inv_n, inv_f);
    m[(2, 2)] = -m[(2, 2)];
    m[(3, 2)] = -m[(3, 2)];
    m
}

impl Camera {
    /// Create a new `Camera` with default parameters.
    pub fn new(aspect: f32, mode: CameraMode) -> Self {
        // Make sure aspect is valid
        let aspect = if aspect.is_normal() { aspect } else { 1.0 };

        let dist = match mode {
            CameraMode::ThirdPerson => 10.0,
            CameraMode::FirstPerson | CameraMode::Freefly => MIN_ZOOM,
        };

        Self {
            tgt_focus: Vec3::unit_z() * 10.0,
            focus: Vec3::unit_z() * 10.0,
            tgt_ori: Vec3::zero(),
            ori: Vec3::zero(),
            tgt_dist: dist,
            dist,
            tgt_fov: 1.1,
            fov: 1.1,
            tgt_fixate: 1.0,
            fixate: 1.0,
            aspect,
            mode,

            last_time: None,

            dependents: Dependents {
                view_mat: Mat4::identity(),
                view_mat_inv: Mat4::identity(),
                proj_mat: Mat4::identity(),
                proj_mat_inv: Mat4::identity(),
                proj_mat_treeculler: Mat4::identity(),
                cam_pos: Vec3::zero(),
                cam_dir: Vec3::unit_y(),
            },
            frustum: Frustum::from_modelview_projection(Mat4::identity().into_col_arrays()),
        }
    }

    /// Compute the transformation matrices (view matrix and projection matrix)
    /// and position of the camera.
    pub fn compute_dependents(&mut self, terrain: &TerrainGrid) {
        self.compute_dependents_full(terrain, |block| block.is_opaque())
    }

    /// The is_fluid argument should return true for transparent voxels.
    pub fn compute_dependents_full<V: ReadVol>(
        &mut self,
        terrain: &V,
        is_transparent: fn(&V::Vox) -> bool,
    ) {
        span!(_guard, "compute_dependents", "Camera::compute_dependents");
        // TODO: More intelligent function to decide on which strategy to use
        if self.tgt_dist < CLIPPING_MODE_RANGE.end {
            self.compute_dependents_near(terrain, is_transparent)
        } else {
            self.compute_dependents_far(terrain, is_transparent)
        }
    }

    fn compute_dependents_near<V: ReadVol>(
        &mut self,
        terrain: &V,
        is_transparent: fn(&V::Vox) -> bool,
    ) {
        const FRUSTUM_PADDING: [Vec3<f32>; 4] = [
            Vec3::new(0.0, 0.0, -1.0),
            Vec3::new(0.0, 0.0, 1.0),
            Vec3::new(0.0, 0.0, -1.0),
            Vec3::new(0.0, 0.0, 1.0),
        ];
        // Calculate new frustum location as there may have been lerp towards tgt_dist
        // Without this, there will be camera jumping back and forth in some scenarios
        // TODO: Optimize and fix clipping still happening if self.dist << self.tgt_dist

        // Use tgt_dist, as otherwise we end up in loop due to dist depending on frustum
        // and vice versa
        let local_dependents = self.compute_dependents_helper(self.tgt_dist);
        let frustum = self.compute_frustum(&local_dependents);
        let dist = {
            frustum
                .points
                .iter()
                .take(4)
                .zip(FRUSTUM_PADDING.iter())
                .map(|(pos, padding)| {
                    let fwd = self.forward();
                    // TODO: undo once treeculler is vek15.7
                    let transformed = Vec3::new(pos.x, pos.y, pos.z);
                    transformed + 0.6 * (fwd.cross(*padding) + fwd.cross(*padding).cross(fwd))
                })
                .chain([(self.focus - self.forward() * (self.dist + 0.5))])  // Padding to behind
                .map(|pos| {
                    match terrain
                        .ray(self.focus, pos)
                        .ignore_error()
                        .max_iter(500)
                        .until(is_transparent)
                        .cast()
                    {
                        (d, Ok(Some(_))) => f32::min(d, self.tgt_dist),
                        (_, Ok(None)) => self.dist,
                        (_, Err(_)) => self.dist,
                    }
                    .max(0.0)
                })
                .reduce(f32::min)
                .unwrap_or(0.0)
        };

        // If the camera ends up being too close to the focus point, switch policies.
        if dist < CLIPPING_MODE_RANGE.start {
            self.compute_dependents_far(terrain, is_transparent);
        } else {
            if self.dist >= dist {
                self.dist = dist;
            }

            // Recompute only if needed
            if (dist - self.tgt_dist).abs() > f32::EPSILON {
                let dependents = self.compute_dependents_helper(dist);
                self.frustum = self.compute_frustum(&dependents);
                self.dependents = dependents;
            } else {
                self.dependents = local_dependents;
                self.frustum = frustum;
            }
        }
    }

    fn compute_dependents_far<V: ReadVol>(
        &mut self,
        terrain: &V,
        is_transparent: fn(&V::Vox) -> bool,
    ) {
        let dist = {
            let (start, end) = (self.focus - self.forward() * self.dist, self.focus);

            match terrain
                .ray(start, end)
                .ignore_error()
                .max_iter(500)
                .until(|b| !is_transparent(b))
                .cast()
            {
                (d, Ok(Some(_))) => f32::min(self.dist - d - 0.03, self.dist),
                (_, Ok(None)) => self.dist,
                (_, Err(_)) => self.dist,
            }
            .max(0.0)
        };

        let dependents = self.compute_dependents_helper(dist);
        self.frustum = self.compute_frustum(&dependents);
        self.dependents = dependents;
    }

    fn compute_dependents_helper(&self, dist: f32) -> Dependents {
        let view_mat = Mat4::<f32>::identity()
            * Mat4::translation_3d(-Vec3::unit_z() * dist)
            * Mat4::rotation_z(self.ori.z)
            * Mat4::rotation_x(self.ori.y)
            * Mat4::rotation_y(self.ori.x)
            * Mat4::rotation_3d(PI / 2.0, -Vec4::unit_x())
            * Mat4::translation_3d(-self.focus.map(|e| e.fract()));
        let view_mat_inv: Mat4<f32> = view_mat.inverted();

        let fov = self.get_effective_fov();
        // NOTE: We reverse the far and near planes to produce an inverted depth
        // buffer (1 to 0 z planes).
        let proj_mat =
            perspective_rh_zo_general(fov, self.aspect, 1.0 / FAR_PLANE, 1.0 / NEAR_PLANE);
        // For treeculler, we also produce a version without inverted depth.
        let proj_mat_treeculler =
            perspective_rh_zo_general(fov, self.aspect, 1.0 / NEAR_PLANE, 1.0 / FAR_PLANE);

        Dependents {
            view_mat,
            view_mat_inv,
            proj_mat,
            proj_mat_inv: proj_mat.inverted(),
            proj_mat_treeculler,
            cam_pos: Vec3::from(view_mat_inv * Vec4::unit_w()),
            cam_dir: Vec3::from(view_mat_inv * -Vec4::unit_z()),
        }
    }

    fn compute_frustum(&mut self, dependents: &Dependents) -> Frustum<f32> {
        Frustum::from_modelview_projection(
            (dependents.proj_mat_treeculler
                * dependents.view_mat
                * Mat4::translation_3d(-self.focus.map(|e| e.trunc())))
            .into_col_arrays(),
        )
    }

    pub fn frustum(&self) -> &Frustum<f32> { &self.frustum }

    pub fn dependents(&self) -> Dependents { self.dependents }

    /// Rotate the camera about its focus by the given delta, limiting the input
    /// accordingly.
    pub fn rotate_by(&mut self, delta: Vec3<f32>) {
        let delta = delta * self.fixate;
        // Wrap camera yaw
        self.tgt_ori.x = (self.tgt_ori.x + delta.x).rem_euclid(2.0 * PI);
        // Clamp camera pitch to the vertical limits
        self.tgt_ori.y = (self.tgt_ori.y + delta.y).clamp(-PI / 2.0 + 0.001, PI / 2.0 - 0.001);
        // Wrap camera roll
        self.tgt_ori.z = (self.tgt_ori.z + delta.z).rem_euclid(2.0 * PI);
    }

    /// Set the orientation of the camera about its focus.
    pub fn set_orientation(&mut self, ori: Vec3<f32>) { self.tgt_ori = clamp_and_modulate(ori); }

    /// Set the orientation of the camera about its focus without lerping.
    pub fn set_orientation_instant(&mut self, ori: Vec3<f32>) {
        self.set_orientation(ori);
        self.ori = self.tgt_ori;
    }

    /// Zoom the camera by the given delta, limiting the input accordingly.
    pub fn zoom_by(&mut self, delta: f32, cap: Option<f32>) {
        if self.mode == CameraMode::ThirdPerson {
            // Clamp camera dist to the 2 <= x <= infinity range
            self.tgt_dist = (self.tgt_dist + delta).max(2.0);
        }

        if let Some(cap) = cap {
            self.tgt_dist = self.tgt_dist.min(cap);
        }
    }

    /// Zoom with the ability to switch between first and third-person mode.
    ///
    /// Note that cap > 18237958000000.0 can cause panic due to float overflow
    pub fn zoom_switch(&mut self, delta: f32, cap: f32, scale: f32) {
        if delta > 0_f32 || self.mode != CameraMode::FirstPerson {
            let t = self.tgt_dist + delta;
            const MIN_THIRD_PERSON: f32 = 2.35;
            match self.mode {
                CameraMode::ThirdPerson => {
                    if t < MIN_THIRD_PERSON * scale {
                        self.set_mode(CameraMode::FirstPerson);
                    } else {
                        self.tgt_dist = t;
                    }
                },
                CameraMode::FirstPerson => {
                    self.set_mode(CameraMode::ThirdPerson);
                    self.tgt_dist = MIN_THIRD_PERSON * scale;
                },
                _ => {},
            }
        }

        self.tgt_dist = self.tgt_dist.min(cap);
    }

    /// Get the distance of the camera from the focus
    pub fn get_distance(&self) -> f32 { self.dist }

    /// Set the distance of the camera from the focus (i.e., zoom).
    pub fn set_distance(&mut self, dist: f32) { self.tgt_dist = dist; }

    pub fn update(&mut self, time: f64, dt: f32, smoothing_enabled: bool) {
        // This is horribly frame time dependent, but so is most of the game
        let delta = self.last_time.replace(time).map_or(0.0, |t| time - t);
        if (self.dist - self.tgt_dist).abs() > 0.01 {
            self.dist = Lerp::lerp(
                self.dist,
                self.tgt_dist,
                0.65 * (delta as f32) / self.interp_time(),
            );
        }

        if (self.fov - self.tgt_fov).abs() > 0.01 {
            self.fov = Lerp::lerp(
                self.fov,
                self.tgt_fov,
                0.65 * (delta as f32) / self.interp_time(),
            );
        }

        if (self.fixate - self.tgt_fixate).abs() > 0.01 {
            self.fixate = Lerp::lerp(
                self.fixate,
                self.tgt_fixate,
                0.65 * (delta as f32) / self.interp_time(),
            );
        }

        if (self.focus - self.tgt_focus).magnitude_squared() > 0.001 {
            let lerped_focus = Lerp::lerp(
                self.focus,
                self.tgt_focus,
                (delta as f32) / self.interp_time()
                    * if matches!(self.mode, CameraMode::FirstPerson) {
                        2.0
                    } else {
                        1.0
                    },
            );

            self.focus.x = lerped_focus.x;
            self.focus.y = lerped_focus.y;

            // Always lerp in z
            self.focus.z = lerped_focus.z;
        }

        let lerp_angle = |a: f32, b: f32, rate: f32| {
            let offs = [-2.0 * PI, 0.0, 2.0 * PI]
                .iter()
                .min_by_key(|offs: &&f32| ((a - (b + *offs)).abs() * 1000.0) as i32)
                .unwrap();
            Lerp::lerp(a, b + *offs, rate)
        };

        let ori = if smoothing_enabled {
            Vec3::new(
                lerp_angle(self.ori.x, self.tgt_ori.x, LERP_ORI_RATE * dt),
                Lerp::lerp(self.ori.y, self.tgt_ori.y, LERP_ORI_RATE * dt),
                lerp_angle(self.ori.z, self.tgt_ori.z, LERP_ORI_RATE * dt),
            )
        } else {
            self.tgt_ori
        };
        self.ori = clamp_and_modulate(ori);
    }

    pub fn interp_time(&self) -> f32 {
        match self.mode {
            CameraMode::FirstPerson => FIRST_PERSON_INTERP_TIME,
            CameraMode::ThirdPerson => THIRD_PERSON_INTERP_TIME,
            CameraMode::Freefly => FREEFLY_INTERP_TIME,
        }
    }

    /// Get the focus position of the camera.
    pub fn get_focus_pos(&self) -> Vec3<f32> { self.focus }

    /// Set the focus position of the camera.
    pub fn set_focus_pos(&mut self, focus: Vec3<f32>) { self.tgt_focus = focus; }

    /// Set the focus position of the camera, without lerping.
    pub fn force_focus_pos(&mut self, focus: Vec3<f32>) {
        self.tgt_focus = focus;
        self.focus = focus;
    }

    /// Get the aspect ratio of the camera.
    pub fn get_aspect_ratio(&self) -> f32 { self.aspect }

    /// Set the aspect ratio of the camera.
    pub fn set_aspect_ratio(&mut self, aspect: f32) {
        self.aspect = if aspect.is_normal() { aspect } else { 1.0 };
    }

    /// Get the orientation of the camera.
    pub fn get_orientation(&self) -> Vec3<f32> { self.ori }

    /// Get the orientation that the camera is moving toward.
    pub fn get_tgt_orientation(&self) -> Vec3<f32> { self.tgt_ori }

    /// Get the field of view of the camera in radians, taking into account
    /// fixation.
    pub fn get_effective_fov(&self) -> f32 { self.fov * self.fixate }

    // /// Get the field of view of the camera in radians.
    // pub fn get_fov(&self) -> f32 { self.fov }

    /// Set the field of view of the camera in radians.
    pub fn set_fov(&mut self, fov: f32) { self.tgt_fov = fov; }

    /// Set the 'fixation' proportion, allowing the camera to focus in with
    /// precise aiming. Fixation is applied on top of the regular FoV.
    pub fn set_fixate(&mut self, fixate: f32) { self.tgt_fixate = fixate; }

    /// Set the FOV in degrees
    pub fn set_fov_deg(&mut self, fov: u16) {
        //Magic value comes from pi/180; no use recalculating.
        self.set_fov((fov as f32) * 0.01745329)
    }

    /// Set the mode of the camera.
    pub fn set_mode(&mut self, mode: CameraMode) {
        if self.mode != mode {
            self.mode = mode;
            match self.mode {
                CameraMode::ThirdPerson => {
                    self.zoom_by(5.0, None);
                },
                CameraMode::FirstPerson => {
                    self.set_distance(MIN_ZOOM);
                },
                CameraMode::Freefly => {
                    self.set_distance(MIN_ZOOM);
                },
            }
        }
    }

    /// Get the mode of the camera
    pub fn get_mode(&self) -> CameraMode {
        // Perform a bit of a trick... don't report first-person until the camera has
        // lerped close enough to the player.
        match self.mode {
            CameraMode::FirstPerson if self.dist < 0.5 => CameraMode::FirstPerson,
            CameraMode::FirstPerson => CameraMode::ThirdPerson,
            mode => mode,
        }
    }

    /// Cycle the camera to its next valid mode. If is_admin is false then only
    /// modes which are accessible without admin access will be cycled to.
    pub fn next_mode(&mut self, is_admin: bool, has_target: bool) {
        if has_target {
            self.set_mode(match self.mode {
                CameraMode::ThirdPerson => CameraMode::FirstPerson,
                CameraMode::FirstPerson => {
                    if is_admin {
                        CameraMode::Freefly
                    } else {
                        CameraMode::ThirdPerson
                    }
                },
                CameraMode::Freefly => CameraMode::ThirdPerson,
            });
        } else {
            self.set_mode(CameraMode::Freefly);
        }
    }

    /// Return a unit vector in the forward direction for the current camera
    /// orientation
    pub fn forward(&self) -> Vec3<f32> {
        Vec3::new(
            f32::sin(self.ori.x) * f32::cos(self.ori.y),
            f32::cos(self.ori.x) * f32::cos(self.ori.y),
            -f32::sin(self.ori.y),
        )
    }

    /// Return a unit vector in the right direction for the current camera
    /// orientation
    pub fn right(&self) -> Vec3<f32> {
        const UP: Vec3<f32> = Vec3::new(0.0, 0.0, 1.0);
        self.forward().cross(UP).normalized()
    }

    /// Return a unit vector in the forward direction on the XY plane for
    /// the current camera orientation
    pub fn forward_xy(&self) -> Vec2<f32> { Vec2::new(f32::sin(self.ori.x), f32::cos(self.ori.x)) }

    /// Return a unit vector in the right direction on the XY plane for
    /// the current camera orientation
    pub fn right_xy(&self) -> Vec2<f32> { Vec2::new(f32::cos(self.ori.x), -f32::sin(self.ori.x)) }
}