【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 定义与课程介绍相反):按照本次实验给出的框架,我们进一步可以将伪代码改写为:
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
- 轮盘赌概率采样……
附录
作者:砥才人
出处:https://www.cnblogs.com/shiroe
本系列文章为笔者整理原创,只发表在博客园上,欢迎分享本文链接,如需转载,请注明出处!