Ray Tracing in One Week

Ray Tracing in One Week系列笔记

Chapter 1~2 : Output an image and Vec3

输出到PPM图片文件

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 <iostream>
#include <fstream>
#include "vec3.h"

int main() {
int nx = 200;
int ny = 100;

// 以写模式打开文件
std::ofstream outfile;
outfile.open("color.ppm");

// PPM格式,P3表示颜色以ASCII表示,200行,100列,最大颜色数量255
outfile << "P3\n" << nx << " " << ny << "\n255\n";
for (int j = ny - 1; j >= 0; j--) {
for (int i = 0; i < nx; i++) {
vec3 col(float(i) / float(nx), float(j) / float(ny), 0.2);
// 将(0,1)扩张到(0,255.99)
int ir = int(255.99 * col[0]);
int ig = int(255.99 * col[1]);
int ib = int(255.99 * col[2]);
outfile << ir << " " << ig << " " << ib << "\n";
}
}

outfile.close();

return 0;
}

输出结果:

image-20220912173008874

Chapter 3 :Rays, a simple camera, and background

射线的构建

所有的Ray Trace都以Ray类为基础,光线追踪器的核心是发送光线穿过像素,并计算在这些光线的方向上看到什么颜色。
它的形式是计算光线从眼睛到一个像素,计算该光线的相交,并计算出该交点的颜色。

image-20220912174316734

一条射线的表示方式:

p(t)=A+tBp(t) = A + t * B

因此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
//
// Created by yw_li on 2022/9/12.
//

#ifndef RAYTRACINGTOY_RAY_H
#define RAYTRACINGTOY_RAY_H

#include "vec3.h"

class ray {
public:
ray() {}

/// 声明一条射线
/// \param a 射线起点
/// \param b 射线终点
ray(const vec3 &a, const vec3 &b) {
A = a;
B = b;
}

/// 返回射线起点
/// \return 射线起点
vec3 origin() const { return A; }

/// 返回射线的指向
/// \return 射线的指向
vec3 direction() const { return B; }

/// 返回射线上某一点
/// \param t
/// \return
vec3 point_at_parameter(float t) const { return A + B * t; }

vec3 A;
vec3 B;
};

#endif //RAYTRACINGTOY_RAY_H

构建一个视角,摄像机或眼睛放在原点

image-20220912192140976

线性插值lerp:

