Kagamine Len
文章20
标签10
分类2
作业7 部分问题 加分部分-多线程

作业7 部分问题 加分部分-多线程

作业7常见踩坑:https://blog.csdn.net/ycrsw/article/details/124565054

作业7实现参考:https://blog.csdn.net/weixin_42489848/article/details/125548847

https://blog.csdn.net/Xuuuuuuuuuuu/article/details/129001805

作业7多线程实现:https://blog.csdn.net/qq_41835314/article/details/125235659

这部分多线程主要采用std::thread实现,主要参考文章:

https://blog.csdn.net/sjc_0910/article/details/118861539

出现的一些问题

代码如下所示:

{
    // TO DO Implement Path Tracing Algorithm here
    auto inter_p = intersect(ray);
    if(!inter_p.happened){
        return {100}; //return for background color
    }
    if(inter_p.m->hasEmission()){
        return inter_p.m->getEmission(); //对于光源,我们有光源发出的光Lo(p,wo) = Le(p,wo)
    }

    Intersection inter_light;
    float pdf_light;
    sampleLight(inter_light, pdf_light); //按道理,我们应该对所有的光源计算总的pdf,不过这里只有一个光源,应该没有问题。
    Vector3f L_dir;


    auto &x = inter_p.coords;
    auto wo = (ray.origin - x).normalized();
    auto &xx = inter_light.coords;
    auto &n = inter_p.normal;
    auto wi = (xx - x).normalized();

//    Vector3f p_deviation = (dotProduct(ray.direction, inter_p.normal) < 0) ?
//                           x + inter_p.normal * EPSILON :
//                           x - inter_p.normal * EPSILON ;
    Ray wi_ray(x,wi);
    Intersection x2light = intersect(wi_ray);

    float d = (xx - x).norm();

//    if(x2light.distance - d <= -0.01 && (x2light.happened && (x2light.coords - xx).norm() < 5e-3)){
//        assert(false);
//    }//没有交打到无穷远处使光线能量出现了负值出现了问题,还会出现交到更远处的情况-》灯光平面和所在平面平行的情况
    if(x2light.happened && abs(x2light.distance - d) <= 0.001){ //这里注意使用inter_light.normal 而非 x2light.normal 两者在交接点上很有可能不是同一个物体
    //if(x2light.happened && (x2light.coords - xx).norm() < 5e-3){
        L_dir = inter_light.emit * inter_p.m->eval(wo,wi,n) * dotProduct(n,wi) * dotProduct(inter_light.normal,-wi) //没有东西在光线的上面,因此后一项一定大于0
                / (d * d * pdf_light);
        //assert(L_dir.x >= 0);
    }
    Vector3f L_indir;
    //对material的sample是随机sample,即按照漫反射的方式sample,而对于object的sample则是对面积的均匀采样。
    float rr = get_random_float();
    if(rr <= RussianRoulette){
        wi = inter_p.m->sample(wo,n);
        //assert(wi.norm()<=1.01&&wi.norm()>=0.99);
        Ray indir_ray(x,wi);
        Intersection inter_indir = intersect(indir_ray);

        if(inter_indir.happened && !inter_indir.m->hasEmission()) //发光部分已经计算过了,这里不再计算
        {
            float pdf = inter_p.m->pdf(wo,wi,n);
            if(pdf > EPSILON) {
                L_indir = castRay(indir_ray, depth + 1) * inter_p.m->eval(wo, wi, n) * dotProduct(wi, n)
                          / (pdf * RussianRoulette); //我说能量怎么越算越大
            }
        }


    }
    return L_dir + L_indir;
}

主要问题在上面的注释中都有提到,包括前面的文章提到的一些问题。

出现白色噪点的问题和能量越算越大分别是因为pdf过小(实际上是0),和没有在最后除以(pdf * p_RR)

这里首先我们需要注意eval()inter_p.m->pdf(wo,wi,n)等一系列包含入射方向的函数,实际上这些函数的入射方向均是没有使用的,但是在实现微表面模型的情况下会进行使用,以下这行代码的错误使用,会导致箱子阴面出现黑色噪点:

黑色噪点

L_dir = inter_light.emit * inter_p.m->eval(wo,wi,n) * dotProduct(n,wi) * dotProduct(inter_light.normal,-wi)

由于是漫反射模型,eval(wo,wi,n)为一个常值,但是我们需要保证wi(指向光源)和n(法向)方向角度小于180°,以此让dotProduct(n,wi)<0,后面那一项由于光线天然就在所有的物体上方,-wi一定是从光源向下的,正好和n同侧,反而不需要处理。

当我们写成(wi,wo,n)时,就会出现负值了,当然由于wo并没有真正使用,因此他的方向是随意的,从origin->p和反过来均可,主要影响后面微表面模型的实现。

下面这个问题就比较坑了,在一开始的版本中,直接光的判断中,我使用的是如下语句:

if(x2light.distance - d) > -0.001){ /*calculate direct lightning...*/}

逻辑上来说,实际发出的检测射线最远就和光线相交,但是由于浮点数精度误差,他可能打到其他平面(如天花板表面的点,和灯交就直接打到了墙壁上)甚至有可能出现求不出交的情况。

我们需要避免这两种情况发生(至少避免负数出现,虽然会导致结果有偏)。

此时,正是我们认为不可能取负数的dotProduct(inter_light.normal, -wi)取到了负数,导致L_dir甚至会变成一个很大的负数。

基本上,我们就是在处理浮点数导致的误差。

多线程部分

在构造std::thread时,我们需要将需要调用的函数和参数传入构造函数中,否则将会构造一个空thread,此时该thread仅可被移动赋值,并不会真正执行。当然在构造该函数时,为了保证thread在执行过程中,传入的参数是存在的,我们无法调用一个拥有一个引用参数的函数(对于std::bind也是类似),当我们传入一个引用值时,他也会被decay_copy成一个不带引用且无cv的参数列表。

decay代码分析:https://blog.csdn.net/shift_wwx/article/details/120270204

decay_copy的过程:https://zhuanlan.zhihu.com/p/425393495

decay_copy过程中参数如何保存和展开?

模版参数包/可变模板参数:https://www.bbsmax.com/A/6pdD6EgDJw/

https://www.cnblogs.com/qicosmos/p/4325949.html

https://blog.csdn.net/chenlong_cxy/article/details/126807356

补充阅读:

模板偏特化:https://blog.csdn.net/K346K346/article/details/82179205

c++17折叠表达式:https://blog.csdn.net/qq_43964318/article/details/127127521

https://blog.csdn.net/qq_38409301/article/details/122276764

如果想要传入一个引用值,我们需要通过std::refstd::cref传入伪装的引用值。

std::ref内容解析:https://blog.csdn.net/leapmotion/article/details/120338292

template <class U, class = decltype(
    detail::FUN<T>(std::declval<U>()), //declval仅做参数推导,不做实际运算,这里不允许使用右值
    std::enable_if_t<!std::is_same_v<reference_wrapper, std::remove_cvref_t<U>>>() //enable_if_t有两个参数,若果左边成立,返回右边的值(这里右边为void),即成立就能编译通过
  )>//这里的逗号表达式仅返回有伴的值,即void,实际上只是进行了类型检查
constexpr reference_wrapper(U&& u) noexcept(noexcept(detail::FUN<T>(std::forward<U>(u))))
    : _ptr(std::addressof(detail::FUN<T>(std::forward<U>(u)))) {} //返回完美转发

Decltype:https://blog.csdn.net/u014609638/article/details/106987131

std::forward完美转发:简单版:https://blog.csdn.net/xiangbaohui/article/details/103673177

超全版:https://www.cnblogs.com/5iedu/p/11324772.html

返回类型(按值返回,按常量引用返回,按引用返回):https://blog.csdn.net/u012814856/article/details/84099328

template<typename T>
T&& forward(typename remove_reference<T>::type& param)
{
    return static_cast<T&&>(param); //可能会发生引用折叠!
}

类型前的typename:https://zhuanlan.zhihu.com/p/398447963

如果我们不使用typename,即

template<typename T>
void test(T t){
		T::a; //表示内部的某个静态变量
}

指向数组的引用(?):https://blog.csdn.net/m0_52525841/article/details/122734831

#include <iostream>
#include <vector>

class Test{
    std::string name;
public:
    template<typename T>void setName(T&& new_name){  //"abc"在这里被解析为const char & [],直接完美转发后下方将直接调用构造函数
        this->name = std::forward<T>(new_name);
    }
  	void setName(const std::string &s){ //这里会生成临时的中间变量s,并且由于s是左值引用,导致我们必须对其再进行一次拷贝
    	this->name = s;
    }
    void print(){
        std::cout<<name<<std::endl;
    }
};

int main() {
    Test t;
    t.setName("abc");
    t.print();
  	std::string a = std::string("abc");
}
//隐式转换的规则是什么?
//使用const std::string &s的情况
t.setName(std::basic_string<char>("abc", std::allocator<char>())); //构造中间变量
inline void setName(const std::basic_string<char> & s) //如果这里再写一个右值引用的情况,应该可以避免一次拷贝
{
  this->name.operator=(s); //如果是右值引用,这里就需要进行移动
  //因此完美转发更适用于万能引用的情况
}
//使用T&&的情况
template<>
inline void setName<const char (&)[4]>(const char (&new_name)[4])
{
  this->name.operator=(std::forward<const char (&)[4]>(new_name));
}
t.setName<const char (&)[4]>("abc"); //避免了拷贝的发生
//模板如何进行的匹配? 为什么不使用const std::string &
//https://blog.csdn.net/weixin_44410704/article/details/127983116 c++函数模板 匹配过程?
template<typename T>
auto test(T&& t) //auto->remove_reference<T>
{
    return std::forward<T>(t); //由于t是一个万能引用对象。按值返回时实施std::forward
                               //如果原对象一是个右值,则被移动到返回值上。如果原对象
                               //是个左值,则会被拷贝到返回值上。
}

为什么forward需要显式指明类型T?

首先,是由于forward函数加入了remove_reference 进行类型推断,导致我们必须显示说明需要使用的类型。

原理上可能是typename T::some_class 不允许我们隐式做类型推断。

实际上是为了避免下文所说的这种问题,即自动的类型推断退化了完美转发的功能。

https://stackoverflow.com/questions/7779900/why-is-template-argument-deduction-disabled-with-stdforward

假如我们去掉remove_reference<T>,即直接使用T&&,对于左值,是没有问题的,对于右值T&& t虽然被以右值传入,但是由于这是个有名变量,已经被变化为了左值,此时也会被转发为左值,失去了完美转发的功能。

typename是否禁止了自动类型推断?

多线程相关部分:读写者模型,生产者消费者模型

CAS LL/SC(检查寄存器):https://zhuanlan.zhihu.com/p/371954148

c++并发编程:https://paul.pub/cpp-concurrency/#id-%E9%80%9A%E7%94%A8%E4%BA%92%E6%96%A5%E7%AE%A1%E7%90%86

这篇文章的并发有一个问题,在条件变量的章节,我们在读取账户资金的过程中发生了写,此时会发生读取的数据不正确,我们反而可能需要对写部分加读锁,因为写部分是可以并发的,而写和读的部分是不可以的。

c++内存模型:https://paul.pub/cpp-memory-model/

more about memory_order:https://www.zhihu.com/question/24301047

需要关注多个文件中变量和函数的声明方式,包含extern,inline和static

结果

对于多线程实现本身,我们只需要将height划分到各个进程上即可,在macbook2023上,大概能提速三倍左右(和电源配置也有关系),下图是最终获取的图像,实现过程中还遇到了一些问题。

binary

×