今天讲这本书最后一种材质
Preface
水,玻璃和钻石等透明材料是电介质。当光线照射它们时,它会分裂成反射光线和折射(透射)光线。
处理方案:在反射或折射之间随机选择并且每次交互仅产生一条散射光线
(实施方法:随机取样,具体见后文)
调试最困难的部分是折射光线。如果有折射光线的话,我通常首先让所有的光折射。对于这个项目,我试图在我们的场景中放置两个玻璃球,我得到了这个:
上述图片是对的吗?显然,在实际生活中,那两个玻璃球看起来怪怪的,实际情况下,里面的内容应该将现在的进行上下颠倒,且没有黑色成分。
Ready
定量计算光的折射
-------------------------------------------- 数学分割线 --------------------------------------------
公式中的η为相对折射率:n2/n1
而由于入射光线方向的随机性和eta的不同,可能导致 1-η*η*(1-cosθ1 * cosθ1)小于0,此时取根号毫无意义
而事实上,这也就是全反射现象。即:当光线从光密介质进入光疏介质中如果入射角大于某个临界值的时候,就会发生全反射现象。
该临界角即折射角为90°时对应的入射角,也就是cosθ2恰好等于0的时候
------------------------------------------------ END ------------------------------------------------
正文
我们来封装一个电介质类
首先明确,它是材质的一种,即
#ifndef DIELECTRIC_H #define DIELECTRIC_H namespace rt { class dielectric :public material { public: dielectric(rtvar RI) :_RI(RI) { } virtual bool scatter(const ray& rIn, const hitInfo& info, rtvec& attenuation, ray& scattered)const; inline bool refract(const rtvec& rIn, const rtvec& n, rtvar eta, rtvec& refracted)const; private: rtvar _RI; //refractive indices }; bool dielectric::scatter(const ray& rIn, const hitInfo& info, rtvec& attenuation, ray& scattered)const { rtvec outward_normal; rtvec refracted; rtvar eta; attenuation = rtvec(1., 1., 1.); if (dot(rIn.direction(), info._n) > 0) { outward_normal = -info._n; eta = _RI; } else { outward_normal = info._n; eta = 1. / _RI; } if (refract(rIn.direction(), outward_normal, eta, refracted)) { scattered = ray(info._p, refracted); return true; } return false; } inline bool dielectric::refract(const rtvec& rIn, const rtvec& n, rtvar eta, rtvec& refracted)const { rtvec unitIn = rIn.ret_unitization(); rtvar cos1 = dot(-unitIn, n); rtvar cos2 = 1. - eta*eta*(1 - cos1*cos2); if (cos2 > 0) { refracted = eta * rIn + n*(eta*cos1 - sqrt(cos2)); return true; } return false; //全反射 } } #endif
dielectric.h
attenuation的值总是1,因为玻璃表面不吸收任何光,即没有rgb强度衰减
我们会很容易想到前言部分中的方法:如果有折射,那么让所有的光线折射,就像上面代码中scatter函数描述的那样,那么就会得到那张图
我们把metal中的reflect函数设置为静态的,或者是命名空间内“全局”函数,这样用起来比较方便,换句话讲,这个公式并不属于任何类,它是3D数学通用公式
main函数球体设置:
上述代码是前言中图像的生成代码
然而,它没有加入全反射,所以导致了黑色成分的出现,所以,我们将全反射加入到上述代码中
#ifndef DIELECTRIC_H #define DIELECTRIC_H namespace rt { class dielectric :public material { public: dielectric(const rtvar RI) :_RI(RI) { } virtual bool scatter(const ray& InRay, const hitInfo& info, rtvec& attenuation, ray& scattered)const override; inline bool refract(const rtvec& rIn, const rtvec& n, rtvar eta, rtvec& refracted)const { rtvec unitIn = rIn.ret_unitization(); //将入射光线单位化 rtvar cos1 = dot(unitIn, n); rtvar cos2 = 1. - eta*eta*(1. - cos1*cos1); if (cos2 > 0) { refracted = eta * (rIn - n * cos1) - n * sqrt(cos2); return true; } return false; } private: rtvar _RI; }; bool dielectric::scatter(const ray& InRay, const hitInfo& info, rtvec& attenuation, ray& scattered)const { rtvec outward_normal; rtvec reflected = metal::reflect(InRay.direction(), info._n); rtvar eta; attenuation = rtvec(1., 1., 1.); rtvec refracted; if (dot(InRay.direction(), info._n) > 0) { outward_normal = -info._n; eta = _RI; } else { outward_normal = info._n; eta = 1. / _RI; } if (refract(InRay.direction(), outward_normal, eta, refracted)) { scattered = ray(info._p, refracted); } else { scattered = ray(info._p, reflected); return false; } return true; } } #endif
dielectric.h
会得到如下图:
得到这张图是真的不容易,踩了一天坑
主要是,渲染一张图看下效果基本要7~10分钟,玩不起,放开双手~~
坑点
这里的反射公式有三种形式,但是它们化简之后都是一个式子
我们这里采用的是纸上推出来的,但是用哪个式子,我们都要注意三点:
1.向量的符号!!!
我们知道cos(theta1) = dot(- 入射向量,法线)
折射向量 = eta * 入射 + 法线*eta*cos(theta1)- 法线 * cos(theta2)
但是,如果你代码中的cos(theta1) = dot(入射,法线)
那么, 折射向量 = eta * 入射 - .... 这里就不是+了
这是公式的符号的问题
2.入射向量的单位化
为什么要单位化呢,这个还是很重要的
因为你传入的入射向量是有长度的,你用你传入的入射向量计算出来的折射向量也是有长度的,显然,折射不会衰减光的强度,也不会平白无故缩短向量
这时候你就要考虑了,你传出的折射向量是要干嘛用的
折射向量是要作为新的视线的方向向量的对吧
而我们都知道,视线有三部分,eye的位置,方向向量,t系数(伸长长度)
还有一点,我们计算景物的画面的时候,计算的是视线延伸后的离眼球最近的点画在屏幕上
如果,你的视线最初的方向向量本身就有好长,你的眼球好大一颗,那么本来离eye点最近的点可能就被这颗偌大的眼球边界包在里面可能不是眼球之外最近的点了
所以,我们的方向向量一定是最短的,即单位1,这样,我们伸长之后,触碰到的第一个点才能保证是离眼球最近的点,如果方向向量过长,可能包在里面的点就被忽略了
第二点不注意就会出现下面这张图
左球的景象少了些,可能就是上述原因,视线的方向向量太长了,未经过单位化
3.向量统一
如果你要用入射向量的单位向量,那么,所有涉及入射向量的地方都用入射单位向量代替
如果不用入射单位向量,那么整个代码计算过程中就都不用,不要混用。
例: 下面是书上的折射函数代码
函数体第一行,它把入射光单位化,第二行用 uv 做了点乘,然而后面的五行却用的是 v 而不是uv ,没道理!!第五行的 dt 和discriminant都是用 uv 算出来的,前面突然用个v是什么操作??
我们把v改成uv就可以了
坑点结束
然而,还是存在玻璃内图像颠倒的现象
解释如下:
这里面有一个反射系数的问题,上面我们都考虑的是反射系数为0的情况,实际生活中的玻璃透明介质是有反射系数的。
此时,我们需要引入一个新的概念——反射系数
它是由 Christopher Schlick 提出的:
rtvar schlick(rtvar cosine, rtvar RI) { rtvar r0 = (1-RI)/(1+RI); r0 *= r0; return r0 + (1-r0)*pow((1-cosine),5); }
这里面还有一个问题
我们折射的 scatter 函数需要全反射的时候return 的 是false , 意思是 if 只计算折射情况,全反射是按照 rtvec(0,0,0)运算的,压根就没算
所以,我们改一下代码:
#ifndef DIELECTRIC_H #define DIELECTRIC_H namespace rt { class dielectric :public material { public: dielectric(const rtvar RI) :_RI(RI) { } virtual bool scatter(const ray& InRay, const hitInfo& info, rtvec& attenuation, ray& scattered)const override; inline static bool refract(const rtvec& rIn, const rtvec& n, rtvar eta, rtvec& refracted); protected: rtvar _RI; rtvar dielectric::schlick(const rtvar cosine, const rtvar RI)const; }; bool dielectric::scatter(const ray& InRay, const hitInfo& info, rtvec& attenuation, ray& scattered)const { rtvec outward_normal; rtvec refracted; rtvec reflected = metal::reflect(InRay.direction(), info._n); rtvar eta; rtvar reflect_prob; rtvar cos; attenuation = rtvec(1., 1., 1.); if (dot(InRay.direction(), info._n) > 0) { outward_normal = -info._n; eta = _RI; cos = _RI*dot(InRay.direction(), info._n) / InRay.direction().normal(); } else { outward_normal = info._n; eta = 1.0 / _RI; cos = -dot(InRay.direction(), info._n) / InRay.direction().normal(); } if (refract(InRay.direction(), outward_normal, eta, refracted)) reflect_prob = schlick(cos, _RI); //如果有折射,计算反射系数 else reflect_prob = 1.0; //如果没有折射,那么为全反射 if (rtrand01() < reflect_prob) scattered = ray(info._p, reflected); else scattered = ray(info._p, refracted); return true; } inline bool dielectric::refract(const rtvec& rIn, const rtvec& n, rtvar eta, rtvec& refracted) { rtvec unitIn = rIn.ret_unitization(); //将入射光线单位化 rtvar cos1 = dot(-unitIn, n); rtvar cos2 = 1. - eta*eta*(1. - cos1*cos1); if (cos2 > 0) { refracted = eta * rIn + n * (eta * cos1 - sqrt(cos2)); return true; } return false; } rtvar dielectric::schlick(const rtvar cosine, const rtvar RI)const { rtvar r0 = (1. - RI) / (1. + RI); r0 *= r0; return r0 + (1 - r0)*pow((1 - cosine), 5); } } #endif
dielectric.h
里面涉及到了rtrand01,还记得吗,这个是我们在学漫反射的时候弄的
那么放在这里作什么嘞?
还记得Preface中我们说过的处理方案吗
我们现在就是这么做的,我们得到一个reflect_prob,它介于0~1之间,如果我们取0~1之间的随机数,根据随机数确定选择反射还是折射,这个还是很科学的,为什么呢?因为我们做了100次采样!!,那么我们可以理直气壮的说,我们的透明电介质真正做到了反射和折射的混合(除了全反射现象),而且,前言也说过,光线照射透明电介质时,它会分裂为反射光线和折射光线。
主函数:
#define LOWPRECISION #include ...... #define stds std:: using namespace rt; rtvec lerp(const ray& sight, intersect* world, int depth) { hitInfo info; if (world->hit(sight, (rtvar)0.001, rtInf(), info)) { ray scattered; rtvec attenuation; if (depth < 50 && info.materialp->scatter(sight, info, attenuation, scattered)) return attenuation * lerp(scattered, world, depth + 1); else return rtvec(0, 0, 0); } else { rtvec unit_dir = sight.direction().ret_unitization(); rtvar t = 0.5*(unit_dir.y() + 1.); return (1. - t)*rtvec(1., 1., 1.) + t*rtvec(0.5, 0.7, 1.0); } } void build_9_1() { stds ofstream file("graph9-1.ppm"); size_t W = 400, H = 200, sample = 100; if (file.is_open()) { file << "P3\n" << W << " " << H << "\n255\n" << stds endl; size_t sphereCnt = 4; intersect** list = new intersect*[sphereCnt]; list[0] = new sphere(rtvec(0, 0, -1), 0.5, new lambertian(rtvec(0.1, 0.2, 0.5))); list[1] = new sphere(rtvec(0, -100.5, -1), 100, new lambertian(rtvec(0.8, 0.8, 0.))); list[2] = new sphere(rtvec(-1, 0, -1), 0.5, new dielectric(1.5)); list[3] = new sphere(rtvec(1, 0, -1), 0.5, new metal(rtvec(0.8, 0.6, 0.2))); intersect* world = new intersections(list, sphereCnt); camera cma; for (int y = H - 1; y >= 0; --y) for (int x = 0; x < W; ++x) { rtvec color; for (int cnt = 0; cnt < sample; ++cnt) { lvgm::vec2<rtvar> para{ (rtrand01() + x) / W, (rtrand01() + y) / H }; color += lerp(cma.get_ray(para), world, 0); } color /= sample; color = rtvec(sqrt(color.r()), sqrt(color.g()), sqrt(color.b())); //gamma 校正 int r = int(255.99 * color.r()); int g = int(255.99 * color.g()); int b = int(255.99 * color.b()); file << r << " " << g << " " << b << stds endl; } file.close(); if (list[0])delete list[0]; if (list[1])delete list[1]; if (list[2])delete list[2]; if (list[3])delete list[3]; if (list)delete[] list; if (world)delete world; stds cout << "complished" << stds endl; } else stds cerr << "open file error" << stds endl; } int main() { build_9_1(); } /*********************************************************/
电介质球体的一个有趣且简单的技巧是要注意,如果使用负半径,几何体不受影响但表面法线指向内部,因此它可以用作气泡来制作空心玻璃球体:
我们实验一下书上的负半径:
得到这样的图:
为了能够看懂空心球是个啥玩意儿,我把eta 颠倒了一下
dot小于0,说明入射光线是从表面法线指向的方向空间入射到内部空间,例如:光从空气入射到水中
dot大于0,说明入射光线是从表面法线的反方向空间入射到表面法线指向的空间
这样,我们就可以看到那个内球了
感谢您的阅读,生活愉快~
原文地址:https://www.cnblogs.com/lv-anchoret/p/10217719.html