0%

Github Repo


抗锯齿

生成随机数

首先,需要实现一个随机数生成器生成 ${0 \le r < 1}$ 的随机实数,Rust 的 std 里没有可以实现 random 的方法,只能去找其他 crate 实现。在[Cargo.toml]写入

[dependencies]
rand = "0.7"

因为 Rust 既不支持函数重载,也不支持默认实参,所以只能定义实参类型为 Option

[rtweekend.rs]
1
2
3
4
5
6
7
8
9
use rand::{thread_rng, Rng};

...

pub fn random_double(min: Option<f32>, max: Option<f32>) -> f32 {
let mut rng = thread_rng();
let n: f32 = rng.gen_range(min.unwrap_or(0.0), min.unwrap_or(1.0));
n
}

使用多个样本生成像素

对于给定的像素,在该像素内设置几个样本点,发送射线穿过各个样本点。然后平均这些射线的颜色。

创建一个Camera类对虚拟摄像机和与之相关的场景采样进行管理,以下是相机的简单实现

[camera.rs]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
use crate::rayh::*;
use ray::*;

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

impl Camera {
pub fn new() -> Camera {
let aspect_ratio = 16.0 / 9.0;
let viewport_height = 2.0;
let viewport_width = aspect_ratio * viewport_height;
let focal_length = 1.0;
Camera {
origin: Vec3(0.0, 0.0, 0.0),
horizontal: Vec3(viewport_width, 0.0, 0.0),
vertical: Vec3(0.0, viewport_height, 0.0),
lower_left_corner: Vec3(0.0, 0.0, 0.0)
- Vec3(viewport_width, 0.0, 0.0) / 2.0
- Vec3(0.0, viewport_height, 0.0) / 2.0
- Vec3(0.0, 0.0, focal_length),
}
}

pub fn get_ray(&self, u: f32, v: f32) -> Ray {
Ray {
orig: self.origin,
dir: self.lower_left_corner + u * self.horizontal + v * self.vertical - self.origin,
}
}
}

rtweekend.h中添加clamp(x,min,max)使得输出值在[min,max]范围内

[rtweekend.rs]
1
2
3
4
5
6
7
8
9
10
...

pub fn clamp(x: f32, min: f32, max: f32) -> f32 {
if x < min {
return min;
}
if x > max {
return max;
}
x

重写color.rs内的write_color()以及main.rs中的main()

[color.rs]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
use crate::rtweekend::*;
use ray::Color;

pub fn write_color(pixel_color: Color, samples_per_pixel: u16) -> 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 f32;
r *= scale;
g *= scale;
b *= scale;
unsafe {
format!(
"{} {} {}\n",
(256.0 * clamp(r, 0.0, 0.999)).to_int_unchecked::<u16>(),
(256.0 * clamp(g, 0.0, 0.999)).to_int_unchecked::<u16>(),
(256.0 * clamp(b, 0.0, 0.999)).to_int_unchecked::<u16>(),
)
}
}

[main.rs]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
mod camera;
use camera::*;

...

fn main() {
let IMAGE_HEIGHT = unsafe { (IMAGE_WIDTH as f32 / ASPECT_RATIO).to_int_unchecked::<u16>() };

let mut world = Hittable_list {
objects: Vec::new(),
};
world.add(Sphere {
center: Vec3(0.0, 0.0, -1.0),
radius: 0.5,
});
world.add(Sphere {
center: Vec3(0.0, -100.5, -1.0),
radius: 100.0,
});

let cam = Camera::new();

println! {"P3\n{} {}\n255",IMAGE_WIDTH,IMAGE_HEIGHT};
for j in (0..IMAGE_HEIGHT).rev() {
eprintln!("\rScanlines remaining:{}", j);
for i in 0..IMAGE_WIDTH {
let mut pixel_color = Vec3(0.0, 0.0, 0.0);
for s in 0..SAMPLES_PER_PIXEL {
let u = (i as f32 + random_double(None, None)) / (IMAGE_WIDTH - 1) as f32;
let v = (j as f32 + random_double(None, None)) / (IMAGE_HEIGHT - 1) as f32;
let r = cam.get_ray(u, v);
pixel_color += ray_color(&r, &world);
}
print!("{}", write_color(pixel_color, SAMPLES_PER_PIXEL));
}
}
eprintln!("\nDone");
}

