总览
在之前的练习中,我们实现了 Whitted-Style Ray Tracing 算法,并且用 BVH等加速结构对于求交过程进行了加速。在本次实验中,我们将在上一次实验的基础上实现完整的 Path Tracing 算法。至此,我们已经来到了光线追踪版块的最后一节内容。
调通框架
2.1修改的内容
相比上一次实验,本次实验对框架的修改较大,主要在以下几方面:
• 修改了 main.cpp,以适应本次实验的测试模型 CornellBox
• 修改了 Render,以适应 CornellBox 并且支持 Path Tracing 需要的同一 Pixel多次 Sample
• 修改了 Object,Sphere,Triangle,TriangleMesh,BVH,添加了 area 属性与Sample 方法,以实现对光源按面积采样,并在 Scene 中添加了采样光源的接口 sampleLight
• 修改了 Material 并在其中实现了 sample, eval, pdf 三个方法用于 Path Tracing 变量的辅助计算2.2你需要迁移的内容
你需要从上一次编程练习中直接拷贝以下函数到对应位置:
• Triangle::getIntersection in Triangle.hpp: 将你的光线-三角形相交函数粘贴到此处,请直接将上次实验中实现的内容粘贴在此。
• IntersectP(const Ray& ray, const Vector3f& invDir,const std::array<int, 3>& dirIsNeg) in the Bounds3.hpp: 这个函数的2作用是判断包围盒 BoundingBox 与光线是否相交,请直接将上次实验中实现的内容粘贴在此处,并且注意检查 t_enter = t_exit 的时候的判断是否正确。
• getIntersection(BVHBuildNode* node, const Ray ray)in BVH.cpp: BVH查找过程,请直接将上次实验中实现的内容粘贴在此处.2.3编译运行
基础代码只依赖于 CMake,下载基础代码后,执行下列命令,就可以编译这个项目:mkdir build cd ./ build cmake .. make
在此之后,你就可以通过 ./Raytracing 来执行程序。请务必确保程序可以正常编译之后,再进入下一节的内容。
3 开始实现
3.1代码框架
在本次实验中,你只需要修改这一个函数:
• castRay(const Ray ray, int depth)in Scene.cpp: 在其中实现 Path Tracing 算法
可能用到的函数有:
• intersect(const Ray ray)in Scene.cpp: 求一条光线与场景的交点
• sampleLight(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 Maerial.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 的概率
IntersectP(const Ray& ray, const Vector3f& invDir,const std::array<int, 3>& dirIsNeg) in the Bounds3.hpp: 这个函数的作用是判断包围盒 BoundingBox 与光线是否相交,请直接将上次实验中实现的内容粘贴在此处,并且注意检查 t_enter = t_exit 的时候的判断是否正确。
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;
}
BVHBuildNode* BVHAccel::recursiveBuild(std::vector<Object*> objects)
{
BVHBuildNode* node = new BVHBuildNode();
// Compute bounds of all primitives in BVH node
Bounds3 bounds;
for (int i = 0; i < objects.size(); ++i)
bounds = Union(bounds, objects[i]->getBounds());
if (objects.size() == 1) {
// Create leaf _BVHBuildNode_
node->bounds = objects[0]->getBounds();
node->object = objects[0];
node->left = nullptr;
node->right = nullptr;
node->area = objects[0]->getArea();
return node;
}
else if (objects.size() == 2) {
node->left = recursiveBuild(std::vector{objects[0]});
node->right = recursiveBuild(std::vector{objects[1]});
node->bounds = Union(node->left->bounds, node->right->bounds);
node->area = node->left->area + node->right->area;
return node;
}
else {
Bounds3 centroidBounds;
for (int i = 0; i < objects.size(); ++i)
centroidBounds =
Union(centroidBounds, objects[i]->getBounds().Centroid());
int dim = centroidBounds.maxExtent();
switch (dim) {
case 0:
std::sort(objects.begin(), objects.end(), [](auto f1, auto f2) {
return f1->getBounds().Centroid().x <
f2->getBounds().Centroid().x;
});
break;
case 1:
std::sort(objects.begin(), objects.end(), [](auto f1, auto f2) {
return f1->getBounds().Centroid().y <
f2->getBounds().Centroid().y;
});
break;
case 2:
std::sort(objects.begin(), objects.end(), [](auto f1, auto f2) {
return f1->getBounds().Centroid().z <
f2->getBounds().Centroid().z;
});
break;
}
auto beginning = objects.begin();
auto middling = objects.begin() + (objects.size() / 2);
auto ending = objects.end();
auto leftshapes = std::vector<Object*>(beginning, middling);
auto rightshapes = std::vector<Object*>(middling, ending);
assert(objects.size() == (leftshapes.size() + rightshapes.size()));
node->left = recursiveBuild(leftshapes);
node->right = recursiveBuild(rightshapes);
node->bounds = Union(node->left->bounds, node->right->bounds);
node->area = node->left->area + node->right->area;
}
return node;
}
getIntersection(BVHBuildNode* node, const Ray ray)in BVH.cpp: BVH查找过程,请直接将上次实验中实现的内容粘贴在此处.
Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const
{
// TODO Traverse the BVH to find intersection
Intersection inter;
Vector3f invdir(1 / ray.direction.x , 1 / ray.direction.y , 1 / ray.direction.z);
//判断射线的方向正负,如果负,为1;bounds3.hpp中会用到。
std::array<int , 3> dirIsNeg;
dirIsNeg[0] = ray.direction.x < 0;
dirIsNeg[1] = ray.direction.y < 0;
dirIsNeg[2] = ray.direction.z < 0;
//没有交点
if (!node->bounds.IntersectP(ray,invdir,dirIsNeg)){
return inter;
}
//有交点,且该点为叶子节点,去和三角形求交
if (node -> left ==nullptr && node -> right == nullptr){
return node -> object -> getIntersection(ray);
}
//该点为中间节点,继续判断,并返回最近的包围盒交点
Intersection hit1 = getIntersection(node->left , ray);
Intersection hit2 = getIntersection(node->right , ray);
return hit1.distance < hit2.distance ? hit1:hit2;
}
Triangle::getIntersection in Triangle.hpp: 将你的光线-三角形相交函数粘贴到此处,请直接将上次实验中实现的内容粘贴在此。
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)
return inter;
// TODO find ray triangle intersection
inter.normal = normal;
inter.coords = ray(t_tmp);
inter.distance = t_tmp;
inter.happened = true;
inter.m = m;
inter.obj = this;
return inter;
}
castRay(const Ray ray, int depth)in Scene.cpp: 在其中实现 Path Tracing 算法
// Implementation of Path Tracing
Vector3f Scene::castRay(const Ray &ray, int depth) const
{
// TO DO Implement Path Tracing Algorithm here
//创建变量以储存直接和间接光照计算值
Vector3f L_dir = {0,0,0} , L_indir = {0,0,0};
//1.判断是否有交点:光线与场景中物体相交?
Intersection intersection = Scene::intersect(ray); //求一条光线与场景的交点
if (!intersection.happened) //没交点
return {};
//2.ray打到光源了:说明渲染方程只用算前面的自发光项,因此直接返回材质的自发光项
if (intersection.m->hasEmission()){//一、交点是光源:
// if (depth == 0)//第一次打到光
return intersection.m->getEmission();
// return {};//弹射打到光,直接返回0,0.0
}
//---------二、交点是物体:1)向光源采样计算direct----------
Intersection lightpos;
float lightpdf = 0.0f;
//对场景中的光源进行采样,得到采样点light_pos和pdf_light
sampleLight(lightpos,lightpdf) ;//获得对光源的采样,包括光源的位置和采样的pdf(在场景的所有光源上按面积 uniform 地 sample 一个点,并计算该 sample 的概率密度)
//计算光源到物体点的距离
Vector3f collisionlight = lightpos.coords - intersection.coords;
float dis = dotProduct(collisionlight,collisionlight);
//判断光源与物体间有无遮挡,从光源发射一条相同方向的光线打到场景上,比较最后两者的距离
Vector3f collisionlightdir = collisionlight.normalized(); //光源到物体的光线方向
Ray light_to_object_ray (intersection.coords,collisionlightdir); //光源到物体的光线
Intersection light_to_anything_ray = Scene::intersect(light_to_object_ray);
auto f_r = intersection.m -> eval(ray.direction,collisionlightdir,intersection.normal);//材质,课上说了,BRDF==材质,ws不参与计算
//判断光源与物体间有无遮挡
if (light_to_anything_ray.distance - collisionlight.norm() > -0.005){//无遮挡
//渲染方程
L_dir = lightpos.emit * f_r * dotProduct(collisionlightdir , intersection.normal) * dotProduct(-collisionlightdir,lightpos.normal) / dis / lightpdf;
}
//--------二、交点是物体:2)向其他物体采样递归计算indirect---------;
//俄罗斯轮盘赌
if (get_random_float() > RussianRoulette)//打到物体后对半圆随机采样使用RR算法
return L_dir;
//随机生成一个w0方向
Vector3f w0 = intersection.m ->sample(ray.direction , intersection.normal).normalized();//这里的w0其实没参与计算,返回的是一个随机的方向
Ray object_to_object_ray(intersection.coords , w0);
Intersection islight = Scene::intersect(object_to_object_ray);
if (islight.happened && !islight.m->hasEmission()){ //光线击中的不是点光源
float pdf = intersection.m ->pdf(ray.direction,w0,intersection.normal);
f_r = intersection.m->eval(ray.direction,w0,intersection.normal);//材质,课上说了,BRDF==材质,ws不参与计算
//渲染方程
L_indir = castRay(object_to_object_ray,depth+1)*f_r*dotProduct(w0 , intersection.normal) / pdf / RussianRoulette;
}
return L_dir + L_indir;
}
结果:
PS:在windows上运行的时候需要修改global.cpp文件的以下函数,不然可能会跑不了
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);
}