今天講這本書最後一種材質html
Prefaceide
水,玻璃和鑽石等透明材料是電介質。當光線照射它們時,它會分裂成反射光線和折射(透射)光線。函數
處理方案:在反射或折射之間隨機選擇而且每次交互僅產生一條散射光線ui
(實施方法:隨機取樣,具體見後文)spa
調試最困難的部分是折射光線。若是有折射光線的話,我一般首先讓全部的光折射。對於這個項目,我試圖在咱們的場景中放置兩個玻璃球,我獲得了這個:調試
上述圖片是對的嗎?顯然,在實際生活中,那兩個玻璃球看起來怪怪的,實際狀況下,裏面的內容應該將如今的進行上下顛倒,且沒有黑色成分。code
Chapter9:Dielectrics orm
Readyhtm
定量計算光的折射blog
-------------------------------------------- 數學分割線 --------------------------------------------
公式中的η爲相對摺射率: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圖片,可放大
點此處查看或者翻閱相冊內容
感謝您的閱讀,生活愉快~