运行过后的效果图

Github Repo


如果坐标系内有多个球体会怎么样?无论是一个球或是多个球,将射线可能射到的物体化为一个抽象类 Hittable,但是由于 Rust 没有继承的概念,所以将其定义为 trait.Hittable trait 有一个 hit 功能用来吸收射线。同时给 t 设置一个区间,当 $t_{min} < t < t_{max}$才算相交。

[hittable.rs] 定义 Hittable trait
1
2
3
4
5
6
7
8
9
10
11
12
13
14
use crate::rayh::Ray;
use ray::Point3;
use ray::Vec3;

#[derive(Debug, PartialEq, Clone, Copy)]
pub struct Hit_record {
pub p: Point3,
pub normal: Vec3,
pub t: f32,
}

pub trait Hittable {
fn hit(&self, r: &Ray, t_min: f32, t_max: f32, rec: &mut Hit_record) -> bool;
}

然后是定义 sphere 类

[hittable.rs] 定义 Hittable trait
1
2
3
4
5
6
7
8
9
10
11
12
13
14
use crate::rayh::Ray;
use ray::Point3;
use ray::Vec3;

#[derive(Debug, PartialEq, Clone, Copy)]
pub struct Hit_record {
pub p: Point3,
pub normal: Vec3,
pub t: f32,
}

pub trait Hittable {
fn hit(&self, r: &Ray, t_min: f32, t_max: f32, rec: &mut Hit_record) -> bool;
}

对射线同样要进行分情况讨论,定义法线方向永远朝外,若是在球外与球相交的射线,法线方向与射线方向相反,射线若是在球内相交,法线方向与射线方向相同;又或者,可以定义法线永远指向射线的反方向,射线在球内,法线方向向内,射线在球外,法线方向向外。
如图所示:

如果我们定义法线方向永远朝外,则对射线着色时需确定射线在哪一侧,这可以通过比较射线与法线所得,如果射线与法线方向相同,则射线在物体内部,若相反则在物体外部。这可以根据两向量的点乘看出,点乘为正则方向相同。

1
2
3
4
5
6
7
if dot(ray_direction, outward_normal) > 0.0{
// 射线在物体内部
...
} else{
// 射线在物体外部
...
}

相对的,如果我们定义法线方向与射线方向永远相反,则无法通过点乘确定射线位于表面的哪一侧,因此,我们需要储存这个信息。

1
2
3
4
5
6
7
8
9
10
front_face: bool
if dot(ray_direction, outward_normal) > 0.0{
// 射线在物体内部
normal = -outward_normal;
front_face = false;
} else{
// 射线在物体外部
normal = outward_normal;
front_face = true;
}

定义法线方向永远朝外或是法线方向永远与射线方向相反,取决于是要在几何相交时还是在着色时确定表面的侧面。演示中含有的材料类型比集合类型多,所以将重点放在几何相交上。
给 Hit_record 加上 front_face 以储存方向信息,给 Hittable trait 增加 set_face_normal() 方法

% [hittable.rs]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#[derive(Debug, PartialEq, Clone, Copy)]
pub struct Hit_record {
pub p: Point3,
pub normal: Vec3,
pub t: f32,
pub front_face: bool,
}

...

impl Hit_record {
pub fn set_face_normal(&mut self, r: &Ray, outward_normal: Vec3) {
self.front_face = dot(r.direction(), outward_normal) < 0.0;
self.normal = if self.front_face {
outward_normal
} else {
-outward_normal
};
}
}

再将 set_face_normal() 用于 Sphere.hit() 中

% [sphere.rs]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
...
impl Hittable for Sphere {
fn hit(&self, r: &Ray, t_min: f32, t_max: f32, rec: &mut Hit_record) -> bool {
let oc: Vec3 = r.origin() - self.center;
let a = r.direction().length_squared();
let half_b = dot(oc, r.direction());
let c = oc.length_squared() - self.radius * self.radius;
let discriminant = half_b * half_b - a * c;
if discriminant < 0.0 {
let root = discriminant.sqrt();

let temp = (-half_b - root) / a;
if temp < t_max && temp > t_min {
rec.t = temp;
rec.p = r.at(rec.t);
let outward_normal = (rec.p - self.center) / self.radius;
rec.set_face_normal(&r, outward_normal);
return true;
}

let temp = (-half_b - root) / a;
if temp < t_max && temp > t_min {
rec.t = temp;
rec.p = r.at(rec.t);
let outward_normal = (rec.p - self.center) / self.radius;
rec.set_face_normal(&r, outward_normal);
return true;
}
}
false
}
}

