参考课程:
https://www.bilibili.com/video/BV1X7411F744?p=12
P12大约从50分钟开始,就不是讲Geometry了,而是Shadow Mapping
一、Shadow Mapping 阴影贴图
经典的Shadow Mapping 只能处理点光源或方向光源投射下的阴影,并且会产生走样。
可以这样理解:
- 你通过相机看到了这个点,但光源看不到这个点,就是阴影;
- 而如果你通过相机看到这个点,光源也看到了这个点,那么不是阴影。
这是一个非0即1的结果,阴影会出现很明显的边界,也就是硬阴影(之后会有对应的软阴影)。
1.原理
先从光源看向场景,虚拟地放一个相机,做一遍光栅化,我们就会得到光源能看到什么。也就是通过zbuffer做深度测试,会有一部分遮挡。然后不进行着色,只是把所有的深度记下来。
然后从摄像机出发,再次看向这个场景。把现在看到的点,投影回光源刚才看到的投影平面上。进行深度比较,如果深度一致,就可以被看到。如果不一致,就看不到。
如上图,那根黄褐色的线,投影回光源记录的平面上,发现是一致的,就能看到。
如上图,那根黄褐色的线,投影回去后,发现之前记录的深度不一致(之前记的是上图红圈那个遮挡物的深度),此时就是看不到,这里就是阴影。
2.问题
从相机看过去,做完测试的结果看着很脏,为什么呢?这也是这个方法存在的问题之一:浮点数的相等比较存在精度问题,无法完全相等。人们处理精度的方法很多,比如不判断相等,只判断大于之前记录的深度就算阴影。但是有些值是非常接近的,那么再加一个阈值bias呢,就是在大于的基础上,要大于一个阈值这种。但是这些优化,只能改善,仍然不能彻底解决。
还有一个问题是,一开始我们从光源看向场景,我们要把它存到一个图里面。这个图本身存在分辨率,它与渲染时的ShadowMap本身分辨率搭配不好的话,会存在走样。当然用更大的深度图的分辨率,开销也会变大。
但是即使存在以上这些问题,目前依然是主流方法:
在我们打游戏时,会有阴影选项,低中高这些,其实就是在选阴影图的分辨率,当然效果越好,就越慢了。
二、软阴影
不透明体遮住光源时,如果光源是比较大的发光体,所产生的影子就有两部分,完全暗的部分叫本影,半明半暗的部分叫半影。
软阴影就是本影半影和影子之外这么一个过渡。这也说明,阴影取决于能看到多少多大的光源。而对于点光源来说,只能出现硬阴影,因为出现软阴影就要求光源有一定大小。
三、光追
观察光追效果,光线会经过多次反射才进入人的眼睛,更加真实。之前的光栅化很快速,但是质量不高。相对的,光追很准确,但是会比较慢。光栅化很容易做到实时,而光追经常做离线的应用,比如电影。
1.光线的定义
我们首先需要对光线定义:
- 光沿直线传播,
- 不发生碰撞,即两束光碰到了会继续传播
- 是从光源到人眼的。
对于第三个性质,我们在根据光路可逆性,应用时会采取从人眼到光源的方法。
2.Ray Casting 光线投射
我们假设往虚拟的世界中看,眼前放了一个成像平面,成像平面被我们画成不同的像素格子。对于每一个像素,我们可以从相机连一条线,穿过这个像素,这样就可以打出一根光线,可以打到场景中。如果光线和场景的某一物体相交,那么交点和光源连线,看光源是否可见这个点(这个点在不在阴影里),如果可见,那么就形成一条有效的光路。那么就可以计算这条光路上的能量,进行着色。
在下面的例子中,我们永远考虑眼睛是一个针孔摄像机,即眼睛是一个点,一个位置,不考虑实际相机的处理,以及镜头什么的(这部分会在路径追踪说)
对于光源,假设是点光源。对于场景中的物体,我们假设光打到它之后会发生完美的折射与反射,即镜面反射。
下图从眼睛开始,穿过成像平面的一个像素,投射一条光线(eye ray)
这个光线会打到场景的某一个位置上,我们取最近的交点。这里是要考虑遮挡的,即人眼发生的这条射线,只能看到最近的未被遮挡的物体。这样就解决了深度测试的问题,在之前光栅化的硬阴影中,这一步费了很大劲,效果还不好。
当我们发现了一个点之后,我们要考虑这个点会不会被照亮。我们从这点到光源连一条线(shadow ray),如果可以连上就表示能被照亮(下图黑线箭头为法线)。有了法线,入射方向,出射方向,我们就可以做着色,写入像素的值,这时候可以用各种各样的着色模型 比如之前的Blinn Phong。
现在总结一下:光线投射做了这么一件事,每一个像素投出去一个光线,和场景相交求的话求最近交点,最近交点和光源连线,判定是否可见,然后算着色,写回像素的值。
3.递归(Whitted风格)光线追踪
之前就是用光线投射的方法,我们还是只考虑光线弹射一次,但其实光线可以弹射很多次,这也就是接下来要介绍的这个方法能做的。
我们先考虑这个球是一个玻璃球。光线打到这个球上肯定发生两个事情,一个是要被反射掉,一个是被折射进去,然后再出来。
在算着色的过程中也发生了一点变化。之前是光线投射到这个点之后,看这个点能不能被照亮,然后再计算它的着色。在光线弹射次数多了以后,我们在每一个弹射点都会去计算着色的值(能量损失什么也要算,不然一直弹,就会过曝),然后把它们都加回这个像素的值里面去。
四、光线与物体相交,求交点
1.光线的定义
光线定义也就是一条射线,有一个起点o,有一个方向d,有这两个量就可以定义一条光线:
2.光线与球的交点
这个直接解方程:
我们拓展到一般性的隐式表面,方法都是一样的
3.光线与显式表面求交点
对于显式表面的渲染,光线如何与三角形求交是一个很重要的话题。
在2D几何上,任意封闭形状内,比如圆,或者正方形。选一个点向任意方向打一条射线,得到的交点数量一定是奇数。通过这个办法也可以判断一个点在不在物体内。而且,推广到3D,这个结论仍然成立。这里要强调的是,必须是封闭形状。
话题回到光线与三角形求交,在下图的小奶牛中,判断光线是否与它相交。最最简单的做法就是把它的三角形面挨个判断一遍(每个三角形面都会有0个或者1个交点),很直观但是很慢(之后会介绍加速方法)
怎么样做三角形和光线的交点呢?三角形肯定在一个平面内,所以问题可以被分成两部分:
- 光线是否和平面有交点
- 这个交点在不在三角形内部
点在不在三角形内部很容易判断了,主要是光线和平面交点如何判断,那么首先要定义平面。
4.平面的定义
采用点法式的定义:即用一个平面上的点,与平面上的法线,利用点乘为0的方式建立平面方程。
5.光线和平面的交点
现在又可以像之前的思路,联立解方程了:
6.Moller Trumbore Algorithm
上面两步仍然有些麻烦,可以合成一步,就是下面这个算法:
左边是光线上的点,右边是用重心坐标表示的三角形内的点,解法如下图,就是用克莱默法则解线性方程组。解出来之后要判断是否合理,首先 t 得是正的,并且b1 b2 b3都是非负的。
五、Accelerating Ray-Surface Intersection
上面的算法是与每一个三角形求交点,再找到最近的交点。但是计算次数非常多,我们需要改进。
1.轴对齐包围盒(AABB)的求交
包围盒的思想是,将一个复杂的物体,用简单的形状围起来,保证物体一定在这个简单的形状之内。如果光线连包围盒都碰不到,那肯定碰不到包围盒里的物体。
对于三维的形状,我们最常用的是长方体——不同的三组相对的平面形成的形状。这也是AABB——轴对齐包围盒,Axis-Aligned Bounding Box。所谓轴对齐,也就是长方体的任何一个轴都是沿着某个坐标轴比如x,y,z。
接下来我们考虑光线与包围盒的求交。我们先从二维的角度(由不同的两对对面形成)考虑,三维可以同理得到。对于给定的一个光线,我们可以分别求出它与竖直和水平面的交点(此时t可能会有正负)。我们取min里的max,max里的min,于是得到了进入和出去包围盒的 t 的值
为什么是取min的最大值,max的最小值呢。实际上,进入包围盒,意味着,光线要进入三组相对的平面,三个条件都达到,才算进入。而离开盒子,则是只要离开任意一组相对的平面,就算离开了盒子。
现在要考虑时间为负数的情况:因为光线实际上是一条射线,如果时间为负数,意味着盒子在射线的背后,需要反向延长,才能找到进入的时间值。
所以,如果离开时间小于0,肯定是不会有交点的。
那如果离开时间大于0,进入时间小于0呢?这说明,射线的起点在计算时,就已经在盒子里面。显而易见,这种情况,肯定有交点。
最后的结论就是,有交点的条件是,当且仅当,进入时间小于离开时间,并且离开时间大于等于0。