1use crate::{Point, Direction, Float, Vec2, rand_utils};
2use glam::IVec2;
3use crate::tracer::{
4 film::FilmSample, ray::Ray,
5 onb::Onb, Color
6};
7
8pub struct CameraConfig {
10 pub origin: Point,
12 pub camera_basis: Onb,
14 pub resolution: IVec2,
16 pub focal_length: Float,
18 pub lens_radius: Float,
20}
21
22impl CameraConfig {
23 pub fn new(
25 origin: Point,
26 towards: Point,
27 up: Direction,
28 lens_radius: Float,
29 focal_length: Float,
30 resolution: (i32, i32)
31 ) -> Self {
32 assert!(resolution.0 > 0 && resolution.1 > 0);
33 assert!(lens_radius >= 0.0);
34 assert!(origin != towards);
35 assert!(up.length() != 0.0);
36
37 let forward = (towards - origin).normalize();
38 let right = forward.cross(up).normalize();
39 let down = forward.cross(right);
40
41 let camera_basis = Onb::new_from_basis(right, down, forward);
43 let (width, height) = resolution;
44
45 Self {
46 lens_radius,
47 focal_length,
48 origin,
49 camera_basis,
50 resolution: IVec2::new(width, height),
51 }
52 }
53}
54
55pub enum Camera {
57 Perspective(CameraConfig, Float),
59 Orthographic(CameraConfig, Float),
61}
62
63impl Camera {
64 #[allow(clippy::too_many_arguments)]
77 pub fn orthographic(
78 origin: Point,
79 towards: Point,
80 up: Direction,
81 image_plane_scale: Float,
82 lens_radius: Float,
83 focal_length: Float,
84 width: i32,
85 height: i32,
86 ) -> Self {
87 assert!(image_plane_scale > 0.0);
88
89 Self::Orthographic(
90 CameraConfig::new(
91 origin,
92 towards,
93 up,
94 lens_radius,
95 focal_length,
96 (width, height)
97 ),
98 image_plane_scale,
99 )
100 }
101
102 #[allow(clippy::too_many_arguments)]
115 pub fn perspective(
116 origin: Point,
117 towards: Point,
118 up: Direction,
119 vfov: Float,
120 lens_radius: Float,
121 focal_length: Float,
122 width: i32,
123 height: i32,
124 ) -> Self {
125 assert!(vfov > 0.0 && vfov < 180.0);
126
127 Self::Perspective(
128 CameraConfig::new(
129 origin,
130 towards,
131 up,
132 lens_radius,
133 focal_length,
134 (width, height)
135 ),
136 vfov.to_radians() / 2.0,
137 )
138 }
139
140 pub fn default(width: i32, height: i32) -> Self {
143 Self::perspective(
144 Point::ZERO,
145 Point::NEG_Z,
146 Direction::Y,
147 90.0,
148 0.0,
149 0.0,
150 width,
151 height,
152 )
153 }
154
155 fn get_cfg(&self) -> &CameraConfig {
156 match self {
157 Self::Orthographic(cfg, _) | Self::Perspective(cfg, _) => cfg,
158 }
159 }
160
161 pub fn get_resolution(&self) -> IVec2 {
163 self.get_cfg().resolution
164 }
165
166 fn add_dof(xo_local: Point, wi_local: Direction, cfg: &CameraConfig) -> Ray {
168 let (xo_local, wi_local) = if cfg.lens_radius == 0.0 {
169 (xo_local, wi_local)
170 } else {
171 let lens_xy = cfg.lens_radius
172 * rand_utils::square_to_disk(rand_utils::unit_square());
173 let lens_xyz = lens_xy.extend(0.0);
174
175 let focus_distance = cfg.focal_length / wi_local.z;
176
177 let focus_xyz = focus_distance * wi_local;
178
179 (lens_xyz, focus_xyz - lens_xyz)
180 };
181 let xo = cfg.origin + cfg.camera_basis.to_world(xo_local);
183 let wi = cfg.camera_basis.to_world(wi_local);
184 Ray::new(xo, wi)
185 }
186
187 pub fn generate_ray(&self, raster_xy: Vec2) -> Ray {
189 let resolution = self.get_resolution();
190 let resolution = Vec2::new(
191 resolution.x as Float,
192 resolution.y as Float,
193 );
194 let min_res = resolution.min_element();
195 let screen_xy = (2.0 * raster_xy - resolution) / min_res;
197
198 match self {
199 Self::Perspective(cfg, vfov_half) => {
200 let wi_local = screen_xy.extend(
202 resolution.y / (min_res * vfov_half.tan())
203 ).normalize();
204
205 Self::add_dof(Point::ZERO, wi_local, cfg)
206 }
207 Self::Orthographic(cfg, scale) => {
208 let screen_xyz = screen_xy.extend(0.0);
209 let xo_local = *scale * screen_xyz;
210
211 Self::add_dof(xo_local, Direction::Z, cfg)
212 }
213 }
214 }
215
216 pub fn sample_towards(&self, xi: Point, rand_sq: Vec2) -> Ray {
218 let cfg = self.get_cfg();
219 let xo_local = rand_utils::square_to_disk(rand_sq).extend(0.0)
220 * cfg.lens_radius;
221 let xo = cfg.origin + cfg.camera_basis.to_world(xo_local);
222
223 let wi = (xi - xo).normalize();
224
225 Ray::new(xo, wi)
226 }
227
228 pub fn sample_towards_pdf(&self, ro: &Ray, xi: Point) -> Float {
230 let cfg = self.get_cfg();
231 let xo = ro.origin;
232 let wi = ro.dir;
233 let ng = cfg.camera_basis.to_world(Direction::Z);
234
235 let lens_area = if cfg.lens_radius == 0.0 {
236 1.0
237 } else {
238 cfg.lens_radius * cfg.lens_radius * crate::PI
239 };
240
241 let pdf = xi.distance_squared(xo) / (ng.dot(wi) * lens_area);
242 pdf.max(0.0)
243 }
244
245 pub fn pdf(&self, wi: Direction) -> Float {
247 let cfg = self.get_cfg();
248 let wi_local = cfg.camera_basis.to_local(wi);
249 let cos_theta = wi_local.z;
250
251 if cos_theta <= 0.0 {
252 0.0
253 } else {
254 let area_coeff = {
255 let res = self.get_resolution();
256 let res = Vec2::new(
257 res.x as Float,
258 res.y as Float,
259 );
260 let min_res = res.min_element();
261 let screen_bounds = res / min_res;
262 screen_bounds.x * screen_bounds.y
263 };
264
265 1.0 / (area_coeff * cos_theta * cos_theta * cos_theta)
266 }
267 }
268
269 pub fn importance_sample(&self, ro: &Ray) -> FilmSample {
271 match self {
272 Self::Orthographic(..) => unimplemented!(),
273 Self::Perspective(cfg, _) => {
274 let wi = ro.dir;
275 let wi_local = cfg.camera_basis.to_local(wi);
276 let cos_theta = wi_local.z;
277 if cos_theta < 0.0 {
278 return FilmSample::default();
279 }
280
281 let pdf = self.pdf(wi);
282
283 let color = Color::splat(pdf);
284
285 let fl = if cfg.lens_radius == 0.0 {
286 1.0 / cos_theta
287 } else {
288 cfg.focal_length / cos_theta
289 };
290
291 let resolution = self.get_resolution();
292 let resolution = Vec2::new(
293 resolution.x as Float,
294 resolution.y as Float,
295 );
296 let min_res = resolution.min_element();
297
298 let focus = ro.at(fl);
299 let focus_local = cfg.camera_basis.to_local(focus) + cfg.origin;
300 let raster_xy = (focus_local.truncate() * min_res + resolution) / 2.0;
301
302 FilmSample::new(color, raster_xy, true)
303 }
304 }
305 }
306}