blended_value=(1tstart_value+tend_valueblended\_value = (1-t)*start\_value + t*end\_value

利用ray去计算指向点的颜色,修改过的main.cpp如下:

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
#include <iostream>
#include <fstream>
#include "vec3.h"
#include "ray.h"

/// color(ray)函数根据Y坐标的上/下限线性地混合白色和蓝色。
/// \param r 射线
/// \return 返回射线所指向的点的颜色
vec3 color(const ray &r) {
vec3 unit_direction = unit_vector(r.direction()); // 射线的单位方向向量
float t = 0.5 * (unit_direction.y() + 1.0);
// 返回混合后的值
// vec3(1.0, 1.0, 1.0)是白色,vec3(0.5, 0.7, 1.0f)是蓝色
return (1.0f - t) * vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
}

int main() {
int nx = 200;
int ny = 100;

// 以写模式打开文件
std::ofstream outfile;
outfile.open("color.ppm");

// PPM格式,P3表示颜色以ASCII表示,200行,100列,最大颜色数量255
outfile << "P3\n" << nx << " " << ny << "\n255\n";

vec3 lower_left_corner(-2.0, -1.0, -1.0); // 定义左下角
vec3 horizontal(4.0, 0.0, 0.0); // x轴向右,最大为4
vec3 vertical(0.0, 2.0, 0.0); // y轴向上,最高为2
vec3 origin(0.0, 0.0, 0.0); // 摄像机在坐标原点

for (int j = ny - 1; j >= 0; j--) {
for (int i = 0; i < nx; i++) {
float u = float(i) / float(nx);
float v = float(j) / float(ny);

ray r(origin, lower_left_corner + u * horizontal + v * vertical);
vec3 col = color(r);

// 将(0,1)映射到(0,255.99)
int ir = int(255.99 * col[0]);
int ig = int(255.99 * col[1]);
int ib = int(255.99 * col[2]);
outfile << ir << " " << ig << " " << ib << "\n";
}
}

outfile.close();

return 0;
}

输出的结果:

image-20220912192114174

Chapter 4 :Adding a sphere

球心在原点,且半径为RR的球体可以表示为:

xx+yy+zz=RRx*x + y * y + z*z = R * R

球心在(cx,cy,cz)(cx,cy,cz),半径为RR的球体可以表示为:

(xcx)(xcx)+(ycy)(ycy)+(zcz)(zcz)=RR(x - cx)*(x - cx) + (y - cy) * (y - cy) + (z - cz)*(z - cz) = R*R

当球心表示为

C=(cx,cy,cz)C = (cx,cy,cz)

球体上任一点表示为

p=(x,y,z)p = (x,y,z)

CC指向pp(pC)(p-C),因此

dot((pC),(pC))=(xcx)(xcx)+(ycy)(ycy)+(zcz)(zcz)dot((p-C),(p-C)) = (x - cx)*(x - cx) + (y - cy) * (y - cy) + (z - cz)*(z - cz)

即球体方程可以表示为:

dot((pC),(pC))=RRdot((p-C),(p-C)) = R*R

等价于:

dot((A+tBC),(A+tBC))=RRdot((A + t*B - C),(A + t*B - C)) = R*R

展开可以得到:

ttdot(B,B)+2tdot(AC,B)+dot(AC,AC)RR=0t*t*dot(B,B) + 2*t*dot(A-C,B) + dot(A-C,A-C) - R*R = 0

A,B,C,RA,B,C,R均为已知量,因此得到关于tt的一元二次方程,关于tt的解的数量,有如下关系:

image-20220912195109354

对是否相交进行判断,并修改color函数:

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
/// 检测射线是否与球心为center,半径为radius的球相交
/// \param center 球心
/// \param radius 半径
/// \param r 射线
/// \return 是否相交
bool hit_sphere(const vec3 &center, float radius, const ray &r) {
vec3 oc = r.origin() - center;
float a = dot(r.direction(), r.direction()); // 一元二次方程系数a
float b = 2.0 * dot(oc, r.direction());
float c = dot(oc, oc) - radius * radius;
float discriminant = b * b - 4 * a * c;
return (discriminant > 0);
}

/// color(ray)函数根据Y坐标的上/下限线性地混合白色和蓝色。
/// \param r 射线
/// \return 返回射线所指向的点的颜色
vec3 color(const ray &r) {
if (hit_sphere(vec3(0, 0, -1), 0.5, r)) {
return vec3(1, 0, 0); // 如果射线与球心为(0,0,-1),半径为0.5的球相交,则返回红色
}
vec3 unit_direction = unit_vector(r.direction()); // 射线的单位方向向量
float t = 0.5 * (unit_direction.y() + 1.0);
// 返回混合后的值
// vec3(1.0, 1.0, 1.0)是白色,vec3(0.5, 0.7, 1.0f)是蓝色
return (1.0f - t) * vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
}

最终输出结果如下:

image-20220912201155670

修改后的main.cppmain.cpp如下:

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
#include <iostream>
#include <fstream>
#include "vec3.h"
#include "ray.h"

/// 检测射线是否与球心为center,半径为radius的球相交
/// \param center 球心
/// \param radius 半径
/// \param r 射线
/// \return 是否相交
bool hit_sphere(const vec3 &center, float radius, const ray &r) {
vec3 oc = r.origin() - center;
float a = dot(r.direction(), r.direction()); // 一元二次方程系数a
float b = 2.0 * dot(oc, r.direction());
float c = dot(oc, oc) - radius * radius;
float discriminant = b * b - 4 * a * c;
return (discriminant > 0);
}

/// color(ray)函数根据Y坐标的上/下限线性地混合白色和蓝色。
/// \param r 射线
/// \return 返回射线所指向的点的颜色
vec3 color(const ray &r) {
if (hit_sphere(vec3(0, 0, -1), 0.5, r)) {
return vec3(1, 0, 0); // 如果射线与球心为(0,0,-1),半径为0.5的球相交,则返回红色
}
vec3 unit_direction = unit_vector(r.direction()); // 射线的单位方向向量
float t = 0.5 * (unit_direction.y() + 1.0);
// 返回混合后的值
// vec3(1.0, 1.0, 1.0)是白色,vec3(0.5, 0.7, 1.0f)是蓝色
return (1.0f - t) * vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
}

int main() {
int nx = 200;
int ny = 100;

// 以写模式打开文件
std::ofstream outfile;
outfile.open("color.ppm");

// PPM格式,P3表示颜色以ASCII表示,200行,100列,最大颜色数量255
outfile << "P3\n" << nx << " " << ny << "\n255\n";

vec3 lower_left_corner(-2.0, -1.0, -1.0); // 定义左下角
vec3 horizontal(4.0, 0.0, 0.0); // x轴向右,最大为4
vec3 vertical(0.0, 2.0, 0.0); // y轴向上,最高为2
vec3 origin(0.0, 0.0, 0.0); // 摄像机在坐标原点

for (int j = ny - 1; j >= 0; j--) {
for (int i = 0; i < nx; i++) {
float u = float(i) / float(nx);
float v = float(j) / float(ny);

ray r(origin, lower_left_corner + u * horizontal + v * vertical);
vec3 col = color(r);

// 将(0,1)映射到(0,255.99)
int ir = int(255.99 * col[0]);
int ig = int(255.99 * col[1]);
int ib = int(255.99 * col[2]);
outfile << ir << " " << ig << " " << ib << "\n";
}
}

outfile.close();

return 0;
}

Chapter 5 : Surface normals and multiple objects

球体的表面的法线向量:由球心指向接触点

image-20220912223950476

我们先求得交点处的法线单位向量NN,再将其映射到[0,1][0,1]

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
/// 检测射线是否与球心为center,半径为radius的球相交
/// \param center 球心
/// \param radius 半径
/// \param r 射线
/// \return 是否相交
float hit_sphere(const vec3 &center, float radius, const ray &r) {
vec3 oc = r.origin() - center;
float a = dot(r.direction(), r.direction()); // 一元二次方程系数a
float b = 2.0 * dot(oc, r.direction());
float c = dot(oc, oc) - radius * radius;
float discriminant = b * b - 4 * a * c;
if (discriminant < 0) {
return -1.0;
} else {
// 一元二次方程的两个解中较小的那个,即先发生碰撞的点
return (-b - sqrt(discriminant) / (2.0 * a));
}
}

/// color(ray)函数根据Y坐标的上/下限线性地混合白色和蓝色。
/// \param r 射线
/// \return 返回射线所指向的点的颜色
vec3 color(const ray &r) {
// 求得射线与球发生相交时的第一个点对应的t
float t = hit_sphere(vec3(0, 0, -1), 0.5, r);
if (t > 0.0) {
// 求得球心指向交点的法线单位向量
vec3 N = unit_vector(r.point_at_parameter(t) - vec3(0, 0, -1));
// 将求得的法线单位向量由[-1,1]映射到[0,1]
return 0.5 * vec3(N.x() + 1, N.y() + 1, N.z() + 1);
}
vec3 unit_direction = unit_vector(r.direction()); // 射线的单位方向向量
t = 0.5 * (unit_direction.y() + 1.0);
// 返回混合后的值
// vec3(1.0, 1.0, 1.0)是白色,vec3(0.5, 0.7, 1.0f)是蓝色
return (1.0f - t) * vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
}

输出结果如下:

image-20220913193437991

现在考虑多个可被击中的物体。

构建一个Hitable的抽象类,包含抽象方法hit判断是否击中物体,以及记录hit到的数据,包括hit的位置,hit点的法向,以及距离参数t。

设定tt的区间tmin<t<tmaxtmin < t < tmax,可以控制在此区域内的hit才计数。在计算hit时,只需要最接近的事物的法线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//
// Created by yw_li on 2022/9/13.
//

#ifndef RAYTRACINGTOY_HITABLE_H
#define RAYTRACINGTOY_HITABLE_H

#include "ray.h"

struct hit_record {
float t; // 射线参数t
vec3 p; // 射线与物体的交点(碰撞点)
vec3 normal; // 碰撞点处的法线向量
};

class hitable {
public:
virtual bool hit(const ray &r, float t_min, float t_max, hit_record &rec) const = 0;
};

#endif //RAYTRACINGTOY_HITABLE_H

对于sphere类基础hitable抽象类,实现自己的hit方法,去判断是否击中了球的对象

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
//
// Created by yw_li on 2022/9/13.
//

#ifndef RAYTRACINGTOY_SPHERE_H
#define RAYTRACINGTOY_SPHERE_H

#include "hitable.h"

class sphere : public hitable {
public:
sphere() {}

// 构造球心为cen,半径为r的球体
sphere(vec3 cen, float r) : center(cen), radius(r) {};

virtual bool hit(const ray &r, float tmin, float tmax, hit_record &rec) const;

vec3 center; // 球心坐标
float radius; // 球体半径
};

bool sphere::hit(const ray &r, float tmin, float tmax, hit_record &rec) const {
vec3 oc = r.origin() - center;
float a = dot(r.direction(), r.direction());
float b = dot(oc, r.direction());
float c = dot(oc, oc) - radius * radius;
float discriminant = b * b - a * c;
if (discriminant > 0) {
// 射线与球体相交时,找出参数t处于[tmin,tmax]范围内的交点
// 两个交点都在范围内的话,返回参数t较小的那个(即先碰撞的点)
// 将碰撞点信息存储rec中
float temp = (-b - sqrt(b * b - a * c)) / a;
if (temp < tmax && temp > tmin) {
rec.t = temp;
rec.p = r.point_at_parameter(rec.t);
rec.normal = (rec.p - center) / radius;
return true;
}
temp = (-b + sqrt(b * b - a * c)) / a;
if (temp < tmax && temp > tmin) {
rec.t = temp;
rec.p = r.point_at_parameter(rec.t);
rec.normal = (rec.p - center) / radius;
return true;
}
}
return false;
}

#endif //RAYTRACINGTOY_SPHERE_H

还需要一个hitable list去记录击中所有的物体,也是继承hitable类,实现hit方法,去找出最近的物体。

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
//
// Created by yw_li on 2022/9/13.
//

#ifndef RAYTRACINGTOY_HITABLELIST_H
#define RAYTRACINGTOY_HITABLELIST_H

#include "hitable.h"

class hitable_list : public hitable {
public:
hitable_list() {}

hitable_list(hitable **l, int n) {
list = l;
list_size = n;
}

virtual bool hit(const ray &r, float tmin, float tmax, hit_record &rec) const;

hitable **list; // 元素类型为hitable的数组
int list_size; // hitable list元素个数
};

bool hitable_list::hit(const ray &r, float tmin, float tmax, hit_record &rec) const {
hit_record temp_rec;
bool hit_anything = false;
double closet_so_far = tmax;
// 对hitable list中的所有元素进行hit检测,并将最近的hit信息存入rec中
for (int i = 0; i < list_size; i++) {
// 每次只对[tmin, closet_so_far]范围内的检测hit,即
if (list[i]->hit(r, tmin, closet_so_far, temp_rec)) {
hit_anything = true;
closet_so_far = temp_rec.t; // 更新closet_so_far信息,closet_so_far只会越来越小
rec = temp_rec;
}
}
return hit_anything;
}

#endif //RAYTRACINGTOY_HITABLELIST_H

更新过的main.cpp如下:

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
#include <iostream>
#include <fstream>
#include <float.h>
#include "vec3.h"
#include "ray.h"
#include "sphere.h"
#include "hitablelist.h"

///
/// \param r 射线
/// \param world 可碰撞物体
/// \return 射线交点处的颜色,发生碰撞返回碰撞点法线,未发生碰撞返回背景色
vec3 color(const ray &r, hitable *world) {
hit_record rec;
if (world->hit(r, 0.0, FLT_MAX, rec)) {
// 如果射线与物体发生碰撞,则将碰撞点法线向量从[-1,1]映射到[0,1]
return 0.5 * vec3(rec.normal.x() + 1, rec.normal.y() + 1, rec.normal.z() + 1);
} else {
// 如果射线没有与物体发生碰撞,则返回背景的颜色
vec3 unit_direction = unit_vector(r.direction());
float t = 0.5 * (unit_direction.y() + 1.0);
return (1.0 - t) * vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
}
}

int main() {
int nx = 200;
int ny = 100;

// 以写模式打开文件
std::ofstream outfile;
outfile.open("color.ppm");

// PPM格式,P3表示颜色以ASCII表示,200行,100列,最大颜色数量255
outfile << "P3\n" << nx << " " << ny << "\n255\n";

vec3 lower_left_corner(-2.0, -1.0, -1.0); // 定义左下角
vec3 horizontal(4.0, 0.0, 0.0); // x轴向右,最大为4
vec3 vertical(0.0, 2.0, 0.0); // y轴向上,最高为2
vec3 origin(0.0, 0.0, 0.0); // 摄像机在坐标原点

hitable *list[2];
list[0] = new sphere(vec3(0, 0, -1), 0.5);
list[1] = new sphere(vec3(0, -100.5, -1), 100);
hitable *world = new hitable_list(list, 2);

for (int j = ny - 1; j >= 0; j--) {
for (int i = 0; i < nx; i++) {
float u = float(i) / float(nx);
float v = float(j) / float(ny);

ray r(origin, lower_left_corner + u * horizontal + v * vertical);
vec3 p = r.point_at_parameter(2.0);
vec3 col = color(r, world);

// 将(0,1)映射到(0,255.99)
int ir = int(255.99 * col[0]);
int ig = int(255.99 * col[1]);
int ib = int(255.99 * col[2]);
outfile << ir << " " << ig << " " << ib << "\n";
}
}

outfile.close();

return 0;
}

最终的结果:

image-20220914013825807

Chapter 6 : Antialiasing

本节目标是实现抗锯齿,首先需要实现在一定范围内的随机采样,因此我们需要一个随机函数。我们用到random头文件,利用rand()函数。

  • 要取得 [a,b) 的随机整数,使用 (rand() % (b-a))+ a;
  • 要取得 [a,b] 的随机整数,使用 (rand() % (b-a+1))+ a;
  • 要取得 (a,b] 的随机整数,使用 (rand() % (b-a))+ a + 1;
  • 通用公式: a + rand() % n;其中的 a 是起始值,n 是整数的范围。
  • 要取得 a 到 b 之间的随机整数,另一种表示:a + (int)b * rand() / (RAND_MAX + 1)。
  • 要取得 0~1 之间的浮点数,可以使用 rand() / double(RAND_MAX)。

因此我们可以在main.cpp定义一个宏,用来生成[a.b][a.b]之间的随机数。

1
#define random(a, b) (rand()%(b-a+1)+a)

当然,缺点是每此生成的随机结果都是一样的。

对于给定的一个像素,我们有好几个随机的采样点,对每个采样点进行ray tracer,然后再平均结果。

image-20220919165426334

然后我们实现一个camera类。

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
//
// Created by yw_li on 2022/9/15.
//

#ifndef RAYTRACINGTOY_CAMERA_H
#define RAYTRACINGTOY_CAMERA_H

#include "ray.h"

class camera {
public:
camera() {
lower_left_corner = vec3(-2.0, -1.0, -1.0);
horizontal = vec3(4.0, 0.0, 0.0);
vertical = vec3(0.0, 2.0, 0.0);
origin = vec3(0.0, 0.0, 0.0);
}

ray get_ray(float u, float v) {
return ray(origin, lower_left_corner + u * horizontal + v * vertical - origin);
}

vec3 origin;
vec3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
};

#endif //RAYTRACINGTOY_CAMERA_H

最终的main.cpp

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
73
74
75
76
77
#include <iostream>
#include <fstream>
#include <float.h>
#include "vec3.h"
#include "ray.h"
#include "sphere.h"
#include "hitablelist.h"
#include "camera.h"
#include "random"

#define random(a, b) (rand()%(b-a+1)+a)

///
/// \param r 射线
/// \param world 可碰撞物体
/// \return 射线交点处的颜色,发生碰撞返回碰撞点法线,未发生碰撞返回背景色
vec3 color(const ray &r, hitable *world) {
hit_record rec;
if (world->hit(r, 0.0, FLT_MAX, rec)) {
// 如果射线与物体发生碰撞,则将碰撞点法线向量从[-1,1]映射到[0,1]
return 0.5 * vec3(rec.normal.x() + 1, rec.normal.y() + 1, rec.normal.z() + 1);
} else {
// 如果射线没有与物体发生碰撞,则返回背景的颜色
vec3 unit_direction = unit_vector(r.direction());
float t = 0.5 * (unit_direction.y() + 1.0);
return (1.0 - t) * vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
}
}

int main() {
int nx = 200;
int ny = 100;

int ns = 100;

// 以写模式打开文件
std::ofstream outfile;
outfile.open("color.ppm");

// PPM格式,P3表示颜色以ASCII表示,200行,100列,最大颜色数量255
outfile << "P3\n" << nx << " " << ny << "\n255\n";

camera cam;

hitable *list[2];
list[0] = new sphere(vec3(0, 0, -1), 0.5);
list[1] = new sphere(vec3(0, -100.5, -1), 100);
hitable *world = new hitable_list(list, 2);

for (int j = ny - 1; j >= 0; j--) {
for (int i = 0; i < nx; i++) {
vec3 col(0, 0, 0);

// 随机生成ns个采样点,并将颜色叠加
for (int s = 0; s < ns; s++) {
float u = float((i + random(0, 100) / 100) / float(nx));
float v = float((j + random(0, 100) / 100) / float(ny));

ray r = cam.get_ray(u, v);
vec3 p = r.point_at_parameter(2.0);
col += color(r, world);
}

col /= float(ns);

// 将(0,1)映射到(0,255.99)
int ir = int(255.99 * col[0]);
int ig = int(255.99 * col[1]);
int ib = int(255.99 * col[2]);
outfile << ir << " " << ig << " " << ib << "\n";
}
}

outfile.close();

return 0;
}

Chapter7 : Diffuse Materials

本章的目标是实现漫反射。

通常在渲染中,每个物体都有自己的材质。

对于不发光的物体,漫反射是吸收周围的颜色,并与自身的颜色混合调节。

从漫反射物体表面反射的光线的方向是随机的。因此,如下图,如果我们向两个漫反射物体形成的夹缝中射入3条光线,会有不同的反射路径结果。

image-20220919170654874

物体表面越暗,看起来越像是光线被吸收了,吸收之后的表面更像是是一个哑光的表面。

我们选择表面上一点hitpoint点PP,在该点做一个与表面相切的单位球,这个球的球心是(P+N)(P+N),我们从球体中随机选择一个点SS,从hitpoint点PP处,发射一条由PP指向SS的射线作为漫反射的方向。

image-20220919171416861

我们构造一个方法来从单位半径球体中选择一个随机点:首先,选择一个单位立方体中的随机点,其中x,y,z的范围都在-1到+1之间。如果这个点在球体外,则重新选择一个随机点。

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
/// 单位cube随机取点,并返回处于球体内的一点
/// \return
vec3 random_in_unit_sphere() {
vec3 p;
do {
//srand((unsigned) time(NULL));
p = 2.0 * vec3(random1, random1, random1) - vec3(1, 1, 1);
//std::cout << random(0, 100) / 100 << std::endl;
} while (dot(p, p) >= 1.0); // 如果点在半径为1的球体外,则重新生成
return p;
}

///
/// \param r 射线
/// \param world 可碰撞物体
/// \return 射线交点处的颜色,发生碰撞返回碰撞点法线,未发生碰撞返回背景色
vec3 color(const ray &r, hitable *world) {
hit_record rec;
if (world->hit(r, 0.0, FLT_MAX, rec)) {
vec3 target = rec.p + rec.normal + random_in_unit_sphere();
return 0.5 * color(ray(rec.p, target - rec.p), world);
} else {
// 如果射线没有与物体发生碰撞,则返回背景的颜色
vec3 unit_direction = unit_vector(r.direction());
float t = 0.5 * (unit_direction.y() + 1.0);
return (1.0 - t) * vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
}
}

此时我们得到的结果:

image-20220920001101349

可以看出球体底部过暗,因此我们可以进行gamma补偿。

1
2
3
4
5
6
7
col /= float(ns);
// 进行gamma补偿
col = vec3(sqrt(col[0]), sqrt(col[1]), sqrt(col[2]));
// 将(0,1)映射到(0,255.99)
int ir = int(255.99 * col[0]);
int ig = int(255.99 * col[1]);
int ib = int(255.99 * col[2]);

得到的补偿后的结果:

image-20220920001519893

最终的mian.cpp:

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
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
#include <iostream>
#include <fstream>
#include <float.h>
#include "vec3.h"
#include "ray.h"
#include "sphere.h"
#include "hitablelist.h"
#include "camera.h"
#include "random"
#include <iostream>

#define random(a, b) (rand()%(b-a+1)+a) //使用rand()的一个后果是,种子相同时每次的随机结果都相同
#define random1 (float((rand() % 100) / 100.f))

/// 单位cube随机取点,并返回处于球体内的一点
/// \return
vec3 random_in_unit_sphere() {
vec3 p;
do {
//srand((unsigned) time(NULL));
p = 2.0 * vec3(random1, random1, random1) - vec3(1, 1, 1);
//std::cout << random(0, 100) / 100 << std::endl;
} while (dot(p, p) >= 1.0); // 如果点在半径为1的球体外,则重新生成
return p;
}

///
/// \param r 射线
/// \param world 可碰撞物体
/// \return 射线交点处的颜色,发生碰撞返回碰撞点法线,未发生碰撞返回背景色
vec3 color(const ray &r, hitable *world) {
hit_record rec;
if (world->hit(r, 0.0, FLT_MAX, rec)) {
vec3 target = rec.p + rec.normal + random_in_unit_sphere();
return 0.5 * color(ray(rec.p, target - rec.p), world);
} else {
// 如果射线没有与物体发生碰撞,则返回背景的颜色
vec3 unit_direction = unit_vector(r.direction());
float t = 0.5 * (unit_direction.y() + 1.0);
return (1.0 - t) * vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
}
}

int main() {
int nx = 200;
int ny = 100;

int ns = 200;

// 以写模式打开文件
std::ofstream outfile;
outfile.open("color.ppm");

// PPM格式,P3表示颜色以ASCII表示,200行,100列,最大颜色数量255
outfile << "P3\n" << nx << " " << ny << "\n255\n";

camera cam;

hitable *list[2];
list[0] = new sphere(vec3(0, 0, -1), 0.5); // 球1
list[1] = new sphere(vec3(0, -100.5, -1), 100); // 球2
hitable *world = new hitable_list(list, 2);

for (int j = ny - 1; j >= 0; j--) {
for (int i = 0; i < nx; i++) {
vec3 col(0, 0, 0);

// 随机生成ns个采样点,并将颜色叠加
for (int s = 0; s < ns; s++) {
//srand((unsigned) time(NULL));
float u = float((i + random(0, 100) / 100) / float(nx));
float v = float((j + random(0, 100) / 100) / float(ny));

ray r = cam.get_ray(u, v);
vec3 p = r.point_at_parameter(2.0);
col += color(r, world);
}

col /= float(ns);
// 进行gamma补偿
col = vec3(sqrt(col[0]), sqrt(col[1]), sqrt(col[2]));
// 将(0,1)映射到(0,255.99)
int ir = int(255.99 * col[0]);
int ig = int(255.99 * col[1]);
int ib = int(255.99 * col[2]);
outfile << ir << " " << ig << " " << ib << "\n";
}
}

outfile.close();

return 0;
}

Chapter 8 : Metal

如果我们需要不同的物体拥有不同的材质,一种本方法是:我们可以定义一个具有很多参数的材质,每种不同的材质就是不同参数的组合。

或者我们可以定义一个材质的抽象类:

我们定义的材质需要能够做到:

  1. 产生散射线(或者说它吸收了入射线)
  2. 如果发生散射,则应该确定光线究竟衰减了多少

定义一个如下的material抽象类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//
// Created by yw_li on 2022/9/20.
//
#ifndef RAYTRACINGTOY_MATERIAL_H
#define RAYTRACINGTOY_MATERIAL_H

#include "ray.h"
#include "hitable.h"

class material {
public:
virtual bool scatter(const ray &r_in, const hit_record &rec, vec3 &attenuation, ray &scattered) const = 0;
};

#endif //RAYTRACINGTOY_MATERIAL_H

在我们计算碰撞的信息时,我们需要获得碰撞处的材质,因此我们在hit_recordhit\_record类中添加一个指向材质的指针:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//
// Created by yw_li on 2022/9/13.
//

#ifndef RAYTRACINGTOY_HITABLE_H
#define RAYTRACINGTOY_HITABLE_H

#include "ray.h"
#include "material.h"

struct hit_record {
float t; // 射线参数t
vec3 p; // 射线与物体的交点(碰撞点)
vec3 normal; // 碰撞点处的法线向量
material *mat_ptr; // 保存碰撞点处的材质
};

class hitable {
public:
virtual bool hit(const ray &r, float t_min, float t_max, hit_record &rec) const = 0;
};

#endif //RAYTRACINGTOY_HITABLE_H

对于我们已经拥有的Lambertian(漫反射)情况,它既可以总是散射,也可以通过其反射率RR衰减,或者它可以在没有衰减的情况下散射但吸收部分1R1-R射线,或者混合使用这些策略。对于Lambertian材质,我们可以设计这样一个类:

1
2
3
4
5
6
7
8
9
10
11
12
13
class lambertian : public material {
public:
lambertian(const vec3 &a) : albedo(a) {}

virtual bool scatter(const ray &r_in, const hit_record &rec, vec3 &attenuation, ray &scattered) const {
vec3 target = rec.p + rec.normal + random_in_unit_sphere(); // 散射方向由random_in_unit_sphere()控制
scattered = ray(rec.p, target - rec.p);
attenuation = albedo; // attenuation衰减,控制散射后的光纤强度
return true;
}

vec3 albedo; // 反射率
};

我们也可以仅按照某个概率pp进行散射,并且衰减为albedo/palbedo/p

对于光滑的表面,光线不会随机散射。光滑表面的光线反射物理规律是出射角等于入射角,会发生镜面反射

image-20220920170048181

图中红色的是反射光线,向量表示为(v+2B)(v+2B)NN是单位法向量,vv是入射光线的方向向量,BB的模是vvNN的点乘dot(v,N)dot(v,N)。公式为:

1
2
3
4
5
6
7
/// 求镜面反射出射向量
/// \param v 入射光线向量
/// \param n 平面单位法向量
/// \return 出射光线向量
vec3 reflect(const vec3 &v, const vec3 &n) {
return v - 2 * dot(v, n) * n;
}

接下来我们就可以利用镜面反射来构造一个金属材质(matelmatel):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 金属材质
class metal : public material {
public:
metal(const vec3 &a) : albedo(a) {}

virtual bool scatter(const ray &r_in, const hit_record &rec, vec3 &attenuation, ray &scattered) const {
vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal);
scattered = ray(rec.p, reflected);
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}

vec3 albedo; // 反射率
};

