

image.png
本文基于《Ray Tracing One Weekend》该文章对于光线追踪的基本原理进行实现,并对里面的一些代码和原理做细微修改,最终得到上面这幅由光线追踪得到的图像。
首先我们来思考如何存储我们的光线追踪结果。自然是使用图像来存储。这里我们使用一种非常简单的图像格式:ppm格式。对于ppm图像文件,我们只需要在文件头标明 格式型号 和 图像的宽高,然后在里面按照一个像素一行,每一行写入三个0~255的数值,分布对应 RGB值即可。 我们这里创建一个 空的 C++ 的项目,来编写代码,输出一个 ppm文件的数据。
[main.cpp]
#include <iostream>
int main(int argc, char* argv[])
{
// Image Settings
const int image_width = 256;
const int image_height = 256;
// Render
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n"; // ppm文件头
for (int j = image_height - 1; j >= 0; --j)
{
for (int i = 0; i < image_width; ++i)
{
auto r = double(i) / (image_width - 1);
auto g = double(j) / (image_height - 1);
auto b = 0.25;
int ir = static_cast<int>(255.999 * r);
int ig = static_cast<int>(255.999 * g);
int ib = static_cast<int>(255.999 * b);
std::cout << ir << ' ' << ig << ' ' << ib << '\n';
}
}
return 0;
} 编译生成后,我们可以在命令行窗口中,通过链接符来创建一个.ppm文件,并把程序的输出写入进去:

image.png
得到ppm图像:

image.png
其文件内容格式:

image.png
接下来我们再在控制台输出些额外信息,来让我们能够看到当前图像渲染的进度。由于我们已经使用 std::cout 来把内容输出到 ppm文件中,因此为了避免把这些额外信息也写入文件,我们这里使用 std::cerr 来输出额外信息到控制台:
[main.cpp]
#include <iostream>
int main(int argc, char* argv[])
{
// Image Settings
const int image_width = 256;
const int image_height = 256;
// Render
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n"; // ppm文件头
clock_t start_time = clock();
for (int j = image_height - 1; j >= 0; --j)
{
clock_t dual_time = clock();
std::cerr << "\rScanlines remaining: " << j << ' ' << "The run time is: " << (double)(dual_time - start_time) / CLOCKS_PER_SEC << "s" << " " << std::flush;
for (int i = 0; i < image_width; ++i)
{
auto r = double(i) / (image_width - 1);
auto g = double(j) / (image_height - 1);
auto b = 0.25;
int ir = static_cast<int>(255.999 * r);
int ig = static_cast<int>(255.999 * g);
int ib = static_cast<int>(255.999 * b);
std::cout << ir << ' ' << ig << ' ' << ib << '\n';
}
}
std::cerr << "\nDone.\n";
return 0;
}

image.png
几乎所有图形程序都会有一个向量类,来表示点、向量 和 颜色,它们通常是四维向量。但由于我们这个简易的光追渲染器中不会牵扯到矩阵运算,因此使用一个三维向量即可。
[vec3.h]
#ifndef VEC3_H
#define VEC3_H
#include <cmath>
#include <iostream>
using std::sqrt;
class vec3
{
public:
double e[3];
public:
vec3() : e{ 0,0,0 } {}
vec3(double e0, double e1, double e2) : e{ e0, e1, e2 } {}
double x() const { return e[0]; }
double y() const { return e[1]; }
double z() const { return e[2]; }
vec3 operator-() const { return vec3(-e[0], -e[1], -e[2]); }
double operator[](int i) const { return e[i]; }
double& operator[](int i) { return e[i]; }
vec3& operator+=(const vec3& v)
{
e[0] += v.e[0];
e[1] += v.e[1];
e[2] += v.e[2];
return *this;
}
vec3& operator*=(const double t)
{
e[0] *= t;
e[1] *= t;
e[2] *= t;
return *this;
}
vec3& operator/=(const double t)
{
return *this *= 1 / t;
}
double length() const
{
return sqrt(length_squared());
}
double length_squared() const
{
return e[0] * e[0] + e[1] * e[1] + e[2] * e[2];
}
};
// Type aliases for vec3
using point3 = vec3; // 3D point
using color = vec3; // RGB color
#endif 接下来再实现一些向量的基本运算,如 加减乘除 和 点乘、叉乘、单位化:
[vec3.h]
inline std::ostream& operator<<(std::ostream& out, const vec3& v)
{
return out << v.e[0] << ' ' << v.e[1] << ' ' << v.e[2];
}
inline vec3 operator+(const vec3& u, const vec3& v)
{
return vec3(u.e[0] + v.e[0], u.e[1] + v.e[1], u.e[2] + v.e[2]);
}
inline vec3 operator-(const vec3& u, const vec3& v)
{
return vec3(u.e[0] - v.e[0], u.e[1] - v.e[1], u.e[2] - v.e[2]);
}
inline vec3 operator*(const vec3& u, const vec3& v)
{
return vec3(u.e[0] * v.e[0], u.e[1] * v.e[1], u.e[2] * v.e[2]);
}
inline vec3 operator*(double t, const vec3& v)
{
return vec3(t * v.e[0], t * v.e[1], t * v.e[2]);
}
inline vec3 operator*(const vec3& v, double t)
{
return t * v;
}
inline vec3 operator/(vec3 v, double t)
{
return (1 / t) * v;
}
inline double dot(const vec3& u, const vec3& v)
{
return u.e[0] * v.e[0]
+ u.e[1] * v.e[1]
+ u.e[2] * v.e[2];
}
inline vec3 cross(const vec3& u, const vec3& v)
{
return vec3(u.e[1] * v.e[2] - u.e[2] * v.e[1],
u.e[2] * v.e[0] - u.e[0] * v.e[2],
u.e[0] * v.e[1] - u.e[1] * v.e[0]);
}
inline vec3 unit_vector(vec3 v)
{
return v / v.length();
} 对于颜色,我们再单独创建一个方法,用于把颜色输出到 ppm文件中:
[color.h]
#ifndef COLOR_H
#define COLOR_H
#include "vec3.h"
#include <iostream>
void write_color(std::ostream& out, color pixel_color)
{
// Write the translated [0,255] value of each color component.
out << static_cast<int>(255.999 * pixel_color.x()) << ' '
<< static_cast<int>(255.999 * pixel_color.y()) << ' '
<< static_cast<int>(255.999 * pixel_color.z()) << '\n';
}
#endif 相应的 main 做出修改:
[main.cpp]
#include "vec3.h"
#include "color.h"
#include <iostream>
int main(int argc, char* argv[])
{
// Image Settings
const int image_width = 256;
const int image_height = 256;
// Render
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n"; // ppm文件头
clock_t start_time = clock();
for (int j = image_height - 1; j >= 0; --j)
{
clock_t dual_time = clock();
std::cerr << "\rScanlines remaining: " << j << ' ' << "The run time is: " << (double)(dual_time - start_time) / CLOCKS_PER_SEC << "s" << " " << std::flush;
for (int i = 0; i < image_width; ++i)
{
color pixel_color = color(double(i) / (image_width - 1), double(j) / (image_height - 1), 0.25);
write_color(std::cout, pixel_color);
}
}
std::cerr << "\nDone.\n";
return 0;
} 首先我们来看现实世界我们人眼是如何看到世界的图像的:

