<RayTracing In Weekend>读书笔记

关于RayTracing In A Weekend一书的实现细节。

Chapter1:Output an Image

下载一个ppm软件,我下载的是XnView

第一章里面给出的代码并不完整,如果想得到PPM格式的文件的话,需要在代码里加入写入txt文件的代码:

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

using namespace std;
int main()
{
int nx = 200;
int ny = 100;

ofstream outfile(".\\result\\FirstPicture.txt", ios_base::out);
outfile << "P3\n" << nx << " " << ny << "\n255\n";
for (int j = ny - 1; j >= 0; j--)
{
for (int i = 0; i < nx; i++)
{
float r = float(i) / float(nx);
float g = float(j) / float(ny);
float b = 0.2;
int ir = int(255.99*r);
int ig = int(255.99*g);
int ib = int(255.99*b);
outfile << ir << " " << ig << " " << ib << "\n";
std::cout << ir << " " << ig << " " << ib << "\n";
}
}

return 0;
}

在VmView中可以看到对应的图片:

Chapter2:The vec3 class

重载了一组关于vec3的运算符。

包括向量间的加减乘除、向量与标量的加减乘除、向量的叉积点积等。

1
2
3
4
5
6
7
8
9
10
11
inline float dot(const vec3 &v1, const vec3 &v2)
{
return v1.e[0] * v2.e[0] + v1.e[1] * v2.e[1] + v1.e[2] * v2.e[2];
}

inline vec3 cross(const vec3 &v1, const vec3 &v2)
{
return vec3((v1.e[1] * v2.e[2] - v1.e[2] * v2.e[1]),
(-(v1.e[0] * v2.e[2] - v1.e[2] * v2.e[0])),
(v1.e[0] * v2.e[1] - v1.e[1] * v2.e[0]));
}

Chapter3:Rays,a simple camera, and background

定义了射线类,并定义了三个函数,分别返回原点、方向以及射线本身。射线本身由一个Vec3表示。

Chapter4:

在运行的代码中加入了一个检测与球体相交的函数,这个函数可以从2D中射线与圆的相交检测函数出发去理解。

后面会将其抽离到单独的头文件中。

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
bool sphere :: hit(const ray& r, float t_min, float t_max, 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)
{
float temp = (-b - sqrt(b*b - a * c)) / a;
if (temp < t_max && temp > t_min)
{
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<t_max && temp>t_min)
{
rec.t = temp;
rec.p = r.point_at_parameter(rec.t);
rec.normal = (rec.p - center) / radius;
return true;
}
}
return false;
}

Chapter5:Surface normals and multiple objects.

加入了对法线方向的运算,并将对应的射线检测函数抽离到了对应的头文件中。

从球心到球上某点的向量即为该点的法线方向。

另外定义了一个类,用来计算并存储距离射线命中物体的一组点中距离射线原点最近的一个点。

Chapter6:Antialasing

题目意为抗锯齿。

这里的做法是在每个以像素点为中心,向外距离为1的范围内采样n次,将n个采样的值进行平均即为该点最终的像素值。

在数字图像处理中类似的方式被称为均值滤波。

1
2
3
4
5
6
7
8
9
10
11
vec3 col(0, 0, 0);
for (int s = 0; s < ns; s++)
{
float random = rand() % (100) / (float)(100);
float u = float(i + random) / float(nx);
float v = float(j + random) / float(ny);
ray r = cam.get_ray(u, v);
vec3 p = r.point_at_parameter(2.0);
col += color(r, world);
}
col /= float(ns);

Chapter7:Diffuse Materials

题目名为漫反射材质。

Pick a random point s from the unit radius sphere that is tangent to the hitpoint, and send a ray from the hitpoint p to the random point s. That sphere has center (p+N):

从与命中点相切的单位半径球中选择一个随机点s,并将一条射线从命中点射向随机点s。球心为(p+N)

这里的做法是将反射方向加上一个随机的方向作为漫反射的方向。

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
vec3 random_in_unit_sphere()
{
vec3 p;
do {
float random0 = rand() % (100) / (float)(100);
float random1 = rand() % (100) / (float)(100);
float random2 = rand() % (100) / (float)(100);
p = 2.0*vec3(random0, random1, random2) - vec3(1, 1, 1);
} while (p.squared_length() >= 1.0);

return p;
}

vec3 color(const ray& r, hitable *world)
{
hit_record rec;
if (world->hit(r, 0.001, (numeric_limits<float>::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);
}
}

Chapter8:Metal

第八章加上了简单的材质。

我们定义了一个简单的朗伯反射的类用来描述这一材质:理想的“遮罩”或漫反射表面的属性。无论观察者的视角如何,朗伯表面对观察者的表观亮度都是相同的。

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
#ifndef LANBERTIANH
#define LANBERTIANH

#include "material.h"
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 albedo;
};

