今天我們開始進入正篇
Chapter 3: Rays, a simple camera, and background
對於所有的光線追蹤器,基本都有一個光線類,計算沿光線看到的顏色。
我們的光線是一個矢量運算:
p(t) = a + t * b.
書中的向量用大寫粗體字表示,但這里我遵循一般表示法:矩陣為粗體大寫字母,向量為粗體小寫字母,而標量為正常字體
P是光線指向的目標位置,a是光線的起點,b是光線指向的方向,t則是光線步長。
如圖:
我們如下定義ray-class
/// 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 class ray { public: using value_type = lvgm::precision; using vec_type = lvgm::vec3<value_type>; public: ray() :_a{vec_type()} , _b{vec_type()} { } ray(const vec_type& a, const vec_type& b) :_a(a) ,_b(b) { } ray(const ray& r) :_a(r._a) ,_b(r._b) { } inline vec_type origin()const { return _a; } inline vec_type direction()const { return _b; } inline vec_type go(const value_type t)const { return _a + t * _b; } private: vec_type _a; vec_type _b; }; #endif //ray_h
然后我們來看制作一個光線追蹤器,它的核心在於,發送光線去穿過像素,進而計算出沿着光線方向,什么顏色被看到了
我們看到物體,是外部光照射到物體表面,經過表面反射之后,那部分反射進入眼睛的光被我們捕捉,從而看到了光來源位置的物體,那么,我們假設從眼睛發射一束光,它代表我們的視線,當它沿着某個方向一直向前,視線會與物體表面相交,那么,我們就捕捉到了一個像素。光線追蹤器就是計算視線的一種形式。
為了更貼切,我們在此之后將光線統一稱為視線。
它可以計算視線與表面交點以及交點處的顏色。
上述內容會在后續的案例講解過程中逐步理解的。
現在,我們來了解一下本書采用的默認坐標系統
coord 1.1
坐標(0,0,0)處為我們的眼睛(或者相機),我們的繪圖區域為藍色框代表的矩形平面,也就是你觀察圖像的那個屏幕。
按照學OpenGL的老規矩,第一堂當然是線性插值
差值公式:
blended_value = (1-t) * start_value + t * end_value
解釋一下上面的公式:
t 是一個系數,根據情況確定,將開始顏色和終止顏色進行比例混合,然后得到混合色
如果我們要做一個從白色到藍色根據坐標位置確定混合比例進行插值的矩形彩圖
我們該如何做呢?
第一步,我們需要確定分辨率,假定為400*200,就用coord1.1的坐標體系去做。
第二步,我們需要確定開始位置和終止位置,假定從左下角混合到右上角,混合顏色為白色和藍色。
第三步,我們需要確定視線,即確定從眼睛出發到屏幕的向量
1. 確定在平面中的位置,從左下角開始每一個平面位置均由水平和垂直兩個分向量疊加而成:
diagram 3-1
由坐標系統得知,lower-left的坐標為(-2,-1,-1)
若y =(0,0.5,0),x =(1,0,0),則pos =(-1,-0.5,-1)
2.當我們確定了pos之后,其實,視線已經確定好了,因為眼睛的坐標為(0,0,0),pos即為視線向量
第四步,我們需要從分辨率到屏幕做一個映射。
看圖還記得我們的分辨率是400*200嗎,而屏幕的范圍是4*2的矩形
所以,我們需要做一個映射,和上次一樣,我們可以將分辨率下的位置通過除法轉換到標准坐標,然后再通過標准坐標轉換到屏幕坐標
例如一個分辨率下的x的步長為388,首先通過388/400變為一個0~1的實數,然后乘以4,即可變為屏幕步長
然后通過diagram 3-1,確定屏幕中的位置
第五步,我們需要確定比例系數t,我們可以選擇x或y方向的其中一個做映射。
因為屏幕坐標系不確定,我們采用的是4*2的,但不是銘文規定的,所以我們需要將視線向量(等同於pos坐標位置)進行單位化,這樣的話就可以把每個坐標分量的長度控制在[-1, 1],如果我們把它+1再除以2,那么就完全轉化到[0, 1]了,此時,每個坐標方向的分向量范數均為[0, 1],它們是由三個坐標基共同作用而成的獨一無二的,所以,你可以采用x的單位化值作為t,也可以把y作為t。
第六步,經過上述一頓操作,我們終於得到了屏幕某個點對應的blend_value,顏色混合值(或稱為插值)
至此,我們基於位置進行的顏色插值就講解完了
下面是代碼:
#define LOWPRECISION #include <fstream> #include "ray.h" using namespace lvgm; #define stds std:: ray::vec_type lerp(const ray& r) { ray::vec_type unit_dir = r.direction().ret_unitization(); //單位化 ray::value_type t = 0.5*(unit_dir.y() + 1.0); //將y分量映射到[0, 1] //插值公式 白色&藍色 return (1.0 - t)*ray::vec_type(1.0, 1.0, 1.0) + t*ray::vec_type(0.0, 0.0, 1.0); } void build_3_1() { int X = 400, Y = 200; //分辨率 400*200 stds ofstream file("graph3-1.ppm"); if (file.is_open()) { file << "P3\n" << X << " " << Y << "\n255\n"; ray::vec_type left_bottom{ -2.0,-1.0,-1.0 }; //左下角作為開始位置 ray::vec_type horizontal{ 4.0,0,0 }; //屏幕水平寬度 ray::vec_type vertical{ 0,2.0,0 }; //屏幕垂直高度 ray::vec_type eye{ 0,0,0 }; //眼睛位置 for (int j = Y - 1; j >= 0; --j) for (int i = 0; i < X; ++i) { vec2<ray::value_type> para(ray::value_type(i) / X, ray::value_type(j) / Y); ray r(eye, left_bottom + para.u() * horizontal + para.v() * vertical); ray::vec_type color = lerp(r); //得到插值顏色(rgb) int ir = int(255.99*color.r()); int ig = int(255.99*color.g()); int ib = int(255.99*color.b()); file << ir << " " << ig << " " << ib << stds endl; } file.close(); } else stds cerr << "load file failed!" << stds endl; stds cout << "complished" << stds endl; } int main() { build_3_1(); }
下面是效果圖(y分量作為插值公式系數)
當然你也可以用x分量作為插值公式系數 t
感謝您的閱讀,生活愉快~