URP管线,RenderFeature后处理模糊小记

参数界面

参考文章:Carle专栏 算法取自毛老师 https://zhuanlan.zhihu.com/p/125744132
模糊算法这里用的是Kawase和DualKawase,最后整合到了一起~~
文末附代码~~~~~
(才接触RenderFeature,如有解释不准确的地方,希望斧正 感谢)
简单解释一下Kawase思路:

一、算法解析

Kawase模糊精华在于算子是随模糊迭代而变化的,图中黑点为被渲染像素点,红点为采样点,由采样点的值计算出被渲染像素点的值。注意,这里4个采样点位于黑点像素的四个角!!四个角!意味着这里UV偏移就不能是意义上的One pixe,而是偏移半个像素点的距离,知道这里后咋们上蒜子图~~

二、shader部分
回顾KawaseBlur图,第一次采样UV偏移为 0.5 * KawaseBlurKernel[m] * TexturePixeSize。第二次采样UV偏移为1.5*XXX,第三次采样为2.5*XXX~~~如此类推,所以说算子是迭代变化的。上shader关键代码:

三、RenderFeature部分
shader都是小菜,RenderFeature才是最复杂的,上菜!!!

这里我喜欢把Setting、Feature、Pass分别定义成单独的CS文件。
1、Setting解析
这里定义的参数是显示到面板的接口,


看红框部分!!!
药点
①[System.Serializable]是“序列化参数列表”,没懂什么意思,大概看来就是配合后面在Feature中定义好Public该类,就能暴露出参数,可通过面板直接修改。
②这里shader可直接Feature中指定路径,也可以暴露出来手动指定
③downsamplescale是将处理源图像给缩小的倍数,passloop是迭代次数,BlurRadius是将要传入到Pass中的定义的Offset值
2、Feature解析

药点
①Create函数当Setting变化时执行,所以这里我们将函数外声明好的m_pass放到这里面进行实例化,他的有参构造函数在Pass的Class中,简单的赋值并指定渲染时机。

②AddRenderPasses每帧执行,将Pass放入到缓冲池里。这里可以做一个判空,有需要再Debug一下也行。
③Setupsource函数是我们定义的将相机图像传入到RT的一个函数。
3、Pass解析,重中之重来咯

这里定义了Pass类中需要的变量,以及enum类型的BlurPassIndex。另外临时RT的申请流程分三步走,
先用 Shader.PropertyToID("XXXX")定义int类型的ID,
再用cmd.GetTemporaryRT(ID)申请RT,
最后实例化RT(将RT与ID绑定的操作)RenderTargetIdentifier RTNAME = new RenderTargetIdentifier(ID)。

这里因为后面涉及到RT在循环中申请和释放,所以我只是先声明了RT并未将其实例化。
Pass的大体结构可分为三部分
第一部分:

OnCameraSetup函数是初始化函数,可以将申请RT、配置RT、申请变量等初始化操作放在这里面执行。(这里的操作也可以放到第二部分Execute中做,看个人选择和功能需求)
第二部分:

Render函数如图

Execute函数就是具体Pass的执行函数了,也是最重要的部分。
药点
①因为Execute函数中没有自定义commandBuffer,所以我们会在函数体内用CommandBufferPool.Get(“PassNameXXX”)命令申请一个CommandBuffer类型的缓冲区命令,最后由context.ExecuteCommandBuffer(cmd);注入到缓冲池中
②用了个Switch控制Feature执行哪个函数,因为我们将Kawase和DualKawase写到了一起
③申请的缓冲区命令和RT一样,最终都需要手动去释放以免内存泄漏。CommandBufferPool.Release(cmd);
④看Render函数,先用cmd.Blit命令将Source图像复制给RT1,再结合for函数和一个bool值和语法糖,乒乓执行Pass

稍等一下,插句嘴



