OpenGL学习笔记(十)

OpenGL 高级篇(一)

(一)高级光照

Blinn-Phong

Phong反射的θ小于90度。如下图:
在这里插入图片描述
视线和反射之间的角θ大于90度,这样镜面反射成分将会被消除。通常这也不是问题,因为视线方向距离反射方向很远,但如果使用一个数值较低的发光值参数的话,镜面半径就会足够大,以至于能够贡献一些镜面反射的成份了。在下面的例子中,在角度大于90度时消除了这个贡献
在这里插入图片描述
Blinn - Phong模型很大程度上和Phong是相似的,不过它稍微改进了Phong模型,使之能够克服所讨论到的问题。它放弃使用反射向量,而是基于一个叫做半程向量(halfway vector)的向量,即光线与视线夹角一半方向上的一个单位向量。半程向量和表面法线向量越接近,镜面反射成份就越大。
在这里插入图片描述
当视线正好与(现在不需要的)反射向量对齐时,半程向量就会与法线完美契合。所以当观察者视线越接近于原本反射光线的方向时,镜面高光就会越强。
现在,不论观察者向哪个方向看,半程向量与表面法线之间的夹角都不会超过90度(除非光源在表面以下)。它产生的效果会与冯氏光照有些许不同,但是大部分情况下看起来会更自然一点,特别是低高光的区域。Blinn - Phong着色模型正是早期固定渲染管线时代时OpenGL所采用的光照模型。
得到半程向量很容易,将光的方向向量和观察向量相加,然后将结果归一化(normalize);
在这里插入图片描述
GLSL代码如下:
在这里插入图片描述
实际的镜面反射的计算,就成为计算表面法线和半程向量的点乘,并对其结果进行约束(大于或等于0),然后获取它们之间角度的余弦,再添加上发光值参数:
在这里插入图片描述
Blinn - PhongPhong的镜面反射唯一不同之处在于,现在要测量法线和半程向量之间的角度,而半程向量是视线方向和反射向量之间的夹角。
Blinn - Phong着色的一个附加好处是,它比Phong着色性能更高,因为不必计算更加复杂的反射向量了。
引入半程向量计算镜面反射后,再也不会遇到Phong着色骤然截止问题了。下图展示了两种不同方式下发光值指数为0.5时镜面区域的不同效果:
在这里插入图片描述
另一个细微差别是半程向量和表面法线之间的角度经常会比视线和反射向量之间的夹角更小。结果就是,为了获得和Phong着色相似的效果,必须把发光值参数设置的大一点。通常的经验是将其设置为Phong着色的发光值参数的2至4倍。下图是Phong指数为8.0和Blinn - Phong指数为32的时候,两种specular反射模型的对比:
在这里插入图片描述在这里插入图片描述

(二)阴影映射

1.阴影映射

阴影是光线被阻挡的结果:当一个光源的光线由于其他物体的阻挡不能够达到一个物体的表面的时候,那么这个物体就在阴影中了。阴影能够使场景看起来真实得多,并且让观察者获得物体之间的空间位置关系。下图展示了有阴影和没有阴影的情况下的不同:
在这里插入图片描述
阴影映射(Shadow Mapping) 背后的思路非常简单:以光的位置为视角进行渲染,能看到的东西都将被点亮,看不见的一定是在阴影之中了。假设有一个地板,在光源和它之间有一个大盒子。由于光源处向光线方向看去,可以看到这个盒子,但看不到地板的一部分,这部分就应该在阴影中了。
在这里插入图片描述
希望得到射线第一次击中的那个物体,然后用这个最近点和射线上其他点进行对比。然后将测试一下看看射线上的其他点是否比最近点更远,如果是的话,这个点就在阴影中。对从光源发出的射线上的成千上万个点进行遍历是个极端消耗性能的举措,实时渲染上基本不可取。
可以采取相似举措,不用投射出光的射线:深度缓冲
在深度缓冲里的一个值是摄像机视角下,对应于一个片元的一个0到1之间的深度值。如果从光源的透视图来渲染场景,并把深度值的结果储存到纹理中会怎样?通过这种方式,就能对光源的透视图所见的最近的深度值进行采样。最终,深度值就会显示从光源的透视图下见到的第一个片元了。管储存在纹理中的所有这些深度值,叫做深度贴图(depth map)阴影贴图
在这里插入图片描述
上图展示了一个定向光源(所有光线都是平行的)在立方体下的表面投射的阴影。通过储存到深度贴图中的深度值,就能找到最近点,用以决定片元是否在阴影中。使用一个来自光源的视图和投影矩阵来渲染场景就能创建一个深度贴图。这个投影和视图矩阵结合在一起成为一个T变换,它可以将任何三维位置转变到光源的可见坐标空间。
在下图中显示出同样的平行光和观察者。渲染一个点P处的片元,需要决定它是否在阴影中。先得使用T把P变换到光源的坐标空间里。既然点P是从光的透视图中看到的,它的 z 坐标就对应于它的深度,例子中这个值是0.9。使用点P在光源的坐标空间的坐标,可以索引深度贴图,来获得从光的视角中最近的可见深度,结果是点C,最近的深度是0.4。因为索引深度贴图的结果是一个小于点P的深度,可以断定P被挡住了,它在阴影中了。
在这里插入图片描述

