又名蒙特卡洛路徑追蹤
總覽
- 在之前的練習中,我們實現了Whitted-Style Ray Tracing 算法,並且用BVH等加速結構對於求交過程進行了加速。
- 在本次實驗中,我們將在上一次實驗的基礎上實現完整的Path Tracing 算法。
- 至此,我們已經來到了光線追蹤版塊的最后一節內容。請認真閱讀本文檔,按照本文檔指示的流程完成本次實驗。
代碼框架
- 修改的內容
相比上一次實驗,本次實驗對框架的修改較大,主要在以下幾方面:- 修改了main.cpp,以適應本次實驗的測試模型CornellBox
- 修改了Render,以適應CornellBox 並且支持Path Tracing 需要的同一Pixel多次Sample
- 修改了Object,Sphere,Triangle,TriangleMesh,BVH,添加了area 屬性與Sample 方法,以實現對光源按面積采樣,並在Scene 中添加了采樣光源的接口sampleLight
- 修改了Material 並在其中實現了sample, eval, pdf 三個方法用於Path Tracing變量的輔助計算
- 你需要遷移的內容
你需要從上一次編程練習中直接拷貝以下函數到對應位置:Triangle::getIntersection
in Triangle.hpp:
將你的光線-三角形相交函數粘貼到此處,請直接將上次實驗中實現的內容粘貼在此。IntersectP(const Ray& ray, const Vector3f& invDir,const std::array<int, 3>& dirIsNeg)
in the Bounds3.hpp:
這個函數的作用是判斷包圍盒BoundingBox 與光線是否相交,請直接將上次實驗中實現的內容粘貼在此處,並且注意檢查t_enter = t_exit 的時候的判斷是否正確。getIntersection(BVHBuildNode* node, const Ray ray)
in BVH.cpp:
BVH查找過程,請直接將上次實驗中實現的內容粘貼在此處.
- 本次作業需要實現的內容
在本次實驗中,你只需要修改這一個函數:-
castRay(const Ray ray, int depth)
in Scene.cpp: 在其中實現Path Tracing算法 -
可能用到的函數有:
intersect(const Ray ray)
in Scene.cpp:
求一條光線與場景的交點ampleLight(Intersection pos, float pdf)
in Scene.cpp:
在場景的所有光源上按面積uniform 地sample 一個點,並計算該sample 的概率密度sample(const Vector3f wi, const Vector3f N)
in Material.cpp:
按照該材質的性質,給定入射方向與法向量,用某種分布采樣一個出射方向pdf(const Vector3f wi, const Vector3f wo, const Vector3f N)
in Material.cpp:
給定一對入射、出射方向與法向量,計算sample 方法得到該出射方向的概率密度eval(const Vector3f wi, const Vector3f wo, const Vector3f N)
in Material.cpp:
給定一對入射、出射方向與法向量,計算這種情況下的f_r 值
-
可能用到的變量有:
RussianRoulette
in Scene.cpp:
P_RR, Russian Roulette 的概率
-
Path Tracing 的實現說明
課程中介紹的Path Tracing 偽代碼如下(為了與之前框架保持一致,wo 定義與課程介紹相反):按照本次實驗給出的框架,我們進一步可以將偽代碼改寫為:
shade (p, wo) sampleLight (inter , pdf_light ) Get x, ws , NN , emit from inter Shoot a ray from p to x If the ray is not blocked in the middle L_dir = emit * eval(wo , ws , N) * dot(ws , N) * dot(ws , NN) / |x-p|^2 / pdf_light L_indir = 0.0 Test Russian Roulette with probability RussianRoulette wi = sample (wo , N) Trace a ray r(p, wi) If ray r hit a non - emitting object at q L_indir = shade (q, wi) * eval (wo , wi , N) * dot(wi , N) / pdf(wo , wi , N) / RussianRoulette Return L_dir + L_indir
-
注意事項
- 本次實驗代碼的運行非常慢,建議調試時調整main.cpp 中的場景大小或Render.cpp 中的SPP 數以加快運行速度;此外,還可以實現多線程來進一步加快運算。
- 注意數值精度問題,尤其注意pdf 接近零的情況,以及sampleLight 時判斷光線是否被擋的邊界情況。這些情況往往會造成渲染結果噪點過多,或出現黑色橫向條紋。
材質的拓展
- 目前的框架中拆分sample, eval, pdf,實現了最基礎的Diffuse 材質。請在不破壞這三個函數定義方式的情況下修改這三個函數,實現Microfacet 模型。
- 本任務不要求你實現復雜的采樣手段,因此你依然可以沿用Diffuse 材質采用的sample與pdf 計算。
- Microfacet 相關知識見第十七講Slides https://sites.cs.ucsb.edu/~lingqi/teaching/resources/GAMES101_Lecture_17.pdf
評分
- [5 points] 提交格式正確,包含所有需要的文件;代碼可以在虛擬機下正確編譯運行。
- [45 points] Path Tracing:正確實現Path Tracing 算法,並提交分辨率不小於512*512,采樣數不小於8 的渲染結果圖片。
- [加分項10 points] 多線程:將多線程應用在Ray Generation 上,注意實現時可能涉及的沖突。
- [加分項10 points] Microfacet:正確實現Microfacet 材質,並提交可體現Microfacet 性質的渲染結果。
UE4 實現
-
版本 4.26.2
-
Render(const Ray& ray, int depth)
和castRay(const Ray ray, int depth)
//【多線程】 FVector AHw7_Main::Render(const Ray& ray, int depth) { FVector hitColor = FVector::ZeroVector; for (int i = 0; i < SPP; i++) { hitColor += castRay(ray, depth); } hitColor /= SPP; return hitColor; } //【多線程】 FVector AHw7_Main::castRay(const Ray& ray, int depth) { Intersection hit_inter = bvhTree->Intersect(ray); // 獲取相交信息 FVector hitColor = FVector::ZeroVector; if (hit_inter.happened) { // 判斷是否直接打中發光源 if (hit_inter.m->hasEmission()) { if (depth == 0) { // 主線程中繪制 hitColor = hit_inter.m->getEmission(); if (bAllowDrawDebug) { AsyncTask(ENamedThreads::GameThread, [=]() { UKismetSystemLibrary::DrawDebugLine(GetWorld(), ray.origin, hit_inter.coords, hitColor, 0.1f, 1.0f); } ); } return hitColor; } else // 間接打到光源 return FVector::ZeroVector; } //return hitColor; FVector hit_pos = hit_inter.coords; FVector hit_normal = hit_inter.normal; // 直接光照 FVector L_dir = FVector::ZeroVector; Intersection light_inter; float light_pdf = 0; sampleLight(light_inter, light_pdf); //隨機采樣光照,用采樣結果判斷是否打到光源 FVector light_dir = light_inter.coords - hit_pos; float light_distance2 = FVector::DotProduct(light_dir, light_dir); light_dir.Normalize(); Ray light_ray = Ray(hit_pos, light_dir); Intersection Inter_light_2_point = bvhTree->Intersect(light_ray); // 反射光線 // 如果打到光源 if (Inter_light_2_point.happened && Inter_light_2_point.m->hasEmission()) { // L_dir = L_i * f_r * cos_theta * cos_theta_x / |x-p|^2 / pdf_light // L_dir = emit * eval(wo , ws , N) * dot(ws , N) * dot(ws , NN) / |x-p|^2 / pdf_light FVector L_i = light_inter.emit; FVector f_r = hit_inter.m->eval(ray.direction, light_dir, hit_normal); float cos_theta = FVector::DotProduct(hit_normal, light_dir); float cos_theta_x = FVector::DotProduct(-light_dir, light_inter.normal); //此處注意向量方向 L_dir = L_i * f_r * cos_theta * cos_theta_x / light_distance2 / light_pdf; } // 間接光照 FVector L_indir = FVector::ZeroVector; if (UKismetMathLibrary::RandomFloat() < RussianRoulette) { FVector next_dir = hit_inter.m->sample(ray.direction, hit_normal); next_dir.Normalize(); Ray next_ray(hit_pos, next_dir); Intersection next_hit_inter = bvhTree->Intersect(next_ray); if (next_hit_inter.happened && !next_hit_inter.m->hasEmission()) { // L_indir = shade (q, wi) * f_r * cos_theta / pdf_hemi / P_RR // L_indir = shade (q, wi) * eval (wo , wi , N) * dot(wi , N) / pdf(wo , wi , N) / RussianRoulette FVector f_r = hit_inter.m->eval(ray.direction, next_dir, hit_normal); float pdf = hit_inter.m->pdf(ray.direction, next_dir, hit_normal); float cos_theta = FVector::DotProduct(hit_normal, next_dir); L_indir = castRay(next_ray, depth + 1) * f_r * cos_theta / pdf / RussianRoulette; } } hitColor = L_dir + L_indir; if (bAllowDrawDebug){ AsyncTask(ENamedThreads::GameThread, [=]() { UKismetSystemLibrary::DrawDebugLine(GetWorld(), ray.origin, hit_pos, hitColor, 0.1f, 1); } ); } } return hitColor; }
-
多線程
- 利用 FRunnable 根據cpu核心數創建多個線程,暫時稱為 calc線程
- GameThread 預計算屏幕坐標,將數據壓隊TQueque rayTraceQueue 隊列中
- 若rayTraceQueue不為空,calc線程則將數據出隊,並開始計算光線路徑
- 根據 spp數計算光線后,將色值壓隊到 TQueque pixelQueue 隊列中
- GameThread 每次取出 pixelQueue 固定數量的數據,將其寫入到Texture2D當中並更新
-
Microfacet 材質(未實現)
效果
-
-
gif
小結
- 向量計算的時候需要注意方向取值,否則效果不對
- 向量轉顏色的時候注意范圍,否則顏色不對
TQueue
可以用於主線程和多線程之間的數據通信。它是一種無鎖的不限制大小的隊列,支持SPSC(單生產者單消費者)/MPSC(多生產者單消費者)兩種模式。注意TQueue
析構導致Tail is nullptr,從而訪問無效而崩潰- 本次在多線程中調用了主線程中的函數,但這些函數只是利用主線程中的數據進行計算,暫時沒出什么問題。
- UMG 更新貼圖比較慢,創建材質實例賦給image
- 輪盤賭概率采樣……