GAMES101作业7及课程总结(重点实现多线程加速,微表面模型材质)

news2024/9/22 3:59:46

目录

  • 闲言碎语
  • 最终全部效果展示(均为1024×1024×512ssp)
  • 课程总结与理解(Path Tracing)
  • 框架梳理
  • 任务一:迁移相关代码
  • 任务二:实现path tracing
  • 任务三:多线程加速(包括其他加速的小trick)
    • 1.随机数构造器优化
    • 2.多线程加速
    • 3.debug改成release版本
  • 任务四:微表面模型
    • 1. 微表面材质定义
    • 2. 微表面分布的表达
      • 2.1. D(h):法线分布函数(**Normal Distribution Fuction,NDF**)
      • 2.1. G(i,o,h):几何遮挡函数(**Shadowing and Making term**)
    • 3.光在每个微表面散射的BRDF
    • 4.Torrance-Sparrow Model
    • 5.代码实现
  • 任务五:完美反射模型(看到顺便实现了,作业没要求)
  • 代码实现可能遇到的问题
    • 1.光源融入到整个天花板,没有出现黑色
    • 2.箱子背阴处有很多黑色噪点
    • 3.有白色噪点(ssp调高仍然有)
  • 理论思考
    • 1.偏移为什么要分两种情况这么偏移?
    • 2.为什么path tracing有软阴影?
    • 3.把witted style和path tracing的结果对比一下,两者的实现的不同点有哪些?
    • 4.为什么程序中,渲染方程的入射radiance是光源的emit,是一个常量?
    • 5.为什么说光栅化比光追快?
  • 个人感悟
  • 参考资料

闲言碎语

前言:从这篇文章开始,转知乎了,感觉知乎的社区氛围更好,emmm,哈哈哈主要还是想接触一下知乎的图形学大佬们。知乎账号链接:https://www.zhihu.com/people/xuu-27-24

emmm,距离完成作业6已经过去了一个月多的时间(我承认中间有段时间在摆烂==),怎么说呢,感觉作业7真的知识体系非常庞大,虽然实现的代码量不多,但是里面真的很多细节很多trick,对我这种小菜鸡感觉还是有挺大难度的。。。再加上感觉自己挺钻牛角尖的,所以搞了挺久的,也最终算是完成了作业7的全部要求,害,希望后面本科毕设不要太赶吧。

提个建议:如果遇到实在解决不了的问题,可以上games论坛看看,里面有很多共性的问题,上面的问题我几乎都遇到了。。。

最终全部效果展示(均为1024×1024×512ssp)

基础path tracing实现(作业基本要求)
在这里插入图片描述
微表面材质
在这里插入图片描述
完美镜面反射材质
在这里插入图片描述

课程总结与理解(Path Tracing)

课程方面,主要是先讲辐射度量学,然后推导出渲染方程,之后介绍用来求解积分的蒙特卡洛方法,最后讲如何在程序中求解渲染方程计算颜色(包括很多的trick细节,比如对光源进行直接采样提高采样效率,俄罗斯轮盘赌解决无限递归等等),从而实现path tracing。

path tracing流程:整体的渲染流程实际上更多的是在Whitted-Style上的改进,前期添加物体,构建包围盒,这些都是一样的。不一样的地方在于path tracing在最终着色的时候本质上是求解一个渲染方程,需要做大量的采样,近似积分结果,里面小trick非常多。而Whitted-Style并没有做采样求积分,就是简单的光线反射和折射,无法像path tracing一样考虑全局光照。 总体上来说,就是每个像素点发出多条光线,每条光线会通过之前构建的包围盒找到与场景中物体的第一个交点,然后根据渲染方程计算该交点的颜色,并返回,最后将全部发射的光线的颜色求平均,就得到该像素的颜色。

其实这个渲染方程本身的原理还是挺复杂的,可以研究的很深入,尤其辐射度量学那块,课程本身其实讲的并不是很细,如果想细致了解的话可以去翻虎书,虎书可以说对辐射度量学进行了深度的解刨,直接从光子开始讲。。。

框架梳理

和作业6使用的框架其实差不多,可以参考我之前写的https://blog.csdn.net/Xuuuuuuuuuuu/article/details/128556319

作业7的框架实际上主要就是castRay函数有所不同,因为path tracing的最终实现是在castRay中实现。

任务一:迁移相关代码

之前写的在这里:https://blog.csdn.net/Xuuuuuuuuuuu/article/details/128556319,把相关的代码贴进去就行。其中要注意的一个点,IntersectP中的等于号一定要去掉,要不然之后会出现部分物体不可见的情况,去掉的位置在这里,见下图:

在这里插入图片描述
为什么这里要去掉等于号,之前闫老师说,图形学编程中很少考虑等于号的情况,几乎是可以忽略的。这是因为这次的场景比较特殊,以右边的绿色墙壁为例,实际上它的Boundbox就是一个与某一轴平行长方形(不是长方体,不信的话可以printBoundbox的那两个点出来看看)所以光线和这个Boundbox相交检测的时候,算出来的t_enter和t_exit是相等的。此时如果相等时仍判断为不相交,就会出错,此时部分物体不可见,如下图:

在这里插入图片描述

任务二:实现path tracing

代码量也不是很大,主要就是跟着作业中给的伪代码照着敲,但是想呈现出最终的结果有很多小细节要注意,其中这里面各个向量的方向问题就很容易搞错,一定要仔细琢磨清楚。因为里面要修的细节太多了,我这边直接给出任务二完整的代码(个人建议不要完全照抄,可以自己先跟着作业提供的伪代码敲一下,遇到问题再去自己想办法找资料解决,在最终得到想要的结果后,你会发现这个过程非常值得。),具体的很多细节问题放到后文讨论。很多细节的问题建议直接看代码,可以理解的更清楚。

