正交相机所看到的东西大小,与远近无关,只与正交相机的视野(FOV)有关。FOV越大,能看到的世界范围就大,也就是能看到更多的东西,而FOV越小,能看到的世界范围就越小,也就是只能看到较少的东西。
由于FOV越小,看到的范围就越小,从而,相机的上下界,所发出的射线,所能覆盖的范围,就小。也就是相当于所有的射线,都集中在世界中一个小范围,从这个小范围中取得颜色,填充画布(最后渲染的图片),所以看到的东西就大。而如果FOV很大,射线所能覆盖的世界范围就大,用这个大范围来填充画布,自然同一个物体就会看起来小。
可以想象一个两个盒子,一个大的,假设口径是50厘米,扣在一把键盘上,可以扣住整个键盘,相当于摄像机看到了整个键盘。而将一个1厘米口径的盒子,扣在键盘上,可能只能覆盖其中一个键,也就是相机只能看到这一个键范围的东西。但最后都会将扣到的东西填充到画布上,所以,就相当于FOV越小,看到的东西就越大。
添加相机FOV逻辑
在本文中,相机的FOV,我们使用角度来表示,上图中,$\theta$ 就是相机的开口大小,而 $h = \tan(\frac{\theta}{2})$,$2 * h$就是视口的高度,而视口的宽度,通过自定义的宽高比,来动态计算出来。
下面修改 camera.rs 的代码,添加 fov 和 宽高比。
// src/camera.rs
impl Camera {
pub fn new(vfov: f64, aspect_ratio: f64) -> Self {
let theta = vfov * std::f64::consts::PI / 180.0;
let h = (theta / 2.0).tan();
let viewport_height = 2.0 * h;
let viewport_width = aspect_ratio * viewport_height;
let focal_length = 1.0;
let origin = Vec3::zero();
let horizontal = Vec3::new(viewport_width, 0.0, 0.0);
let vertical = Vec3::new(0.0, viewport_height, 0.0);
let lower_left_corner =
origin - horizontal / 2.0 - vertical / 2.0 - Vec3::new(0.0, 0.0, focal_length);
Self {
origin,
lower_left_corner,
horizontal,
vertical,
}
}
pub fn get_ray(&self, u: f64, v: f64) -> Ray {
let direction =
self.lower_left_corner + u * self.horizontal + v * self.vertical - self.origin;
Ray::new(self.origin, direction)
}
}
然后修改 main 函数,来测试一下上面的修改是否正确,将原来的材质定义,和向 world 中添加物体的代码先注释掉,将下面的代码添加到 main 函数里。
// src/main.rs
let R: f64 = (std::f64::consts::PI / 4.0).cos();
let material_left = Lambertian::new(Color::new(0.0, 0.0, 1.0));
let material_right = Lambertian::new(Color::new(1.0, 0.0, 0.0));
world.add(Box::new(Sphere::new(
Vec3::new(-R, 0.0, -1.0),
R,
material_left,
)));
world.add(Box::new(Sphere::new(
Vec3::new(R, 0.0, -1.0),
R,
material_right,
)));
// Camera config
let cam = Camera::new(90.0, 16.0 / 9.0);
上面的代码将相机的 fov 设置为了 90 度,然后宽高比设置为 16:9
使用 cargo run --release > wide_angle_view.ppm
将生成下面的图
自定义相机的位置,角度
为了能够从世界的任意位置,看上世界的任意位置,也就是将相机放在某一个位置,来看世界上的某一个位置,我们还需要将这些参数改成自定义。之前的代码,相机的位置恒定在 (0,0,0)。
还是修改 Camera 的实现
// src/camera.rs
impl Camera {
pub fn new(lookfrom: Vec3, lookat: Vec3, vup: Vec3, vfov: f64, aspect_ratio: f64) -> Self {
let theta = vfov * std::f64::consts::PI / 180.0;
let h = (theta / 2.0).tan();
let viewport_height = 2.0 * h;
let viewport_width = aspect_ratio * viewport_height;
let w = Vec3::unit_vector(lookfrom - lookat);
let u = Vec3::unit_vector(Vec3::cross(vup, w));
let v = Vec3::cross(w, u);
let origin = lookfrom;
let horizontal = viewport_width * u;
let vertical = viewport_height * v;
let lower_left_corner = origin - horizontal / 2.0 - vertical / 2.0 - w;
Self {
origin,
lower_left_corner,
horizontal,
vertical,
}
}
pub fn get_ray(&self, s: f64, t: f64) -> Ray {
let direction =
self.lower_left_corner + s * self.horizontal + t * self.vertical - self.origin;
Ray::new(self.origin, direction)
}
}
上面的代码中,lookfrom
就是相机的位置,而 lookat
就是相机看向的位置点,通过这两个点,可以计算出相机的方向。(0,1,0)为世界坐标下的向上。
原来的相机是正向的,没有任何旋转,所以原来代码中的 horizontal 和 vertical 所在的面,是正右正上的,也就是世界坐标中的 (1,0,0)和(0,1,0)方向。
而现在相机有了旋转,它的裁面,就不是原来垂直于世界坐标的 Z 轴方向,而是垂直于代码中 lookat - lookfrom
的向量。上面的代码中,通过向量的叉乘,得出新的载面向上和向右的方向,也就是代码中的 u 和 v。
修改 main 函数中的代码,首先将材质创建和向 world 添加物体的代码恢复到之前。然后修改相机的创建代码。这里使用了 FOV 为 90 度。
// src/main.rs
let cam = Camera::new(
Vec3::new(-2.0, 2.0, 1.0),
Vec3::new(0.0, 0.0, -1.0),
Vec3::new(0.0, 1.0, 0.0),
90.0,
16.0 / 9.0,
);
使用 cargo run --release > a_distant_view.ppm
会生成下面的图
将上面的 FOV 从 90.0 修改为 20.0,可以得到一个拉进的效果,修改后
cargo run --release > zooming_in.ppm
会生成下面的图