我們上一篇寫了Chapter5 的第一個部分表面法線,那么我們來學剩下的部分,以及Chapter6.
Chapter5:Surface normals and multiple objects.
我們這一節主要向場景中添加對象。
依據代碼重用原則,此時應該抽象出對象創、繪制的公共部分
All what we do are followed by object-oriented !
我們先來抽象並定義一些基本的類型
1>.ray.
這個不用說了,但是我們發現,在后面涉及到的所有的向量和精度類型均取決於ray,所以,我們不妨把所有的抽象類放入統一的命名空間,把類型方面的定義放在空間內,而不是每次都需要ray::vec_type

/// ray.h // ----------------------------------------------------- // [author] lv // [begin ] 2018.12 // [brief ] the ray-class for the ray-tracing project // from the 《ray tracing in one week》 // ----------------------------------------------------- #ifndef RAY_H #define RAY_H #include <lvgm\type_vec\type_vec.h> //https://www.cnblogs.com/lv-anchoret/p/10163085.html namespace rt { using rtvar = lvgm::precision; using rtvec = lvgm::vec3<rtvar>; class ray { public: ray() :_a{ rtvec() } , _b{ rtvec() } { } ray(const rtvec& a, const rtvec& b) :_a(a) , _b(b) { } ray(const ray& r) :_a(r._a) , _b(r._b) { } inline rtvec origin()const { return _a; } inline rtvec direction()const { return _b; } inline rtvec go(const rtvar t)const { return _a + t * _b; } private: rtvec _a; rtvec _b; }; } #endif //ray_h
2>.intersect.
這個類名的由來是依據書中描述光線追蹤的一句話,我覺得總結的很精煉,我自己將它理解為對光線追蹤的一個定義:
Ray Tracer is of the form calculate which ray goes from the eye to a pixel, compute what that ray intersects, and compute a color for that intersection ppoint.
而我們這個類完成的就是前半部分:計算光線相交點,或者說是交叉點,或者說是撞擊點。
所以講基類命名為intersect
因為在實際操作中可能需要對根進行條件過濾,所以,我們在hit中增加了關於系數t的上限和下限,增加靈活度,強化用戶體驗。

/// intersect.h // ----------------------------------------------------- // [author] lv // [begin ] 2018.12 // [brief ] the intersect-class for the ray-tracing project // from the 《ray tracing in one week》 // ----------------------------------------------------- #ifndef INTERSECT_H #define INTERSECT_T #include "ray.h" namespace rt { struct hitInfo { lvgm::precision _t; //ray 中的系數t rtvec _p; //相交點、撞擊點 rtvec _n; //_p點的表面法線 }; class intersect { public: intersect() { } constexpr static rtvar inf() { return 0x3f3f3f3f; } //最大值 virtual bool hit(const ray& sight, rtvar t_min, rtvar t_max, hitInfo& rec)const = 0; virtual ~intersect() { } }; } #endif //INTERSECT_H
3>.sphere.
球體函數,撞擊函數和之前的hit一樣,只不過我們優先選取比較小的根,因為它離我們的視線更近,因為我們看東西也是先看到的是近處的,遠處的被遮擋了。如果一個根都沒有,那么我們返回false

/// sphere.h // ----------------------------------------------------- // [author] lv // [begin ] 2018.12 // [brief ] the sphere-class for the ray-tracing project // from the 《ray tracing in one week》 // ----------------------------------------------------- #ifndef SPHERE_H #define SPHERE_H namespace rt { class sphere :public intersect { public: sphere() { } /* @para1: 球心坐標 @para2: 球半徑 */ sphere(const rtvec& h, rtvar r) :_heart(h), _radius(r) { } /* @brief: 撞擊函數,求取撞擊點相關記錄信息 @param: sight->視線 系數t的上下界->篩選撞擊點 rec->返回撞擊點信息 @retur: 是否存在合法撞擊點 */ virtual bool hit(const ray& sight, rtvar t_min, rtvar t_max, hitInfo& rec)const override; /* @ get-functions */ inline const rtvar r()const { return _radius; } inline const rtvec& heart()const { return _heart; } inline rtvar& r() { return _radius; } inline rtvec& heart() { return _heart; } private: rtvec _heart; rtvar _radius; }; bool sphere::hit(const ray& sight, rtvar t_min, rtvar t_max, hitInfo& rec)const { rtvec trace = sight.origin() - _heart; rtvar a = dot(sight.direction(), sight.direction()); rtvar b = 2.0 * dot(trace, sight.direction()); rtvar c = dot(trace, trace) - _radius * _radius; rtvar delt = b*b - 4.0*a*c; if (delt > 0) { rtvar x = (-b - sqrt(delt)) / (2.0*a); if (x < t_max && x > t_min) { rec._t = x; rec._p = sight.go(rec._t); rec._n = (rec._p - _heart) / _radius; return true; } x = (-b + sqrt(delt)) / (2.0*a); if (x < t_max && x > t_min) { rec._t = x; rec._p = sight.go(x); rec._n = (rec._p - _heart) / _radius; return true; } } return false; } } #endif
4>.intersections.
顧名思義,這個就是用於記錄多個交叉點的一個表
它包含一個二維指針,高維指的是一個有關於基類指針的數組,低維度就是指向基類——intersect的一個多態指針。
而它的hit函數就是,遍歷每一個sphere對象,求取得到視線穿過的離eye最近的交叉點。掃描屏幕的每一條視線均如此做,可翻閱上一篇,我們的3條line的那個實線和虛線圖,對於每一條視線,如果與多個對象存在交叉點,那么最短的那一條是實線,我們求取的始終是實線部分,而實線的長,就是t