定义一个类来储存一堆有 Hittable trait 的泛型数据类型,同时也要为其定义 Hittable trait

% [hittable_list.rs]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
use crate::hittable::*;
use crate::rayh::*;
use ray::*;

pub struct Hittable_list<T> {
pub objects: Vec<T>,
}

impl<T: Hittable> Hittable_list<T> {
pub fn new(object: T) -> Hittable_list<T> {
Hittable_list {
objects: vec![object],
}
}
pub fn clear(&mut self) {
(*self).objects.truncate(0);
}
pub fn add(&mut self, object: T) {
(*self).objects.push(object);
}
}
impl<T: Hittable> Hittable for Hittable_list<T> {
fn hit(&self, r: &Ray, t_min: f32, t_max: f32, rec: &mut Hit_record) -> bool {
let mut temp_rec = Hit_record {
p: Vec3(0.0, 0.0, 0.0),
normal: Vec3(0.0, 0.0, 0.0),
t: 0.0,
front_face: true,
};
let mut hit_anything = false;
let mut closest_so_far = t_max;
let mut ar_temp_rec = temp_rec;

for object in &self.objects {
if object.hit(r, t_min, closest_so_far, &mut temp_rec) {
hit_anything = true;
closest_so_far = temp_rec.t;
ar_temp_rec = temp_rec;
}
}
*rec = ar_temp_rec;
hit_anything
}
}

定义常用数学常量

% [rtweekend.rs]
1
2
3
4
5
6
pub const infinity: f32 = f32::INFINITY;
pub const PI: f32 = 3.14159274f32;

pub fn degrees_to_radians(degrees: f32) -> f32 {
degrees * PI / 180.0
}

更改 main.rs

% [main.rs]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
mod color;
mod hittable;
mod hittable_list;
mod rayh;
mod rtweekend;
mod sphere;
use color::*;
use hittable::*;
use hittable_list::*;
use ray::*;
use rayh::*;
use rtweekend::*;
use sphere::*;

const IMAGE_WIDTH: u16 = 400;
const ASPECT_RATIO: f32 = 16.0 / 9.0;

fn ray_color<T: Hittable>(r: &Ray, world: &T) -> Color {
let mut rec = Hit_record {
p: Vec3(0.0, 0.0, 0.0),
normal: Vec3(0.0, 0.0, 0.0),
t: 0.0,
front_face: true,
};
if world.hit(r, 0.0, infinity, &mut rec) {
return 0.5 * (rec.normal + Vec3(1.0, 1.0, 1.0));
}
let unit_direction = unit_vector(r.direction());
let t = 0.5 * (unit_direction.y() + 1.0);
(1.0 - t) * Vec3(1.0, 1.0, 1.0) + t * Vec3(0.5, 0.7, 1.0)
}
fn main() {
let IMAGE_HEIGHT = unsafe { (IMAGE_WIDTH as f32 / ASPECT_RATIO).to_int_unchecked::<u16>() };

let mut world = Hittable_list {
objects: Vec::new(),
};
world.add(Sphere {
center: Vec3(0.0, 0.0, -1.0),
radius: 0.5,
});
world.add(Sphere {
center: Vec3(0.0, -100.5, -1.0),
radius: 100.0,
});

println! {"P3\n{} {}\n255",IMAGE_WIDTH,IMAGE_HEIGHT};
let viewport_height = 2.0;
let viewport_width: f32 = ASPECT_RATIO * viewport_height;
let focal_length = 1.0;

let origin: Point3 = Vec3(0.0, 0.0, 0.0);
let horizontal = Vec3(viewport_width, 0.0, 0.0);
let vertical = Vec3(0.0, viewport_height, 0.0);
let lower_left_corner =
origin - horizontal / 2 as f32 - vertical / 2 as f32 - Vec3(0.0, 0.0, focal_length);
for j in (0..IMAGE_HEIGHT).rev() {
eprintln!("\rScanlines remaining:{}", j);
for i in 0..IMAGE_WIDTH {
let u = i as f32 / (IMAGE_WIDTH - 1) as f32;
let v = j as f32 / (IMAGE_HEIGHT - 1) as f32;
let r = Ray {
orig: origin,
dir: lower_left_corner + u * horizontal + v * vertical - origin,
};
let pixel_color = ray_color(&r, &world);
print!("{}", write_color(pixel_color));
}
}
eprintln!("\nDone");
}

