come from:http://www.cnblogs.com/mavaL/archive/2010/12/02/1894151.html
这个PostProcess例子还真是复杂,包含了很多我没接触过的盲点,只能一点一点的消化。不过令人兴奋的是,这个例子包含很多有用的知识点。我觉得PostProcess----后期效果处理就是对RTT(Render to Texture)技术的进阶(本例在各个源RT,目的RT之间反复处理)。对于本例来说,RTT和图像处理PipeLine是重点(不知道我把一个后期效果的实现,比如bloom比作一次PipeLine行不行...),另外还涉及到其他一些知识,我认为还是非常有用的,如一些基础图像处理方法(blur,bloom,UpFilter,DownFilter等),注解的使用,字符串处理,控件处理,特效文件的封装和管理。
其实我觉得,对每个例子,你能把它简单的学学,看看主要实现方法也行。但是如果深究起来,想要完全的捋清程序的逻辑走向,就会发现细节真的很多,越看越复杂。我想这就是为什么看别人的代码好像很简单,自己实现起来,到处是问题的原因。
所以,我做下笔记,一是记录下学到的知识点,以后实际应用时说不定想起了还能回来查阅下;
二是记下有些关键点或不理解的地方;
三是梳理出本例的主题的实现流程,比如一个bloom效果,到底经历了哪些处理过程最终得以实现。
1.
在CPostProcess的Init中,阐释了注解(Annotation)的实际应用:
假如一个材质系统什么的要加载上百个特效文件的话,而每个都有各自独有的参数,像bloomscale什么的,我们不可能手动的为每个参数去GetParameterByName吧,这就可以像Init里面那样利用注解了,把每个特效文件独有的参数绑定到其technique的注解里面,然后在应用程序中我们通过操控注解就能掌握这些参数包括其默认值等等了。这样Init函数就能抽象出来实现对任意特效文件的管理了。
2.
没看懂的一个地方,CPostProcess中有个bool m_bWrite[4],从对它的赋值看出,应该表示的是当前特效文件的PS结果是否输出到某个RT。但不是已经有了个m_nRenderTarget来表示输出到哪里了吗:
// Obtain the render target channel
D3DXHANDLE hAnno;
hAnno = m_pEffect->GetAnnotationByName( m_hTPostProcess, "nRenderTarget" );
if( hAnno )
m_pEffect->GetInt( hAnno, &m_nRenderTarget );
而且我发现没在程序中任何地方用到这个m_bWrite,这是什么意思呢?多此一举?l
3.
在每个PostProcess特效文件中,PixelKernel数组是我们要在源采样纹理中采样的纹素与当前纹素的偏移量,比如在bloom vertical处理中,我们要采样当前像素的上下各6个纹素颜色,就这样:
static const int g_cKernelSize = 13;
float2 PixelKernel[g_cKernelSize] =
{
{ 0, -6 },
{ 0, -5 },
{ 0, -4 },
{ 0, -3 },
{ 0, -2 },
{ 0, -1 },
{ 0, 0 },
{ 0, 1 },
{ 0, 2 },
{ 0, 3 },
{ 0, 4 },
{ 0, 5 },
{ 0, 6 },
};
但我们知道,在纹理中采样用的是纹理坐标,所以要做转换,转换后就放在特效文件的TexelKernel数组中。
为什么不直接在特效文件中计算TexelKernel呢,因为采样纹理的大小是未知的,要在应用程序中给出,所以这里采用了在对象的OnReset方法中传入采样纹理的大小,再计算出TexelKernel传入特效文件供其使用。
4.
scene.fx中,RenderNoLight是一个特别的technique,它用来将源RT的内容复制到目的RT。
本例中我们最终完成后期处理时,结果在一个texture里,我们通过往屏幕缓冲绘制一个全屏quad,并设定RenderNoLight来将最终图像复制到屏幕上。
但本例文档里提到,StrechRect也可以实现这个功能。于是我试了下,果然可以。
IDirect3DTexture9* pPrevTarget;
IDirect3DSurface9* pSrc, *pDest;
pPrevTarget = ( bPerformPostProcess ) ? g_RTChain[0].GetPrevTarget() : g_pSceneSave[0];
pPrevTarget->GetSurfaceLevel( 0, &pSrc );
pd3dDevice->GetRenderTarget( 0, &pDest );
pd3dDevice->StretchRect( pSrc, 0, pDest, 0, D3DTEXF_NONE );
不知道程序中没这么用是为什么,而是用一个RenderNoLight的ps+DrawPrimitiveUP绘制一个全屏quad来完成的。
5.
在OnFrameRender中,当我们渲染完场景的颜色,法线,位置的纹理后:
// Reset all render targets used besides RT 0
for( i = 1; i < g_nRtUsed; ++i )
V( pd3dDevice->SetRenderTarget( i, NULL ) );
设备的RT1,RT2都被重设了,RT0却没被重设。我在这里就试了下把设备的原RT0还原了,结果运行画面不正常。捋了捋逻辑,应该是这样的:
在PerformSinglePostProcess的注释中有:
// This method changes render target without saving any. The caller
// should ensure that the default render target is saved before calling
// this.
也就是调用该函数执行RTT时,这个函数直接改变设备的RT但并不保存,他提醒调用者调用之前要注意保存设备的原始RT!也就是如果在上面我们就把设备的RT0还原了再调用PerformPostProcess的话,那么执行完PostProcess我们设备的帧缓冲也不知道跑哪儿去了。。。。。。所以这里不忙还原设备的原始RT0。
6.
在OnFrameRender和PostProcess处理中,都是往目的RT上绘制一个全屏大小的quad,令人不解的是,为什么要把每个顶点坐标减去0.5f呢,我开始以为是不是窗口的border占了点宽度,所以有个偏移量。。。。。。。。。想象力真太丰富了。
幸好google时无意看到了这篇文章,不然我还迷失在自己的误解中:
http://www.cnitblog.com/wjk98550328/archive/2007/10/24/35258.html
上面这篇文章说,在有时候RTT的时候,我们会在目的RT上绘制一个全屏quad,利用rhw的顶点声明我们直接指定4个顶点的屏幕空间坐标,但由于DX中《Directly Mapping Texels to Pixels》文档阐述的,我们必须对每项顶点坐标减去0.5f.也就是顶点屏幕坐标的(0,0)不能准确对应顶点纹理坐标的(0,0),而(-0.5f,-0.5f)才能准确对应纹理坐标的(0,0).具体分析参见上述DX文档。
7.
PerformSinglePostProcess中,对当前特效文件的每个pass:
如果当前Pass中含有fScaleX,fScaleY,则说明该pass要downfilter或upFilter.比如fScaleX,fScaleY为1/4,则目RT的大小是源RT的1/16。所以我们要修改在目的RT上渲染的quad的大小:
if( fScaleX != 1.0f )
{ //+0.5抵消我们在aQuad中预先做的-0.5的偏移量,再缩放,然后再减去-0.5实现Mapping Texels to Pixels
aQuad[1].x = ( aQuad[1].x + 0.5f ) * fScaleX - 0.5f;
aQuad[3].x = ( aQuad[3].x + 0.5f ) * fScaleX - 0.5f; bUpdateVB = true;
}
if( fScaleY != 1.0f )
{
aQuad[2].y = ( aQuad[2].y + 0.5f ) * fScaleY - 0.5f;
aQuad[3].y = ( aQuad[3].y + 0.5f ) * fScaleY - 0.5f;
bUpdateVB = true;
}
然后在随后的其他的PostProcess中,我们的目的RT的大小都设为这次Scale的结果,直到遇到下次fScaleX,fScaleY:
//fExtentX,fExtentY是PerformSinglePostProcess的参数,而且是引用,所以做了变动将会影响到以后PerformSinglePostProcess调用。
fExtentX *= fScaleX;
fExtentY *= fScaleY;
还没完,我们这次的目的RT被缩放了,那么下次PostProcess时,我们要以这次的目的RT为源RT进行采样,而下次的目的quad的纹理坐标还是对应这次的目的RT的整个texture的呢,我们得让它对应这次缩放后的quad(确实很绕,不过要想搞清楚怎么设置每次目的quad的大小和采样纹理坐标,必须强迫自己去捋清这些逻辑):
//
// If the extents has been modified, the texture coordinates
// in the quad need to be updated.
//
if( aQuad[1].tu != fExtentX )
{
aQuad[1].tu = aQuad[3].tu = fExtentX;
bUpdateVB = true;
}
if( aQuad[2].tv != fExtentY )
{
aQuad[2].tv = aQuad[3].tv = fExtentY; //tu,tv是在源纹理中采样的纹理坐标
bUpdateVB = true;
}
8.
对于每个PostProcess特效文件,都包含了6个texture对象(那2个Velocity纹理对象没看到设置和使用,就不管了,应该是扩展用的):
texture g_txSrcColor;
texture g_txSrcNormal;
texture g_txSrcPosition;
texture g_txSceneColor;
texture g_txSceneNormal;
texture g_txScenePosition;
这6个纹理对象是表示该PostProcess的源纹理采样对象,前三个表示上一步PostProcess处理的结果texture,后三个表示在OnFrameRender中渲染的那三个texture。当本次PostProcess是序列中的第一次时,这两套texture是相等的。
每次PostProcess,都设定顶点声明为:pd3dDevice->SetVertexDeclaration( g_pVertDeclPP );
这个顶点声明依次包含已转换的顶点位置,源采样纹理坐标(Texcoord for post-process source)和源场景纹理坐标(Texcoord for the original scene)。
所以每个PostProcess特效文件的PS的输入参数都有两套纹理坐标,第一套用来采样上次PostProcess的结果texture,第二套用来采样在OnFrameRender中渲染的三个texture中的任意一个。
9.
程序使用CRenderTargetChain来连接前一次和后一次的PostProcess。非常有技巧性,对于每条链来说,只需要2个texture,本次PostProcess结束后,翻转该链中srcRT和destRT(本次的destRT作为下次的srcRT,本次的srcRT作为下次的destRT),然后进行下次PostProcess。
这让人想到帧缓冲的SwapChain:一个FrontBuffer,一个BackBuffer,轮流显示帧图像并翻转。
以后如果自己要实现PostProcess,可以借鉴这个做法。
10.
现在从大局上来梳理下一个PostProcess效果的实现历程。就看Blur吧。它的后期处理特效序列是:
[Color]Down Filter 4x, [Color]Gaussian Blur Horizontal, [Color]Gaussian Blur Vertical, [Color]Up Filter 4x
可以多来一次Gaussian Blur以使模糊效果更加明显。
另外Blur效果只对场景的颜色信息进行处理,所以只涉及到color这一条CRenderTargetChain,即上图中的第一条。
有些效果比如Edge Glow会同时涉及到颜色处理和法线处理,这时就要在2条链中操作,不过原理都差不多,没什么大不了的。
第一步:在OnFrameRender中利用scene.fx往3个texture中分别输出了场景的color,normal,position信息。
第二步:开始执行PostProcess序列的第一个:[Color]Down Filter 4x。因为这是首次往颜色链RTT,所以颜色链g_RTChain[0].m_bFirstRender为true。这种情况下我们设定该次处理的srcTexuture为第一步的color texture,然后设定destRT: g_RTChain[0].GetNextTarget();
接下来把g_RTChain[0].m_bFirstRender为false。
然后就可以利用该次的特效文件进行绘制quad了。
最后,调用g_RTChain[0].Flip()来翻转srcTexture和destRT为下次处理做准备。
第三步至第五步: 完成剩下的3次PostProcess,每步跟第二步基本相同,不同的是设定srcTexture为g_RTChain[0].GetNextSource()。
第五步完成后,我们最终的模糊效果图像已经存在于颜色链的g_RTChain[0].GetPrevTarget()里了。
第六步: 在OnFrameRender中,我们可以通过StretchRect来将最终图像复制到帧缓冲中显示。
-------------------------完毕
11.
那十几个PostProcess实现特效文件我觉得也很有必要看看是怎么实现的,以后自己要实现DOF,Blur,Bloom这些效果时可以借鉴它们的实现。
12.
对于有些后期效果,比如Blur来说,我们是通过先down filter4x使图像变为原来的1/16,然后执行模糊处理,最后再up filter4x还原图像的大小并显示。
我发现如果不down filter4x和up filter4x,那么帧数会下降40乃至更多!DX文档中关于这一点说得很清楚:
This can have a significant performance boost, because the pixel shaders have fewer pixels to process. A common postprocessing practice is to scale down the image, apply postprocess effects to the sub-area of the texture, then scale up the result image to the original size. The performance gain from having fewer pixels to process usually far exceeds the penalty of performing the extra steps of scaling.