In computer graphics, ray tracing is a rendering technique for generating an image by tracing the path of light as pixels in an image plane and simulating the effects of its encounters with virtual objects. The technique is capable of producing a very high degree of visual realism, usually higher than that of typical scanline rendering methods, but at a greater computational cost. ——wikipedia
光线追踪 Ray Tracing
是一种渲染算法,准确地说它应该叫路径追踪 Path Tracing
。它经过追踪入射到人眼或者摄像机的光线来决定这道光线经过的像素点是什么颜色。与光栅化 rasterization
不一样,光栅化将3维空间内的物体投影到屏幕上,逐行或者逐列扫描像素,决定每一个像素的颜色,经过既有现象的模拟,来实现光照、反射、散射等现象的表现状况。光线追踪算法的渲染方式从一开始就是契合物理规律的,所以生成的图像更加真实。可是缺点在于计算量过于巨大,主要在计算光线的相交和反射、折射。c++
下节开始将会介绍如何从零开始实现一个光线追踪渲染器。看过 'Ray Tracing In The Weekend'
的同窗可能会更加喜欢他那种渐进式的面向代码的叙述方式。本系列只是对本人某个阶段的渲染器完成总结,所以我会算法
咱们选择一种比较简单的图像格式——PNM格式,做为输出,相似于位图,它使用正整数保存每一个像素点的RGB信息,以下展现了一个PNM图像的ascii编码:编程
a.ppm文件内容: P3 400 300 255 149 192 255 149 192 255 149 191 255 149 191 255 149 191 255 148 191 255 148 191 255 148 191 255 148 191 255 148 191 255 ......
第一行P3
是全部PNM图像的头,第二行400 300
描述了这个图像的横向和纵向的尺寸。255描述了每一个像素的颜色份量的最大值,这里咱们选择255做为最大值。下面的每一行则是从左上角开始的每一个像素点的RGB份量,a.ppm中应该有400*300即120000行信息,很显然这是很是低效的储存方式。segmentfault
下面的代码可以写出一个ppm图像,其中 Color
类能够简单理解成一个包含RGB三个颜色份量的struct,后续会有详细介绍。dom
#include "ppm.h" using namespace std; int WriteRGBImg(const char* path, int nx, int ny, Color *pix) { std::ofstream fout(path); fout << "P3\n" << nx << " " << ny << "\n255\n"; for (int i = 0; i < nx * ny; ++i) { fout << (int) pix[i][0] << "\t" << (int) pix[i][1] << "\t" << (int) pix[i][2] << "\n"; } fout.flush(); return 0; }
<center>图1.1 写入ppm文件代码图</center>ide
现阶段咱们只打算在世界中引入球体。this
能够首先考虑一下实现一个三维世界中的光线追踪渲染器须要表示哪些概念,例如点、线、面、颜色。他们无一例外均可以用三维的向量或者三维向量的组合来表示。所以一个良好完备的三维向量定义是整个项目的重要基础设施,下图给出了个人Vector3定义。编码
/** * Common 3-d vector definition */ class Vector3 { public: Vector3() = default; Vector3(double a, double b, double c); double e[3]; inline double& operator[](int i) { return e[i]; } inline Vector3& operator=(const Vector3& vec); inline Vector3 operator-() const; friend inline Vector3 operator+(const Vector3& vec1, const Vector3& vec2); friend inline Vector3 operator-(const Vector3& vec1, const Vector3& vec2); inline Vector3 operator*(double k) const; inline Vector3 operator*(const Vector3& vec); inline Vector3 operator/(double k) const; inline Vector3 operator/(const Vector3& vec); inline Vector3& operator+=(const Vector3& vec); inline Vector3& operator-=(const Vector3& vec); inline Vector3& operator*=(double k); inline Vector3& operator/=(double k); inline double Dot(const Vector3 &vec) const; inline Vector3 Cross(const Vector3 &vec) const; inline Vector3 UnitVector(); inline double Length() const; inline bool operator!=(const Vector3& v); inline bool Parallel(const Vector3 &v) const; friend std::ostream& operator<<(std::ostream& os, const Vector3& v); };
<center>图2.1 vector3定义代码</center>spa
须要注意的是,尽可能引导编译器将向量的运算inline,由于渲染器几乎全部的计算都涉及向量的运算,inline能够提高渲染速度。Vector3
的具体实现这里不在话下。unix
一个三维向量就能够表示一个点,(下文的全部向量用大写字母表示,实数用小写字母表示,点乘用·表示,叉乘用×表示)。
Vector3 p{0, 0, 0};
上面的代码表示一个在(0, 0, 0)的点。
空间中的一条直线能够表示为以下。k是一个参数,这是一个直线上的点p关于k的参数方程。
l = A + k·B
个人光纤定义以下:
/* * described by P = A + k·B * P is any point on the ray. * A is the origin point. B is the direction. */ class Ray { public: Ray() = default; Ray(const Vector3& A, const Vector3& B) : A(A), B(B) { } Ray(const Vector3& A, const Vector3& B, const Ray& previous): Ray(A, B) { refracted = previous.refracted; } Vector3 Origin() const { return A; } Vector3 Direction() const { return B; } Vector3 P(double k) const { return A + B * k; } Vector3 operator[](double k) const { return P(k); } bool refracted = false; protected: Vector3 A, B; };
<center>图2.3 光线定义代码</center>
注意这里的光线定义不是真实世界的光线,而是从摄像机中发出的“追踪光线”。
对于球体的描述很是简单,只须要圆心和半径就知道了这个圆的全部信息。个人圆的定义以下:
class Sphere : public Object { public: Sphere(Vector3 center, double radius, const Material& m) : center(center), radius(radius), Object(m) { } bool IsHit(const Ray& r, double minT, double maxT, HitRecord& hitRec) override; Vector3 Center() { return center; } double Radius() { return radius; } protected: Vector3 center; double radius; };
首先描述光线追踪的编程模型,以便后续物理模型叙述的展开。
世界中的全部物体都须要计算与光线的相交,而且须要给出光线的交点和交点的切面信息,这是现阶段全部光线算法的所需的全部信息。计算交点的工做彷佛放在物体的定义中是更加合适的,由于光线的信息简单的多,而计算交点信息很是依赖物体的信息。个人物体定义以下:
/** * stores information about at which point a ray hits * an object and what the t-param is in the ray. */ struct HitRecord { HitRecord() = default; HitRecord( double t, const Vector3& p, const Vector3 normal, const Material& m ) : t(t), p(p), normal(normal), scatterInfos(scatterInfos) { } double t; Vector3 p, normal; std::vector<ScatterInfo> scatterInfos; }; /** * common object definition. */ class Object { public: Object(const Material& m): material(m) { } // decide whether the ray r hits this object. virtual bool IsHit(const Ray& r, double minT, double maxT, HitRecord& hitRec) = 0; const Material& material; };
HitRecord
描述了光线与物体交点的位置、切面的法向量以及下一步可能会衍生的多条光线及其占比。多条光线可能比较难以理解,想象一个玻璃材质的物体,来自某个交点的光线多是折射出来的光线,也多是外界反射的光线,这两种光线叠加在一块儿。物体中包含了一个 Material
类成员,表示这个物体表面的材质。材质将会在 3.3 小节详细描述。
我也考虑将物体群继承自物体,由于一条光线与物体群只会有至多一个交点,这与单个物体的行为是一致的。而且我但愿多个物体能够组合为一个物体,好比玻璃泡能够由一个相对折射率为n的玻璃球和一个相对折射路为1/n的玻璃球组合而成。可是这里咱们更倾向于将物体群理解成物体的集合,它承担着相对于物体而言更多的职责,包括根据材质计算光线的下一步走向,若是但愿将 Object 组合在一块儿,能够将。
个人物体群定义以下:
class Objects { public: virtual bool IsHit(const Ray& r, double minT, double maxT, HitRecord& hitRec); void Add(Object* hittable) { objects.push_back(hittable); } void Release() { for (auto* p: objects) delete p; } protected: std::vector<Object*> objects; };
渲染 render
的含义是根据模型生成图像的过程,更细地说就是选择每一个像素点的颜色。光线追踪的算法核心是追踪某个视角发往某个像素点的光线的路径,当光线到达光源时决定这条光路上光线的颜色。
提到渲染就要首先提到摄像机的概念。
材质决定了光线在接触到物体以后的行为,材质定义以下:
class Material { public: virtual bool Scatter( const Ray &r, HitRecord &hr) const { hr.scatterInfos = {}; }; };
目前咱们关注几个简单的材质种类。
漫反射材质的某个点反射进入摄像机的光线来自多个无规律的方向。所以咱们以交点为起点的单位法向量终点为圆心,做一个半径为1的圆,并在园内随机取一点 P
做为入射光线方向。
图3.1 漫反射材质示意图
漫反射材质还会按比例吸取颜色光,下面是完整的漫反射材质定义。
class Lambertian : public Material { public: Lambertian(const Vector3& attenuation) : attenuation(attenuation) { } bool Scatter( const Ray& r, HitRecord& hr) const override; protected: Vector3 attenuation; }; Vector3 RandomUnitVector() { Vector3 p; do { p = 2.f * Vector3((double) drand48(), (double) drand48(), (double) drand48()) - Vector3(1, 1, 1); } while (p.Length() >= 1); return p; } bool Lambertian::Scatter(const Ray &r, HitRecord &hr) const { Vector3 dir = hr.normal + RandomUnitVector(); hr.scatterInfos.push_back({ attenuation, Ray(hr.p, dir) }); return true; }
咱们使用 drand48()
做为随机数生成器,不一样的随机数序列会有不一样的效果, drand48()
是unix平台会提供的快速随机数实现,它可以生成0-1之间的双精度浮点数。 Windows 平台上可能没有定义,直接引用下面的头文件便可:
/** * drand48.h */ #include <stdlib.h> #define m 0x100000000LL #define c 0xB16 #define a 0x5DEECE66DLL static unsigned long long seed = 1; double drand48(void) { seed = (a * seed + c) & 0xFFFFFFFFFFFFLL; unsigned int x = seed >> 16; return ((double)x / (double)m); } void srand48(unsigned int i) { seed = (((long long int)i) << 16) | rand(); }
根据这个算法,咱们会获得相似下图的图像。
显然这是因为漫反射的每一个像素点只采样了一次致使的,提升采样数能够获得更加真实的图像。咱们为每一个像素点采样100次,能够获得以下的结果:
……未完