作业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::ref、std::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
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 不允许我们隐式做类型推断。
实际上是为了避免下文所说的这种问题,即自动的类型推断退化了完美转发的功能。
假如我们去掉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上,大概能提速三倍左右(和电源配置也有关系),下图是最终获取的图像,实现过程中还遇到了一些问题。

