将上一节生成的图放大来看,可以看到球体的边缘与存在着很明显的锯齿,这一节我们首先将相机的代码进行抽象,写在一个 Camera 结构体里,然后加入抗锯齿的逻辑。

最终生成图如下

放大这次与上次生成的图,边缘对比图如下(左边为上次的图,右边为这次的图)

抗锯齿有各种各样方式,简单来说就是一个像素与周围像素的融合过渡。我们这里采用的抗锯齿的方式,就是多次采样,一个像素的颜色,通过随机叠加它周围的像素颜色,然后取平均,作为最终颜色。

这次的逻辑中使用到了一个外部的随机库 rand,所以先将 rand 库加入到工程中,使用命令 cargo add rand 来添加即可。

颜色的结构体 Color,本质上就是一个 Vec3,只不过我们给 Vec3 起了一个别名叫 Color 而已。因为这次要加入抗锯齿的像素颜色计算,所以我们将 Color 的逻辑放到一个单独的文件中。新建一个文件 color.rs,代码如下

Vec3 中的 get_color_string 函数可以直接删除。这里新的 get_color_string 中的参数 pixel_color 就是一个像素与周围像素叠加后的颜色,而 samples_per_pixel 是随机采样的次数,也就是这个像素颜色随机叠加了多少个周围颜色,然后在函数中取平均,计算出最终的 RGB 颜色值。

clamp 函数是将一个值限定在一个范围内。

// src/color.rs
use crate::vec3::Vec3;

pub type Color = Vec3;

pub fn get_color_string(pixel_color: Color, samples_per_pixel: u64) -> String {
    let mut r = pixel_color.x;
    let mut g = pixel_color.y;
    let mut b = pixel_color.z;

    let scale = 1.0 / samples_per_pixel as f64;
    r *= scale;
    g *= scale;
    b *= scale;

    let r = (256.0 * r.clamp(0.0, 0.999)) as u64;
    let g = (256.0 * g.clamp(0.0, 0.999)) as u64;
    let b = (256.0 * b.clamp(0.0, 0.999)) as u64;

    format!("{} {} {}\n", r, g, b)
}

接下来添加相机的逻辑,其实就是将原来 main 函数里相机的逻辑放到一个单独 Camera 结构体里。新建一个名为 camera.rs 的文件,代码如下

// src/camera.rs
use crate::ray::Ray;
use crate::vec3::Vec3;

pub struct Camera {
    origin: Vec3,
    lower_left_corner: Vec3,
    horizontal: Vec3,
    vertical: Vec3,
}

impl Camera {
    pub fn new() -> Self {
        let aspect_ratio = 16.0 / 9.0;
        let viewport_height = 2.0;
        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 函数的渲染部分

// src/main.rs
#![allow(dead_code)]
mod camera;
mod color;
mod hittable;
mod hittable_list;
mod ray;
mod sphere;
mod vec3;
use camera::Camera;
use color::Color;
use hittable::Hittable;
use hittable_list::HittableList;
use rand::Rng;
use ray::Ray;
use sphere::Sphere;
use vec3::Vec3;

fn ray_color(r: Ray, world: &dyn Hittable) -> Color {
    if let Some(hit_record) = world.hit(r, 0.0, f64::INFINITY) {
        return 0.5 * (hit_record.normal + Color::one());
    }

    let unit_direction = Vec3::unit_vector(r.direction);
    let t = 0.5 * (unit_direction.y + 1.0);
    return (1.0 - t) * Color::one() + t * Color::new(0.5, 0.7, 1.0);
}

fn main() {
    // Image config
    const ASPECT_RATIO: f64 = 16.0 / 9.0;
    const IMAGE_WIDTH: u64 = 400;
    const IMAGE_HEIGHT: u64 = ((IMAGE_WIDTH as f64) / ASPECT_RATIO) as u64;
    const SAMPLES_PER_PIXEL: u64 = 100;

    // World
    let mut world = HittableList::new();
    world.add(Box::new(Sphere::new(Vec3::new(0.0, 0.0, -1.0), 0.5)));
    world.add(Box::new(Sphere::new(Vec3::new(0.0, -100.5, -1.0), 100.0)));

    // Camera config
    let cam = Camera::new();

    // Render
    let mut rng = rand::thread_rng();
    let mut image_file_string = String::new();
    image_file_string.push_str(&format!("P3\n{} {}\n255\n", IMAGE_WIDTH, IMAGE_HEIGHT));
    for j in (0..=IMAGE_HEIGHT - 1).rev() {
        for i in 0..IMAGE_WIDTH {
            let mut pixel_color = Color::zero();
            for _ in 0..SAMPLES_PER_PIXEL {
                let u_rand: f64 = rng.gen();
                let v_rand: f64 = rng.gen();
                let u = (i as f64 + u_rand) / (IMAGE_WIDTH - 1) as f64;
                let v = (j as f64 + v_rand) / (IMAGE_HEIGHT - 1) as f64;
                let r = cam.get_ray(u, v);
                pixel_color += ray_color(r, &world);
            }

            image_file_string.push_str(&format!(
                "{}",
                color::get_color_string(pixel_color, SAMPLES_PER_PIXEL)
            ));
        }
    }

    println!("{}", image_file_string);
}

最后,运行 cargo run --release > world_antialiasing.ppm。这里使用了 release 模式,因为增加了多次采样,所以 debug 模式慢很多,要几十秒才能生成完图片,所以这里直接使用了 release 模式。

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