2-12.安装后的处理框架
问题
您要添加二维后处理影响到XNA工程的最终图像,如模糊,纹波,震动,镜头变焦,边缘增强,或其他后处理的效果。
解决方案
您首先使您的二维或三维场景到屏幕上。在结束 Draw阶段,之前的内容回缓冲区提交给屏幕,你准备截取后备缓冲区的内容把它用2D图象保存,如3-8节所解释。
接下来,您将促使本二维图像到屏幕,但您将通过一个自定义象素阴影,它正是这章我们要讨论的。在象素阴影里,您可以操纵个别图象的每一个像素。
你可以做到这一点,使用简单 Spritebatch你是否有任何其他的二维图像到屏幕(见3-1节 ) ,但 SpriteBatch不支持 alpha混合的效果多个入口(如效果表明在下一章节) 。为了解决这个,创造了一个框架,支持所有的后期处理效果,你将手工定义两个三角形跨越覆盖整个屏幕,这两个三角形的最后形象。这样,您就可以使用任何支持 Pixel Shader去处理最后图象的像素。
如果你想整合多个 post-processing效果,你可以使所产生的每一个形象的一个变量而不是后备缓冲。最后效果的最后结果应该绘制回缓冲区,返回缓冲区的内容出现在屏幕上。
如何工作的
首先,你需要添加一些变量到你的工程中。包括 ResolveTexture2D,它会被用来截获返回缓冲的内容, RenderTarget2D将会使用一系列多样效果。你同样需要一个效果文件包含有 post-processing技术。
1
VertexPositionTexture[] ppVertices;
2 RenderTarget2D targetRenderedTo;
3 ResolveTexture2D resolveTexture;
4 Effect postProcessingEffect;
5 float time = 0 ;
因为你必须定义两个三角形是布置在整个屏幕上,你应该确定其顶点:
2 RenderTarget2D targetRenderedTo;
3 ResolveTexture2D resolveTexture;
4 Effect postProcessingEffect;
5 float time = 0 ;
1
private
void
InitPostProcessingVertices()
2 {
3 ppVertices = new VertexPositionTexture[ 4 ];
4 int i = 0 ;
5 ppVertices[i ++ ] = new VertexPositionTexture( new Vector3( - 1 , 1 ,0f), new Vector2( 0 , 0 ));
6 ppVertices[i ++ ] = new VertexPositionTexture( new Vector3( 1 , 1 ,0f), new Vector2( 1 , 0 ));
7 ppVertices[i ++ ] = new VertexPositionTexture( new Vector3( - 1 , - 1 ,0f), new Vector2( 0 , 1 ));
8 ppVertices[i ++ ] = new VertexPositionTexture( new Vector3( 1 , - 1 ,0f), new Vector2( 1 , 1 ));
9 }
这种方法确定了四叉树的四个顶点,所提供的两个三角形
TraingleStrip(见5-1) 。在这一点上,它可能有助于记住,屏幕指定的坐标内的[ -1,1 ]区域,而纹理坐标指定在[ 0,1 ]区域。
2 {
3 ppVertices = new VertexPositionTexture[ 4 ];
4 int i = 0 ;
5 ppVertices[i ++ ] = new VertexPositionTexture( new Vector3( - 1 , 1 ,0f), new Vector2( 0 , 0 ));
6 ppVertices[i ++ ] = new VertexPositionTexture( new Vector3( 1 , 1 ,0f), new Vector2( 1 , 0 ));
7 ppVertices[i ++ ] = new VertexPositionTexture( new Vector3( - 1 , - 1 ,0f), new Vector2( 0 , 1 ));
8 ppVertices[i ++ ] = new VertexPositionTexture( new Vector3( 1 , - 1 ,0f), new Vector2( 1 , 1 ));
9 }
注意:即使使用 pretransformed坐标(在屏幕空间而不是三维空间) ,请务必你想想背面 culling(见5-6节 ) 。当使用 pretransformed坐标,你的眼睛是
相机,因此您需要确保您定义三角形顺时针在屏幕上。这样做的以前的代码:第一个三角形的定义是从左上角,到右上角,然后到左下角。
你可以看到的位置是指在屏幕上立即坐标,而(-1,1)点对应的左上角的窗口,并在(1 ,-1)点对应于右下角。您还规定,(0,0)左上角的质地应该定位在(-1,1 )的左上角你的窗口,而(1,1)右下角的纹理应定位在右下角的窗口。如果你绘制这个四叉树到屏幕,你指定的这个图象将覆盖整个窗口。
注意:由于您的窗口只有二维,你可以说你不需要第三的位置坐标这些顶点。然而,把每个像素输出在屏幕上, XNA跟踪距离和深度缓冲之间的距离,这也正是为什么用第三坐标轴。指定0的距离,则表明图像得出尽可能接近相机(更确切的,就表示它是吸取了近
裁剪平面)
别忘了从 Initialize方法中调用这个方法:
1
InitPostProcessingVertices();
三个最后的变量应该被初始化在
LoadContent方法:
1
PresentationParameters pp
=
GraphicsDevice.PresentationParameters;
2 targetRenderedTo = new RenderTarget2D(device,pp.BackBufferWidth,pp.BackBufferHeight, 1 ,device.DisplayMode.Format);
3 resolveTexture = new ResolveTexture2D(device,pp.BackBufferWidth,pp.BackBufferHeight, 1 ,device.DisplayMode.Format);
4 postProcessingEffect = content.Load < Effect > ( " content/postprocessing " );
关于绘制目标的详细信息参看3-8节。更重要的是在这种情况下新绘制的目标和你的窗口具有同样的属性;它需要同样的宽度,高度和颜色格式化,你可以在图形设备的
PresentationParameters结构中找到它们。这种方法,你可以简单的找到它的材质,
post-process它,把结果输出到屏幕上,不需要做任何的扫描和位图操作。因为只有在充分等级下你可以使用所产生的纹理,你不在需要任何
mipmaps。也就是说你只需要
mipmaps的等级,它来自于纹理在其原来的大小。您还加载效果文件,其中包含您的后处理技术 。
2 targetRenderedTo = new RenderTarget2D(device,pp.BackBufferWidth,pp.BackBufferHeight, 1 ,device.DisplayMode.Format);
3 resolveTexture = new ResolveTexture2D(device,pp.BackBufferWidth,pp.BackBufferHeight, 1 ,device.DisplayMode.Format);
4 postProcessingEffect = content.Load < Effect > ( " content/postprocessing " );
随着参数的加载,你可以真正开始工作了。在您提供您的像往常一样场景,你要调用一个方法来截取返回缓冲的内容,操作它,然后将结果在次送回缓冲。 PostProcess方法处理的很严密:
1
private
void
PostProcess()
2 {
3 device.ResolveBackBuffer(resolveTexture, 0 );
4 Texture2D textureRenderedTo = resolveTexture;
5 }
第一行转化返回缓冲的内容成
ResolveTexture2D,在这种情况下,变量
resolveTexture 。它包含了屏幕,因为它将被发送到屏幕上。保存它作为一个普通的
Texture2D,调用
textureRenderedto.
2 {
3 device.ResolveBackBuffer(resolveTexture, 0 );
4 Texture2D textureRenderedTo = resolveTexture;
5 }
下一步,你要绘制 textureRenderedTo在四叉树用 post-processing效果来覆盖整个窗口。在这节的样列,你要定义个一个效果,调用 Invert,转化这副图片上所有象素的颜色:
1
postProcessingEffect.CurrentTechnique
=
postProcessingEffect.Techniques[
"
Invert
"
];
2 postProcessingEffect.Begin();
3 postProcessingEffect.Parameters[ " textureToSampleFrom " ].SetValue(textureRenderedTo);
4 foreach (EffectPass pass in postProcessingEffect.CurrentTechnique.Passes)
5 {
6 pass.Begin();
7 device.VertexDeclaration = new VertexDeclaration(device,VertexPositionTexture.VertexElement);
8 device.DrawUserPrimitives < VertextPositionTexture > (PrimitiveType.TriangleStrip,ppVertices, 0 , 2 );
9 pass.End();
10 }
11 postProcessingEffect.End();
你开始选择
post-processing技术你想用来绘制你最终的图象到屏幕上,开始这些效果。
2 postProcessingEffect.Begin();
3 postProcessingEffect.Parameters[ " textureToSampleFrom " ].SetValue(textureRenderedTo);
4 foreach (EffectPass pass in postProcessingEffect.CurrentTechnique.Passes)
5 {
6 pass.Begin();
7 device.VertexDeclaration = new VertexDeclaration(device,VertexPositionTexture.VertexElement);
8 device.DrawUserPrimitives < VertextPositionTexture > (PrimitiveType.TriangleStrip,ppVertices, 0 , 2 );
9 pass.End();
10 }
11 postProcessingEffect.End();
接下来,你传递 textureRenderedTo到你图形卡,所以你的效果可以用这个为列。最后,每个通过您的后处理技术,您指示您的图形卡,使两个三角形,涵盖整个窗口。编辑你的效果代码使两个三角形显示图象,用一些你选择的操作。
注意:当绘图对象从一个3D世界,你总是需要设置世界,视阈和投影矩阵在这本书的很多章节有介绍。这些矩阵允许顶点着色器在您的图形卡绘制三维坐标从对象到正确的像素在屏幕上。在这种情况下,然而,您已经确定的你的两个三角形在屏幕空间的位置!因此,没有必要设置这些矩阵,因为现在的顶点着色不会改变位置的顶点,而不是简单地将通过他们的支持 Pixel Shader ,因为你会看到在HLSL代码。
不要忘记调用这个方法在Draw方法的最后:
1
postProcess();
HLSL
只有最后一个步骤是:用HLSL定义它自己的 post-processing.不需要着急,这里使用HLSL特别容易。所以,打开一个新文件,命名为 postprocessing.fx,我把它的内容放在一块:
1
texture textureToSampleForm;
2 sampler textureSampler = sampler_state
3 {
4 texture =< textureToSampleFrom > ;
5 magfilter = POINT;
6 minfilter = POINT;
7 mipfilter = POINT;
8 };
9 struct PPVertexToPixel
10 {
11 float4 Position:POSITION;
12 float2 TexCoord:TEXCOORDo;
13 };
14 struct PPPixelToFrame
15 {
16 float4 Color:COLORo;
17 };
18 PPVertexToPixel PassThroughVertexShader(float4 inPos:POSITIONo,float2 inTexCoord:TEXCOORDo)
19 {
20 PPVertexToPixel Output = (PPVertexToPixel)o;
21 Output.Position = inPos;
22 Output.TexCoord = inTexCoord;
23 return Output;
24 }
25 // ----------PP Technique:Invert----------------
26 PPPixelToFrame InvertPS(PPVertexToPixel):COLORo
27 {
28 PPPixelToFrame Output = (PPPixelToFrame)o;
29 float4 colorFromTexture = text2D(textureSampler,PSIn.TexCoord);
30 Output.Color = 1 - colorFromTexture;
31 return Output;
32 }
33 technique Tnvert
34 {
35 pass Passo
36 {
37 VertexShader = compile vs_1_1 PassThroughVertexShader();
38 PixelShader = compile pv_1_1 InertPS();
39 }
40 }
上面的代码有点短,但是我想完整把HLSL的代码完整的显示出来。在下面的代码是效果技术的定义,说明这个技术的名称和它需要的阴影顶点,象素阴影。上面的是阴影顶点和象素阴影,最上面的代码是在XNA程序中设置的变量。对于这个简单的例子,您可以设定只有您截取从您的回到缓冲中收到二维图像被送往屏幕。
2 sampler textureSampler = sampler_state
3 {
4 texture =< textureToSampleFrom > ;
5 magfilter = POINT;
6 minfilter = POINT;
7 mipfilter = POINT;
8 };
9 struct PPVertexToPixel
10 {
11 float4 Position:POSITION;
12 float2 TexCoord:TEXCOORDo;
13 };
14 struct PPPixelToFrame
15 {
16 float4 Color:COLORo;
17 };
18 PPVertexToPixel PassThroughVertexShader(float4 inPos:POSITIONo,float2 inTexCoord:TEXCOORDo)
19 {
20 PPVertexToPixel Output = (PPVertexToPixel)o;
21 Output.Position = inPos;
22 Output.TexCoord = inTexCoord;
23 return Output;
24 }
25 // ----------PP Technique:Invert----------------
26 PPPixelToFrame InvertPS(PPVertexToPixel):COLORo
27 {
28 PPPixelToFrame Output = (PPPixelToFrame)o;
29 float4 colorFromTexture = text2D(textureSampler,PSIn.TexCoord);
30 Output.Color = 1 - colorFromTexture;
31 return Output;
32 }
33 technique Tnvert
34 {
35 pass Passo
36 {
37 VertexShader = compile vs_1_1 PassThroughVertexShader();
38 PixelShader = compile pv_1_1 InertPS();
39 }
40 }
接 下来,您提出了一个纹理采样阶段在您的图形卡,实际使用后会在您的支持Pixel Shader样品色彩这是你的变量。 您链接到本采样器你刚才定义和简单说明什么应该做,如果你的代码要求坐标轴的颜色不对应百分之白的象素。在这里,您指定您的采样阶段应 只要采取的颜色最接近的像素。
注意 纹理坐标是 float2 ,与X和Y的值介于0和1 。由于这些数字是floats,几乎对他们任何操作将产生一个非常小的舍入误差。这意味着当您
使用这坐标轴到从纹理到例子,纹理坐标将是不完全相符, 像素的纹理,但将是非常接近的。这就是为什么要在这种情况下你需要说明的纹理采样器
。
接下来,您定义两个structs :一个拥有发送的信息的顶点着色的像素着色器。此信息只不过是画面的像素和纹理的位置 需要加以采样获取颜色的像素。第二结构象素阴影的输出。每一个象素,象素阴影只需要计算颜色:
顶点着色器可让您的数据进行操控在每个顶点从您构建三角形寄出到显卡。在3D程序中顶点着色器的最重要的任务是将3D 坐标转换成2D屏幕坐标。在post-processing效果的情况下,顶点着色器不在有用,因为你已经定义了两个三角形的位置顶点在 屏幕坐标系上!您只是想的顶点着色器的投入产出能力。
接下来,您来到像素着色器,这是有趣的一部分post-processing效果。每一个象素需要被绘制到屏幕上,这个方法被调用, 允许你转换这些象素的颜色。在象素着色器,简单的调用Output一个空的输出结构被建立.下一步,采样的颜色从textureSampler在纹理 坐标这项被送往支持Pixel Shader。如果你的象素阴影简单的输出这些颜色,输出的结果图象可能会与输入的图片一致, 因为每个像素的窗口将采样的颜色从原来的位置的原始图像获得。所以,您要更改的协调东西正在抽取颜色,取自原始图像,这是这样做的下段
这个colorFromTexture变量包含4个值(红,绿,蓝和alpha)在0和1之间。在例子中,你用1减去他们而改变他们的值。在输出结构中保存他们转变后的值然后返回它。
当你执行你的代码时,你正常的屏幕将截取 textureRenderedTo材质,每一个象素的颜色将被转变在最后绘制到屏幕之前。
多队列Post-Processing效果
作为一些代码的补充允许你多队列 Post-Processing的效果。在你的 Draw方法里,你可以建立一个表单包含有 post-processing技术你想应用与这个图象上之前发送到屏幕上,你将会传递这个表单到 PostProcess方法中:
1
List
<
string
>
ppEffectsList
=
new
List
<
string
>
();
2 ppEffectsList.Add( " Invert " );
3 ppEffectsList.Add( " Invert " );
4 PostProcess(ppEffectsList);
此时,你定义了唯一的转化技术,以这个简单的例子你将2次使用这个技术。转换一个转化过的图象,你可以获得最初始的图象。多么让人兴奋?
2 ppEffectsList.Add( " Invert " );
3 ppEffectsList.Add( " Invert " );
4 PostProcess(ppEffectsList);
你必须调整你的 PostProcess方法它可以接收效果的例子做为说明。正如你看到的,方法的开始就倾向声明队列:
1
public
void
PostProcess(List
<
string
>
ppEffectsList)
2 {
3 for ( int currentTechniue = 0 ;currentTechnique < ppEffectList.Count;currentTechnique ++ )
4 {
5 device.SetRenderTarget( 0 , null );
6 Texture2D textureRenderedTo;
7 if (currentTechnique == 0 )
8 {
9 device.ResolveBackBuffer(resolveTexture, 0 );
10 textureRenderedTo = resolveTexture;
11 }
12 else
13 {
14 textureRenderedTo = targetRenderedTo.GetTexture();
15 }
16 if (currentTechnique == ppEffectsList.Count - 1 )
17 device.SetRenderTarget( 0 , null );
18 else
19 device.SetRenderTarget( 0 ,targetRenderedTo);
20 postProcessingEffect.CurrentTechnique = postProcessingEffect.Techniques[ppEffectsList[currentTechnique]];
21 postProcessingEffect.Begin();
22 postProcessingEffect.Parameters[ " textureToSampleFrom " ].SetValue(textureRenderedTo);
23 foreach (EffectPass pass in postProcessingEffect.CurrentTechnique.Passes)
24 {
25 pass.Begin();
26 device.VertexDeclaration = new VertexDeclaration(device,VertexPositionTexture.VertexElements);
27 device.DrawUserPrimitives < VertexPositionTexture > (PrimitiveType.TriangleStrip,ppVertices, 0 , 2 );
28 pass.End();
29 }
30 }
31 }
这个方法的思想如图2-24所示。基本上,您的清单中每一个效果,您将检索当前内容
Render成一个纹理并使其再次融入
RenderTarget使用当前的效果。有两个这一基本规则的例外。
2 {
3 for ( int currentTechniue = 0 ;currentTechnique < ppEffectList.Count;currentTechnique ++ )
4 {
5 device.SetRenderTarget( 0 , null );
6 Texture2D textureRenderedTo;
7 if (currentTechnique == 0 )
8 {
9 device.ResolveBackBuffer(resolveTexture, 0 );
10 textureRenderedTo = resolveTexture;
11 }
12 else
13 {
14 textureRenderedTo = targetRenderedTo.GetTexture();
15 }
16 if (currentTechnique == ppEffectsList.Count - 1 )
17 device.SetRenderTarget( 0 , null );
18 else
19 device.SetRenderTarget( 0 ,targetRenderedTo);
20 postProcessingEffect.CurrentTechnique = postProcessingEffect.Techniques[ppEffectsList[currentTechnique]];
21 postProcessingEffect.Begin();
22 postProcessingEffect.Parameters[ " textureToSampleFrom " ].SetValue(textureRenderedTo);
23 foreach (EffectPass pass in postProcessingEffect.CurrentTechnique.Passes)
24 {
25 pass.Begin();
26 device.VertexDeclaration = new VertexDeclaration(device,VertexPositionTexture.VertexElements);
27 device.DrawUserPrimitives < VertexPositionTexture > (PrimitiveType.TriangleStrip,ppVertices, 0 , 2 );
28 pass.End();
29 }
30 }
31 }
首先,第一个效果,截取返回缓冲的内容,取代 RenderTarget的内容。最后,第二个效果,绘制结果到返回缓冲,所以它会被绘制到屏幕。这一过程如图2-24
前面的代码显示它如何工作的。如果是第一个技术效果,保存返回缓冲的内容到 textureRenderedTo里。否则,得到 RenderTarget的内容然后保存到 textureRenderedTo中。不管哪种方法, textureRenderTo将保存最后的绘制。参见3-8,在 RenderTarget上调用 GetTexture方法之前,你需要完成另外一个 RenderTarget,用该方法的第一行代码执行。
接下来检测当前的技术效果是否是表单上最后一个。如果是,你设置返回缓冲用传递给 device.SetRenderTarget方法 null值作为绘制当前目标(您可以离开此行了,因为它是在方法开始时执行的) 。否则, 您重置您的自定义 RenderTarget2D的积极渲染目标。
其余的代码保持不变.
作为 post-processing技术效果的第二的简单例子,依靠时间改变颜色的值。添加下面的代码到 .fx文件顶部:
1
float
xTime;
这是一个变量可以设置在你的XNA程序中同时也能从你的HLSL代码中读出。添加代码到你的
.fx文件的末尾:
1
//
---------PP Technique:TimeChange-------
2 PPPixelToFrame TimeChangePS(PPVertexToPixel PSIn):COLORo
3 {
4 PPPiexToFrame Output = (PPPixelToFrame) 0 ;
5 Output.Color = tex2D(textureSampler,PSIn.TexCoord);
6 Output.Color.b *= sin(xTime);
7 Output.Color.rg *= cos(xTime);
8 Output.Color += 0.2f ;
9 return Output;
10 }
11 techniqueTimeChange
12 {
13 pss Passo
14 {
15 VertexShader = compile vs_1_1 PassThroughVertexShader();
16 PiexlShader = compile ps_2_0 TimeChangePS();
17 }
18 }
图象上的每一个象素,蓝色部分是乘以一个正弦波这取决于
xTime变量,绿色和红色部分是乘以余弦波被统一变量。记住,正弦和余弦在-1和1之间产生波形,所有颜色部分的负值将被置于0。
2 PPPixelToFrame TimeChangePS(PPVertexToPixel PSIn):COLORo
3 {
4 PPPiexToFrame Output = (PPPixelToFrame) 0 ;
5 Output.Color = tex2D(textureSampler,PSIn.TexCoord);
6 Output.Color.b *= sin(xTime);
7 Output.Color.rg *= cos(xTime);
8 Output.Color += 0.2f ;
9 return Output;
10 }
11 techniqueTimeChange
12 {
13 pss Passo
14 {
15 VertexShader = compile vs_1_1 PassThroughVertexShader();
16 PiexlShader = compile ps_2_0 TimeChangePS();
17 }
18 }
用这个技术效果呈现最后的图象:
1
List
<
string
>
ppEffectList
=
new
List
<
string
>
();
2 ppEffectsList.Add( " Invert " );
3 ppEffectsList.Add( " TimeChange " );
4 postProcessingEffect.Parameters[ " xTime " ].SetValue(time);
5 PostProcess(ppEffectsList);
注意:您将
xTime价值一定值时,保持时间。您需要指定此时间变量的代码在您的
XNA内:
2 ppEffectsList.Add( " Invert " );
3 ppEffectsList.Add( " TimeChange " );
4 postProcessingEffect.Parameters[ " xTime " ].SetValue(time);
5 PostProcess(ppEffectsList);
1
float
time;
同样,用
Update方法更新这些值
1
time
+=
gameTime.ElapsedGameTime.Milliseconds
/
1000.0f
;
当你执行这个时,您会看到您的图片颜色随时间而改变。这不太好,但你改变像素的色彩值仅仅基于其原始的颜色。看下一节,其中的颜色周围像素等,也在考虑之时确定最后颜色的像素。
代码
略