OpenGL基础53:阴影映射(下)

 

接上文:OpenGL基础52:阴影映射(上)

五、阴影失真

按照上文的计算的结果,一个很明显的问题是:对于参与计算深度贴图的物体,其表面可以看到这样的栅格状的阴影,这种常见的错误表现也叫做阴影失真(Shadow Acne)

出现这种请情况的原因是:在计算深度贴图的时候,分辨率不够高,毕竟这张深度贴图是对于整个场景而言的,想要一一对应到每个模型的每个片段就是个不可能的事,这样对于实际的模型的多个片段,就都会从深度贴图中的同一个坐标去采样,如果这些相邻片段的实际深度值一致那其实不会有什么问题,但由于光照角度的问题,这些相邻片段的深度值就会是不一样的,因此之前的比较方法在这里就会将部分片段给算在了阴影之中

解决方法也比较容易想到:多个相邻片段它们就算深度不同,但是它们的深度肯定是非常接近的,因此在计算当前片段是否被遮挡时,可以允许一定的误差

例子使用的是平行光,在平行光刚好垂直于表面时,相邻片段的深度值就是一样的,而光照倾斜角度越大,计算得到深度值差也就越大,所以可以在光照角度越接近于法向量时,使用更小的误差值

代码修改如下(就加了个bias的计算):

float bias = max(0.05 * (1.0 - dot(normalIn, lightDir)), 0.005);
float shadow = currentDepth - bias > closestDepth ? 0.75 : 0.0;

如果对产生的原因没有怎么明白,可以参考上面的图片:上图光照对于平面的倾斜角大约是45°,那么图中的 bias 的值就可以理解为当前实际片段与对应深度贴图采样片段的深度差,如果这个差是负数,那么当前的片段就会被归于被遮挡的片段

悬浮:

使用了上面的误差参与计算,也就相当于是对物体的实际深度进行了整体平移,如果这个误差值定义的过大了,就会有得到种物体“悬浮”的错觉:阴影有一点对不上实际的物体,因此误差值在保证解决了阴影失真的情况下要尽可能的小

当然还有个非常tricks的方法一样可以解决大部分情况下的阴影失真问题,还不会产生上面悬浮的效果:那就是在渲染深度贴图的时候是开启正面剔除,而非背面剔除,也就是永远使用物体的背面来计算深度

当然了,这种方法只对封闭的物体有效,并且接近阴影的物体仍然可能会出现不正确的效果,所以需要注意使用场景,不过大部分情况下并没有人会去关心理应不可能被看到的物体内部

上图为解决了阴影失真/悬浮的渲染结果

 

六、采样错误

这算是一个注意事项:

因为使用的是正交投影,所以要注意投影的范围一定尽可能的把场景中的所有物体包住,否则没有包住的部分就无法从深度贴图中采样,当然了,为了避免范围外的物体呈现完全的阴影,那么可以在设置深度贴图环绕方式为GL_CLAMP_TO_BORDER,也就是让所有超出深度贴图的坐标的深度范围是1.0,这样超出的坐标将永远不在阴影之中

除此之外,着色器部分也可以进行对应的修改,如果投影向量的z坐标大于1.0,就直接把shadow的值强制设为0.0,因为这部分的纹理已经超出了正交投影的远平面

float ShadowCalculation(vec4 fragPosLightSpace, vec3 lightDir)
{
    //……
    float bias = max(0.05 * (1.0 - dot(normalIn, lightDir)), 0.005);
    float shadow = currentDepth - bias > closestDepth ? 0.75 : 0.0;
    if(projCoords.z > 1.0)
        shadow = 0.0;
    return shadow;
}
glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, SHADOW_WIDTH, SHADOW_HEIGHT, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
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);

 

七、百分比渐近过滤(PCF)

仔细看这个阴影,可以发现它的边缘非常的“锋利”,并且还呈现明显的锯齿状,而在现实世界里,阴影往往都是柔和的,越是离物体表面越远则边缘越“软”

产生锯齿的原因和前面的阴影失真原因一样:深度贴图有一个固定的分辨率,从而在计算阴影时多个相邻片段就会只对应到一个纹理像素,理论上把深度贴图的分辨率调高就能有效缓解锯齿的现象

