« OpenGL怎样近似进行同时到FBO和屏幕的渲染Shadow Volume 阴影锥技术之探Ⅵ »

Shadow Volume 阴影锥技术之探Ⅴ

本文是ZwqXin关于Shadow volume阴影锥技术实践记录的第五辑。Z-PASS之后,终于来到Z-FAIL了。Z-FAIL与Z-PASS最大的区别在于robustness,很明显地,在上次Z-PSS例子中你一旦进入阴影锥内部,将会“目空一切”。如果有时候你不得不“靠近阴影”,Z-PASS就不是适合的算法了(尽管它效率高),这时候,毫不犹豫地换用Z-FAIL吧。——ZwqXin.com  前文见:

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

其实,从Z-PASS转入Z-FAIL很简单(如果你不太追求的话),但是有一些地方还是得注意的。这也可以算是我给读者你的“前车之鉴”了。从这里你也可以看到Z-PASS与Z-FAIL实现的不同之处。以下地方可以参考我的shadow volume demo2例子的代码修改。

首先第一处,要求你清晰知道什么是Z-FAIL。Z-PASS按其名字解释,“Z测试(深度测试)通过的像素处,让其蒙板值变化(分正反面)”(具体见 ),那么照此推测,Z-FAIL则表示“Z测试(深度测试)不通过的像素处,让其蒙板值变化(分正反面)”。所以,这么看来很简单,改改Z-PASS版本代码的这里应该就可以了:把glStencilOp(GL_KEEP,GL_KEEP,****)改成glStencilOp(GL_KEEP,*****,GL_KEEP),****处改为Z-FAIL算法对应的变化值。没有那么简单呢。最近寻得GameDev那篇关于Shadow Volume技术概述的文章,里面关于Z-PASS和Z-FAIL的精简总结摘录如下:
  1. Render front face of shadow volume. If depth test passes, increment stencil value, else does nothing. Disable draw to frame and depth buffer.
  2. Render back face of shadow volume. If depth test passes, decrement stencil value, else does nothing. Disable draw to frame and depth buffer.

上面是Z-PASS的,下面是Z-FAIL的:

  1. Render back face of shadow volume. If depth test fails, increment stencil value, else does nothing. Disable draw to frame and depth buffer.
  2. Render front face of shadow volume. If depth test fails, decrement stencil value, else does nothing. Disable draw to frame and depth buffer.

看到了没?上面的讨论可以说都关注了黑体部分,但是忽略了(事实上我当时忽略了)顺序。Z-PASS要求先正面后背面(符合一般人认知顺序呵呵),但是Z-FAIL要求先背面后正面。所以代码应该这样改:

 
  1. //上面是针对人眼的(人换个位置看东西状况就不同了)。下面是针对阴影的(只有光和景物相对位置变动才会改变)
  2.  glStencilFunc(GL_ALWAYS,1, 0xFFFFFFF);
  3.  glStencilOp(GL_KEEP,GL_INCR,GL_KEEP);//接下来画的"新占像素"深度测试失败的,蒙板加1
  4.  
  5.  //这两个函数堆在一起,表示接下来画出来的东西会让它所占用的那些像素(这里干脆叫"新占像素"啦)的蒙板值增加1,稍微解释一下啦:
  6.  //1.glStencilFunc前两个参数用来作比较,譬如 GL_GREATER, 1的话就是"新占像素的蒙板值大于1才画出来",这里是GL_ALWAYS,1,前者就表示新像素总能通过测试而被画出了,所以后面那个参数其实没什么用途;
  7.  //最后那个参数,明眼人一看就知道是个16进制表示的二进制数啦,其实跟IP掩码一样的,但它掩的是像素值。化为二进制数后位为1的那部分才比较,这里既然全F(111……)就是说全比较啦。所以也是没用途的(都GL_ALWAYS了)
  8.  
  9.  //2.glStencilOp表示处理结果,参数分别指明了:没通过深度测试和蒙板测试的时候怎样;通过了蒙板测试却没通过深度测试时怎样;都通过了时又怎样.
  10.  //Z-FAIL检测深度测试不通过的像素,所以根据中间那个参数,"新占像素"蒙板值都加1了.
  11.  
  12.  //说了那么多,究竟让什么的蒙板加1啊?根据Z-fail算法,这里是让ShadowVolume面背朝我们的那些面加1;怎么判断哪些面背朝我们呢?阴影体画出来后你看不见的那些面咯(只渲染你所见的所有顺时针画出来的面)
  13.  
  14.  glFrontFace(GL_CW);//变为:看上去顺时针的才画出来 
  15.  ShadowVolume(true);//再渲染一次shadow volume
  16.  
  17. //按照Z-FAIL算法,接下来是:正面朝对你的面蒙板减1.这个类似的.
  18.  //glStencilFunc(GL_ALWAYS,1, 0xFFFFFFF);//这句一样的就不要写了,OPENGL会直接继承上面的设置的
  19.  glStencilOp(GL_KEEP,GL_DECR,GL_KEEP);   
  20.  glFrontFace(GL_CCW);//GL_CCW表示逆时针,只渲染看上去逆时针画的面 
  21.  ShadowVolume(true);//小函数卖面不卖线~

