本文目录
1 相比光栅化,为什么需要光线追踪
这是课上闫老师举的例子和自己的注释,已经很能够说明光栅化的不足之处。
除了上面的三种之外,还有一种就是无法展示透明的玻璃杯、水……这种有折射的物体。
2 光线追踪的思路综述
2.1综述文章
光线追踪综述
这三篇文章的内容其实不局限whitted风格的光线追踪,包含后面的路径追踪,算是不错的综述文章。
2.2核心思路
光线追踪方法主要思想是从视点向成像平面上的像素发射光线,找到与该光线相交的最近物体的交点,如果该点处的表面是散射面,则计算光源直接照射该点产生的颜色;如果该点处表面是镜面或折射面,则继续向反射或折射方向跟踪另一条光线,如此递归下去,直到光线逃逸出场景或达到设定的最大递归深度。
2.3 基本假设
这三个假设适用后面的路径追踪。
2.4 算法过程
大致上,Whitted RayTrace其主要把光线分为四种
- 视角光线,和之前的一样,没什么好说的
- 反射光线。在表面沿镜面反射方向继续照射。反射的颜色由反射光线与场景中的对象的交点决定。
- 折射光线,其创建与反射光线类似,只是它的方向是进入对象并最终可以退出对象。
- 阴影光线,是通过创建从交点到所有灯光的阴影光线来计算的。如果阴影光线在到达灯光之前与某个对象相交,则该交点将从该特定灯光中阴影显示。
(对应下图中的编号①②③④四种光线)
上图所示也是whitted的过程: - 从像素处发射一根光线,求与场景中物体的交点;
- 如果是反射材质,就求反射光线
- 如果是折射材质,就求折射光线,颜色占比按照菲涅尔折射定律确定比例
- 如果是漫反射材质,就用布林冯或者其他的光照模型求颜色。光线传播结束。
伪代码表示如下:
遍历每一个像素
{
从观察点到像素初始化一条射线;
求摄像是否与场景物体有交点;(怎么求交点是一个很重要的部分,下一篇单独讲)
if (无交点)
返回背景色;
else
switch(材质)
case (反射折射材质):
求反射向量;
求折射向量;
菲涅尔系数kr;
return kr*递归反射 + (1-kr)*递归折射
case (反射材质)
求菲涅尔系数kr;
求反射向量;
return 反射递归*kr
case (漫反射材质)
if(在阴影里)
光源强度为0;
return 着色模型着色
return 颜色
}
从这个whitted光线追踪的过程可以发现,它的缺点:
①没有模拟漫反射,无法更好地模拟全局光照;
②整个过程并没有遵循物理规律,比如折射和反射都是比较理想化的,并没有这种理想材质;
③依然无法表现软阴影
3 算法实现的技术关键点
3.1 光线与物体求交
真实的场景中,物体可能有成千上百个,三角面可能有几十万个,如果计算光线与具体哪个三角面有交点,这假设1080p的画面有1920x1080x100w个三角面,这个计算量太大了。
所以,计算光线与物体的交点,是整个光线追踪算法的核心之一,也是运算量的大占比之一。
求光线与物体的交点主要分为:①查找光线与哪个物体、进而与哪个三角面相交;②求光线与三角面的交点坐标
上面的第①步是下一篇文章,也是作业06重点关注的BVH
算法。本小节重点介绍求光线与一个三角面的交点的算法Möller Trumbore
算法。
思路就是交点既可以用射线表示,又可以用重心坐标表示,联立方程求解即可。
作业任务之一就是求解这个,照着公式敲出来其实很容易,但是关键是判断条件的添加。
判断条件:
①t>0;
②0<b1 && 0<b2 && 0<1-b1-b2 均为true
③fabs(s1* e1) < EPSILON;分母不能太小
3.2 菲涅尔折射
作业中直接给出了代码,具体的推导参考这两篇博客即可。
计算光的折射向量
3.3 递归的终止条件
递归终止
条件一:遇到漫反射物体;
条件二:设置一个最大光线传播次数,作业中是变量depth=5
4 作业代码
4.1 Möller Trumbore
算法
bool rayTriangleIntersect(const Vector3f& v0, const Vector3f& v1,
const Vector3f& v2, const Vector3f& orig,
const Vector3f& dir, float& tnear, float& u, float& v)
{
Vector3f edge1 = v1 - v0;
Vector3f edge2 = v2 - v0;
Vector3f pvec = crossProduct(dir, edge2);
float det = dotProduct(edge1, pvec);
if (det < EPSILON )
return false;
Vector3f tvec = orig - v0;
u = dotProduct(tvec, pvec);
if (u < 0 || u > det)
return false;
Vector3f qvec = crossProduct(tvec, edge1);
v = dotProduct(dir, qvec);
if (v < 0 || u + v > det)
return false;
float invDet = 1 / det;
tnear = dotProduct(edge2, qvec) * invDet;
u *= invDet;
v *= invDet;
return true;
}
4.2 初始化像素对应光线
void Renderer::Render(const Scene& scene)
{
std::vector<Vector3f> framebuffer(scene.width * scene.height);
float scale = std::tan(deg2rad(scene.fov * 0.5f));
float imageAspectRatio = scene.width / (float)scene.height;
// Use this variable as the eye position to start your rays.
Vector3f eye_pos(0);
int m = 0;
for (int j = 0; j < scene.height; ++j)
{
for (int i = 0; i < scene.width; ++i)
{
float xx;
float yy;
xx = (-1.0f + 2.0f / scene.width * i + 2.0f / scene.width * 0.5f) * imageAspectRatio;
yy = 1.0f - 2.0f / scene.height * j - 2.0f / scene.height * 0.5f;
float zz = -1.0f;
float norm_my = sqrt(xx * xx + yy * yy + zz * zz);
Vector3f dir = Vector3f(xx / norm_my, yy / norm_my, zz / norm_my); // Don't forget to normalize this direction!
/*/网上的代码,比较两者的区别,上面是按照框架里的z=-1写的,好复杂
float x = i + 0.5f - scene.width / 2;
float y = scene.height / 2 - j - 0.5f;
float z = -scene.height / 2;
*/
framebuffer[m++] = castRay(eye_pos, dir, scene, 0);
}
UpdateProgress(j / (float)scene.height);
}
// 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] = (char)(255 * clamp(0, 1, framebuffer[i].x));
color[1] = (char)(255 * clamp(0, 1, framebuffer[i].y));
color[2] = (char)(255 * clamp(0, 1, framebuffer[i].z));
fwrite(color, 1, 3, fp);
}
fclose(fp);
}
4.3作业结果
光线弹射一次的结果:
光线弹射2次:
因为折射的光线此时还在球里面,还没出来
光线弹射3次:
光线最大弹射5次结果(即作业要求结果):
5 读取ppm文件
因为作业生成的是.ppm文件,该文件在Linux下可以点击查看,win 上不行,所以需要另外的插件读取,下面链接是一个小软件,安装后即可在win上查看ppm文件
读ppm文件