看到这里细心的朋友应该注意到了,我们用RT1 RT2都是先申请后使用,为什么这个this.Source只是声明了都没申请就能用!!!!
而且最后一个Blit我们将结果直接复制到Source中就收工大吉了,
在FreamDebug中却显示我们最后的Blit是将结果Copy给了cameraColorTarget
这是为什么呢???有的同学肯定就说了Class类直接用等号赋值相当于引用,可以理解为指针。但是我发现RT、cameraColorTarget、Source的类型RenderTargetIdentifier为结构体!!不是Class!!!结构体等号赋值实际上是新定义了一个变量,这里把我困扰到了,所以我觉得最合理的应该把这里的Source全部替换为cameraColorTarget,省去AddRenderPasses里的那步,我懒了 试过可行,这儿就把重新截图了。Unity肯定哪步将二者的地址给指定了一下,所以我最后也不用再释放Source这个变量了......
有知道的朋友能告知一下原因吗!!!!!!!感谢了

继续药点
⑤这里这个语法糖我觉得非常巧妙,避免了我们去再定义一个逻辑或者RT来充当temp打工人。
⑥最后释放两张RT,这步其实可以放到接下来要截图的第三部分中,但是我还是就写在第二步的Execute中吧。
三、第三部分

就是可以用来最终注销释放RT等操作,没什么可说的。
DualKawase就先鸽一鸽吧,码字太累了...
上源码:
BlurSetting.cs

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
namespace BlurSettingnamespace
{
[System.Serializable]
public class BlurSetting
{
public Shader shader = null;
public BlurType blurType = BlurType.Kawase;
public RenderPassEvent passEvent = RenderPassEvent.AfterRenderingSkybox;
public enum BlurType
{
Kawase ,
DualKawase,
Gaussian
}
}
[System.Serializable]
public class KawaseBlurSetting
{
[Range(1, 10)]
public int downsamplescale = 1;
[Range(0, 10)]
public int passloop = 5;
[Range(0.0f, 5.0f)]
public float BlurRadius = 1.0f;
}
[System.Serializable]
public class DualKawaseBlurSetting
{
[Range(0, 10)]
public int iteration = 5;
[Range(0.0f, 5.0f)]
public float BlurRadius = 1.0f;
}
}

KawaseBlurRenderPass.cs

