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
// Follow camera — third-person camera with exponential smoothing.
//
// Tracks a target position with configurable distance, height, and smoothing.
// Mouse drag controls yaw/pitch, scroll controls distance.
/// Third-person follow camera.
pub struct FollowCamera {
/// Distance from target (meters).
pub distance: f32,
/// Height offset above target (meters).
pub height: f32,
/// Camera yaw (radians, 0 = looking along +X).
pub yaw: f32,
/// Camera pitch (radians, positive = looking down).
pub pitch: f32,
/// Exponential smoothing factor (0 = instant, 1 = never moves).
pub smoothing: f32,
/// Current smoothed camera position.
pub position: [f32; 3],
}
impl Default for FollowCamera {
fn default() -> Self {
Self {
distance: 8.0,
height: 3.0,
yaw: std::f32::consts::FRAC_PI_2, // PI/2: camera behind player (+Z)
pitch: 0.3,
smoothing: 0.1,
position: [0.0, 3.0, 8.0],
}
}
}
impl FollowCamera {
pub fn new(distance: f32, height: f32) -> Self {
Self {
distance,
height,
..Default::default()
}
}
/// Update camera position to follow a target with exponential smoothing.
/// Returns (camera_position, look_target).
pub fn update(&mut self, target: [f32; 3], dt: f32) -> ([f32; 3], [f32; 3]) {
// Compute ideal camera position from yaw/pitch/distance.
let cos_pitch = self.pitch.cos();
let sin_pitch = self.pitch.sin();
let cos_yaw = self.yaw.cos();
let sin_yaw = self.yaw.sin();
let ideal = [
target[0] + self.distance * cos_pitch * cos_yaw,
target[1] + self.height + self.distance * sin_pitch,
target[2] + self.distance * cos_pitch * sin_yaw,
];
// Exponential lerp toward ideal position.
let alpha = 1.0 - (-dt / self.smoothing.max(0.001)).exp();
self.position[0] += (ideal[0] - self.position[0]) * alpha;
self.position[1] += (ideal[1] - self.position[1]) * alpha;
self.position[2] += (ideal[2] - self.position[2]) * alpha;
(self.position, target)
}
/// Handle mouse drag input (dx, dy in screen pixels).
pub fn handle_mouse_drag(&mut self, dx: f32, dy: f32) {
let sensitivity = 0.005;
self.yaw += dx * sensitivity;
self.pitch = (self.pitch + dy * sensitivity).clamp(-1.2, 1.2);
}
/// Handle scroll wheel input (delta in scroll units).
pub fn handle_scroll(&mut self, delta: f32) {
self.distance = (self.distance - delta * 0.5).clamp(2.0, 30.0);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn camera_converges_on_target() {
let mut camera = FollowCamera::new(10.0, 5.0);
camera.smoothing = 0.05;
let target = [10.0, 0.0, 10.0];
// Run many frames to converge.
for _ in 0..200 {
camera.update(target, 0.016);
}
// Camera should be near the ideal position relative to target.
let dx = camera.position[0] - target[0];
let dz = camera.position[2] - target[2];
let dist = (dx * dx + dz * dz).sqrt();
assert!(
(dist - camera.distance).abs() < 1.0,
"camera should converge: dist={dist}, expected ~{}",
camera.distance
);
}
}