2.深度贴图

深度映射由两个步骤组成:首先渲染深度贴图,然后渲染场景,使用生成的深度贴图来计算片元是否在阴影之中。听起来有点复杂,但随着一步一步地讲解这个技术,就能理解了。
第一步需要生成一张深度贴图(Depth Map)。深度贴图是从光的透视图里渲染的深度纹理,用它计算阴影。因为需要将场景的渲染结果储存到一个纹理中,将再次需要帧缓冲。
首先,要为渲染的深度贴图创建一个帧缓冲对象:
在这里插入图片描述
然后,创建一个2D纹理,提供给帧缓冲的深度缓冲使用。因为只关心深度值,要把纹理格式指定为GL_DEPTH_COMPONENT。再把纹理的高宽设置为1024。
在这里插入图片描述
把生成的深度纹理作为帧缓冲的深度缓冲:
需要的只是在从光的透视图下渲染场景时的深度信息,所以颜色缓冲没有用。然而帧缓冲对象是完全不包含颜色缓冲的,所以需要显式告诉OpenGL不使用任何颜色数据进行渲染。通过调用glDrawBufferglReadBuffer把读和绘制缓冲设置为GL_NONE来做这件事。
在这里插入图片描述
合理配置将深度值渲染到纹理的帧缓冲后,就可以开始第一步了:生成深度贴图。两个步骤的完整的渲染阶段,看起来有点像这样:
在这里插入图片描述

光源空间的变换

前面那段代码中有一个函数是ConfigureShaderAndMatrices。它是用来在第二个步骤确保为每个物体设置了合适的投影和视图矩阵,以及相关的模型矩阵。然而,第一个步骤中,从光的位置的视野下使用了不同的投影和视图矩阵来渲染景。
因为使用的是一个所有光线都平行的定向光。所以,将为光源使用正交投影矩阵,透视图将没有任何变形:
在这里插入图片描述
同时,投影矩阵间接决定可视区域的范围,以及哪些东西不会被裁切,需要保证投影视锥(frustum)的大小,以包含在深度贴图中包含的物体。当物体和片元不在深度贴图中时,它们就不会产生阴影。
为了创建一个视图矩阵来变换每个物体,把它们变换到从光源视角可见的空间中,将使用glm::lookAt();这次从光源的位置看向场景中央。
在这里插入图片描述
二者相结合提供了一个光空间的变换矩阵,它将每个世界空间坐标变换到光源处所见到的那个空间;这正是渲染深度贴图所需要的。
在这里插入图片描述
这个lightSpaceMatrix正是前面称为 T 的那个变换矩阵。有了lightSpaceMatrix只要给shader提供光空间的投影和视图矩阵,就能像往常那样渲染场景了。然而,只关心深度值,并非所有片元计算都在着色器中进行。为了提升性能,将使用一个与之不同但更为简单的着色器来渲染出深度贴图。

渲染至深度贴图