image.png
首先一束光经由 光路1,射到了 A物体表面,一部分能量被 A吸收,另外一部分进行反射,经由 光路2 射到了 B物体表面,同样一部分能量被 B吸收,另一部分进行反射,经由 光路3 射到了 C物体,C物体吸收一部分能量,剩下的能量经由 光路4 射入了人眼。 那么该束光在 C物体上形成的颜色,就最终由 光路4 进入了人眼。 那么由于光路是可逆的,因此我们可以直接从人眼 沿光路4 发射射线,且设该射线的能量为1个单位,那么射线到 C 会被吸收一部分能量,再沿 光路3 到 B 被吸收一部分能量,再沿 光路2 到 A 被吸收一部分能量,最后沿 光路1 到达光源,那么最后乘上光源的能量,那么最终仍然会得到 该束光在人眼中形成的 C物体上的颜色。 那么再考虑现实情况,由于太阳到达地球的能量大部分被大气层吸收,然后由大气层辐射到地面,因此整个大气层可以看作一个光源。那么如果我们按照正向的光路去做光线追踪,那么可想而知从大气层发射射线,需要的射线数量是异常庞大的,且这些射线最终不一定会都射入人眼,因为人眼无法看到全景范围,也无法看到自己脚下地球的另一面。 因此我们选用第二种方式进行光线追踪,且该方式还有个好处在,如果我们想直到 某点的渲染结果,那么直接往该点打射线即可,非常的直接。
接下来我们来实现 ray 类,来表示射线。这里我们使用 P(t)=A+tb 这个射线表达式,其中 P 是射线上的一点,A 是射线的起点,b 是射线的方向向量,t 是射线起点A到一点P的距离。

image.png
[ray.h]
#ifndef RAY_H
#define RAY_H
#include "vec3.h"
class ray
{
public:
point3 orig;
vec3 dir;
public:
ray() {}
ray(const point3& origin, const vec3& direction)
: orig(origin), dir(direction)
{
}
point3 origin() const { return orig; }
vec3 direction() const { return dir; }
point3 at(double t) const
{
return orig + t * dir;
}
};
#endif 接下来我们设置一个简易相机,并通过该相机向屏幕像素发射射线。
这里我们不再单独设置屏幕的宽高,也即我们图像的宽高,而是设置一个纵横比,然后只设置屏幕的宽,这样就可以计算出屏幕的高度。 对于相机我们通常还会抽象出一个视口的概念。这样的好处在于,不论图像的宽高是多少,只要其纵横比与相机视口的纵横比相同,那么这个相机就可以用来渲染所有这些图像。 这里我们将视口高度设置为2(-1~1),视口据相机1个距离:

image.png
接下来我们就可以通过这个相机向屏幕发射射线,并且这里我先根据射线的方向来输出一个背景颜色,我们可以先认为这个背景颜色就是大气层颜色。
[main.cpp]
#include "vec3.h"
#include "color.h"
#include "ray.h"
#include <iostream>
color ray_color(const ray& r)
{
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);
}
int main(int argc, char* argv[])
{
// Image Settings
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
// Camera
auto viewport_height = 2.0;
auto viewport_width = aspect_ratio * viewport_height;
auto focal_length = 1.0;
auto origin = point3(0, 0, 0);
auto horizontal = vec3(viewport_width, 0, 0);
auto vertical = vec3(0, viewport_height, 0);
auto lower_left_corner = origin - horizontal * 0.5 - vertical * 0.5 - vec3(0, 0, focal_length);
// Render
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n"; // ppm文件头
clock_t start_time = clock();
for (int j = image_height - 1; j >= 0; --j)
{
clock_t dual_time = clock();
std::cerr << "\rScanlines remaining: " << j << ' ' << "The run time is: " << (double)(dual_time - start_time) / CLOCKS_PER_SEC << "s" << " " << std::flush;
for (int i = 0; i < image_width; ++i)
{
auto u = double(i) / (image_width - 1);
auto v = double(j) / (image_height - 1);
ray r(origin, lower_left_corner + u * horizontal + v * vertical - origin);
color pixel_color = ray_color(r);
write_color(std::cout, pixel_color);
}
}
std::cerr << "\nDone.\n";
return 0;
} 得到背景图:

image.png
当我们往场景中添加几何时,最关心的是该如何计算我们的射线与该几何的相交情况。对于球体,我们知道球体表面的公式:x2+y2+z2=R2 那么如果给定点(x, y, z) 有 x2+y2+z2<R2则说明该点在球内,如果有 x2+y2+z2>R2则说明该点在球外。 我们设 射线上一点 P(t) 到到达了球的表面,可得: (P(t)−C)⋅(P(t)−C)=r2 其中 C 是球的球心点。 将 P(t) 展开得: (A+tb−C)⋅(A+tb−C)=r2 再展开得: t2b⋅b+22b⋅(A−C)+(A−C)⋅(A−C)−r2=0 那么我们就可以通过求根公式来得到射线与球的相交情况了 如果该方程解得没有根,就说明该射线不与该球相交,有一个根则说明相切,两个根则说明相交

image.png
接下来用代码表示这些:
[main.cpp]
bool hit_sphere(const point3& center, double radius, const ray& r)
{
vec3 oc = r.origin() - center;
auto a = dot(r.direction(), r.direction());
auto b = 2.0 * dot(oc, r.direction());
auto c = dot(oc, oc) - radius * radius;
auto discriminant = b * b - 4 * a * c;
return (discriminant > 0);
}
color ray_color(const ray& r)
{
if (hit_sphere(point3(0, 0, -1), 0.5, r))
{
return color(1, 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);
} 这里我们放置了一个球心在 (0, 0, -1) 半径为 0.5 的球,得到图像:

image.png
接下来我们求解出 t,这样就可以得到射线在球上的相交点,并且可以计算出球的法线并输出:
[main.cpp]
doubule hit_sphere(const point3& center, double radius, const ray& r)
{
vec3 oc = r.origin() - center;
auto a = dot(r.direction(), r.direction());
auto b = 2.0 * dot(oc, r.direction());
auto c = dot(oc, oc) - radius * radius;
auto discriminant = b * b - 4 * a * c;
if (discriminant < 0)
{
return -1.0;
}
else
{
return (-b - sqrt(discriminant)) / (2.0 * a);
}
}
color ray_color(const ray& r)
{
point3 sphere_center = point3(0, 0, -1);
auto t = hit_sphere(sphere_center, 0.5, r);
if (t > 0.0)
{
vec3 N = unit_vector(r.at(t) - sphere_center);
return 0.5 * color(N.x() + 1, N.y() + 1, N.z() + 1);
}
vec3 unit_direction = unit_vector(r.direction());
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);
}

