本文是ZwqXin关于Shadow volume阴影锥技术实践记录的第五辑。Z-PASS之后,终于来到Z-FAIL了。Z-FAIL与Z-PASS最大的区别在于robustness,很明显地,在上次Z-PSS例子中你一旦进入阴影锥内部,将会“目空一切”。如果有时候你不得不“靠近阴影”,Z-PASS就不是适合的算法了(尽管它效率高),这时候,毫不犹豫地换用Z-FAIL吧。——ZwqXin.com 前文见:
本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
原文地址:http://www.zwqxin.cn/archives/opengl/shadow-volume-5.html
其实,从Z-PASS转入Z-FAIL很简单(如果你不太追求的话),但是有一些地方还是得注意的。这也可以算是我给读者你的“前车之鉴”了。从这里你也可以看到Z-PASS与Z-FAIL实现的不同之处。以下地方可以参考我的shadow volume demo2例子的代码修改。
- Render front face of shadow volume. If depth test passes, increment stencil value, else does nothing. Disable draw to frame and depth buffer.
- 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的:
- Render back face of shadow volume. If depth test fails, increment stencil value, else does nothing. Disable draw to frame and depth buffer.
- 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要求先背面后正面。所以代码应该这样改:
- //上面是针对人眼的(人换个位置看东西状况就不同了)。下面是针对阴影的(只有光和景物相对位置变动才会改变)
- glStencilFunc(GL_ALWAYS,1, 0xFFFFFFF);
- glStencilOp(GL_KEEP,GL_INCR,GL_KEEP);//接下来画的"新占像素"深度测试失败的,蒙板加1
- //这两个函数堆在一起,表示接下来画出来的东西会让它所占用的那些像素(这里干脆叫"新占像素"啦)的蒙板值增加1,稍微解释一下啦:
- //1.glStencilFunc前两个参数用来作比较,譬如 GL_GREATER, 1的话就是"新占像素的蒙板值大于1才画出来",这里是GL_ALWAYS,1,前者就表示新像素总能通过测试而被画出了,所以后面那个参数其实没什么用途;
- //最后那个参数,明眼人一看就知道是个16进制表示的二进制数啦,其实跟IP掩码一样的,但它掩的是像素值。化为二进制数后位为1的那部分才比较,这里既然全F(111……)就是说全比较啦。所以也是没用途的(都GL_ALWAYS了)
- //2.glStencilOp表示处理结果,参数分别指明了:没通过深度测试和蒙板测试的时候怎样;通过了蒙板测试却没通过深度测试时怎样;都通过了时又怎样.
- //Z-FAIL检测深度测试不通过的像素,所以根据中间那个参数,"新占像素"蒙板值都加1了.
- //说了那么多,究竟让什么的蒙板加1啊?根据Z-fail算法,这里是让ShadowVolume面背朝我们的那些面加1;怎么判断哪些面背朝我们呢?阴影体画出来后你看不见的那些面咯(只渲染你所见的所有顺时针画出来的面)
- glFrontFace(GL_CW);//变为:看上去顺时针的才画出来
- ShadowVolume(true);//再渲染一次shadow volume
- //按照Z-FAIL算法,接下来是:正面朝对你的面蒙板减1.这个类似的.
- //glStencilFunc(GL_ALWAYS,1, 0xFFFFFFF);//这句一样的就不要写了,OPENGL会直接继承上面的设置的
- glStencilOp(GL_KEEP,GL_DECR,GL_KEEP);
- glFrontFace(GL_CCW);//GL_CCW表示逆时针,只渲染看上去逆时针画的面
- ShadowVolume(true);//小函数卖面不卖线~
下一个要注意的地方,Capping(封口),而且是上下封口,让阴影锥(Shadow Volume)完全封闭。Z-PASS里不需要这样做,但是Z-FAIL要,因为这是完整的一套Z-FAIL算法(再见Ⅰ中的推荐文章:阴影锥(shadow volume)原理与展望---真实的游戏效果的实现,或者你看上面GAMEDEV的英文文章也可),你要Robustness,你要眼睛在锥内锥外表现相当,就得封口,没什么好说的。怎么封呢?也很简单。前面不是有“朝光面判断”这个步骤吗?那些模型上所有朝光面在一起不就可以充当“上封口”了吗?把它与阴影锥侧面(silhouette edges)一样沿相应光线延伸,不就能充当“下封口”了吗?也许还有其他不这么费效率的做法,但既然你现在不太追求的话还是先别陷入那些优化方法里。注意,这里模型的朝光面(即相应的顶点)共被渲染了3次:第一次是模型本身的;第2,3次是作为阴影锥的一部分(封口)而渲染的。在画阴影锥的函数里,加上它们就可以了。
- void CShadow3DS::ShadowVolume(bool apply)
- {......
- for(vector<t3DObject>::iterator p_r = GetModel().pObject.begin(); p_r!=GetModel().pObject.end(); p_r++)
- {
- ...........//渲染侧边silhouette edges
- glPushMatrix();
- glBegin(GL_TRIANGLES);
- glColor4f(0.0f ,0.2f, 0.9f, 0.2f);
- for(int k = 0 ; k< p_r->numOfFaces; k++)
- if(psObject[iter_obj].psFaces[k].facelight == true)
- for(int l = 0 ; l<3; l++)
- {
- int vert = p_r->pFaces[k].vertIndex[l];
- glVertex3f(p_r->pVerts[vert].x, p_r->pVerts[vert].y, p_r->pVerts[vert].z);
- }
- glEnd();
- glPopMatrix();
- glPushMatrix();
- glBegin(GL_TRIANGLES);
- glColor4f(0.0f ,0.2f, 0.9f, 0.2f);
- for(int m = 0 ; m< p_r->numOfFaces; m++)
- if(psObject[iter_obj].psFaces[m].facelight == true)
- for(int n = 2 ; n>=0; n--)
- {
- int vert = p_r->pFaces[m].vertIndex[n];
- CVector3 lv;
- lv.set(p_r->pVerts[vert].x - mvLightPos.x,
- p_r->pVerts[vert].y - mvLightPos.y,
- p_r->pVerts[vert].z - mvLightPos.z);
- lv = lv * LenghtAdjust;
- glVertex3f(p_r->pVerts[vert].x+lv.x, p_r->pVerts[vert].y+lv.y, p_r->pVerts[vert].z+lv.z);
- }
- glEnd();
- glPopMatrix();
- }
- }
下封口中for(int n = 2 ; n>=0; n--) 这里正是我第2个要提醒你的地方。为什么要反转顶点绕序来画呢?还记得假设吗?逆时针为正。原本“朝光面”中在我们看来是正面的那些面,被投射到远处后依然是“朝光面”,但是记住,阴影锥各面的法向量是向外的,也就是说假如转入阴影锥可见模式,我们在锥外看锥,看到的都应该是正面(这也是为什么之前画阴影锥侧面矩形(silhouette edges)我要强调逆时针绕序)。所以这里对被投射的“朝光面”要让它的三角面片绕序反转,好让正面“朝外”。不然就算不上做好“封底”,你可以看到反转绕向前后的区别(底根本没被封上,这错误也让我找出了这个注意点):
好了,改成正常模式渲染吧。等等,怎么模型整个“黑”掉了?呵呵,这是因为你做了上封口,上封口以下都会处于阴影中的,而上封口是与模型“朝光面”重合的,因此边界处就出现了纠纷:边界不应该被“黑”嘛,所以,在上面Z-FAIL算法前面,把那个深度测试glDepthFunc(GL_LEQUAL)改成glDepthFunc(GL_LESS)吧。本来是与正常渲染一样小于等于原深度才“算数”,改成“小于”好了,最外面那层不被影响了,没问题了。ZPASS到Z-FAIL的改造完成,现在眼睛进阴影锥里也没问题了!
代码不单独发布了,因为后面将要介绍的shader版本(大致实现了)也就用了Z-FAIL算法,只不过封口方式不一样罢了。详情见:Shadow Volume 阴影锥技术之探Ⅵ(vertex shader ver.)
本文来源于 ZwqXin (http://www.zwqxin.cn/), 转载请注明
原文地址:http://www.zwqxin.cn/archives/opengl/shadow-volume-5.html