当以光的透视图进行场景渲染的时候,会用一个比较简单的着色器,这个着色器除了把顶点变换到光空间以外,不会做得更多了。这个简单的着色器叫做simpleDepthShader,就是使用下面的这个着色器:
在这里插入图片描述
由于没有颜色缓冲,最后的片元不需要任何处理,所以可以简单地使用一个空片段着色器:
在这里插入图片描述
这个空像素着色器什么也不干,运行完后,将会更新深度缓冲。可以取消那行的注释,来显式设置深度,但是注释掉那行代码之后是更有效率的,因为底层无论如何都会默认去设置深度缓冲。
渲染深度缓冲代码如下:
在这里插入图片描述
RenderScene()的参数是一个着色器程序(shader program),它调用所有相关的绘制函数,并在需要的地方设置相应的模型矩阵。
将深度贴图渲染到四边形上的片段着色器:
在这里插入图片描述

3.渲染阴影

正确生成深度贴图后,可以开始生成阴影。这段代码在像素着色器中执行,用来检验一个片元是否在阴影之中,不过在顶点着色器中进行光空间的变换:
在这里插入图片描述
上面的代码中,FragPosLightSpace为新添加的输出向量。用同一个lightSpaceMatrix,把世界空间顶点位置转换为光空间。顶点着色器传递一个普通的经变换的世界空间顶点位置vs_out.FragPos和一个光空间的vs_out.FragPosLightSpace给片段着色器。
片段着色器使用Blinn - Phong光照模型渲染场景。接着计算出一个shadow值,当fragment在阴影中时是1.0,在阴影外是0.0。然后,diffuse和specular颜色会乘以这个阴影元素。由于阴影不会是全黑的(由于散射),把ambient分量从乘法中剔除。
在这里插入图片描述在这里插入图片描述
片段着色器大部分是从之前课程中复制过来,只不过加上阴影计算。声明一个shadowCalculation(),来计算阴影。
片段着色器的最后,把diffusespecular乘以(1 - 阴影元素),这表示这个片元有多大成分不在阴影中。这个片段着色器还需要两个额外输入,一个是光空间的片元位置和第一个渲染阶段得到的深度贴图。
首先要检查一个片元是否在阴影中,把光空间片元位置转换为裁切空间的标准化设备坐标。当在顶点着色器输出一个裁切空间顶点位置到gl_Position时,OpenGL自动进行一个透视除法,将裁切空间坐标的范围 - w到w转为 - 1到1,这要将x、y、z元素除以向量的w元素来实现。由于裁切空间的FragPosLightSpace并不会通过gl_Position传到像素着色器里,必须自己做透视除法:
在这里插入图片描述
因为来自深度贴图的深度在0到1的范围,使用projCoords从深度贴图中去采样,所以将NDC坐标变换为0到1的范围:
在这里插入图片描述
有了这些投影坐标,就能从深度贴图中采样得到0到1的结果,从第一个渲染阶段的projCoords坐标直接对应于变换过的NDC坐标。将得到光的位置视野下最近的深度:
在这里插入图片描述
为了得到片元的当前深度,简单获取投影向量的z坐标,它等于来自光的透视视角的片元的深度。
在这里插入图片描述
实际的对比就是简单检查currentDepth是否高于closetDepth,如果是,那么片元就在阴影中。
在这里插入图片描述
完整的shadowCalculation()如下所示:
在这里插入图片描述
激活着色器,绑定合适的纹理,激活第二个渲染阶段默认的投影以及视图矩阵,结果如下图所示,可以看到地板上有立方体的阴影:
在这里插入图片描述

4.渲染阴影

因为阴影贴图受限于大小(解析度),在距离光源较远的情况下,多个片元可能从深度贴图的同一个值中去采样。图片每个斜坡代表深度贴图一个单独的纹理像素。可以看到,多个片元从同一个深度值进行采样。
在这里插入图片描述
可以用阴影偏移(shadow bias)的技巧来解决这个问题,简单的对表面的深度(或深度贴图)应用一个偏移量,这样片元就不会被错误地认为在表面之下了。
在这里插入图片描述
使用了偏移量后,所有采样点都获得了比表面深度更小的深度值,这样整个表面就正确地被照亮,没有任何阴影。可以这样实现这个偏移:
在这里插入图片描述
设置0.005的偏移就能帮到很大的忙,但是有些表面坡度很大,仍然会产生阴影失真。
有一个更好的办法能够根据表面朝向光线的角度更改偏移量:使用点乘:
在这里插入图片描述
设置一个偏移量的最大值0.05,和一个最小值0.005,它们是基于表面法线和光照方向的。这样像地板这样的表面几乎与光源垂直,得到的偏移就很小,而比如立方体的侧面这种表面得到的偏移就更大。
下图展示了同一个场景,但使用了阴影偏移,效果比之前更好:
在这里插入图片描述

