【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
  • 轮盘赌概率采样……

附录

posted @ 2021-10-29 21:35  砥才人  阅读(1849)  评论(0编辑  收藏  举报