image.png
对于这里的求根公式我们可以进行简化,分子可以提取出一个2,与分母相抵消:
[main.cpp]
double hit_sphere(const point3& center, double radius, const ray& r)
{
vec3 oc = r.origin() - center;
auto a = r.direction().length_squared();
auto half_b = dot(oc, r.direction());
auto c = oc.length_squared() - radius * radius;
auto discriminant = half_b * half_b - a * c;
if (discriminant < 0)
{
return -1.0;
}
else
{
return (-half_b - sqrt(discriminant)) / a;
}
} 接下来我们对场景物体进行抽象,提取出一个 hittable 类,表示可与射线进行相交的物体:
[hittable]
#ifndef HITTABLE_H
#define HITTABLE_H
#include "ray.h"
struct hit_record {
point3 p;
vec3 normal;
double t;
};
class hittable {
public:
virtual bool hit(const ray& r, double t_min, double t_max, hit_record& rec) const = 0;
};
#endif hit 是物体与射线的相交函数,该函数还传入了 t_min,t_max,这里我们认为当 t_min < t < t_max 才发生了射线碰撞。 那么我们就可以得到球体的类:
[sphere.h]
#ifndef SPHERE_H
#define SPHERE_H
#include "hittable.h"
#include "vec3.h"
class sphere : public hittable
{
public:
point3 center;
double radius;
public:
sphere() {}
sphere(point3 cen, double r) : center(cen), radius(r) {};
virtual bool hit(
const ray& r, double t_min, double t_max, hit_record& rec) const override;
};
bool sphere::hit(const ray& r, double t_min, double t_max, hit_record& rec) const
{
vec3 oc = r.origin() - center;
auto a = r.direction().length_squared();
auto half_b = dot(oc, r.direction());
auto c = oc.length_squared() - radius * radius;
auto discriminant = half_b * half_b - a * c;
if (discriminant < 0) return false;
auto sqrtd = sqrt(discriminant);
// Find the nearest root that lies in the acceptable range.
auto root = (-half_b - sqrtd) / a;
if (root < t_min || t_max < root)
{
root = (-half_b + sqrtd) / a;
if (root < t_min || t_max < root)
return false;
}
rec.t = root;
rec.p = r.at(rec.t);
rec.normal = (rec.p - center) / radius;
return true;
}
#endif
通常对于几何体,其还有内、外两面。如果射线方向与当前法线的点乘<0,那么就可以认为当前射线射中的是物体的外表面,如果>0 则是内表面,那么此时法线要去反:

