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
use glam::{Mat4, Quat, Vec2, Vec3};
use crate::math::deg;
/// A perspective or orthographic camera, three.js `PerspectiveCamera` with `lookAt`.
///
/// A camera is a plain value. Build a new one for each frame:
///
/// ```rust
/// use fframes::three_d::{Camera, vec3};
///
/// let t = 1.5;
/// let camera = Camera::perspective(35.0).orbit(vec3(0.0, 1.0, 0.0), 6.0, 30.0 + t * 20.0, 12.0);
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Camera {
/// The position in meters.
pub position: Vec3,
/// The point that the camera looks at.
pub target: Vec3,
/// The up direction before roll. Default +Y.
pub up: Vec3,
/// The rotation around the view direction in radians.
pub roll: f32,
/// The vertical field of view in degrees.
pub fov: f32,
/// The distance of the near clip plane in meters. Default 0.05.
pub near: f32,
/// The distance of the far clip plane in meters. Default 500.
pub far: f32,
/// `Some(height)` makes the camera orthographic. It then shows `height` meters
/// vertically.
pub ortho_height: Option<f32>,
/// The image shift in normalized device units, as with a shift lens. `1.0` is half the
/// frame. It frames a subject off center without a turn of the camera.
pub shift: Vec2,
/// The depth of field. Default `None`: everything is sharp.
pub focus: Option<Focus>,
}
/// The depth of field: the distance that is sharp and the blur of everything else.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Focus {
/// The distance from the camera that is sharp, in meters.
pub distance: f32,
/// The blur radius in pixels at 1080 lines, for things infinitely far behind the focus.
/// Things in front blur by `blur * (focus / depth - 1)`.
pub blur: f32,
}
impl Default for Camera {
fn default() -> Self {
Self::perspective(50.0)
}
}
impl Camera {
/// A perspective camera at `(0, 0, 5)` that looks at the origin. `fov_degrees` is the
/// vertical field of view.
pub fn perspective(fov_degrees: f32) -> Self {
Self {
position: Vec3::new(0.0, 0.0, 5.0),
target: Vec3::ZERO,
up: Vec3::Y,
roll: 0.0,
fov: fov_degrees,
near: 0.05,
far: 500.0,
ortho_height: None,
shift: Vec2::ZERO,
focus: None,
}
}
/// An orthographic camera at `(0, 0, 5)` that shows `height` meters vertically.
pub fn orthographic(height: f32) -> Self {
Self {
ortho_height: Some(height),
..Self::default()
}
}
/// Sets the position in meters.
pub fn at(self, x: f32, y: f32, z: f32) -> Self {
self.position(Vec3::new(x, y, z))
}
/// Sets the position in meters.
pub fn position(mut self, position: Vec3) -> Self {
self.position = position;
self
}
/// Sets the point that the camera looks at.
pub fn look_at(mut self, target: Vec3) -> Self {
self.target = target;
self
}
/// Sets the vertical field of view in degrees.
pub fn fov(mut self, fov_degrees: f32) -> Self {
self.fov = fov_degrees;
self
}
/// Adds depth of field with the focus at `distance` meters.
///
/// `blur` is the blur radius of the far background in pixels at 1080p. 6 is subtle, 16
/// looks like a macro lens.
pub fn focus(mut self, distance: f32, blur: f32) -> Self {
self.focus = Some(Focus { distance, blur });
self
}
/// Adds depth of field with the focus on `point`, see [`Camera::focus`].
pub fn focus_on(self, point: Vec3, blur: f32) -> Self {
let distance = (point - self.position).dot(self.forward()).max(0.05);
self.focus(distance, blur)
}
/// Sets the rotation around the view direction in radians.
pub fn roll(mut self, radians: f32) -> Self {
self.roll = radians;
self
}
/// Sets the distances of the near and far clip planes in meters.
pub fn clip(mut self, near: f32, far: f32) -> Self {
self.near = near;
self.far = far;
self
}
/// Puts the camera on a sphere around `target` and looks at the target.
///
/// `distance` is the radius in meters. `azimuth` is the angle around Y in degrees: 0
/// looks from +Z, 90 from +X. `elevation` is the angle above the horizon in degrees.
pub fn orbit(mut self, target: Vec3, distance: f32, azimuth: f32, elevation: f32) -> Self {
let (az, el) = (deg(azimuth), deg(elevation));
self.target = target;
self.position =
target + Vec3::new(az.sin() * el.cos(), el.sin(), az.cos() * el.cos()) * distance;
self
}
/// The unit view direction.
pub fn forward(&self) -> Vec3 {
(self.target - self.position).normalize_or(Vec3::NEG_Z)
}
/// The unit direction to the right of the image.
pub fn right(&self) -> Vec3 {
self.forward().cross(self.up).normalize_or(Vec3::X)
}
/// The unit up direction of the image, after roll.
pub fn up_vector(&self) -> Vec3 {
let forward = self.forward();
let up = self.right().cross(forward);
Quat::from_axis_angle(forward, self.roll) * up
}
/// The transform from world space to camera space. A camera at its target looks down
/// -Z.
pub fn view_matrix(&self) -> Mat4 {
Mat4::look_to_rh(self.position, self.forward(), self.up_vector())
}
/// The projection for an image with the width to height ratio `aspect`.
pub fn projection_matrix(&self, aspect: f32) -> Mat4 {
let projection = match self.ortho_height {
Some(height) => {
let h = height * 0.5;
let w = h * aspect;
Mat4::orthographic_rh(-w, w, -h, h, self.near, self.far)
}
None => Mat4::perspective_rh(deg(self.fov), aspect, self.near, self.far),
};
if self.shift == Vec2::ZERO {
projection
} else {
Mat4::from_translation(self.shift.extend(0.0)) * projection
}
}
/// The transform from world space to clip space.
pub fn view_projection(&self, aspect: f32) -> Mat4 {
self.projection_matrix(aspect) * self.view_matrix()
}
/// The position of a world point on a `width` x `height` image.
///
/// `x` and `y` are pixels, `z` is the distance in front of the camera in meters. `None`
/// when the point is behind the camera. Use it to put SVG labels, lines and particles
/// on 3D objects: `camera.project(scene.world_position(head), 1920.0, 1080.0)`.
pub fn project(&self, world: Vec3, width: f32, height: f32) -> Option<Vec3> {
let clip = self.view_projection(width / height) * world.extend(1.0);
if clip.w <= 1e-6 {
return None;
}
let ndc = clip / clip.w;
Some(Vec3::new(
(ndc.x * 0.5 + 0.5) * width,
(0.5 - ndc.y * 0.5) * height,
-(self.view_matrix() * world.extend(1.0)).z,
))
}
/// The size in pixels of an object `world_size` meters across at `world`, on an image
/// `height` pixels high.
pub fn pixel_size(&self, world: Vec3, world_size: f32, height: f32) -> f32 {
if let Some(h) = self.ortho_height {
world_size / h * height
} else {
let depth = (self.view_matrix() * world.extend(1.0)).z.abs().max(1e-4);
world_size / (2.0 * depth * (deg(self.fov) * 0.5).tan()) * height
}
}
}