前言
本系列文章链接
带你从零开始徒手撸光线追踪代码(1)—— Ray Tracing in One Weekend
带你从零开始徒手撸光线追踪代码(2)—— Ray Tracing in One Weekend
带你从零开始徒手撸光线追踪代码(3)—— Ray Tracing in One Weekend
上回书说道,彼时的璃月,不好意思,串台了。上回书说道,我们定义了自己的三维向量类,定义了光线,并将两个球放入到场景中。这一节我们最主要的目的是抗锯齿以及解决材质问题,这一部分对应的是《Ray Tracing in One Weekend》的第7章至第9章。
行文目录
抗锯齿
随机函数
关于我们为什么要说随机数的问题这个待会再讨论,我们产生随机数的一个最基本的方法就是采用中的 rand() 函数,想要获取一个[0, 1)之间的数,只需要利用关系式: rand() / (RAND_MAX + 1.0) 即可,对于想获得任意两个数之间的随机数,则使用关系式:min + (max-min)*random_num,其中random_num是一个[0, 1)之间的随机数。
但是这种传统的C++生成随机数的方法不是标准随机数,其并不符合均匀分布,因此我们利用C++11标准的<random>头文件来实现,利用 uniform_real_distribution 生成一个均匀分布,在利用mt19973生成随机数,这样我们就可以得到一个符合均匀分布的随机数了。要得到任意区间的随机数的话还是利用关系式:min + (max-min) * random_num,其中random_num就是我们刚刚生成的均匀分布的随机数。关于uniform_real_distribution 和 mt19973可以到参考文献([2]-[4])看到参考的博客链接。
#include <random>
inline double random_double() {
static std::uniform_real_distribution<double> distribution(0.0, 1.0);
static std::mt19937 generator;
return distribution(generator);
}
MSAA抗锯齿
我们都知道,在计算机中,我们通过像素点来显示图像,这叫光栅化,然而这就会导致一个问题,那就是锯齿,直观来看就是在物体边缘会出现阶梯状的观感,而不够丝滑。这是一种采样率不够的结果,理论上来说,当我们的像素点足够多的话,我们在观感上是感觉不到锯齿的。但是有的时候像素不够多,因此我们需要人为的提高采样率,以尽可能的降低这种锯齿所带来的差观感。具体的做法就是在一个像素中采样多次,然后综合这些采样的值做个平均,得到的平均值即为该像素位置的颜色。这样做虽然不能达到效果如同增加像素那样丝滑的效果,但可以稍微做到点改善,比如边缘部分有的地方颜色会呈现不同程度的浅,以此来弱化这种锯齿感。具体的做法如下的示意图所示:
这里我们可以暂时缓一缓,将照相机也定义成一个类,取名为camera.h。其中构造函数就主要还是视口定义,以及寻找左下角的点。然后还需要一个获取光线的函数,就是以照相机的位置为起点,图中点的位置与摄像机的连线为方向的光线。
#ifndef CAMERA_H
#define CAMERA_H
#include "rtweekend.h"
class camera {
public:
camera() {
auto aspect_ratio = 16.0 / 9.0;
auto viewport_height = 2.0;
auto viewport_width = aspect_ratio * viewport_height;
auto focal_length = 1.0;
origin = point3(0, 0, 0);
horizontal = vec3(viewport_width, 0.0, 0.0);
vertical = vec3(0.0, viewport_height, 0.0);
lower_left_corner = origin - horizontal/2 - vertical/2 - vec3(0, 0, focal_length);
}
ray get_ray(double u, double v) const {
return ray(origin, lower_left_corner + u*horizontal + v*vertical - origin);
}
private:
point3 origin;
point3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
};
#endif
为了防止越界,我们需要一个函数对其进行限制,当超过时就返回最大值,小于时就返回最小值,这个函数就放入工具包中。
inline double clamp(double x, double min, double max) {
if (x < min) return min;
if (x > max) return max;
return x;
}
write_color函数更新的主要地方在于它将得到的该像素点的rgb值除以了采样点个数,求了个平均,得到该点真正的色彩。
void write_color(std::ostream &out, color pixel_color, int samples_per_pixel) {
auto r = pixel_color.x();
auto g = pixel_color.y();
auto b = pixel_color.z();
// Divide the color by the number of samples.
auto scale = 1.0 / samples_per_pixel;
r *= scale;
g *= scale;
b *= scale;
// Write the translated [0,255] value of each color component.
out << static_cast<int>(256 * clamp(r, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(g, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(b, 0.0, 0.999)) << '\n';
}
最后是main函数,我们更新一下,在图像的时候多加了一个循环,循环次数是 samples_per_pixel,这个表示我们将一个像素划分了多少个采样点。在循环中我们也采用我们之前写的随机函数,用于生成一个[0, 1)之间的数,这样就可以表示我们在这一个像素所组成的微小正方形中进行一个采样,之后就是获取一个从摄像机到该点的光线。遍历完一个像素的采样点后传入write_color函数写入计算后的该点真正的像素点。
#include "camera.h"
...
int main() {
// Image
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
const int samples_per_pixel = 100;
// World
hittable_list world;
world.add(make_shared<sphere>(point3(0,0,-1), 0.5));
world.add(make_shared<sphere>(point3(0,-100.5,-1), 100));
// Camera
camera cam;
// Render
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n";
for (int j = image_height-1; j >= 0; --j) {
std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;
for (int i = 0; i < image_width; ++i) {
color pixel_color(0, 0, 0);
for (int s = 0; s < samples_per_pixel; ++s) {
auto u = (i + random_double()) / (image_width-1);
auto v = (j + random_double()) / (image_height-1);
ray r = cam.get_ray(u, v);
pixel_color += ray_color(r, world);
}
write_color(std::cout, pixel_color, samples_per_pixel);
}
}
std::cerr << "\nDone.\n";
}
最后效果如下所示,这是局部放大的效果,左边是没做抗锯齿,右边是做了抗锯齿的结果。
漫射材料
从这里开始,我们逐渐步入我们的正题——光线追踪,所谓的光线追踪说白了就是我们模拟光的运行轨迹,光沿着什么方向,打到什么物体,发生了什么反射或者折射都模拟出来。
漫反射
现在我们有了每个像素的多条射线,我们可以做一些逼真的物体,比如一些看上去表面粗糙的物体,这种物体上发生了光的漫反射。所谓的漫反射就是光在粗糙表面进行的反射。
漫反射的物体本身是不发光的,它们只是呈现出周围环境的颜色,然后用自身的颜色进行调解,当光打到这些物体上的时候,光的反射方向是随机的,其示意图如下所示。
那么我们如何描绘出这样一种现象呢?下面我们来详细进行一下分析
首先我们沿着摄像机的光线,然后光线会打到物体,我们也因此得到了一个交点,之后再这个交点上,在这个交点上我们理应还会得到一个法线向量,其是 n ⃗ \vec{n} n,因此我们可以顺着这个法线得到一个球心是 p + n ⃗ p+\vec{n} p+n,我们顺着这个球心形成一个单位球,在这个单位球中随机生成一个点 s ,最后我们连接 s 与 p会得到一个方向向量,我们将这个方向向量就作为我们光线经过该点发生漫反射后的方向。然后以交点 p 为源,方向为 s ⃗ − p ⃗ \vec{s}-\vec{p} s−p得到一个新的光线,再以该光线的轨迹进行下一次相交与反射,如此反复便可以模拟光线与物体之间发生漫反射的结果。
该过程的示意图如下所示。
首先我们整理一下思路,我们首先需要以 s ⃗ − p ⃗ \vec{s}-\vec{p} s−p 为球心,创建一个单位球,并且随机生成一个点位于这个单位球中。但是这样做说实话,有点麻烦,光是以特定点为球心创建单位球的过程就很麻烦,那我们不妨换个思路,我们最终的目的是要求 s ⃗ − p ⃗ \vec{s}-\vec{p} s−p,运用向量的性质,我们可以求得 p + n ⃗ + [ s ⃗ − ( p + n ⃗ ) ] p + \vec{n} + [\vec{s}-(p + \vec{n})] p+n+[s−(p+n)],而 s ⃗ − ( p + n ⃗ ) \vec{s}-(p + \vec{n}) s−(p+n) 其实就是一个随机点到球心的向量,而我们知道,向量在移动的过程中是不会变的,因此我们可以将问题简化成一个单位球内求一个随机点,然后连接随机点和原点即可得到。
我们在vec3中定义一下产生随机向量的函数
class vec3 {
public:
...
inline static vec3 random() {
return vec3(random_double(), random_double(), random_double());
}
inline static vec3 random(double min, double max) {
return vec3(random_double(min,max), random_double(min,max), random_double(min,max));
}
然后我们定义单位球中的随机点,过程大致就会一直循环,直到我们产生的随机三维向量的模式小于1的,这时返回这个点。
vec3 random_in_unit_sphere() {
while (true) {
auto p = vec3::random(-1,1);
if (p.length_squared() >= 1) continue;
return p;
}
}
之后我们更新一下返回光线颜色的函数,将其变为递归函数,设置一下递归的重点,即我们设定的dept变量,当我们的depth变为0的时候,返回(0, 0, 0),或者当反射的光线没有遇到物体,返回环境的颜色。
color ray_color(const ray& r, const hittable& world, int depth) {
hit_record rec;
// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return color(0,0,0);
if (world.hit(r, 0, infinity, rec)) {
point3 target = rec.p + rec.normal + random_in_unit_sphere();
return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1);
}
vec3 unit_direction = unit_vector(r.direction());
auto t = 0.5*(unit_direction.y() + 1.0);
return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}
最后是main函数,这里主要是增加了最大深度,即光线反射的最大次数,并且修改了ray_color函数的调用。
int main() {
// Image
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
const int samples_per_pixel = 100;
const int max_depth = 50;
...
// Render
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n";
for (int j = image_height-1; j >= 0; --j) {
std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;
for (int i = 0; i < image_width; ++i) {
color pixel_color(0, 0, 0);
for (int s = 0; s < samples_per_pixel; ++s) {
auto u = (i + random_double()) / (image_width-1);
auto v = (j + random_double()) / (image_height-1);
ray r = cam.get_ray(u, v);
pixel_color += ray_color(r, world, max_depth);
}
write_color(std::cout, pixel_color, samples_per_pixel);
}
}
std::cerr << "\nDone.\n";
}
效果如下所示,我们可以看到整个物体看上去有了粗糙感,并且有了阴影。
用伽马校正颜色强度
我们可以看到,上面的效果看起来球十分的黯淡,这是由于我们显示的并非是真实的值,事实上这个物体应该是一种亮灰色,举个例子,假如你计算机存储的亮度是0.5(亮度范围是0~1),CRT显示器的输出亮度并不是0.5,而是约等于 0.218,这时我们需要进行伽马校正,将显示的结果修正为真实的颜色。这个伽马值一般是2.2,为了渐变,我们这时就直接用2作为伽马值,也就是我们的色彩要从原来的 I 变成 x \sqrt x x ,即原来的1/2次方。因此我们需要找到我们的write_color函数,将rgb值修改为开根号的形式。
void write_color(std::ostream &out, color pixel_color, int samples_per_pixel) {
auto r = pixel_color.x();
auto g = pixel_color.y();
auto b = pixel_color.z();
// Divide the color by the number of samples and gamma-correct for gamma=2.0.
auto scale = 1.0 / samples_per_pixel;
r = sqrt(scale * r);
g = sqrt(scale * g);
b = sqrt(scale * b);
// Write the translated [0,255] value of each color component.
out << static_cast<int>(256 * clamp(r, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(g, 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(b, 0.0, 0.999)) << '\n';
}
校正后的结果如下所示:
最后我们还可以解决一下阴影失真的问题,即我们可以在调用物体的 hit 函数的时候,将 t_min 从0改成一个接近0但大于0的值。
if (world.hit(r, 0.001, infinity, rec))
兰伯特反射
这部分没咋看懂,有懂得读者可以私信或者评论区讨论,但是这部分的操作是简单的,就是将我们上面求到的单位球内的随机向量,做一个标准化,变成单位球上的随机向量,示意图和代码如下所示:
inline vec3 random_in_unit_sphere() {
...
}
vec3 random_unit_vector() {
return unit_vector(random_in_unit_sphere());
}
color ray_color(const ray& r, const hittable& world, int depth) {
hit_record rec;
// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return color(0,0,0);
if (world.hit(r, 0.001, infinity, rec)) {
point3 target = rec.p + rec.normal + random_unit_vector(); // 改动在此处
return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1);
}
vec3 unit_direction = unit_vector(r.direction());
auto t = 0.5*(unit_direction.y() + 1.0);
return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}
最终效果如下所示,与之前的没有太大的区别,硬说区别的话就是阴影更浅了,比如球体的正下方那块,比较明显上一小节的阴影部分会更加深一点,还有一个不太明显的就是两个球体都变得更浅了。
半球形散射
说实话,这部分博主还是看的比较懵,之后等我了解的更多后会将这部分内容再补上吧,这里的具体做法就是我们在单位球内不是随机生成一个向量嘛,然后我们筛选出与我们的法向量同一个半球的,也就是两个向量点乘的值大于0的,否则就取反,反正最后的结果一定是落到与法向量同一半球的向量。
vec3 random_in_hemisphere(const vec3& normal) {
vec3 in_unit_sphere = random_in_unit_sphere();
if (dot(in_unit_sphere, normal) > 0.0) // In the same hemisphere as the normal
return in_unit_sphere;
else
return -in_unit_sphere;
}
color ray_color(const ray& r, const hittable& world, int depth) {
hit_record rec;
// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return color(0,0,0);
if (world.hit(r, 0.001, infinity, rec)) {
point3 target = rec.p + random_in_hemisphere(rec.normal); // 改动的地方
return 0.5 * ray_color(ray(rec.p, target - rec.p), world, depth-1);
}
vec3 unit_direction = unit_vector(r.direction());
auto t = 0.5*(unit_direction.y() + 1.0);
return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}
结果如下所示,我们可以看到,由于我们只保留了与法向量同一个半球的随机向量,因此阴影部分很明显变得很浅。
金属材料
材料的抽象类
我们上面的是漫反射的材料,现在我们要创建一个由金属感的材料,这是两种截然不同的材质类型,但它们有一个共同的地方,那就是都是物体的材质,那么我们自然可以创建一个材料的抽象类,然后生成子类用于产生不同的材料。
这个材料的抽象类中需要满足的条件是:首先可以产生一个散射光线,其次是如果发生了散射,光线衰减了多少。据此,我们在其中设计了一个虚函数,参数主要是入射光线,交点,光线吸收率,以及散射光线。
#ifndef MATERIAL_H
#define MATERIAL_H
#include "rtweekend.h"
struct hit_record;
class material {
public:
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const = 0;
};
#endif
既然我们已经定义了材料类,而材料又是物体的性质,于是我们可以在hit_record中增加一个属性,叫做物体的材质,同样我们用一个智能指针指向这个类。这样当我们的光线打到这个物体的时候,我们可以根据我们原本赋予这个物体的材质设置该交点的材质,从而打包进行下一步操作。
#include "rtweekend.h"
class material;
struct hit_record {
point3 p;
vec3 normal;
shared_ptr<material> mat_ptr;
double t;
bool front_face;
inline void set_face_normal(const ray& r, const vec3& outward_normal) {
front_face = dot(r.direction(), outward_normal) < 0;
normal = front_face ? outward_normal :-outward_normal;
}
};
与此同时,我们在我们的物体类中也需要增加这样一个材质属性,并且在寻找到交点的时候将材质属性赋给该交点。
class sphere : public hittable {
public:
sphere() {
}
sphere(point3 cen, double r, shared_ptr<material> m) // 构造函数增加了材质
: center(cen), radius(r), mat_ptr(m) {
};
virtual bool hit(
const ray& r, double t_min, double t_max, hit_record& rec) const override;
public:
point3 center;
double radius;
shared_ptr<material> mat_ptr; // 增加材质属性
};
bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const {
...
rec.t = root;
rec.p = r.at(rec.t);
vec3 outward_normal = (rec.p - center) / radius;
rec.set_face_normal(r, outward_normal);
rec.mat_ptr = mat_ptr; // 将物体的材质赋给交点
return true;
}
兰伯特反射类
既然我们已经有了材料类,那么我们上一节漫反射中提到的兰伯特反射就可以继承材料类,然后重写scatter函数,生成散射光线,在这个类中,我们可以定义一个反射率,即光线的衰减,在之前我们都是很简单粗暴的设定为0.5。
当然在此之前,我们还有一点需要注意,那就是我们在生成随机单位向量的时候,如果恰好与我们的法线向量相反,那么在相加的时候就会相互抵消从而变为0,这样就以为这没有光线发生散射,后面也将出现很大的问题(出现无穷或者NAN)。鉴于判断一个向量是否会是0是向量的特征,因此我们可以在vec3类中判断一下该向量是否为接近0的向量。判断的方法如下所示。
class vec3 {
...
bool near_zero() const {
// Return true if the vector is close to zero in all dimensions.
const auto s = 1e-8;
return (fabs(e[0]) < s) && (fabs(e[1]) < s) && (fabs(e[2]) < s);
}
...
};
以下是定义兰伯特反射的类,继承了材料类,成员函数就是它的反射率,光线的衰减就是这个折射率。其他的都是和我们之前的内容保持一致。
class lambertian : public material {
public:
lambertian(const color& a) : albedo(a) {
}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override {
auto scatter_direction = rec.normal + random_unit_vector();
// Catch degenerate scatter direction
if (scatter_direction.near_zero())
scatter_direction = rec.normal;
scattered = ray(rec.p, scatter_direction);
attenuation = albedo;
return true;
}
public:
color albedo;
};
镜面光反射
我们知道,金属有一定的镜面光反射,简单来说就是能够在自己的表面反映一部分环境的物体和颜色。那么我们如何来实现这一效果呢?
相较于漫反射,这种镜面光反射是不会发生随机散射的。那么我们反射的光线如何计算呢,我们可以看到下面的示意图。我们可以看出来,我们要求的红色向量就是反射向量,它应该是 v ⃗ + 2 B ⃗ \vec{v}+2\vec{B} v+2B。
下面我们来详细分析一下如何求,这部分原书中只是一笔带过,所以下面是博主自己的理解。在如下的示意图中,我们的入射光线是 v ⃗ \vec{v} v,法线向量是 n ⃗ \vec{n} n,反射光线是 v ⃗ ′ \vec{v}' v′。我们知道, n ⃗ \vec{n} n 是单位向量,这个使我们处理过的,但是 v ⃗ \vec{v} v不是。由于光线的入射角与反射角相等,因此应该满足关系 − 2 ( v ⃗ ⋅ n ⃗ ) n ⃗ = − v ⃗ + v ⃗ ′ -2(\vec{v}·\vec{n}) \vec{n}= -\vec{v} +\vec{v}' −2(v⋅n)n=−v+v′,因此 − 2 ( v ⃗ ⋅ n ⃗ ) n ⃗ + v ⃗ = v ⃗ ′ -2(\vec{v}·\vec{n}) \vec{n} + \vec{v} = \vec{v}' −2(v⋅n)n+v=v′。
这里解释一下 − 2 ( v ⃗ ⋅ n ⃗ ) n ⃗ -2(\vec{v}·\vec{n}) \vec{n} −2(v⋅n)n,根据四边形法则,我们知道 − v ⃗ + v ⃗ ′ -\vec{v} +\vec{v}' −v+v′会得到对角线的向量,但是 n ⃗ \vec{n} n是一个单位向量,我们要得到正确的结果的话就应该计算 v ⃗ \vec{v} v在 n ⃗ \vec{n} n方向上的投影大小,再乘以2倍就得到对角线的大小,然后再乘以 n ⃗ \vec{n} n就得到对角线向量。而最前面的那个负号是因为 v ⃗ \vec{v} v是向里的,我们计算的角度是(π - α \alpha α ),而cos(π - α \alpha α ) = -cos( α \alpha α) ,但是我们只需要正值,所以加个负号抵消一下。
综上,我们可以在vec3.h中添加一个反射函数,返回的向量就是我们上面分析的 − 2 ( v ⃗ ⋅ n ⃗ ) n ⃗ + v ⃗ -2(\vec{v}·\vec{n}) \vec{n} + \vec{v} −2(v⋅n)n+v。
vec3 reflect(const vec3& v, const vec3& n) {
return v - 2*dot(v,n)*n;
}
接下来我们就定义一个金属类,继承自材料类,大致和兰伯特反射类一直,只不过反射从漫反射变成了镜面反射。此外在返回的结果中,我们也要保证我们的反射结果和交点的法线是在同一个半球上,即两个向量相乘的结果大于0。
class metal : public material {
public:
metal(const color& a) : albedo(a) {
}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override {
vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal);
scattered = ray(rec.p, reflected);
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}
public:
color albedo;
};
color ray_color(const ray& r, const hittable& world, int depth) {
hit_record rec;
// If we've exceeded the ray bounce limit, no more light is gathered.
if (depth <= 0)
return color(0,0,0);
if (world.hit(r, 0.001, infinity, rec)) {
ray scattered;
color attenuation;
if (rec.mat_ptr->scatter(r, rec, attenuation, scattered)) // 此处更改
return attenuation * ray_color(scattered, world, depth-1);
return color(0,0,0);
}
vec3 unit_direction = unit_vector(r.direction());
auto t = 0.5*(unit_direction.y() + 1.0);
return (1.0-t)*color(1.0, 1.0, 1.0) + t*color(0.5, 0.7, 1.0);
}
...
#include "material.h"
...
int main() {
// Image
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
const int samples_per_pixel = 100;
const int max_depth = 50;
// World
hittable_list world;
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0)); // 这里修改,添加了物体和材质,参数是颜色
auto material_center = make_shared<lambertian>(color(0.7, 0.3, 0.3));
auto material_left = make_shared<metal>(color(0.8, 0.8, 0.8));
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2));
world.add(make_shared<sphere>(point3( 0.0, -100.5, -1.0), 100.0, material_ground));
world.add(make_shared<sphere>(point3( 0.0, 0.0, -1.0), 0.5, material_center));
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.5, material_left));
world.add(make_shared<sphere>(point3( 1.0, 0.0, -1.0), 0.5, material_right));
// Camera
camera cam;
// Render
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n";
for (int j = image_height-1; j >= 0; --j) {
std::cerr << "\rScanlines remaining: " << j << ' ' << std::flush;
for (int i = 0; i < image_width; ++i) {
color pixel_color(0, 0, 0);
for (int s = 0; s < samples_per_pixel; ++s) {
auto u = (i + random_double()) / (image_width-1);
auto v = (j + random_double()) / (image_height-1);
ray r = cam.get_ray(u, v);
pixel_color += ray_color(r, world, max_depth);
}
write_color(std::cout, pixel_color, samples_per_pixel);
}
}
std::cerr << "\nDone.\n";
}
下面使我们的结果,左右两边是两个镜面反射的球,中间和下面是漫反射的球,我们可以看到镜面反射上可以看到其他物体。
模糊反射
我们可以给这个金属加点磨砂质感,也就是加点模糊,加的方式同样是在反射上加个单位球,然后在其中球随机点,连接交点和随机点形成新的反射光线,示意图如下所示:
我们还可以加个模糊参数,用于控制模糊度
class metal : public material {
public:
metal(const color& a, double f) : albedo(a), fuzz(f < 1 ? f : 1) {
} // 修改构造函数
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override {
vec3 reflected = reflect(unit_vector(r_in.direction()), rec.normal);
scattered = ray(rec.p, reflected + fuzz*random_in_unit_sphere()); // 添加随机向量
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}
public:
color albedo;
double fuzz; // 添加模糊度参数
};
int main() {
...
// World
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<lambertian>(color(0.7, 0.3, 0.3));
auto material_left = make_shared<metal>(color(0.8, 0.8, 0.8), 0.3);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);
...
}
最终的效果如下所示,多了一层磨砂质感
总结
这部分主要就是抗锯齿与光线追踪,抗锯齿简单来说就是人为提高采样率,让一个像素内尽可能多采点颜色值。光线追踪部分就是模拟光线的路径,然后我们还模拟了漫反射与镜面反射,定义了两个物体,两个为漫反射,两个为镜面反射。至此,本系列接近尾声,计划还有最后一期结束,感谢观看。
参考文献
[1] Ray Tracing in One Weekend
[2] C++11实践指南(2.重要特性-更强大的功能:正则表达式;计时工具;随机分布生成器)
[3] C++ STL-- mt19937
[4] C++11 随机数学习
[5] 伽马校正(Gamma Correction)