
该模糊算法取自该文章https://blog.csdn.net/poem_qianmo/article/details/105350519。
眼馋它极好的性能,故本人把该模糊移植到URP管线下使用。双重模糊里双重的意思是采样迭代过程中的先降低RT尺寸在提升RT尺寸,最终就以更小的内存,更少的绘制次数达到很好的模糊效果。下图是只绘制6次就达到非常出色的模糊效果的展示图。

效果图
第一步:先介绍他的shader部分,shader所使用的算子在升采样和降采样的pass不一样,这里分开介绍。降采样的算子如下。

降采用的算子
降采样过程中,每个像素采样了5个像素,中间的占1/2,边缘4个占1/8,这样它的着色器的写法如下图,非常简单。

降采样pass
而升采样过程中,他采样了8个算子。

升采样算子
四个近的算子权重为1/6,远的算子权重为1/12,着色器写法也简单,如下图所示。

升采样pass
shader是开胃菜,而自定义的render feather才是真正的麻烦(各位程序大大请轻喷,本人只是个小美术,对美术摸urp的新东西确实挺头疼的)。下面本人说明一下render feather的内容。
第二步,自定义render feather。先定义一个setting,这个大家都懂。

定义seting
初始化和传参数的函数。之前专栏里说了多次,也不多讲了。

然后就是定义custompass类了。熟悉的老伙计,但是本人改了不少内容和之前的不一样,这里说明下。
定义level结构体,存down和up,它是为了记录升采样和降采样对应的RT的id的。由于我们循环过程之中,要定义很多个不同尺寸大小RT的id,索性在定义一个level的数组来存储。然后在初始化的时候,把他们初始化一下,但是注意id不能重复哦。

初始化
执行函数还是和之前一致。后面的循环略用得比较取巧:在第一个降采样的循环过程中,就把升采样的RT也申请了。故在第二个升采样的循环中并未申请RT.

执行函数
我手绘了一张图来解释这个执行函数里的每一步对应的那个阶段,字写得不好请轻喷,参考我这张图,读者应该可以更快速理解每一步的作用了吧。