悬浮

使用阴影偏移的一个缺点是你对物体的实际深度应用了平移。偏移有可能足够大,以至于可以看出阴影相对实际物体位置的偏移,可以从下图看到这个现象(这是一个夸张的偏移值):
在这里插入图片描述
这个阴影失真叫做悬浮,因为物体看起来轻轻悬浮在表面之上。可以使用一个技巧解决大部分的悬浮问题:当渲染深度贴图时候使用正面剔除(front face culling)
前面学习了在OpenGL中默认是背面剔除,现在要告诉OpenGL需要剔除正面。
因为只需要深度贴图的深度值,对于实体物体无论用它们的正面还是背面都没问题。使用背面深度不会有错误,因为阴影在物体内部有错误我们也看不见。
在这里插入图片描述
为了修复悬浮,需要进行正面剔除,先必须开启GL_CULL_FACE:
在这里插入图片描述
这十分有效地解决了悬浮问题,但只针对实体物体。场景中,在立方体上工作的很好,但在地板上无效,因为正面剔除完全移除了地板。地面是一个单独的平面,不会被完全剔除。如果打算使用这个技巧解决悬浮则必须考虑到只有剔除物体的正面才有意义。

采样过多

另一个要考虑到的地方是接近阴影的物体仍然会出现不正确的效果。必须考虑到何时使用正面剔除对物体才有意义。不过使用普通的偏移值通常就能避免悬浮。
还有一个视觉差异问题,就是光的视锥不可见的区域一律被认为是处于阴影中,不管它真的处于阴影之中。出现这个状况是因为超出光的视锥的投影坐标比1.0大,这样采样的深度纹理就会超出它默认的0到1的范围。根据纹理环绕方式,将会得到不正确的深度结果,它不是基于真实的来自光源的深度值。
可以在图中看到,光照有一个区域,超出该区域就成为了阴影;这个区域实际上代表着深度贴图的大小,这个贴图投影到了地板上。发生这种情况的原因是之前将深度贴图的环绕方式设置成了GL_REPEAT
在这里插入图片描述
可以让所有超出深度贴图的坐标的深度范围是1.0,这样超出的坐标将永远不在阴影之中。可以储存一个边框颜色,然后把深度贴图的纹理环绕选项设置为GL_CLAMP_TO_BORDER
在这里插入图片描述
现在如果采样深度贴图0到1坐标范围以外的区域,纹理函数总会返回一个1.0的深度值,阴影值为0.0。结果看起来会更真实,但是仍有一部分是黑暗区域。那里的坐标超出了光的正交视锥的远平面。可以看到这片黑色区域总是出现在光源视锥的极远处。:
在这里插入图片描述
当一个点比光的远平面还要远时,它的投影坐标的 z 坐标大于1.0。这种情况下,GL_CLAMP_TO_BORDER环绕方式不起作用,因为把坐标的z元素和深度贴图的值进行了对比;它总是为大于1.0的z返回true。
解决办法:只要投影向量的 z 坐标大于1.0,就把shadow的值强制设为0.0:
在这里插入图片描述
检查远平面,并将深度贴图限制为一个手工指定的边界颜色,就能解决深度贴图采样超出的问题,最终得到下面所运行的效果:
在这里插入图片描述

5.PCF

