OpenGL.Shader:12-阴影实现 - 解决阴影失真

 OpenGL.Shader:12-阴影实现 - 解决阴影失真

紧接上文的内容,那么怎么解决阴影失真的问题呢?这些问题其实都是不可回避的存在,现代技术只能尽量优化效果已达以假乱真的效果。 首先回到深度纹理的函数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();

注销掉地板的深度测试之后,效果大概是这样的。但是正方体上还存在失真,这下我们就要认真去了解下为什么会出现阴影失真

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

OpenGL.Shader:12-阴影实现 - 解决阴影失真_第1张图片

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

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

OpenGL.Shader:12-阴影实现 - 解决阴影失真_第2张图片

使用了偏移量后,所有采样点都获得了比表面深度更小的深度值,这样整个表面就正确地被照亮,没有任何阴影。我们可以在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坐标才有阴影,所以任何超出范围的都将会没有阴影。

这下效果应该是最帅的一个了,看看效果是不是这样?

OpenGL.Shader:12-阴影实现 - 解决阴影失真_第3张图片

其实我们还可以把光源位置做成动态效果,这个就非常接近游戏的效果体验了。

参考代码:https://github.com/MrZhaozhirong/NativeCppApp  工程内的ShadowFBORender.cpp    CubeShdow.hpp/LandShadow.hpp  IlluminateWithShadow.hpp    FramebufferObject.hpp

你可能感兴趣的:(OpenGL.Shader)