/// intersections.h // ----------------------------------------------------- // [author] lv // [begin ] 2018.12 // [brief ] the intersections-class for the ray-tracing project // from the 《ray tracing in one week》 // ----------------------------------------------------- #ifndef INTERSECTIONS_H #define INTERSECTIONS_H namespace rt { class intersections :public intersect { public: intersections() { } intersections(intersect** list, size_t n) :_list(list), _size(n) { } virtual bool hit(const ray& sight, rtvar t_min, rtvar t_max, hitInfo& rec)const override; private: intersect** _list; size_t _size; }; bool intersections::hit(const ray& sight, rtvar t_min, rtvar t_max, hitInfo& rec)const { hitInfo t_rec; bool hitSomething = false; rtvar far = t_max; //剛開始可以看到無限遠 for (int i = 0; i < _size; ++i) { if (_list[i]->hit(sight, t_min, far, t_rec)) { hitSomething = true; far = t_rec._t; //將上一次的最近撞擊點作為視線可達最遠處 rec = t_rec; } } return hitSomething; } } #endif //INTERSECTIONS_H
5>.camera
獲取視線

/// camera.h // ----------------------------------------------------- // [author] lv // [begin ] 2018.12 // [brief ] the camera-class for the ray-tracing project // from the 《ray tracing in one week》 // ----------------------------------------------------- #ifndef CAMERA_H #define CAMERA_H #include "ray.h" namespace rt { class camera { public: camera( const rtvec& eye = rtvec(0.,0.,0.), const rtvec& start = rtvec(-2., -1., -1.), const rtvec& horizon = rtvec(4., 0., 0.), const rtvec& vertical = rtvec(0., 2., 0.)) :_eye{ eye } ,_start{start} ,_horizontal{horizon} ,_vertical{vertical} { } inline const ray get_ray(const rtvar u,const rtvar v)const { return ray{ _eye, _start + u*_horizontal + v*_vertical }; } inline const ray get_ray(const lvgm::vec2<rtvar>& para)const { return ray{_eye, _start + para.u()*_horizontal + para.v()*_vertical}; } inline const rtvec& eye()const { return _eye; } inline const rtvec& start()const { return _start; } inline const rtvec& horizontal()const { return _horizontal; } inline const rtvec& vertical()const { return _vertical; } private: rtvec _eye; rtvec _start; rtvec _horizontal; rtvec _vertical; }; } #endif
------------ 完畢 --------------
進入正題,我們今天來做多對象的場景
我們還選用原來的球,那么再添加一個看似草原的東東(我一開始認為是草原)。
先上圖:
其實這個還是比較簡單的,我們在很遠處,想像那個坐標系統,如果我們在(0,-100.5,-1)處放一個半徑為100的球,是不就是這樣了,然后,在屏幕空間內,小球的幾何表面比大球的幾何表面離眼睛更近,自然就會把小球凸顯出來
代碼:
#define LOWPRECISION #include <fstream> #include "intersect.h" #include "sphere.h" #include "intersections.h" #include "camera.h" #define stds std:: using namespace rt; rtvec lerp(const ray& sight, const intersect* world) { hitInfo rec; if (world->hit(sight, 0., intersect::inf(), rec)) return 0.5*rtvec(rec._n.x() + 1., rec._n.y() + 1., rec._n.z() + 1.); else { rtvec dirUnit = sight.direction().ret_unitization(); rtvar t = 0.5*(dirUnit.y() + 1.); return (1. - t)*rtvec(1., 1., 1.) + t*rtvec(0.5, 0.7, 1.0); } } void build_5_2() { stds ofstream file("graph5-2.ppm"); size_t W = 400, H = 200; if (file.is_open()) { file << "P3\n" << W << " " << H << "\n255\n" << stds endl; intersect** list = new intersect*[2]; list[0] = new sphere(rtvec(0, 0, -1), 0.5); list[1] = new sphere(rtvec(0, -100.5, -1), 100); intersect* world = new intersections(list, 2); camera cma; for (int y = H - 1; y >= 0; --y) for (int x = 0; x < W; ++x) { lvgm::vec2<rtvar> para{ rtvar(x) / W,rtvar(y) / H }; rtvec color = lerp(cma.get_ray(para), world); int r = int(255.99 * color.r()); int g = int(255.99 * color.g()); int b = int(255.99 * color.b()); file << r << " " << g << " " << b << stds endl; } stds cout << "complished" << stds endl; file.close(); if (list[0])delete list[0]; if (list[1])delete list[1]; if (list)delete[] list; } else stds cerr << "open file error" << stds endl; } int main() { build_5_2(); }
Chapter6:Antialiasing
這一章也是超簡單。
用最簡單的采樣模式對鋸齒進行修繕。
引用書中的圖片:
我們掃描屏幕的每一個點,得到的水平步長和垂直步長u和v,但是我們采用的都是整數點,而對於屏幕上的點來說應該是有無數個的對不對,而每個點對應的顏色都是不一樣的,如果我們把屏幕分辨率調的非常高,也就是把屏幕划分地更加細微,鋸齒就會更小。
所以,我們發現,在選取某個整數坐標點進行着色的時候,我們其實是用整數坐標的點的顏色覆蓋了周圍很多本應該是其他顏色的點,就比如說上面的紅色方格,我們之前選取的是方格中心的位置,進行計算得到那一處的像素值,然后用它來代替整個方框的顏色
現在我們賦予方格中心周圍的在方格內部的其他點點的表達自己的權利。
就像投票
位於城市中心的周圍的小村庄也有發言權,他們各個小村庄之間的權利是平等的,我們收集夠一定的票數,然后把值取平均作為最后的像素值。
假設每個整數點之間相隔一個單位,這樣我們每個方格的像素充分考慮了周圍[0,1)的像素值,在未觸及下一個整數坐標點的所有范圍都考慮在內,那么我們相鄰兩個像素的顏色差就不會那么突兀,就可以顯得非常平滑了
之前鋸齒很明顯,是因為每個像素格點只考慮了自己應有的顏色,未考慮兩個相鄰格點之間的漸變像素值,導致相鄰的兩個格點像素值差別較大,不平滑,所以出現鋸齒。
當然,增大分辨率是將相鄰兩個點的坐標更加貼近,使得顏色差別不大。
我做一個Chapter5-1的球,然后再用采樣的方法,采取周圍50個隨機點的像素值取均值,進行對比
分辨率均為200*100
原圖
采樣抗鋸齒圖:
可以看出來平滑了很多
方法:采樣總值 = Σpixel_value(每個坐標分量+一個[0,1)隨機值形成的周圍采樣坐標)
采樣結果 = 采樣總值/樣本數
std::uniform_real_distribution默認產生[0,1)的隨機值
std::mt19937是一種隨機生成算法,用此算法去初始化上面那個即可
測試如下:

事實證明,完全可以完成我們的需要
代碼:
#define LOWPRECISION #include <fstream> #include "intersect.h" #include "sphere.h" #include "intersections.h" #include "camera.h" #include <random> #define stds std:: using namespace rt; stds mt19937 mt; stds uniform_real_distribution<rtvar> rtrand; rtvec lerp(const ray& sight, const intersect* world) { hitInfo rec; if (world->hit(sight, 0., intersect::inf(), rec)) return 0.5*rtvec(rec._n.x() + 1., rec._n.y() + 1., rec._n.z() + 1.); else { rtvec dirUnit = sight.direction().ret_unitization(); rtvar t = 0.5*(dirUnit.y() + 1.); return (1. - t)*rtvec(1., 1., 1.) + t*rtvec(0.5, 0.7, 1.0); } } void build_6_1() { stds ofstream file("graph6-2.ppm"); size_t W = 200, H = 100; if (file.is_open()) { file << "P3\n" << W << " " << H << "\n255\n" << stds endl; intersect** list = new intersect*[1]; list[0] = new sphere(rtvec(0, 0, -1), 0.5); //list[1] = new sphere(rtvec(0, -100.5, -1), 100); intersect* world = new intersections(list, 1); camera cma; for (int y = H - 1; y >= 0; --y) for (int x = 0; x < W; ++x) { rtvec color; for (int cnt = 0; cnt < 50; ++cnt) { lvgm::vec2<rtvar> para{ (rtrand(mt) + x) / W, (rtrand(mt) + y) / H }; color += lerp(cma.get_ray(para), world); } color /= 50; int r = int(255.99 * color.r()); int g = int(255.99 * color.g()); int b = int(255.99 * color.b()); file << r << " " << g << " " << b << stds endl; } stds cout << "complished" << stds endl; file.close(); if (list[0])delete list[0]; if (list)delete[] list; if (world)delete world; } else stds cerr << "open file error" << stds endl; } int main() { build_6_1(); }
感謝您的閱讀,生活愉快~