阴影生成技术

目前普遍采用的一般有三种:Planar Shadow、Shadow Mapping和Shadow Volume,前者类似投影,计算最简单,缺点只能绘制抛射在平面上的阴影;Shadow mapping利用站在光源处所沿光源法线看去所生成的深度图来检测场景中的体象素是否处于阴影中,缺点是光源与物体位置相对固定、且在极端情况下计算精度差,不太适合精确到象素的动态光阴场合;Shadow Volume是目前最适合精确表现动态光阴场景的技术,适用性最广,其典型的适用范例便是Doom 3,不足在于阴影体积引入了额外的顶点和面,加大了存储和处理强度,同时渲染出的阴影比较硬,如果要实现软阴影,仍需其他技术配合。

这里我们快速往前跳,Perspective projection、Depth test、Stencil buffer等概念就不多谈了。Shadow Volume的一般步骤为:生成阴影体积(Mesh)和阴影渲染,阴影体积生成算法又分两种,一种是D3D SDK sample中所采用的方法,先分离/插补物体面,然后用Vertex Shader加速,对复制所得的未拉伸的阴影体积进行沿光线方向的Extrusion;另一种则是多数tutorial中常用的Software Renderer方式,首先拣选出所有向光面,将每一个向光面的所有边均加入一个list,每加入一个边时,如果list中已经存在相同的边了,则不加入,同时将list中相同的边删除,这样处理完所有的向光面后,list中便只会剩下物体向光面轮廓的边信息,将该轮廓沿光矢量方向进行延伸,然后完成侧面Quad插补和前后封口(如果使用的是Z-fail)即可。而对于阴影渲染,则一般用4个pass,具体有Z-Pass/Z-Fail两种做法:

Z-pass算法
1. 先关闭光源,将整个scence渲染一遍,此时一片漆黑,但获得了深度值
2. 关闭深度写,渲染阴影体的正面,深度测试通过则模板值加1
3. 然后渲染阴影体的背面,深度测试通过则模板值减1
4. 最后模板值不为0的面就在阴影体中,开启深度写
5. 用模板手法重新渲染一次加光的scence即可,让阴影部分为黑色
6. 其致命缺点是当视点在阴影中时,会导致模板计数错误
7. 同时,有可能因为Z-near clip plane过近而导致模板计数错误

Z-fail算法(John Carmack's Reverse)
1. 先关闭光源,将整个scence渲染一遍,获得深度值
2. 关闭深度写,渲染阴影体的背面,深度测试失败则模板值加1
3. 渲染阴影体的正面,深度测试失败则模板值减1
4. 最后模板值不为0的面便处于阴影体中,开启深度写
5. 用模板手法重新渲染一次加光的scence即可,阴影部分不渲染色度
6. 注意,该算法要求阴影体积是闭合的,即需要前后封口
7. 该方法不是没有缺陷的,有可能因为Z-far clip plane过近而导致模板计数错误

值得注意的方面
1. Z-pass由于不用封口,因此速度比Z-fail快,但存在处理不了的情况
2. Quake 3貌似使用的是z-pass shadow volume和planar shadow
3. 为保证足够robust,必须确保z-near/z-far中至少一个不出问题,Nvidia的论文推荐采用z-fail,用w=0来实现无穷远的z-far平面
4. 记住使用Z-fail一定要封口,而且阴影体积的每个面的法线必须正确地指向物体之外,包括front cap和back cap
5. 上面给出的是简单过程,阴影很硬,可以稍微变通一下:先以环境光渲染一遍,然后计算模板,再用模板渲染打光的scene,最后以alpha blend加深阴影

下面开始大面积地贴图,首先来看个Z-fail的具体例子(z-pass简单,就不举例了):从外面看一个面向光源的单面的情况。图中,虚线框起来的为阴影体积,圆形为点光源,其中,面ADFB和面DEF是back faces,而面ABC、ACED和CBFE则是三个front faces(注意这里顶点顺序用的是左手系),为说明问题(主要是为了说明在计算阴影体积时如何避免z-fighting),我故意把面ABC画在了黄色三角形下面一点点的位置,而且其中的灰色阴影是与平面共面的,而面DEF则处于平面下面一点点的地方。

a.png

首先进行2个back faces的depth test:粉红色的是stencil buffer中因Z- fail而加1的区域;然后进行3个front faces的depth test:亮绿色的是stencil buffer中因Z- fail而减1的区域

b.png

最后红绿区域正负相抵,stencil buffer中只剩下平面上的灰色三角区域中的值不为0,即原图阴影所在的位置。上面是个从单面外看的例子,值得注意的是,单面与两个背靠背双面的情况是不一样的,那么后者的计算结果是不是也是正常的呢?继续看图:

1.png

上图的红、绿细线分别表示两个背靠背单面,而粗线则构成了体积阴影,红粗线、绿粗线分别表示红面、绿面沿光线方向的的投影,一般而言,back cap处于无穷远处,而front cap则值得注意,为避免z-fighting,它的位置应该是在红面之后、绿面之前,也许你会说:“hey, 这里的front cap完全可以直接等于红面嘛”,是的,当在eye看来红面是背面时的确可以这么做,可当eye位置变化、红面不再是背面时,简单的以红面为front cap便会导致z-fighting。从图上可看出,当eye处于阴影内部时,模板计数也是正确的。下面再来看看仅有一个单面、且处于阴影内部的情况:

2.png

只要front cap注意了z-fighting的情况,其计算结果也是正确的,有意思的是,实际上当eye处于阴影内部时,在back culling的时候,三角形的背面便已经被去除了,因此在实际渲染时,在三角面的背面处并不会存在其深度信息,但即便如此,z-fail的模板计数仍是正确的。

3.png

最后,再来看一个计算方块的阴影的例子,可以看出,当front cap为背面时,阴影体积的计算很简单,无需考虑其z-fighting的情况,直接用方块的向光面作为front cap即可;但当front cap为正面时,还是面临着位置需要微调的问题。

总结一下,用z-fail方式来计算没有厚度的面的阴影和具有厚度的物体的阴影还是有点微妙区别的;在计算阴影体积时,要注意避免front cap处z-fighting的情况,稍微调整一下front cap的位置,即将front cap沿光线方向稍微向后挪那么一点点;或者是.... 等等,还有一种更简洁优雅的办法,即将Z-fail算法中的"Depth Test失败"理解成象素深度">="所在位置的深度而不仅仅只是">",将ZBufferFunction设为Campare.Less而不是Campare.LessEqual,这样的话便完全避开了z-fighting的问题,微调什么的都可以免了,无论是面还是物体,在任何情况下都可以直接用物体向光面作为front cap

 

 现在比较普遍的实现方法有三种:Z Pass、Z Fail 和DirectX Samples 里面介绍的那种(其实Z Fail只是Z Pass的改进版)。这里只介绍Z Pass的实现方法。如下图所示,有一个三角形abc和它投到地面的影子三角形def。从摄像机分别看A、B、C三处,明显只有B是处在三角形的阴影中。

    

 

       那么如何通过程序来判断一个点是否在阴影中呢?在多面体abcdef中,从视点看A点时,视线没有经过多面体;从视点看B点时,视线经过多面体正对视点的一个面abde;从视点看C点,视线首先经过多面体正对视点的面abde,然后经过背对视点的面bcef,然后才到达C点。由此可得,当视线只经过阴影多面体正对面而没经过多面体背对面时,所看到的点就在阴影中。当场景中有多个阴影体时,我们可以认为当视点经过阴影多面体正对面次数大于经过背对面次数时,看到的点就在阴影中。

       在程序中实现的时候,需要做以下几步操作:

1) 为有阴影的模型生成它的阴影多面体。

