今天講這本書最后一種材質
Preface
水,玻璃和鑽石等透明材料是電介質。當光線照射它們時,它會分裂成反射光線和折射(透射)光線。
處理方案:在反射或折射之間隨機選擇並且每次交互僅產生一條散射光線
(實施方法:隨機取樣,具體見后文)
調試最困難的部分是折射光線。如果有折射光線的話,我通常首先讓所有的光折射。對於這個項目,我試圖在我們的場景中放置兩個玻璃球,我得到了這個:
上述圖片是對的嗎?顯然,在實際生活中,那兩個玻璃球看起來怪怪的,實際情況下,里面的內容應該將現在的進行上下顛倒,且沒有黑色成分。
Chapter9:Dielectrics
Ready
定量計算光的折射
-------------------------------------------- 數學分割線 --------------------------------------------
公式中的η為相對折射率:n2/n1
而由於入射光線方向的隨機性和eta的不同,可能導致 1-η*η*(1-cosθ1 * cosθ1)小於0,此時取根號毫無意義
而事實上,這也就是全反射現象。即:當光線從光密介質進入光疏介質中如果入射角大於某個臨界值的時候,就會發生全反射現象。
該臨界角即折射角為90°時對應的入射角,也就是cosθ2恰好等於0的時候
------------------------------------------------ END ------------------------------------------------
正文
我們來封裝一個電介質類
首先明確,它是材質的一種,即

#ifndef DIELECTRIC_H #define DIELECTRIC_H namespace rt { class dielectric :public material { public: dielectric(rtvar RI) :_RI(RI) { } virtual bool scatter(const ray& rIn, const hitInfo& info, rtvec& attenuation, ray& scattered)const; inline bool refract(const rtvec& rIn, const rtvec& n, rtvar eta, rtvec& refracted)const; private: rtvar _RI; //refractive indices }; bool dielectric::scatter(const ray& rIn, const hitInfo& info, rtvec& attenuation, ray& scattered)const { rtvec outward_normal; rtvec refracted; rtvar eta; attenuation = rtvec(1., 1., 1.); if (dot(rIn.direction(), info._n) > 0) { outward_normal = -info._n; eta = _RI; } else { outward_normal = info._n; eta = 1. / _RI; } if (refract(rIn.direction(), outward_normal, eta, refracted)) { scattered = ray(info._p, refracted); return true; } return false; } inline bool dielectric::refract(const rtvec& rIn, const rtvec& n, rtvar eta, rtvec& refracted)const { rtvec unitIn = rIn.ret_unitization(); rtvar cos1 = dot(-unitIn, n); rtvar cos2 = 1. - eta*eta*(1 - cos1*cos2); if (cos2 > 0) { refracted = eta * rIn + n*(eta*cos1 - sqrt(cos2)); return true; } return false; //全反射 } } #endif
attenuation的值總是1,因為玻璃表面不吸收任何光,即沒有rgb強度衰減
我們會很容易想到前言部分中的方法:如果有折射,那么讓所有的光線折射,就像上面代碼中scatter函數描述的那樣,那么就會得到那張圖
我們把metal中的reflect函數設置為靜態的,或者是命名空間內“全局”函數,這樣用起來比較方便,換句話講,這個公式並不屬於任何類,它是3D數學通用公式
main函數球體設置:
上述代碼是前言中圖像的生成代碼
然而,它沒有加入全反射,所以導致了黑色成分的出現,所以,我們將全反射加入到上述代碼中