图例
至此,双重模糊的解析就完成了,下面再附录我的render feather的源码和shader源码。
Render Feather:
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
public class Doualblur : ScriptableRendererFeature
{
[System.Serializable]public class mysetting//定义一个设置的类
{
public RenderPassEvent passEvent=RenderPassEvent.AfterRenderingTransparents;//默认插到透明完成后
public Material mymat;
[Range(1,8)]public int downsample=2;
[Range(2,8)]public int loop=2;
[Range(0.5f,5f)]public float blur=0.5f;
public string RenderFeatherName="我的双重Kawase模糊";//render feather的名字
}
public mysetting setting=new mysetting();
class CustomRenderPass : ScriptableRenderPass//自定义pass
{
public Material passMat=null;
public int passdownsample=2;//降采样
public int passloop=2;//模糊的迭代次数
public float passblur=4;
private RenderTargetIdentifier passSource{get;set;}
RenderTargetIdentifier buffer1;//RTa1的ID
RenderTargetIdentifier buffer2;//RTa2的ID
string RenderFeatherName;//feather名
struct LEVEL//
{
public int down;
public int up;
};
LEVEL[] my_level;
int maxLevel=16;//指定一个最大值来限制申请的ID的数量,这里限制到16个,这么多肯定用不完了
public CustomRenderPass(string name)//构造函数
{
RenderFeatherName=name;
}
public void setup(RenderTargetIdentifier sour)//初始化,接收render feather传的图
{
this.passSource=sour;
my_level = new LEVEL[maxLevel];
for (int t = 0; t < maxLevel; t++)//申请32个ID的,up和down各16个,用这个id去代替临时RT来使用
{
my_level[t] = new LEVEL
{
down = Shader.PropertyToID("_BlurMipDown"+t),
up = Shader.PropertyToID("_BlurMipUp"+t)
};
}
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)//执行
{
CommandBuffer cmd=CommandBufferPool.Get(RenderFeatherName);//定义cmd
passMat.SetFloat("_Blur",passblur);//指定材质参数
//cmd.SetGlobalFloat("_Blur",passblur);//设置模糊,但是我不想全局设置怕影响其他的shader,所以注销它了用上面那个,但是cmd这个性能可能好些?
RenderTextureDescriptor opaquedesc=renderingData.cameraData.cameraTargetDescriptor;//定义屏幕图像参数结构体
int width=opaquedesc.width/passdownsample;//第一次降采样是使用的参数,后面就是除2去降采样了
int height=opaquedesc.height/passdownsample;
opaquedesc.depthBufferBits=0;
//down
RenderTargetIdentifier LastDown=passSource;//把初始图像作为lastdown的起始图去计算
for(int t=0;t<passloop;t++)
{
int midDown=my_level[t].down;//middle down ,即间接计算down的工具人ID
int midUp=my_level[t].up; //middle Up ,即间接计算的up工具人ID
cmd.GetTemporaryRT(midDown,width,height,0,FilterMode.Bilinear,RenderTextureFormat.ARGB32);//对指定高宽申请RT,每个循环的指定RT都会变小为原来一半
cmd.GetTemporaryRT(midUp,width,height,0,FilterMode.Bilinear,RenderTextureFormat.ARGB32);//同上,但是这里申请了并未计算,先把位置霸占了,这样在UP的循环里就不用申请RT了
cmd.Blit(LastDown,midDown,passMat,0);//计算down的pass
LastDown=midDown;//工具人辛苦了
width=Mathf.Max(width/2,1);//每次循环都降尺寸
height=Mathf.Max(height/2,1);
}
//up
int lastUp=my_level[passloop-1].down;//把down的最后一次图像当成up的第一张图去计算up
for(int j=passloop-2;j>=0;j--)//这里减2是因为第一次已经有了要减去1,但是第一次是直接复制的,所以循环完后还得补一次up
{
int midUp=my_level[j].up;
cmd.Blit(lastUp,midUp,passMat,1);//这里直接开干就是因为在down过程中已经把RT的位置霸占好了,这里直接用,不虚
lastUp=midUp;//工具人辛苦了
}
cmd.Blit(lastUp,passSource,passMat,1);//补一次up,顺便就输出了
context.ExecuteCommandBuffer(cmd);//执行命令缓冲区的该命令
CommandBufferPool.Release(cmd);//释放cmd
for(int k=0;k<passloop;k++)//清RT,防止内存泄漏
{
cmd.ReleaseTemporaryRT(my_level[k].up);
cmd.ReleaseTemporaryRT(my_level[k].down);
}
}
}
CustomRenderPass mypass;
public override void Create()//进行初始化,这里最先开始
{
mypass = new CustomRenderPass(setting.RenderFeatherName);//实例化一下并传参数,name就是tag
mypass.renderPassEvent=setting.passEvent;
mypass.passblur=setting.blur;
mypass.passloop=setting.loop;
mypass.passMat=setting.mymat;
mypass.passdownsample=setting.downsample;
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)//传值到pass里
{
mypass.setup(renderer.cameraColorTarget);
renderer.EnqueuePass(mypass);
}
}
shader源码,注意我的2个pass的v2f结构体是用的同一个,而down的pass并没有完全用完结构体,这样会造成一些性能损失,读者若是在正式项目建议分开写v2f,不要学我偷懒。
Shader "WX/URP/Post/DualBlur"
{
Properties
{
//
[HideInInspector]_MainTex("MainTex",2D)="white"{}
//_Blur("Blur",float)=3
}
SubShader
{
Tags{
"RenderPipeline"="UniversalRenderPipeline"
}
Cull Off ZWrite Off ZTest Always
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_ST;
float4 _MainTex_TexelSize;
float _Blur;
CBUFFER_END
TEXTURE2D( _MainTex);
SAMPLER(sampler_MainTex);
struct a2v
{
float4 positionOS:POSITION;
float2 texcoord:TEXCOORD;
};
struct v2f
{
float4 positionCS:SV_POSITION;
float4 texcoord[4]:TEXCOORD;
};
ENDHLSL
pass//Down
{
NAME"Down"
HLSLPROGRAM
#pragma vertex VERT
#pragma fragment FRAG
v2f VERT(a2v i)
{
v2f o;
o.positionCS=TransformObjectToHClip(i.positionOS.xyz);
o.texcoord[2].xy=i.texcoord;
o.texcoord[0].xy=i.texcoord+float2(1,1)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;
o.texcoord[0].zw=i.texcoord+float2(-1,1)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;
o.texcoord[1].xy=i.texcoord+float2(1,-1)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;
o.texcoord[1].zw=i.texcoord+float2(-1,-1)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;
return o;
}
half4 FRAG(v2f i):SV_TARGET
{
half4 tex=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord[2].xy)*0.5;
for(int t=0;t<2;t++)
{
tex+=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord[t].xy)*0.125;
tex+=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord[t].zw)*0.125;
}
return tex;
}
ENDHLSL
}
pass//up
{
NAME"Up"
HLSLPROGRAM
#pragma vertex VERT
#pragma fragment FRAG
v2f VERT(a2v i)
{
v2f o;
o.positionCS=TransformObjectToHClip(i.positionOS.xyz);
o.texcoord[0].xy=i.texcoord+float2(1,1)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;
o.texcoord[0].zw=i.texcoord+float2(-1,1)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;
o.texcoord[1].xy=i.texcoord+float2(1,-1)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;
o.texcoord[1].zw=i.texcoord+float2(-1,-1)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;
o.texcoord[2].xy=i.texcoord+float2(0,2)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;
o.texcoord[2].zw=i.texcoord+float2(0,-2)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;
o.texcoord[3].xy=i.texcoord+float2(-2,0)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;
o.texcoord[3].zw=i.texcoord+float2(2,0)*_MainTex_TexelSize.xy*(1+_Blur)*0.5;
return o;
}
half4 FRAG(v2f i):SV_TARGET
{
half4 tex=0;
for(int t=0;t<2;t++)
{
tex+=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord[t].xy)/6;
tex+=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord[t].zw)/6;
}
for(int k=2;k<4;k++)
{
tex+=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord[k].xy)/12;
tex+=SAMPLE_TEXTURE2D(_MainTex,sampler_MainTex,i.texcoord[k].zw)/12;
}
return tex;
}
ENDHLSL
}
}
}