【Ray Tracing in One Weekend 超詳解】 光線追蹤1-5
一天一篇,今天來學習第7章 (散射)漫反射材質
Chapter7: Diffuse Materials
Preface
從這一章開始,我們將通過光線追蹤製作一些逼真的材質。
我們將從漫射(磨砂)材料開始。
先看效果:
正文
不發光的漫射物體僅僅呈現其周圍的顏色,但是它們用它們自己的固有顏色來調和這些色彩。
從漫反射表面反射的光方向是隨機的,比如:如果我們將三條光線傳送到一個漫反射表面,它們將各自具有不同的隨機行為:
引用書上的圖:
diagram 7-1
它們也可能被吸收而不是被反射。 表面越暗,光線越可能被吸收。 (這就是為什麼它是黑的!)
任何隨機化方向的演算法都會產生看起來很粗糙的表面。 最簡單的方法之一是理想的漫反射表面。
原文還提到了Lambertian發射面
我們來看一下,如何實現上述功能
圖說一切:
diagram 7-2
圖解
先簡述一下各個原件:左黃球是以eye為中心的一個單位圓,右黃球是一個和左黃球一樣的圓,至於怎麼生成的,後續說
左黃球上有兩個隨機點,藍紫色的s1,紅紫色的s2,對應於右黃球上為s1' 和s2'
紅色為視線;深綠色為反射線;三個黑球為漫反射球體,黑色只是用顏色來區分各個原件的功能,並不是黑色的漫反射球(畫完才發現,黑球都把光線吸收了。。。。==!)
實現過程
步驟一:從eye發出一條視線,交球面於p點,之後我們確定隨機反射方向
將右邊的黃色圓部分放大:
引用書中一張圖:
diagram 7-3
n為P點的單位法向量,方向向外,下面那個點是碰撞點P,找一個和點P相切的單位圓
而這個圓的圓心o的位置就等於p+n(p : eye->P),因為我們的原點就是eye,所以根據向量就可以得出位置資訊
基於eye的向量和位置體系,其實方便了我們利用向量運算代替位置運算,更直觀。這個自己理解下就好,不是重點。
步驟二:then, we pick a random point s from the unit randius sphere.
當我們找到這個s點之後,我們將沿著p->s的方向進行反射,但是我們如何找這個random point呢?
這個時候我們就需要用到我們的diagram 7-2了,回去看一眼那個藍紫色點s1,做一個平行四邊形,對應到s1',他們是等價的(向量只用方向和大小進行定義,不規定起始位置,所以我們能說它們等價)。
我們先在原點單位球中找一個隨機點,構成一個eye->s的向量s1,然後,將s1的起點移動到o處,即s1',也就是說s1'就是我們要求的隨機點,因為直接求隨機點s1'的位置並不好求,所以,只能這樣,其實想是很好想,但是要描述清楚就應該是這麼描述。
步驟三:最後我們得到反射線的方向dir = s1' - p,s1' = o + s1, o = p + n
然後,我們來求s1:
#include <random> #define stds std:: using namespace rt; stds mt19937 mt; stds uniform_real_distribution<rtvar> rtrand; const rtvec random_unit_sphere() { rtvec p; do { p = 2.0*rtvec(rtrand(mt), rtrand(mt), rtrand(mt)) - rtvec(1, 1, 1); } while (dot(p, p) >= 1.0); //rejection method return p; }
關於隨機數生成,在上一篇講過了,應該是靠後講的
rtrand生成的是0~1的隨機數,然後乘以2再減去1,得到的p的每一個分量均位於-1~1,其實它的範圍是一個正方體,而我們要求的是球內隨機點。
所以我們採用書中所述的rejection方法,拒絕非法點:如果基於原點eye找一個隨機點(x,y,z)
如果x*x+y*y+z*z>=1,那麼它不符合我們的需要,我們重新找。
最後,我們通過上面的程式碼就得到了一個球內隨機點。
上述就是diagram 7-2中基於藍紫色點進行反射的深綠色光線的反射過程
當然,還有基於紅紫色的反射線,前半部分就和上面一樣,所以也沒有畫平行四邊形,關於後續反射
步驟四:將當前碰撞點P作為eye,以反射方向向量dir為視線方向進行步驟一
直到沒有碰撞,為止
而且,光線沒經過一次反射強度就會衰減,我們也是這麼做的,我們採用的是每反射一次,衰減一半。
#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; const rtvec random_unit_sphere() { rtvec p; do { p = 2.0*rtvec(rtrand(mt), rtrand(mt), rtrand(mt)) - rtvec(1, 1, 1); } while (dot(p, p) >= 1.0); return p; } rtvec lerp(const ray& sight, const intersect* world) { hitInfo rec; if (world->hit(sight, 0., intersect::inf(), rec)) //如果沒有有效碰撞點 { rtvec target = rec._p + rec._n + random_unit_sphere(); //隨機點s的最後位置 return 0.5*lerp(ray{ rec._p,target - rec._p }, world); //強度衰減,新建eye繼續發射視線 } 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_7_1() { stds ofstream file("graph7-1.ppm"); size_t W = 400, H = 200, sample = 100; 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) { rtvec color; for (int cnt = 0; cnt < sample; ++cnt) { lvgm::vec2<rtvar> para{ (rtrand(mt) + x) / W, (rtrand(mt) + y) / H }; color += lerp(cma.get_ray(para), world); } color /= sample; 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; if (world)delete world; } else stds cerr << "open file error" << stds endl; } int main() { build_7_1(); }
效果圖如下:
注意球體下的陰影。 這張照片非常暗,但是我們的球體在光線每次反射時只吸收了一半的能量,因此它們是50%的反射器。
在現實生活中, 這些球體應該是淺灰色的。 其原因在於幾乎所有影象觀看者都假設影象是“伽馬校正的”,這意味著這些0到1的值在被儲存為位元組之前做了一些變換。這種做法有很多好處,但就我們的目的而言,今天不講這個,瞭解即可。
如果我們對我們日常的視覺做一個近似,我們可以使用“gamma 2”,即只是簡單的平方根:
這樣就會得到下圖:
看起來更好些。
感謝您的閱讀,生活愉快~