紧接上文的内容,那么怎么解决阴影失真的问题呢?这些问题其实都是不可回避的存在,现代技术只能尽量优化效果已达以假乱真的效果。 首先回到深度纹理的函数renderDepthFBO,地板LandShadow其实是可以不参与阴影遮挡的深度测试,从而省去很大一部分遮挡测试。
depthFBO.begin();
{
glEnable(GL_DEPTH_TEST);
glClear(GL_DEPTH_BUFFER_BIT|GL_COLOR_BUFFER_BIT);
//glEnable(GL_CULL_FACE);
//glCullFace(GL_FRONT);
// 地板不参与阴影遮挡的深度测试 避免造成阴影失真
//landShadow.render(mLightProjectionMatrix,mLightViewMatrix,
// mLightPosition,
// mLightProjectionMatrix,mLightViewMatrix);
cubeShadow.render(mLightProjectionMatrix,mLightViewMatrix,
mLightPosition,
mLightProjectionMatrix,mLightViewMatrix);
//glCullFace(GL_BACK);
//glDisable(GL_CULL_FACE);
}
depthFBO.end();
注销掉地板的深度测试之后,效果大概是这样的。但是正方体上还存在失真,这下我们就要认真去了解下为什么会出现阴影失真
阴影贴图受限于解析度,在距离光源比较远的情况下,多个片元可能从深度贴图的同一个值中去采样。图片每个斜坡代表深度贴图一个单独的纹理像素。你可以看到,多个片元从同一个深度值进行采样。
虽然很多时候没问题,但是当光源以一个角度朝向表面的时候就会出问题,这种情况下深度贴图也是从一个角度下进行渲染的。多个片元就会从同一个斜坡的深度纹理像素中采样,有些在地板上面,有些在地板下面;这样我们所得到的阴影就有了差异。因为这个,有些片元被认为是在阴影之中,有些不在,由此产生了图片中的条纹样式。
我们可以用一个叫做阴影偏移(shadow bias)的技巧来解决这个问题,我们简单的对表面的深度(或深度贴图)应用一个偏移量,这样片元就不会被错误地认为在表面之下了。
使用了偏移量后,所有采样点都获得了比表面深度更小的深度值,这样整个表面就正确地被照亮,没有任何阴影。我们可以在shader中这样实现这个偏移:
float bias = 0.0005;
float shadow = currentDepth - bias > closestDepth ? 1.0 : 0.0;
选用正确的偏移数值,在不同的场景中需要一些像这样的轻微调校,但大多情况下,实际上就是增加偏移量直到所有失真都被移除的问题。
经过调整之后,效果大致如下:
但是地板的左、右、后出现了三块“不和谐”的阴影呢?
这是因为光照有一个区域,超出该区域就成为了阴影;这个区域实际上代表着深度贴图的大小,这个贴图投影到了地板上。发生这种情况的原因是我们创建FBO的将深度贴图的环绕方式设置成了GL_REPEAT。
我们宁可让所有超出深度贴图的坐标的深度范围是1.0,这样超出的坐标将永远不在阴影之中。我们可以把深度贴图的纹理环绕选项设置为GL_CLAMP_TO_EDGE:
void createDepthTexture()
{
glGenTextures(1, &_depthTexId);
glBindTexture(GL_TEXTURE_2D, _depthTexId);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
//glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, _width, _height, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT, 0);
}
但是这样只能解决左右两边的阴影,至于在后方的阴影是另外一个原因产生的。
那里的坐标超出了光的正交视锥的远平面。你可以看到这片黑色区域总是出现在光源视锥的极远处。当一个点比光的远平面还要远时,它的投影坐标的z坐标大于1.0。这种情况下,GL_CLAMP_TO_EDGE环绕方式不起作用,因为我们把坐标的z元素和深度贴图的值进行了对比;它总是为大于1.0的z返回true。解决这个问题也很简单,只要投影向量的z坐标大于1.0,我们就把shadow的值强制设为0.0:
float ShadowCalculation(vec4 fragPosLightSpace)
{
[...]
if(projCoords.z > 1.0) shadow = 0.0;
return shadow;
}
这样做意味着,只有在深度贴图范围以内的被投影的fragment坐标才有阴影,所以任何超出范围的都将会没有阴影。
这下效果应该是最帅的一个了,看看效果是不是这样?
其实我们还可以把光源位置做成动态效果,这个就非常接近游戏的效果体验了。
参考代码:https://github.com/MrZhaozhirong/NativeCppApp 工程内的ShadowFBORender.cpp CubeShdow.hpp/LandShadow.hpp IlluminateWithShadow.hpp FramebufferObject.hpp