vec3 random_in_unit_sphere() {
vec3 p;
do {
float random0 = rand() % (100) / (float)(100);
float random1 = rand() % (100) / (float)(100);
float random2 = rand() % (100) / (float)(100);
p = 2.0*vec3(random0, random1, random2) - vec3(1, 1, 1);
} while (p.squared_length() >= 1.0);
return p;
}

bool lambertian::scatter(const ray& r_in, const hit_record& rec, vec3& attenuation, ray& scattered) const {
vec3 target = rec.p + rec.normal + random_in_unit_sphere();
scattered = ray(rec.p, target - rec.p);
attenuation = albedo;
return true;
}

#endif // !LANBERTIANH

注意原书给出的随机函数没有找到对应的引用,所以在这份代码中用rand()代替。

另外原书还定义了一个镜面反射类,这个类的反射函数参考初中物理。

1
2
3
4
vec3 reflect(const vec3& v, const vec3& n)
{
return v - 2 * dot(v, n)*n;
}

在本章的最后我们添加了一个模糊的参数,这个模糊的参数使得反射光线的方向有着一定量的偏移。

Chapter9:Dielectrics

这一章的标题为电解质,实际上想说明的内容包括玻璃、水这种透明的可以折射光线的材质:

1
2
3
4
5
6
7
8
9
10
11
12
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 * (uv - n * dt) - n * sqrt(discriminant);
return true;
}
else return false;
}

注意,这里的情况判断包括一种全反射的情况。

关于折射的公式请参考:斯涅尔定律)

Chapter10:Positionable camera

在第十章,我们定义了一个具有透视功能的相机用于观察场景。

参数包括位置,观察方向,fov等。

Chapter11:Defocus Blur

题目的翻译为散焦模糊,在相机的头文件中加入了一个因光圈与焦距导致成像模糊的功能。

Chapter12:

第十二章算是一个总结与引导,描述了接下来要处理的一些细节:

You now have a cool ray tracer! What next?

  1. Lights. You can do this explicitly, by sending shadow rays to lights. Or it can be done implicitly by making some objects emit light,
  2. biasing scattered rays toward them, and then downweighting those rays to cancel out the bias. Both work. I am in the minority in favoring the latter approach.
  3. Triangles. Most cool models are in triangle form. The model I/O is the worst and almost everybody tries to get somebody else’s code to do this.
  4. Surface textures. This lets you paste images on like wall paper. Pretty easy and a good thing to do.
  5. Solid textures. Ken Perlin has his code online. Andrew Kensler has some very cool info at his blog.
  6. Volumes and media. Cool stuff and will challenge your software architecture. I favor making volumes have the hitable interface and probabilistically have intersections based on density. Your rendering code doesn’t even have to know it has volumes with that method.
  7. Parallelism. Run N copies of your code on N cores with different random seeds. Average the N runs. This averaging can also be done hierarchically where N/2 pairs can be averaged to get N/4 images, and pairs of those can be averaged. That method of parallelism should extend well into the thousands of cores with very little coding.

Have fun, and please send me your cool images!

你现在是一个很酷的Ray Tracer了,那么接下来呢?

  1. 灯:您可以通过向灯光发送阴影光线来明确地执行此操作。或者它可以通过使一些物体发光来隐式地完成。
  2. 将散射光线偏向它们,然后减轻这些光线以抵消偏差。两者都有效。我是为数不多的喜欢后一种做法的人。
  3. 三角形:最酷的模型是三角形的。模型I/O是最糟糕的,几乎每个人都试图复制别人的代码来做到这一点(I/O)。
  4. 表面纹理。这样可以将纹理像墙纸一样贴到物体上。非常简单,也是一件值得去做的好事。
  5. 坚固的纹理。 Ken Perlin在线提供他的代码。 Andrew Kensler在他的博客上有一些非常酷的信息。
  6. 卷和媒体。很酷的东西,将挑战您的软件架构。我赞成使卷具有可调整的界面,并且概率上具有基于密度的交叉点。您的呈现代码甚至不必知道它具有该方法的卷。(这里我猜是卷积?存疑。)
  7. 并行。使用不同的随机种子在N个核心上运行N个代码副本。平均N次运行。这种平均也可以分层次地完成,其中N / 2对可以被平均以获得N / 4个图像,并且可以对这些图像的对进行平均。这种并行方法应该可以很好地扩展到数千个内核中。

享受你的乐趣,请将你很酷的图像发送给我吧~