#ifndef DIELECTRIC_H #define DIELECTRIC_H namespace rt { class dielectric :public material { public: dielectric(const rtvar RI) :_RI(RI) { } virtual bool scatter(const ray& InRay, const hitInfo& info, rtvec& attenuation, ray& scattered)const override; inline bool refract(const rtvec& rIn, const rtvec& n, rtvar eta, rtvec& refracted)const { rtvec unitIn = rIn.ret_unitization(); //將入射光線單位化 rtvar cos1 = dot(unitIn, n); rtvar cos2 = 1. - eta*eta*(1. - cos1*cos1); if (cos2 > 0) { refracted = eta * (rIn - n * cos1) - n * sqrt(cos2); return true; } return false; } private: rtvar _RI; }; bool dielectric::scatter(const ray& InRay, const hitInfo& info, rtvec& attenuation, ray& scattered)const { rtvec outward_normal; rtvec reflected = metal::reflect(InRay.direction(), info._n); rtvar eta; attenuation = rtvec(1., 1., 1.); rtvec refracted; if (dot(InRay.direction(), info._n) > 0) { outward_normal = -info._n; eta = _RI; } else { outward_normal = info._n; eta = 1. / _RI; } if (refract(InRay.direction(), outward_normal, eta, refracted)) { scattered = ray(info._p, refracted); } else { scattered = ray(info._p, reflected); return false; } return true; } } #endif
會得到如下圖:
得到這張圖是真的不容易,踩了一天坑
主要是,渲染一張圖看下效果基本要7~10分鍾,玩不起,放開雙手~~
坑點
這里的反射公式有三種形式,但是它們化簡之后都是一個式子
我們這里采用的是紙上推出來的,但是用哪個式子,我們都要注意三點:
1.向量的符號!!!
我們知道cos(theta1) = dot(- 入射向量,法線)
折射向量 = eta * 入射 + 法線*eta*cos(theta1)- 法線 * cos(theta2)
但是,如果你代碼中的cos(theta1) = dot(入射,法線)
那么, 折射向量 = eta * 入射 - .... 這里就不是+了
這是公式的符號的問題
2.入射向量的單位化
為什么要單位化呢,這個還是很重要的
因為你傳入的入射向量是有長度的,你用你傳入的入射向量計算出來的折射向量也是有長度的,顯然,折射不會衰減光的強度,也不會平白無故縮短向量
這時候你就要考慮了,你傳出的折射向量是要干嘛用的
折射向量是要作為新的視線的方向向量的對吧
而我們都知道,視線有三部分,eye的位置,方向向量,t系數(伸長長度)
還有一點,我們計算景物的畫面的時候,計算的是視線延伸后的離眼球最近的點畫在屏幕上
如果,你的視線最初的方向向量本身就有好長,你的眼球好大一顆,那么本來離eye點最近的點可能就被這顆偌大的眼球邊界包在里面可能不是眼球之外最近的點了
所以,我們的方向向量一定是最短的,即單位1,這樣,我們伸長之后,觸碰到的第一個點才能保證是離眼球最近的點,如果方向向量過長,可能包在里面的點就被忽略了
第二點不注意就會出現下面這張圖
左球的景象少了些,可能就是上述原因,視線的方向向量太長了,未經過單位化
3.向量統一
如果你要用入射向量的單位向量,那么,所有涉及入射向量的地方都用入射單位向量代替
如果不用入射單位向量,那么整個代碼計算過程中就都不用,不要混用。
例: 下面是書上的折射函數代碼
函數體第一行,它把入射光單位化,第二行用 uv 做了點乘,然而后面的五行卻用的是 v 而不是uv ,沒道理!!第五行的 dt 和discriminant都是用 uv 算出來的,前面突然用個v是什么操作??
我們把v改成uv就可以了
坑點結束
然而,還是存在玻璃內圖像顛倒的現象
解釋如下:
這里面有一個反射系數的問題,上面我們都考慮的是反射系數為0的情況,實際生活中的玻璃透明介質是有反射系數的。
此時,我們需要引入一個新的概念——反射系數
它是由 Christopher Schlick 提出的:
rtvar schlick(rtvar cosine, rtvar RI) { rtvar r0 = (1-RI)/(1+RI); r0 *= r0; return r0 + (1-r0)*pow((1-cosine),5); }
這里面還有一個問題
我們折射的 scatter 函數需要全反射的時候return 的 是false , 意思是 if 只計算折射情況,全反射是按照 rtvec(0,0,0)運算的,壓根就沒算
所以,我們改一下代碼:

#ifndef DIELECTRIC_H #define DIELECTRIC_H namespace rt { class dielectric :public material { public: dielectric(const rtvar RI) :_RI(RI) { } virtual bool scatter(const ray& InRay, const hitInfo& info, rtvec& attenuation, ray& scattered)const override; inline static bool refract(const rtvec& rIn, const rtvec& n, rtvar eta, rtvec& refracted); protected: rtvar _RI; rtvar dielectric::schlick(const rtvar cosine, const rtvar RI)const; }; bool dielectric::scatter(const ray& InRay, const hitInfo& info, rtvec& attenuation, ray& scattered)const { rtvec outward_normal; rtvec refracted; rtvec reflected = metal::reflect(InRay.direction(), info._n); rtvar eta; rtvar reflect_prob; rtvar cos; attenuation = rtvec(1., 1., 1.); if (dot(InRay.direction(), info._n) > 0) { outward_normal = -info._n; eta = _RI; cos = _RI * dot(InRay.direction(), info._n) / InRay.direction().normal(); } else { outward_normal = info._n; eta = 1.0 / _RI; cos = -dot(InRay.direction(), info._n) / InRay.direction().normal(); } if (refract(InRay.direction(), outward_normal, eta, refracted)) reflect_prob = schlick(cos, _RI); //如果有折射,計算反射系數 else reflect_prob = 1.0; //如果沒有折射,那么為全反射 if (rtrand01() < reflect_prob) scattered = ray(info._p, reflected); else scattered = ray(info._p, refracted); return true; } inline bool dielectric::refract(const rtvec& rIn, const rtvec& n, rtvar eta, rtvec& refracted) { rtvec unitIn = rIn.ret_unitization(); //將入射光線單位化 rtvar cos1 = dot(-unitIn, n); rtvar cos2 = 1. - eta*eta*(1. - cos1*cos1); if (cos2 > 0) { refracted = eta * unitIn + n * (eta * cos1 - sqrt(cos2)); return true; } return false; } rtvar dielectric::schlick(const rtvar cosine, const rtvar RI)const { rtvar r0 = (1. - RI) / (1. + RI); r0 *= r0; return r0 + (1 - r0)*pow((1 - cosine), 5); } } #endif
里面涉及到了rtrand01,還記得嗎,這個是我們在學漫反射的時候弄的
那么放在這里作什么嘞?
還記得Preface中我們說過的處理方案嗎
我們現在就是這么做的,我們得到一個reflect_prob,它介於0~1之間,如果我們取0~1之間的隨機數,根據隨機數確定選擇反射還是折射,這個還是很科學的,為什么呢?因為我們做了100次采樣!!,那么我們可以理直氣壯的說,我們的透明電介質真正做到了反射和折射的混合(除了全反射現象),而且,前言也說過,光線照射透明電介質時,它會分裂為反射光線和折射光線。
主函數:
#define LOWPRECISION #include ...... #define stds std:: using namespace rt; rtvec lerp(const ray& sight, intersect* world, int depth) { hitInfo info; if (world->hit(sight, (rtvar)0.001, rtInf(), info)) { ray scattered; rtvec attenuation; if (depth < 50 && info.materialp->scatter(sight, info, attenuation, scattered)) return attenuation * lerp(scattered, world, depth + 1); else return rtvec(0, 0, 0); } else { rtvec unit_dir = sight.direction().ret_unitization(); rtvar t = 0.5*(unit_dir.y() + 1.); return (1. - t)*rtvec(1., 1., 1.) + t*rtvec(0.5, 0.7, 1.0); } } void build_9_1() { stds ofstream file("graph9-1.ppm"); size_t W = 400, H = 200, sample = 100; if (file.is_open()) { file << "P3\n" << W << " " << H << "\n255\n" << stds endl; size_t sphereCnt = 4; intersect** list = new intersect*[sphereCnt]; list[0] = new sphere(rtvec(0, 0, -1), 0.5, new lambertian(rtvec(0.1, 0.2, 0.5))); list[1] = new sphere(rtvec(0, -100.5, -1), 100, new lambertian(rtvec(0.8, 0.8, 0.))); list[2] = new sphere(rtvec(-1, 0, -1), 0.5, new dielectric(1.5)); list[3] = new sphere(rtvec(1, 0, -1), 0.5, new metal(rtvec(0.8, 0.6, 0.2))); intersect* world = new intersections(list, sphereCnt); camera cma; for (int y = H - 1; y >= 0; --y) for (int x = 0; x < W; ++x) { rtvec color; for (int cnt = 0; cnt < sample; ++cnt) { lvgm::vec2<rtvar> para{ (rtrand01() + x) / W, (rtrand01() + y) / H }; color += lerp(cma.get_ray(para), world, 0); } color /= sample; color = rtvec(sqrt(color.r()), sqrt(color.g()), sqrt(color.b())); //gamma 校正 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; } file.close(); if (list[0])delete list[0]; if (list[1])delete list[1]; if (list[2])delete list[2]; if (list[3])delete list[3]; if (list)delete[] list; if (world)delete world; stds cout << "complished" << stds endl; } else stds cerr << "open file error" << stds endl; } int main() { build_9_1(); } /*********************************************************/
電介質球體的一個有趣且簡單的技巧是要注意,如果使用負半徑,幾何體不受影響但表面法線指向內部,因此它可以用作氣泡來制作空心玻璃球體:
我們實驗一下書上的負半徑:
得到這樣的圖:
為了能夠看懂空心球是個啥玩意兒,我把eta 顛倒了一下
dot小於0,說明入射光線是從表面法線指向的方向空間入射到內部空間,例如:光從空氣入射到水中
dot大於0,說明入射光線是從表面法線的反方向空間入射到表面法線指向的空間
這樣,我們就可以看到那個內球了
原著
本章節原書pdf圖片,可放大
點此處查看或者翻閱相冊內容
感謝您的閱讀,生活愉快~