【UE4】GAMES101 圖形學作業7:光線追蹤 Path Tracing


又名蒙特卡洛路徑追蹤

總覽

  • 在之前的練習中,我們實現了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 定義與課程介紹相反):

      image

      按照本次實驗給出的框架,我們進一步可以將偽代碼改寫為:

      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
      

注意事項

  1. 本次實驗代碼的運行非常慢,建議調試時調整main.cpp 中的場景大小或Render.cpp 中的SPP 數以加快運行速度;此外,還可以實現多線程來進一步加快運算。
  2. 注意數值精度問題,尤其注意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;
    }
    
  • 多線程

    1. 利用 FRunnable 根據cpu核心數創建多個線程,暫時稱為 calc線程
    2. GameThread 預計算屏幕坐標,將數據壓隊TQueque rayTraceQueue 隊列中
    3. 若rayTraceQueue不為空,calc線程則將數據出隊,並開始計算光線路徑
    4. 根據 spp數計算光線后,將色值壓隊到 TQueque pixelQueue 隊列中
    5. GameThread 每次取出 pixelQueue 固定數量的數據,將其寫入到Texture2D當中並更新
  • Microfacet 材質(未實現)

效果

小結

  • 向量計算的時候需要注意方向取值,否則效果不對
  • 向量轉顏色的時候注意范圍,否則顏色不對
  • TQueue 可以用於主線程和多線程之間的數據通信。它是一種無鎖的不限制大小的隊列,支持SPSC(單生產者單消費者)/MPSC(多生產者單消費者)兩種模式。注意TQueue 析構導致Tail is nullptr,從而訪問無效而崩潰
  • 本次在多線程中調用了主線程中的函數,但這些函數只是利用主線程中的數據進行計算,暫時沒出什么問題。
  • UMG 更新貼圖比較慢,創建材質實例賦給image
  • 輪盤賭概率采樣……

附錄


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM