Irrlicht源码分析_2_1_scene_ShadowVolume
这里要说的是Irrlicht中命名空间scene下的CShadowVolumeSceneNode实现。
<这里及以后所有的driver都默认为d3d9>
使用方式:
1 device设置渲染状态ZPass、ZFail
2 由CShadowVolumeSceneNode创建阴影体
3 device绘制阴影
阴影体的理论到处都是,不再做重复工作。
前提:
显示driver(IVideoDriver*)存在这么两个方法:
//! Draws a shadow volume into the stencil buffer. To draw a stencil shadow, do //! this: Frist, draw all geometry. Then use this method, to draw the shadow //! volume. Then, use IVideoDriver::drawStencilShadow() to visualize the shadow. virtual void drawStencilShadowVolume(const core::vector3df* triangles, s32 count, bool zfail); //! Fills the stencil shadow with color. After the shadow volume has been drawn //! into the stencil buffer using IVideoDriver::drawStencilShadowVolume(), use this //! to draw the color of the shadow. virtual void drawStencilShadow(bool clearStencilBuffer=false, video::SColor leftUpEdge = video::SColor(0,0,0,0), video::SColor rightUpEdge = video::SColor(0,0,0,0), video::SColor leftDownEdge = video::SColor(0,0,0,0), video::SColor rightDownEdge = video::SColor(0,0,0,0));
第一个方法只是为了设置好driver 的状态,并将其绘制出来,阴影体数据来自CShadowVolumeSceneNode。
譬如设置ZPass的情况如下:
// Draw front-side of shadow volume in stencil/z only pID3DDevice->SetRenderState(D3DRS_CULLMODE, D3DCULL_CCW ); pID3DDevice->SetRenderState(D3DRS_STENCILPASS, D3DSTENCILOP_INCRSAT); pID3DDevice->DrawPrimitiveUP(D3DPT_TRIANGLELIST, count / 3, triangles, sizeof(core::vector3df)); // Now reverse cull order so front sides of shadow volume are written. pID3DDevice->SetRenderState( D3DRS_CULLMODE, D3DCULL_CW ); pID3DDevice->SetRenderState( D3DRS_STENCILPASS, D3DSTENCILOP_DECRSAT); pID3DDevice->DrawPrimitiveUP(D3DPT_TRIANGLELIST, count / 3, triangles, sizeof(core::vector3df));
第二个方法绘制阴影(只是根据stencil调节一些阴影效果罢了)
Irrlicht使用的方法很简单粗暴,直接绘制一个矩形就是了。
所以这里的重点就是如何生成正确的ShadowVolume,也就是CShadowVolumeSceneNode在这里面都做了些啥。
入口点:CShadowVolumeSceneNode::createShadowVolume
void CShadowVolumeSceneNode::createZPassVolume(s32 faceCount,s32 &numEdges,const core::vector3df& lightsource,SShadowVolume* svp,bool caps) { //1:normalize the light core::vector3df light(lightsource); light*=Infinity; if(light == core::vector3df(0,0,0)) light = core::vector3df(0.0001f,0.0001f,0.0001f); //2: get the face normal core::vector3df normal; u16 wFace0,wFace1,wFace2; for(s32 i=0;i<faceCount;i++) { wFace0 = Indices[3*i+0]; wFace1 = Indices[3*i+1]; wFace2 = Indices[3*i+2]; core::vector3df v0(Vertices[wFace2]-Vertices[wFace1]); core::vector3df v1(Vertices[wFace1]-Vertices[wFace0]); normal = v0.crossProduct(v1); //if face is belong to back side if(normal.dotProduct(light) >=0.0f) { //back side Edges[2*numEdges+0]=wFace0; Edges[2*numEdges+1]=wFace1; numEdges++; Edges[2*numEdges+0]=wFace1; Edges[2*numEdges+1]=wFace2; numEdges++; Edges[2*numEdges+0]=wFace2; Edges[2*numEdges+1]=wFace0; numEdges++; //caps=ZFail if(caps && svp->vertices && svp->count <svp->size-5) { //store back vertex pos svp->vertices[svp->count++] = Vertices[wFace0]; svp->vertices[svp->count++] = Vertices[wFace2]; svp->vertices[svp->count++] = Vertices[wFace1]; //move front pos svp->vertices[svp->count++] = Vertices[wFace0]-light; svp->vertices[svp->count++] = Vertices[wFace1]-light; svp->vertices[svp->count++] = Vertices[wFace2]-light; } } } };
这里构造了Edges这个数组:
他是在光照下物体背面下的网格边
在创建了背面的网格边缘数据后:
for(s32 i=0;i<numEdges;i++) { core::vector3df& v1 = Vertices[Edges[2*i+0]]; core::vector3df& v2 = Vertices[Edges[2*i+1]]; core::vector3df v3(v1-ls); core::vector3df v4(v2-ls); //add a quad as boundary of volume if(svp->vertices&& svp->count <svp->size-5) { svp->vertices[svp->count++]=v1; svp->vertices[svp->count++]=v2; svp->vertices[svp->count++]=v3; svp->vertices[svp->count++]=v2; svp->vertices[svp->count++]=v4; svp->vertices[svp->count++]=v3; } }
可以看到,将背面的每一条边沿着光线挤出为一个四边形,构建两个三角形组成阴影体,ZPass不同于ZFail,可以不用保持封口。等之后添加上实验图。
使用如此粗鲁的阴影体生成方式,居然还能到fps74的情况。
还有一张,这张由于没有把台阶作为阴影体源来渲染,所以阴影会有悬空的感觉。
作为参考这里把Irrlicht Engine的渲染图片上传一张:
问题:运行中Irrlicht使用ZPass方法(它默认ZFail)的时候,阴影出现大幅的抖动。效果不好。
发现Irrlicht实现的时候ZPass的实现并不准确,在CD3D9Driver::setRenderStatesStencilFillMode方法中:
在绘制Stencil阴影时
pID3DDevice->SetRenderState(D3DRS_STENCILREF, 0x1); pID3DDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_LESSEQUAL); //pID3DDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_GREATEREQUAL);
但是它是这么清空Stencil的(CD3D9Driver::drawStencilShadow):
if (clearStencilBuffer) pID3DDevice->Clear( 0, NULL, D3DCLEAR_STENCIL,0, 1.0, 0);
原型:STDMETHOD(Clear)(THIS_ DWORD Count,CONST D3DRECT* pRects,DWORD Flags,D3DCOLOR Color,float Z,DWORD Stencil) PURE;
可以看出,将stencil清空为0(最后一个)。
所以正确的方式应为:
pID3DDevice->SetRenderState(D3DRS_STENCILREF, 0x0); pID3DDevice->SetRenderState(D3DRS_STENCILFUNC, D3DCMP_NOTEQUAL);
我看的是1.3版本。只知道后续的版本是否已修改。
思考:Irrlicht使用背面每一个三角形而不是只有边缘去创建爱你阴影体,这时候driver绘制大量无用的数据,虽然最终stencil不会错,但是增加了stencil加1减1的次数。在CShadowVolumeSceneNode::setMeshToRenderForm这个方法中计算的领域信息没有使用到(ZFail用到了)。暂时先就这样,ZFail留待之后加入。