Shadow Mapping (2)

openGL高级光照部分目录 见 openGL高级光照部分目录

改进阴影贴图

我们设法使阴影映射的基础工作,但正如你所能做到的那样,由于与阴影映射相关的一些(清晰可见的)工件,我们还需要修复。我们将在下一节集中讨论修复这些工件。

暗疮

很明显,与之前的图像相比有些问题。更近距离的变焦显示了一个非常明显的云纹图案:

我们可以看到地板四边形的大部分以交替的方式呈现出明显的黑色线条。此阴影映射伪影称为阴影痤疮,可以通过以下图像进行解释:

由于多个阴影碎片距离贴图的深度相对较远,因此它们与贴图的距离相对较远。该图显示了每个黄色倾斜面板代表深度贴图的一个单独的纹理像素。如您所见,多个碎片采样相同深度的样本。

虽然这通常是可以的,但当光源以一个角度观察曲面时,就会出现问题,因为在这种情况下,深度贴图也是从一个角度进行渲染的。几个碎片然后访问相同的倾斜深度texel,而一些在地板上,一些在地板下面;我们得到一个阴影差异。因此,一些碎片被认为是在阴影中,而另一些则不是,从而从图像中得到条纹图案。

我们可以用一个叫做阴影偏移的小技巧来解决这个问题,我们只需将曲面(或阴影贴图)的深度偏移一小部分,这样就不会错误地将碎片放在曲面之下。

应用“偏移”(bias)后,所有采样的深度都小于曲面的深度,因此整个曲面将正确照亮,没有任何阴影。我们可以实施如下偏差:

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

“阴影偏移”为0.005在很大程度上解决了场景的问题,但可以想象,“偏移”值高度依赖于光源和曲面之间的角度。如果表面将有一个陡峭的角度光源,阴影可能仍然显示阴影痤疮。更可靠的方法是根据朝向灯光的曲面角度更改偏移量:我们可以使用点积来解决:

float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);

在这里,基于曲面的法线和灯光方向,我们的最大偏移为0.05,最小为0.005。这样,像地板这样几乎垂直于光源的曲面将获得较小的偏移,而像立方体侧面这样的曲面将获得更大的偏移。下图显示了相同的场景,但现在具有阴影偏移:

选择正确的偏移值需要一些调整,因为这将是不同的场景,但大多数时候,这只是一个问题,慢慢增加bias,直到所有人工痕迹都消除。

悬浮

使用阴影偏移的缺点是将偏移应用于对象的实际深度。因此,偏移可能变得足够大,可以看到与实际对象位置相比的阴影的可见偏移,如下所示(使用放大的偏移值):

这个影子被称为悬浮,因为物体似乎有点脱离他们的影子。我们可以使用一个小技巧来解决大多数悬浮问题,在渲染深度贴图时使用前面的剔除。您可能记得在“面剔除”一章中,OpenGL默认情况下会剔除背面。通过告诉OpenGL,我们希望在阴影贴图阶段剔除前面的面,我们正在改变顺序。

因为我们只需要深度贴图的深度值,所以对于实体对象来说,无论是获取它们的正面深度还是背面深度,都不重要。使用它们的背面深度不会产生错误的结果,因为我们在对象内部是否有阴影并不重要;我们无论如何都看不到。

为了修复peter平移,我们在阴影贴图生成过程中剔除所有正面。请注意,您需要首先启用GL_CULL_FACE。

glCullFace(GL_FRONT);
RenderSceneToDepthMap();
glCullFace(GL_BACK); // don't forget to reset original culling face

这有效地解决了peter平移问题,但仅适用于内部实际上没有开口的实体对象。例如,在我们的场景中,这对立方体非常有效。但是,在地板上,它不会像剔除前面那样有效,完全将地板从等式中移除。地板是一个单一的平面,因此将被完全剔除。如果有人想用这个技巧来解决悬浮的问题,就必须注意只在有意义的地方剔除物体的正面。

另一个要考虑的是,靠近阴影接收器的对象(如远处的立方体)可能仍会给出错误的结果。但是,使用正常的偏移值,通常可以避免悬浮

过采样

另一个视觉上的差异,你可能喜欢或不喜欢的是,光的可见截头之外的区域被认为是在阴影中,而它们(通常)不是。发生这种情况是因为灯光的平截头外的投影坐标高于1.0,因此将在默认范围[0,1]之外对深度纹理进行采样。基于纹理的包裹方法,我们将得到不正确的深度结果,而不是基于光源的真实深度值。

你可以在图像中看到,有某种虚构的光区域,而这个区域之外的很大一部分处于阴影中;这个区域表示投影到地板上的深度贴图的大小。发生这种情况的原因是,我们先前将深度贴图的包装选项设置为GL_REPEAT。

我们希望深度贴图范围之外的所有坐标的深度为1.0,这意味着这些坐标永远不会处于阴影中(因为没有对象的深度将大于1.0)。我们可以通过配置纹理边界颜色并将“深度贴图”的“纹理包裹”选项设置为GL_CLAMP_TO_BORDER来完成此操作:

glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
float borderColor[] = { 1.0f, 1.0f, 1.0f, 1.0f };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

现在,每当我们在深度贴图的[0,1]坐标范围之外采样时,纹理函数将始终返回1.0的深度,生成0.0的阴影值。现在的结果看起来更合理:

似乎仍有一部分显示了一个黑暗的区域。这些是光的正截头远平面外的坐标。通过观察阴影方向,可以看到这个黑暗区域总是出现在光源的截头台的远端

当一个点比光的远平面还要远时,灯光的z坐标大于1.0。在这种情况下,当我们将坐标的z分量与深度贴图值进行比较时,GL_CLAMP_TO_BORDER 环绕方法不再工作;对于大于1.0的z,这始终返回true。

修复此问题也相对容易,因为只要投影向量的z坐标大于1.0,我们就将阴影值强制为0.0:

float ShadowCalculation(vec4 fragPosLightSpace)
{
    [...]
    if(projCoords.z > 1.0)
        shadow = 0.0;

    return shadow;
}

检查远平面并将深度贴图钳制为手动指定的边界颜色可以解决深度贴图的过采样问题。这最终给了我们想要的结果:

所有这些的结果意味着我们只有投影碎片坐标位于深度贴图范围内的阴影,因此光截锥之外的任何东西都不会有可见的阴影。由于游戏通常会确保这种情况只发生在远处,这是一个比我们以前明显的黑色区域更可信的效果。

PCF

现在的阴影是对风景的一个很好的补充,但它仍然不是我们想要的。如果要放大阴影,阴影贴图的分辨率依赖性将很快变得明显。

因为深度贴图具有固定的分辨率,所以深度通常跨越每个纹理的多个片段。因此,多个片段从深度贴图中采样相同的深度值,并得出相同的阴影结论,从而产生这些锯齿状的块状边缘。

您可以通过增加深度贴图分辨率,或尝试使光截锥尽可能接近场景来减少这些块状阴影。

另一种(部分)解决这些锯齿状边缘的方法称为PCF,或称为百分比闭合过滤(percentage closer filtering),它包含许多不同的过滤功能,这些功能可以产生更柔和的阴影,使阴影看起来不那么块状或坚硬。这个想法是从深度贴图中多次采样,每次都使用稍微不同的纹理坐标。对于每个单独的样本,我们检查它是否在阴影中。所有的子结果,然后合并和平均,我们得到一个漂亮的软外观阴影。

PCF的一个简单实现是简单地对深度贴图的周围纹理进行采样并平均结果:

float shadow = 0.0;
vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
for(int x = -1; x <= 1; ++x)
{
    for(int y = -1; y <= 1; ++y)
    {
        float pcfDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r; 
        shadow += currentDepth - bias > pcfDepth ? 1.0 : 0.0;        
    }    
}
shadow /= 9.0;

这里,textureSize返回mipmap级别0处给定采样器纹理的宽度和高度vec2。1除以此值返回单个纹理的大小,我们使用它来偏移纹理坐标,以确保每个新样本采样一个不同的深度值。在这里,我们在投影坐标的x和y值周围采样9个值,测试阴影遮挡,最后通过采样总数平均结果。

通过使用更多采样和/或更改texelSize变量,可以提高软阴影的质量。下面您可以看到应用了简单PCF的阴影:

从远处看,阴影看起来好多了,也不那么坚硬。如果放大,您仍然可以看到阴影映射的分辨率瑕疵,但一般来说,这对于大多数应用程序都是很好的结果。

您可以在这里找到示例的完整源代码。

实际上有更多的PCF和相当多的技术来显著提高软阴影的质量,但为了本章的篇幅,我们将留待以后讨论。

正交与投影

使用正交矩阵或投影矩阵渲染深度贴图是有区别的。正交投影矩阵不会在透视时使场景变形,因此所有视图/光线都是平行的。这使得它成为平行光的一个很好的投影矩阵。然而,透视投影矩阵会基于透视对所有顶点进行变形,从而产生不同的结果。下图显示了两种投影方法的不同阴影区域:

与平行光不同,透视投影对于具有实际位置的光源最有意义。透视投影最常用于聚光灯和点光源,而正交投影用于平行光。

使用透视投影矩阵的另一个细微差别是,可视化深度缓冲区通常会产生几乎完全白色的结果。之所以会发生这种情况,是因为使用透视投影时,深度会转化为非线性深度值,而其大部分可见范围都靠近近平面(意思是,在变换的时候,因为透视投影是非线性的,所以深度缓冲区里都比较接近1,所以显示的特别白,看不出区别来)。为了能够正确地查看深度值,就像我们对正交投影所做的那样,您首先需要将非线性深度值转换为线性,正如我们在深度测试一章中所讨论的:

#version 330 core
out vec4 FragColor;
  
in vec2 TexCoords;

uniform sampler2D depthMap;
uniform float near_plane;
uniform float far_plane;

float LinearizeDepth(float depth)
{
    float z = depth * 2.0 - 1.0; // Back to NDC 
    return (2.0 * near_plane * far_plane) / (far_plane + near_plane - z * (far_plane - near_plane));
}

void main()
{             
    float depthValue = texture(depthMap, TexCoords).r;
    FragColor = vec4(vec3(LinearizeDepth(depthValue) / far_plane), 1.0); // perspective
    // FragColor = vec4(vec3(depthValue), 1.0); // orthographic
}

这显示了与我们在正交投影中看到的类似的深度值。请注意,这只对调试有用(就是说调试显示的时候线性输出,否则在其他计算中不要这样做);对于正交矩阵或投影矩阵,深度检查保持不变,因为相对深度不变。

猜你喜欢

转载自blog.csdn.net/tiao_god/article/details/107288366