这个系列的博客是使用 Rust 来实现 《Ray Tracing in One Weekend》相关的内容,我们把整本书拆开,来分篇实现,这们可以进一步降低难度。PPM是一个简单的图片格式,它将RGB的颜色使用 ASCII 的形式记录在文件中。

这一小节的目标是使用Rust输出一张PPM的图片,最终效果图如下。

首先使用 cargo new rtiow-rs 创建一个名为 rtiow-rs 的工程,然后在 src 目录中新简一个名为 vec3.rs 的文件,并且输入下面的代码。

vec3.rs 主要是对 Vector3 三维向量的抽象,以及一些操作符的重载。

// src/vec3.rs
use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign};

#[derive(Debug, Copy, Clone)]
pub struct Vec3 {
    pub x: f64,
    pub y: f64,
    pub z: f64,
}

impl Vec3 {
    pub fn zero() -> Self {
        Self {
            x: 0.0,
            y: 0.0,
            z: 0.0,
        }
    }

    pub fn new(x: f64, y: f64, z: f64) -> Self {
        Self { x, y, z }
    }

    pub fn length_squared(&self) -> f64 {
        self.x * self.x + self.y * self.y + self.z * self.z
    }

    pub fn length(&self) -> f64 {
        self.length_squared().sqrt()
    }

    fn to_u64(&self) -> (u64, u64, u64) {
        let x = (self.x * 255.999) as u64;
        let y = (self.y * 255.999) as u64;
        let z = (self.z * 255.999) as u64;
        (x, y, z)
    }

    pub fn get_color_string(&self) -> String {
        let xyz = self.to_u64();
        format!("{} {} {}\n", xyz.0, xyz.1, xyz.2)
    }

    pub fn dot(u: Vec3, v: Vec3) -> f64 {
        u.x * v.x + u.y * v.y + u.z * v.z
    }

    pub fn cross(u: Vec3, v: Vec3) -> Self {
        Self {
            x: u.y * v.z - u.z * v.y,
            y: u.z * v.x - u.x * v.z,
            z: u.x * v.y - u.y * v.x,
        }
    }

    pub fn unit_vector(v: Self) -> Self {
        v / v.length()
    }
}

impl Neg for Vec3 {
    type Output = Self;
    fn neg(self) -> Self::Output {
        Self {
            x: -self.x,
            y: -self.y,
            z: -self.z,
        }
    }
}

impl Add for Vec3 {
    type Output = Self;
    fn add(self, other: Self) -> Self {
        Self {
            x: self.x + other.x,
            y: self.y + other.y,
            z: self.z + other.z,
        }
    }
}

impl AddAssign for Vec3 {
    fn add_assign(&mut self, other: Self) {
        *self = Self {
            x: self.x + other.x,
            y: self.y + other.y,
            z: self.z + other.z,
        }
    }
}

impl Sub for Vec3 {
    type Output = Self;
    fn sub(self, other: Self) -> Self::Output {
        Self {
            x: self.x - other.x,
            y: self.y - other.y,
            z: self.z - other.z,
        }
    }
}

impl SubAssign for Vec3 {
    fn sub_assign(&mut self, other: Self) {
        *self = Self {
            x: self.x - other.x,
            y: self.y - other.y,
            z: self.z - other.z,
        }
    }
}

impl Mul<f64> for Vec3 {
    type Output = Self;
    fn mul(self, rhs: f64) -> Self {
        Self {
            x: self.x * rhs,
            y: self.y * rhs,
            z: self.z * rhs,
        }
    }
}

impl Mul for Vec3 {
    type Output = Self;
    fn mul(self, rhs: Self) -> Self {
        Self {
            x: self.x * rhs.x,
            y: self.y * rhs.y,
            z: self.z * rhs.z,
        }
    }
}

impl MulAssign<f64> for Vec3 {
    fn mul_assign(&mut self, rhs: f64) {
        *self = Self {
            x: self.x * rhs,
            y: self.y * rhs,
            z: self.z * rhs,
        };
    }
}

impl DivAssign<f64> for Vec3 {
    fn div_assign(&mut self, rhs: f64) {
        *self = Self {
            x: self.x / rhs,
            y: self.y / rhs,
            z: self.z / rhs,
        };
    }
}

impl Div<f64> for Vec3 {
    type Output = Self;
    fn div(self, rhs: f64) -> Self {
        Self {
            x: self.x / rhs,
            y: self.y / rhs,
            z: self.z / rhs,
        }
    }
}

第二步就是在 main.rs 里写 PPM 的输出代码,代码如下

main.rs中首先是引入上面创建 vec3 的模块文件,然后使用里面的 Vec3 这个结构体。

// src/main.rs
#![allow(dead_code)]
mod vec3;
use vec3::Vec3;

fn main() {
    // 定义输出文件的宽度和高度
    const IMAGE_WIDTH: u32 = 200;
    const IMAGE_HEIGHT: u32 = 100;

    let mut content_string = String::new();
    content_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 r = (i as f64) / IMAGE_WIDTH as f64;
            let g = (j as f64) / IMAGE_HEIGHT as f64;
            let b: f64 = 0.2;
            let color = Vec3::new(r, g, b);

            content_string.push_str(&color.get_color_string());
        }
    }
    println!("{}", content_string);
}

输出文件使用命令 cargo run > image.ppm 即可在项目目录看到输出的ppm格式的图片。