然后我们对color()color()函数进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
///
/// \param r 射线
/// \param world 可碰撞物体
/// \param depth 反射深度
/// \return 射线交点处的颜色,发生碰撞返回碰撞点法线,未发生碰撞返回背景色
vec3 color(const ray &r, hitable *world, int depth) {
hit_record rec;
if (world->hit(r, 0.0001, FLT_MAX, rec)) {
ray scattered;
vec3 attenuation;

if (depth < 50 && rec.mat_ptr->scatter(r, rec, attenuation, scattered)) {
return attenuation * color(scattered, world, depth + 1);
} else {
return vec3(0, 0, 0);
}
} else {
// 如果射线没有与物体发生碰撞,则返回背景的颜色
vec3 unit_direction = unit_vector(r.direction());
float t = 0.5 * (unit_direction.y() + 1.0);
return (1.0 - t) * vec3(1.0, 1.0, 1.0) + t * vec3(0.5, 0.7, 1.0);
}
}

也需要对spheresphere类进行修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
class sphere : public hitable {
public:
sphere() {}

// 构造球心为cen,半径为r的球体
sphere(vec3 cen, float r, material *mat) : center(cen), radius(r), mat_ptr(mat) {};

virtual bool hit(const ray &r, float tmin, float tmax, hit_record &rec) const;

vec3 center; // 球心坐标
float radius; // 球体半径
material *mat_ptr; // 球体材质
};