另一个简单的解决方案叫做百分比渐近过滤(PCF),它是一个比较的常见的进行阴影边缘反走的技术,原理是从深度贴图中多次采样,并且每一次采样的纹理坐标都稍有不同,这样每个独立的样本可能在也可能不再阴影中,所有的次生结果接着结合在一起进行平均化,就得到了相对柔和的阴影

一个最简单的PCF例子如下:

float ShadowCalculation(vec4 fragPosLightSpace, vec3 lightDir)
{
    // 执行透视除法
    vec3 projCoords = fragPosLightSpace.xyz / fragPosLightSpace.w;
    // 变换到[0,1]的范围
    projCoords = projCoords * 0.5 + 0.5;
    // 取得当前片段在光源视角下的深度
    float currentDepth = projCoords.z;
    // 检查当前片段是否在阴影中
    float bias = max(0.05 * (1.0 - dot(normalIn, lightDir)), 0.005);
    float shadow = 0.0;
    vec2 texelSize = 1.0 / textureSize(shadowMap, 0);
    for(int x = -1; x <= 1; ++x)
    {
        for(int y = -1; y <= 1; ++y)
        {
            float closestDepth = texture(shadowMap, projCoords.xy + vec2(x, y) * texelSize).r; 
            shadow += currentDepth - bias > closestDepth ? 0.75 : 0.0;    
        }    
    }
    shadow /= 9.0;
    if(projCoords.z > 1.0)
        shadow = 0.0;
    return shadow;
}

可以说和抗锯齿的方式有点相似

当然这只是最简单的PCF实现,对于下图中的效果,如果从一个较远的距离去观察阴影,就可以发现它没有那么生硬了,看起来要自然的多

 

八、扩展

关于阴影的实现,可以扩展的就可太多了,哪怕是已经介绍的方案与解决方法,仍然有可优化的空间,不过这两章至少是实现了最简单的阴影。总之,在此之后还是要注重方法而不是openGL本身,如果是换一个图形接口,又或者是用游戏引擎来实现一样的效果,也要能够很轻松的实现

光源烘培:

如果打过ACM,又或者解决过一些项目中的数据读取问题,都知道有一个很经典的方法叫做离线打表,也就是先运行一次打表代码,运算出一些重要的数据记录到本地,然后等到真正需要这些数据来解决问题的时候,直接读取之前打出来的数据即可,这个方法可以说比较tricks,之前一个很经典的题目:2017百度之星资格赛:1005. 寻找母串(卡特兰数+分块打表)有兴趣的话可以去看下

为什么要提到这个呢?因为在渲染领域,有一个很经典的方法叫做光源烘培,是和离线打表一样的原理

思考一下:是不是对于游戏的场景,极大多数的模型(墙壁、地面、路灯)等它们属性和位置是永远不会改变的?这也意味着,除非玩家从旁边走过,又或者游戏中有天气环境变化等,它们的渲染效果也是永远不会变的(其实哪怕是环境有改变,一个属性不变的光源对附近一个属性不变的静态物体产生的光效,也是不变的)

所以很多情况下,可以离线渲染出深度贴图,甚至是离线计算出模型每一个片段带光照的实际颜色,然后直接以贴图文件的形式存入本地,这样等真正运行游戏时,就可以直接使用这张贴图作为最终颜色,不但不需要专门的帧缓冲去重新生成深度贴图,也不需要在着色器中计算对应物体的漫反射光照和镜面光照了

好处很明显,其一是省去了实时渲染的复杂度,这是一个很好的优化,其二就是离线渲染没有复杂度要求,你甚至可以直接模拟每一条光线或者使用非常复杂的算法,只要能最后够烘培出很好的效果就都不是问题

坏处的话的也很明显:

  • 无法渲染出镜面光照,因为这和视角有关
  • 接上,静态物体无法折射出动态的游戏对象与坏境,也无法产生对应的阴影
  • 光照结果是死的,没有生机感
  • ……

关于光源烘培对于动态游戏对象的效果,有个妥协方案叫做光照探针,有兴趣的话可以去了解下

一句话总结:如果是静态物体+属性不变的区域光,可以直接现烘培出光照结果,得到对应的光照贴图/深度贴图,而不再需要在每帧中单独去计算

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值