image.png
因此我们可以修改代码:
[hittable.h]
struct hit_record
{
point3 p;
vec3 normal;
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;
}
}; 把法线修正加入到球体中:
[sphere]
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);
return true;
} 接下来我们可以创建一个 hittable_list 类,来组装我们整个场景
[hittable_list.h]
#ifndef HITTABLE_LIST_H
#define HITTABLE_LIST_H
#include "hittable.h"
#include <memory>
#include <vector>
using std::shared_ptr;
using std::make_shared;
class hittable_list : public hittable
{
public:
std::vector<shared_ptr<hittable>> objects;
public:
hittable_list() {}
hittable_list(shared_ptr<hittable> object) { add(object); }
void clear() { objects.clear(); }
void add(shared_ptr<hittable> object) { objects.push_back(object); }
virtual bool hit(
const ray& r, double t_min, double t_max, hit_record& rec) const override;
};
bool hittable_list::hit(const ray& r, double t_min, double t_max, hit_record& rec) const
{
hit_record temp_rec;
bool hit_anything = false;
auto closest_so_far = t_max;
for (const auto& object : objects)
{
if (object->hit(r, t_min, closest_so_far, temp_rec))
{
hit_anything = true;
closest_so_far = temp_rec.t;
rec = temp_rec;
}
}
return hit_anything;
}
#endif 后面的一些运算我们会经常用到一些数学常数 和 工具类,因此我们可以先把它们都包含在一起,创建一个 common文件:
[common.h]
#ifndef COMMON_H
#define COMMON_H
#include <cmath>
#include <limits>
#include <memory>
// Usings
using std::shared_ptr;
using std::make_shared;
using std::sqrt;
// Constants
const double infinity = std::numeric_limits<double>::infinity();
const double pi = 3.1415926535897932385;
// Utility Functions
inline double degrees_to_radians(double degrees)
{
return degrees * pi / 180.0;
}
// Common Headers
#include "ray.h"
#include "vec3.h"
#endif main 同样需要做出修改:
[main.cpp]
#include "common.h"
#include "color.h"
#include "hittable_list.h"
#include "sphere.h"
#include <iostream>
color ray_color(const ray& r, hittable& world)
{
hit_record rec;
if (world.hit(r, 0, infinity, rec))
{
return 0.5 * (rec.normal + color(1, 1, 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);
}
int main(int argc, char* argv[])
{
// Image Settings
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 400;
const int image_height = static_cast<int>(image_width / aspect_ratio);
// 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
auto viewport_height = 2.0;
auto viewport_width = aspect_ratio * viewport_height;
auto focal_length = 1.0;
auto origin = point3(0, 0, 0);
auto horizontal = vec3(viewport_width, 0, 0);
auto vertical = vec3(0, viewport_height, 0);
auto lower_left_corner = origin - horizontal * 0.5 - vertical * 0.5 - vec3(0, 0, focal_length);
// Render
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n"; // ppm文件头
clock_t start_time = clock();
for (int j = image_height - 1; j >= 0; --j)
{
clock_t dual_time = clock();
std::cerr << "\rScanlines remaining: " << j << ' ' << "The run time is: " << (double)(dual_time - start_time) / CLOCKS_PER_SEC << "s" << " " << std::flush;
for (int i = 0; i < image_width; ++i)
{
auto u = double(i) / (image_width - 1);
auto v = double(j) / (image_height - 1);
ray r(origin, lower_left_corner + u * horizontal + v * vertical - origin);
color pixel_color = ray_color(r, world);
write_color(std::cout, pixel_color);
}
}
std::cerr << "\nDone.\n";
return 0;
} 得到的图像结果:

image.png
我们当前图像的边缘像素是非常硬的,没有柔和的过渡:

image.png
而实际上这些边缘像素的颜色应该是前景和后景的一个融合结果,从而形成一个从前景过渡到远景的一个渐变。 由于我们这里一个像素只发射一条射线,因此一个像素只会取一条射线的结果,并不存在混合。 所以如果我们想要得到一个柔和的结果,即要进行抗锯齿,那么我们就可以在一个像素内随机取多个点,然后再往每个随机点上发射射线,最后所有这些射线结果取平均。

image.png
我们可以在 common 文件中,加入取随机值的函数:
[common.h]
inline double random_double()
{
// Returns a random real in [0,1).
return rand() / (RAND_MAX + 1.0);
}
inline double random_double(double min, double max)
{
// Returns a random real in [min,max).
return min + (max - min) * random_double();
} 现在是时候抽象出我们的相机类了,并把之前main中的相机设置移到里面来:
[camera.h]
#ifndef CAMERA_H
#define CAMERA_H
#include "common.h"
class camera {
private:
point3 origin;
point3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
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);
}
};
#endif 由于现在我们得到的一个像素的颜色,其该像素内多个射线结果的叠加,因此要对 write_color 函数进行修改,其在写入像素颜色前,要对颜色进行平均。同时我们叠加平均后的结果可能会超过[0, 255]范围,因此可以在 common 中添加一个 clamp 函数:
[common.h]
inline double clamp(double x, double min, double max) {
if (x < min) return min;
if (x > max) return max;
return x;
} [color.h]
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 同样要进行修改:
[main.cpp]
#include "camera.h"
...
int main(int argc, char* argv[])
{
// Image Settings
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"; // ppm文件头
clock_t start_time = clock();
for (int j = image_height - 1; j >= 0; --j)
{
clock_t dual_time = clock();
std::cerr << "\rScanlines remaining: " << j << ' ' << "The run time is: " << (double)(dual_time - start_time) / CLOCKS_PER_SEC << "s" << " " << 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";
return 0;
} 得到图像:

image.png
光线追踪中的材质,其描述的主要为光线的散射反向 和 光线的能量变化 在经典的数学概念上,我们通常认为当光射到物体表面后,其会根据表面法线进行镜面反射:

image.png
但这其实是对于理想的光滑表面而言。对于现实中的一般物体,其整体表面是由许多的微表面构成的,而大量光束射向这些微表面,其散射结果则显得很有随机性:

image.png

image.png
那么对散射方向的选取,就成为了一个概率问题 我们这样子选取散射方向:

在表面沿法线做一个相切的单位球,然后在该单位球表面随机选取一点,该点与切点p的连线即为散射方向。 这样的选取方式在概率学上被证明,散射方向v被选取的概率是其与法线n夹角的余弦成正比例的,即符合lambert模型。 我们现在 vec3 文件中创建出获取随机点的函数:
[vec3.h]
class vec3
{
public:
double e[3];
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));
}
}; 然后是获取单位球内随机一点的方法:
[vec3.h]
inline vec3 random_in_unit_sphere()
{
while (true)
{
auto p = vec3::random(-1, 1);
if (p.length_squared() >= 1) continue;
return p;
}
} 而要取单位球表面一点,只需要对球内随机一点进行单位化:
[vec3.h]
inline vec3 random_unit_vector()
{
return unit_vector(random_in_unit_sphere());
} 而对于 ray_color 函数,我们的射线在击中物体表面后也不再只是输出表面法线了,而是让表面吸收一部分能量,剩下的能量按照散射方向继续进行碰撞检测:
[main.cpp]
color ray_color(const ray& r, hittable& world)
{
hit_record rec;
if (world.hit(r, 0, infinity, rec))
{
point3 target = rec.p + rec.normal + random_unit_vector();
return 0.5 * ray_color(ray(rec.p, target - rec.p), world);
}
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.cpp]
...
color ray_color(const ray& r, hittable& world, int depth)
{
if (depth <= 0)
{
return color(0, 0, 0);
}
hit_record rec;
if (world.hit(r, 0, 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);
}
int main(int argc, char* argv[])
{
// Image Settings
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"; // ppm文件头
clock_t start_time = clock();
for (int j = image_height - 1; j >= 0; --j)
{
clock_t dual_time = clock();
std::cerr << "\rScanlines remaining: " << j << ' ' << "The run time is: " << (double)(dual_time - start_time) / CLOCKS_PER_SEC << "s" << " " << 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";
return 0;
} 得到图像:

可以发现,我们代码中对于光能量的吸收为 0.5,即材质反照率为 0.5,而图像结果显然比我们想象中的要黑,这是因为我们需要在输出图像时进行gama矫正:
[color.h]
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 = sqrt(r * scale);
g = sqrt(g * scale);
b = sqrt(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';
} 得到图像:

这里还存在一个问题,表面散射的射线可能由于数值误差问题会在t=0时又击中表面,因此需要设置 t_min 为一个大于 0 的公差值,来避免该问题。该问题会导致阴影粉刺问题。
[main.cpp]
color ray_color(const ray& r, hittable& world, int depth)
{
if (depth <= 0)
{
return color(0, 0, 0);
}
hit_record rec;
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);
} 接下来让我创建新的材质。这也正是时候来抽象我们的材质类了。
[material.h]
#ifndef MATERIAL_H
#define MATERIAL_H
#include "common.h"
#include "hittable.h"
class material
{
public:
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const = 0;
};
#endif 我们抽象出了材质的 scatter 函数,该函数返回材质对光能量的吸收系数:attenuation 和 光的散射方向:scattered 让我们的 hit_record 也记录击中表面的材质:
[hittable.h]
#include "ray.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;
}
}; 我们可以将材质附着在几何体中:
[sphere.h]
#ifndef SPHERE_H
#define SPHERE_H
#include "hittable.h"
#include "vec3.h"
class sphere : public hittable
{
public:
point3 center;
double radius;
shared_ptr<material> mat_ptr;
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;
};
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;
}
#endif 接下来把前面lambert散射整合到lambert材质类中:
[material.h]
class lambertian : public material
{
public:
color albedo;
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();
scattered = ray(rec.p, unit_vector(scatter_direction));
attenuation = albedo;
return true;
}
}; 这我们把散射射线的方向给单位化了,之前是没有的,因为之前渲染部分并没有使用散射射线的t值,而在后面我们的渲染会与t相关,因此这里给方向进行单位化,来得到准确的 t 值。 这里还有一个问题,我们选取的散射向量可能是一个零向量,这里我们认为此时实际的向量方向为法线方向。首先添加一个判断零向量的函数:
[vec3.h]
...
class vec3
{
public:
double e[3];
public:
...
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);
}
...
};
...
#endif 然后修正散射向量:
[material.h]
class lambertian : public material
{
public:
color albedo;
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, unit_vector(scatter_direction));
attenuation = albedo;
return true;
}
}; 接下来实现我们的金属材质。

