本次作业其实就是在 whitted-style 的 ray tracing 上进行改进,实现更常见的 path tracing,原理部分闫老师的课程和作业文档中已经讲地十分清楚,不再赘述(如有一些细节上的疑惑,可以参考官网的论坛作业7发布公告 – 计算机图形学与混合现实在线平台 (games-cn.org) 或者这篇文章Games 101 | 作业7 + 路径追踪 Path Tracing + 多线程 - 知乎 (zhihu.com)),这里主要分享几个可以优化的点。
优化一:多线程加速
对于屏幕上的每个像素,因为不考虑光线之间的碰撞,从每个像素发出的追踪光线其实是相互独立的,故而可以给每个像素使用一个线程用于光线追踪。
在实际中,由于硬件限制,可以考虑在一个线程中绘制多行像素,达到多线程加速的目的。
具体做法是,使用一个绘制函数,新开线程给函数传参,告诉它要绘制那几行像素,使用隐函数会比较方便,参考代码如下:
int num_threads = 64;
std::thread th[num_threads];
int thread_height = scene.height/num_threads;
auto renderRows = [&](uint32_t start_height, uint32_t end_height) {
for (uint32_t j = start_height; j < end_height; ++j) {
for (uint32_t i = 0; i < scene.width; ++i) {
// generate primary ray direction
// random SSAA
for (int k = 0; k < spp; k++){
// trace light
}
}
progressMtx.lock();
progress++;
UpdateProgress(progress / (float)scene.height);
progressMtx.unlock();
}};
for (int t = 0; t < num_threads; ++t) {
th[t] = std::thread(renderRows, t*thread_height, (t+1)*thread_height);
}
for (int t = 0; t < num_threads; ++t) {
th[t].join();
}
其中,加锁是为了 progress 的正常显示,不影响最终结果。
优化二:随机超采样
原来的代码中使用的超采样全都是从像素中心发出光线做平均,光线没有分散开来,这样并不能真正体现超采样的优点,也无法达到抗锯齿的效果。可以考虑使用 0~1 之间的均匀随机位置发出光线,然后再平均,当然也可以采用更好的平均算法,如高斯加权平均等。
// generate primary ray direction
// random SSAA
for (int k = 0; k < spp; k++){
float x = (2 * (i + get_random_float()) / (float)scene.width - 1) * imageAspectRatio * scale;
float y = (1 - 2 * (j + get_random_float()) / (float)scene.height) * scale;
Vector3f dir = normalize(Vector3f(-x, y, 1));
framebuffer[(int)(j*scene.width+i)] += scene.castRay(Ray(eye_pos, dir), 0) / spp;
}
优化三:光线遮挡判断
在判断光线是否被遮挡时,需要从当前点往采样的光源方向发出一个射线,然后判断射线所交的物体是否为光源。很多人的做法是通过距离进行判断,但这样的判断往往存在浮点数误差问题,需要引入一个 EPSILON 放宽限制。
在这里,其实可以直接判断光线所交物体是否为采样的光源,只需要改一个地方:
在三角形求交时需要存储 Object 指针;
inline Intersection Triangle::getIntersection(Ray ray)
{
// ...
// TODO find ray triangle intersection
if(t_tmp <= 0) {
return inter;
}
inter.happened = true;
inter.coords = ray(t_tmp);
inter.normal = normal;
inter.distance = t_tmp;
inter.m = m;
inter.obj = this;
return inter;
}
然后就行可以直接判断,所交物体是否为采样的光源;
Intersection lightDirBlock = intersect(Ray(p, lightDir));
Vector3f L_dir;
//if(lightDirBlock.distance - (p-lightPos).norm() > -10 * EPSILON) {
if(lightInter.obj == lightDirBlock.obj) {
L_dir = lightEmit * pInter.m->eval(ray.direction, lightDir, pNormal) *
dotProduct(lightDir, pNormal) * dotProduct(-lightDir, lightNormal) /
pow((p-lightPos).norm(), 2) / lightPdf;
}
优化四:随机数生成
在提供的随机数生成函数中,使用局部变量来存储随机分布的相关变量,这样会使得每次生成都需要重新初始化变量,会带来一些性能瓶颈。
可以直接把所需要的变量设为静态,这样就只会进行一次初始化。
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);
}
优化结果
最后,使用SPP=256,64线程,在8G内存8核CPU的条件下,共运行了23分钟,效果如下: