OpenGL核心技术之如何改进Shadow Mapping技术

发表于2017-04-07
评论1 2k浏览

在上一个博客中介绍了Shadow Mapping技术,但是正如你所看到的哦,Shadow Mapping还是有点不真实,本篇文章开始介绍如何改进Shadow Mapping技术。在上篇文章中渲染的阴影有所失真,正如如下图片所示的:


我们可以看到地板四边形渲染出很大一块交替黑线。这种阴影贴图的不真实感叫做阴影失真(Shadow Acne),下图解释了成因:


因为阴影贴图受限于解析度,在距离光源比较远的情况下,多个片元可能从深度贴图的同一个值中去采样。图片每个斜坡代表深度贴图一个单独的纹理像素。你可以看到,多个片元从同一个深度值进行采样。

虽然很多时候没问题,但是当光源以一个角度朝向表面的时候就会出问题,这种情况下深度贴图也是从一个角度下进行渲染的。多个片元就会从同一个斜坡的深度纹理像素中采样,有些在地板上面,有些在地板下面;这样我们所得到的阴影就有了差异。因为这个,有些片元被认为是在阴影之中,有些不在,由此产生了图片中的条纹样式。

我们可以用一个叫做阴影偏移(shadow bias)的技巧来解决这个问题,我们简单的对表面的深度(或深度贴图)应用一个偏移量,这样片元就不会被错误地认为在表面之下了。

使用了偏移量后,所有采样点都获得了比表面深度更小的深度值,这样整个表面就正确地被照亮,没有任何阴影。我们可以这样实现这个偏移:

[cpp] view plain copy
 
  1. float bias = 0.005;  
  2. float shadow = currentDepth - bias > closestDepth  ? 1.0 : 0.0;  
一个0.005的偏移就能帮到很大的忙,但是有些表面坡度很大,仍然会产生阴影失真。有一个更加可靠的办法能够根据表面朝向光线的角度更改偏移量:使用点乘:

[cpp] view plain copy
 
  1. float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);  
这里我们有一个偏移量的最大值0.05,和一个最小值0.005,它们是基于表面法线和光照方向的。这样像地板这样的表面几乎与光源垂直,得到的偏移就很小,而比如立方体的侧面这种表面得到的偏移就更大。下图展示了同一个场景,但使用了阴影偏移,效果的确更好:

选用正确的偏移数值,在不同的场景中需要一些像这样的轻微调校,但大多情况下,实际上就是增加偏移量直到所有失真都被移除的问题。

使用阴影偏移的一个缺点是你对物体的实际深度应用了平移。偏移有可能足够大,以至于可以看出阴影相对实际物体位置的偏移,你可以从下图看到这个现象(这是一个夸张的偏移值):


这个阴影失真叫做悬浮(Peter Panning),因为物体看起来轻轻悬浮在表面之上(译注Peter Pan就是童话彼得潘,而panning有平移、悬浮之意,而且彼得潘是个会飞的男孩…)。我们可以使用一个叫技巧解决大部分的Peter panning问题:当渲染深度贴图时候使用正面剔除(front face culling)你也许记得在面剔除博文中OpenGL默认是背面剔除。我们要告诉OpenGL我们要剔除正面。


为了修复peter游移,我们要进行正面剔除,先必须开启GL_CULL_FACE:

[cpp] view plain copy
 
  1. glCullFace(GL_FRONT);  
  2. RenderSceneToDepthMap();  
  3. glCullFace(GL_BACK); // 不要忘记设回原先的culling face  

这十分有效地解决了peter panning的问题,但只针对实体物体,内部不会对外开口。我们的场景中,在立方体上工作的很好,但在地板上无效,因为正面剔除完全移除了地板。地面是一个单独的平面,不会被完全剔除。如果有人打算使用这个技巧解决peter panning必须考虑到只有剔除物体的正面才有意义。

另一个要考虑到的地方是接近阴影的物体仍然会出现不正确的效果。必须考虑到何时使用正面剔除对物体才有意义。不过使用普通的偏移值通常就能避免peter panning。

无论你喜不喜欢还有一个视觉差异,就是光的视锥不可见的区域一律被认为是处于阴影中,不管它真的处于阴影之中。出现这个状况是因为超出光的视锥的投影坐标比1.0大,这样采样的深度纹理就会超出他默认的0到1的范围。根据纹理环绕方式,我们将会得到不正确的深度结果,它不是基于真实的来自光源的深度值。


你可以在图中看到,光照有一个区域,超出该区域就成为了阴影;这个区域实际上代表着深度贴图的大小,这个贴图投影到了地板上。发生这种情况的原因是我们之前将深度贴图的环绕方式设置成了GL_REPEAT。

