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),它调用所有相关的绘制函数,并在需要的地方设置相应的模型矩阵。
将深度贴图渲染到四边形上的片段着色器:
在这里插入图片描述

渲染阴影

正确生成深度贴图后,可以开始生成阴影。这段代码在像素着色器中执行,用来检验一个片元是否在阴影之中,不过在顶点着色器中进行光空间的变换:
在这里插入图片描述
上面的代码中,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()如下所示:
在这里插入图片描述
激活着色器,绑定合适的纹理,激活第二个渲染阶段默认的投影以及视图矩阵,结果如下图所示,可以看到地板上有立方体的阴影:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/weixin_42050609/article/details/125194654