mainmain中新建两个金属球:

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
int main() {
int nx = 600;
int ny = 300;

int ns = 500;

// 以写模式打开文件
std::ofstream outfile;
outfile.open("color.ppm");

// PPM格式,P3表示颜色以ASCII表示,200行,100列,最大颜色数量255
outfile << "P3\n" << nx << " " << ny << "\n255\n";

camera cam;

hitable *list[2];
list[0] = new sphere(vec3(0, 0, -1), 0.5, new lambertian(vec3(0.8, 0.3, 0.3))); // 球1
list[1] = new sphere(vec3(0, -100.5, -1), 100, new lambertian(vec3(0.8, 0.8, 0.8))); // 球2
list[2] = new sphere(vec3(1, 0, -1), 0.5, new metal(vec3(0.8, 0.6, 0.2))); // 球3金属球
list[3] = new sphere(vec3(-1, 0, -1), 0.5, new metal(vec3(0.8, 0.8, 0.8))); // 球4金属球
hitable *world = new hitable_list(list, 4);

for (int j = ny - 1; j >= 0; j--) {
for (int i = 0; i < nx; i++) {
vec3 col(0, 0, 0);

// 随机生成ns个采样点,并将颜色叠加
for (int s = 0; s < ns; s++) {
//srand((unsigned) time(NULL));
float u = float((i + random(0, 100) / 100) / float(nx));
float v = float((j + random(0, 100) / 100) / float(ny));

ray r = cam.get_ray(u, v);
vec3 p = r.point_at_parameter(2.0);
col += color(r, world, 0);
}

col /= float(ns);
// 进行gamma补偿
col = vec3(sqrt(col[0]), sqrt(col[1]), sqrt(col[2]));
// 将(0,1)映射到(0,255.99)
int ir = int(255.99 * col[0]);
int ig = int(255.99 * col[1]);
int ib = int(255.99 * col[2]);
outfile << ir << " " << ig << " " << ib << "\n";
}
}

outfile.close();

return 0;
}