阴影现在已经附着到场景中了,不过仍然有待完善。如果放大看阴影,阴影映射对解析度的依赖很快变得很明显。
在这里插入图片描述
因为深度贴图有一个固定的解析度,多个片元对应于一个纹理像素。结果就是多个片元会从深度贴图的同一个深度值进行采样,这几个片元便得到的是同一个阴影,这就会产生锯齿边。
可以通过增加深度贴图解析度的方式来降低锯齿块,也可以尝试尽可能的让光的视锥接近场景。
另一个解决方案叫做PCF(percentage - closer filtering),这是一种多个不同过滤方式的组合,它产生柔和阴影,使它们出现更少的锯齿块和硬边。核心思想是从深度贴图中多次采样,每一次采样的纹理坐标都稍有不同。每个独立的样本可能在也可能不再阴影中。所有的次生结果接着结合在一起,进行平均化,就得到了柔和阴影。
一个简单的PCF的实现是简单的从纹理像素四周对深度贴图采样,然后把结果平均起来:
在这里插入图片描述
textureSize返回一个给定采样器纹理的0级mipmap的vec2类型的宽和高。用1除以它返回一个单独纹理像素的大小。用来对纹理坐标进行偏移,确保每个新样本,来自不同的深度值。这里采样得到9个值,它们在投影坐标的x和y值的周围,为阴影阻挡进行测试,并最终通过样本的总数目将结果平均化。
使用更多的样本,更改texelSize变量,就可以增加阴影的柔和程度。
下面可以看到应用了PCF的阴影,从稍微远一点的距离看去,阴影效果好多了,也不那么生硬了。如果放大,仍会看到阴影贴图解析度的不真实感,但通常对于大多数应用来说效果已经很好了。
在这里插入图片描述

正交 vs 投影

在渲染深度贴图的时候,正交(Orthographic)投影(Projection) 矩阵之间有所不同。正交投影矩阵并不会将场景用透视图进行变形,所有视线 / 光线都是平行的,这使它对于定向光来说是个很好的投影矩阵。然而透视投影矩阵,会将所有顶点根据透视关系进行变形,结果因此而不同。下图展示了两种投影方式所产生的不同阴影区域:
在这里插入图片描述
透视投影对于光源来说更合理,不像定向光,它是有自己的位置的。透视投影因此更经常用在点光源和聚光灯上,而正交投影经常用在定向光上。
另一个细微差别是,透视投影矩阵,将深度缓冲视觉化经常会得到一个几乎全白的结果。发生这个是因为透视投影下,深度变成了非线性的深度值,它的大多数可辨范围接近于近平面。为了可以像使用正交投影一样合适的观察到深度值,必须先将非线性深度值转变为线性的。
这个深度值与见到的用正交投影的很相似。需要注意的是,这个只适用于调试;正交或投影矩阵的深度检查仍然保持原样,因为相关的深度并没有改变。
在这里插入图片描述

(三)点光源阴影

上小节学到了如何使用阴影映射技术创建动态阴影。效果不错,但它只适合定向光,因为阴影只是在单一定向光源下生成的。所以它也叫定向阴影映射,深度(阴影)贴图生成自定向光的视角。
本节焦点是在各种方向生成动态阴影。这个技术可以适用于点光源,生成所有方向上的阴影。这个技术叫做点光源阴影,或万向阴影贴图(omnidirectional shadow maps)技术。
本节代码基于前面的阴影映射教程,所以如果对传统阴影映射不熟悉,还是建议先熟悉阴影映射。 算法和定向阴影映射差不多:从光的透视图生成一个深度贴图,基于当前fragment位置来对深度贴图采样,然后用储存的深度值和每个fragment进行对比,看看它是否在阴影中。定向阴影映射和万向阴影映射的主要不同在于深度贴图的使用上。
对于深度贴图,需要从一个点光源的所有渲染场景,普通2D深度贴图不能工作;如果使用立方体贴图会怎样?因为立方体贴图可以储存6个面的环境数据,它可以将整个场景渲染到立方体贴图的每个面上,把它们当作点光源四周的深度值来采样。
生成后的深度立方体贴图被传递到光照像素着色器,它会用一个方向向量来采样立方体贴图,从而得到当前的fragment的深度(从光的透视图)。大部分复杂的事情已经在阴影映射教程中讨论过了。算法只是在深度立方体贴图生成上稍微复杂一点。
在这里插入图片描述

生成深度立方体贴图

