基于Shadow Map的阴影实现

来源:互联网 发布:海迅拆单软件正版价格 编辑:程序博客网 时间:2024/04/29 01:26

0、简介

Shadow Mapping是一种基于图像空间的阴影实现方法,其优点是实现简单,适应于大型动态场景;缺点是由于shadow map的分辨率有限,使得阴影边缘容易出现锯齿(Aliasing);关于SM的研究很活跃,主要围绕着阴影抗锯齿,出现了很多SM变种,如PSM,LPSM,VSM等等,在这里http://en.wikipedia.org/wiki/Shadow_mapping可以找到很多SM变种的链接;;SM的实现分为两个pass,第一个pass以投射阴影的灯光为视点渲染得到一幅深度图纹理,该纹理就叫Shadow Map;第二个pass从摄像机渲染场景,但必须在ps中计算像素在灯光坐标系中的深度值,并与Shadow Map中的相应深度值进行比较以确定该像素是否处于阴影区;经过这两个pass最终就可以为场景打上阴影。这篇文章主要总结一下自己在实现基本SM的过程中遇到的一些问题以及解决方法,下面进入正题。

1、生成Shadow Map

为了从灯光角度渲染生成Shadow Map,有两个问题需要解决:一是要渲染哪些物体,二是摄像机的参数怎么设置。对于问题一,显然我们没必要渲染场景中的所有物体,但是只渲染当前摄像机视景体中的物体又不够,因为视景体之外的有些物体也可能投射阴影到视景体之内的物体,所以渲染Shadow Map时,这些物体必须考虑进来,否则可能会出现阴影随着摄像机的移动时有时无的现象,综上,我们只需要渲染位于当前摄像机视景体内的所有物体以及视景体之外但是会投射阴影到视景体之内的物体上的物体,把它们的集合称为阴影投射集,为了确定阴影投射集,可以根据灯光位置以及当前的视景体计算出一个凸壳,位于该凸壳中的物体才需要渲染,如图1所示。对于问题二,灯光处摄像机的视景体应该包含阴影投射集中的所有物体,另外应该让物体尽量占据设置的视口,以提高Shadow Map的精度;对于方向光和聚光灯,摄像机的look向量可以设置为光的发射发向,摄像机的位置设置为灯光所在的位置,为了包含阴影集中的所有物体,可以计算阴影投射集在灯光视图空间中的轴向包围盒,然后根据面向光源的那个面设置正交投影参数,就可以保证投射集中的所有物体都位于灯光视景体中,并且刚好占据整个视口,如图2所示。更详细的信息可以参考《Mathematics for 3D Game Programming and Computer Graphics, Third Edition》一书的10.2节。

图1:灯光位置与视景体构成一个凸壳,与该凸壳相交的物体称为阴影投射集

图2:根据阴影投射集在灯光视图空间中的包围盒来计算正交投影参数

2、生成阴影场景

生成了ShadowMap之后,就可以根据相应灯光的视图矩阵和投影矩阵从摄像机角度渲染带有阴影的场景了。下面把相关的shader代码贴上:

vertex shader:

// for projective texturinguniform mat4 worldMatrix;uniform mat4 lightViewMatrix;uniform mat4 lightProjMatrix;varying vec4 projTexCoord;void main(){// for projective texture mappingprojTexCoord = lightProjMatrix*lightViewMatrix*worldMatrix*pos;// map project tex coord to [0,1]projTexCoord.x = (projTexCoord.x + projTexCoord.w)*0.5;projTexCoord.y = (projTexCoord.y + projTexCoord.w)*0.5;projTexCoord.z = (projTexCoord.z + projTexCoord.w)*0.5;gl_Position = gl_ModelViewProjectionMatrix * pos;}
pixel shader(版本一):

// The shadow mapuniform sampler2DShadow shadowDepthMap;varying vec4 projTexCoord;void main(){// light computingvec4 lightColor = Lighting(...);vec4 texcoord = projTexCoord;texcoord.x /= texcoord.w;texcoord.y /= texcoord.w;texcoord.z /= texcoord.w;// depth comparisonvec4 color = vec4(1.0,1.0,1.0,1.0);float depth = texture(shadowDepthMap,vec3(texcoord.xy,0.0));if(texcoord.z > depth){// this pixel is in shadow areacolor = vec4(0.6,0.6,0.6,1.0);}gl_FragColor = lightColor*color;}
这样就实现了最基本的Shadow Mapping,对于每个像素只采样一个深度texel,看看效果吧。


                                                                图3:最基本的Shadow Mapping