最后得出的图片如下图所示

Github Repo


接下来是球面着色,第一步,确定球面法线,法线是球面上点垂直于球面的向量。对于球 C 与 球面上点 P,P 点向外法线方向与$\boldsymbol {CP} $方向一致,如下图所示

因为目前没有光线,所以用颜色映射来可视化法线,将各分量映射至(0,1)的区间(即求其单位向量 $\vec{n}$),然后将 x/y/z 映射至 r/g/b,对于法线来说,我们要求的是交点,而不是相交与否。假定与原点最近的交点(即最小的 t),在 main.rs 做出如下更改使得程序可以计算并可视化 $\vec{n}$.

[main.rs]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fn hit_sphere(center: Point3, radius: f32, r: &Ray) -> f32 {
let oc: Vec3 = r.origin() - center;
let a = dot(r.direction(),r.direction());
let b = 2.0 * dot(oc, r.direction());
let c = dot(oc, oc) - radius * radius;
let discriminant = b*b - 4.0 * a * c;
if discriminant < 0.0{
return -1.0;
}else{
return (-b - discriminant.sqrt() ) / (2.0 * a);
}
}
fn ray_color(r: &Ray) -> Color {
let mut t = hit_sphere(Vec3(0.0, 0.0, -1.0), 0.5, r);
if t > 0.0{
let N: Vec3 = unit_vector(r.at(t) - Vec3(0.0, 0.0, -1.0));
return 0.5 * Vec3(N.x() + 1.0, N.y() + 1.0, N.z() + 1.0);
}
let unit_direction = unit_vector(r.direction());
t = 0.5 * (unit_direction.y() + 1.0);
(1.0 - t) * Vec3(1.0, 1.0, 1.0) + t * Vec3(0.5, 0.7, 1.0)
}

运行得出下图:


对现有代码进行数学层面的简化,重新审视射线与球面相交的代码

[main.rs] 先前代码
1
2
3
4
5
6
7
8
9
10
11
12
fn hit_sphere(center: Point3, radius: f32, r: &Ray) -> f32 {
let oc: Vec3 = r.origin() - center;
let a = dot(r.direction(),r.direction());
let b = 2.0 * dot(oc, r.direction());
let c = dot(oc, oc) - radius * radius;
let discriminant = b*b - 4.0 * a * c;
if discriminant < 0.0{
return -1.0;
}else{
return (-b - discriminant.sqrt() ) / (2.0 * a);
}
}

首先,一个向量点乘自身等于这个向量的长度。其次,对于一元二次方程的求根公式,不难发现 b 的定义里有一个乘 2,假设 $ b=2h $,则求根公式可以做以下变形:
$$
\frac {-b \pm \sqrt {b^2-4ac}}{2a}
$$

$$
=\frac {-2h \pm \sqrt {(2h)^2-4ac}}{2a}
$$

$$
=\frac {-2h \pm 2 \sqrt {h^2-ac}}{2a}
$$

$$
=\frac {-h \pm \sqrt {h^2-ac}}{a}
$$

因此我们可以简化射线与球相交的代码

[main.rs] 简化之后代码
1
2
3
4
5
6
7
8
9
10
11
12
fn hit_sphere(center: Point3, radius: f32, r: &Ray) -> f32 {
let oc: Vec3 = r.origin() - center;
let a = r.direction().length_squared();
let half_b = dot(oc, r.direction());
let c = oc.length_squared() - radius * radius;
let discriminant = half_b * half_b - a * c;
if discriminant < 0.0{
return -1.0;
}else{
return (-half_b - discriminant.sqrt() ) / a;
}
}

Github Repo


