前言:我们为什么需要光线追踪?由之前的光栅化呈像难道不够吗?
- 是的,光栅化对于软阴影,模糊反射,间接光照(关在物体之间弹射很多次)问题时很难支持这些效果
- 其次,光栅化的成像速度很快,但是图片的质量很低
- 光线追踪的图像是很准确的,但是成像的速度是很慢的
摘要
本篇内容主要分为两部分,第一部分会从为什么需要光线追踪入手,一步步介绍Whitted-style光线追踪的原理。第二部分则会具体介绍一些光线追踪的实现细节,包括光线的表示,光线与物体的求交。
Whitted-style光线追踪原理
在计算机图形学里面我们要先将光线这个东西定义好
- 光线是直线传播
- 光线交叉是不会互相发生影响
- 计算机中模拟光线的方向是与现实相反的,就是我们是从摄像机的视角发出光线来感知物体位置颜色什么的
- 光线在遇到物体是假设是直接完美的进行了反射或折射方式
光线成像的方法
一般使用的是 光线投射法:
我们从摄像机(眼睛)投射出光线(eye ray)出去,当这个光线遇到一个最近物体时会与物体相交一个点,然后我们要看看着点是否在光源照射的范围以内,做法就是将这个点与光源直线连接(shadow ray),如果这个过程没有其他的物体阻拦,说明是被光源照射的
当我们知道这个点是可以被光源照射到的时候,就可以利用Blinn-Phong模型对这个点进行局部光照模型计算,得到该像素的颜色
多次反射与折射
我们知道如果只是单一的进行这种“光线追踪”,实现的仍然是局部关光照效果,我们要正式的考虑反射折射,实现全局光照效果
在光线发出后遇到第一个物体后,会根根据材质的信息发生对应反射与折射(假设该圆球是一个玻璃球)
这时候刚开始相交的那一个点的着色就不单单是那一点材质数据计算出来的了
这时候的做法是:我们要将每一个反射折射交点都与光源连线,看是否处于光源的照射下,再计算每一个交点的着色,最后将每一个点的着色都根据材质对应的一些权重累加到eye ray光线的这个像素上
光线的表示
我们使用单位向量和一个起点来定义光线
- 光源的起点是一个点 O ( x , y , z)
- 传播方向是一个单位向量d = ( x , y , z )
根据这个我们给出光线上任意一点的位置矢量的表计算方法
注意这是矢量的计算式!
光线传播伪代码
//传入的参数有这个光线对应的像素的位置,发出光线的方向,所有物体,深度
Color Ray_Tracing(oringinal_point, ray_derection, objs , depth)
{
if (depth > max_depth) return color(0, 0, 0);
//如果与物体有相交
if (IsHitObj(oringinal_point, ray_derection, objs))
{
hitpoint = GetHitPoint();//计算交点
normal = GetHitNormal();//得到交点的面材质
ReflectDirection = Reflect(ray_derection, normal);//有反射计算反射后的向量
RefractDirection = Refract(ray_derection, normal);//有折射计算折射后的向量
Local_color = Shader(oringinal_point, normal, light_position);//计算没反射折射的着色
//最后返回加权重的着色和
return Local_color
+ k1 * Ray_Tracing(hitpoint, ReflectDirection, objs, depth + 1);
+k2 * Ray_Tracing(hitpoint, RefractDirection, objs, depth + 1);
}
else
return Background_color;//没交点返回背景色
}
光线与物体的交点
简单引入
怎么来求光线与物体的交点呢?我们先从最简单的开始,就是光线与球体的交点
光线 r 表示:
球体 p 的表示:
然后我们联立两个式子解出光线相交物体的时间 t , 当 t 有两解或一解时,说明这个光线和物体有两个或一个交点,说明和球体是相割相切的,这时候离摄像机最近的点就是 t 取小的那一个对应的点
联立光线和球体的方程得到式子,可以使用一下的方式解:
推广普及
隐式表面求交
接下来我们推广到所有情况,在这之情,我们怎么表示一个隐式表面表面呢?隐式表面就是一组满足某种性质点的位置矢量的集合 , 比如表面
这时候,计算光与物体的交点时,我们考虑这个点p ,一定既在光线矢量上,又满足表面方程,由于在光线上,所以点 p 可以表示成某一个时间t对应的光线上的点o + t d, 并且这个点满足表面方程f,故得到方程f ( o + t d ) = 0,然后解出这个 t 的值 , 如果 这个t 是有意义并且是最近的一个交点时,那就把这个t 代入原光线方程, 就得到交点的位置矢量坐标了
其实我们不用太关心计算过程,因为目前有很多数学库可以实现各种隐式表面方程与光线交点的求解了,不用太担心
显式表面求交
显式表面是由三角形构成的, 在计算一个光线与物体的交点时,我们做法是将这个物体的所有三角形面都判断一下是不是与光线相交,解出得到很多个 t 或者一个, 这时候最小的 t 对应的那一个点就是我们要的交点。我们怎么得到光线与三角形平面的交点呢?
- 先判断光线是否相交三角形所在的平面里,计算交点p
- 判断点p是否在三角形内部
第二步我们知道如何求,主要看第一步怎么实现,怎么计算出光线与平面的交点?
做法
1.组成平面的点的集合(方程)p的定义我们使用点法式
2.求交点联立解出 t 的值,带回光线方程就可以得到交点
最后我们将这个点和对应的三角形判断一下是否在其内部,就可以知道这个三角形是否被这一根光线照亮了,但是这样的判定效率太低,我们可以使用MT算法来直接判断是否在三角形内部而不是作为两步来做
MT算法优化
我们使用重心坐标描述三角形,即三角形上的点的位置矢量一定可以表示成:
其中p1 , p2 , p3是三角形三个点的位置矢量,b1 , b2 是未知常量,由点的位置而定,当光线与三角形有交点时,一定有方程:
可以解出线性方程组中未知量 t , b1 , b2 ,如果b1 , b2 ,1- b1 - b2非负,而且t > 0 ,则可以确定这个点是否在三角形里面
解t和b1和b2的方法如下:
缺点
我们不难发现不管用上面的哪一个方法,判断所有光线的最近交点的效率是及其低下的,我们怎么加速这个求最近交点的过程呢?
加速求交方法
轴对称包围盒
我们为了优化,对于每一个物体我们使用一个长方体进行包装,保证物体全部在长方体里面,我们不难知道,如果光线连长方体都照射不到,那绝对不会照射到里面的物体了,这样可以大大简化计算的三角形的个数
其中我们的长方体是由三个无限大的对面形成的交集,并且平行于xyz轴,所以应该包围盒就是一组(x,y, z)的范围
判断光线是否与盒子相交
我们先考虑二维的情况,如何判断光线是否与长方形相交,对于长方形的盒子,有一组x的范围,y的范围,首先根据x和y的范围计算出两组数据{t_min1 , t_min2}和 {t_max1 , t_max2} ,然后我们取t_min中的最大的t1当做进入长方形的时间, 取t_max中最小的t2当做离开长方形的时间
如果0 <t1 < t2 说明就是经过这个长方体!!!
对于三维的同样方法:
根据x和y和z的范围计算出两组{t_min1 , t_min2 , t_min3}{t_max1 , t_max2 , t_max3} ,然后我们取t_min中的最大的t1当做进入长方体的时间, 取t_max中最小的t2当做离开长方体的时间
现在对于得到的t1 t1 分类讨论一下:
- t2 < 0 则与盒子无交点
- t1 < 0 , t2 >0 则光原在盒子里面,有交点
综上,光线在盒子里面的条件就是 t1 < t2 && t2 > 0
实现加速求交
普通解法
我们对于每一个包围盒,按照一定密度划分成更小的盒子,预处理小盒子里面有物体的,将其标记好,如下图,灰色的就是这个小盒子里面是有物体的
在我们判断出来光线是和这个盒子有交点后,按照光线的方向,一个格子一个格子的判断这个小盒子里面是是否有物体,如果有,说明这个光线是有可能打到物体的,所以要进行三角形求交,看有没有交点,没有就继续走下一个盒子,直到找到了交点。在这之后该折射就折射该反射就反射, 继续判断交点
但是这种方法在有很多空的区域的场景的效率还是很低,因为光线在空的场景中一个一个的判断小格子有无物体,然后再一步一步走太慢了,我们需要别的方法
空间划分
这同样是一个预处理,不是在渲染的时候需要考虑的事情
对于一个包围盒,对于空区域我们画大盒子,有物体的画密点的盒子,一般的划分方法有三种:八叉树 , KD划分,bsp划分
我们主要学KD-树,这同样是在做光线追踪之前先处理好,先把树建好
建KD树
建立过程如图:
下图我们只考虑了一个格子,其实每一个格子都会进行划分的,最终叶子节点上才是储存的物体信息
利用KD数求最近交点
使用递归思想,首先判断光线是否可以与当前盒子相交,在依次访问子节点,直到到达叶子节点从而返回最近的交点
伪代码:
struct ElemType_of_AABB
{
//aabb盒子的边界信息
//如果是叶子节点同时还存有物体的信息
};
struct KDTree //节点设置
{
ElemType_of_AABB aabb;
KDTree* lchild;
KDTree* rchild;
};
//求光线和当前盒子最近交点函数,返回一个点
Node NIntsection(Type_of_light light, KDTree* Tree )
{
//先看看光线是否和盒子有相交
if ( is_intersect (light, Tree->aabb) )
{
//有交点先判断是不是叶子
if (Tree->lchild == nullptr && Tree->rchild == nullptr)
//是叶子直接计算这个叶子里面物体的最近交点并返回
return NObjIntsection(light, Tree->aabb);
else
{
//递归看左右kd树,收集最近节点
Node l = NIntsection(light, Tree->lchild);
Node r = NIntsection(light, Tree->rchild);
return Min(l, r);//返回左右中近的那一个
}
}
}
这样的一个函数就可以返回这一根光线与场景里面最近物体的交点,进而计算着色信息叠加在这个光线对应的像素上
KD划分的局限性
使用划分空间的KD树来建立aabb树有很大局限,由于是划分空间,所以有的物体可能会出现在两个aabb盒子里面,这样在存储计算有很大的难度
BVH加速结构
所以我们最终的方法就是划分物体,将物体进行分组来实现加速,这个加速结构叫做BVH
我们将物体进行类似KD树的划分,只是是将物体进行分组
划分依据:
- 考虑划分之后的两个子盒子里面的物体数量尽量要相近
- 往最长的那个方向垂直的位置划
划分方法:
在预处理的时候进行划分,比如这一次的划分是一个yx平面,那我们将这次被划分的大盒子里面的三角形按照z轴大小排序,取中间的那个三角形的重心作为划分的位置,这个边界的三角形取左边还是右边取决于你自己
ps:当场景里面移动了物体,那其实是需要重新计算BVH的
划分步骤:
先对于所有物体找到包围盒,形成根节点,保存好aabb的范围,由于没有停止递归,所以aabb里面不会存储物体信息。
然后把物体分成两部分,比如在某一个方向上划分成两个区域,使用原函数传入划分参数进行递归,
不断的递归重复这个过程,知道满足一定的条件(当节点包含少于5个元素)后停止,这时候需要把当前aabb范围里面的物体存入obj列表,存储物体信息。
结构大概
BVH本质上是树,节点包含孩子和这级aabb的范围,示意图如下:
内点结构包括:
- 这一级的包围盒,但是里面没有存储物体信息
- 子节点的指针
叶子节点:
- 叶子节点的包围盒
- 其中叶子里面包围盒存储有所有物体的列表
伪代码
Node BVH_Intersect(Type_of_light light, BVH* tree)
{
//先看看光线是否和盒子有相交
if (Is_Intersect(light, tree->aabb));
{
//有交点先判断是不是叶子
if (tree->lchild == nullptr && tree->rchild == nullptr)
//是叶子直接计算这个叶子的aabb里面所有物体的最近交点并返回
return NObjIntsection(light, tree->aabb);
else
{
//递归看左右bvh树,收集最近节点
Node l = NIntsection(light, tree->lchild);
Node r = NIntsection(light, tree->rchild);
return Min(l, r);//返回左右中近的那一个
}
}
}