阴影映射+阴影贴图
一: 阴影映射
- 阴影是光线被阻挡的结果;当一个光源的光线由于其他物体的阻挡不能够达到一个物体的表面的时候,那么这个物体就在阴影中了。阴影能够使场景看起来真实得多,并且可以让观察者获得物体之间的空间位置关系。
- 阴影还是比较不好实现的,因为当前实时渲染领域还没找到一种完美的阴影算法。
- 阴影贴图(shadow mapping),效果不错,而且相对容易实现。阴影贴图并不难以理解,性能也不会太低,而且非常容易扩展成更高级的算法(比如 Omnidirectional Shadow Maps和 Cascaded Shadow Maps)。
- 阴影映射(Shadow Mapping)背后的思路非常简单:我们以光的位置为视角进行渲染,我们能看到的东西都将被点亮,看不见的一定是在阴影之中了。假设有一个地板,在光源和它之间有一个大盒子。由于光源处向光线方向看去,可以看到这个盒子,但看不到地板的一部分,这部分就应该在阴影中了。
- 在深度测试教程中,在深度缓冲里的一个值是摄像机视角下,对应于一个片元的一个0到1之间的深度值。
- 我们从光源的透视图来渲染场景,并把深度值的结果储存到纹理中,就能对光源的透视图所见的最近的深度值进行采样。最终,深度值就会显示从光源的透视图下见到的第一个片元了。我们管储存在纹理中的所有这些深度值,叫做深度贴图(depth map)或阴影贴图。
- 通过储存到深度贴图中的深度值,我们就能找到最近点,用以决定片元是否在阴影中。我们使用一个来自光源的视图和投影矩阵来渲染场景就能创建一个深度贴图。这个投影和视图矩阵结合在一起成为一个T变换,它可以将任何三维位置转变到光源的可见坐标空间。
- 深度映射由两个步骤组成:首先,我们渲染深度贴图,然后我们像往常一样渲染场景,使用生成的深度贴图来计算片元是否在阴影之中。
二: 深度贴图
- 深度贴图是从光的透视图里渲染的深度纹理,用它计算阴影。因为我们需要将场景的渲染结果储存到一个纹理中,我们将再次需要帧缓冲。
- 首先,我们要为渲染的深度贴图创建一个帧缓冲对象。然后,创建一个2D纹理,提供给帧缓冲的深度缓冲使用。
- 生成深度贴图不太复杂。因为我们只关心深度值,我们要把纹理格式指定为GL_DEPTH_COMPONENT。我们还要把纹理的高宽设置为1024:这是深度贴图的解析度。
- 把生成的深度纹理作为帧缓冲的深度缓冲。我们需要的只是在从光的透视图下渲染场景的时候深度信息,所以颜色缓冲没有用。然而帧缓冲对象不是完全不包含颜色缓冲的,所以我们需要显式告诉OpenGL我们不适用任何颜色数据进行渲染。我们通过将调用glDrawBuffer和glReadBuffer把读和绘制缓冲设置为GL_NONE来做这件事。
// 1. 首选渲染深度贴图 glViewport(0, 0, SHADOW_WIDTH, SHADOW_HEIGHT); glBindFramebuffer(GL_FRAMEBUFFER, depthMapFBO); glClear(GL_DEPTH_BUFFER_BIT); ConfigureShaderAndMatrices(); RenderScene(); glBindFramebuffer(GL_FRAMEBUFFER, 0); // 2. 像往常一样渲染场景,但这次使用深度贴图 glViewport(0, 0, SCR_WIDTH, SCR_HEIGHT); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); ConfigureShaderAndMatrices(); glBindTexture(GL_TEXTURE_2D, depthMap); RenderScene();
- 这里一定要记得调用glViewport。因为阴影贴图经常和我们原来渲染的场景(通常是窗口解析度)有着不同的解析度,我们需要改变视口(viewport)的参数以适应阴影贴图的尺寸。如果我们忘了更新视口参数,最后的深度贴图要么太小要么就不完整。
- ConfigureShaderAndMatrices函数是用来在第二个步骤确保为每个物体设置了合适的投影和视图矩阵,以及相关的模型矩阵的。
- 因为我们使用的是一个所有光线都平行的定向光。出于这个原因,我们将为光源使用正交投影矩阵,透视图将没有任何变形:
- 为了创建一个视图矩阵来变换每个物体,把它们变换到从光源视角可见的空间中,我们将使用glm::lookAt函数;这次从光源的位置看向场景中央。
三: 渲染至深度贴图
- 当我们以光的透视图进行场景渲染的时候,我们会用一个比较简单的着色器,这个着色器除了把顶点变换到光空间以外,不会做得更多了。这个简单的着色器叫做simpleDepthShader。
- 这个顶点着色器将一个单独模型的一个顶点,使用lightSpaceMatrix变换到光空间中。
- 由于我们没有颜色缓冲,最后的片元不需要任何处理,所以我们可以简单地使用一个空像素着色器:
- RenderScene函数的参数是一个着色器程序(shader program),它调用所有相关的绘制函数,并在需要的地方设置相应的模型矩阵。
- 最后,在光的透视图视角下,很完美地用每个可见片元的最近深度填充了深度缓冲。通过将这个纹理投射到一个2D四边形上(和我们在帧缓冲一节做的后处理过程类似),就能在屏幕上显示出来
四: 渲染阴影
- 正确地生成深度贴图以后我们就可以开始生成阴影了。这段代码在像素着色器中执行,用来检验一个片元是否在阴影之中,不过我们在顶点着色器中进行光空间的变换。
- 顶点着色器传递一个普通的经变换的世界空间顶点位置vs_out.FragPos和一个光空间的vs_out.FragPosLightSpace给像素着色器。
- 像素着色器使用Blinn-Phong光照模型渲染场景。我们接着计算出一个shadow值,当fragment在阴影中时是1.0,在阴影外是0.0。然后,diffuse和specular颜色会乘以这个阴影元素。由于阴影不会是全黑的(由于散射),我们把ambient分量从乘法中剔除。
- 像素着色器的最后,我们我们把diffuse和specular乘以(1-阴影元素),这表示这个片元有多大成分不在阴影中。这个像素着色器还需要两个额外输入,一个是光空间的片元位置和第一个渲染阶段得到的深度贴图。
- 首先要检查一个片元是否在阴影中,把光空间片元位置转换为裁切空间的标准化设备坐标。当我们在顶点着色器输出一个裁切空间顶点位置到gl_Position时,OpenGL自动进行一个透视除法,将裁切空间坐标的范围-w到w转为-1到1,这要将x、y、z元素除以向量的w元素来实现。由于裁切空间的FragPosLightSpace并不会通过gl_Position传到像素着色器里,我们必须自己做透视除法
- 因为来自深度贴图的深度在0到1的范围,我们也打算使用projCoords从深度贴图中去采样,所以我们将NDC坐标变换为0到1的范围。从第一个渲染阶段的projCoords坐标直接对应于变换过的NDC坐标。我们将得到光的位置视野下最近的深度。
- 为了得到片元的当前深度,我们简单获取投影向量的z坐标,它等于来自光的透视视角的片元的深度。
float ShadowCalculation(vec4 fragPosLightSpace) { // 执行透视除法 vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w; // 变换到[0,1]的范围 projCoords = projCoords * 0.5 + 0.5; // 取得最近点的深度(使用[0,1]范围下的fragPosLight当坐标) float closestDepth = texture(shadowMap, projCoords.xy).r; // 取得当前片元在光源视角下的深度 float currentDepth = projCoords.z; // 检查当前片元是否在阴影中 float shadow = currentDepth > closestDepth ? 1.0 : 0.0; return shadow; }
五:阴影贴图的改进
- 阴影失真
1.1 放大看会发现明显的线条样式:我们可以看到地板四边形渲染出很大一块交替黑线。这种阴影贴图的不真实感叫做阴影失真(Shadow Acne)
1.2 多个片元就会从同一个斜坡的深度纹理像素中采样,有些在地板上面,有些在地板下面;这样我们所得到的阴影就有了差异。因为这个,有些片元被认为是在阴影之中,有些不在,由此产生了图片中的条纹样式。
1.3 我们可以用一个叫做阴影偏移(shadow bias)的技巧来解决这个问题,我们简单的对表面的深度(或深度贴图)应用一个偏移量,这样片元就不会被错误地认为在表面之下了
1.4 使用了偏移量后,所有采样点都获得了比表面深度更小的深度值,这样整个表面就正确地被照亮,没有任何阴影。我们可以这样实现这个偏移: - 悬浮
2.1 使用阴影偏移的一个缺点是你对物体的实际深度应用了平移。偏移有可能足够大,以至于可以看出阴影相对实际物体位置的偏移
2.2 这个阴影失真叫做悬浮(Peter Panning),因为物体看起来轻轻悬浮在表面之上。我们可以使用一个叫技巧解决大部分的Peter panning问题:当渲染深度贴图时候使用正面剔除(front face culling)你也许记得在面剔除教程中OpenGL默认是背面剔除。我们要告诉OpenGL我们要剔除正面。
2.3 因为我们只需要深度贴图的深度值,对于实体物体无论我们用它们的正面还是背面都没问题。使用背面深度不会有错误,因为阴影在物体内部有错误我们也看不见。
2.4 另一个要考虑到的地方是接近阴影的物体仍然会出现不正确的效果。必须考虑到何时使用正面剔除对物体才有意义。不过使用普通的偏移值通常就能避免peter panning。 - 采样过多
3.1 还有一个视觉差异,就是光的视锥不可见的区域一律被认为是处于阴影中,不管它真的处于阴影之中。出现这个状况是因为超出光的视锥的投影坐标比1.0大,这样采样的深度纹理就会超出他默认的0到1的范围。根据纹理环绕方式,我们将会得到不正确的深度结果,它不是基于真实的来自光源的深度值。
3.2 在图中看到,光照有一个区域,超出该区域就成为了阴影;这个区域实际上代表着深度贴图的大小,这个贴图投影到了地板上。发生这种情况的原因是我们之前将深度贴图的环绕方式设置成了GL_REPEAT。
3.3 我们可以储存一个边框颜色,然后把深度贴图的纹理环绕选项设置为GL_CLAMP_TO_BORDER:
六:PCF
- PCF(percentage-closer filtering),这是一种多个不同过滤方式的组合,它产生柔和阴影,使它们出现更少的锯齿块和硬边。
- 核心思想是从深度贴图中多次采样,每一次采样的纹理坐标都稍有不同。每个独立的样本可能在也可能不再阴影中。所有的次生结果接着结合在一起,进行平均化,我们就得到了柔和阴影。
- 一个简单的PCF的实现是简单的从纹理像素四周对深度贴图采样,然后把结果平均起来:
Either Excellent or Rusty