接下来是在原有图像上增加球体。一个球心为原点且半径为 R 的球,球上点的坐标$(x,y,z)$满足$x^2+y^2+z^2 = R^2$,球内点坐标满足$x^2+y^2+z^2 < R^2$,球外点坐标满足$x^2+y^2+z^2 > R^2$,如果球心C为$(C_x,C_y,C_z)$的话,则球体用$(x-C_x)^2+(y-C_y)^2+(z-C_z)^2 = R^2$表示,设某点 P 坐标$(x,y,z)$,则$\boldsymbol{P-C} = (x-C_x,y-C_y,z-C_z)$,不难看出$$(\boldsymbol{P-C}) \cdot (\boldsymbol{P-C})=(x-C_x)^2+(y-C_y)^2+(z-C_z)^2$$
所以用向量表示球体等式为
$$(\boldsymbol{P-C}) \cdot (\boldsymbol{P-C})=R^2$$
任何满足等式的点 P 都在球面上,更进一步,对于射线 $\boldsymbol P(t) =\boldsymbol A + t\boldsymbol b$(即 vol3 提到的 orig + dir*t),如果 P(t) 与球有交点,则
$$(\boldsymbol P(t)-\boldsymbol C)(\boldsymbol P(t)-\boldsymbol C) = R^2$$

$$(\boldsymbol A + t\boldsymbol b)(\boldsymbol A + t\boldsymbol b) = R^2$$
将等式以 $t$ 为因变量展开
$$
t^2\boldsymbol b \cdot \boldsymbol b
+2t \boldsymbol b \cdot (\boldsymbol A - \boldsymbol C)
+(\boldsymbol A - \boldsymbol C)(\boldsymbol A - \boldsymbol C)
-R^2
=0
$$
这是关于 $t$ 的一元二次方程,根据$\boldsymbol A,\boldsymbol b,\boldsymbol C,R$的值会有零个实根(无交点)、一个实根(一个交点)、两个实根(两个交点)三种情况

我画的示意图稍微与原版有出入,原版看上去像是在 $\boldsymbol b$ 不动的情况下移动 $\boldsymbol A$,但是我认为保持起点 $\boldsymbol A$ 不动更加有利于接下来的理解。
根据上文所述,对 main.rs 进行更改,增加一个 hit_sphere() 方法,在 ray_color() 中修改返回值。

[main.rs]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
fn hit_sphere(center: Point3, radius: f32, r: &Ray) -> bool {
let oc: Vec3 = r.origin() - center;
let a = dot(r.direction(), r.direction());
let b = 2.0 * dot(oc, r.direction());
let c = dot(oc, oc) - radius * radius;
let discriminant = b*b - 4.0 * a * c; //二次方程解的情况讨论
return discriminant > 0.0;
}
fn ray_color(r: &Ray) -> Color {
if hit_sphere(Vec3(0.0,0.0,-1.0), 0.5, r){
return Vec3(1.0, 0.0, 0.0);
}
let unit_direction = unit_vector(r.direction());
let t = 0.5 * (unit_direction.y() + 1.0);
(1.0 - t) * Vec3(1.0, 1.0, 1.0) + t * Vec3(0.5, 0.7, 1.0)
}


图像上的圆被红色(255,0,0)覆盖,像是反射或者光影都没有,而且还有一些问题,当圆心为(0,0,1)时,程序输出的结果与上图一致,但是现实中并非如此,在观察点观察方向相反方向的物体是不会呈现在画面上的。

Github Repo


定义一个 Ray 结构体表示射线,分为两个字段,orig 表示射线的起点,dir 表示射线的方向,定义一个方法at(t),输出 orig + dir*t,使其能够表示由原点与方向确定的唯一直线上的所有点。

[rayh.rs] 定义 Ray 结构体
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
use ray::Color;
use ray::Point3;
use ray::Vec3;

pub struct Ray {
pub orig: Point3,
pub dir: Vec3,
}

impl Default for Ray {
fn default() -> Self {
Ray {
orig: Default::default(),
dir: Default::default(),
}
}
}

impl Ray {
pub fn new(origin: &Point3, direction: &Vec3) -> Self {
Ray {
orig: origin.clone(),
dir: direction.clone(),
}
}

pub fn origin(&self) -> Point3 {
self.orig
}

pub fn direction(&self) -> Vec3 {
self.dir
}

pub fn at(&self, t: f32) -> Point3 {
self.orig + t * self.dir
}
}