我们宁可让所有超出深度贴图的坐标的深度范围是1.0,这样超出的坐标将永远不在阴影之中。我们可以储存一个边框颜色,然后把深度贴图的纹理环绕选项设置为GL_CLAMP_TO_BORDER:

[cpp] view plain copy
 
  1. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);  
  2. glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);  
  3. GLfloat borderColor[] = { 1.0, 1.0, 1.0, 1.0 };  
  4. glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);  
现在如果我们采样深度贴图0到1坐标范围以外的区域,纹理函数总会返回一个1.0的深度值,阴影值为0.0。结果看起来会更真实:

仍有一部分是黑暗区域。那里的坐标超出了光的正交视锥的远平面。你可以看到这片黑色区域总是出现在光源视锥的极远处。

当一个点比光的远平面还要远时,它的投影坐标的z坐标大于1.0。这种情况下,GL_CLAMP_TO_BORDER环绕方式不起作用,因为我们把坐标的z元素和深度贴图的值进行了对比;它总是为大于1.0的z返回true。

解决这个问题也很简单,我们简单的强制把shadow的值设为0.0,不管投影向量的z坐标是否大于1.0:

[cpp] view plain copy
 
  1. float ShadowCalculation(vec4 fragPosLightSpace)  
  2. {  
  3.     [...]  
  4.     if(projCoords.z > 1.0)  
  5.         shadow = 0.0;  
  6.   
  7.     return shadow;  
  8. }  

检查远平面,并将深度贴图限制为一个手工指定的边界颜色,就能解决深度贴图采样超出的问题,我们最终会得到下面我们所追求的效果:


这些结果意味着,只有在深度贴图范围以内的被投影的fragment坐标才有阴影,所以任何超出范围的都将会没有阴影。由于在游戏中通常这只发生在远处,就会比我们之前的那个明显的黑色区域效果更真实。

阴影现在已经附着到场景中了,不过这仍不是我们想要的。如果你放大看阴影,阴影映射对解析度的依赖很快变得很明显。


因为深度贴图有一个固定的解析度,多个片元对应于一个纹理像素。结果就是多个片元会从深度贴图的同一个深度值进行采样,这几个片元便得到的是同一个阴影,这就会产生锯齿边。

你可以通过增加深度贴图解析度的方式来降低锯齿块,也可以尝试尽可能的让光的视锥接近场景。

另一个(并不完整的)解决方案叫做PCF(percentage-closer filtering),这是一种多个不同过滤方式的组合,它产生柔和阴影,使它们出现更少的锯齿块和硬边。核心思想是从深度贴图中多次采样,每一次采样的纹理坐标都稍有不同。每个独立的样本可能在也可能不再阴影中。所有的次生结果接着结合在一起,进行平均化,我们就得到了柔和阴影。

一个简单的PCF的实现是简单的从纹理像素四周对深度贴图采样,然后把结果平均起来:

[cpp] view plain copy
 
  1. float shadow = 0.0;  
  2. vec2 texelSize = 1.0 / textureSize(shadowMap, 0);  
  3. for(int x = -1; x <= 1; ++x)  
  4. {  
  5.     for(int y = -1; y <= 1; ++y)  
  6.     {  
  7.         float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;   
  8.         shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;          
  9.     }      
  10. }  
  11. shadow /= 9.0;  

这个textureSize返回一个给定采样器纹理的0级mipmap的vec2类型的宽和高。用1除以它返回一个单独纹理像素的大小,我们用以对纹理坐标进行偏移,确保每个新样本,来自不同的深度值。这里我们采样得到9个值,它们在投影坐标的x和y值的周围,为阴影阻挡进行测试,并最终通过样本的总数目将结果平均化。

使用更多的样本,更改texelSize变量,你就可以增加阴影的柔和程度。下面你可以看到应用了PCF的阴影:

从稍微远一点的距离看去,阴影效果好多了,也不那么生硬了。如果你放大,仍会看到阴影贴图解析度的不真实感,但通常对于大多数应用来说效果已经很好了。

下面将PCF的Shader代码展示如下所示,首先展示的是顶点着色器:

