打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
Shadow Map阴影贴图技术之探

上篇(Ⅲ)里的最后最后,提及了几种比较有名的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]

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


本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
关于 UI 中的投影,该如何更好的处理!
花半天做完的彩平、效果图'投影'太假,让我试试一键投影!
Unity3D
shadow滤镜代码解析及表达方式【投射阴影滤镜】
PS教程连载第65课:图层样式之内阴影讲解
Ae:灯光选项
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服