[OpenGL] shadow map - 改进 (1)

[OpenGL] shadow map - 改进 (1)_第1张图片

         之所以叫改进(1),是因为之后可能会有2,3之类的。

        相比起之前比较粗糙的版本,本篇改进的内容包含:

        (1) 阴影贴图支持随着窗口缩放变化大小

        (2) 提升了阴影精度,使用浮点纹理

        (3) 使用percentage-closer filtering,减少边缘锯齿

        (4) 修正一些计算不准确的地方

以上部分改进主要参考文章:https://learnopengl-cn.readthedocs.io/zh/latest/05%20Advanced%20Lighting/03%20Shadows/01%20Shadow%20Mapping/

       这篇文章专门用了一个叫深度缓冲的东西,我目前用的还是颜色缓冲。

        待改进的内容:

        (1) 支持有阴影部分有重叠的多个物体阴的影渲染,目前的版本好像还不能很好地处理这种情况。

        (2) 支持多光源渲染,因为多光源比较耗性能,所以打算先把流程从前向渲染迁移到延迟渲染。

补充:新的几个问题已经有了解决思路,等解决了之后就慢慢在此处继续更新吧,开太多文章讨论这一话题感觉有点乱;此外还会慢慢更正自己之前做的不对的地方。

阴影贴图大小

        阴影贴图我目前是按照屏幕大小来生成的,这样纹理访问会比较直接,像生成比屏幕更大的贴图暂时没有考虑过。

        存在一个问题是,随着resize的操作,屏幕大小会发生变化。最终采取的解决方案是,在Resize操作时,将旧的阴影纹理删除,根据屏幕大小重新生成新的阴影纹理。

void RenderCommon::UpdateShadowMapTex()
{
    glDeleteTextures(1, &shadowMap);

    // 创建一个帧缓冲对象
    glGenFramebuffers(1, &shadowFrameBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, shadowFrameBuffer);

    // 生成纹理图像,附加到帧缓冲
    glGenTextures(1, &shadowMap);
    glBindTexture(GL_TEXTURE_2D, shadowMap);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F_ARB, screenX, screenY, 0, GL_RGB, GL_FLOAT, nullptr);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glBindTexture(GL_TEXTURE_2D, 0);

    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, shadowMap, 0);
}

阴影精度提升

        之前使用的是整数纹理,范围在0~255,然后做了一点特殊处理,将浮点数存到4个通道里,在这种情况下,由于写入深度的时候,存的是z/zFar,远裁剪面作为分数来保证取值小于1。此时,远裁剪面只要稍大一点儿,就容易出现误差,比如我取值500左右就会出现问题,比如,离镜头比较近的阴影深度因为精度不够最后得到的结果是0,所以就不绘制了。而实际游戏中,基本都是大场景,这么小的远裁剪面肯定是不行的。

         修改之后,使用了16位浮点纹理,我又多存了几个通道,最后测试了一下,远裁剪面取10000都没什么问题了。

glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F_ARB, screenX, screenY, 0, GL_RGB, GL_FLOAT, nullptr);

阴影锯齿

        之前做的阴影锯齿非常严重,我认为锅是纹理精度的,但后来发现并不是。有实例证明,以下是我在使用了浮点纹理后,得到的阴影效果(图中球体未加入光照,纯灰色):

[OpenGL] shadow map - 改进 (1)_第2张图片

        实际上,此处是因为从阴影图中采样加比较导致的误差。所以,需要专门对阴影进行反走样操作。使用percentage-closer filtering的方法,也就是从当前纹理坐标的邻域采样(3x3的范围),最后平均得到最终阴影因子,采样的邻域范围和屏幕大小相关。

    for(int i = -1; i <= 1; i++)
    {
        for(int j = -1; j <= 1; j++)
        {
            vec4 distance = texture2D(shadowMap, uv + vec2(1.0 * i / ScreenX, 1.0 * j / ScreenY));
            float fDistanceMap = distance.r + distance.g/65536.0 + distance.b /(65536.0*65536.0);
            //此处是解码纹理,可以无视这段
            fShadow += fDistance - offset > fDistanceMap ? 0.4 : 0.8;
        }
    }
    fShadow /= 9.0;

        通过加权平均,最终改进的效果为:

[OpenGL] shadow map - 改进 (1)_第3张图片

阴影采样过多

        测试第一版阴影效果的时候,我试着改了一些参数,原本地面上的阴影应该是一个圆,修改后发现地面上出现了很多个圆,看起来很像纹理平铺的效果(也就是重复延展)。所以可以确定是采样阴影纹理的时候,uv值有一些不在(0,1)的范围内。(补充:后来进一步发现,这是我的灯光视锥体取的不合适导致的,正在研究改进中)

[OpenGL] shadow map - 改进 (1)_第4张图片

        我做了一个操作 uv = clamp(uv, 0.0, 1.0); 结果就正确了。

        之后看了上面的参考文章,发现里面也提到了这个问题,它给出的解决方法是:直接在生成阴影纹理的时候修改纹理环绕方式即可,使其不要重复。发现我并没想到这个思路。

阴影失真

[OpenGL] shadow map - 改进 (1)_第5张图片 未加入偏移值的效果

 

      其实这个问题之前已经做了一些解决,也就是在比较实际深度和纹理图采样的深度时,加上一个偏移值,避免浮点误差和采样精度导致的误差。但一开始取的是一个定值。后来发现在取不同远裁剪面后,这个偏移值也得相应的变化,才能达到更好的效果,所以经过改进,我把它做成了一个zFarPlane相关的值。

最终:

float GetShadow()
{
    float fShadow = 0.0;
    float fDistance = lightPos.z / zFar;

    vec2 uv = lightPos.xy / lightPos.w * 0.5 + vec2(0.5, 0.5);

    uv.x = clamp(uv.x, 0, 1);
    uv.y = clamp(uv.y, 0, 1);

    float offset = 0.5/zFar;
    for(int i = -1; i <= 1; i++)
    {
        for(int j = -1; j <= 1; j++)
        {
            vec4 distance = texture2D(shadowMap, uv + vec2(1.0 * i / ScreenX, 1.0 * j / ScreenY));
            float fDistanceMap = distance.r + distance.g/65536.0 + distance.b /(65536.0*65536.0);
            fShadow += fDistance - offset > fDistanceMap ? 0.1 : 0.9;
        }
    }
    fShadow /= 9.0;

    return fShadow;
}

       

你可能感兴趣的:(OpenGL)