下一个要注意的地方,Capping(封口),而且是上下封口,让阴影锥(Shadow Volume)完全封闭。Z-PASS里不需要这样做,但是Z-FAIL要,因为这是完整的一套Z-FAIL算法(再见Ⅰ中的推荐文章:阴影锥(shadow volume)原理与展望---真实的游戏效果的实,或者你看上面GAMEDEV的英文文章也可),你要Robustness,你要眼睛在锥内锥外表现相当,就得封口,没什么好说的。怎么封呢?也很简单。前面不是有“朝光面判断”这个步骤吗?那些模型上所有朝光面在一起不就可以充当“上封口”了吗?把它与阴影锥侧面(silhouette edges)一样沿相应光线延伸,不就能充当“下封口”了吗?也许还有其他不这么费效率的做法,但既然你现在不太追求的话还是先别陷入那些优化方法里。注意,这里模型的朝光面(即相应的顶点)共被渲染了3次:第一次是模型本身的;第2,3次是作为阴影锥的一部分(封口)而渲染的。在画阴影锥的函数里,加上它们就可以了。

  1.  void CShadow3DS::ShadowVolume(bool apply)
  2. {......
  3. for(vector<t3DObject>::iterator p_r = GetModel().pObject.begin(); p_r!=GetModel().pObject.end(); p_r++)
  4.  {
  5. ...........//渲染侧边silhouette edges
  6. glPushMatrix();
  7.     glBegin(GL_TRIANGLES);  
  8.        glColor4f(0.0f ,0.2f, 0.9f, 0.2f);
  9.     for(int k = 0 ; k< p_r->numOfFaces; k++)
  10.      if(psObject[iter_obj].psFaces[k].facelight == true)
  11.         for(int l = 0 ; l<3; l++)
  12.         {
  13.          int vert = p_r->pFaces[k].vertIndex[l];
  14.             glVertex3f(p_r->pVerts[vert].x,       p_r->pVerts[vert].y,       p_r->pVerts[vert].z);
  15.         }
  16.       glEnd();
  17. glPopMatrix();
  18.  
  19.  glPushMatrix();
  20.    glBegin(GL_TRIANGLES); 
  21.        glColor4f(0.0f ,0.2f, 0.9f, 0.2f);
  22.     for(int m = 0 ; m< p_r->numOfFaces; m++)
  23.      if(psObject[iter_obj].psFaces[m].facelight == true)
  24.         for(int n = 2 ; n>=0; n--)
  25.         {          
  26.             int vert = p_r->pFaces[m].vertIndex[n];
  27.             CVector3 lv;
  28.                      lv.set(p_r->pVerts[vert].x - mvLightPos.x, 
  29.                             p_r->pVerts[vert].y - mvLightPos.y,
  30.                             p_r->pVerts[vert].z - mvLightPos.z);
  31.             lv = lv * LenghtAdjust;
  32.             glVertex3f(p_r->pVerts[vert].x+lv.x, p_r->pVerts[vert].y+lv.y, p_r->pVerts[vert].z+lv.z);
  33.         }
  34.       glEnd();
  35. glPopMatrix();  
  36. }
  37. }

下封口中for(int n = 2 ; n>=0; n--) 这里正是我第2个要提醒你的地方。为什么要反转顶点绕序来画呢?还记得假设吗?逆时针为正。原本“朝光面”中在我们看来是正面的那些面,被投射到远处后依然是“朝光面”,但是记住,阴影锥各面的法向量是向外的,也就是说假如转入阴影锥可见模式,我们在锥外看锥,看到的都应该是正面(这也是为什么之前画阴影锥侧面矩形(silhouette edges)我要强调逆时针绕序)。所以这里对被投射的“朝光面”要让它的三角面片绕序反转,好让正面“朝外”。不然就算不上做好“封底”,你可以看到反转绕向前后的区别(底根本没被封上,这错误也让我找出了这个注意点):


shadow volume demo3

shadow volume demo3

好了,改成正常模式渲染吧。等等,怎么模型整个“黑”掉了?呵呵,这是因为你做了上封口,上封口以下都会处于阴影中的,而上封口是与模型“朝光面”重合的,因此边界处就出现了纠纷:边界不应该被“黑”嘛,所以,在上面Z-FAIL算法前面,把那个深度测试glDepthFunc(GL_LEQUAL)改成glDepthFunc(GL_LESS)吧。本来是与正常渲染一样小于等于原深度才“算数”,改成“小于”好了,最外面那层不被影响了,没问题了。ZPASS到Z-FAIL的改造完成,现在眼睛进阴影锥里也没问题了!

shadow volume demo3

代码不单独发布了,因为后面将要介绍的shader版本(大致实现了)也就用了Z-FAIL算法,只不过封口方式不一样罢了。详情见:Shadow Volume 阴影锥技术之探Ⅵ(vertex shader ver.)

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

发表评论:

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

IE下本页面显示有问题?

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

日历

Search

网站分类

最新评论及回复

最近发表

Powered By Z-Blog 1.8 Walle Build 100427

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