此时的结果:

image-20220920174800044

对于metalmetal的材质,也可以用一个随机性的反射方向,来做微量的偏移,相当于在一个小球上选择一个endpointendpoint,而fuzzinssfuzzinss就相当于这个小球的半径,可以决定反射偏移的多少,fuzzinssfuzzinss的取值在[0,1][0,1]之间。

image-20220920175010641

Chapter9:Dielectrics

透明的材料,如水,玻璃和钻石是电介质。

当一束光线照射到它们时,它会分裂成一束反射光线和一束折射(透射)光线。我们将通过在反射和折射之间随机选择并且每次相互作用只产生一个散射光来处理这个问题。

最难调试的部分是折射光。通常我们假设所有的折射,只有一条折射光线。我们先尝试在场景中放入两个玻璃球。得到如下图:

image-20221012035029579

但是看起来并不对,现实生活中没有黑色的东西。

光从一种介质进入另一种介质时,实际上,有一部分光会折射进入另一种介质,有另一部分光则会反射回来。反射系数=反射光振幅(能量)/入射光振幅(能量)。

折射满足斯涅尔定律(Snell’s law):

nsin(theta)=nsin(theta)n * sin(theta) = n' * sin(theta')

其中nn'是折射率。一般空气的折射率为1,玻璃的折射率为1.3-1.7,钻石的折射率为2.4。