Vector3f Scene::castRay(const Ray& ray, int depth) const
{
    // TO DO Implement Path Tracing Algorithm here
    Vector3f hitColor = this->backgroundColor;
    Intersection shade_point_inter = Scene::intersect(ray);
    if (shade_point_inter.happened)
    {

        Vector3f p = shade_point_inter.coords;
        Vector3f wo = ray.direction;
        Vector3f N = shade_point_inter.normal;
        Vector3f L_dir(0), L_indir(0);

       //sampleLight(inter,pdf_light)
        Intersection light_point_inter;
        float pdf_light;
        sampleLight(light_point_inter, pdf_light);
        //Get x,ws,NN,emit from inter
        Vector3f x = light_point_inter.coords;
        Vector3f ws = normalize(x-p);
        Vector3f NN = light_point_inter.normal;
        Vector3f emit = light_point_inter.emit;
        float distance_pTox = (x - p).norm();
        //Shoot a ray from p to x
        Vector3f p_deviation = (dotProduct(ray.direction, N) < 0) ?
                p + N * EPSILON :
                p - N * EPSILON ;

        Ray ray_pTox(p_deviation, ws);
        //If the ray is not blocked in the middleff
        Intersection blocked_point_inter = Scene::intersect(ray_pTox);
        if (abs(distance_pTox - blocked_point_inter.distance < 0.01 ))
        {
            L_dir = emit * shade_point_inter.m->eval(wo, ws, N) * dotProduct(ws, N) * dotProduct(-ws, NN) / (distance_pTox * distance_pTox * pdf_light);
        }
        //Test Russian Roulette with probability RussianRouolette
        float ksi = get_random_float();
        if (ksi < RussianRoulette)
        {
            //wi=sample(wo,N)
            Vector3f wi = normalize(shade_point_inter.m->sample(wo, N));
            //Trace a ray r(p,wi)
            Ray ray_pTowi(p_deviation, wi);
            //If ray r hit a non-emitting object at q
            Intersection bounce_point_inter = Scene::intersect(ray_pTowi);
            if (bounce_point_inter.happened && !bounce_point_inter.m->hasEmission())
            {
                float pdf = shade_point_inter.m->pdf(wo, wi, N);
				if(pdf> EPSILON)
					L_indir = castRay(ray_pTowi, depth + 1) * shade_point_inter.m->eval(wo, wi, N) * dotProduct(wi, N) / (pdf *RussianRoulette);
            }
        }
        hitColor = shade_point_inter.m->getEmission() + L_dir + L_indir;
    }
    return hitColor;
}

应该能得到下图所示的结果(1024×1024×512ssp):在这里插入图片描述

任务三:多线程加速(包括其他加速的小trick)

我觉得加速是渲染中一个非常重要的点,也是我个人比较感兴趣的点。tmd没有加速之前,代码的运行速度简直惨不忍睹。。。一开始1024×1024×512ssp的图渲染了七八个小时才渲染了百分之20(渲染到百分之20我就直接放弃了==)。。。最终经过不懈努力,总算把速度优化到了40分钟,可以说快了几乎60-80倍。渲染速度的提高可以说极大地帮助了后面的调试!!!

按照下面三步逐步进行优化,我渲染一张512×512×8ssp的图的速度从13分钟提到了10秒钟

1.随机数构造器优化

使用C++的性能分析器:https://blog.csdn.net/u011942101/article/details/123656944

不难发现,主要是求交和采样两部分花了很多的时间,求交这部分本来就耗时很长,里面很多递归。但是为啥,采样这部分要这么长时间?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nHT8YMY1-1676280691347)(C:\Users\lenovo\AppData\Roaming\Typora\typora-user-images\image-20230120142906735.png)]

最后发现是这个函数,特别耗时。一分析是因为构建开销过大,实际上把里面三个变量都定义成static就行(只会初始化一次,避免重复构建),没必要重新构建。经测试,改成static后,512×512×8ssp的渲染速度从13分钟提到了5分钟。

在这里插入图片描述
参考:https://games-cn.org/forums/topic/zuoyeqidexingnengpingjingshisuijishushengcheng/

2.多线程加速

使用多线程加速之前,问自己一个问题,为什么这里可以使用多线程加速?因为这里满足两个条件:1.这个场景下程序可以写成多线程,2.电脑多核。

实际上实现多线程加速不是很难,因为整个场景太适合写成多线程加速了。。。就每个线程分别独立一定数量计算像素颜色就行。原则上就是要最大化每个线程的计算量,对整个frame计算使用多线程。 具体使用过的是C++里面的thread进行多线程,代码如下:

// The main render function. This where we iterate over all pixels in the image,
// generate primary rays and cast these rays into the scene. The content of the
// framebuffer is saved to a file.
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;

    // change the spp value to change sample ammount
    int spp = 512;//16
    int thread_num = 8;//我的电脑有8核,所以开8个线程。注:屏幕的高度一定要是线程数的倍数
    int thread_height = scene.height / thread_num;
    std::vector<std::thread> threads(thread_num);
    std::cout << "SPP: " << spp << "\n";
    
    //多线程实现
    std::mutex mtx;
    float process=0;
    float Reciprocal_Scene_height=1.f/ (float)scene.height;
    auto castRay = [&](int thread_index) 
    {
        int height = thread_height * (thread_index + 1);
        for (uint32_t j = height - thread_height; j < height; j++)
        {
            for (uint32_t i = 0; i < scene.width; ++i) {
                // generate primary ray direction
                float x = (2 * (i + 0.5) / (float)scene.width - 1) *
                    imageAspectRatio * scale;
                float y = (1 - 2 * (j + 0.5) / (float)scene.height) * scale;

                //eye的位置对结果有影响
                Vector3f dir = normalize(Vector3f(-x, y, 1));
                for (int k = 0; k < spp; k++)
                {
                    framebuffer[j*scene.width+i] += scene.castRay(Ray(eye_pos, dir), 0) / spp;  
                }
            }
            mtx.lock();
            process = process + Reciprocal_Scene_height;
            UpdateProgress(process);
            mtx.unlock();
        }
    };

    for (int k = 0; k < thread_num; k++)
    {
        threads[k] = std::thread(castRay,k);
    }
    for (int k = 0; k < thread_num; k++)
    {
        threads[k].join();
    }
    UpdateProgress(1.f);
    
    // save framebuffer to file
    FILE* fp = fopen("binary.ppm", "wb");
    (void)fprintf(fp, "P6\n%d %d\n255\n", scene.width, scene.height);
    for (auto i = 0; i < scene.height * scene.width; ++i) {
        static unsigned char color[3];
        color[0] = (unsigned char)(255 * std::pow(clamp(0, 1, framebuffer[i].x), 0.6f));
        color[1] = (unsigned char)(255 * std::pow(clamp(0, 1, framebuffer[i].y), 0.6f));
        color[2] = (unsigned char)(255 * std::pow(clamp(0, 1, framebuffer[i].z), 0.6f));
        fwrite(color, 1, 3, fp);
    }
    fclose(fp);    
}

