天天看點

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

目錄

  • 1. 作業描述
    • 1.1 修改的内容
    • 1.2 你需要遷移的内容
    • 1.3 代碼架構
  • 2. 解
    • 2.1 遷移部分(Triangle::getIntersection、Bounds3::IntersectP、BVHAccel::getIntersection)
    • 2.2 修改部分(Scene::castRay)
      • 2.2.1 僞代碼
      • 2.2.2 實作
  • 3. 提高
    • 3.1 多線程
      • 3.1.1 std::thread
      • 3.1.2 OpenMP (推薦,更快的方法!!!)
      • 3.1.3 效果(32線程,spp=4)
    • 3.2 Microfacet
      • 3.2.1 全鏡面反射BRDF分析
      • 3.2.2 基本設定
      • 3.2.3 采樣方向
      • 3.2.4 機率密度函數
      • 3.2.5 BRDF
      • 3.2.7 castRay的修改
      • 3.2.8 最終效果
    • 3.3 MSAA抗鋸齒
    • 3.4 其他模型
  • 4. 一些可能遇到的問題
    • 4.1 渲染耗時過長?
    • 4.2 渲染結果中光源區域為純黑?
    • 4.3 渲染結果較暗?
    • 4.4 天花闆黑色,牆面沒有影子?
    • 4.5 多線程編譯時出現undefined reference to `pthread_create’?
    • 4.6 渲染出現白色噪點?
  • 5. 效果集合
  • 6. 附件

1. 作業描述

在之前的練習中,我們實作了 Whitted-Style Ray Tracing 算法,并且用 BVH等加速結構對于求交過程進行了加速。在本次實驗中,我們将在上一次實驗的基礎上實作完整的 Path Tracing 算法。至此,我們已經來到了光線追蹤版塊的最後一節内容。

1.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 變量的輔助計算

1.2 你需要遷移的内容

你需要從上一次程式設計練習中直接拷貝以下函數到對應位置:

  • 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查找過程,請直接将上次實驗中實作的内容粘貼在此處

1.3 代碼架構

在本次實驗中,你隻需要修改這一個函數: • 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

    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 的機率

2. 解

2.1 遷移部分(Triangle::getIntersection、Bounds3::IntersectP、BVHAccel::getIntersection)

這部分是将上次作業的代碼直接移植過來,沒有特别要修改的地方

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;

    // TODO find ray triangle intersection
    inter.happened = true;
    inter.obj = this;
    inter.distance = t_tmp;
    inter.normal = normal;
    inter.coords = ray(t_tmp);
    inter.m = this->m;
    return inter;
}
           
inline bool Bounds3::IntersectP(const Ray& ray, const Vector3f& invDir, const std::array<int, 3>& dirIsNeg) const
{
    // invDir: ray direction(x,y,z), invDir=(1.0/x,1.0/y,1.0/z), use this because Multiply is faster that Division
    // dirIsNeg: ray direction(x,y,z), dirIsNeg=[int(x>0),int(y>0),int(z>0)], use this to simplify your logic
    // TODO test if ray bound intersects
    const auto& origin = ray.origin;
	float ten = -std::numeric_limits<float>::infinity();
	float tex = std::numeric_limits<float>::infinity();
	for (int i = 0; i < 3; i++)
	{
		float min = (pMin[i] - origin[i]) * invDir[i];
		float max = (pMax[i] - origin[i]) * invDir[i];
		if (!dirIsNeg[i])
			std::swap(min, max);
		ten = std::max(min, ten);
		tex = std::min(max, tex);
    }
    return ten <= tex && tex >= 0;
}

           
Intersection BVHAccel::getIntersection(BVHBuildNode* node, const Ray& ray) const
{
    // TODO Traverse the BVH to find intersection
    Intersection intersect, intersectl, intersectr;
    std::array<int, 3> dirIsNeg;
	dirIsNeg[0] = int(ray.direction.x >= 0);
	dirIsNeg[1] = int(ray.direction.y >= 0);
	dirIsNeg[2] = int(ray.direction.z >= 0);
    if(!node->bounds.IntersectP(ray, ray.direction_inv))
        return intersect;
    if(node->left == nullptr && node->right == nullptr){
        intersect = node->object->getIntersection(ray);
        return intersect;
    }
    intersectl = getIntersection(node->left,ray);
    intersectr = getIntersection(node->right,ray);
    return intersectl.distance < intersectr.distance ? intersectl : intersectr;
}

           

2.2 修改部分(Scene::castRay)

2.2.1 僞代碼

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
           

2.2.2 實作

首先就是利用intersect函數判斷光線與場景的交點,如果沒有交點自然就不用繼續往下求了,如果有交點的話判斷是不是打到光源上,是的話也是直接傳回光源顔色,因為這裡預設光源反射其他方向光線的部分可以忽略不計

// Implementation of Path Tracing
Vector3f Scene::castRay(const Ray &ray, int depth) const
{
    // TO DO Implement Path Tracing Algorithm here
    Intersection intersec = intersect(ray);
    if (!intersec.happened) {
        return Vector3f();
    }

    // 打到光源
    if (intersec.m->hasEmission()) {
        return intersec.m->getEmission();
    }

    Vector3f l_dir(0,0,0);
    Vector3f l_indir(0,0,0);
    ...
           

接下來是直接光照的部分,這裡采用的是對光源求積分的方式(用蒙特卡洛積分簡化了),注意要判斷光線是否被遮擋的問題(對光源采樣出來的光線做一次求交,如果交點距離小于到光源的距離,說明被遮擋了):

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
...
    // 直接光照
    Intersection lightInter;
    float lightPdf = 0.0f;
    sampleLight(lightInter, lightPdf);

    Vector3f obj2light = lightInter.coords - intersec.coords;
    Vector3f obj2lightDir = obj2light.normalized();
    float obj2lightPow = obj2light.x * obj2light.x + obj2light.y * obj2light.y + obj2light.z * obj2light.z;

    Ray obj2lightRay(intersec.coords, obj2lightDir);
    Intersection t = intersect(obj2lightRay);
    if (t.distance - obj2light.norm() > -EPSILON)
    {
        l_dir = lightInter.emit * intersec.m->eval(ray.direction, obj2lightDir, intersec.normal) 
            * dotProduct(obj2lightDir, intersec.normal) 
            * dotProduct(-obj2lightDir, lightInter.normal) 
            / obj2lightPow / lightPdf;
    }
    ...
           

接下來為了保證光線不會無限反射,用俄羅斯輪盤賭的方式決定光線是否繼續:

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
...
    if (get_random_float() > RussianRoulette) {
        return l_dir;
    }
    ...
           

若光線存活,這繼續對間接光照的求解,因為已經對光源進行積分了,是以這裡的間接光照求的是不發光的物體反射的光線:

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

(因為求的是随機采樣的結果,為了保證能量守恒,需要對結果再除一個俄羅斯輪盤賭的機率)

// 間接光照
    ...
    Vector3f obj2nextobjdir = intersec.m->sample(ray.direction, intersec.normal).normalized();
    Ray obj2nextobjray(intersec.coords, obj2nextobjdir);
    Intersection nextObjInter = intersect(obj2nextobjray);
    if (nextObjInter.happened && !nextObjInter.m->hasEmission())
    {
        float pdf = intersec.m->pdf(ray.direction, obj2nextobjdir, intersec.normal);
        l_indir = castRay(obj2nextobjray, depth + 1) 
            * intersec.m->eval(ray.direction, obj2nextobjdir, intersec.normal) 
            * dotProduct(obj2nextobjdir, intersec.normal)
            / pdf / RussianRoulette;
    }
    return l_dir + l_indir;
}
           

3. 提高

3.1 多線程

為提升程式執行效率,我們可以考慮利用多線程并發執行,這裡我們可以從render函數發射primary ray的時候入手,将螢幕像素分成多塊給多個線程執行,比如我們的scene尺寸為784*784,要用32個線程并發執行時,就将每塊設定為(784/32) * 784的大小,接下來介紹兩種多線程的做法:

3.1.1 std::thread

(出現undefined reference to `pthread_create’或其他編譯問題的可以劃到下面的第四部分看解決方法哦)

