正交相机所看到的东西大小,与远近无关,只与正交相机的视野(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 会生成下面的图

完整代码:https://github.com/moeif/rtiow-rs/tree/12