对于理想的光滑金属,其散射方向就是镜面反射方向,如上图即 b = -(v · n) * n,r = v + 2b
[vec3.h]
inline vec3 reflect(const vec3& v, const vec3& n)
{
return v - 2 * dot(v, n) * n;
} 那么就可以得到我们的金属材质类:
[material.h]
class metal : public material
{
public:
color albedo;
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, unit_vector(reflected));
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}
}; 我们还需要修改 ray_color:
[main.cpp]
color ray_color(const ray& r, hittable& world, int depth)
{
if (depth <= 0)
{
return color(0, 0, 0);
}
hit_record rec;
if (world.hit(r, 0.001, infinity, rec))
{
ray scatterd;
color attenuation;
if (rec.mat_ptr->scatter(r, rec, attenuation, scatterd))
{
return attenuation * ray_color(scatterd, 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);
} 在 main 函数中,我们创建更多的球体:
[main.cpp]
...
#include "material.h"
...
int main(int argc, char* argv[])
{
// Image Settings
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"; // ppm文件头
clock_t start_time = clock();
for (int j = image_height - 1; j >= 0; --j)
{
clock_t dual_time = clock();
std::cerr << "\rScanlines remaining: " << j << ' ' << "The run time is: " << (double)(dual_time - start_time) / CLOCKS_PER_SEC << "s" << " " << 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";
return 0;
}
得到图像:

实际上理想光滑的金属并不存在,其散射方向是一个在镜面反射方向附近的一个随机向量。其越粗糙,这个范围就越大。 因此我们可以设置一个模糊值 fuzz,对反射方向进行一个偏移:r = r + fuzz * random_unit_vector() 这也 fuzz 越大,我们的随机偏移向量就越大,得到的结果就越模糊
[material.h]
class metal : public material
{
public:
color albedo;
double fuzz;
public:
metal(const color& a, const double f) : albedo(a), fuzz(f) {}
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, unit_vector(reflected + fuzz * random_unit_vector()));
attenuation = albedo;
return (dot(scattered.direction(), rec.normal) > 0);
}
}; 在 main 中修改我们的材质:
[main.cpp]
...
int main(int argc, char* argv[])
{
...
// 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), 0.3);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0);
...
} 得到图像:

一个物体其半透明,说明光线能够穿过这个物体,空气作为一种介质、透明物体作为另一种介质,当光从一种介质进入另一种介质时会产生折射,根据斯涅尔公式我们可以得到折射角度。 斯涅日公式: η⋅sinθ=η′⋅sinθ′

其中 η 是介质的折射率,空气为 1,玻璃为 1.3~1.7 那么可以解得 θ' 为: sinθ′=η′η⋅sinθ 设折射射线为 R' 我们可以将其进行正交分解: R′=R′⊥+R′∥ 可解出: R′⊥=η′η(R+cosθ∗n) nR′∥=−1−∣R′⊥∣2∗n 其中 cosθ 为 R · (-n),因此可得: R′⊥=η′η(R−(R⋅N)∗n) 那么我们就可以写出折射函数来得到折射向量了:
[vec3.h]
inline vec3 refract(const vec3& uv, const vec3& n, double etai_over_etat)
{
auto cos_theta = fmin(dot(-uv, n), 1.0);
vec3 r_out_perp = etai_over_etat * (uv + cos_theta * n);
vec3 r_out_parallel = -sqrt(fabs(1.0 - r_out_perp.length_squared())) * n;
return r_out_perp + r_out_parallel;
} 接下来得到电介质材质:
[material.h]
class dielectric : public material
{
public:
double ir; // Index of Refraction
public:
dielectric(double index_of_refraction) : ir(index_of_refraction) {}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override
{
attenuation = color(1.0, 1.0, 1.0);
double refraction_ratio = rec.front_face ? (1.0 / ir) : ir;
vec3 unit_direction = unit_vector(r_in.direction());
vec3 refracted = refract(unit_direction, rec.normal, refraction_ratio);
scattered = ray(rec.p, unit_vector(refracted));
return true;
}
}; 在 main 中给一个球附着电介质材质:
[main.cpp]
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.0));
auto material_center = make_shared<dielectric>(1.5);
auto material_left = make_shared<dielectric>(1.5);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 1.0); 得到图像:

实际上对于电介质还有几个问题我们没用考虑到: 首先折射向量可能存在无解的情况,让我们再看一下斯涅日定律得到的方程: sinθ′=η′η⋅sinθ 如果射线是从一种介质(η>1)进入到空气,那么等号右边是有可能大于1的,而等号左边的sin不可能大于1,此时就存在无解的情况。在现实中,此时光线不会发射折射,而是会进行反射,这也就是为什么我们能在海面外透过海面看到水下面的情况,而无法在海里透过海面看到外面的原因。 因此让我们对这种情况做出判断:
[material.h]
class dielectric : public material
{
public:
double ir; // Index of Refraction
public:
dielectric(double index_of_refraction) : ir(index_of_refraction) {}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override
{
attenuation = color(1.0, 1.0, 1.0);
double refraction_ratio = rec.front_face ? (1.0 / ir) : ir;
vec3 unit_direction = unit_vector(r_in.direction());
double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = sqrt(1.0 - cos_theta * cos_theta);
vec3 direction;
if (refraction_ratio * sin_theta > 1.0)
{
// Must Reflect
direction = reflect(unit_direction, rec.normal);
}
else
{
// Can Refract
direction = refract(unit_direction, rec.normal, refraction_ratio);
}
scattered = ray(rec.p, unit_vector(direction));
return true;
}
}; 同时光线射中电介质后本身就有一定概率发生散射,其概率可以使用 Schlick 定律近似得到:
[material.h]
class dielectric : public material
{
public:
double ir; // Index of Refraction
public:
dielectric(double index_of_refraction) : ir(index_of_refraction) {}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override
{
attenuation = color(1.0, 1.0, 1.0);
double refraction_ratio = rec.front_face ? (1.0 / ir) : ir;
vec3 unit_direction = unit_vector(r_in.direction());
double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = sqrt(1.0 - cos_theta * cos_theta);
vec3 direction;
if (refraction_ratio * sin_theta > 1.0 || reflectance(cos_theta, refraction_ratio) > random_double())
{
// Must Reflect
direction = reflect(unit_direction, rec.normal);
}
else
{
// Can Refract
direction = refract(unit_direction, rec.normal, refraction_ratio);
}
scattered = ray(rec.p, unit_vector(direction));
return true;
}
private:
static double reflectance(double cosine, double ref_idx)
{
// Use Schlick's approximation for reflectance.
auto r0 = (1 - ref_idx) / (1 + ref_idx);
r0 = r0 * r0;
return r0 + (1 - r0) * pow((1 - cosine), 5);
}
}; 同样的,现实中的电介质不可能这么理想,我们同样可以利用 fuzz 值,对折射和反射进行模糊,并且我们也可也给电介质添加反照率:
[material]
class dielectric : public material
{
public:
color albedo;
double ir; // Index of Refraction
double fuzz;
public:
dielectric(const color& a, double index_of_refraction, double f) : color(a), ir(index_of_refraction), fuzz(f) {}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override
{
attenuation = albedo;
double refraction_ratio = rec.front_face ? (1.0 / ir) : ir;
vec3 unit_direction = unit_vector(r_in.direction());
double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = sqrt(1.0 - cos_theta * cos_theta);
vec3 direction;
if (refraction_ratio * sin_theta > 1.0 || reflectance(cos_theta, refraction_ratio) > random_double())
{
// Must Reflect
direction = reflect(unit_direction, rec.normal) + fuzz * random_unit_vector();
}
else
{
// Can Refract
direction = refract(unit_direction, rec.normal, refraction_ratio) + fuzz * random_unit_vector();
}
scattered = ray(rec.p, unit_vector(direction));
return true;
}
private:
static double reflectance(double cosine, double ref_idx)
{
// Use Schlick's approximation for reflectance.
auto r0 = (1 - ref_idx) / (1 + ref_idx);
r0 = r0 * r0;
return r0 + (1 - r0) * pow((1 - cosine), 5);
}
}; 在创建电介质球体时,我们可以先创建一个 η 为 1 的小球,在创建一个 η > 1 的大球将其包裹,这两个组合起来就是一个 玻璃球壳:
[main.cpp]
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.2));
auto material_center = make_shared<lambertian>(color(0.2, 0.2, 0.9));
auto material_left1 = make_shared<dielectric>(color(1.0, 1.0, 1.0), 1, 0.0);
auto material_left2 = make_shared<dielectric>(color(0.9, 0.9, 0.9), 1.5, 0.2);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 0.5);
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.4, material_left1));
world.add(make_shared<sphere>(point3(-1.0, 0.0, -1.0), 0.5, material_left2));
world.add(make_shared<sphere>(point3(1.0, 0.0, -1.0), 0.5, material_right)); 得到图像:

或者放置一个半径为负值,即正方面交换的球,然后再套一个球,得到另一种球壳:
[main.cpp]
auto material_ground = make_shared<lambertian>(color(0.8, 0.8, 0.2));
auto material_center = make_shared<lambertian>(color(0.2, 0.2, 0.9));
auto material_left = make_shared<dielectric>(color(0.9, 0.9, 0.9), 1.5, 0.01);
auto material_right = make_shared<metal>(color(0.8, 0.6, 0.2), 0.5);
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.4, material_left));
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)); 得到图像:

对于自发光物体,我们将其看作是一种光源,当光线射中后,不会发射散射。一方面原因是光线射中了光源,那么根据我们光路可逆原理得到的光追路径实际上就是走到了终点,直接返回该光源发出的能量即可。另一方面在现实世界我们看到的光源其颜色也就是其本身发射的颜色,不会受周围环境影响。 我们现在 material 类中加入 emitted 函数:
[material.h]
class material
{
public:
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const = 0;
virtual color emitted(const hit_record& rec) const
{
return color(0, 0, 0);
}
}; 对于光源,其 scatter 函数返回false,emitted 函数返回其光强:
[material.h]
class emission : public material
{
public:
color lighting;
public:
emission(color intensity) : lighting(intensity) {}
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override
{
return false;
}
virtual color emitted(const hit_record& rec) const
{
return lighting;
}
}; ray_color 函数需要做出修改,来接收 emitted 的能量:
[main.cpp]
color ray_color(const ray& r, hittable& world, int depth)
{
if (depth <= 0)
{
return color(0, 0, 0);
}
hit_record rec;
if (world.hit(r, 0.001, infinity, rec))
{
color attenuation;
color emit;
ray scattered;
emit = rec.mat_ptr->emitted(rec);
if (!rec.mat_ptr->scatter(r, rec, attenuation, scattered))
{
return emit;
}
else
{
return emit + attenuation * ray_color(scattered, 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);
} 到目前为止,我们都认为光在空气中传播时,光的能量是没有损失的。但实际上光在空气中传播能量是会损失的。我们可能会在其它的图形学教程中知道,点光源的强度会随着点到光源距离的平方衰减,但这是光源发射的总能量在空间中的扩散造成的空间上的分布不均,在光线追踪中,对于单个光束的路径并不存在这个问题。 在空气光路中光的能量损失是由于大气散射,这里我们不会讨论复杂的大气散射原理和公式,我们只需要知道根据基础的大气散射原理,光路的能量损失为exp(-a*d),其中 a 为一个常数系数,我们这里取 0.25,d 为光路走过的路程,即我们射线的 t 这里我们要得到准确的 t,那么我们射线的方向必须是单位向量。在前面我们已经把散射射线的方向给单位化了,但是相机发出的射线现在还不是单位化的,因此我们对其单位化:
[camera.h]
ray get_ray(double u, double v) const
{
return ray(origin, unit_vector(lower_left_corner + u * horizontal + v * vertical - origin));
} 然后将 exp(-0.25*t) 添加到我们材质的attenuation计算中:
[material.h]
#ifndef MATERIAL_H
#define MATERIAL_H
#include "common.h"
#include "hittable.h"
class material
{
public:
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const = 0;
};
class lambertian : public material
{
public:
color albedo;
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, unit_vector(scatter_direction));
attenuation = albedo * exp(-0.25*rec.t);
return true;
}
};
class metal : public material
{
public:
color albedo;
double fuzz;
public:
metal(const color& a, const double f) : albedo(a), fuzz(f) {}
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, unit_vector(reflected + fuzz * random_unit_vector()));
attenuation = albedo * exp(-0.25*rec.t);
return (dot(scattered.direction(), rec.normal) > 0);
}
};
class dielectric : public material
{
public:
color albedo;
double ir; // Index of Refraction
double fuzz;
public:
dielectric(const color& a, double index_of_refraction, double f) : albedo(a), ir(index_of_refraction), fuzz(f) {}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override
{
attenuation = albedo * exp(-0.25*rec.t);
double refraction_ratio = rec.front_face ? (1.0 / ir) : ir;
vec3 unit_direction = unit_vector(r_in.direction());
double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = sqrt(1.0 - cos_theta * cos_theta);
vec3 direction;
if (refraction_ratio * sin_theta > 1.0 || reflectance(cos_theta, refraction_ratio) > random_double())
{
// Must Reflect
direction = reflect(unit_direction, rec.normal) + fuzz * random_unit_vector();
}
else
{
// Can Refract
direction = refract(unit_direction, rec.normal, refraction_ratio) + fuzz * random_unit_vector();
}
scattered = ray(rec.p, unit_vector(direction));
return true;
}
private:
static double reflectance(double cosine, double ref_idx)
{
// Use Schlick's approximation for reflectance.
auto r0 = (1 - ref_idx) / (1 + ref_idx);
r0 = r0 * r0;
return r0 + (1 - r0) * pow((1 - cosine), 5);
}
};
class emission : public material
{
public:
color lighting;
public:
emission(color intensity) : lighting(intensity) {}
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override
{
return false;
}
virtual color emitted(const hit_record& rec) const
{
return lighting * exp(-0.25*rec.t);
}
};
#endif 得到图像:

这里地面的边缘呈现出一条黑色,与天空形成了明显的交界线,这与我们现实生活不符。这是因为我们的天空颜色不是由大气散射相关的算法得到,材质的大气散射也只是简单的对光能量进行衰减。但为了让本文保持精简,这里我不会继续去深入讨论大气散射。我们先接受这样的结果。
接下来我们继续完善我们的相机类。我们添加定义相机的竖直fov,这样可以通过fov计算出视口高度,再由视口高度和纵横比计算出视口宽度:

[camera.h]
#ifndef CAMERA_H
#define CAMERA_H
#include "common.h"
class camera
{
private:
point3 origin;
point3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
public:
camera(
double vfov, // vertical field-of-view in degrees
double aspect_ratio
)
{
auto theta = degrees_to_radians(vfov);
auto h = tan(theta * 0.5);
auto viewport_height = 2.0 * h;
auto viewport_width = aspect_ratio * viewport_height;
auto focal_length = 1;
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 * 0.5 - vertical * 0.5 - vec3(0, 0, focal_length);
}
ray get_ray(double u, double v) const
{
return ray(origin, unit_vector(lower_left_corner + u * horizontal + v * vertical - origin));
}
};
#endif 这样我们可以得到一个广角相机:
[main.cpp]
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.4, material_left));
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(120.0, aspect_ratio);
// Render
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n"; // ppm文件头
clock_t start_time = clock();
for (int j = image_height - 1; j >= 0; --j)
{ 得到图像:

接下来设置相机的观察角度 我们设相机位置为 lookfrom 点,相机看向的点为 lookat 点,那么就得到了相机的forward向量 将相机的朝向向量与 vup向量(通常为(0,1,0))进行叉乘,可得到相机的 right 向量,right 向量 再和 forward 向量进行叉乘即可得到相机的 up向量

[camera.h]
#ifndef CAMERA_H
#define CAMERA_H
#include "common.h"
class camera
{
private:
point3 origin;
point3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
public:
camera(
point3 lookfrom,
point3 lookat,
vec3 vup,
double vfov, // vertical field-of-view in degrees
double aspect_ratio
)
{
auto theta = degrees_to_radians(vfov);
auto h = tan(theta * 0.5);
auto viewport_height = 2.0 * h;
auto viewport_width = aspect_ratio * viewport_height;
auto w = unit_vector(lookfrom - lookat);
auto u = unit_vector(cross(vup, w));
auto v = cross(w, u);
origin = lookfrom;
horizontal = viewport_width * u;
vertical = viewport_height * v;
lower_left_corner = origin - horizontal * 0.5 - vertical * 0.5 - w;
}
ray get_ray(double s, double t) const
{
return ray(origin, unit_vector(lower_left_corner + s * horizontal + t * vertical - origin));
}
};
#endif 这样就可以重新摆放我们的相机了:
[main.cpp]
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.4, material_left));
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(point3(-2, 2, 1), point3(0, 0, -1), vec3(0, 1, 0), 90, aspect_ratio);
// Render
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n"; // ppm文件头
clock_t start_time = clock();
for (int j = image_height - 1; j >= 0; --j)
{ 相机景深的原理为,相机的镜片会把聚焦平面上一点发出来的多束光聚焦到底片的一个点上。而如果一个点不在聚焦平面上,那么其发出来的多束光就会分在的底片的多个点上,从而形成模糊的效果。

image.png
而对于我们的光追相机,我们采取这样的策略: 在相机位置点抽象出一个与视口平行的光圈平面,在光圈内随机取一个向量,让相机进行偏移。对于聚焦面上的一点,无论相机偏移前后,相机都是朝该点发出射线,那么对于聚焦面外的点,则相机偏移前后射线与其的交点会产生变化,这样就可以实现景深的模糊效果。而光圈越大,相机偏移范围就越大,模糊效果就越明显。

image.png
首先是在圆面内随机取点的函数:
[vec3.h]
inline vec3 random_in_unit_disk()
{
while (true)
{
auto p = vec3(random_double(-1, 1), random_double(-1, 1), 0);
if (p.length_squared() >= 1) continue;
return p;
}
} 接下来给相机添加聚焦平面距离(焦距)设置 和 光圈模糊的半径设置:
[camera.h]
#ifndef CAMERA_H
#define CAMERA_H
#include "common.h"
class camera
{
private:
point3 origin;
point3 lower_left_corner;
vec3 horizontal;
vec3 vertical;
vec3 u, v, w;
double lens_radius;
public:
camera(
point3 lookfrom,
point3 lookat,
vec3 vup,
double vfov, // vertical field-of-view in degrees
double aspect_ratio,
double aperture,
double focus_dist
)
{
auto theta = degrees_to_radians(vfov);
auto h = tan(theta * 0.5);
auto viewport_height = 2.0 * h;
auto viewport_width = aspect_ratio * viewport_height;
w = unit_vector(lookfrom - lookat);
u = unit_vector(cross(vup, w));
v = cross(w, u);
origin = lookfrom;
horizontal = focus_dist * viewport_width * u;
vertical = focus_dist * viewport_height * v;
lower_left_corner = origin - horizontal * 0.5 - vertical * 0.5 - focus_dist * w;
lens_radius = aperture * 0.5;
}
ray get_ray(double s, double t) const
{
vec3 rd = lens_radius * random_in_unit_disk();
vec3 offset = u * rd.x() + v * rd.y();
return ray(
origin + offset,
unit_vector(lower_left_corner + s * horizontal + t * vertical - origin - offset)
);
}
};
#endif 最后放置相机:
[main.cpp]
point3 lookfrom(3,3,2);
point3 lookat(0,0,-1);
vec3 vup(0,1,0);
auto dist_to_focus = (lookfrom-lookat).length();
auto aperture = 2.0;
camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus); 得到图像:

image.png
最后让我们来实现本文开头图片中的场景,同时这里我还对大气散射进行了修改:我们不再时直接按照exp进行能量衰减,而是按照exp在一个衰减范围内进行插值。
[common.h]
#include "ray.h"
#include "vec3.h"
color atmoscatter(double t)
{
double e = exp(-0.25 * t);
return ((1.0 - e) * color(.3, .4, .5) + e * color(1, 1, 1));
} [material.h]
#ifndef MATERIAL_H
#define MATERIAL_H
#include "common.h"
#include "hittable.h"
class material
{
public:
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const = 0;
virtual color emitted(const hit_record& rec) const
{
return color(0, 0, 0);
}
};
class lambertian : public material
{
public:
color albedo;
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, unit_vector(scatter_direction));
attenuation = albedo * atmoscatter(rec.t);
return true;
}
};
class metal : public material
{
public:
color albedo;
double fuzz;
public:
metal(const color& a, const double f) : albedo(a), fuzz(f) {}
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, unit_vector(reflected + fuzz * random_unit_vector()));
attenuation = albedo * atmoscatter(rec.t);
return (dot(scattered.direction(), rec.normal) > 0);
}
};
class dielectric : public material
{
public:
color albedo;
double ir; // Index of Refraction
double fuzz;
public:
dielectric(const color& a, double index_of_refraction, double f) : albedo(a), ir(index_of_refraction), fuzz(f) {}
virtual bool scatter(
const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered
) const override
{
attenuation = albedo * atmoscatter(rec.t);
double refraction_ratio = rec.front_face ? (1.0 / ir) : ir;
vec3 unit_direction = unit_vector(r_in.direction());
double cos_theta = fmin(dot(-unit_direction, rec.normal), 1.0);
double sin_theta = sqrt(1.0 - cos_theta * cos_theta);
vec3 direction;
if (refraction_ratio * sin_theta > 1.0 || reflectance(cos_theta, refraction_ratio) > random_double())
{
// Must Reflect
direction = reflect(unit_direction, rec.normal) + fuzz * random_unit_vector();
}
else
{
// Can Refract
direction = refract(unit_direction, rec.normal, refraction_ratio) + fuzz * random_unit_vector();
}
scattered = ray(rec.p, unit_vector(direction));
return true;
}
private:
static double reflectance(double cosine, double ref_idx)
{
// Use Schlick's approximation for reflectance.
auto r0 = (1 - ref_idx) / (1 + ref_idx);
r0 = r0 * r0;
return r0 + (1 - r0) * pow((1 - cosine), 5);
}
};
class emission : public material
{
public:
color lighting;
public:
emission(color intensity) : lighting(intensity) {}
virtual bool scatter(const ray& r_in, const hit_record& rec, color& attenuation, ray& scattered) const override
{
return false;
}
virtual color emitted(const hit_record& rec) const
{
return lighting * atmoscatter(rec.t);
}
};
#endif [main.cpp]
#include "common.h"
#include "color.h"
#include "hittable_list.h"
#include "sphere.h"
#include "camera.h"
#include "material.h"
#include <iostream>
color ray_color(const ray& r, hittable& world, int depth)
{
if (depth <= 0)
{
return color(0, 0, 0);
}
hit_record rec;
if (world.hit(r, 0.001, infinity, rec))
{
color attenuation;
color emit;
ray scattered;
emit = rec.mat_ptr->emitted(rec);
if (!rec.mat_ptr->scatter(r, rec, attenuation, scattered))
{
return emit;
}
else
{
return emit + attenuation * ray_color(scattered, world, depth - 1);
}
}
vec3 unit_direction = unit_vector(r.direction());
const int H = 6400000;
double t = 0;
color back = color(1.5, 1.5, 1.5) + color(2.4, 2.4, 1.2) * clamp(dot(unit_vector(vec3(0, 1, -1)), unit_direction), 0, 1);
if (r.direction().y() < 0)
t = (H - r.origin().y()) / -r.direction().y();
else
t = (H - r.origin().y()) / r.direction().y();
return back * atmoscatter(t);
}
hittable_list random_scene()
{
hittable_list world;
auto ground_material = make_shared<lambertian>(color(0.8, 0.8, 0.8));
world.add(make_shared<sphere>(point3(0, -1000, 0), 1000, ground_material));
for (int a = -11; a < 11; ++a)
{
for (int b = -11; b < 11; ++b)
{
auto choose_mat = random_double();
point3 center = point3(a + 0.9 * random_double(), 0.2, b + 0.9 * random_double());
if ((center - point3(4, 0.2, 0)).length() > 0.9)
{
shared_ptr<material> sphere_material;
if (choose_mat < 0.8)
{
auto albedo = color::random() * color::random();
sphere_material = make_shared<lambertian>(albedo);
}
else if (choose_mat < 0.95)
{
auto albedo = color::random(0.5, 1);
auto fuzz = random_double(0, 0.5);
sphere_material = make_shared<metal>(albedo, fuzz);
}
else
{
auto albedo = color::random(0.8, 1.0);
auto ir = random_double(1.2, 1.8);
auto fuzz = random_double(0, 0.4);
sphere_material = make_shared<dielectric>(albedo, ir, fuzz);
}
world.add(make_shared<sphere>(center, 0.2, sphere_material));
}
}
}
auto mat1 = make_shared<dielectric>(color(0.98, 0.98, 0.98), 1.5, 0.05);
world.add(make_shared<sphere>(point3(-4, 1, 0), 1, mat1));
auto mat2 = make_shared<lambertian>(color(0.4, 0.2, 0.1));
world.add(make_shared<sphere>(point3(0, 1, 0), 1, mat2));
auto mat3 = make_shared<metal>(color(0.8, 0.6, 0.2), 0.1);
world.add(make_shared<sphere>(point3(4, 1, 0), 1, mat3));
auto mat4 = make_shared<emission>(color(3.6, 2.4, 2.4));
world.add(make_shared<sphere>(point3(0, 0.5, 2), 0.5, mat4));
return world;
}
int main(int argc, char* argv[])
{
// Image Settings
const auto aspect_ratio = 16.0 / 9.0;
const int image_width = 1080;
const int image_height = static_cast<int>(image_width / aspect_ratio);
const int samples_per_pixel = 128;
const int max_depth = 128;
// World
auto world = random_scene();
// Camera
point3 lookfrom(13, 2, 3);
point3 lookat(0, 0, 0);
vec3 vup(0, 1, 0);
auto dist_to_focus = 10.0;
auto aperture = 0.1;
camera cam(lookfrom, lookat, vup, 20, aspect_ratio, aperture, dist_to_focus);
// Render
std::cout << "P3\n" << image_width << " " << image_height << "\n255\n"; // ppm文件头
clock_t start_time = clock();
for (int j = image_height - 1; j >= 0; --j)
{
clock_t dual_time = clock();
std::cerr << "\rScanlines remaining: " << j << ' ' << "The run time is: " << (double)(dual_time - start_time) / CLOCKS_PER_SEC << "s" << " " << 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";
return 0;
}
如果在进行最终渲染时觉得渲染时常过慢,可以参考下面的代码,对渲染进行多线程加速。由于本人对C++不是怎么了解,这里的多线程是一边百度一边各种自己尝试整出来的,不一定是最优方案,大家也可也自行尝试用自己的方法进行多线程加速。
[main.cpp]
#include <iostream>
#include <thread>
...
void ray_tracing_thread(int i, int j, int samples_per_pixel, int image_width, int image_height, color* colors, camera cam, hittable_list world, int max_depth)
{
color pixel_color(0, 0, 0);
hit_record rec;
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(colors, i, j, image_width, pixel_color, samples_per_pixel);
}
int main(int argc, char* argv[])
{
...
camera cam(lookfrom, lookat, vup, 30, aspect_ratio, aperture, dist_to_focus);
const int thread_group_x_count = 32;
const int thread_group_y_count = static_cast<int>(thread_group_x_count / aspect_ratio);
const int group_count = thread_group_x_count * thread_group_y_count;
const int thread_group_x = image_width / thread_group_x_count;
const int thread_group_y = static_cast<int>(image_height / thread_group_y_count);
std::thread* threads = new std::thread[thread_group_x_count * thread_group_y_count];
color* colors = (color*)malloc(sizeof(color) * image_width * image_height);
std::cout << "P3\n" << image_width << ' ' << image_height << "\n255\n";
clock_t start_time = clock();
for (int y = thread_group_y - 1; y >= 0; --y)
{
for (int x = 0; x < thread_group_x; ++x)
{
clock_t dual_time = clock();
double progress = ((thread_group_y - y) * thread_group_x + x) * 1.0 / (thread_group_x * (thread_group_y + 1));
std::cerr << "\rprgoress: " << progress << " " << "The run time is: " << (double)(dual_time - start_time) / CLOCKS_PER_SEC << "s" << ' ' << std::flush;
for (int k = thread_group_y_count - 1; k >= 0; --k)
{
for (int z = 0; z < thread_group_x_count; ++z)
{
int j = y * thread_group_y_count + k;
int i = x * thread_group_x_count + z;
if (j * image_width + i > image_width * image_height) continue;
threads[k * thread_group_x_count + z] = std::thread(ray_tracing_thread, i, j, samples_per_pixel, image_width, image_height, colors, cam, world, max_depth);
}
}
for (int k = thread_group_y_count - 1; k >= 0; --k)
{
for (int z = 0; z < thread_group_x_count; ++z)
{
threads[k * thread_group_x_count + z].join();
}
}
}
}
for (int j = image_height - 1; j >= 0; --j)
{
for (int i = 0; i < image_width; ++i)
{
color col = colors[j * image_width + i];
std::cout << static_cast<int>(256 * clamp(col.x(), 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(col.y(), 0.0, 0.999)) << ' '
<< static_cast<int>(256 * clamp(col.z(), 0.0, 0.999)) << '\n';
}
}
delete[]threads;
free(colors);
clock_t end_time = clock();
std::cerr << "\nDone.\n";
std::cerr << "The run time is: " << (double)(end_time - start_time) / CLOCKS_PER_SEC << "s\n";
}
#ifndef COLOR_H
#define COLOR_H
#include "vec3.h"
#include <iostream>
void write_color(color* colors, int i, int j, int image_width, color pixel_color, int samples_per_pixel)
{
auto r = pixel_color.x();
auto g = pixel_color.y();
auto b = pixel_color.z();
auto scale = 1.0 / samples_per_pixel;
r = sqrt(r * scale);
g = sqrt(g * scale);
b = sqrt(b * scale);
colors[j * image_width + i] = color(r, g, b);
}
#endif 结果图像:

image.png

image_night.png