512×512×8ssp的渲染速度从5分钟提到了1分钟!!!
真的很担心cpu被烧坏。。。这种情况下说明多线程编程成功,CPU被疯狂地调用。

在这里插入图片描述

3.debug改成release版本

无心之举,512×512×8ssp的渲染速度从1分钟提到了10秒钟。。。 只能说release比debug版本快了真的不止一点半点。

任务四:微表面模型

101讲的不是很细,因此我就花了不少时间仔细地看了下202和一些其他资料。

参考1:https://zhuanlan.zhihu.com/p/434964126(写得贼nb,推导地很深入)

参考2:https://zhuanlan.zhihu.com/p/152226698(这个更加全面一点,涉及的更广,但1更深)

本质上微表面材质是描述高光项,真正的brdf是由漫反射项+高光项组成的,如下(参考2):

在这里插入图片描述

各自的比例是由菲涅尔项计算的,而菲涅尔项是折射和反射的比例,这里漫反射等价于折射(因为漫反射从微观角度是折射进入物体,然后多次反弹弹出表面,产生漫反射现象)。

说白了就是一部分算折射然后反弹出来的能量,一部分算反射出来的能量。

在这里插入图片描述

1. 微表面材质定义

说白了,就是把一个物体表面看作有多个微表面组成的表面,从而展示更加真实的材质细节。

下图的虚线就是物体表面,而对其着色的时候微表面模型会将物体表面近似成下图实线尖尖样子进行着色,具体就是通过定义微表面模型的prdf实现的。

思想和凹凸贴图那边有点想,实际上没有改变物体的几何模型,但是通过改变了prdf,使物体有了多个微表面组成的感觉。所以有了这句话:远处看是材质和外观,近处看是几何。

在这里插入图片描述

以太空图为例,远处看一个物体的时候看不到很微小的东西,看到的是一个全局效果。

在这里插入图片描述

ok,上面主要大致讲微表面模型大概是个什么东西,下面就是具体深入的理解,

微表面模型主要分成两个部分(也就是说设计一个微表面模型就必须要考虑到这两部分):

①微表面分布的表达(全局,比如法线分布、几何遮挡等)

②光在每个微表面散射的BRDF(细节,单个微表面的材质,即prdf,可以是完美镜面反射,又可以是理想漫反射)

接下来依次介绍以上两部分,然后再介绍最常见的Torrance-Sparrow模型。

2. 微表面分布的表达

包括D(h)和G(i,o,h)两块,即法线分布和几何遮挡。

