阴影贴图

阴影贴图

摊牌

  • 在游戏中实现实时阴影时,阴影贴图是使用较多的一种技术,用这种方式实现的阴影,效果不错、相对容易实现、性能也不会太低,而且很 容易扩展到更高级的算法,比如点光源阴影和CSM

思路

  • 当以光源的位置为视角进行渲染,能看到的东西都将被照亮,看不到的一定是在阴影中

原理

  • 第一步
  • 以光源的位置为视角,进行视图和投影变换,渲染场景,这一步的主要目的是把深度值存储到纹理中,这样的纹理就叫做深度贴图或者阴影贴图,这个技术的名字也就是从这来的
  • 保存视图矩阵和投影矩阵;他们可以将任何一个位置变换到光源的视角空间中,这是这个技术至关重要的一步
  • 之后对这样的深度贴图采样后,得到的深度值就是:从光源的角度下看到的第一个物体的深度值,这也是判断一个物体是否在阴影中的临界深度值
  • 第二步
  • 从摄像机的视角渲染场景,在给每个片段着色前,将物体顶点在世界坐标中的位置变换到光源的视角空间中
  • 变换到光源空间中的顶点(光源空间就是以光源为视角的空间),他们的坐标是光源空间中的裁剪空间坐标;因为世界坐标左乘视图矩阵再左乘投影矩阵就变成了裁剪空间坐标,这里用到的视图矩阵和投影矩阵是光源空间中的,所以得到的就是光源空间中的裁剪空间坐标
  • 然后我们需要得到顶点在光源空间中的实际深度值,然后用这个实际深度值与深度贴图中对应位置的深度值比较,如果顶点的实际深度值大于深度贴图中的深度值,就说明该顶点在光源视角下不是第一个被看到的,所以他就是在阴影中
  • 获取顶点在光源空间中实际深度值的方法
  • 根据世界坐标到屏幕坐标的变换过程可知,对裁剪空间中的坐标进行透视除法,得到[-1.0, 1.0]范围内的值,再将他们变换到[0.0, 1.0]之间,这时坐标的z分量就是深度值
  • 在上面的第二步中,我们将物体顶点的世界坐标变换到了光源视角下的裁剪空间坐标,然后再对他进行透视除法等一系列变换,最终的坐标z值就是该顶点在光源视角下的实际深度值
  • 再多说一句世界坐标到屏幕坐标的变换过程:世界坐标左乘视图矩阵得到视图空间的坐标,视图空间的坐标左乘投影矩阵得到裁剪空间的坐标,对裁剪空间的坐标做透视除法,得到[-1.0, 1.0]之间的标准设备坐标,再将他们乘以0.5加上0.5得到[0.0, 1.0]之间的值,最后乘以视口的宽和高就是屏幕坐标
  • 对深度贴图采样
  • 对深度贴图采样是为了获取从光源看向顶点方向上的第一个物体的深度值;顶点位置是变换到光源空间中的裁剪空间位置,又进行了透视除法和一系列变换,变换到了[0.0, 1.0]之间,把他当作纹理坐标对深度贴图采样,得到的就是从光源看向这个顶点方向上的第一个物体的深度值;如果顶点在光源空间中的实际深度值大于光源看到的第一个物体的深度值,就说明顶点在阴影中

实现

  • 第一步
  • 为深度贴图创建一个帧缓冲对象,因为需要将场景的渲染结果保存到一个纹理中,所以将需要使用帧缓冲
  • 创建一个2D纹理,给帧缓冲的深度缓冲使用,因为这里只关心深度值
  • 把创建的纹理对象绑定到帧缓冲的深度附件上
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, depthMap, 0);
  • 因为我们需要的只是从光源的视角下渲染场景的深度值,所以不需要颜色缓冲。然而,不包含颜色缓冲的帧缓冲对象是不完整的,所以我们需要显示的告诉OpenGL我们不使用任何颜色数据进行渲染,这通过调用glDrawBuffer(GL_NONE)和glReadBuffer(GL_NONE),把读和绘制颜色缓冲设置为空来实现
  • 从光源视角渲染场景用的视图矩阵用glm::lookAt(…)来创建
  • 当使用平行光模拟光源时,投影矩阵使用正交投影矩阵,这可以使物体的投影没有变形
  • 第二步
  • 渲染阴影,首先将帧缓冲绑定到默认帧缓冲上
  • 在顶点着色器中,将顶点的世界坐标输出到片段着色器中,以便计在片段着色器中算顶点是否在阴影中;还要计算顶点在摄像机视角下的坐标变换,用于正常的渲染场景
  • 在片段着色器中,首先将顶点的世界坐标变换到光源空间中,判断顶点是否在阴影中,并用一个int变量nShadow来标记
  • 然后使用Blinn-Phong光照模型,来计算顶点的光照;需要注意的是:漫反射光照和镜面光照会收到阴影的影响,环境光照是个固定的值不受阴影的影响,所以片段最终的颜色应该是这样:
vec3 color = (ambient + (1.0 - nShadow) * (diffuse + specular)) * objColor;
  • 需要注意的地方:
  • 在两次渲染前一定要记得调用glViewport(…),因为阴影贴图经常和原来渲染的场景有不同的分辨率,所以需要通过改变视口的大小以适应阴影贴图的尺寸
  • 如果想把深度贴图渲染出来,当使用的是正交投影时,可以直接显示;当使用透视投影时,需要把非线性的深度值变换成线性后在显示
  • 与上一条同样的道理,对深度贴图采样时,当投影矩阵使用的是正交投影时,可以不用进行透视除法直接采样;当投影使用的是透视投影时,就必须进行透视除法;而我们在实现的时候为了哪种情况都适用,一般都进行了透视除法
  • 渲染效果
    在这里插入图片描述

副作用

阴影失真

  • 现象:接收阴影的面会交替出现黑线
  • 原因:因为阴影贴图受限于分辨率,在距离光源比较远的情况下,多个片段可能从深度贴图的一个值中取采样
  • 使用深度贴图渲染阴影时,虽然在大部分区域没有问题,但当光源在某一个特定的角度时就会出现该问题。在这种情况下,深度贴图也是从那个角度渲染出来的,多个片段就会从同一个深度贴图中采样,造成有些在接收面的上方,有些在接收面的下方,这样我们得到的阴影就有了差异;因此有些片段被认为在阴影中,有些不在阴影中,由此就产生了阴影条纹
  • 解决办法:阴影偏移(shadow bias)
  • 简单的对实际深度值应用一个偏移量,让实际的深度值再小一点,这样片段就不会被误认为在接收面之下了
  • 0.005的偏移量就能帮到很大的忙,但是有些时候还是会有失真。有一个更加可靠的方法是:根据物体表面朝向光源的角度更改偏移量
float bias = max(0.05 * (1.0 - dot(normal, lightDir)), 0.005);\\这样像地面的表面几乎垂直于光源,得到的偏移量就很小;像竖直的侧面得到的偏移量就更大

在这里插入图片描述

悬浮

  • 现象:阴影相对于实体物体的位置有一段间隔
  • 原因:这是使用阴影偏移造成的问题,当偏移量足够大时,就可以看到阴影与实际物体的位置有一段距离;这也是一种失真,这种失真叫做悬浮
  • 解决办法:使用正面剔除。我们在渲染深度贴图时,使用的是物体的正面,如果使用背面,首先对深度贴图的大致结果不会有影响,第二由于背面更接近阴影接收面,会天然的抵消一些误差,而这些误差恰好能减弱悬浮现象
glCullFace(GL_FRONT);
renderSceneToDepthMap();
glCullFace(GL_BACK);//不要忘记设回原来的裁剪面
  • 使用条件:正面剔除只对闭合物体有效

采样过多1

  • 现象:可以明显的看出光照区域,超出该区域的地方都是在阴影中
  • 原因:明显的光照区域是深度贴图被投影的部分,另外的阴影区域是超出平截头体的部分,他们的投影坐标大于1.0。假设某个位置的投影坐标是(1.2,1.7,0.9),把他当作纹理坐标对纹理采样时,实际用的是深度贴图上(0.2,0.7)处的值,而当深度贴图上的深度值小于0.9时,这个位置一定是在阴影中。当然前提是纹理的环绕方式是默认的GL_REPEAT
  • 解决办法:首先不能让纹理的环绕方式是重复的,第二采样深度贴图范围以外的区域,得到的都是1.0,这样就使超出平截头的区域始终不在阴影中
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
GLfloat borderColor[] = { 1.0, 1.0, 1.0, 1.0 };
glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, borderColor);

采样过多2

在这里插入图片描述

  • 现象:实际情况不应该在阴影中的区域,却始终在阴影中
  • 原因:原因与上一条的类似,不同的是,当一个位置离平截头体的远平面很远时,他的投影坐标的z坐标也大于1.0,这时候把纹理的环绕方式改为GL_CLAMP_TO_BORDER也不起作用,因为我们把超出纹理范围的深度值都设置为了1.0,这仍然小于实际的深度值z,所以他将一直在阴影中
  • 解决办法:对于一个点的投影坐标大于1.0的时候,我们强行设置他不在阴影中在这里插入图片描述

锯齿

  • 现象:阴影边缘出现明显的锯齿
  • 原因:因为深度贴图有一个固定的分辨率,当多个片段对应于一个纹素采样时,就出现了这种效果
  • 解决办法:
    1. 增加深度贴图的分辨率
    2. 尽可能的让平截头体的接近场景
    3. PCF(percentage-closer filtering)
  • PCF指的是使用多个不同的过滤方式,使阴影的锯齿边缘变得更柔和
  • PCF的核心思想是:同一个位置对深度贴图多次采样,每一次对采样的纹理坐标稍做偏移,最后对所有的采样结果进行平均
float shadow = 0.0; 
vec2 texelSize = 1.0 / textureSize(shadowMap, 0); //计算纹素大小;textureSize(..)返回纹理的宽和高,第二个参数是mipmap级别;改变texelSize的大小,可以改变阴影边缘的柔和度
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;

在这里插入图片描述

  • PCF的最终效果看起来与软阴影类似,但他不是软阴影
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值