所谓planar shadow就是用一个投影矩阵(不是opengl流水线所谓之projection matrix,而是根据光源和投影面位置将一个点投射到投影面上的矩阵),将模型的所有顶点投射到投影面(比如地面)上。一般或者直接使用模型经过skin动画计算好的mesh进行投影并绘制,或者使用一个低模独立计算mesh然后投影绘制。无论哪种方法,都有一个问题,由于影子是很多三角形重叠在一起的,如果使用半透明材质绘制,众多的三角形会重叠在一起非常难看,且会有z-fighting现象闪烁不止。。当然如果有stencil buffer,可以使用stencil buffer进行mask,如果没有呢?确实在某些平台上没有stencil buffer,往往只能通过depth buffer做一些有限的模拟,本文所提的方法就是一例。
------------------------------------------
绘制过程:
1)首先绘制场景里所有其他东西
2)用0清除depth buffer:
glClearDepth(0);
glClear(GL_DEPTH_BUFFER_BIT);
3)确保深度测试开启,z write允许,并且设置depth func为always
glEnable(GL_DEPTH_TEST);
glDepthMask(GL_TRUE);
glDepthFunc(GL_ALWAYS);
4)禁止color buffer写入:
glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE );
5)绘制影子(solid材质,无纹理,无blend)
经过上面的设置,实际上只会更新depth buffer, 写入z buffer时,默认的depth range是0~1,所以没有影子的地方,z buffer中是0,有影子的地方是一个大于0的值,因为影子一般不可能离near plane很近很近,所以这个值应该>0。
6)将model-view和projection矩阵设为单位阵
glMatrixMode (GL_MODELVIEW);
glLoadIdentity ();
glMatrixMode (GL_PROJECTION);
glLoadIdentity ();
7)重新开启color buffer写入,depth func为LEQUAL, 并关闭depth buffer的写入
8)绘制一个全屏的矩形,开启blend,设置矩形顶点颜色为半透明黑色
矩阵顶点坐标为:
glVertex3i (-1, -1, -0.99);
glVertex3i (1, -1, -0.99);
glVertex3i (1, 1, -0.99);
glVertex3i (-1, 1, -0.99);
绘制这个矩形时进行深度测试LEQUAL,z值-0.99是clip space中的值,通过depth range的映射,最终的z值是>0但很接近0的值,而影子应该是离near plane比这个面更远的(即z值比这个面的z值要大),所以经过LEQUAL的测试,画这个矩形的时候,z buffer上有影子的部分通过深度测试,而z buffer为0的地方不通过所以不绘制。而且由于启用了blend,矩形的像素还要和color buffer上的已有颜色进行混合,达到半透明效果。这样半透明的矩形被影子mask出来了,就得到了半透明的影子。
另外,为啥是-0.99而不是-1呢?因为clip space, -1就是在near clip plane上了,窗口坐标映射后的z值是0,0==0就能通过LEQUAL的测试了就会全部画出来了。。但是如果使用LESS测试,-1也就可以用了。
注:以上代码仅作参考,实际调用的都是引擎的接口
当然这个方法不完美,毛病也很多,比如影子会遮住人的脚,平台边缘无法裁掉影子等
ps:复习一下向z buffer写入z值的整个过程:此Z非x,y,z的z,而是eye space, clip space, NDC, windows space的z。通过modle-view变换,相机被变换到默认位置,即view为-z,此为eye space。我们在世界坐标系下,往往采用z up,所以此z非彼z,无论世界坐标z表示什么,到eye space之后,z值就表示深度了,所谓深度就是指离camera的距离。从eye space经过投影变换,到达clip space, clip space中(x,y,z)都被变换到[-w,w]的范围,再经过透视除法,x,y,z都除以w,使得x,y,z都在[-1,1]之间这就是NDC了(归一化坐标)。然后再经过viewport变换,变换到窗口坐标,此时z值会根据depth range进行映射,默认的是映射为[0,1]。clip,透视除法和视口变换都属于图元装配过程,之后是光栅化阶段,图元被光栅化为片段(fragment),在光栅化阶段,为了降低zfighting现象,可以使用polygon offset, polygon offset是在进行深度测试和写入zbuffer之前,将计算好的z值加上一个偏移量,如果深度测试通过,会将原始深度值(而不是加上offset之后的z值)写入zbuffer。也就是说无论是否使用polygon offset,最终写入zbuffer的z值是视口变换之后通过depth range映射的windows coordinate z值,但polygon offset会对depth test产生影响。