之所以叫改进(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);
之前做的阴影锯齿非常严重,我认为锅是纹理精度的,但后来发现并不是。有实例证明,以下是我在使用了浮点纹理后,得到的阴影效果(图中球体未加入光照,纯灰色):
实际上,此处是因为从阴影图中采样加比较导致的误差。所以,需要专门对阴影进行反走样操作。使用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;
通过加权平均,最终改进的效果为:
测试第一版阴影效果的时候,我试着改了一些参数,原本地面上的阴影应该是一个圆,修改后发现地面上出现了很多个圆,看起来很像纹理平铺的效果(也就是重复延展)。所以可以确定是采样阴影纹理的时候,uv值有一些不在(0,1)的范围内。(补充:后来进一步发现,这是我的灯光视锥体取的不合适导致的,正在研究改进中)
我做了一个操作 uv = clamp(uv, 0.0, 1.0); 结果就正确了。
之后看了上面的参考文章,发现里面也提到了这个问题,它给出的解决方法是:直接在生成阴影纹理的时候修改纹理环绕方式即可,使其不要重复。发现我并没想到这个思路。
其实这个问题之前已经做了一些解决,也就是在比较实际深度和纹理图采样的深度时,加上一个偏移值,避免浮点误差和采样精度导致的误差。但一开始取的是一个定值。后来发现在取不同远裁剪面后,这个偏移值也得相应的变化,才能达到更好的效果,所以经过改进,我把它做成了一个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;
}