遍历模型的每一个面,如果该面是正对光源的,就把它的三个边添加到一个列表中。

如果发现某一个边已经在列表中,则不再添加该边,并且把列表中的该边剔除。当遍历完整个模型的所有三角形后,列表中剩下的边就是阴影的边缘。

2) 关闭光照,渲染一次场景,生成一个具有场景深度信息的表面。

3) 对阴影多面体的正对面进行深度测试,如果测试通过,则模版缓存加1。

4) 对阴影多面体的背对面进行深度测试,如果测试通过,则模版缓存减1,这时我们获得了一个模版缓存stencil_1。

5) 打开光照,根据所得的模版缓存stencil_1,再次渲染场景。如果stencil_1的值跟这一次渲染场景的模版值相等,则渲染场景;否则,保留表面原色(阴影)。

这样,阴影就渲染出来了。第二步到第五步都涉及到深度测试和模版测试的问题,如果对这两种测试不太熟悉的话,实现起来是一件很头痛的事情。下面附上一段代码,用以说明从第二步到第五步的深度测试和模版测试。

 

 

g_pDev->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_ARGB( 0, 66, 75, 121), 1.0f, 0);

     g_pDev->BeginScene();

     //关闭光源并渲染场景

     g_pDev->SetRenderState(D3DRS_LIGHTING, TRUE);

     g_pDev->LightEnable(0, FALSE);  

     RenderScene();

     //把模版缓存设为0

     g_pDev->Clear(0, NULL, D3DCLEAR_STENCIL, D3DCOLOR_ARGB( 0, 66, 75, 121), 1.0f, 0);

     //备份渲染设置

     DWORD CullMode, AlphaBlendEnable, SrcBlend, DestBlend, ZFunc, StencilRef, StencilMask, StencilWriteMask, StencilFunc, StencilZFail, StencilFail, StencilPass;

     g_pDev->GetRenderState(D3DRS_CULLMODE, &CullMode);

     g_pDev->GetRenderState(D3DRS_ALPHABLENDENABLE, &AlphaBlendEnable);

     g_pDev->GetRenderState(D3DRS_SRCBLEND, &SrcBlend);

     g_pDev->GetRenderState(D3DRS_DESTBLEND, &DestBlend);

     g_pDev->GetRenderState(D3DRS_ZFUNC, &ZFunc);

     g_pDev->GetRenderState(D3DRS_STENCILREF, &StencilRef);

     g_pDev->GetRenderState(D3DRS_STENCILMASK, &StencilMask);

     g_pDev->GetRenderState(D3DRS_STENCILWRITEMASK, &StencilWriteMask);

     g_pDev->GetRenderState(D3DRS_STENCILFUNC, &StencilFunc);

     g_pDev->GetRenderState(D3DRS_STENCILZFAIL, &StencilZFail);

     g_pDev->GetRenderState(D3DRS_STENCILFAIL, &StencilFail);

     g_pDev->GetRenderState(D3DRS_STENCILPASS, &StencilPass);

 

     //设置第一轮z pass 渲染

     g_pDev->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW); //渲染逆时针三角形(即正对摄像机的三角形)

     g_pDev->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);

     g_pDev->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_ZERO); //使阴影网格全透明

     g_pDev->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_ONE);

     g_pDev->SetRenderState(D3DRS_ZWRITEENABLE, FALSE); //禁止阴影网格改变场景深度

     g_pDev->SetRenderState(D3DRS_ZFUNC, D3DCMP_LESS); //与D3DRS_STENCILPASS 一起使用(实现z pass 时,stencil + 1 或stencil - 1)

     g_pDev->SetRenderState(D3DRS_STENCILENABLE, TRUE); //开启stencil test

     g_pDev->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_ALWAYS); //stencil test总是成功

     g_pDev->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_INCR); //stencil test 成功并且z test 成功时,stencil + 1

     RenderShadow();

     //设置第二轮z pass 渲染

     g_pDev->SetRenderState(D3DRS_CULLMODE, D3DCULL_CW); //渲染顺时针三角形(即背对摄像机的三角形)

     g_pDev->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_DECR); //stencil test 成功并且z test 成功时,stencil - 1

     RenderShadow();

     //此时的stencil 缓存值,与原来相比,真正阴影部分的stencil + 1

     //还原渲染设置

     g_pDev->SetRenderState(D3DRS_CULLMODE, CullMode);

     g_pDev->SetRenderState(D3DRS_ALPHABLENDENABLE, AlphaBlendEnable);

     g_pDev->SetRenderState(D3DRS_SRCBLEND, SrcBlend);

     g_pDev->SetRenderState(D3DRS_DESTBLEND, DestBlend);

     g_pDev->SetRenderState(D3DRS_ZWRITEENABLE, TRUE);

     g_pDev->SetRenderState(D3DRS_ZFUNC, ZFunc);

 

     //设置stencil test 方法,使得再次渲染场景时,只渲染stencil value 没被改变的部分

     g_pDev->SetRenderState(D3DRS_STENCILENABLE, TRUE);

     g_pDev->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_EQUAL); //与D3DRS_STENCILREF、D3DRS_STENCILMASK 和D3DRS_STENCILWRITEMASK一起使用

     g_pDev->SetRenderState(D3DRS_STENCILREF, 0x0); // 当符合(ref & mask) == (stencil & mask) 条件时(即stencil 没被改变),渲染场景

     g_pDev->SetRenderState(D3DRS_STENCILMASK, 0xffffffff);

     g_pDev->SetRenderState(D3DRS_STENCILWRITEMASK, 0xffffffff);

 

     //打开光源并渲染场景

     g_pDev->SetLight(0, &g_Light);

     g_pDev->LightEnable(0, TRUE);   

     RenderScene();

     //还原渲染设置

     g_pDev->SetRenderState(D3DRS_CULLMODE, CullMode);

     g_pDev->SetRenderState(D3DRS_ALPHABLENDENABLE, AlphaBlendEnable);

     g_pDev->SetRenderState(D3DRS_SRCBLEND, SrcBlend);

     g_pDev->SetRenderState(D3DRS_DESTBLEND, DestBlend);

     g_pDev->SetRenderState(D3DRS_ZWRITEENABLE, TRUE);

     g_pDev->SetRenderState(D3DRS_ZFUNC, ZFunc);

     g_pDev->SetRenderState(D3DRS_STENCILENABLE, FALSE);

     g_pDev->SetRenderState(D3DRS_STENCILREF, StencilRef);

     g_pDev->SetRenderState(D3DRS_STENCILMASK, StencilMask);

     g_pDev->SetRenderState(D3DRS_STENCILWRITEMASK, StencilWriteMask);

     g_pDev->SetRenderState(D3DRS_STENCILFUNC, StencilFunc);

     g_pDev->SetRenderState(D3DRS_STENCILZFAIL, StencilZFail);

     g_pDev->SetRenderState(D3DRS_STENCILFAIL, StencilFail);

     g_pDev->SetRenderState(D3DRS_STENCILPASS, StencilPass);

g_pDev->EndScene();

     

然而,Z Pass 并不是一个完美的解决方案。比如,当摄像机处在一个阴影里面的时候,我们就无法用Z Pass来实现了。Z Fail的出现就是为了弥补Z Pass的不足。

 

转载于:https://my.oschina.net/robslove/blog/740913

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值