为创建一个光周围的深度值的立方体贴图,必须渲染场景6次:每次一个面。显然渲染场景6次需要6个不同的视图矩阵,每次把一个不同的立方体贴图面附加到帧缓冲对象上。如所示:
在这里插入图片描述
首先,需要创建一个立方体贴图:
GLuint depthCubemap;
glGenTextures(1, &depthCubemap);
在这里插入图片描述
不要忘记设置合适的纹理参数:
在这里插入图片描述
正常情况下,把立方体贴图纹理的一个面附加到帧缓冲对象上,渲染场景6次,每次将帧缓冲的深度缓冲目标改成不同立方体贴图面。
由于将使用一个几何着色器,它允许把所有面在一个过程渲染,可以使用glFramebufferTexture直接把立方体贴图附加成帧缓冲的深度附件:
在这里插入图片描述
万向阴影贴图有两个渲染阶段:首先生成深度贴图,然后正常使用深度贴图渲染,在场景中创建阴影。帧缓冲对象和立方体贴图的处理看起是这样的:
在这里插入图片描述
这个过程和默认的阴影映射一样,尽管这次渲染和使用的是一个立方体贴图深度纹理,而不是2D深度纹理。在实际开始从光的视角的所有方向渲染场景之前,先得计算出合适的变换矩阵。

光空间的变换

设置了帧缓冲和立方体贴图,需要一些方法来讲场景的所有几何体变换到6个光的方向中相应的光空间。与阴影映射类似,将需要一个光空间的变换矩阵T,但是这次是每个面都有一个。
每个光空间的变换矩阵包含投影和视图矩阵。对于投影矩阵来说,将使用一个透视投影矩阵;光源代表一个空间中的点,所以透视投影矩阵更有意义。每个光空间变换矩阵使用同样投影矩阵:
在这里插入图片描述
非常重要的一点是,这里glm::perspective的视野参数,设置为90度。90度才能保证视野足够大到可以合适地填满立方体贴图的一个面,立方体贴图的所有面都能与其他面在边缘对齐。
在这里插入图片描述
因为投影矩阵在每个方向上并不会改变,可以在6个变换矩阵中重复使用。要为每个方向提供一个不同的视图矩阵。用glm::lookAt创建6个观察方向,每个都按顺序注视着立方体贴图的
在这里插入图片描述
这里创建了6个视图矩阵,把它们乘以投影矩阵,来得到6个不同的光空间变换矩阵。glm::lookAt的target参数是它注视的立方体贴图的面的一个方向。

几何着色器

为了把值渲染到深度立方体贴图,将需要3个着色器:顶点和片段着色器,以及一个它们之间的几何着色器。
几何着色器是负责将所有世界空间的顶点变换到6个不同的光空间的着色器。因此顶点着色器简单地将顶点变换到世界空间,然后直接发送到几何着色器:
在这里插入图片描述
紧接着几何着色器以3个三角形的顶点作为输入,它还有一个光空间变换矩阵的uniform数组。几何着色器接下来会负责将顶点变换到光空间。
几何着色器有一个内建变量叫做gl_Layer,它指定发散出基本图形送到立方体贴图的哪个面。当不管它时,几何着色器就会像往常一样把它的基本图形发送到输送管道的下一阶段,但当更新这个变量就能控制每个基本图形将渲染到立方体贴图的哪一个面。当然这只有当有了一个附加到激活的帧缓冲的立方体贴图纹理才有效:
在这里插入图片描述
几何着色器相对简单。输入一个三角形,输出总共6个三角形(6 * 3顶点,所以总共18个顶点)。在main函数中,遍历立方体贴图的6个面,每个面指定为一个输出面,把这个面的interger(整数)存到gl_Layer。然后,通过把面的光空间变换矩阵乘以FragPos,将每个世界空间顶点变换到相关的光空间,生成每个三角形。注意,还要将最后的FragPos变量发送给片段着色器,需要计算一个深度值。
上一小节使用的是一个空的片段着色器,让OpenGL配置深度贴图的深度值。这次将计算自己的深度,这个深度就是每个fragment位置和光源位置之间的线性距离。计算自己的深度值使得之后的阴影计算更加直观。
在这里插入图片描述
片段着色器将来自几何着色器的FragPos、光的位置向量和视锥的远平面值作为输入。这里把fragment和光源之间的距离,映射到0到1的范围,把它写入为fragment的深度值。
使用这些着色器渲染场景,立方体贴图附加的帧缓冲对象激活以后,会得到一个完全填充的深度立方体贴图,以便于进行第二阶段的阴影计算。

万向阴影贴图

接着来渲染万向阴影(Omnidirectional Shadow)。这个过程和定向光阴影映射相似,尽管这次绑定的深度贴图是一个立方体贴图,而不是2D纹理,并且将光的投影的远平面发送给了着色器。
在这里插入图片描述
这里的renderScene函数在一个大立方体房间中渲染一些立方体,它们散落在大立方体各处,光源在场景中央。
在这里插入图片描述
顶点着色器和片段着色器和原来的阴影映射着色器大部分都一样:不同之处是在光空间中片段着色器不再需要一个fragment位置,现在可以使用一个方向向量采样深度值。