可以看到效果不尽如人意,主要有三个问题(分别对应上图标记):1、物体的背光面当作阴影处理了;2、正对着光的一些像素也划到阴影区去了(Self-shadowing);3、阴影边缘有比较强的锯齿效果。对于问题一,可以判断当前像素是否背着灯光,方法是求得像素在灯光视图空间中的位置以及法线,然后求一个点积就可以了,对于这种像素,不用进行阴影计算即可;问题二产生的原因是深度误差导致的,当物体表面在灯光视图空间中的倾斜度越大时,误差也越大;解决办法有多种,第一种是在进行深度比较时,将深度值减去一个阈值再进行比较,第二种是在生成shadow map时,只绘制背面,即将背面设置反转,第三种方法是使用OpenGL提供的depth offset,在生成shadow map时,给深度值都加上一个跟像素斜率相关的值;第一种方法阈值比较难确定,无法完全解决Self-shadowing问题,第二种方法在场景中都是二维流形物体时可以工作的很好,第三种方法在绝大多数情况都可以工作得很好,这里使用这种方法,如下面代码所示:

        // handle depth precision problem glEnable(GL_POLYGON_OFFSET_FILL); glPolygonOffset(1.0f,1.0f);// 绘制阴影投射集中的物体glDisable(GL_POLYGON_OFFSET_FILL);
解决第一和第二个问题后的效果如下面所示图4所示:

                                         图4:解决背光面阴影和Self-shadowing问题之后的效果

还有最后一个问题未解决,从上面的图也可看得出来,阴影边缘锯齿比较严重,解决这个问题也有两种较常用方法,第一种方法是用PCF(Percentage Closer Filtering),基本思想是对每个像素从shadow map中采样相邻的多个值,然后对每个值都进行深度比较,如果该像素处于阴影区就把比较结果记为0,否则记为1,最后把比较结果全部加起来除以采样点的个数就可以得到一个百分比p,表示其处在阴影区的可能性,若p为0代表该像素完全处于阴影区,若p为1表示完全不处于阴影区,最后根据p值设定混合系数即可。第二种方法是在阴影渲染pass中不计算光照,只计算阴影,可以得到一幅黑白二值图像,黑色的表示阴影,白色表示非阴影,然后对这幅图像进行高斯模糊,以对阴影边缘进行平滑,以减小据齿效果,最后在光照pass中将像素的光照值与相应二值图像中的值相乘就可以得到打上柔和阴影的场景了;在理论上来说,两种方法都能达到柔和阴影的效果,本文采用的PCF方法,第二种方法后面会尝试,并在效果和速度上与PCF做一下比较,等测试完了会贴到这里来。下面贴上加了PCF的像素shader的代码:

// The shadow mapuniform sampler2DShadow shadowDepthMap;varying vec4 projTexCoord;void main(){// light computingvec4 lightColor = Lighting(...);float shadeFactor = 0.0;shadeFactor += textureProjOffset(shadowDepthMap, projTexCoord, ivec2(-1, -1));shadeFactor += textureProjOffset(shadowDepthMap, projTexCoord, ivec2(-1, 1));shadeFactor += textureProjOffset(shadowDepthMap, projTexCoord, ivec2( 1, -1));shadeFactor += textureProjOffset(shadowDepthMap, projTexCoord, ivec2( 1, 1));shadeFactor *= 0.25;// map from [0.0,1.0] to [0.6,1.0]shadeFactor = shadeFactor * 0.4 + 0.6; gl_FragColor = lightColor*shadeFactor;}
另外注意要对shadow map纹理设置以下纹理参数:

glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_MODE,GL_COMPARE_REF_TO_TEXTURE);glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_COMPARE_FUNC,GL_LEQUAL);
PCF效果如下图 所示,可以看到阴影边缘变柔和了:

                            图5:柔和的阴影边缘(PCF:采样pattern:左上,左下,右下,右上)

不同的采样pattern对结果也会有影响,比如采用下面的采样pattern效果如图6所示:

shadeFactor += textureProjOffset(shadowDepthMap, projTexCoord, ivec2(-1.5, 0.5));shadeFactor += textureProjOffset(shadowDepthMap, projTexCoord, ivec2(0.5, 0.5));shadeFactor += textureProjOffset(shadowDepthMap, projTexCoord, ivec2(-1.5, -1.5));shadeFactor += textureProjOffset(shadowDepthMap, projTexCoord, ivec2(0.5, -1.5));

                             图6:柔和的阴影边缘(PCF:使用非对齐采样pattern)

最后再贴一个2048分变率的shadow map产生的阴影效果,以作比较,PCF的采样pattern跟图6中一样。

 图7:shadow map大小为2048时的阴影效果(PCF采样pattern与图6一样)。


3、结语

关于Shadow Map的变种有很多,不同的变种针对不同情况不同场景提供SM阴影抗锯齿解决方案,本文实现的只是基本的SM,后面考虑实现某种SM变种,进一步提高阴影的效果。