现在的位置: 首页 > 综合 > 正文

shadow map 阴影贴图技术之探 4

2019年03月19日 ⁄ 综合 ⁄ 共 10859字 ⁄ 字号 评论关闭

这里是ZwqXin关于Shadow Map阴影贴图的OpenGL实现记录的第四辑。终于回到了这个节骨眼上,请与我一起进军时尚的Cascaded Shadow Maps(CSM)吧。——ZwqXin.com

Shadow Map阴影贴图技术之探Ⅰ
Shadow Map阴影贴图技术之探Ⅱ
Shadow Map阴影贴图技术之探Ⅲ

本文来源于 ZwqXin (http://www.zwqxin.com/), 转载请注明
原文地址:http://www.zwqxin.com/archives/opengl/shadow-map-4.html

上篇(Ⅲ)里的最后最后,提及了几种比较有名的Shadow Map的延展技术,Cascaded Shadow Maps是其中比较近期才出现的,而且它引进了Cascade(级联,层)这个概念,与另一个颇为我们中国人骄傲的名词PSSM(Parallel-split Shadow Maps)中的Parallel-split指的是同一个概念。事实上两者的原理是基本一样的。

它先在我们的视锥上动手脚,用几个与近远平面平行的截面把视锥分成几份(Parallel-split);然后针对每一份,通过修改光源投影矩阵,使之后生成的Shadow Map中只有该份“Splited视锥”里的物体;这样,在pass1阶段就生成了几张针对不同“Splited视锥”的Shadow Maps,在渲染阶段,依据像素深度就可以判断该位置应用哪张Shadow Map了。

这样做的好处在上篇已经讲过了。在距离眼睛近的地方,应用的是分辨率高的阴影图,距离眼睛远的地方则是低分辨率。这样是符合视觉特点的,而且没有什么浪费的地方。

如图,假设光从视锥正上方射下来(其他方向同理),按CSM的意思,应该把光源视觉下的投影面放在图示位置(四条短的水平的线)。这里我把视锥分割成四份,因此需要对应的四张ShadowMap,与人看东西一样,视像面越靠近阴影(假设位于被投影面,图中长水平线),看到的阴影越清晰。反映在生成阴影图阶段,表现为具体caster(被光源直接照射的投射物表面)在光源投影面上占据的范围大。假设阴影图尺寸是固定的(譬如1024*1024),在第一个“Splited视锥”和第四个“Splited视锥”里的投射阴影的物体[投射物]大小也相同(其阴影在实际世界里占地面积必然也相同),则其阴影在阴影图里占的像素数会有很大差别(譬如前者占500,000个,后者可能才占5000个),这就是分辨率的差异。最后把ShdowMap帖在场景里(假设在世界空间下该种投射物的阴影应该占100,000个像素),前者就会比后者效果好很多。(一个是需要进行OverSampling,另一个就得进行UnderSampling。)所以越靠近眼睛的、越小的Splited视锥里的阴影越高“画质”,反之则越粗糙(但比起传统Shadow
Map技术也许效果还好一点)——而我们正希望要眼前的事物清晰,远处的事物模糊甚至不表现出影子也可以——CSM(或者说,PSSM)做到了。

重新回头看看技术实现过程。这里有两个主要的技术点,一是“怎么分割视锥”,二是“怎么设置每个小视锥的光源投影矩阵”。

1. Cascade(Split)的准则

从上图和上分析可以看出,“Splited视锥”沿视线的长度(Zfar - Znear)应该越分越大比较合理,指数增长符合这个规律,但指数增长一般太夸张了,所以配合一个线性增长比较好。在PSSM里,这两种分法叫logarithmic split scheme和the
uniform split scheme,前者的表达式是经过科学的推导的,这部分也是CSM/PSSM最数学的部分,在GPU GEMS3里有详细的推导,或者你看PSSM推广人Fan Zhang[HKUST]那篇"Hardware-Accelerated Parallel-Split Shadow Maps." (IF YOU CAN FIND IT)也该有。它从Shadow-Map Aliasing(dp/ds,单位阴影图像素单位对应的屏幕像素)的推导开始,找出能满足使perspective
aliasing(由投影缩减效应形成)在各个视锥里均匀分配的分割式。

后者只是一个线性式,但它的调和作用避免了“Splited视锥”的过小与过大,通过一个mix因子混合两式子(我在应用中默认分配logarithmic split scheme的因子是0.75,余者0.25):

  1. // www.ZwqXin.com Cascaded Shadow Maps
  2. void CCascadingSM::ComputeSplits(float strength,float Dis_Near,
    float Dis_Far)
  3. {
  4. float distance_scale = Dis_Far / Dis_Near;
  5. splitfrust[0].ResightNear(Dis_Near); //开始分割
  6. float partisionFactor = 0.0;
  7. float lerpValue1 = 0.0, lerpValue2 = 0.0;
  8. float SplitsZ = 0.0;
  9. for(int i = 1; i < NumofSplits; ++i)
  10. {
  11. partisionFactor = i / (float)NumofSplits;
  12. lerpValue1 = Dis_Near + partisionFactor * (Dis_Far - Dis_Near);
  13. lerpValue2 = Dis_Near * powf(distance_scale, partisionFactor);
  14. // 分割面的Z值. 1.005f防止前一个子视锥的远裁切面与后一个子视锥的近裁切面冲突
  15. SplitsZ = (1-strength) * lerpValue1 + strength * lerpValue2;
  16. splitfrust[i].ResightNear(SplitsZ * 1.002f);
  17. splitfrust[i-1].ResightFar(SplitsZ);
  18. }
  19. splitfrust[NumofSplits-1].ResightFar(Dis_Far);//结束分割
  20. }
// www.ZwqXin.com  Cascaded Shadow Mapsvoid CCascadingSM::ComputeSplits(float strength, float Dis_Near, float Dis_Far){   float distance_scale = Dis_Far / Dis_Near;   splitfrust[0].ResightNear(Dis_Near); //开始分割   float partisionFactor = 0.0;   float lerpValue1 = 0.0, lerpValue2 = 0.0;   float SplitsZ = 0.0;	for(int i = 1; i < NumofSplits; ++i)	{		partisionFactor = i / (float)NumofSplits;		lerpValue1 = Dis_Near + partisionFactor * (Dis_Far - Dis_Near);		lerpValue2 = Dis_Near * powf(distance_scale, partisionFactor);		// 分割面的Z值. 1.005f防止前一个子视锥的远裁切面与后一个子视锥的近裁切面冲突        SplitsZ =  (1-strength) * lerpValue1  + strength * lerpValue2; 			       		splitfrust[i].ResightNear(SplitsZ * 1.002f);		splitfrust[i-1].ResightFar(SplitsZ);	}	splitfrust[NumofSplits-1].ResightFar(Dis_Far);//结束分割}

2. Crop It !

针对每个光源投影矩阵进行的调整,在CSM/PSSM里称为Crop(这么有诗情画意噶?)。这个过程其实很好理解的,我们在照相的时候,一开始要在CCD液晶屏的画面上把焦点确定吧——Cascaded Shadow Maps技术中的光源就是照相者,光源的视像平面就是屏幕,我们是对每个“Splited视锥”都照一张相,因为照的是casters,所以可以说是照人物相片——把casters所在的“Splited视锥”(对应人物背景)在光源投影空间的中心挪移到视像平面的中心,然后进行光学变焦,使人物背景尽量充满屏幕,从而突出人物——casters,噢,不,应说是shadows。

恩,这是个具有平移和缩放的线性变换——CROP MATRIX,合适地构造它,然后乘在光源投影矩阵前面(形成新的投影矩阵),就能完成匹配投影矩阵匹配“Splited视锥”的任务。假如目前处理第i个分割视锥,生成CropMatrix[i],那么对场景坐标系的变换就是:(CropMatrix[i] * LightProjectMatrix) * LightViewMatrix * ModelMatrix * pos。也可认为(CropMatrix[i] *
LightProjectMatrix)是二次投影,因为Crop Matrix实质也是个投影矩阵,而且是个名副其实的Otho正交投影矩阵。

  1. // www.ZwqXin.com Cascaded Shadow Maps
  2. void CCascadingSM::ApplyCropProjectMatrix(CFrustum &frust)
  3. {
  4. CVector3 maxFrustumCoord, minFrustumCoord;
  5. CMatrix16 CurrentMatrix;//当前矩阵
  6. CMatrix16 CropMatrix;//协调光源视野与视锥的Crop Matrix
  7. //光源视图矩阵
  8. glGetFloatv(GL_MODELVIEW_MATRIX, CurrentMatrix.mt);
  9. //生成视锥的AABB特征向量,视锥先经CurrentMatrix变换到光源视图空间
  10. GetFrustumAABBCoords(frust, maxFrustumCoord, minFrustumCoord, &CurrentMatrix);
  11. //计算给Crop Matrix的调整参数
  12. float scaleX = 2.0f/(maxFrustumCoord.x - minFrustumCoord.x);
  13. float scaleY = 2.0f/(maxFrustumCoord.y - minFrustumCoord.y);
  14. float offsetX = -0.5f*(maxFrustumCoord.x + minFrustumCoord.x) * scaleX;
  15. float offsetY = -0.5f*(maxFrustumCoord.y + minFrustumCoord.y) * scaleY;
  16. CropMatrix = CMatrix16(scaleX, 0.0f, 0.0f, 0.0f,
  17. 0.0f, scaleY, 0.0f, 0.0f,
  18. 0.0f, 0.0f, 1.0f, 0.0f,
  19. offsetX, offsetY, 0.0f, 1.0f );
  20. //CropProjectMatrix(光源投影矩阵 = CropMatrix*ProjectZMatrix)
  21. glLoadIdentity();
  22. glLoadMatrixf(CropMatrix.mt);
  23. //以max_Z和min_Z作为远近裁切面的正投影矩阵
  24. glOrtho(-1.0, 1.0, -1.0, 1.0, -maxFrustumCoord.z, -minFrustumCoord.z );
  25. }
// www.ZwqXin.com  Cascaded Shadow Maps void CCascadingSM::ApplyCropProjectMatrix(CFrustum &frust) {      CVector3 maxFrustumCoord, minFrustumCoord;       CMatrix16 CurrentMatrix;//当前矩阵     CMatrix16 CropMatrix;//协调光源视野与视锥的Crop Matrix                            //光源视图矩阵     glGetFloatv(GL_MODELVIEW_MATRIX, CurrentMatrix.mt);         //生成视锥的AABB特征向量,视锥先经CurrentMatrix变换到光源视图空间     GetFrustumAABBCoords(frust, maxFrustumCoord, minFrustumCoord, &CurrentMatrix);        //计算给Crop Matrix的调整参数     float scaleX = 2.0f/(maxFrustumCoord.x - minFrustumCoord.x);     float scaleY = 2.0f/(maxFrustumCoord.y - minFrustumCoord.y);     float offsetX = -0.5f*(maxFrustumCoord.x + minFrustumCoord.x) * scaleX;     float offsetY = -0.5f*(maxFrustumCoord.y + minFrustumCoord.y) * scaleY;       CropMatrix = CMatrix16(scaleX,    0.0f,  0.0f, 0.0f,                              0.0f,  scaleY,  0.0f, 0.0f,                              0.0f,    0.0f,  1.0f,  0.0f,                           offsetX, offsetY,  0.0f,  1.0f );      //CropProjectMatrix(光源投影矩阵 = CropMatrix*ProjectZMatrix)      glLoadIdentity();      glLoadMatrixf(CropMatrix.mt);      //以max_Z和min_Z作为远近裁切面的正投影矩阵       glOrtho(-1.0, 1.0, -1.0, 1.0, -maxFrustumCoord.z, -minFrustumCoord.z );   }

CropMatrix简直就跟glOrtho生成的矩阵一模一样,功用也一样。只不过这里我没有对Z坐标进行变换,因为把它交给生成光源投影矩阵的glOrtho了(反而它只变换Z坐标)。前面不是说把坐标都变换到光源投影CLip空间后再提取AABB吗,为什么就到光源视图空间就比较了?因为这里是平行光的投影,所以用的是正交投影glOrtho,在glOrtho中没有对X,Y坐标进行变换(看看它的spec就知道了,-1与1为参数是不改变X,Y数值的),所以两个空间下的X,Y坐标是一致的,而CropMatrix正是只变换X,Y坐标,所以实在没必要多此一举。

但有两种情况是“需要多此一举”的。一是光源为点光源且需要透视投影;二是在光源与视锥之间还有其他caster。对第二种情况尤其值得注意。看回我在文章最上面放的自画示意图,有个打了X的地方,那里假设有只bird,那么它会否对地面产生阴影呢?——按照CSM基础理论,不会!因为CropMATRIX修改后的光源投影平面已经越过它了,已经看不见它了——我们只能看见视锥里(更准确说是视锥的AABB包围盒里)的物体所留下的阴影!解决法是把该物件bondingbox在光源视图空间下的最大Z坐标作为上述算法最后的minFrustumCoord.z,使光源投影平面恰在该位置而不再下降。这样做多了些麻烦,而且该“Splited视锥”对应的Shadow
Map的分辨率会降低,物体离视锥越远,分辨率下降越严重。所以,如非必要投射那样的物体(或者部分穿出视锥之外的物体)的阴影,不必这样做:

先计算普适意义下的光源投影矩阵和视图矩阵(类似传统SM那样),用它们的积Light-ProjectView把各个小视锥变换到CLIP投影空间,用同样方法得到该空间下的包围盒(特征向量maxFrustumCoord, minFrustumCoord),这里继续计算的Crop矩阵就需要用到Z值了,因为我们要修改其中的minFrustumCoord.z。让它等于-1——OPENGL在CLIP投影空间的最小坐标值。没错,即使该物件在光源正体位置之上,也把它计算入要投影的物件集(casters)里(况且平行光源本来该是无限远而不是在那个虚拟位置上的)。最后依然是:CropMatrix[i]
*( LightProjectMatrix * LightViewMatrix) * ModelMatrix * pos。

  1. // www.ZwqXin.com Cascaded Shadow Maps
  2. CVector3 maxFrustumCoord, minFrustumCoord;
  3. //.....
  4. GetFrustumAABBCoords(frust, maxFrustumCoord, minFrustumCoord, &CurrentMV);
  5. minFrustumCoord.z = -1.0f;
  6. //计算给Crop Matrix的调整参数
  7. float scaleX = 2.0f/(maxFrustumCoord.x - minFrustumCoord.x);
  8. float scaleY = 2.0f/(maxFrustumCoord.y - minFrustumCoord.y);
  9. float scaleZ = 2.0f / (maxFrustumCoord.z - minFrustumCoord.z);
  10. float offsetX = -0.5f*(maxFrustumCoord.x + minFrustumCoord.x) * scaleX;
  11. float offsetY = -0.5f*(maxFrustumCoord.y + minFrustumCoord.y) * scaleY;
  12. float offsetZ = -0.5f*(maxFrustumCoord.z + minFrustumCoord.z) * scaleZ;
  13. CropMatrix = CMatrix16(scaleX, 0.0f, 0.0f, 0.0f,
  14. 0.0f, scaleY, 0.0f, 0.0f,
  15. 0.0f, 0.0f, scaleZ, 0.0f,
  16. offsetX, offsetY, 0.0f, 1.0f );
  17. //....
// www.ZwqXin.com  Cascaded Shadow Maps      CVector3 maxFrustumCoord, minFrustumCoord;   //.....	GetFrustumAABBCoords(frust, maxFrustumCoord, minFrustumCoord, &CurrentMV);	minFrustumCoord.z = -1.0f;	 //计算给Crop Matrix的调整参数    float scaleX = 2.0f/(maxFrustumCoord.x - minFrustumCoord.x);	float scaleY = 2.0f/(maxFrustumCoord.y - minFrustumCoord.y);	float scaleZ  = 2.0f / (maxFrustumCoord.z - minFrustumCoord.z);	float offsetX = -0.5f*(maxFrustumCoord.x + minFrustumCoord.x) * scaleX;	float offsetY = -0.5f*(maxFrustumCoord.y + minFrustumCoord.y) * scaleY;	float offsetZ = -0.5f*(maxFrustumCoord.z + minFrustumCoord.z) * scaleZ;      CropMatrix = CMatrix16(scaleX,    0.0f,  0.0f, 0.0f,                              0.0f,  scaleY,  0.0f, 0.0f,                              0.0f,    0.0f,  scaleZ,  0.0f,                           offsetX, offsetY,  0.0f,  1.0f ); //....

3. Cast 阴影

通过上面矩阵配合(0,1)映射矩阵之类的生成shadow maps后,这就来到第二PASS了,它与传统Shadow Map(Shadow Map阴影贴图技术之探Ⅰ)一样,只是根据像素深度决定用哪张而已。注意,把视锥分割的是近/远平面,其值是距视点的距离,定义于视图空间——把它变换到眼睛的屏幕CLIP空间,就能在shader里“分割”像素深度,把像素都分到SplitNum个区域里(应用中我取了4个)。好了,接下来你知道怎么用if-else来Cast
阴影图了吧。

  1. // www.ZwqXin.com Cascaded Shadow Maps
  2. //fragment shader中获取当前像素阴影状态:
  3. //shadow_color [阴影factor], 还是1.0[表明不贡献阴影之factor]
  4. const float shadow_color = 0.3;
  5. const float depth_error = 0.005;
  6. //上面提到的那几个分割值,藏在xyz通道了
  7. uniform vec3 frustum_far;
  8. uniform sampler2DArray shadowmap;
  9. vec4 shadeFact()
  10. {
  11. int index = 3;
  12. //决定cascade,应用的shadowMap index
  13. //gl_FragCoord(当前pixel的x,y窗口坐标,z分量为深度)
  14. if(gl_FragCoord.z < frustum_far.x)
  15. {
  16. index = 0;
  17. }
  18. else if(gl_FragCoord.z < frustum_far.y)
  19. {
  20. index = 1;
  21. }
  22. else if(gl_FragCoord.z < frustum_far.z)
  23. {
  24. index = 2;
  25. }
  26. //转换像素位置参量pos, 到光源视觉(Croped)-纹理空间
  27. vec4 shadowTexcoord = gl_TextureMatrix[index] * pos;
  28. //对纹理投影,变换到纹理空间的场景坐标总作为TEXCOORD,这时就得自行为之“透视相除”了
  29. //小声:对正交投影其实是不必的。。。
  30. if(shadowTexcoord.w != 1.0)
  31. {
  32. shadowTexcoord = shadowTexcoord / shadowTexcoord.w;
  33. }
  34. //映射到(0~1)以进行纹理检索
  35. shadowTexcoord = 0.5 * shadowTexcoord + 0.5;
  36. //本像素的位置在当前空间(光源视觉(Croped)-纹理空间)的实际深度
  37. float realDepth = shadowTexcoord.z;
  38. //Texture Array 中以z分量选择纹理Layer(Shadow Map No.i)
  39. shadowTexcoord.z = float(index);
  40. //检索出Shadow Map中对应位置(x,y)的深度值
  41. float depth = texture2DArray(shadowmap, shadowTexcoord.xyz).x;
  42. //当 depth >= realDepth, 该位置所属caster 或 no-shadow领域, 输出阴影分量1.0[无阴影]
  43. //当 depth < realDepth, 该位置所属shadowed领域 , 输出阴影分量0.0[有阴影]
  44. float diff = depth - realDepth;
  45. //为了精度问题,如果差值diff是个很小很小的负量,把该量设定为1.0
  46. //当diff > -0.005(根据应用调节), 认为depth - realDepth >= 0.0[无阴影]
  47. diff = diff / depth_error + 1.0;
  48. return vec4(diff < 0.0 ? shadow_color : 1.0) ;
  49. }
// www.ZwqXin.com  Cascaded Shadow Maps//fragment shader中获取当前像素阴影状态://shadow_color [阴影factor], 还是1.0[表明不贡献阴影之factor]const float shadow_color = 0.3;const float depth_error = 0.005;//上面提到的那几个分割值,藏在xyz通道了uniform vec3 frustum_far; uniform sampler2DArray shadowmap;vec4 shadeFact(){   int index = 3;      //决定cascade,应用的shadowMap index   //gl_FragCoord(当前pixel的x,y窗口坐标,z分量为深度)   if(gl_FragCoord.z < frustum_far.x)    {     index = 0;   }   else if(gl_FragCoord.z < frustum_far.y)   {     index = 1;   }   else if(gl_FragCoord.z < frustum_far.z)   {     index = 2;   }        //转换像素位置参量pos, 到光源视觉(Croped)-纹理空间     vec4 shadowTexcoord = gl_TextureMatrix[index] * pos;     //对纹理投影,变换到纹理空间的场景坐标总作为TEXCOORD,这时就得自行为之“透视相除”了     //小声:对正交投影其实是不必的。。。     if(shadowTexcoord.w != 1.0)     {        shadowTexcoord = shadowTexcoord / shadowTexcoord.w;     }     //映射到(0~1)以进行纹理检索     shadowTexcoord = 0.5 * shadowTexcoord + 0.5;           //本像素的位置在当前空间(光源视觉(Croped)-纹理空间)的实际深度     float realDepth = shadowTexcoord.z;     //Texture Array 中以z分量选择纹理Layer(Shadow Map No.i)     shadowTexcoord.z = float(index);          //检索出Shadow Map中对应位置(x,y)的深度值    float depth =  texture2DArray(shadowmap, shadowTexcoord.xyz).x;    //当 depth >= realDepth, 该位置所属caster 或 no-shadow领域, 输出阴影分量1.0[无阴影]    //当 depth <  realDepth, 该位置所属shadowed领域           , 输出阴影分量0.0[有阴影]    float diff = depth - realDepth;    //为了精度问题,如果差值diff是个很小很小的负量,把该量设定为1.0    //当diff > -0.005(根据应用调节), 认为depth - realDepth >= 0.0[无阴影]    diff = diff / depth_error + 1.0;    return vec4(diff < 0.0 ? shadow_color : 1.0) ;}

最后是放出演示DEMO了吧:请看 [Shadow Map Demo2]

www.zwqxin.com  shadow map demo2

在该日志将展示DEMO并浅谈一下CSM一些小细节的地方,包括caster-receiver-splitedFrustum组合生成的SCREEN DEPENDENT的crop矩阵。最后是这段时间个人学习Shadow技法的小小总结。

抱歉!评论已关闭.