image-20221012035740658

一个棘手的实际问题是,当光现在具有较高折射率的材料中时,斯涅尔定律并没有真正的解决方案,不可能存在折射。所有的光线都在其内部反射,因为实际上这些光线通常在固体物体内部,所以被称为全内反射。这就是为什么有时当你被淹没在水中时,水-空气的界限就像一面完美的镜子。因此,折射的代码要比反射的代码复杂一些。

折射的部分代码:

1
2
3
4
5
6
7
8
9
10
11
bool refract(const vec3 &v, const vec3 &n, float ni_over_nt, vec3 &refracted) {
vec3 uv = unit_vector(v);
float dt = dot(uv, n);
float discriminant = 1.0 - ni_over_nt * ni_over_nt * (1 - dt * dt);
if (discriminant > 0) {
refracted = ni_over_nt * (v - n * dt) - n * sqrt(discriminant);
return true;
} else {
return false;
}
}

电介质材质总是会发生折射,因此我们在材质类中派生dielectrics材质:

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
// 电介质
class dielectric : public material {
public:
dielectric(float ri) : ref_idx(ri) {}

virtual bool scatter(const ray &r_in, const hit_record &rec, vec3 &attenuation, ray &scattered) const {
vec3 outward_normal;
vec3 reflected = reflect(r_in.direction(), rec.normal);
float ni_over_nt;
attenuation = vec3(1.0, 1.0, 0.0);
vec3 refracted;
if (dot(r_in.direction(), rec.normal) > 0) {
outward_normal = -rec.normal;
ni_over_nt = ref_idx;
} else {
outward_normal = rec.normal;
ni_over_nt = 1.0 / ref_idx;
}
if (refract(r_in.direction(), outward_normal, ni_over_nt, refracted)) {
scattered = ray(rec.p, refracted);
} else {
scattered = ray(rec.p, reflected);
return false;
}
return true;
}

float ref_idx;
};