using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using BlurSettingnamespace;
namespace KawaseBlurRenderPassnamespace
{
class KawaseBlurRenderPass : ScriptableRenderPass
{
private BlurSetting m_setting;
private KawaseBlurSetting kawaseBlurSetting;
private DualKawaseBlurSetting dualKawaseBlurSetting;
enum BlurPassIndex
{
KawasePass = 0,
DualKawaseUpPass = 1,
DualKawaseDownPass = 2
}
public KawaseBlurRenderPass(BlurSetting blurSetting, KawaseBlurSetting kawaseBlurSetting , DualKawaseBlurSetting dualKawaseBlurSetting)
{
this.m_setting = blurSetting;
this.kawaseBlurSetting = kawaseBlurSetting;
this.dualKawaseBlurSetting = dualKawaseBlurSetting;
this.renderPassEvent = blurSetting.passEvent;
}
//定义源RT
private RenderTargetIdentifier Source { get; set; }
//声明两个临时RT(未实例化) 定义其ID
private readonly int TempRTID1 = Shader.PropertyToID("TempRTID1Name");
private readonly int TempRTID2 = Shader.PropertyToID("TempRTID2Name");
RenderTargetIdentifier TempRT1;
RenderTargetIdentifier TempRT2;
//定义一个shader参数ID
private readonly int _OffsetID = Shader.PropertyToID("_Offset");
Material passmat;
public void Setupsource(RenderTargetIdentifier source)
{
this.Source = source;
}
// This method is called before executing the render pass.
// It can be used to configure render targets and their clear state. Also to create temporary render target textures.
// When empty this render pass will render to the active camera render target.
// You should never call CommandBuffer.SetRenderTarget. Instead call <c>ConfigureTarget</c> and <c>ConfigureClear</c>.
// The render pipeline will ensure target setup and clearing happens in a performant manner.
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
if(m_setting.blurType == BlurSetting.BlurType.Kawase)
Initialize(cmd, renderingData);
if (m_setting.shader == null)
return;
else
passmat = new Material(m_setting.shader);
}
// Here you can implement the rendering logic.
// Use <c>ScriptableRenderContext</c> to issue drawing commands or execute command buffers
// https://docs.unity3d.com/ScriptReference/Rendering.ScriptableRenderContext.html
// You don't have to call ScriptableRenderContext.submit, the render pipeline will call it at specific points in the pipeline.
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
CommandBuffer cmd = CommandBufferPool.Get("KawaseBlurPass");
//重要操作
switch(m_setting.blurType)
{
case BlurSetting.BlurType.Kawase:
Render(cmd , renderingData);
break;
case BlurSetting.BlurType.DualKawase:
DualRender(cmd, renderingData);
break;
default:
break;
}
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
// Cleanup any allocated resources that were created during the execution of this render pass.
public override void OnCameraCleanup(CommandBuffer cmd)
{
}
void Initialize(CommandBuffer cmd , RenderingData renderingData)
{
RenderTextureDescriptor renderTextureDescriptor = renderingData.cameraData.cameraTargetDescriptor;
int width = Mathf.Max(renderTextureDescriptor.width / kawaseBlurSetting.downsamplescale, 1);
int height = Mathf.Max(renderTextureDescriptor.height / kawaseBlurSetting.downsamplescale, 1);
//申请两个临时RT
cmd.GetTemporaryRT(TempRTID1, width, height, 0, FilterMode.Trilinear, renderTextureDescriptor.colorFormat);
cmd.GetTemporaryRT(TempRTID2, width, height, 0, FilterMode.Trilinear, renderTextureDescriptor.colorFormat);
//指定RT 实例化的过程
TempRT1 = new RenderTargetIdentifier(TempRTID1);
TempRT2 = new RenderTargetIdentifier(TempRTID2);
}
void DualInitialize(CommandBuffer cmd , RenderingData renderingData)
{
//RenderTextureDescriptor renderTextureDescriptor = renderingData.cameraData.cameraTargetDescriptor;
}
void Render(CommandBuffer cmd ,RenderingData renderingData)
{
cmd.Blit(this.Source, TempRTID1);
bool NeedSwith = true;
for(int i = 1;i < (kawaseBlurSetting.passloop + 1); i++)
{
//cmd.SetGlobalFloat(_OffsetID , i / kawaseBlurSetting.downsamplescale + kawaseBlurSetting.BlurRadius);
cmd.SetGlobalFloat(_OffsetID , i + kawaseBlurSetting.BlurRadius);
cmd.Blit(NeedSwith ? TempRT1 : TempRT2, NeedSwith ? TempRT2 : TempRT1, passmat, (int)BlurPassIndex.KawasePass);
NeedSwith = !NeedSwith;
}
cmd.Blit(NeedSwith ? TempRT1 : TempRT2, this.Source);
//释放RT
cmd.ReleaseTemporaryRT(TempRTID1);
cmd.ReleaseTemporaryRT(TempRTID2);
}
void DualRender(CommandBuffer cmd , RenderingData renderingData)
{
RenderTextureDescriptor renderTextureDescriptor = renderingData.cameraData.cameraTargetDescriptor;
int Width = renderTextureDescriptor.width;
int Height = renderTextureDescriptor.height;
bool IsChange = true;
//初始化RT1 RT2
SetupRT(cmd, TempRT1, TempRTID1, Width, Height);
Width /= 2;
Height /= 2;
SetupRT(cmd, TempRT2, TempRTID2, Width , Height);
//将Source传入RT1中
cmd.Blit(this.Source, TempRTID1);
//传递参数给shader
cmd.SetGlobalFloat(_OffsetID, dualKawaseBlurSetting.BlurRadius);
//DownSample
for (int i = 1;i < dualKawaseBlurSetting.iteration + 1; i++)
{
Width /= 2;
Height /= 2;
cmd.Blit(IsChange ? TempRTID1 : TempRTID2,
IsChange ? TempRTID2 : TempRTID1, passmat, (int)BlurPassIndex.DualKawaseDownPass);
IsChange = !IsChange;
//释放RT 准备参加下次循环计算
cmd.ReleaseTemporaryRT(IsChange ? TempRTID2 : TempRTID1);
//重新申请RT 准备参加下次循环计算
SetupRT(cmd, IsChange ? TempRT2 : TempRT1, IsChange ? TempRTID2 : TempRTID1, Width, Height);
}
//找到最后参与Pass计算的RT 此时两张RT都已申请,未释放
//释放最后申请的RT
cmd.ReleaseTemporaryRT(IsChange ? TempRTID2 : TempRTID1);
//重新申请正常尺寸的RT
Width *= 4;
Height *= 4;
SetupRT(cmd, IsChange ? TempRT2 : TempRT1, IsChange ? TempRTID2 : TempRTID1, Width, Height);
//升采样循环
for (int i = 1; i < dualKawaseBlurSetting.iteration + 1; i++)
{
Width *= 2;
Height *= 2;
cmd.Blit(IsChange ? TempRTID1 : TempRTID2,
IsChange ? TempRTID2 : TempRTID1, passmat, (int)BlurPassIndex.DualKawaseUpPass);
IsChange = !IsChange;
//释放RT
cmd.ReleaseTemporaryRT(IsChange ? TempRTID2 : TempRTID1);
//重新申请RT
SetupRT(cmd, IsChange ? TempRT2 : TempRT1, IsChange ? TempRTID2 : TempRTID1, Width, Height);
}
//最后将最后参与计算的RT传递回源RT中,再将两个RT都释放
cmd.Blit(IsChange ? TempRTID1 : TempRTID2, this.Source);
cmd.ReleaseTemporaryRT(TempRTID1);
cmd.ReleaseTemporaryRT(TempRTID2);
}
void SetupRT(CommandBuffer cmd, RenderTargetIdentifier renderTargetIdentifier, int ID , int width , int heigh)
{
//申请临时RT
cmd.GetTemporaryRT(ID, width, heigh, 0, FilterMode.Trilinear, RenderTextureFormat.Default);
//实例化RT
renderTargetIdentifier = new RenderTargetIdentifier(ID);
}
}
}

