« Shadow Map阴影贴图技术之探ⅢShadow Map Demo2 »

Shadow Map阴影贴图技术之探Ⅳ

这里是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.  
  6.    splitfrust[0].ResightNear(Dis_Near); //开始分割
  7.  
  8.    float partisionFactor = 0.0;
  9.    float lerpValue1 = 0.0, lerpValue2 = 0.0;
  10.    float SplitsZ = 0.0;
  11.  
  12.     for(int i = 1; i < NumofSplits; ++i)
  13.     {
  14.         partisionFactor = i / (float)NumofSplits;
  15.  
  16.         lerpValue1 = Dis_Near + partisionFactor * (Dis_Far - Dis_Near);
  17.  
  18.         lerpValue2 = Dis_Near * powf(distance_scale, partisionFactor);
  19.  
  20.         // 分割面的Z值. 1.005f防止前一个子视锥的远裁切面与后一个子视锥的近裁切面冲突
  21.         SplitsZ =  (1-strength) * lerpValue1  + strength * lerpValue2; 
  22.                    
  23.         splitfrust[i].ResightNear(SplitsZ * 1.002f);
  24.         splitfrust[i-1].ResightFar(SplitsZ);
  25.     }
  26.  
  27.     splitfrust[NumofSplits-1].ResightFar(Dis_Far);//结束分割
  28. }

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.   
  6.     CMatrix16 CurrentMatrix;//当前矩阵 
  7.     CMatrix16 CropMatrix;//协调光源视野与视锥的Crop Matrix                      
  8.   
  9.     //光源视图矩阵 
  10.     glGetFloatv(GL_MODELVIEW_MATRIX, CurrentMatrix.mt); 
  11.   
  12.       //生成视锥的AABB特征向量,视锥先经CurrentMatrix变换到光源视图空间 
  13.     GetFrustumAABBCoords(frust, maxFrustumCoord, minFrustumCoord, &CurrentMatrix); 
  14.   
  15.      //计算给Crop Matrix的调整参数 
  16.     float scaleX = 2.0f/(maxFrustumCoord.x - minFrustumCoord.x); 
  17.     float scaleY = 2.0f/(maxFrustumCoord.y - minFrustumCoord.y); 
  18.     float offsetX = -0.5f*(maxFrustumCoord.x + minFrustumCoord.x) * scaleX; 
  19.     float offsetY = -0.5f*(maxFrustumCoord.y + minFrustumCoord.y) * scaleY; 
  20.   
  21.     CropMatrix = CMatrix16(scaleX,    0.0f,  0.0f, 0.0f, 
  22.                              0.0f,  scaleY,  0.0f, 0.0f, 
  23.                              0.0f,    0.0f,  1.0f,  0.0f, 
  24.                           offsetX, offsetY,  0.0f,  1.0f ); 
  25.   
  26.    //CropProjectMatrix(光源投影矩阵 = CropMatrix*ProjectZMatrix) 
  27.      glLoadIdentity(); 
  28.      glLoadMatrixf(CropMatrix.mt); 
  29.      //以max_Z和min_Z作为远近裁切面的正投影矩阵  
  30.      glOrtho(-1.0, 1.0, -1.0, 1.0, -maxFrustumCoord.z, -minFrustumCoord.z ); 
  31.   
  32. }

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.  
  6.     minFrustumCoord.z = -1.0f;
  7.  
  8.      //计算给Crop Matrix的调整参数
  9.     float scaleX = 2.0f/(maxFrustumCoord.x - minFrustumCoord.x);
  10.     float scaleY = 2.0f/(maxFrustumCoord.y - minFrustumCoord.y);
  11.     float scaleZ  = 2.0f / (maxFrustumCoord.z - minFrustumCoord.z);
  12.     float offsetX = -0.5f*(maxFrustumCoord.x + minFrustumCoord.x) * scaleX;
  13.     float offsetY = -0.5f*(maxFrustumCoord.y + minFrustumCoord.y) * scaleY;
  14.     float offsetZ = -0.5f*(maxFrustumCoord.z + minFrustumCoord.z) * scaleZ;
  15.  
  16.   
  17.     CropMatrix = CMatrix16(scaleX,    0.0f,  0.0f, 0.0f, 
  18.                              0.0f,  scaleY,  0.0f, 0.0f, 
  19.                              0.0f,    0.0f,  scaleZ,  0.0f, 
  20.                           offsetX, offsetY,  0.0f,  1.0f ); 
  21. //....

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

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

www.zwqxin.com  shadow map demo2

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

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

  • quote 1.天宇
  • 你好/很感谢你认真做的笔记/让我收获很大。。
    不过还是不懂该怎么去划分视锥。。

    例如我的camera 位置在0,0,0, 朝向指向+z,near为1,far为4000。
    分割4个的话,要怎么给你的computeSplites传参数呢?

    computeSplites(float strength,float dis_near,float dis_far)

  • 2010-5-12 17:01:42 回复该留言
  • quote 2.老盖
  • 那个,我设置好了还是不行,不能执行。demo1可以执行,我也设置了glew。冒昧的问一下能不能把完整的代码传给我能呢。最后再次感谢,一直看你的blog,很有收获48
    zwqxin 于 2010-7-1 21:41:26 回复
    可能是显卡不支持吧? 这样的话可以考虑另辟方法咯
  • 2010-6-28 19:09:04 回复该留言
  • quote 3.xx
  • http://www.baidu.com
  • 不错,以前闲下来思考过全场景阴影的问题,一直没思路,也没时间去看论文。偶尔这里逛了下,然后弄下了NV的那PDF看了下,还行~
    现在年轻人比我们老一辈强多了。
  • 2010-7-5 17:57:42 回复该留言
  • quote 4.blw
  • 您好,我现在也正在做cascade shadow map,遇到了一些问题,请问您能不能把代码发给参看参考啊?万分感谢
  • 2012-8-9 10:57:30 回复该留言
  • quote 5.smn
  • 你好,我现在对csm算法有所了解了,,能否把代码发到我邮箱,谢谢。
  • 2013-1-17 13:36:39 回复该留言
  • quote 6.waljx
  • 大神,请问为什么我Attach多个shader然后link时总会提示重复定义main的错误,一次只能连接成功一个vert和frag。。。有什么地方需要注意吗
    zwqxin 于 2013-2-14 19:14:10 回复
    一次渲染过程只能对应最多一个vert和一个frag,多了的话必然链接shader对象失败的说
  • 2013-2-14 10:09:00 回复该留言
  • quote 7.Renyq
  • 感谢阁下,我也一直纠结中:
    我不明白那个切分函数为什么分出的第一块是正确的,其他就错了呢?能否请阁下明示?谢谢!
  • 2013-4-12 16:29:28 回复该留言

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

IE下本页面显示有问题?

→点击地址栏右侧【兼容视图】←

日历

Search

网站分类

最新评论及回复

最近发表

Powered By Z-Blog 1.8 Walle Build 100427

Copyright 2008-2013 ZwqXin. All Rights Reserved. Theme edited from ipati.