永远也不完美的程序

不断学习,不断实践,不断的重构……

常用链接

统计

积分与排名

好友链接

最新评论

Cascaded shadow map(转)

转自:http://class.gd/content/shadow-map%E9%98%B4%E5%BD%B1%E8%B4%B4%E5%9B%BE%E6%8A%80%E6%9C%AF%E4%B9%8B%E6%8E%A2%E2%85%A2

本文来源:http://www.zwqxin.com/archives/opengl

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

01 // <a href="http://www.ZwqXin.com" title="www.ZwqXin.com">www.ZwqXin.com</a>  Cascaded Shadow Maps
02 void CCascadingSM::ComputeSplits(float strength, float Dis_Near, float Dis_Far)
03 {
04    float distance_scale = Dis_Far / Dis_Near;
05    
06    splitfrust[0].ResightNear(Dis_Near); //开始分割
07    
08    float partisionFactor = 0.0;
09    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正交投影矩阵。

01 // <a href="http://www.ZwqXin.com" title="www.ZwqXin.com">www.ZwqXin.com</a>  Cascaded Shadow Maps 
02 void CCascadingSM::ApplyCropProjectMatrix(CFrustum &frust) 
03 {  
04     CVector3 maxFrustumCoord, minFrustumCoord; 
05     
06     CMatrix16 CurrentMatrix;//当前矩阵 
07     CMatrix16 CropMatrix;//协调光源视野与视锥的Crop Matrix                      
08     
09     //光源视图矩阵 
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。

01 // <a href="http://www.ZwqXin.com" title="www.ZwqXin.com">www.ZwqXin.com</a>  Cascaded Shadow Maps  
02     CVector3 maxFrustumCoord, minFrustumCoord; 
03   //.....
04     GetFrustumAABBCoords(frust, maxFrustumCoord, minFrustumCoord, &CurrentMV);
05    
06     minFrustumCoord.z = -1.0f;
07    
08      //计算给Crop Matrix的调整参数
09     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 阴影图了吧。

01 // <a href="http://www.ZwqXin.com" title="www.ZwqXin.com">www.ZwqXin.com</a>  Cascaded Shadow Maps
02 //fragment shader中获取当前像素阴影状态:
03 //shadow_color [阴影factor], 还是1.0[表明不贡献阴影之factor]
04    
05 const float shadow_color = 0.3;
06 const float depth_error = 0.005;
07 //上面提到的那几个分割值,藏在xyz通道了
08 uniform vec3 frustum_far; 
09 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了吧

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



posted on 2010-11-24 10:20 狂烂球 阅读(4737) 评论(0)  编辑 收藏 引用 所属分类: 图形编程


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理