Attenuation(衰减)总为1,玻璃材质的表面不吸收任何光线。

我们将球4材质改为玻璃球。

1
list[3] = new sphere(vec3(-1, 0, -1), 0.5, new dielectric(1.5)); // 球4玻璃球

真正的玻璃具有随角度变化的反射率,从陡峭的角度看窗户,它就变成了一面镜子。这里一个简陋但是大家都在用的,由Christophe Schlick提出的多项式近似:

1
2
3
4
5
float schlick(float cosine, float ref_idx) {
float r0 = (1 - ref_idx) / (1 + ref_idx);
r0 = r0 * r0;
return r0 + (1 - r0) * pow((1 - cosine), 5);
}

因此完成后的电介质材质:

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
// 电介质
class dielectric : public material {
public:
dielectric(float ri) : ref_idx(ri) {}

virtual bool scatter(const ray &r_in, const hit_record &rec, vec3 &attenuation, ray &scattered) const {
vec3 outward_normal;
vec3 reflected = reflect(r_in.direction(), rec.normal);
float ni_over_nt;
attenuation = vec3(1.0, 1.0, 1.0);
vec3 refracted;
float reflect_prob;
float cosine;
if (dot(r_in.direction(), rec.normal) > 0) {
outward_normal = -rec.normal;
ni_over_nt = ref_idx;
cosine = ref_idx * dot(r_in.direction(), rec.normal) / r_in.direction().length();
} else {
outward_normal = rec.normal;
ni_over_nt = 1.0 / ref_idx;
cosine = -dot(r_in.direction(), rec.normal) / r_in.direction().length();
}
if (refract(r_in.direction(), outward_normal, ni_over_nt, refracted)) {
//scattered = ray(rec.p, refracted);
reflect_prob = schlick(cosine, ref_idx);
} else {
scattered = ray(rec.p, reflected);
reflect_prob = 1.0;
//return false;
}
// 随机数小与反射系数,设为反射光线,反之为折射光线
if (drand48() < reflect_prob) {
scattered = ray(rec.p, reflected);
} else {
scattered = ray(rec.p, refracted);
}
return true;
}

float ref_idx;
};