使用的頭檔案:

#include < thread >

#include < mutex >

void Renderer::Render(const Scene& scene)
{
    std::vector<Vector3f> framebuffer(scene.width * scene.height);

    float scale = tan(deg2rad(scene.fov * 0.5));
    float imageAspectRatio = scene.width / (float)scene.height;
    Vector3f eye_pos(278, 273, -800);
    int m = 0;
    int thread_num = 32;
    int thread_step = scene.height / thread_num;
    std::vector<std::thread> rays;
    // change the spp value to change sample ammount
    int spp = 1024;
    std::cout << "SPP: " << spp << "\n";
    for (int i = 0; i < thread_num; i++) 
        rays.push_back(std::thread(para, eye_pos, std::ref(framebuffer), std::ref(scene), spp, 
                    imageAspectRatio, scale, i * thread_step, (i + 1) * thread_step));
    for (int i = 0; i < thread_num; i++) 
        rays[i].join();
    UpdateProgress(1.f);
    ...
           

這裡我們定義一個para函數作為多線程的入口,别忘記定義一個mutex鎖用來鎖住進度條更新相關語句,用來避免多線程同時通路全局變量的時候出現沖突

int prog = 0;
std::mutex lock;

void para(Vector3f eye_pos, std::vector<Vector3f> &framebuffer, const Scene& scene, int spp, float imageAspectRatio, float scale, int start, int end){
    int width, height;
    width = height = sqrt(spp);
    float step = 1.0f / width;
    for (uint32_t j = start; j < end; ++j) {
        for (uint32_t i = 0; i < scene.width; ++i) {
            // generate primary ray direction   
            for (int k = 0; k < spp; k++){
                float x = (2 * (i + step / 2 + step * (k % width)) / (float)scene.width - 1) *
                        imageAspectRatio * scale;
                float y = (1 - 2 * (j + step / 2 + step * (k / height)) / (float)scene.height) * scale;
                Vector3f dir = normalize(Vector3f(-x, y, 1));
                framebuffer[j * scene.width + i] += scene.castRay(Ray(eye_pos, dir), 0) / spp;  
            }
        }
        lock.lock();
        prog++;
        UpdateProgress(prog / (float)scene.height);
        lock.unlock();
    }
}
           

同時别忘記用join方法等待所有線程結束,防止有的線程還沒結束主程式就結束了

for (int k = 0; k < spp; k++)
        rays[k].join();
           

3.1.2 OpenMP (推薦,更快的方法!!!)

使用的頭檔案:

#include <omp.h>

操作非常簡單:

在你需要并行化的for前面,加上

它可以将跟在後面的for循環語句分成多個線程并發執行

然後在cmakelists.txt中加上

重新執行cmake …然後編譯即可。

上面的-O3也可以提速。

void Renderer::Render(const Scene& scene)
{
    std::vector<Vector3f> framebuffer(scene.width * scene.height);

    float scale = tan(deg2rad(scene.fov * 0.5));
    float imageAspectRatio = scene.width / (float)scene.height;
    Vector3f eye_pos(278, 273, -800);
    int m = 0;
    int thread_num = 32;
    int thread_step = scene.height / thread_num;
    std::vector<std::thread> rays;
    // change the spp value to change sample ammount
    int spp = 4;
    std::cout << "SPP: " << spp << "\n";
    #pragma omp parallel for
        for (int i = 0; i < thread_num; i++) 
            para(eye_pos, std::ref(framebuffer), std::ref(scene), spp, 
                    imageAspectRatio, scale, i * thread_step, (i + 1) * thread_step);
    UpdateProgress(1.f);
           

更新進度條時加鎖:

omp_set_lock(&lock1);
        prog++;
        UpdateProgress(prog / (float)scene.height);
        omp_unset_lock(&lock1);
           

同樣也要記住把鎖設定為全局變量:

3.1.3 效果(32線程,spp=4)

不用多線程:

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

std::thread(32線程):

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

openMP:

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

可見,openMP簡直就是為本次作業量身定做的方法

3.2 Microfacet

3.2.1 全鏡面反射BRDF分析

對于一個全鏡面反射來說, 隻需要考慮菲涅反射的系數, 也就是說

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

這裡 wr 和 wo 是關于表面法線對稱的.

現在的問題是, 我們如何描述這裡的 fr BRDF項? 答案是使用狄拉克 函數, 狄拉克 函數滿足:

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

也就是隻能接收到唯一一個方向的光線

這樣, 将狄拉克函數帶入到方程中, 得到:

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

得到BRDF的表示為:

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

3.2.2 基本設定

首先在Material枚舉類中添加MIRROR材質

然後在MAIN函數裡設定MIRROR參數,并且将兩個長方體設定為MIRROR材質

Material* white1 = new Material(MIRROR, Vector3f(0.0f));
white1->Kd = Vector3f(0.0f, 0.0f, 0.0f);
white1->ior = 40.0f;
MeshTriangle shortbox("../models/cornellbox/shortbox.obj", white1);
MeshTriangle tallbox("../models/cornellbox/tallbox.obj", white1);
           

3.2.3 采樣方向

全鏡面反射隻有一個方向的光線能被眼睛接收,是以采樣函數就隻傳回反射方向

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
Vector3f Material::sample(const Vector3f &wi, const Vector3f &N){
    switch(m_type){
        case DIFFUSE:
        {
            // uniform sample on the hemisphere
            float x_1 = get_random_float(), x_2 = get_random_float();
            float z = std::fabs(1.0f - 2.0f * x_1);
            float r = std::sqrt(1.0f - z * z), phi = 2 * M_PI * x_2;
            Vector3f localRay(r*std::cos(phi), r*std::sin(phi), z);
            return toWorld(localRay, N);
            break;
        }
        case MIRROR:
        {
            Vector3f localRay = reflect(wi, N);
            return localRay;
            break;
        }
    }
}
           
Vector3f reflect(const Vector3f &I, const Vector3f &N) const
{
	return I - 2 * dotProduct(I, N) * N;
}
           

3.2.4 機率密度函數

因為全鏡面反射隻有一個方向的光線能被眼睛接收,是以pdf就設定為1

float Material::pdf(const Vector3f &wi, const Vector3f &wo, const Vector3f &N){
    switch(m_type){
        case DIFFUSE:
        {
            // uniform sample probability 1 / (2 * PI)
            if (dotProduct(wo, N) > 0.0f)
                return 0.5f / M_PI;
            else
                return 0.0f;
            break;
        }
        case MIRROR:
        {
            if (dotProduct(wo, N) > EPSILON)
                return 1.0f;
            else
                return 0.0f;
            break;
        }
    }
}
           

3.2.5 BRDF

為了保證最終結果隻和菲涅爾項和反射光線有關,brdf裡還要抵消掉cosθi的影響:

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
Vector3f Material::eval(const Vector3f &wi, const Vector3f &wo, const Vector3f &N){
    switch(m_type){
        case DIFFUSE:
        {
            // calculate the contribution of diffuse   model
            float cosalpha = dotProduct(N, wo);
            if (cosalpha > 0.0f) {
                Vector3f diffuse = Kd / M_PI;
                return diffuse;
            }
            else
                return Vector3f(0.0f);
            break;
        }
        case MIRROR:
        {
            float cosalpha = dotProduct(N, wo);
            float kr;
            if (cosalpha > EPSILON) {
                fresnel(wi, N, ior, kr);
                Vector3f mirror = 1 / cosalpha;
                return kr * mirror;        
            }
            else
                return Vector3f(0.0f);
            break;
        }
    }
}
           
void fresnel(const Vector3f &I, const Vector3f &N, const float &ior, float &kr) const
{
	float cosi = clamp(-1, 1, dotProduct(I, N));
	float etai = 1, etat = ior;
	if (cosi > 0) {  std::swap(etai, etat); }
	// Compute sini using Snell's law
	float sint = etai / etat * sqrtf(std::max(0.f, 1 - cosi * cosi));
	// Total internal reflection
	if (sint >= 1) {
		kr = 1;
	}
	else {
		float cost = sqrtf(std::max(0.f, 1 - sint * sint));
		cosi = fabsf(cosi);
		float Rs = ((etat * cosi) - (etai * cost)) / ((etat * cosi) + (etai * cost));
		float Rp = ((etai * cosi) - (etat * cost)) / ((etai * cosi) + (etat * cost));
		kr = (Rs * Rs + Rp * Rp) / 2;
	}
}
           

3.2.7 castRay的修改

當然,完成了鏡面反射的BRDF,我們其實還有一件事要做,還記得之前的渲染方程裡面我們計算了直接光照和間接光照嗎,記得直接光照是對光源積分嗎?沒錯,我們這裡不能再對光源積分了,因為鏡面材質不像漫反射那樣會把各個方向光源的光都反射過來,加上對光源積分可能會導緻面向光源的面過曝全白(感謝評論區 @木木是小呆呆 同學回報的圖檔):

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

是以我們在castRay裡的直接和間接光照計算需要分Diffuse和Mirror兩個情況來讨論,對于Mirror材質,我們直接把直接光照設定為0,但是間接光照要注意不能照搬,之前我們對于Diffuse材質的間接光照是隻對不發光的物體采樣積分,這是因為我們在直接光照裡已經對光源積分了,但是Mirror我們并沒有計算直接光照,是以别忘了把非發光物體的判斷條件給去掉,我們接收所有物體入射的光!

// Implementation of Path Tracing
Vector3f Scene::castRay(const Ray &ray, int depth) const
{
    // TO DO Implement Path Tracing Algorithm here
    Intersection intersec = intersect(ray);
    if (!intersec.happened) {
        return Vector3f();
    }

    // 打到光源
    if (intersec.m->hasEmission()) {
        return intersec.m->getEmission();
    }


    Vector3f l_dir(0,0,0);
    Vector3f l_indir(0,0,0);
    switch(intersec.m->getType()){
        case DIFFUSE:{
            // 對光源積分
            Intersection lightInter;
            float lightPdf = 0.0f;

            sampleLight(lightInter, lightPdf);

            Vector3f obj2light = lightInter.coords - intersec.coords;
            Vector3f obj2lightDir = obj2light.normalized();
            float obj2lightPow = obj2light.x * obj2light.x + obj2light.y * obj2light.y + obj2light.z * obj2light.z;

            Ray obj2lightRay(intersec.coords, obj2lightDir);
            Intersection t = intersect(obj2lightRay);
            if (t.distance - obj2light.norm() > -EPSILON)
            {
                l_dir = lightInter.emit * intersec.m->eval(ray.direction, obj2lightDir, intersec.normal) 
                    * dotProduct(obj2lightDir, intersec.normal) 
                    * dotProduct(-obj2lightDir, lightInter.normal) 
                    / obj2lightPow / lightPdf;
            }

            if (get_random_float() > RussianRoulette) {
                return l_dir;
            }

            // 對其他方向積分
            Vector3f obj2nextobjdir = intersec.m->sample(ray.direction, intersec.normal).normalized();
            Ray obj2nextobjray(intersec.coords, obj2nextobjdir);
            Intersection nextObjInter = intersect(obj2nextobjray);
            if (nextObjInter.happened && !nextObjInter.m->hasEmission())
            {
                float pdf = intersec.m->pdf(ray.direction, obj2nextobjdir, intersec.normal);
                if (pdf > EPSILON)
                {
                    l_indir = castRay(obj2nextobjray, depth + 1) 
                        * intersec.m->eval(ray.direction, obj2nextobjdir, intersec.normal) 
                        * dotProduct(obj2nextobjdir, intersec.normal)
                        / pdf / RussianRoulette;
                }
            }           
            break;
        }
        case MIRROR:{
            if (get_random_float() > RussianRoulette) {
                return l_dir;
            }
            Vector3f obj2nextobjdir = intersec.m->sample(ray.direction, intersec.normal).normalized();
            Ray obj2nextobjray(intersec.coords, obj2nextobjdir);
            Intersection nextObjInter = intersect(obj2nextobjray);
            if (nextObjInter.happened)
            {
                float pdf = intersec.m->pdf(ray.direction, obj2nextobjdir, intersec.normal);
                if (pdf > EPSILON)
                {
                    l_indir = castRay(obj2nextobjray, depth + 1) 
                        * intersec.m->eval(ray.direction, obj2nextobjdir, intersec.normal) 
                        * dotProduct(obj2nextobjdir, intersec.normal)
                        / pdf / RussianRoulette;
                }
            }
            break;
        }
    }
    return l_dir + l_indir;
}
           

3.2.8 最終效果

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

鏡面反射到牆上的高光區域的效果不太好,看起來像是一堆噪點,是因為該區域在進行采樣的時候直接光照隻對光源積分,但是對于全鏡面反射過來的光源也應該看做一種光源,然後進行專門的重要性采樣,而不是當作漫反射光源進行采樣,這樣的話可能隻有較少的機率采樣到鏡面反射過來的光源,也就是圖中很明顯的離散的高光區域,這個問題可以通過提升spp進行解決,如下圖,但是效率真的比較差:

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

除了針對鏡面反射的光源的重要性采樣之外,為了進一步優化視覺效果,還可以加入伽馬矯正以符合人眼色彩觀測經驗,大概效果如下,因為時間問題我沒有繼續做了,有興趣的朋友可以自己去嘗試一下:

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

3.3 MSAA抗鋸齒

原代碼中隻是重複計算spp次從一個像素發出的光線,最終取平均而已,但是這樣就隻是得到該點像素中心比較接近現實光追的顔色,但是對于計算機顯示來說,他并沒有解決該點像素周圍的平滑過渡問題,比如圖中兩個長方體的邊界:

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

為了解決這個問題,我們在對一個像素進行spp次采樣的同時将這個像素分為spp個小像素,并從這些像素的中心發出primary ray,這樣每個像素的顔色就可以實作和周圍像素的平滑過渡了:

for (int k = 0; k < spp; k++){
	float x = (2 * (i + step / 2 + step * (k % width)) / (float)scene.width - 1) *
	        imageAspectRatio * scale;
	float y = (1 - 2 * (j + step / 2 + step * (k / height)) / (float)scene.height) * scale;
	Vector3f dir = normalize(Vector3f(-x, y, 1));
	framebuffer[m] += scene.castRay(Ray(eye_pos, dir), 0) / spp;  
}
           
【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

3.4 其他模型

添加作業自帶的bunny模型時,記得要對模型進行縮放,平移和旋轉,否則模型将無法在鏡頭裡面顯示,這裡給出我用到的參數:平移是(200,-60,150),縮放是Vector3f(1500,1500,1500),旋轉是繞y軸180°

我們直接對MeshTriangle的構造函數修改一下就行了:

MeshTriangle bunny("../models/bunny/bunny.obj", white1, Vector3f(200,-60,150), 
        Vector3f(1500,1500,1500), Vector3f(-1,0,0), Vector3f(0,1,0), Vector3f(0,0,-1));
           
MeshTriangle(const std::string& filename, Material *mt = new Material(),
                Vector3f Trans = Vector3f(0.0,0.0,0.0), Vector3f Scale = Vector3f(1.0,1.0,1.0), 
                Vector3f xr = Vector3f(1.0,0,0), Vector3f yr = Vector3f(0,1.0,0),  Vector3f zr = Vector3f(0,0,1))
    {
        objl::Loader loader;
        loader.LoadFile(filename);
        area = 0;
        m = mt;
        assert(loader.LoadedMeshes.size() == 1);
        auto mesh = loader.LoadedMeshes[0];

        Vector3f min_vert = Vector3f{std::numeric_limits<float>::infinity(),
                                     std::numeric_limits<float>::infinity(),
                                     std::numeric_limits<float>::infinity()};
        Vector3f max_vert = Vector3f{-std::numeric_limits<float>::infinity(),
                                     -std::numeric_limits<float>::infinity(),
                                     -std::numeric_limits<float>::infinity()};
        for (int i = 0; i < mesh.Vertices.size(); i += 3) {
            std::array<Vector3f, 3> face_vertices;

            for (int j = 0; j < 3; j++) {
                auto vert = Vector3f(mesh.Vertices[i + j].Position.X,
                                     mesh.Vertices[i + j].Position.Y,
                                     mesh.Vertices[i + j].Position.Z);
                                     
                vert.x = dotProduct(vert, xr);
                vert.y = dotProduct(vert, yr);
                vert.z = dotProduct(vert, zr);//旋轉
                vert = Scale*vert+Trans;//平移,縮放
                ...
           
【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

4. 一些可能遇到的問題

4.1 渲染耗時過長?

使用了多線程,但是一次spp為1000以上的渲染還是要幾個小時?可能是global.hpp下的get_random_float()随機數生成函數存在問題,它會導緻在重複調用該函數時,傳回同一個值。

解決方法:把其中定義的dev,rng,dist變量定義為靜态變量(加個static修飾),這樣最後的時間消耗就縮短了大概幾十倍

4.2 渲染結果中光源區域為純黑?

作業給的僞代碼中缺少了光線直接與光源相交的部分,是以是純黑,記得加上這部分

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

4.3 渲染結果較暗?

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

如果有渲染結果較暗,出現橫向黑色條紋的情況,那麼,很可能是因為直接光部分由于精度問題,被錯誤判斷為遮擋,可以試着通過精度調整放寬相交限制(将EPSILON變量增大),同時可能因為老師課上說浮點數相等可能性很低,是以在判斷條件中設定為 t_enter < t_exit而忘了等号,是以可能會有一定的出入,這次作業一定不要忘記加上

4.4 天花闆黑色,牆面沒有影子?

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

同樣的,因為cornell box是由牆壁面組成的,使得包圍盒高度為0,是以 t_exit >= 0也不要忘了等号,否則會導緻天花闆黑色,且盒子在地闆和牆面沒有影子

4.5 多線程編譯時出現undefined reference to `pthread_create’?

pthread 庫不是 Linux 系統預設的庫,連接配接時需要使用靜态庫 libpthread.a.

是以在使用pthread_create()建立線程時,需要連結該庫。

解決方法是打開CMakelists.txt,添加下列語句:

cmake_minimum_required (VERSION 2.6)

find_package (Threads)

add_executable (myapp main.cpp …)

target_link_libraries (myapp ${CMAKE_THREAD_LIBS_INIT})

本作業中為:

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

同時還要注意,往多線程的啟動函數裡傳入參數的時候,需要引用的參數一定要用std::ref來拷貝,否則構析的時候會出錯,如:

rays.push_back(std::thread(para, eye_pos, std::ref(framebuffer), std::ref(scene), spp, 
                    		imageAspectRatio, scale, i * thread_step, (i + 1) * thread_step));
           

4.6 渲染出現白色噪點?

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

原因應該是pdf接近于0時,除以它計算得到的顔色會偏向極限值,展現在圖上也就是白色

要解決這個問題,對于pdf接近于0的情況直接将它的radience算作0就行:

float pdf = intersec.m->pdf(ray.direction, obj2nextobjdir, intersec.normal);
if (pdf > EPSILON)
{
     l_indir = castRay(obj2nextobjray, depth + 1) 
     * intersec.m->eval(ray.direction, obj2nextobjdir, intersec.normal) 
     * dotProduct(obj2nextobjdir, intersec.normal)
     / pdf / RussianRoulette;
}
           

優化後效果:

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

5. 效果集合

【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件
【GAMES101】作業7(提高)路徑追蹤 多線程、Microfacet(全鏡面反射)、抗鋸齒1. 作業描述2. 解3. 提高4. 一些可能遇到的問題5. 效果集合6. 附件

6. 附件

附上源代碼,有興趣的朋友可以自己嘗試一下效果:

CSDN:【GAMES101】作業7

GITHUB:【GAMES101】作業合集

繼續閱讀