[cpp] view plain copy
 
  1. #version 330 core  
  2. layout (location = 0) in vec3 position;  
  3. layout (location = 1) in vec3 normal;  
  4. layout (location = 2) in vec2 texCoords;  
  5.   
  6. out VS_OUT {  
  7.     vec3 FragPos;  
  8.     vec3 Normal;  
  9.     vec2 TexCoords;  
  10.     vec4 FragPosLightSpace;  
  11. } vs_out;  
  12.   
  13. uniform mat4 projection;  
  14. uniform mat4 view;  
  15. uniform mat4 model;  
  16. uniform mat4 lightSpaceMatrix;  
  17.   
  18. void main()  
  19. {  
  20.     gl_Position = projection * view * model * vec4(position, 1.0f);  
  21.     vs_out.FragPos = vec3(model * vec4(position, 1.0));  
  22.     vs_out.Normal = transpose(inverse(mat3(model))) * normal;  
  23.     vs_out.TexCoords = texCoords;  
  24.     vs_out.FragPosLightSpace = lightSpaceMatrix * vec4(vs_out.FragPos, 1.0);  
  25. }  

片段着色器代码如下所示:

[cpp] view plain copy
 
  1. #version 330 core  
  2. out vec4 FragColor;  
  3.   
  4. in VS_OUT {  
  5.     vec3 FragPos;  
  6.     vec3 Normal;  
  7.     vec2 TexCoords;  
  8.     vec4 FragPosLightSpace;  
  9. } fs_in;  
  10.   
  11. uniform sampler2D diffuseTexture;  
  12. uniform sampler2D shadowMap;  
  13.   
  14. uniform vec3 lightPos;  
  15. uniform vec3 viewPos;  
  16.   
  17. uniform bool shadows;  
  18.   
  19. float ShadowCalculation(vec4 fragPosLightSpace)  
  20. {  
  21.     // perform perspective divide  
  22.     vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;  
  23.     // Transform to [0,1] range  
  24.     projCoords = projCoords * 0.5 + 0.5;  
  25.     // Get closest depth value from light's perspective (using [0,1] range fragPosLight as coords)  
  26.     float closestDepth = texture(shadowMap, projCoords.xy).r;   
  27.     // Get depth of current fragment from light's perspective  
  28.     float currentDepth = projCoords.z;  
  29.     // Calculate bias (based on depth map resolution and slope)  
  30.     vec3 normal = normalize(fs_in.Normal);  
  31.     vec3 lightDir = normalize(lightPos - fs_in.FragPos);  
  32.     float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);  
  33.     // Check whether current frag pos is in shadow  
  34.     // float shadow = currentDepth - bias > closestDepth  ? 1.0 : 0.0;  
  35.     // PCF  
  36.     float shadow = 0.0;  
  37.     vec2 texelSize = 1.0 / textureSize(shadowMap, 0);  
  38.     for(int x = -1; x <= 1; ++x)  
  39.     {  
  40.         for(int y = -1; y <= 1; ++y)  
  41.         {  
  42.             float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r;   
  43.             shadow += currentDepth - bias > pcfDepth  ? 1.0 : 0.0;          
  44.         }      
  45.     }  
  46.     shadow /= 9.0;  
  47.       
  48.     // Keep the shadow at 0.0 when outside the far_plane region of the light's frustum.  
  49.     if(projCoords.z > 1.0)  
  50.         shadow = 0.0;  
  51.           
  52.     return shadow;  
  53. }  
  54.   
  55. void main()  
  56. {             
  57.     vec3 color = texture(diffuseTexture, fs_in.TexCoords).rgb;  
  58.     vec3 normal = normalize(fs_in.Normal);  
  59.     vec3 lightColor = vec3(0.4);  
  60.     // Ambient  
  61.     vec3 ambient = 0.2 * color;  
  62.     // Diffuse  
  63.     vec3 lightDir = normalize(lightPos - fs_in.FragPos);  
  64.     float diff = max(dot(lightDir, normal), 0.0);  
  65.     vec3 diffuse = diff * lightColor;  
  66.     // Specular  
  67.     vec3 viewDir = normalize(viewPos - fs_in.FragPos);  
  68.     float spec = 0.0;  
  69.     vec3 halfwayDir = normalize(lightDir + viewDir);    
  70.     spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);  
  71.     vec3 specular = spec * lightColor;      
  72.     // Calculate shadow  
  73.     float shadow = shadows ? ShadowCalculation(fs_in.FragPosLightSpace) : 0.0;                        
  74.     shadow = min(shadow, 0.75); // reduce shadow strength a little: allow some diffuse/specular light in shadowed regions  
  75.     vec3 lighting = (ambient + (1.0 - shadow) * (diffuse + specular)) * color;      
  76.       
  77.     FragColor = vec4(lighting, 1.0f);  
  78. }  
实际上PCF还有更多的内容,以及很多技术要点需要考虑以提升柔和阴影的效果,但处于本章内容长度考虑,我们将留在以后讨论。

如社区发表内容存在侵权行为,您可以点击这里查看侵权投诉指引