一般说起模糊,大体都会想到卷积以及高斯模糊进行处理。
模糊的核心思想在于对图像高频信号的滤波(反向滤波的话可以用作边缘检测),由于涉及到傅里叶快速展开,如有兴趣可以去参考一些关于图像频域转换的知识,这里就不做过多展开。
但是由于高斯模糊需要在二维图像上对两个一维进行分别计算,消耗两个PASS,开销还是有一定大。一般对模糊品质没有特别需求的话,常规会使用Kawase(川瀬)模糊进行替换。
川瀬 Blur于Masaki Kawase 在GDC2003的分享《Frame Buffer Postprocessing Effects in DOUBLE-S.T.E.A.L (Wreckless)》中提出。
Kawase模糊是一种后处理效果,最初用于Bloom特效,但后来被推广为一种专门的模糊算法。它在模糊外观上与高斯模糊非常接近。由于采用了随迭代次数移动的blur kernel,而不是像高斯模糊或box blur一样从头到尾固定的blur kernel。所以相对来说实际的开销会更小一些。
Kawase模糊的思路如下:
- 对距离当前像素越来越远的地方的四个角进行采样。
- 在两个大小相等的纹理之间进行乒乓式的blit(即交替复制)。
备注:SIGGRAPH 2015上ARM团队提出的一种衍生自Kawase Blur的模糊算法Dual Kawase Blur,优化了性能表现。
具体思路是在C#处,基于当前迭代次数,对每次模糊的半径进行设置,并在Shader内实现一个Filter进行处理,如下
fixed4 frag (v2f input) : SV_Target
{
float2 res = _MainTex_TexelSize.xy;
float i = _offset;
fixed4 col;
col.rgb = tex2D( _MainTex, input.uv ).rgb;
col.rgb += tex2D( _MainTex, input.uv + float2( i, i ) * res ).rgb;
col.rgb += tex2D( _MainTex, input.uv + float2( i, -i ) * res ).rgb;
col.rgb += tex2D( _MainTex, input.uv + float2( -i, i ) * res ).rgb;
col.rgb += tex2D( _MainTex, input.uv + float2( -i, -i ) * res ).rgb;
col.rgb /= 5.0f;
return col;
}
由于URP修改了渲染管线,不能使用OnRenderImage()进行后道处理,所以需要继承ScriptableRendererFeature来实现RenderFeature
public partial class URPKawaseBlur : ScriptableRendererFeature
{
public URPKawaseBlurSettings settings = new URPKawaseBlurSettings();
private URPKawaseBlurPass _blurPass;
public override void Create()
{
_blurPass = new URPKawaseBlurPass("KawaseBlur");
_blurPass.blurMaterial = settings.blurMaterial;
_blurPass.passCount = settings.blurPasses;
_blurPass.Downsampling = settings.downsample;
_blurPass.copyToFramebuffer = settings.copyToFramebuffer;
_blurPass.rtID = Shader.PropertyToID(settings.targetName);
_blurPass.renderPassEvent = settings.renderPassEvent;
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
renderer.EnqueuePass(_blurPass);
}
public override void SetupRenderPasses(ScriptableRenderer renderer, in RenderingData renderingData)
{
#if UNITY_2022_1_OR_NEWER
_blurPass.Setup(renderer.cameraColorTargetHandle);
#else
_blurPass.Setup(renderer.cameraColorTarget);
#endif
}
}
这里需要注意,由于Unity在URP 2022.1修改了原先的设计,用RTHandles替换了RenderTargetHandle,需要补充一个条件编译指令,不然会报错。
详情:Upgrading to version 2022.1 of the Universal Render Pipeline | Universal RP | 13.1.9 (unity3d.com)
C#处的pingpong迭代:
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
CommandBuffer cmd = CommandBufferPool.Get(profilerTag);
RenderTextureDescriptor opaqueDesc = renderingData.cameraData.cameraTargetDescriptor;
opaqueDesc.depthBufferBits = 0;
// first pass
cmd.SetGlobalFloat("_offset", 1.5f);
cmd.Blit(source, tmpRT1, blurMaterial);
for (var i = 1; i < passCount - 1; i++)
{
cmd.SetGlobalFloat("_offset", 0.5f + i);
cmd.Blit(tmpRT1, tmpRT2, blurMaterial);
// pingpong
var rttmp = tmpRT1;
tmpRT1 = tmpRT2;
tmpRT2 = rttmp;
}
// final pass
cmd.SetGlobalFloat("_offset", 0.5f + passCount - 1f);
if (copyToFramebuffer)
{
cmd.Blit(tmpRT1, source, blurMaterial);
}
else
{
cmd.Blit(tmpRT1, tmpRT2, blurMaterial);
cmd.SetGlobalTexture(rtID, tmpRT2);
}
context.ExecuteCommandBuffer(cmd);
cmd.Clear();
CommandBufferPool.Release(cmd);
}
创建好了这个后处理feature之后,将其加入到现有的管线中。
如果不知道自己用了哪种管线,可以通过Project Setting查看(这里由于后期还需要查看一些Deferred管线的效果,选了High Fidelity管线,个人可依照自己需求进行调整)
当然这个后道的shader只是负责生成一张全局的Blur Render Texture,实际在游戏中还需要制作一个将这全局模糊RT映射给场景物体的材质。
所以这里需要将屏幕空间位置计算到材质上
v2f vert (appdata v)
{
v2f o;
o.color = v.color;
o.vertex = UnityObjectToClipPos(v.vertex);
o.texcoord = TRANSFORM_TEX(v.uv, _MainTex);
float4 screenPos = ComputeScreenPos(o.vertex);
o.uv = screenPos.xy / screenPos.w;
return o;
}
这里需要注意一下函数得出的齐次坐标需要除一下w分量做变换。(齐次坐标系真好用…(x
float4 screenPos = ComputeScreenPos(o.vertex);
o.uv = screenPos.xy / screenPos.w;
像素着色器这里直接对着这个UV采样映射就可以了
fixed4 frag (v2f input) : SV_Target
{
fixed4 color = tex2D(_BlurTexture, input.uv);
color.a = 1;
return color;
}
渲染一下看看效果