0%

Github Repo


漫反射材质

一种简易的漫反射材质

不发光的漫反射物体仅仅吸收其周遭的颜色,但是其固有颜色进行调和。从漫反射表面反射回来光线的方向是随机的,因此,如果我们将三条射线射向两个物体的间隔缝中,它们会有不同的随机表现。

这些射线也有可能被吸收而不是被反射,物体表面越黑,光线越容易被吸收。实际上,任何将射线的反射方向随机化的算法都会产生看起来粗糙的表面。实现这种算法的最简单的方法之一被发现是对实现理想漫反射表面完全恰当的。

有两个单位球与表面上的交点 P 相切,两个球心分别为 $\boldsymbol{(P+n)}$ 和 $\boldsymbol{(P-n)}$, $\boldsymbol{n}$ 是表面在 P 处的法线,把球心为 $\boldsymbol{(P-n)}$ 的球体看作在表面内部,那么球心为 $\boldsymbol{(P+n)}$ 的球体即在表面外部,选择与射线源点在表面同一侧的单位相切球,随机选取球内任意一点 S,从交点 P 发射一条射线到点 S($\boldsymbol{(S-P)}$).

关于球内随机点的选取方法,使用剔除法,首先,在 x,y,z 都在 -1 到 1 范围内的方块中选取一个随机点,如果不在球内则重新随机选取。

[lib.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
impl Vec3 {

...

pub fn random(min: Option<f32>, max: Option<f32>) -> Vec3 {
Vec3(
random_double(min, max),
random_double(min, max),
random_double(min, max),
)
}

...

}

pub fn random_in_unit_sphere() -> Vec3 {
while true {
let p = Vec3::random(-1, 1);
if p.length_squared() >= 1 {
continue;
}
return p;
}
}

[main.rs]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fn ray_color<T: Hittable>(r: &Ray, world: &T) -> Color {
...

if world.hit(r, 0.0, infinity, &mut rec) {
let target = rec.p + rec.normal + random_in_unit_sphere();
return 0.5
* ray_color(
&Ray{
orig:rec.p,
dir:target - rec.p
},
world,
);
}
...

限制子射线的数量

ray_color 函数是递归的,递归出口是其与任何物体没有交点的时候,但是这可能会画上很长时间,因此需要一个最大递归深度,到达最大深度后不返回光线。

[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
const MAX_DEPTH: u16 = 50;

fn ray_color<T: Hittable>(r: &Ray, world: &T, depth: u16) -> 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 depth <= 0 {
return Vec3(0.0, 0.0, 0.0);
}
if world.hit(r, 0.0, infinity, &mut rec) {
let target = rec.p + rec.normal + random_in_unit_sphere();
return 0.5
* ray_color(
&Ray {
orig: rec.p,
dir: target - rec.p,
},
world,
depth - 1,
);
}

...

}
fn main() {

...

pixel_color += ray_color(&r, &world, MAX_DEPTH);

...

得出图形如下


## 使用 Gamma 校正获得准确的色彩强度 我们会发现图片太暗了,以至于看不清图片的阴影,需要做一些 Gamma 矫正。在此情况下,使用 Gamma 2,意味着将色彩提高 1/Gamma 次幂,即 1/2 次幂,即平方根。
[main.rs]
1
2
3
4
5
6
7
8
9
10
11
12
pub fn write_color(pixel_color: Color, samples_per_pixel: u16) -> String {

...

let scale = 1.0 / samples_per_pixel as f32;
r = (scale * r).sqrt();
g = (scale * g).sqrt();
b = (scale * b).sqrt();

...

}
![经过 Gamma 矫正所得](/img/eleventh.png)
## 修复阴影失真 某些反射光线在不完全在 t = 0 时击中了它们正在反射的对象,而是在t=-0.0000001或t=0.00000001或任何浮点近似值与球面交点相交,所以我们需要忽视十分接近于0的浮点数。
[main.rs]
1
if world.hit(r, 0.001, infinity, &mut rec) {

真实的 Lambertian 反射

这里介绍的剔除法在单位球中产生沿表面法线偏移的随机点。这相当于在半球上选取高概率接近法线的方向,而在掠射角上散射射线的概率较低。这个分布的尺度是$\cos^3(ϕ)$,其中ϕ是与法线的角度。这是十分有用,因为到达浅角的光线会扩散到更大的区域,因此对最终颜色的贡献较低。
然而,我们感兴趣的是 Lambertian 分布,它的分布尺度是$cos(ϕ)$。真正的 Lambertian 反射对于接近于法线的射线散射的概率更高,但分布更均匀。这可以通过在单位球面上选取沿表面法线偏移的点来实现。在球面上选点可以通过在单位球中选点,然后将这些点归一化来实现。

[lib.rs] 实现随机单位向量
1
2
3
4
5
6
pub fn random_unit_vector() -> Vec3 {
let a = random_double(Some(0.0), Some(2.0 * PI));
let z = random_double(Some(-1.0), Some(1.0));
let r = (1.0 - z * z).sqrt();
Vec3(r * a.cos(), r * a.sin(), z)
}

random_unit_vector() 是现有的 random_in_unit_sphere() 函数的替代品。

[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
fn ray_color<T: Hittable>(r: &Ray, world: &T, depth: u16) -> 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 depth <= 0 {
return Vec3(0.0, 0.0, 0.0);
}
if world.hit(r, 0.001, infinity, &mut rec) {
let target = rec.p + rec.normal + random_unit_vector();
return 0.5
* ray_color(
&Ray {
orig: rec.p,
dir: target - rec.p,
},
world,
depth - 1,
);
}

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)
}


因为样例实在是太简单所以难以区分两种漫反射方法的区别。但是可以注意到,变化后的阴影不那么明显;变化后,两个球体的外观都变亮了。
这两种变化都是由于光线的散射更均匀,朝向法线散射的光线更少。这意味着对于漫射的物体,它们会显得更亮,因为有更多的光线射向相机方向。对于阴影,直射的光线较少,所以大球体在小球体正下方的部分更亮。

另一种漫反射形式

并不是很多常见的、日常的物体都是完美的漫射,所以我们对这些物体在光照下的表现的视觉直觉会很差。
为了方便学习,在这里加入一种直观易懂的漫射方法。对于上面的两种方法,我们有一个随机矢量,先是随机长度,然后是单位长度,从命中点偏移为法线。为什么矢量要被法线位移可能不是很直观。
一个更直观的方法是对所有远离命中点的角度有一个统一的散射方向,与法线的角度没有关系。许多最早的射线跟踪论文都使用了这种漫射法(在采用Lambertian漫射法之前)。

[lib.rs]
1
2
3
4
5
6
7
8
9
pub fn random_in_hemisphere(normal: Vec3) -> Vec3 {
let in_unit_sphere = random_in_unit_sphere();
if dot(in_unit_sphere, normal) > 0.0 {
//和法线在同一半球
return in_unit_sphere;
} else {
return -in_unit_sphere;
}
}
[main.rs]
1
2
if world.hit(r, 0.001, infinity, &mut rec) {
let target = rec.p + random_in_hemisphere(rec.normal);


在接下来过程中,场景会变得更加复杂,需要在这章介绍的不同漫射渲染器之间进行切换,大多数场景会包含不成比例的漫射材质。

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/