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次,能夠獲得以下的結果:
……未完