ray tracer 分为三个步骤,计算从眼睛到像素的射线,确定与射线有交点的物品,计算交点处的颜色。
首先把图像投影在三维空间的一个平面上,这个平面的长宽比和渲染图片相同,投影平面与投影点之间的举例记为一个单位,称为焦长,即下图的 F 所表示。在(0,0,0)处设置观察点,从图像左下角开始,遍历屏幕,并使用沿屏幕两侧的两个偏移矢量在屏幕上移动射线端点。

更改后的 main 代码如下:

[main.rs]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
mod color;
mod rayh;
use color::*;
use ray::*;
use rayh::*;

const IMAGE_WIDTH: u16 = 400;
const ASPECT_RATIO: f32 = 16.0 / 9.0;

fn ray_color(r: &Ray) -> Color {
let unit_direction = unit_vector(r.direction());
let t = 0.5 * (unit_direction.y() + 1.0);
(1.0 - t) * Vec3(1.0, 1.0, 1.0) + t * Vec3(0.5, 0.7, 1.0)
}
fn main() {
let IMAGE_HEIGHT = unsafe {
(IMAGE_WIDTH as f32 / ASPECT_RATIO).to_int_unchecked::<u16>()
}; //unsafe 函数无法使用 const 定义
println! {"P3\n{} {}\n255",IMAGE_WIDTH,IMAGE_HEIGHT};

let viewport_height = 2.0;
let viewport_width: f32 = ASPECT_RATIO * viewport_height;
let focal_length = 1.0;

let origin: Point3 = Vec3(0.0, 0.0, 0.0);
let horizontal = Vec3(viewport_width, 0.0, 0.0);
let vertical = Vec3(0.0, viewport_height, 0.0);
let lower_left_corner =
origin - horizontal / 2 as f32 - vertical / 2 as f32 - Vec3(0.0, 0.0, focal_length);

for j in (0..IMAGE_HEIGHT).rev() {
eprintln!("\rScanlines remaining:{}", j);
for i in 0..IMAGE_WIDTH {
let u = i as f32 / (IMAGE_WIDTH - 1) as f32;
let v = j as f32 / (IMAGE_HEIGHT - 1) as f32;
let r = Ray {
orig: origin,
dir: lower_left_corner + u * horizontal + v * vertical - origin,
};
let pixel_color = ray_color(&r);
print!("{}", write_color(pixel_color));
}
}
eprintln!("\nDone");
}

所得图片如下

ray_color(r) 方法根据 r 的射线方向的 y 坐标确定白色与蓝色的混合程度,y 的值越大,越趋近于蓝色。如果将 main.rs 第十三行改成(1.0 - t) * Vec3(0.2, 0.9, 0.2) + t * Vec3(0.2, 0.0, 1.0)结果如下

Github Repo