2.1. D(h):法线分布函数(Normal Distribution Fuction,NDF

不同的法线分布会产生完全不一样的材质。

在这里插入图片描述

D(h)本质上是面积微元dA上的法向为h的全部微表面的面积和面积微元dA比例

注:dA是宏观表面上的面积微元。

不难发现,D(h)实际上是针对物体宏观表面上的一个面积微元的,而不是整个物体。

在这里插入图片描述

D(h)的推导公式

在这里插入图片描述

①beckmann NDF

类似高斯分布函数,里面就是有那两个量,α应该是超参数,控制整个分布的宽瘦,θ就是h代进去计算得到的。为什么这么去设计这个函数,是有它的意义在里面。

在这里插入图片描述

②GGX NDF

相较于Beckmann NDF,有了个长尾巴,意味着会有更好的过渡。

在这里插入图片描述

在这里插入图片描述

2.1. G(i,o,h):几何遮挡函数(Shadowing and Making term

描述微表面之间互相遮挡的几何现象的数学模型,本质上是法向为h的微表面在w方向上可见的比例(因为有部分被遮挡了,见下面第一张图)

遮挡项,因为光线或者视线非常平(第二张图的球边界,此时角度称为grazing angle),f的分母会非常小,因此f就会非常大,边界就会非常亮,此时就要引入遮挡项解决这个问题,进行衰减,因为这种情况下,是最容易出现shadowing-masking现象的。

具体推导可以看上述的链接,讲的非常清楚!!!

在这里插入图片描述

在这里插入图片描述

3.光在每个微表面散射的BRDF

这里又可以有很多文章可以做,可以把每个微表面看作理想镜面(完美反射),也可以看成理想玻璃表面(又有折射又有反射),还可以看成漫反射表面。

结合上述两个微表面的分布项,再定义每个微表面的材质,就能推导出相关的微表面模型。

下文以Torrance-Sparrow Model为例。

4.Torrance-Sparrow Model

把每个微表面看成理想镜面。

推导过程看不懂,我太菜了,就这样吧,该章节一开始的链接里有。

在这里插入图片描述

5.代码实现

实现的是上述的Torrance-Sparrow Model,F项是代码中已经写好的菲涅尔项,D项和G项选择GGX,如下图所示:
在这里插入图片描述

在这里插入图片描述
其中各参数的含义可以查看原文https://zhuanlan.zhihu.com/p/434964126,或者可以直接看代码,应该也能够理解。代码实现方面,先在定义里面添加Microfacet材质,如下:

enum MaterialType { DIFFUSE, MICROFACET}

然后在main函数中添加物体,并对其赋予Microfacet材质,如下:

    Material* microfacet = new Material(MICROFACET, Vector3f(0.0f));
    microfacet->Ks = Vector3f(0.45, 0.45, 0.45);
    microfacet->Kd = Vector3f(0.3, 0.3, 0.25);
    microfacet->ior = 12.85;
    Sphere sphere1(Vector3f(150, 100, 200), 100, microfacet);

    scene.Add(&sphere1);

sample和pdf直接照抄原本的就行。最后修改eval,在其中实现Torrance-Sparrow Model模型即可,如下:

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 MICROFACET:
        {
            float cosalpha = dotProduct(N, wo);
            if (cosalpha > 0.0f) 
            {
                // calculate the contribution of Microfacet model
                float F, G, D;

                fresnel(wi, N, ior, F);

                float Roughness = 1;//超参数,控制粗糙度
                auto G_function = [&](const float& Roughness, const Vector3f& wi, const Vector3f& wo, const Vector3f& N)
                {
                    float A_wi, A_wo;
                    A_wi = (-1 + sqrt(1 + Roughness * Roughness * pow(tan(acos(dotProduct(wi, N))), 2))) / 2;
                    A_wo = (-1 + sqrt(1 + Roughness * Roughness * pow(tan(acos(dotProduct(wo, N))), 2))) / 2;
                    float divisor = (1 + A_wi + A_wo);
                    if (divisor < 0.001)
                        return 1.f;
                    else
                        return 1.0f / divisor;
                };
                G = G_function(Roughness, -wi, wo, N);

                auto D_function = [&](const float& Roughness, const Vector3f& h, const Vector3f& N)
                {
                    float cos_sita = dotProduct(h, N);
                    float divisor = (M_PI * pow(1.0 + cos_sita * cos_sita * (Roughness * Roughness - 1), 2));
                    if (divisor < 0.001)
                        return 1.f;
                    else 
                        return (Roughness * Roughness) / divisor;
                };
                Vector3f h = normalize(-wi + wo);
                D = D_function(Roughness, h, N);

                // energy balance
                Vector3f diffuse = (Vector3f(1.0f) - F) * Kd / M_PI;
                Vector3f specular;
                float divisor= ((4 * (dotProduct(N, -wi)) * (dotProduct(N, wo))));
                if (divisor < 0.001)
                    specular= Vector3f(1);
                else
                    specular = F *G * D / divisor;
                //std::cout << "F:"<<F << "\n";
                //std::cout << "diffuse:"<<diffuse<<"\n";
                //std::cout << "specular:" << specular << "\n";
                return diffuse+specular;
            }
            else
                return Vector3f(0.0f);
            break;
        }
    }
}

最终效果(均为1024×1024×512ssp)
在这里插入图片描述
如果黑色噪点很多,尝试修改一下sphere的求交判断阈值,有可能是精度的问题,如下:

    Intersection getIntersection(Ray ray){
        Intersection result;
        result.happened = false;
        Vector3f L = ray.origin - center;
        float a = dotProduct(ray.direction, ray.direction);
        float b = 2 * dotProduct(ray.direction, L);
        float c = dotProduct(L, L) - radius2;
        float t0, t1;
        if (!solveQuadratic(a, b, c, t0, t1)) return result;
        if (t0 < 0) t0 = t1;
        if (t0 < 0) return result;
        if (t0 > 0.5)
        {
            result.happened = true;
            result.coords = Vector3f(ray.origin + ray.direction * t0);
            result.normal = normalize(Vector3f(result.coords - center));
            result.m = this->m;
            result.obj = this;
            result.distance = t0;
        }

        return result;

    }

如果白色噪点很多,在castRay函数的最后,限制一下hitColor的范围,避免出现特别大的值,影响采样质量。如下:

        hitColor = shade_point_inter.m->getEmission()+L_dir + L_indir;
        hitColor.x = (clamp(0, 1, hitColor.x));
        hitColor.y = (clamp(0, 1, hitColor.y));
        hitColor.z = (clamp(0, 1, hitColor.z));

任务五:完美反射模型(看到顺便实现了,作业没要求)

核心是重要性采样,参考这篇文章实现的:https://blog.csdn.net/ycrsw/article/details/124408789,各种细节这里面已经讲得非常清楚了,因此就不再赘述,直接给出代码,和Microfacet材质的实现流程差不多,首先添加材质,如下:

enum MaterialType { DIFFUSE, MICROFACET,MIRROR};

main函数中添加物体,赋予材质,

    Material* mirror = new Material(MIRROR, Vector3f(0.0f));
    mirror->Ks = Vector3f(0.45, 0.45, 0.45);
    mirror->Kd = Vector3f(0.3, 0.3, 0.25);
    mirror->ior = 12.85;
    
    MeshTriangle floor("../models/cornellbox/floor.obj", white);
    MeshTriangle shortbox("../models/cornellbox/shortbox.obj", white);
    MeshTriangle tallbox("../models/cornellbox/tallbox.obj", mirror);
    MeshTriangle left("../models/cornellbox/left.obj", red);
    MeshTriangle right("../models/cornellbox/right.obj", green);
    MeshTriangle light_("../models/cornellbox/light.obj", light);