KawaseBlurRenderfeature.cs

using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;
using KawaseBlurRenderPassnamespace;
using BlurSettingnamespace;
public class KawaseBlurRenderfeature : ScriptableRendererFeature
{
public BlurSetting blurSetting = new();
public KawaseBlurSetting kawaseBlurSetting = new();
public DualKawaseBlurSetting dualKawaseBlurSetting = new();
//public DualKawaseBlurSetting dualKawaseBlurSetting;
KawaseBlurRenderPass m_pass;
public override void Create()
{
m_pass = new(blurSetting, kawaseBlurSetting, dualKawaseBlurSetting);
}
// Here you can inject one or multiple render passes in the renderer.
// This method is called when setting up the renderer once per-camera.
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
m_pass.Setupsource(renderer.cameraColorTarget);
if(blurSetting.shader != null && kawaseBlurSetting.passloop != 0 && dualKawaseBlurSetting.iteration != 0)
renderer.EnqueuePass(m_pass);
}
}

shader源码(本来我写成了三个文件,但是路径难免会不同,我就随便整合成一个了,凑合用吧)

Shader "Post/KawaseBlur"
{
Properties
{
[HideInInspector] _MainTex ("Texture", 2D) = "white" { }
}
HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
//#include "../../RenderFeature/PostShaderSource/postprocessing.hlsl"
//#include "../../RenderFeature/PostShaderSource/KawaseBlurKernel.hlsl"
/////////////////////////////////////////////////////////////////////////////////////////
//kawaseBlur算子和双重向下采样算子
static float2 KawaseBlurKernel[4] = {
float2(-1, -1), //左下
float2(-1, 1), //左上
float2(1, 1), //右上
float2(1, -1) //右下
};
//双重向上采样算子
static float2 KawaseBlurKernel_Up[8] = {
float2(0, 1),
float2(0.5, 0.5),
float2(1, 0),
float2(0.5, -0.5),
float2(0, -1),
float2(-0.5, -0.5),
float2(-1, 0),
float2(-0.5, 0.5),
};
float4 KawaseBlur(TEXTURE2D(Tex), SAMPLER(samplertex), float2 uv, float2 TexSize, float PixeOffset)
{
float4 col = 0;
int loopnum = 4;
for (int i = 0; i < loopnum; i++)
col += SAMPLE_TEXTURE2D(Tex, samplertex, uv + (float2) (PixeOffset +0.5f) * KawaseBlurKernel[i] * TexSize);
return col / loopnum;
}
float4 DualKawaseBlur_Up(TEXTURE2D(Tex), SAMPLER(samplertex), float2 uv, float2 TexSize, float PixeOffset)
{
float4 col = 0;
for (int i = 4; i < 0; i++)
{
col += SAMPLE_TEXTURE2D(Tex, samplertex, uv + KawaseBlurKernel_Up[i] * (1.0 + PixeOffset) * TexSize);
col += SAMPLE_TEXTURE2D(Tex, samplertex, uv + KawaseBlurKernel_Up[i + 1] * (1.0 + PixeOffset) * TexSize) * 2;
}
return col * 0.0833;
}
float4 DualKawaseBlur_Down(TEXTURE2D(Tex), SAMPLER(samplertex), float2 uv, float2 TexSize, float PixeOffset)
{
float4 col = SAMPLE_TEXTURE2D(Tex, samplertex, uv) * 0.5;
for (int i = 0; i < 4; i++)
col += SAMPLE_TEXTURE2D(Tex, samplertex, uv + KawaseBlurKernel[i] * (float2)PixeOffset * TexSize) * 0.125;
return col;
}
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
TEXTURE2D(_MainTex);
SAMPLER(sampler_MainTex);
CBUFFER_START(UnityPerMaterial)
float4 _MainTex_TexelSize;
CBUFFER_END
v2f VertDefault(appdata v)
{
v2f o;
o.vertex = TransformObjectToHClip(v.vertex.xyz);
o.uv = v.uv;
return o;
}
//////////////////////////////////////////////////////////////////////////////////////////////
float _Offset;
float4 frag(v2f i) : SV_Target
{
// sample the texture
float4 col = KawaseBlur(_MainTex, sampler_MainTex, i.uv, _MainTex_TexelSize.xy, _Offset);
return col;
}
float4 frag_Up(v2f i) : SV_Target
{
// sample the texture
float4 col = KawaseBlur(_MainTex, sampler_MainTex, i.uv, _MainTex_TexelSize.xy, _Offset);
return col;
}
float4 frag_Down(v2f i) : SV_Target
{
// sample the texture
float4 col = KawaseBlur(_MainTex, sampler_MainTex, i.uv, _MainTex_TexelSize.xy, _Offset);
return col;
}
ENDHLSL
SubShader
{
Tags { "RenderType" = "Opaque" }
Cull Off
ZWrite Off
ZTest Always
Pass
{
Name "KawaseBlurPass"
HLSLPROGRAM
#pragma vertex VertDefault
#pragma fragment frag
ENDHLSL
}
Pass
{
Name "DualKawaseBlurPass_UpSampling"
HLSLPROGRAM
#pragma vertex VertDefault
#pragma fragment frag_Up
ENDHLSL
}
Pass
{
Name "DualKawaseBlurPass_DownSampling"
HLSLPROGRAM
#pragma vertex VertDefault
#pragma fragment frag_Down
ENDHLSL
}
}
}