定义 Vec3 使用了元组结构体,其中每个字段的类型都为 f32,对 Vec3 进行初始化调用定义, 实现加、减、取反、数乘、复合算数赋值(关于自定义运算符 Trait 可在 https://doc.rust-lang.org 找到)、点乘与叉乘的方法,并且定义了 Vec3 的两个 type alias Color 与 Point3.

[lib.rs] 对 Vec3 结构体进行定义,定义 Vec3 的一些简单方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
use std::ops::{Add,AddAssign,Div,DivAssign,Mul,MulAssign,Neg,Sub};

#[derive(Debug, PartialEq, Clone, Copy)]
pub struct Vec3(pub f32, pub f32, pub f32);

impl Default for Vec3 {
fn default() -> Self {
Vec3(0.0, 0.0, 0.0)
}
} //没有必要

impl Vec3 {
pub fn new(e0: f32, e1: f32, e2: f32) -> Vec3 {
Vec3(e0, e1, e2)
}
pub fn x(&self) -> f32 {
self.0
}
pub fn y(&self) -> f32 {
self.1
}
pub fn z(&self) -> f32 {
self.2
}
pub fn length_squared(&self) -> f32 {
self.0 * self.0 + self.1 * self.1 + self.2 * self.2
}
pub fn length(&self) -> f32 {
self.length_squared().sqrt()
}
}

关于 Vec3 类的构造方式有两种,一种是直接 let v = Vec3(2.0,3.0,4.0);,另一种是 let v = Vec3::new(2.0,3.0,4.0);,我原本在第四行定义的为pub struct Vec3(f32, f32, f32);,这种定义方法会导致在其他 crate 中无法使用第一种构造方法,只能使用第二种。

[lib.rs] 定义 Vec3 的加减取反 Trait
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
impl Neg for Vec3 {
type Output = Self;

fn neg(self) -> Self {
Vec3(-self.0, -self.1, -self.2)
}
}

impl Add for Vec3 {
type Output = Vec3;
fn add(self, other: Self) -> Self {
Vec3(self.0 + other.0, self.1 + other.1, self.2 + other.2)
}
}

impl Sub for Vec3 {
type Output = Vec3;

fn sub(self, other: Self) -> Self {
Vec3(self.0 - other.0, self.1 - other.1, self.2 - other.2)
}
}
[lib.rs] 定义 Vec3 的数乘除 Trait
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
impl Mul for Vec3 {
type Output = Vec3;

fn mul(self, other: Vec3) -> Self {
Vec3(self.0 * other.0, self.1 * other.1, self.2 * other.2)
}
}

impl Mul<f32> for Vec3 {
type Output = Self;

fn mul(self, other: f32) -> Self {
Vec3(self.0 * other, self.1 * other, self.2 * other)
}
}

impl Mul<Vec3> for f32 {
type Output = Vec3;

fn mul(self, other: Vec3) -> Vec3 {
other * self
}
}

impl Div<f32> for Vec3 {
type Output = Self;

fn div(self, other: f32) -> Self {
self * (1.0 / other)
}
}

关于 Vec3 的乘法 Trait,要注意的是需要定义两次,因为乘法左右两边数据结构不同,需要定义f32*Vec3 与 Vec3*f32

[lib.rs] 定义 Vec3 的复合算数赋值 Trait
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
impl AddAssign<Vec3> for Vec3 {
fn add_assign(&mut self, other: Vec3) {
*self = Self(self.0 + other.0, self.1 + other.1, self.2 + other.2);
}
}

impl MulAssign<f32> for Vec3 {
fn mul_assign(&mut self, other: f32) {
*self = Self(self.0 * other, self.1 * other, self.2 * other);
}
}

impl DivAssign<f32> for Vec3 {
fn div_assign(&mut self, other: f32) {
*self *= 1.0 / other;
}
}
[lib.rs] 实现 Vec3 的点乘叉乘和单位向量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pub fn dot(u: Vec3, v: Vec3) -> f32 {
u.0 * v.0 + u.1 * v.1 + u.2 * v.2
}

pub fn cross(u: Vec3, v: Vec3) -> Vec3 {
Vec3(
u.1 * v.2 - u.2 * v.1,
u.2 * v.0 - u.0 * v.2,
u.0 * v.1 - u.1 * v.0,
)
}

pub fn unit_vector(u: Vec3) -> Vec3 {
u / u.length()
}

pub type Point3 = Vec3;
pub type Color = Vec3;
[color.rs] 规范返回值
1
2
3
4
5
6
7
8
9
10
11
12
use ray::Color;

pub fn write_color(pixel_color: Color) -> String {
unsafe {
format!(
"{} {} {}\n",
(255.999 * pixel_color.x()).to_int_unchecked::<u16>(),
(255.999 * pixel_color.y()).to_int_unchecked::<u16>(),
(255.999 * pixel_color.z()).to_int_unchecked::<u16>()
)
}
}
[main.rs] 重构 main 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mod color;
mod rayh;
use color::*;
use ray::Vec3;

const IMAGE_WIDTH: u16 = 256;
const IMAGE_HEIGHT: u16 = 256;

fn main() {
println! {"P3\n{} {}\n255",IMAGE_WIDTH,IMAGE_HEIGHT};
for j in (0..IMAGE_HEIGHT).rev() {
eprintln!("\rScanlines remaining:{}", j);
for i in 0..IMAGE_WIDTH {
let pixel_color = Vec3::new(
i as f32 / (IMAGE_WIDTH - 1) as f32,
j as f32 / (IMAGE_HEIGHT - 1) as f32,
0.25 as f32,
);
print!("{}", write_color(pixel_color));
}
}
eprintln!("\nDone");
}

  前一阵子因为兴趣看了看 Rust 的教程,感觉上手不容易,即使做完了 rustlings 里所有的练习仍感觉云里雾里。偶然在某网站上看到有人推荐这个网站,学习其内容的同时对其中的 C++ 代码使用 Rust 进行重构。


Github Repo


首先,关于作者使用的图片文件格式 ppm

A PPM file consists of two parts, a header and the image data. The header consists of at least three parts normally delineated by carriage returns and/or linefeeds but the PPM specification only requires white space. The first “line” is a magic PPM identifier, it can be “P3” or “P6” . The next line consists of the width and height of the image as ASCII numbers. The last part of the header gives the maximum value of the colour components for the pixels, this allows the format to describe more than single byte (0..255) colour values.


作者在网页中的举例
ppm文件示例
ppm 在线浏览网址
从生成 ppm 图片文件开始,首先 cargo new ray 新建一个名为 ray 的 cargo

[main.rs] 生成一个 ppm 图片
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const IMAGE_WIDTH: u16 = 256;
const IMAGE_HEIGHT: u16 = 256;

fn main() {
println! {"P3\n{} {}\n255",IMAGE_WIDTH,IMAGE_HEIGHT};
for j in (0..IMAGE_HEIGHT).rev() {
eprintln!("\rScanlines remaining:{}", j);
//用于将当前进度输出到标准错误中
for i in (0..IMAGE_WIDTH) {
let r = i as f32 / (IMAGE_WIDTH - 1) as f32;
let g = j as f32 / (IMAGE_HEIGHT - 1) as f32;
let b = 0.25 as f32;
let ir = unsafe { (255.999 * r).to_int_unchecked::<u16>() };
let ig = unsafe { (255.999 * g).to_int_unchecked::<u16>() };
let ib = unsafe { (255.999 * b).to_int_unchecked::<u16>() };
println!("{} {} {}", ir, ig, ib);
//将RGB值输入标准输出中
}
}
eprintln!("\nDone");
}

pub unsafe fn to_int_unchecked<Int>(self) -> Int 是 Rust 1.44.0 的新方法,本机原本装的是 Rust 1.42 ,使用 rustup 对Rust 升级无果,发现是包管理器的锅,当初用的 apt install rustc,而 Ubuntu 的 rustc 最新版只有 1.42,卸载后在官网重新下载安装了最新版的 Rust.
运行上面这段代码 cargo run > first.ppm,标准输出会写入 first.ppm,标准错误会显示在屏幕上,生成的是一个256×256的图片,从第 4 行开始,到第 65539 行结束,每 256 行视为图片的一行
输出效果图


定义一个三维向量结构体Vec3来表示方向、色彩,它有两个 type-alias : Point3,Color(Rust 建议 identifier 首字母大写,以及 can't use a type alias as a constructor)(注:接下来的代码是一大段的关于 Vec3 的运算符方法定义,我原本的想法是使用引用 & 来避免运算中 ownership 的转移,但是这样会导致 &(&(&origin - &(&horizontal / 2 as f32)) - &(&vertical / 2 as f32))- &Vec3(0.0, 0.0, focal_length) 或是 &(&(&lower_left_corner + &(u * &horizontal)) + &(v * &vertical)) - &origin这样可读性与美观都十分差的代码,向其他人询问后发现,可以通过在 Vec3 定义时,在 pub struct Vec3(pub f32, pub f32, pub f32); 上一行插入 #[derive(Debug, PartialEq, Clone, Copy)] 在 Vec3 类调用时进行 Copy 从而不转移 ownership,这意味着我需要重构一遍接下来所有的代码)

普通到不能再普通的大学生,对什么事都充满好奇,都想尝试,但是热情很快就会退却。
ID 是 Putinspudding ,来源是高中拿了一本杂志到学校,封面上印字的是 Putinism(The Economist OCTOBER 22ND-28TH 2016),朋友说读起来好像 Puddingism,就顺势组合了一下。在后来因为政治性避嫌(?)/懒会使用迫真缩写 Pwdding.
Ingress Res L12(AFK游戏打不开了) 有过 Biocard,但是电子模板一时半会找不到了
不会写代码,想当码农
PC 游戏成瘾
Steam 个人界面:https://steamcommunity.com/id/PWdding/