因为这个顶点着色器不再需要将他的位置向量变换到光空间,所以可以去掉FragPosLightSpace变量:
在这里插入图片描述
片段着色器的Blinn - Phong光照代码和之前阴影相乘的结尾部分一样:
在这里插入图片描述
在这里插入图片描述
有一些细微的不同:光照代码一样,但现在有了一个uniform变量samplerCube,shadowCalculation函数用fragment的位置作为它的参数,取代了光空间的fragment位置。
现在还要引入光的视锥的远平面值,后面会需要它。像素着色器的最后,计算出阴影元素,当fragment在阴影中时它是1.0,不在阴影中时是0.0。使用计算出来的阴影元素去影响光照的diffuse和specular元素。
在ShadowCalculation()中有很多不同之处,现在是从立方体贴图中进行采样,不再使用2D纹理了。来一步一步的讨论一下的它的内容。
需要做的第一件事是获取立方体贴图的深度。已经将深度储存为fragment和光位置之间的距离了;这里采用相似的处理方式:
在这里插入图片描述
在这里,得到了fragment的位置与光的位置之间的不同的向量,使用这个向量作为一个方向向量去对立方体贴图进行采样。方向向量不需要是单位向量,所以无需对它进行标准化。最后的closestDepth是光源和它最接近的可见fragment之间的标准化的深度值。
closestDepth值现在在0到1的范围内了,所以先将其转换会0到far_plane的范围,这需要把他乘以far_plane:

closestDepth *= far_plane;

下一步获取当前fragment和光源之间的深度值,可以简单的使用fragToLight的长度来获取它,这取决于如何计算立方体贴图中的深度值:

float currentDepth = length(fragToLight);

返回的是和closestDepth范围相同的深度值。
现在可以将两个深度值对比一下,看看哪一个更接近,以此决定当前的fragment是否在阴影当中。还要包含一个阴影偏移,所以才能避免阴影失真,这在前面教程中已经讨论过了。

float bias = 0.05;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;

完整的ShadowCalculation现在变成了这样:
在这里插入图片描述
有了这些着色器,已经能得到非常好的阴影效果了,这次从一个点光源所有周围方向上都有阴影。有一个位于场景中心的点光源,看起来会像这样:
在这里插入图片描述
由于万向阴影贴图基于传统阴影映射的原则,它便也继承了由解析度产生的非真实感。如果放大就会看到锯齿边了。
PCF或称Percentage - closer filtering允许通过对fragment位置周围过滤多个样本,并对结果平均化。
如果用和前面小节同样的那个简单的PCF过滤器,并加入第三个维度,就是这样的:
在这里插入图片描述
这段代码和传统的阴影映射没有多少不同。这里根据样本的数量动态计算了纹理偏移量,在三个轴向采样三次,最后对子样本进行平均化。
现在阴影看起来更加柔和平滑了,由此得到更加真实的效果:
在这里插入图片描述
然而samples设置4.0,每个fragment总共得到64个样本,这太多了!
大多数这些样本都是多余的,它们在原始方向向量近处采样,不如在采样方向向量的垂直方向进行采样更有意义。可是,没有简单的方式能指出哪一个子方向是多余的,这就难了。有个技巧可以使用,用一个偏移量方向数组,它们差不多都是分开的,每一个指向完全不同的方向,剔除彼此接近的那些子方向。下面就是一个有着20个偏移方向的数组:
在这里插入图片描述
然后把PCF算法与从sampleOffsetDirections得到的样本数量进行适配,使用它们从立方体贴图里采样。这么做的好处是与之前的PCF算法相比,需要的样本数量变少了。
在这里插入图片描述
另一个在这里可以应用的有意思的技巧是,可以基于观察者里一个fragment的距离来改变diskRadius;这样就能根据观察者的距离来增加偏移半径了,当距离更远的时候阴影更柔和,更近了就更锐利。

float diskRadius = (1.0 + (viewDistance / far_plane)) / 25.0;

这是柔和的阴影效果:
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值