GAMES101作业7-路径追踪实现过程代码框架超全解读
目錄
Path Tracing算法過程討論
蒙特卡洛積分
直接光照 direct illumination
間接光照 indirect illumination
?編輯
合成全局光照
解決一些存在的問題
問題1:光線爆炸
問題2:遞歸停止條件
問題3:目前算法并不高效
問題3解決方案:采樣光源Sample the light
Shadow:光源遮擋問題
Sample the Light過程偽代碼
Now path tracing is finally done!
框架修改內(nèi)容詳細(xì)解讀
main.cpp
Renderer.cpp
Object.hpp?
getArea()
Sample()
get_random_float()
hasEmit()
Sphere.hpp
area
Sample()(目前沒搞明白這個函數(shù)怎么寫的)
getArea() & hasEmint()
Triangle()
BVH.hpp
BVH.cpp -> getSample()
Material.hpp
toWorld()
sample()
pdf()
eval()
補全castRay()
粘貼作業(yè)6代碼
粘貼需要注意的點
Bounds3.hpp -> IntersectP()
?Triangle.hpp -> getIntersection(Ray ray)
?BVH.hpp ->?getIntersection()
本次實驗給出的與框架匹配的偽代碼
簡單分析一些新出現(xiàn)的函數(shù)
Scene::sampleLight()
代碼具體實現(xiàn)過程
結(jié)果展示
spp=2
spp=4
spp=16
spp=30
spp=60
spp=256
Path Tracing算法過程討論
在正式寫castRay()之前,先讓我們一起理一下Path Tracing的思路。GAMES101課程上總結(jié)出了一個Render Equation渲染方程,而Path Tracing是一種求解渲染方程的方法。
兩個求解關(guān)鍵點
(1)需要解方程后面的積分;
(2)積分部分要用到遞歸。
首先解決第一個關(guān)鍵點——解積分
用什么方法計算積分呢?蒙特卡洛方法,下面簡單復(fù)習(xí)一下蒙特卡洛方法:
蒙特卡洛積分
蒙特卡洛方法告訴我們,當(dāng)我們想求一個在[a,b]上的積分f(x)
任意取一個合理的pdf(概率密度函數(shù))
只需要在積分域內(nèi)以一定的pdf采樣,則這個積分可以近似成求f(x)在N個隨即變量下的均值:
需要注意的是:(1)如果只采樣一個,即N==1,相差就會很大,因此采樣越多(N越大),結(jié)果越接近;(2)在x上積分,就必須采樣x。
接下來開始用蒙特卡洛方法求解積分。暫時忽略物體自發(fā)光(emission)部分,要用蒙特卡羅方法,就需要選取一個合適的pdf。我們知道,渲染方程是定義在半球內(nèi)對立體角的積分,半球面積為2Π,采用均勻采樣的方法,則pdf為:
要把光照分為直接光照和間接光照來討論:
直接光照 direct illumination
考慮光路直接從光源發(fā)出,就能得到直接光照的結(jié)果,得到偽代碼:
void shade(p, wo) {//隨機生成N個方向的wi并以pdf(wi)分布L0 = 0.0;//對每條wiFor each wi{//向wi方向追蹤一條射線ray r(p,wi)Trace a ray r(p,wi)//如果射線擊中光源(相交)If ray r hit the light//寫出求和式L0 += (1 / N) * L_i * f_r * cosine / pdf(wi)}return L0; }直接光照考慮完了之后,接下來要看間接光照,關(guān)于直接光照和間接光照內(nèi)容可以參考:GAMES101作業(yè)5-從頭到尾理解代碼&Whitted光線追蹤_flashinggg的博客-CSDN博客
間接光照 indirect illumination
如圖所示,開始求光線擊中物體的情況。從上圖看,可以看作從P點觀察Q點,把Q點當(dāng)作直接光照,這就跟之前的聯(lián)系起來了,就可以在直接光照偽代碼的基礎(chǔ)上加上:
//射線擊中物體上的p點(射線r與q相交) Else if ray r hit an object at q L0 += (1 / N) * shade(q,-wi) * f_r * cosine / pdf(wi)合成全局光照
加上后,就得到一個遞歸的、支持全局光照的偽代碼:
void shade(p, wo) {//隨機生成N個方向的wi并以pdf(wi)分布L0 = 0.0;//對每條wiFor each wi{//向wi方向追蹤一條射線ray r(p,wi)Trace a ray r(p,wi)//如果射線擊中光源(相交)If ray r hit the light//寫出求和式L0 += (1 / N) * L_i * f_r * cosine / pdf(wi)//射線擊中物體上的p點(射線r與q相交)Else if ray r hit an object at qL0 += (1 / N) * shade(q,-wi) * f_r * cosine / pdf(wi)}return L0; }但我們的問題還未完全解決!
解決一些存在的問題
問題1:光線爆炸
由于加入了遞歸,導(dǎo)致發(fā)出的光線數(shù)量呈指數(shù)增長!計算量將爆炸!很明顯,只有N==1的時候,才能解決這個問題,偽代碼將修改成這樣:
void shade(p, wo) {//隨機選取1個方向的wi并以pdf(wi)分布L0 = 0.0;//向該方向追蹤一條射線ray r(p,wi)Trace a ray r(p,wi)//如果射線擊中光源(相交)If ray r hit the light//寫出求和式Return L_i * f_r * cosine / pdf(wi)//射線擊中物體上的p點(射線r與q相交)Else if ray r hit an object at qReturn shade(q,-wi) * f_r * cosine / pdf(wi) }由此,N==1,就是我們做的Path Tracing路徑追蹤。
上述N==1的方法雖然解決了光線爆炸問題,但又有了新問題:樣本數(shù)量下降,結(jié)果生成的畫面噪點會變得非常多。
針對這個問題,我們可以發(fā)出多條路徑穿過像素,再將這么多路徑的著色結(jié)果求均值即可。
void ray_generation(camPos, pixel) {//在像素上均勻地選取N個樣本位置pixel_radiance = 0.0;//對于每個樣本位置For each sample in the pixel//發(fā)射一條射線r(CamPos, cam_to_sample)shoot a ray r(CamPos, cam_to_sample)//射線r與場景相交與點pIF ray r hit the scene at ppixel_radiance += 1 / N * shade(p, sample_to_cam)return pixel_radiance; }問題2:遞歸停止條件
我們都知道遞歸實現(xiàn)有兩個條件:①問題得到轉(zhuǎn)移;②遞歸要能停。第一個點剛才已經(jīng)解決了,現(xiàn)在是要找到能停止遞歸的條件。
解決方法1:限制彈射遞歸的深度
也就是限制光線的彈射次數(shù),這個方法意味著能量的削減,能量就不守恒了。
解決方法2:俄羅斯輪盤賭
以一定的概率停止繼續(xù)追蹤,想要實現(xiàn)追蹤停止又不改變得到的結(jié)果Lo:
(1)以一定的概率P(0<P<1)發(fā)射光線 -> return Lo/P;
(2)以概率1-P不發(fā)射光線 -> return 0.
可以計算這種方式的期望值E,會發(fā)現(xiàn)以這種方式得到的結(jié)果還會是Lo,但無限遞歸的概率就收斂到0了。
展示成偽代碼:
void shade(p, wo) {//以某種方法確定一個概率P_RR// 隨機選取一個隨機數(shù)ksi∈[0,1]// if(ksi>P_RR) reutn 0;// ksi<P_RR的情況就正常發(fā)射光線,然后結(jié)果需要/P_RR//隨機選取1個方向的wi并以pdf(wi)分布L0 = 0.0;//向該方向追蹤一條射線ray r(p,wi)Trace a ray r(p, wi)//如果射線擊中光源(相交)If ray r hit the light//寫出求和式,這是需要/PReturn L_i * f_r * cosine / pdf(wi) / P_RR//射線擊中物體上的p點(射線r與q相交)Else if ray r hit an object at qReturn shade(q, -wi) * f_r * cosine / pdf(wi) / P_RR }到此為止,Path Tracing就完成了,但其實還有問題:
這個算法并不高效!It's not efficient!
問題3:目前算法并不高效
Why?
從上圖可以看出,現(xiàn)在這種從著色點向外出射光線的采樣方法,打中光源的概率完全看運氣,光源面積大概率就大;光源面積小概率就小,由此可見這種方法并不高效。
需要找到另一種更高效的采樣方法——采樣光源 Sample the Light
問題3解決方案:采樣光源Sample the light
首先要明確一點,我們?yōu)槭裁茨芨淖儾蓸臃椒?#xff1f;因為:蒙特卡洛方法并沒有規(guī)定pdf的選取,我們可以選擇任意一個合適的pdf進行采樣!以此來大大減少光線的浪費,此時采樣對象就從半球立體角dw轉(zhuǎn)換到了光源表面的微面元dA:
那么隨之而來又有新的問題,在之前講蒙特卡洛方法就提到了其中一個注意點:在x上積分,就必須采樣x。因此,我們需要利用數(shù)學(xué)的方法,將積分對象從dw轉(zhuǎn)變到dA.
做一個變量替換,我們的渲染方程就變成了:
基于此,之前的算法依照全局光照的貢獻對象可以分為兩個部分:
(1)光照來自于光源 - 直接光照 不用輪盤賭
(2)其他非光源 - 間接,需要輪盤賭
Shadow:光源遮擋問題
這個好說,直接在進行直接光照計算前判斷是否被遮擋即可。
就得到了Sample the light的偽代碼:
Sample the Light過程偽代碼
shade (p, wo)//直接光照//在光源上均勻選擇一個采樣點x',以pdf_light分布Uniformly sample the light at x' ( pdf_light = 1 / A)//首先判斷光源是否被遮擋:發(fā)射一條連接物體p和x'的射線Shoot a ray from p to x'//如果射線不被遮擋,則計算直接光照:If the ray is not blocked in the middleL_dir = L_i * f_r * cos_theta * cos_theta' / |x'-p|^2 / pdf_light//間接光照L_indir = 0.0//以某種方法確定一個概率P_RR(0<P_RR<1)Test Russian Roulette with probability P_RR//在均勻分布在[0,1]上的樣本中隨機選取一個值ksiIf ksi>P_RRReturn L_dir;//半球上隨機生成一個方向wi,以pdf=1/2Π分布Uniformly sample the hemisphere toward wi ( pdf_hemi = 1 / 2pi)Trace a ray r(p, wi)//如果射線r擊中一個不自發(fā)光的物體(非光源)If ray r hit a non - emitting object at q L_indir = shade (q, -wi) * f_r * cos_theta / pdf_hemi / P_RRReturn L_dir + L_indirNow path tracing is finally done!
把整個Path Tracing的過程學(xué)習(xí)清楚之后,已經(jīng)可以開始完成作業(yè)了。但是我為了了解清楚整個代碼框架每個部分是什么含義,會先把作業(yè)6和這次作業(yè)給的框架進行對比,學(xué)習(xí)并了解新增的內(nèi)容都有哪些,再進行Path Tracing部分。
框架修改內(nèi)容詳細(xì)解讀
讓我們來看看都是什么地方做了改動
main.cpp
新增:定義了一些材質(zhì)。
//與作業(yè)6多了部分:定義了材質(zhì)(顏色)Material* red = new Material(DIFFUSE, Vector3f(0.0f));red->Kd = Vector3f(0.63f, 0.065f, 0.05f);Material* green = new Material(DIFFUSE, Vector3f(0.0f));green->Kd = Vector3f(0.14f, 0.45f, 0.091f);Material* white = new Material(DIFFUSE, Vector3f(0.0f));white->Kd = Vector3f(0.725f, 0.71f, 0.68f);Material* light = new Material(DIFFUSE, (8.0f * Vector3f(0.747f+0.058f, 0.747f+0.258f, 0.747f) + 15.6f * Vector3f(0.740f+0.287f,0.740f+0.160f,0.740f) + 18.4f *Vector3f(0.737f+0.642f,0.737f+0.159f,0.737f)));light->Kd = Vector3f(0.65f);在作業(yè)6中并沒有給模型賦予材質(zhì),而是直接給了光照,可以看到兔子是黑白的
Renderer.cpp
... //出現(xiàn)了與作業(yè)6不同的點:加了整型變量spp// games101里老師提到過:“path tracing其中一個問題就是:并不高效,low spp->它的// spp(sample per pixel)很低,光線會被浪費”// 這里的spp就是指每個pixel會采樣的次數(shù)// change the spp value to change sample ammountint spp = 16;std::cout << "SPP: " << spp << "\n";for (uint32_t j = 0; j < scene.height; ++j) {for (uint32_t i = 0; i < scene.width; ++i) {// generate primary ray directionfloat x = (2 * (i + 0.5) / (float)scene.width - 1) *imageAspectRatio * scale;float y = (1 - 2 * (j + 0.5) / (float)scene.height) * scale;//這里的dir方向跟我作業(yè)6取的不一樣,作業(yè)6里是(x,y,-1)Vector3f dir = normalize(Vector3f(-x, y, 1));//與作業(yè)6不同:每個pixel分成了spp次采樣for (int k = 0; k < spp; k++){framebuffer[m] += scene.castRay(Ray(eye_pos, dir), 0) / spp; }m++;} ...其實這么看來,Whitted-Style Ray Tracing就相當(dāng)于Path?Tracing中spp==1的情況?(不是很確定這么說是否正確)
Object.hpp?
... //與6相比加了新的屬性:area,以實現(xiàn)對光源按面積采樣virtual float getArea()=0;//與6相比加了新屬性:sameplevirtual void Sample(Intersection &pos, float &pdf)=0;virtual bool hasEmit()=0; ...具體涉及到的函數(shù):
getArea()
//在Triangle.hpp & Sphere.hpp 里都有用到 float getArea(){return area; }Sample()
這個函數(shù)將會在Scene.cpp?->?sampleLight()光源采樣接口函數(shù)中用到。
//class MeshTriangle里:void Sample(Intersection &pos, float &pdf){bvh->Sample(pos, pdf);pos.emit = m->getEmission();}//class Sphere:void Sample(Intersection &pos, float &pdf){float theta = 2.0 * M_PI * get_random_float(), phi = M_PI * get_random_float();Vector3f dir(std::cos(phi), std::sin(phi)*std::cos(theta), std::sin(phi)*std::sin(theta));pos.coords = center + radius * dir;pos.normal = dir;pos.emit = m->getEmission();pdf = 1.0f / area;}//class Triangle:void Sample(Intersection &pos, float &pdf){float x = std::sqrt(get_random_float()), y = get_random_float();pos.coords = v0 * (1.0f - x) + v1 * (x * (1.0f - y)) + v2 * (x * y);pos.normal = this->normal;pdf = 1.0f / area;}get_random_float()
gloal.hpp中定義的一個隨機從范圍[0,1]取浮點數(shù)的函數(shù):
//得到范圍為[0.1]的浮點數(shù) inline float get_random_float() {std::random_device dev;std::mt19937 rng(dev());std::uniform_real_distribution<float> dist(0.f, 1.f); // distribution in range [0,1]return dist(rng); }值得注意的是!?
感謝解決了隨機數(shù)的取值問題:Games101 作業(yè)7 路徑追蹤_gong_zi_shu的博客-CSDN博客
其中提到window系統(tǒng)跑這份代碼的同學(xué),需要修改global.cpp 中的get_random_float()函數(shù),不然你的這個"隨機函數(shù)"每次都是跑出來相同的結(jié)果,修改后能顯著提高效率,改為:
inline float get_random_float() {static std::random_device dev;static std::mt19937 rng(dev());static std::uniform_real_distribution<float> dist(0.f, 1.f); // distribution in range [0,1]return dist(rng); }經(jīng)過實踐這個函數(shù)確實能顯著提高效率!后續(xù)會出一片作業(yè)7加速渲染的優(yōu)化方法匯總,展示前后對比。
hasEmit()
//均相同,是:bool hasEmit(){return m->hasEmission();} //m:Material* m; //hasEmission() 是 class Material 定義的一個bool Material::hasEmission() {if (m_emission.norm() > EPSILON) return true;else return false;}hasEmission()
?Material.hpp中定義的一bool類型:
bool Material::hasEmission() {if (m_emission.norm() > EPSILON) return true;//e_emission有長度,即它存在else return false; }Sphere.hpp
與作業(yè)6相比:多了一個 area屬性 和一個 Sample()函數(shù)
area
... //與作業(yè)6相比多了一個areafloat area;...bool intersect(const Ray& ray) {...//整個球面積=4Πr2float area = 4 * M_PI * radius2;...} ...Sample()(目前沒搞明白這個函數(shù)怎么寫的)
對光源采樣,在上面的Object.hpp已經(jīng)體現(xiàn)過了,這里貼一個課上老師將Sampling the light的截圖幫助理解,可以結(jié)合我對代碼的注釋具體理解這個函數(shù):
//將光源進行按面采樣,隨機從光源發(fā)射一條ray打到場景中的sphere上得到某個交點void Sample(Intersection &pos, float &pdf){//theta(θ)∈[0,2Π],控制著// // //phi(φ)∈[0,Π]float theta = 2.0 * M_PI * get_random_float(), phi = M_PI * get_random_float();//dir -> {cosφ,sinφ*cosθ,sinφ*sinθ}Vector3f dir(std::cos(phi), std::sin(phi)*std::cos(theta), std::sin(phi)*std::sin(theta));pos.coords = center + radius * dir;//O+dir*rpos.normal = dir;pos.emit = m->getEmission();pdf = 1.0f / area;//}getArea() & hasEmint()
Object.hpp已有提及:
...float getArea(){return area;}bool hasEmit(){return m->hasEmission();} ...Triangle()
與Sphere.hpp相同,給Triangle和MeshTriangle類也是加上了Area和Sample()
class Triangle : public Object { ...//三角形面積area = crossProduct(e1, e2).norm()*0.5f; ...void Sample(Intersection &pos, float &pdf){float x = std::sqrt(get_random_float()), y = get_random_float();pos.coords = v0 * (1.0f - x) + v1 * (x * (1.0f - y)) + v2 * (x * y);pos.normal = this->normal;pdf = 1.0f / area;}float getArea(){return area;}bool hasEmit(){return m->hasEmission();} };...class MeshTriangle : public Object { ...area = 0; ...void Sample(Intersection &pos, float &pdf){bvh->Sample(pos, pdf);pos.emit = m->getEmission();}float getArea(){return area;}bool hasEmit(){return m->hasEmission();} };BVH.hpp
定義的BVH類中加上了getSample()和Sample()函數(shù)。
... void getSample(BVHBuildNode* node, float p, Intersection &pos, float &pdf);void Sample(Intersection &pos, float &pdf); ...BVH.cpp -> getSample()
void BVHAccel::getSample(BVHBuildNode* node, float p, Intersection &pos, float &pdf){if(node->left == nullptr || node->right == nullptr){node->object->Sample(pos, pdf);pdf *= node->area;return;}if(p < node->left->area) getSample(node->left, p, pos, pdf);else getSample(node->right, p - node->left->area, pos, pdf); }Material.hpp
實現(xiàn)了smple,eval,pdf三個方法用于Path Tracing變量的輔助計算,我們把這部分代碼從頭到尾看一遍:
首先枚舉定義了一個材料類型漫反射材質(zhì),這次作業(yè)好像也只有這一個材質(zhì)類型;
enum MaterialType { DIFFUSE};接下來定義了一個類Material;
class Material{ private:...public:...};(2)private里首先定義三個向量分別代表:反射射線方向、折射射線方向和菲涅爾方程項,都與作業(yè)5類似,就不過多贅述,具體可以去看看我寫的那篇:GAMES101作業(yè)5-從頭到尾理解代碼&Whitted光線追蹤_flashinggg的博客-CSDN博客
... Vector3f reflect(const Vector3f &I, const Vector3f &N) const{return I - 2 * dotProduct(I, N) * N;}//折射射線方向,與作業(yè)5相同Vector3f refract(const Vector3f &I, const Vector3f &N, const float &ior) const{...}//菲涅爾方程,與作業(yè)5相同void fresnel(const Vector3f &I, const Vector3f &N, const float &ior, float &kr) const{...} ...toWorld()
接著加了一個作業(yè)5中沒有的向量toWorld,作用是將localray的半球坐標(biāo)(局部)變換成世界坐標(biāo).,具體過程可以參考我的代碼注釋:
...// 半球坐標(biāo) -> 世界坐標(biāo)// 半球上的坐標(biāo)是局部坐標(biāo)系下的a,認(rèn)為法向量是N方向上的(0,0,1),所以需要轉(zhuǎn)換// 其中局部坐標(biāo)系下:a.x,a.y,a.z在的三個方向相互垂直,其中a.z的方向就是N的方向// 步驟:// 1.假定有B,C兩個單位向量,B,C由N得出,B,C,N兩兩垂直,且B,C,N都是單位向量// 2.讓a.x,a.y,a.z的值分別去乘B,C,N ->沿著B,C,N三個方向按照對應(yīng)值的比例放大// 3.再將得到的三個向量相加,就能在世界坐標(biāo)中出表示出原始的aVector3f toWorld(const Vector3f &a, const Vector3f &N){//假定B,CVector3f B, C;//這里的條件判斷,應(yīng)該是為了避免出現(xiàn)分母為0的情況if (std::fabs(N.x) > std::fabs(N.y)){//至少在x軸上有分量float invLen = 1.0f / std::sqrt(N.x * N.x + N.z * N.z);//我們就不管y軸的事(已知x一定有值)//保證以下兩點:1.用x,z的數(shù)表示出一個單位向量;2.且要與N垂直C = Vector3f(N.z * invLen, 0.0f, -N.x *invLen);}//這里同理,只不過用的是y,z的數(shù)表示了else {float invLen = 1.0f / std::sqrt(N.y * N.y + N.z * N.z);C = Vector3f(0.0f, N.z * invLen, -N.y *invLen);}//按照步驟1,C,N做叉乘得到與C,N都垂直的單位向量BB = crossProduct(C, N);//進行步驟2,分別相乘,再加在一起(步驟3)return a.x * B + a.y * C + a.z * N;} ...至于半球坐標(biāo)的計算sample()在后面會將到,我們繼續(xù)順著代碼看,下面是public,我做了一些簡單的小注釋幫助理解代碼。
... public:MaterialType m_type;//材質(zhì)類型,只給了一個枚舉項:diffuse//Vector3f m_color;Vector3f m_emission;//材質(zhì)自發(fā)光float ior;//材質(zhì)的折射率Vector3f Kd, Ks;//漫反射和高光項float specularExponent;//高光項指數(shù)//Texture tex;inline Material(MaterialType t=DIFFUSE, Vector3f e=Vector3f(0,0,0));//這里e_type=diffuse, e_emission=einline MaterialType getType();//return m_type//inline Vector3f getColor();inline Vector3f getColorAt(double u, double v);//返回當(dāng)下的vector3f?inline Vector3f getEmission();//return m_emissioninline bool hasEmission();//判斷是否有emission//光線擊中某點后,繼續(xù)隨即彈射的方向inline Vector3f sample(const Vector3f &wi, const Vector3f &N);//計算該光線的pdf(概率密度函數(shù)probability density function,描述連續(xù)隨機變量的概率分布)inline float pdf(const Vector3f &wi, const Vector3f &wo, const Vector3f &N);//計算光線的貢獻inline Vector3f eval(const Vector3f &wi, const Vector3f &wo, const Vector3f &N); ...sample()
下面是class里定義涉及到的一些函數(shù),其中sample()用于采樣光線擊中某點后繼續(xù)隨機彈射的方向:——
//采樣光線擊中某點后繼續(xù)隨機彈射的方向 Vector3f Material::sample(const Vector3f &wi, const Vector3f &N){switch(m_type){case DIFFUSE:{// 均勻地對半球采樣//半球z軸值z∈[0,1]// r -> 以法線為旋轉(zhuǎn)軸的半徑,x2+y2+z2=1,r2=x2+y2//phi∈[0,2Π],旋轉(zhuǎn)角度float x_1 = get_random_float(), x_2 = get_random_float();//隨機[0,1]取值float z = std::fabs(1.0f - 2.0f * x_1);//不是很理解為什么不直接取[0,1]隨機數(shù)float r = std::sqrt(1.0f - z * z), phi = 2 * M_PI * x_2;Vector3f localRay(r*std::cos(phi), r*std::sin(phi), z);//半球面上隨機光線的方向//接著需要把半球上的局部光線坐標(biāo)轉(zhuǎn)換成世界坐標(biāo)return toWorld(localRay, N);break;}} }pdf()
概率密度函數(shù)的計算,可以來回顧一下pdf的定義,幫助更好的理解代碼:
參考:03.隨機變量和3F(PDF、CDF、PMF) - 知乎 (zhihu.com)
概率密度函數(shù)(probability density function),用來描述連續(xù)隨機變量的概率分布,連續(xù)型隨機變量的概率密度函數(shù)是一個描述某個確定的取值點附近的可能性的函數(shù)。
例如:正態(tài)分布的PDF:
計算ray的PDF:
如上圖:課程里老師給出了一個簡單的采樣方法——均勻地采樣
pdf就是個常數(shù),整個半球面對應(yīng)的Solid angle:,均勻的采樣pdf就是:
//計算概率密度函數(shù)pdf float Material::pdf(const Vector3f &wi, const Vector3f &wo, const Vector3f &N){switch(m_type){case DIFFUSE://材質(zhì){//均勻采樣,則pdf為常數(shù)1/2Πif (dotProduct(wo, N) > 0.0f)return 0.5f / M_PI;elsereturn 0.0f;break;}} }eval()
計算某個材質(zhì)對光照的貢獻,本作業(yè)用漫反射系數(shù)kd來體現(xiàn):
//計算材質(zhì)貢獻 Vector3f Material::eval(const Vector3f &wi, const Vector3f &wo, const Vector3f &N){switch(m_type){case DIFFUSE:{//計算DIFFUSE貢獻 -> kd float cosalpha = dotProduct(N, wo);//只看半球,另一半不看,所以要判斷一下wo和N的夾角if (cosalpha > 0.0f) {Vector3f diffuse = Kd / M_PI;return diffuse;}elsereturn Vector3f(0.0f);break;}} }Material.hpp到這里就結(jié)束了。
補全castRay()
粘貼作業(yè)6代碼
粘貼需要注意的點
代碼其他的部分直接粘貼作業(yè)6的內(nèi)容,直接粘貼就好但有兩點值得注意:
(1)Intersectp()函數(shù)注意取等號問題:
... return tenter <= texit&& texit >= 0; ...原因在games101學(xué)習(xí)平臺有大神給了解釋:Games101 作業(yè)7 繞坑引路 (Windows) – 計算機圖形學(xué)與混合現(xiàn)實在線平臺 (games-cn.org)
(2)為了大幅度縮短渲染時長,建議在運行代碼前修改get_random_float()函數(shù),這點在上面的代碼注釋中已有提到,這里就只展示修改后的:
inline float get_random_float() {static std::random_device dev;static std::mt19937 rng(dev());static std::uniform_real_distribution<float> dist(0.f, 1.f); // distribution in range [0,1]return dist(rng); }Bounds3.hpp -> IntersectP()
inline bool Bounds3::IntersectP(const Ray& ray, const Vector3f& invDir,const std::array<int, 3>& dirIsNeg) const {Vector3f tmin = (pMin - ray.origin) * invDir;Vector3f tmax = (pMax - ray.origin) * invDir;if (dirIsNeg[0])std::swap(tmin.x, tmax.x);if (dirIsNeg[1])std::swap(tmin.y, tmax.y);if (dirIsNeg[2])std::swap(tmin.z, tmax.z);float texit = std::min(tmax.x, std::min(tmax.y, tmax.z));float tenter = std::max(tmin.x, std::max(tmin.y, tmin.z));return tenter <= texit&& texit >= 0; }?Triangle.hpp -> getIntersection(Ray ray)
inline Bounds3 Triangle::getBounds() { return Union(Bounds3(v0, v1), v2); }inline Intersection Triangle::getIntersection(Ray ray) {Intersection inter;if (dotProduct(ray.direction, normal) > 0)return inter;double u, v, t_tmp = 0;Vector3f pvec = crossProduct(ray.direction, e2);double det = dotProduct(e1, pvec);if (fabs(det) < EPSILON)return inter;double det_inv = 1. / det;Vector3f tvec = ray.origin - v0;u = dotProduct(tvec, pvec) * det_inv;if (u < 0 || u > 1)return inter;Vector3f qvec = crossProduct(tvec, e1);v = dotProduct(ray.direction, qvec) * det_inv;if (v < 0 || u + v > 1)return inter;t_tmp = dotProduct(e2, qvec) * det_inv;if (t_tmp < 0)//t>0 ray是射線return inter;// TODO find ray triangle intersection//給inter所有參數(shù)賦予值inter.happened = true;//有交點inter.coords = ray(t_tmp);//vector3f operator()(double t){return origin+dir*t};inter.normal = normal;//法向量inter.distance = t_tmp;//double distanceinter.obj = this;//this是所有成員函數(shù)的隱藏函數(shù),一個const指針,指向當(dāng)前對象(正在使用的對象)inter.m = m;//class 材質(zhì) mreturn inter; }?BVH.hpp ->?getIntersection()
Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const {Intersection res;std::array<int, 3>dirIsNeg = { int(ray.direction.x<0), int(ray.direction.y<0), int(ray.direction.z<0) };//無交點if (!node->bounds.IntersectP(ray, ray.direction_inv, dirIsNeg)) {return res;}//有交點//無子節(jié)點if (node->left == nullptr && node->right == nullptr) {res = node->object->getIntersection(ray);return res;}//有子節(jié)點 ->遞歸Intersection left, right;left = getIntersection(node->left, ray);right = getIntersection(node->right, ray);return left.distance < right.distance ? left : right; }下面參考作業(yè)給出的偽代碼補全castRay()。
本次實驗給出的與框架匹配的偽代碼
shade (p, wo)sampleLight ( inter , pdf_light )Get x, ws , NN , emit from interShoot a ray from p to xIf the ray is not blocked in the middleL_dir = emit * eval(wo , ws , N) * dot(ws , N) * dot(ws , NN) / |x-p|^2 / pdf_lightL_indir = 0.0Test Russian Roulette with probability RussianRoulettewi = sample (wo , N)Trace a ray r(p, wi)If ray r hit a non - emitting object at qL_indir = shade (q, -wi) * eval (wo , wi , N) * dot(wi , N) / pdf(wo , wi , N) / RussianRouletteReturn L_dir + L_indir簡單分析一些新出現(xiàn)的函數(shù)
Scene::sampleLight()
偽代碼第一步是sampleLight()函數(shù),這個函數(shù)在Scene.cpp中定義的,是實現(xiàn)采樣光源的接口。
//實現(xiàn)了采樣光源的接口 //對場景中的光源進行隨機采樣,以pdf進行 void Scene::sampleLight(Intersection& pos, float& pdf) const {float emit_area_sum = 0;for (uint32_t k = 0; k < objects.size(); ++k) {if (objects[k]->hasEmit()) {//第k個物體有自發(fā)光,hasEmit ->bool量emit_area_sum += objects[k]->getArea();//得到場景中自發(fā)光區(qū)域的面積和,用以后續(xù)求pdf=1/area}}//對場景中的所有光源按面積均勻采樣一個點,計算float p = get_random_float() * emit_area_sum;//隨機取[0, emit_area_sum]之間的浮點數(shù)emit_area_sum = 0;for (uint32_t k = 0; k < objects.size(); ++k) {if (objects[k]->hasEmit()) {emit_area_sum += objects[k]->getArea();if (p <= emit_area_sum) {//隨機選取一個光源面,即第k個自發(fā)光物體的光源面//利用Sample()在光源面中按照pdf的概率隨即找到一個點pos,得到這個點pos的信息objects[k]->Sample(pos, pdf);break;}}} }代碼具體實現(xiàn)過程
步驟我已經(jīng)盡可能的在注釋寫的很詳細(xì)了,直接看代碼:
// Implementation of Path Tracing Vector3f Scene::castRay(const Ray& ray, int depth) const {//創(chuàng)建變量以儲存直接和間接光照計算值Vector3f dir = { 0.0,0.0,0.0 };Vector3f indir = { 0.0,0.0,0.0 };//1.判斷是否有交點:光線與場景中物體相交?Intersection inter = Scene::intersect(ray);//如果沒交點if (!inter.happened) {return dir;//return 0,0,0}//2.ray打到光源了:說明渲染方程只用算前面的自發(fā)光項,因此直接返回材質(zhì)的自發(fā)光項if (inter.m->hasEmission()) {if (depth == 0) {//第一次打到光return inter.m->getEmission();}else return dir;//彈射打到光,直接返回0,0.0}//3.ray打到物體:這個時候才開始進行偽代碼后面的步驟//對場景中的光源進行采樣,得到采樣點light_pos和pdf_lightIntersection light_pos;float pdf_light = 0.0f;sampleLight(light_pos, pdf_light);//3.1計算直接光照//物體的一些參數(shù)Vector3f p = inter.coords;Vector3f N = inter.normal.normalized();Vector3f wo = ray.direction;//物體指向場景//光源的一些參數(shù)Vector3f xx = light_pos.coords;Vector3f NN = light_pos.normal.normalized();Vector3f ws = (p - xx).normalized();//光源指向物體float dis = (p - xx).norm();//二者距離float dis2 = dotProduct((p - xx), (p - xx));//判斷光源與物體間是否有遮擋://發(fā)出一條射線,方向為ws 光源xx -> 物體pRay light_to_obj(xx, ws);//Ray(orig,dir)Intersection light_to_scene = Scene::intersect(light_to_obj);//假如dis>light_to_scene.distance就說明有遮擋,那么反著給條件即可:if (light_to_scene.happened&& (light_to_scene.distance-dis>-EPSILON)) {//沒有遮擋//為了更貼近偽代碼,先設(shè)定一些參數(shù)Vector3f L_i = light_pos.emit;//光強Vector3f f_r = inter.m->eval(wo, -ws, N);//材質(zhì),課上說了,BRDF==材質(zhì),ws不參與計算float cos_theta = dotProduct(-ws, N);//物體夾角float cos_theta_l = dotProduct(ws, NN);//光源夾角dir = L_i * f_r * cos_theta * cos_theta_l / dis2 / pdf_light;}//3.2間接光照//俄羅斯輪盤賭//Scene.hpp中已經(jīng)定義了P_RR:RussianRoulette=0.8float ksi = get_random_float();//隨機取[0,1]if (ksi < RussianRoulette) {//計算間接光照//隨機生成一個wi方向Vector3f wi = inter.m->sample(wo, N).normalized();//這里的wi其實沒參與計算,返回的是一個隨機的方向Ray r(p, wi);Intersection obj_to_scene = Scene::intersect(r);//擊中了物體&&物體不是光源if (obj_to_scene.happened && !obj_to_scene.m->hasEmission()) {Vector3f f_r = inter.m->eval(wo, wi, N);//wo不參與計算float cos_theta = dotProduct(wi, N);float pdf_hemi = inter.m->pdf(wo, wi, N);indir = castRay(r, depth + 1) * f_r * cos_theta / pdf_hemi / RussianRoulette;}}return dir + indir; }結(jié)果展示
spp=2
spp=4
spp=16
spp=256
?
感謝耐心看到這里的人~
總結(jié)
以上是生活随笔為你收集整理的GAMES101作业7-路径追踪实现过程代码框架超全解读的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 什么是健身房管理系统
- 下一篇: 健身、俱乐部和健身房管理软件系统行业调研