2.13 创建一个模糊(Blur),(发光)Glow Post-Processing Effect
问题
你想在最终图像上添加模糊效果,或给场景中的物体添加光泽。
解决方案
这两个effect都是post-processing effect。可参见前面的教程学习如何建立一个post-processing框架。
你可以通过计算每个像素和一些周围像素的颜色值平均值并用这些值替换像素的原始颜色实现模糊效果。例如,如果图2-25中的格子是一张需要模糊的图像,你该如何计算像素9的最终颜色呢?
图2-25 计算2D纹理中的像素颜色的平均值
你可以通过将9个数字相加并除以9得到这9个数字的平均数,计算颜色平均值的方法是一样的:将2-4像素、8 到10像素、14到16像素的颜色相加并除以像素的数量:这里是9。
如果除了像素9之外其他像素的颜色都是白色的,如图2-16左图的情况,这会导致像素9的颜色扩展到像素2到4,8到10和14到16,使像素9“扩散”了。但是,像素9本身的颜色变暗了,因为它也被周围颜色的平均值替换了,如图2-26中图所示。要获取一个发光效果,你要混合原始图像,在这个图像的某处存储了像素9的原始颜色。所以,最后像素9拥有自己的原始颜色,而像素9周围的像素拥有一点像素9的颜色,就好像像素9有了一个发光效果,如图2-26的右图所示。
图2-26 创建一个发光(glow)效果
工作原理
下面是一些说明。要获取一个漂亮的模糊效果,你需要对中心像素周围的大量像素取平均值。更好的方法是首先通过对中心像素同一行中的一些像素取平均值进行水平方向的模糊,然后,通过对中心像素同一列的一些像素取平均值对刚才得到的模糊进行竖直方向的模糊。这会获得两个1D的平均值而不是2D,而2D的平均值要处理多得多的像素才能获得漂亮的结果。
第二,你需要判断对哪些像素进行平均。要获得最好的结果,你需要给靠近中心像素的像素更多的“关注”,而远处的像素“关注”较少,这叫做施加更多的“权重(weight)”。为了你的方便,我已经计算了对应高斯模糊的一些偏移量和权重,高斯模糊对应通常情况下的模糊。
看一下下面的数组,它包含距离中心像素的距离信息。第一个数据为0偏移量,表示中心像素本身。第二个数据为0.005偏移量,对应距离中心像素非常近的采样像素。要保持对称,你需要采样距离中心像素左右两侧0.005偏移量的两个像素,你给这两个像素颜色都施加了一个0.102大小的权重,这可以在第二个数组中看到。然后,你采样距离中心像素左右距离为0.0117的两个像素,施加一个较小的0.0936的权重。以此类推直至距离中心像素较远的像素 ,而且权重依次减少。
float positions[] = { 0.0f, 0.005, 0.01166667, 0.01833333, 0.025, 0.03166667, 0.03833333, 0.045, }; float weights[] = { 0.0530577, 0.1028506, 0.09364651, 0.0801001, 0.06436224, 0.04858317, 0.03445063, 0.02294906, };
如果你将所有点的权重相加,结果为1,这样才不会让图像中的颜色损失或增加。
注意:相对于其他像素,中央像素的权重看起来较小,但是,因为+0和-0是相同的,中央像素会被处理两次,使它的权重加倍,导致在最终结果中它的影响最大。
你还要用到一个xBlurSize变量,可以从XNA程序中调整模糊效果的宽度:
float xBlurSize = 0.5f;
你已经基于上一个教程创建了一个空的HorBlur effect:
// PP Technique: HorBlur PPPixelToFrame HorBlurPS(PPVertexToPixel PSIn) : COLOR0 { PPPixelToFrame Output = (PPPixelToFrame)0; return Output; } technique HorBlur { pass Pass0 { VertexShader = compile vs_1_1 DefaultVertexShader(); PixelShader = compile ps_2_0 HorBlurPS(); } }
实际的effect定义在pixel shader中。对每个前面的数组中定义的周围像素,你需要采样颜色并乘以它的权重。基于对称,操作是从中央像素两侧进行的。最后,将所有颜色相加。
从中央像素的TexCoord加上来自于positions数组中的值(这个值作为水平纹理坐标)可以获取周围像素的位置:
PPPixelToFrame HorBlurPS(PPVertexToPixel PSIn) : COLOR0 { PPPixelToFrame Output = (PPPixelToFrame)0; for (int i = 0; i < 8; i++) { float4 samplePos = tex2D(textureSampler, PSIn.TexCoord + float2(positions[i], 0)*xBlurSize); samplePos *= weights[i]; float4 sampleNeg = tex2D(textureSampler, PSIn.TexCoord -float2(positions[i], 0)*xBlurSize); sampleNeg *= weights[i]; Output.Color += samplePos + sampleNeg; } return Output; }
你可以看到xBlurSize变量用来增加/减少中心像素和周围像素的距离。
现在在XNA程序中,你只需简单地给这个变量赋一个值并开启这个effect:
List<string> ppEffectsList = new List<string>(); ppEffectsList.Add("HorBlur"); postProcessor.Parameters["xBlurSize"].SetValue(0.5f); postProcessor.PostProcess(ppEffectsList);
垂直模糊
有了水平模糊,垂直模糊也很容易创建。现在不是将positions数组中的值添加到水平纹理坐标,而是添加到竖直坐标,选择同一列的像素作为处理的像素:
// PP Technique: VerBlur PPPixelToFrame VerBlurPS(PPVertexToPixel PSIn) : COLOR0 { PPPixelToFrame Output = (PPPixelToFrame)0; for (int i = 0; i < 8; i++) { float4 samplePos = tex2D(textureSampler, PSIn.TexCoord + float2(0, positions[i])*xBlurSize); samplePos *= weights[i]; float4 sampleNeg = tex2D(textureSampler, PSIn.TexCoord - float2(0, positions[i])*xBlurSize); sampleNeg *= weights[i]; Output.Color += samplePos + sampleNeg; } return Output; } technique VerBlur { pass Pass0 { VertexShader = compile vs_1_1 PassThroughVertexShader(); PixelShader = compile ps_2_0 VerBlurPS(); } }
组合水平和垂直模糊你可以获得一个漂亮的模糊图像:
List<string> ppEffectsList = new List<string>(); ppEffectsList.Add("HorBlur"); ppEffectsList.Add("VerBlur"); postProcessor.Parameters["xBlurSize"].SetValue(0.5f); postProcessor.PostProcess(ppEffectsList);
发光效果
如教程介绍中解释的那样,你可以通过将模糊过的图像与原始图像混合获取发光效果,通过这个方法,原始的轮廓会锐化。
这需要后备缓冲中存在模糊过的图像,这样在第二个pass中,你可以与原始图像进行混合。所以,你需要定义一个新technique,这个technique将垂直模糊作为第一个pass, blend-in作为第二个pass:
technique VerBlurAndGlow { pass Pass0 { VertexShader = compile vs_1_1 PassThroughVertexShader(); PixelShader = compile ps_2_0 VerBlurPS(); } pass Pass1 { AlphaBlendEnable = true; SrcBlend = SrcAlpha; DestBlend = InvSrcAlpha; PixelShader = compile ps_2_0 BlendInPS(); } }
第二个pass是新的。你可以看到在pass开始时改变了三个渲染状态:开启alpha混合和设置alpha函数。马上你就会学到这个alpha函数。Effect由第一个pass开始,这样模糊图像会保存在后备缓冲中。然后,开始第二个pass,让你将原始图像混合到后备缓冲中的图像。
下面是第二个pass的pixel shader:
// PP Technique: VerBlurAndGlow PPPixelToFrame BlendInPS(PPVertexToPixel PSIn) : COLOR0 { PPPixelToFrame Output = (PPPixelToFrame)0; float4 finalColor = tex2D(originalSampler, PSIn.TexCoord); finalColor.a = 0.3f; Output.Color = finalColor; return Output; }
这里,你采样原始图像中像素的颜色并调整它的alpha(透明度)值。当在XNA进行混合时,使用如下规则:
finalColor=sourceBlend*sourceColor+destBlend*destColor
在technique定义中,你将sourceBlend设置为SourceAlpha。这意味着sourceBlend等于你要混合的颜色的alpha值,这里是0.3f,是你在前面的代码中定义的。而且,你将destBlend设置为InvSourceAlpha,意思是1–SourceAlpha,所以destBlend为1–0.3f = 0.7f。
总的来说,最终颜色的70%会取自后备缓冲中的颜色(即第一个pass存储的模糊图像),剩下的30%取自你想写入到后备缓冲中的新颜色,本例中是原始图像。
你还没有定义originalSampler,在effect文件的顶部添加它:
texture originalImage; sampler originalSampler = sampler_state { texture = <originalImage>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; };
在XNA项目中,你需要将后备缓冲中获取的原始图像存储到originalImage变量中。幸运的是,使用前一个教程的框架,在第一个effect中很容易做到:
if (currentTechnique == 0) { device.ResolveBackBuffer(resolveTexture, 0); textureRenderedTo = resolveTexture; ppEffect.Parameters["originalImage"].SetValue(textureRenderedTo); }
最后一行代码将原始图像发送到HLSL effect中。
现在所有effect准备就绪,代码会首先对图像进行垂直模糊,之后,通过VerBlurAndGlow effect的第一个pass对结果进行水平模糊,在第一个pass之后,模糊过的图像保存在帧缓冲中,让你可以混合到原始图像中!
List<string> ppEffectsList = new List<string>(); ppEffectsList.Add("HorBlur"); ppEffectsList.Add("VerBlurAndGlow"); postProcessor.Parameters["xBlurSize"].SetValue(0.5f); postProcessor.PostProcess(ppEffectsList);
代码
为了能够处理所有的post-processing effects,你需要将纹理采样器stage绑定到包含场景的图像上。发光效果还需要在originalImage变量中存储原始图像。
float xTime; float xBlurSize = 0.5f; texture textureToSampleFrom; sampler textureSampler = sampler_state { texture = <textureToSampleFrom>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; } texture originalImage; sampler originalSampler = sampler_state { texture = <originalImage>; magfilter = LINEAR; minfilter = LINEAR; mipfilter = LINEAR; }; float positions[] = { 0.0f, 0.005, 0.01166667, 0.01833333, 0.025, 0.03166667, 0.03833333, 0.045, }; float weights[] = { 0.0530577, 0.1028506, 0.09364651, 0.0801001, 0.06436224, 0.04858317, 0.03445063, 0.02294906, }; struct PPVertexToPixel { float4 Position : POSITION; float2 TexCoord : TEXCOORD0; }; struct PPPixelToFrame { float4 Color : COLOR0; }; PPVertexToPixel PassThroughVertexShader(float4 inPos: POSITION0, float2 inTexCoord: TEXCOORD0) { PPVertexToPixel Output = (PPVertexToPixel)0; Output.Position = inPos; Output.TexCoord = inTexCoord; return Output; } // PP Technique: HorBlur PPPixelToFrame HorBlurPS(PPVertexToPixel PSIn) : COLOR0 { PPPixelToFrame Output = (PPPixelToFrame)0; for (int i = 0; i < 8; i++) { float4 samplePos = tex2D(textureSampler, PSIn.TexCoord + float2(positions[i], 0)*xBlurSize); samplePos *= weights[i]; float4 sampleNeg = tex2D(textureSampler, PSIn.TexCoord - float2(positions[i], 0)*xBlurSize); sampleNeg *= weights[i]; Output.Color += samplePos + sampleNeg; } return Output; } technique HorBlur { pass Pass0 { VertexShader = compile vs_1_1 PassThroughVertexShader(); PixelShader = compile ps_2_0 HorBlurPS(); } } // PP Technique: VerBlur PPPixelToFrame VerBlurPS(PPVertexToPixel PSIn) : COLOR0 { PPPixelToFrame Output = (PPPixelToFrame)0; for (int i = 0; i < 8; i++) { float4 samplePos = tex2D(textureSampler, PSIn.TexCoord + float2(0, positions[i])*xBlurSize); samplePos *= weights[i]; float4 sampleNeg = tex2D(textureSampler, PSIn.TexCoord - float2(0, positions[i])*xBlurSize); sampleNeg *= weights[i]; Output.Color += samplePos + sampleNeg; } return Output; } technique VerBlur { pass Pass0 { VertexShader = compile vs_1_1 PassThroughVertexShader(); PixelShader = compile ps_2_0 VerBlurPS(); } // PP Technique: VerBlurAndGlow PPPixelToFrame BlendInPS(PPVertexToPixel PSIn) : COLOR0 { PPPixelToFrame Output = (PPPixelToFrame)0; float4 finalColor = tex2D(originalSampler, PSIn.TexCoord); finalColor.a = 0.3f; Output.Color = finalColor; return Output; } technique VerBlurAndGlow { pass Pass0 { VertexShader = compile vs_1_1 PassThroughVertexShader(); PixelShader = compile ps_2_0 VerBlurPS(); } pass Pass1 { AlphaBlendEnable = true; SrcBlend = SrcAlpha; DestBlend = InvSrcAlpha; PixelShader = compile ps_2_0 BlendInPS(); } }
扩展阅读
每一个完工的游戏都包含一组post-processing effects,因为它们可以在最终图像上施加额外的处理。模糊效果相对来说是简单的,一些模糊或发光常用来掩盖3D物体边缘的锯齿。
post-processing effects牵涉的东西很多,但通过使用多个pass和alpha混合的发光效果,你可以知道基本的原理。