最后修改eval,在eval中实现完美镜面反射(Mirror),如下:

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);
            if (cosalpha > 0.0f) 
            {
                float divisor = cosalpha;
                if (divisor < 0.001) return 0;
                Vector3f mirror = 1 / divisor;
                float F;
                fresnel(wi, N, ior, F);
                return F * mirror;
            }
            else
                return Vector3f(0.0f);
            break;
        }
        case MICROFACET:
        {
            float cosalpha = dotProduct(N, wo);
            if (cosalpha > 0.0f) 
            {
                // calculate the contribution of Microfacet model
                float F, G, D;

                fresnel(wi, N, ior, F);

                float Roughness = 1;//超参数,控制粗糙度
                auto G_function = [&](const float& Roughness, const Vector3f& wi, const Vector3f& wo, const Vector3f& N)
                {
                    float A_wi, A_wo;
                    A_wi = (-1 + sqrt(1 + Roughness * Roughness * pow(tan(acos(dotProduct(wi, N))), 2))) / 2;
                    A_wo = (-1 + sqrt(1 + Roughness * Roughness * pow(tan(acos(dotProduct(wo, N))), 2))) / 2;
                    float divisor = (1 + A_wi + A_wo);
                    if (divisor < 0.001)
                        return 1.f;
                    else
                        return 1.0f / divisor;
                };
                G = G_function(Roughness, -wi, wo, N);

                auto D_function = [&](const float& Roughness, const Vector3f& h, const Vector3f& N)
                {
                    float cos_sita = dotProduct(h, N);
                    float divisor = (M_PI * pow(1.0 + cos_sita * cos_sita * (Roughness * Roughness - 1), 2));
                    if (divisor < 0.001)
                        return 1.f;
                    else 
                        return (Roughness * Roughness) / divisor;
                };
                Vector3f h = normalize(-wi + wo);
                D = D_function(Roughness, h, N);

                // energy balance
                Vector3f diffuse = (Vector3f(1.0f) - F) * Kd / M_PI;
                Vector3f specular;
                float divisor= ((4 * (dotProduct(N, -wi)) * (dotProduct(N, wo))));
                if (divisor < 0.001)
                    specular= Vector3f(1);
                else
                    specular = F *G * D / divisor;
                //std::cout << "F:"<<F << "\n";
                //std::cout << "diffuse:"<<diffuse<<"\n";
                //std::cout << "specular:" << specular << "\n";
                return diffuse+specular;
            }
            else
                return Vector3f(0.0f);
            break;
        }
    }
}

哦对,由于要重要性采样(采样方向只需要采完美反射的方向即可),所以采样的那两个函数也需要改变,不能像Microfacet一样不变。如下:

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;
        }
        case MICROFACET:
        {
            // 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;
        }
    }
}

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) > 0.0f)
                return 1.0f;
            else
                return 0.0f;
            break;
        }
        case MICROFACET:
        {
            // uniform sample probability 1 / (2 * PI)
            if (dotProduct(wo, N) > 0.0f)
                return 0.5f / M_PI;
            else
                return 0.0f;
            break;
        }
    }
}

哦对了,最后的最后,因为要做重要性采样,castRay函数也要进行修改,否则会有过曝现象,mirror材质不再需要直接光L_dir采样了,就直接算L_indir就行,而且在算L_indir的时候记得那个把里面的避免光源采样的判断条件去了。如下:

Vector3f Scene::castRay(const Ray& ray, int depth) const
{
    // TO DO Implement Path Tracing Algorithm here
    Vector3f hitColor = this->backgroundColor;
    Intersection shade_point_inter = Scene::intersect(ray);
    if (shade_point_inter.happened)
    {
        Vector3f p = shade_point_inter.coords;
        Vector3f wo = ray.direction;
        Vector3f N = shade_point_inter.normal;
        Vector3f L_dir(0), L_indir(0);
        Vector3f p_deviation = (dotProduct(ray.direction, N) < 0) ?
            p + N * EPSILON :
            p - N * EPSILON;

        switch (shade_point_inter.m->getType())
        {
            case MIRROR:
            {
                //Test Russian Roulette with probability RussianRouolette
                float ksi = get_random_float();
                if (ksi < RussianRoulette)
                {
                    //wi=sample(wo,N)
                    Vector3f wi = normalize(shade_point_inter.m->sample(wo, N));
                    //Trace a ray r(p,wi)
                    Ray ray_pTowi(p_deviation, wi);
                    //If ray r hit a object at q
                    Intersection bounce_point_inter = Scene::intersect(ray_pTowi);
                    if (bounce_point_inter.happened)
                    {
                        float pdf = shade_point_inter.m->pdf(wo, wi, N);
                        if (pdf > EPSILON)
                            L_indir = castRay(ray_pTowi, depth + 1) * shade_point_inter.m->eval(wo, wi, N) * dotProduct(wi, N) / (pdf * RussianRoulette);
                    }
                }
                break;
            }
            default:
            {
                //sampleLight(inter,pdf_light)
                Intersection light_point_inter;
                float pdf_light;
                sampleLight(light_point_inter, pdf_light);
                //Get x,ws,NN,emit from inter
                Vector3f x = light_point_inter.coords;
                Vector3f ws = normalize(x - p);
                Vector3f NN = light_point_inter.normal;
                Vector3f emit = light_point_inter.emit;
                float distance_pTox = (x - p).norm();
                //Shoot a ray from p to x
                Ray ray_pTox(p_deviation, ws);
                //If the ray is not blocked in the middleff
                Intersection blocked_point_inter = Scene::intersect(ray_pTox);
                if (abs(distance_pTox - blocked_point_inter.distance < 0.01))
                {
                    L_dir = emit * shade_point_inter.m->eval(wo, ws, N) * dotProduct(ws, N) * dotProduct(-ws, NN) / (distance_pTox * distance_pTox * pdf_light);
                }
                //Test Russian Roulette with probability RussianRouolette
                float ksi = get_random_float();
                if (ksi < RussianRoulette)
                {
                    //wi=sample(wo,N)
                    Vector3f wi = normalize(shade_point_inter.m->sample(wo, N));
                    //Trace a ray r(p,wi)
                    Ray ray_pTowi(p_deviation, wi);
                    //If ray r hit a non-emitting object at q
                    Intersection bounce_point_inter = Scene::intersect(ray_pTowi);
                    if (bounce_point_inter.happened && !bounce_point_inter.m->hasEmission())
                    {
                        float pdf = shade_point_inter.m->pdf(wo, wi, N);
                        if (pdf > EPSILON)
                            L_indir = castRay(ray_pTowi, depth + 1) * shade_point_inter.m->eval(wo, wi, N) * dotProduct(wi, N) / (pdf * RussianRoulette);
                    }
                }
                break;
            }
        }
        hitColor = shade_point_inter.m->getEmission()+L_dir + L_indir;
        hitColor.x = (clamp(0, 1, hitColor.x));
        hitColor.y = (clamp(0, 1, hitColor.y));
        hitColor.z = (clamp(0, 1, hitColor.z));

    }
    return hitColor;
}

最终结果(均为1024×1024×512)
在这里插入图片描述
这时候有人会问了,tmd,重要性采样怎么这么麻烦,那我不用,把ssp调高点不就行了?emmm,如果头铁,不做重要性采样,我也实验了一下,就是下图的结果,ssp已经是2048了,但是噪点还是非常多,就是采样效率太低了,做了重要性采样后,ssp为512的结果可以完美爆杀这张图。

在这里插入图片描述

代码实现可能遇到的问题

1.光源融入到整个天花板,没有出现黑色

光源融入到整个天花板了,==tmd,感觉网上没有人和我这个情况是一样的。。。

光源尼玛的,就是融入整个天花板了,==tmd,感觉网上没有人和我这个情况是一样的。。。

测试的时候发现,其实这个光源是能被检测出来的,只是着色的时候变成这样了,然后分析了好几个小时的源码,艹,一直找不到原因,最后debug了半天,总算知道为什么了。

就是当对光源着色时,目前是分为两个部分,一个是直接光,一个是间接光。

直接光的话应该是不会提供颜色的,因为直接光同样是在光源上采样,相当于ws和N是几乎垂直的,cos(ws,N)项就会使得整个直接光的值接近0。代码中点偏移的处理只会让两者夹角大于90,此时cos算出来甚至还是小于0的。综上,直接光不会给其提供颜色。

间接光会对其提供颜色,并且也是导致光源颜色和天花板颜色接近的直接原因,因为计算间接光时,光源本身是 漫反射材质,所以这边代码会让光源应该会有light->Kd差不多的颜色,而这个值又和天花板的颜色其实差不多,所以看上去才只能看到天花板。

这个地方我看其他博主的解决方法都很简单粗暴,检测出是光源就直接返回光源本身颜色,但实际上光源也是会吸收反射其他光的,如果场景中有其他光源,那这种处理有可能就会出问题。**因此就需要保证光源是可以正常接收直接光和间接光的,不能直接返回光源本身颜色。**此时观察之前的公式,少了一个自身发光项(下面第二张图,之前一直不知道这个有啥用,此时起作用了,醍醐灌顶),最后把这个加上去就可以正常显示了。

emm,之前好几个小时一直纠结在直接光上面,想方设法要避免直接光,其实并不是这个原因导致的,害==,也是尼玛的运气好,多实验了一下,结果和想的不一样,就再思考分析了下,就想出来原因了。。。

也明白了个道理,发现问题并打算编写代码去解决的时候,先看看能不能设计实验用代码验证一下这个问题的提出是不是正确的,如果这个问题本身就是错误的,那就没有必要花很多时间去写代码解决。我这次就花了很多时间去思考怎么避免直接光,实际上这个问题本身就是不成立的==我应该试一下没有直接光的话,光源区域会不会变黑,如果变黑了,就说明我之前提出的问题是成立的。
在这里插入图片描述
在这里插入图片描述
我草,事后去games论坛上,居然真的找到了个一样的了==
https://games-cn.org/forums/topic/zuoye7wenti/

2.箱子背阴处有很多黑色噪点

没对好伪代码中的那些向量方向定义,说白了就是没完全按照伪代码来,改了之后就好了。
在这里插入图片描述
修改后的结果
在这里插入图片描述

3.有白色噪点(ssp调高仍然有)

画面上有少部分白色噪点
在这里插入图片描述
用ps工具放大,发现是某个像素为白色,此时SPP已经是512,所以应该不是采样不够的问题。而且如果是采样不够,应该是会在其周围同时出现大量噪点,而不是只出现这么一个这么突出的,例如下面第三张图。
在这里插入图片描述
在这里插入图片描述
这意味着计算光照时,出现了一个非常大的值。排查代码发现原因很可能在于除数过小(无限接近于0,或等于0,这里就是等于0),结果接近无穷大。使这个像素的全部采样都收到这个无限大值的影响,直接变成白色。这显然是我们不想要的。这就对应作业里的提示:pdf接近0。解决方案就是pdf低于某个指定阈值时,L_indir为0,代码如下:

float pdf = shade_point_inter.m->pdf(wo, wi, N);
if(pdf> EPSILON)
	L_indir = castRay(ray_pTowi, depth + 1) * shade_point_inter.m->eval(wo, wi, N) * dotProduct(wi, N) / (pdf *RussianRoulette);

修改后的结果
在这里插入图片描述
通过这个案例,分析一下为什么要考虑数值精度,emmm,如果不考虑数值精度,很有可能一次数值的不正确就会产生难以想象的结果,例如上述除数为0导致无限大inf的出现,会使得当次像素颜色计算的其他采样结果无效,相当于一个极偏值对整体结果产生了巨大影响,而我们想要的是能够避免极偏值的影响,类似机器学习里面的回归分类问题。所以这个时候就要对其特殊处理,降低他对其他结果的影响。

理论思考

1.偏移为什么要分两种情况这么偏移?

考虑了背光情况(因为世间万物都有厚度,如果光在前面,从背面看就是会被挡住),如下面第二张图。
在这里插入图片描述

2.为什么path tracing有软阴影?

阴影部分应该都是没有直接光的,那么导致软阴影的原因就是环境光,接受的间接光是不一样的,离得近的会被挡住更多的间接光,离得远的间接光挡住的比较少,所以就会有软阴影。

而witted style就没有软阴影,这是因为没有考虑全局光,就是只考虑了那一个方向。

3.把witted style和path tracing的结果对比一下,两者的实现的不同点有哪些?

区别很大,path tracing是基于witted style的改进,很容易看到这两者共同的地方。明显,path tracing是合理的,并且可以基于物理,还能考虑全局光照,能量守恒保证结果更加精确。具体的可以看 Ray Tracing笔记那一章。

反正我觉得两者最大的区别,就是path tracing通过渲染方程考虑了全局光照而witted style无法考虑全局光照(没有全局积分,一个点的颜色就由那么几根光线决定,实现那种完美镜面效果倒是好的很。。。),它因此很难实现很多的细节,大部分材质表现也没有path tracing好。

4.为什么程序中,渲染方程的入射radiance是光源的emit,是一个常量?

emmm,如果要深究,非常复杂,后面的光学理论看得我头痛,大致就下面这么理解就够了。

反正radiance在程序中就是一根光线。
在这里插入图片描述

5.为什么说光栅化比光追快?

光栅化没有大量的采样,光追里面因为要求解积分,需要大量的采样

个人感悟

emmm,可以说这是我从小到大,除一些项目以外,做某门课作业花的最长的时间了,很感谢闫老师的101课程,提供了优质的作业,通过作业7对光线追踪技术有了一个基础的基本认识,也在做的过程中不断发现问题,解决问题,提高自己的coding和思考能力,真的很感谢闫老师,我有一种预感,这门课将会为中国图形学持续输送数以万计的人才。这次作业做完后差不多打好了离线渲染的一些基本的底子,打算明天开始正式做本科的毕设了(之前10月到12月一直在摆烂,1月到2月在补渲染的基础知识,所以说本科毕设正式开始时间就差不多是现在2月,希望来得及==害),加油加油!

也希望这个博客可以对做这个作业的人有一定帮助!

参考资料

1.https://blog.csdn.net/u011942101/article/details/123656944(代码性能分析器)

2.https://blog.csdn.net/ycrsw/article/details/124408789

3.https://blog.csdn.net/qq_41765657/article/details/121942469

4.https://blog.csdn.net/ycrsw/article/details/124565054

5.https://blog.csdn.net/weixin_44491423/article/details/127552276

6.https://games-cn.org/forums/topic/guanyuguangxianzhuizong3zhongradiancedeshizi/

7.https://www.zhihu.com/question/28476602/answer/41003204

8.https://games-cn.org/forums/topic/guanyuxuanranfangchengdiguidingyideyiwen/

9.https://www.jianshu.com/p/0cfc3204af77

10.https://zhuanlan.zhihu.com/p/434964126

11.https://zhuanlan.zhihu.com/p/152226698

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/343263.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Cocos Creator 3.x开发《切水果3D》

今天跟大家分享一个Cocos Creator 3D切水果的实战案例&#xff0c;帮助大家掌握Cocos Creator开发3D微信抖音小游戏&#xff0c;开发工具我们采用的是Cocos Creator 3.6。先上一波游戏操作效果图&#xff0c;接下来通过本文来讲解这个游戏的一些核心的技术点。 对啦&#xff0…

使用sqlmap + burpsuite sql工具注入拿flag

使用sqlmap burpsuite sql工具注入拿flag 记录一下自己重新开始学习web安全之路③。 目标网站&#xff1a;http://mashang.eicp.vip:1651/7WOY59OBj74nTwKzs3aftsh1MDELK2cG/ 首先判断网站是否存在SQL注入漏洞 1.找交互点 发现只有url这一个交互点&#xff0c;搜索框和登录…

Springboot扫描注解类

Springboot扫描注解类的入口在AbstractApplicationContext的refresh中&#xff0c;对启动步骤不太了解的&#xff0c;可参考https://blog.csdn.net/leadseczgw01/article/details/128930925BeanDefinitionRegistryPostProcessor接口有多个实现类&#xff0c;扫描Controller、Se…

【VictoriaMetrics】VictoriaMetrics单机版批量和单条数据写入(Prometheus格式)

VictoriaMetrics单机版支持以Prometheus格式的数据写入,写入支持单条数据写入以及多条数据写入,下面操作演示下如何使用 1、首先需要启动VictoriaMetrics单机版服务 2、使用postman插入单机版VictoriaMetrics,以当前时间插入数据 地址为 http://victoriaMetricsIP:8428/api…

ISYSTEM调试实践10-实时数据采集工具daqIDEA

本文介绍一种实时数据采集的工具daqIDEA&#xff0c;该软件整合在了winIDEA内&#xff0c;可以直接通过winIDEA启动。 daqIDEA类似于jlink的jscop&#xff0c;stlink也有类似功能。原理就是利用仿真探头&#xff0c;将程序运行的变量实时采集出来&#xff0c;并通过曲线的方式显…

记录次数 | V1.1.0版本变动说明

版本内容1、新增词条数据报告---统计累计次数、最早时间、最晚时间等等 2、词条加入内容文本审核功能---创建/修改词条先经过微信文本安全接口审查&#xff0c;审查通过的才能分享公开数据&#xff0c;否则只能自己可见 3、新增分享版本思考这个小程序是有自然流量的&#xff0…

跨域小样本系列4:finetune方法解决CDFSL

来源&#xff1a;投稿 作者&#xff1a;橡皮 编辑&#xff1a;学姐 带你学习跨域小样本系列1-简介篇 跨域小样本系列2-常用数据集与任务设定详解 跨域小样本系列3&#xff1a;元学习方法解决CDFSL以及两篇SOTA论文讲解 跨域小样本系列4&#xff1a;finetune方法解决CDFSL以及…

通付盾汪德嘉——设备指纹的尽头是分布式数字身份

作者简介&#xff1a;汪德嘉&#xff0c;美国威斯康星大学麦迪逊分校数学博士、九三学社社员、正高级工程师&#xff1b;时空码发明者&#xff0c;《身份危机》与《数字身份》专著作者&#xff1b;曾在ORACLE、VISA、IBM等企业部门负责总体设计、产品开发&#xff0c;2011年归国…

深度学习训练营_第P3周_天气识别

&#x1f368; 本文为&#x1f517;365天深度学习训练营 中的学习记录博客&#x1f366; 参考文章&#xff1a;Pytorch实战 | 第P3周&#xff1a;彩色图片识别&#xff1a;天气识别**&#x1f356; 原作者&#xff1a;K同学啊|接辅导、项目定制**␀ 本次实验有两个新增任务&…

信息论绪论

本专栏针包含信息论与编码的核心知识&#xff0c;按知识点组织&#xff0c;可作为教学或学习的参考。markdown版本已归档至【Github仓库&#xff1a;information-theory】&#xff0c;需要的朋友们自取。或者关注公众号【AIShareLab】&#xff0c;回复 信息论 也可获取。 文章目…

「2」线性代数(期末复习)

&#x1f680;&#x1f680;&#x1f680;大家觉不错的话&#xff0c;就恳求大家点点关注&#xff0c;点点小爱心&#xff0c;指点指点&#x1f680;&#x1f680;&#x1f680; 方阵的行列式 (1) &#xff5c;A^T&#xff5c;&#xff5c;A&#xff5c;(2) |&#x1d6…

树莓派 安装 宝塔linux面板5.9. 2023-2-13

​​​​​​​ 一.环境 1.硬件环境: 树莓派3b , 8GB tf卡 ,micro usb电源 2.网络环境: 网线直连路由器 , 可访问互联网 3.软件环境: 树莓派操作系统 CentOS-Userland-7-armv7hl-RaspberryPI-Minimal-2009-sda(linux) 系统刻录工具 Win32DiskImager (win) ip扫描工具 Adv…

Github 上如何提交 pull request

什么是复刻&#xff08;forking&#xff09;? 我们可以通过复刻操作将喜爱的仓库保存自己的Github账户中&#xff0c;以便独立地对其进行操作。 通过复刻&#xff0c;我们可以得到包含完整版本历史的目标仓库的实例&#xff0c;之后可以对复刻得到的仓库进行任意操作而不会影响…

iTOP3588开发板直连电脑配置方法(无线上网)配置主机IP

首先使用网线连接好主机和开发板&#xff0c;在没有上电的情况下&#xff0c;可以看到以太网显示网络电缆 被拔出&#xff0c;如下图所示&#xff1a; 当开发板上电以后&#xff0c;开发板网卡与笔记本电脑的网卡会连接&#xff0c;如下图所示&#xff1a; 然后右键点击以太网…

MY2480-16P语音模块的使用

MY2480-16P语音模块的使用开发环境&#xff1a;STM32CUBEMXKEIL5辅助软件&#xff1a;串口助手、迅捷文字转语音一、MY2480-16P语音模块引脚图及引脚定义二、选择触发方式三、使用串口控制MY2480-16P语音模块四、模块使用指南开发环境&#xff1a;STM32CUBEMXKEIL5 辅助软件&a…

Python解题 - CSDN周赛第28期

上一期周赛问哥因为在路上&#xff0c;无法参加&#xff0c;但还是抽空登上来看了一下题目。4道题都挺简单的&#xff0c;有点遗憾未能参加。不过即使参加了&#xff0c;手速也未必能挤进前十。 本期也是一样&#xff0c;感觉新增的题目都偏数学类&#xff0c;基本用不到所谓的…

Buffer Status Reporting(BSR)

欢迎关注同名微信公众号“modem协议笔记”。 以一个实网中的异常场景开始&#xff0c;大概流程是有UL data要发送&#xff0c;UE触发BSR->no UL grant->SR->no UL grant->trigger RACH->RACH fail->RLF->RRC reestablishment&#xff1a;简单描述就是UE触…

Jmeter压测说明

Jmeter使用说明Jmeter下载安装修改编码UTF-8参数说明Get接口测试创建线程组添加HTTP请求添加结果树添加聚合报告启动POST请求添加HTTP信息头管理器设置参数图片上传mysql压测准备数据添加数据库连接jar配置jdbc添加jdbc requestJmeter下载安装 Jmeter官网下载 要求java 1.8以上…

MACD指标在外汇交易中的另类运用方法

外汇交易中怎么另辟蹊径的使用MACD指标 随着外汇市场的不断发展和变化&#xff0c;交易者们不断探索新的方法和技术指标&#xff0c;以提高自己的交易技巧和赢利能力。其中&#xff0c;MACD指标是广大交易者们喜欢使用的一种技术分析工具&#xff0c;它可以帮助交易者判断价格的…

SQL注入Getshell的奇思妙想(上)

前言 前段时间&#xff0c;hvv和找实习的师傅们也多了起来。而我也不例外&#xff0c;尝试投递了不少简历&#xff0c;结果是积累了大量的面试经验。笔者发现大量的hr面试官都喜欢从SQL注入开始询问&#xff0c;所以留心了一下关于SQL注入的问题的频率。结果非常amazing啊&…