1.为什么要光线追踪
- 光栅化不能很好地处理全局效果,例如下面的
- 软阴影
- Glossy Reflection (存在一部分反射,也有一定的模糊效果,类似毛玻璃)
- 间接光照
-
光栅化是一种很快但质量相对较低的技术
-
光线追踪是一种很准确,但很慢的技术
- 光栅化应用于实时渲染(每秒钟生成超过30帧的图)
- 光线追踪更多地应用于离线渲染(比如电影制作等)
2.基础的光线追踪算法
光线追踪中对光线有以下的假设:
- 光线是沿直线传播的(尽管这是错误的)
- 光线和光线不会发生“碰撞”(尽管这仍然是错误的)
- 光线从光源传播到我们的的眼睛(光路可逆)
[1]Ray Casting(光线投射)
Appel 1968 - Ray casting
- 对于成像平面上每一个像素,从人眼(相机)连一条线,穿过该像素,打出一条光线,该光线会打到场景中某个位置(物体),有些物体可以被前面的物体遮挡到
- 将打到的位置,与光源做一条连线,如果该连线没有和其他物体相交,即该点可以被光源照到,即形成了一条有效的光路,就可应用着色模型着色;
例如:针孔摄像机模型
从人眼对某像素投射出一条线(eye ray)(有了出射光线)到场景中,找到最近的交点,从该点向光源再连一条线(入射光线),如果该点能被光照到,再根据该点的法线,即可应用着色模型计算出着色,再将计算出的值,写入该像素点
但该算法仍只考虑了光的一次弹射
[2]Whitted-Style 递归光线追踪
一种改进的阴影显示照明模型
与 Ray Casting 不同,Whitted-Style 会递归多次计算人眼打出光线的折射,反射,并将每次反射、折射的计算结果叠加到像素上,(注意能量损失)
- primary ray:人眼打出的检测光线;
- second ray:折射、反射的光线;
- shadow ray:计算可见性的光线;
这样基本原理了解后,接下来深入其技术细节
3.光线与平面交点
[1]光线方程及应用
Ray-Surface Intersection
光线在数学上的定义为射线
r ( t ) = o + t d ; 0 ⩽ t < ∞ r(t)=o+td;\\ 0\leqslant t<\infty r(t)=o+td;0⩽t<∞
在推导出光线于各个物体相交前,这里像推导光线与球相交
R
a
y
:
r
(
t
)
=
o
+
t
d
,
0
⩽
t
<
∞
S
p
h
e
r
e
:
p
:
(
p
−
c
)
2
−
R
2
=
0
Ray:r(t)=o+td,0\leqslant t<\infty \\ Sphere:p:(p-c)^2-R^2=0
Ray:r(t)=o+td,0⩽t<∞Sphere:p:(p−c)2−R2=0
求它们的交点,也就是说这个点即在光线上,也在球面上(联立方程)
(
o
+
t
d
−
c
)
2
−
R
2
=
0
(o+td-c)^2-R^2=0
(o+td−c)2−R2=0
最终解出t,t需要有物理意义
[2]光线和隐式表面的求交
常见隐式表面的定义:
p
⃗
:
f
(
p
⃗
)
=
0
\vec p:f(\vec p)=0
p:f(p)=0
与隐式表面求交也就是这个点即在光线上,也在隐式表面上,联合方程
f
(
o
+
t
d
)
=
0
f(o+td)=0
f(o+td)=0
[3]光线和显式表面的求交
光线和显式表面的求交对于最简单的方法是:通过判断光线是否与三角形表面相交来首先,这种方式很简单,但是性能不好(比如一个有千万级三角格网的场景,就需要计算千万级次),这里先按这种方式然后逐渐深入优化。
光线与三角形求交
通过光线和三角形的求交,也可以实现inside/outside test
这里可以将光线与三角形求交简化为:
- 光线和三角形所在平面求交
- 检测交点是否在三角形内
定义平面方程:(平面由一条法线
N
N
N和一个点
p
′
p'
p′定义)
p
⃗
:
(
p
⃗
−
p
⃗
′
)
⋅
N
⃗
=
0
a
x
+
b
y
+
c
z
+
d
=
0
\vec p:(\vec p-\vec p')\cdot\vec N=0\\ ax+by+cz+d=0
p:(p−p′)⋅N=0ax+by+cz+d=0
联合平面方程和光线方程得:
t
=
(
p
⃗
′
−
o
⃗
)
⋅
N
d
⃗
⋅
N
⃗
t=\frac{(\vec p'-\vec o)\cdot N}{\vec d\cdot \vec N}
t=d⋅N(p′−o)⋅N
当
0
⩽
t
<
∞
0\leqslant t<\infty
0⩽t<∞时光线交于平面
Möller Trumbore算法
一种更快的方法,该算法通过重心坐标表示的三角形计算,可以直接算三角形是否与光线相交。(使用克莱姆法则)
尽管如此,使用这种方式判断光线于物体相交还是太慢了。所以后来将对光线与三角形的范围推到对一种光线与一种包含空间对象的容器求交的范围,空间的object完全的被容器包围着。
其中最常用的是 box 容器
[4]Box包围盒
Box包围盒并不是我们之前理解的立方体(在空间中占据一定体积的物体),它是由三对对面取交集组成,每对对面都是无限大的。
我们常用的一种是Axis-Aligned Bounding Box (AABB) 轴对⻬包围盒。如下长方体
光线与二维包围盒求交
二维和三维的原理是一样的,这里用二维的场景来简化理解,对于一个二维的包围盒,也就是只有两对”对面“组成的平面(脑补。。。)
分别计算出光线与每对对面两个交点,然后取交集。即光线进入所有的边后,才算进入,出离一个边后,就算出去。
核心观点:
- 对于三维来说,光线进入所有对面后,才算进入,只要离开任何一对平面后,就算出去。
对于每一对,计算 t m i n t_{min} tmin 和 t m a x t_{max} tmax (可为负)
对于3D box , t e n t e r = m a x [ t m i n ] , t e x i t = m a x [ t m a x ] t_{enter} = max[t_{min}], t_{exit} = max[t_{max}] tenter=max[tmin],texit=max[tmax]
- 如果 t e n t e r < t e x i t t_{enter}<t_{exit} tenter<texit,即光线在盒子里呆了一段时间,即有交点
- 如果 t e x i t > = 0 t_{exit} >= 0 texit>=0,即代表射线位于box内部,即有交点
总之,当且仅当进入时间<离开时间且离开时间>=0时,有交点
为什么要使用轴对齐?
可以优化运算过程
4.Using AABBs to accelerate ray tracing
[1]Uniform Spatial Partitions (Grids)统一空间分区
这里的和物体相交指的是和物体表面相交,将空间中的包围盒重新划分为更小的包围盒
右上角图有误
这样就将这个场景进行了预处理了,将灰色的(与物体相交的box)存储起来
然后就可以做光线追踪了,一条光线从场景中经过,我们只记录与其相交的包围盒,然后判断这个包围盒中是否有物体,如果有,再判断光线与这个物体有没有相交。
比如上图中,在深蓝色的格子与一个物体相交,在此之前有2个有物体的包围盒,但是并不与物体相交
关于怎么寻找相交的包围盒的问题,并不需要与所有的包围盒进行判断,我们可以用光栅化一条线(虎书p161)时用的方法(bresenham)来做
因为判断光线与盒子求交比判断光线与物体求交简单得多,所以宁可多做光线与包围盒求交也不做光线与物体求交
关于划分的数量选择:
- 如果划分的包围盒特别少:几乎没有加速效果
- 如果划分的包围盒特别多:会有很多 多余的、没有物体的 无效包围盒 影响效率
那么对于统一空间分区的使用场景:
- 这样的方法适用于场景内均匀分布的很多物体的情况
- 对于一个空旷的场景,物体与物体之间距离很大,这样的方法效果就会很差(与上同理)
[2]Spatial Partitions空间划分
空间划分是基于统一空间分区的不足之处而实现的另一种加速渲染的方法
- Oct-Tree八叉树:(把一个包围盒切成八块,二维的情况为四叉树:例如上图。八叉树的缺点是随着维度的升高,其分块的数量会成指数级升高)
- KD—Tree:一刀一刀砍,例如先横砍一刀,然后竖砍一刀(类似于二叉树)
- **BSP-Tree:**每次选一次方向来砍
KD-tree
逐步的将物体存储在叶子节点上,内部节点不存储物体
在下图中,有一道光线从场景中穿过,第一步是判断这个光线与整个场景的包围盒(A)是否有相交
上图中做左边1叶子节点处应该还要划分的,这里偷懒不做划分
如果相交,接着判断光线与其2个子区域1、B是否相交,相交的化在划分节点
这里2还需划分
和剩下浅色部分在判断,有交点,划分节点
直到求交到叶子节点于光线无交点。(判断完与某块包围盒相交后还要判断是否和内部的物体相交,如果与里面的物体相交则挺划分,直接成为叶子节点)
- 优点:
利用KD-Tree的结构来构建AABB的好处是倘若光线与哪一部分空间不相交,那么则可以直接省略该部分空间所有子空间的判断过程,进一步提升了加速效率 - 缺点:
缺点是判断包围盒与物体的是否相交较难,因此KD-Tree的建立过程不是那么想象的简单,其次同一个物体可能被不同的包围盒同时占有,在每一个包围盒内都要重复存一次这个物体 - 所以近十年很少用KD-Tree
[3]BVH & 对象划分
直接通过物体来划分,才是现代的做法,不管是实时的光线追踪还是离线的,基本上应用的都是这样的一种结构。
其划分规则如上图所示:将一个包围盒内的物体先划分成两部分,再重新求他们的包围盒,而不是像KD-tree先将空间划分。
这样的划分方法,可以保证每个物体只会出现在一个包围盒当中,避免了KD-Tree的缺点
但是它同样有一个小缺陷,它所划分的物体的包围盒可能会有交错,不过影响并不大
关于如何划分包围盒(节点)的方法:
-
选择要拆分的维度
-
永远沿着包围盒最长的轴划分
-
选择一个中位物体,使得左右两边的物体数量尽可能相等进行划分
(举例:选定一个轴,然后将所有包围盒内的三角形面的重心在这个轴上的坐标进行排序/用堆取中位数,这个三角形面当做中位物体)
终止条件:
- 当节点包含几个元素时停止
BVH的伪代码实现参考: