常见模糊后处理
模糊
最近做的很多效果最终都需要模糊,如镜面反射要模糊模拟粗糙度反射,体积光要降采样然后模糊来降低性能消耗同时柔化效果
更不要说UI上需要模糊背景,透明物体要毛玻璃等等效果,还是很有必要好好整理一下相关的方案同时打包成库方便以后调用。
这就不得不提及毛星云大佬的高品质后处理:十种图像模糊算法的总结与实现。
其中的全部方案都包含在他的开源项目X-PostProcessing-Library中。
当中提及后续也将提供对Unity引擎URP/LWRP/HDRP的兼容支持。
但如今也只能惋惜了。
本着学习以及方便自己日后使用的目的,暂将其中模糊相关实用的部分整理成URP支持的RendererFeature、VolumeComponent等形式。
当然本仓库也不是单纯的翻译,除了可以用作全屏后处理,也抽象成工具类库,方便其他非相机的效果调用模糊处理。
同时提供了一些特定的RenderFeature,比如3D场景中的磨砂玻璃等。
项目地址
这里只记录和URP相关的部分,具体模糊的算法就不赘述了。


Script
先把这里的各个模糊后处理Pass中通用的部分提出来,抽象成一个基类
public abstract class BaseBlurRendererPass : ScriptableRenderPass
{
protected abstract BlurRendererFeature.ProfileId ProfileId { get; }
protected abstract string ShaderName { get; }
protected string MaskBlendShaderName = "KuanMi/MaskBlend";
protected Shader m_Shader;
protected ScriptableRenderer m_Renderer;
protected Material m_Material;
protected Material m_BlendMaterial;
protected RenderTextureDescriptor descriptor;
protected static readonly int BlurRadius = Shader.PropertyToID("_Offset");
protected static readonly int BlitTexture = Shader.PropertyToID("_BlitTexture");
protected static readonly int BlurOffset = Shader.PropertyToID("_BlurOffset");
protected static readonly int BlitTextureSt = Shader.PropertyToID("_BlitTexture_ST");
internal static readonly int GoldenRot = Shader.PropertyToID("_GoldenRot");
internal static readonly int Params = Shader.PropertyToID("_Params");
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
base.OnCameraSetup(cmd, ref renderingData);
var cameraTargetDescriptor = renderingData.cameraData.cameraTargetDescriptor;
descriptor = cameraTargetDescriptor;
descriptor.depthBufferBits = 0;
descriptor.msaaSamples = 1;
}
public abstract void ExecuteWithCmd(CommandBuffer cmd, ref RenderingData renderingData);
public virtual bool Setup(ScriptableRenderer renderer)
{
if(!GetMaterial())
return false;
m_Renderer = renderer;
return true;
}
private bool GetMaterial()
{
if (m_Material != null)
{
return true;
}
if (m_Shader == null)
{
m_Shader = Shader.Find(ShaderName);
if (m_Shader == null)
{
return false;
}
}
m_Material = CoreUtils.CreateEngineMaterial(m_Shader);
m_BlendMaterial = CoreUtils.CreateEngineMaterial(Shader.Find(MaskBlendShaderName));
return m_Material != null;
}
public virtual void Dispose()
{
CoreUtils.Destroy(m_Material);
}
}
为了便于在Pass中获取Volume的属性,再提一个泛型抽象类出来
public abstract class BaseBlurRendererPassWithVolume<K> : BaseBlurRendererPass where K : BaseBlur
{
protected K blurVolume;
protected MaskBlur maskBlur;
protected bool isMask => maskBlur.isMask.value;
protected RTHandle m_MaskTexture;
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
base.OnCameraSetup(cmd, ref renderingData);
var stack = VolumeManager.instance.stack;
blurVolume = stack.GetComponent<K>();
maskBlur = stack.GetComponent<MaskBlur>();
descriptor.width /= blurVolume.DownSample.value;
descriptor.height /= blurVolume.DownSample.value;
if (isMask)
{
RenderingUtils.ReAllocateIfNeeded(ref m_MaskTexture, descriptor, FilterMode.Bilinear,
TextureWrapMode.Clamp, name: "_MaskTex");
}
}
public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
if (m_Material == null)
{
Debug.LogError("Material is null");
return;
}
var stack = VolumeManager.instance.stack;
blurVolume = stack.GetComponent<K>();
maskBlur = stack.GetComponent<MaskBlur>();
var cmd = CommandBufferPool.Get();
using (new ProfilingScope(cmd, ProfilingSampler.Get(ProfileId)))
{
// Debug.Log("ad" + Time.frameCount);
ExecuteWithCmd(cmd, ref renderingData);
if (isMask)
{
m_BlendMaterial.SetFloat("_Spread", maskBlur.areaSmooth.value);
m_BlendMaterial.SetColor("_MaskColor",maskBlur.maskColor.value);
if (maskBlur.maskType.value == MaskBlur.MaskType.Circle)
{
m_BlendMaterial.EnableKeyword("_CIRCLE");
m_BlendMaterial.SetVector("_Center", maskBlur.center.value);
m_BlendMaterial.SetFloat("_Area", maskBlur.radius.value);
}
else
{
m_BlendMaterial.DisableKeyword("_CIRCLE");
m_BlendMaterial.SetFloat("_Area", maskBlur.areaSize.value);
m_BlendMaterial.SetFloat("_Offset", maskBlur.offset.value);
}
Blit(cmd, m_MaskTexture, m_Renderer.cameraColorTargetHandle, m_BlendMaterial);
}
}
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}
public override bool Setup(ScriptableRenderer renderer)
{
if (!base.Setup(renderer))
return false;
blurVolume = VolumeManager.instance.stack.GetComponent<K>();
return blurVolume.IsActive();
}
public override void Dispose()
{
base.Dispose();
m_MaskTexture?.Release();
}
}
这里为了方便复用,把模糊的遮罩单独提出来,单独用一个Pass来混合,但这其实是浪费性能的做法。
然后留给具体的模糊后处理的事情就只剩申请RT与绘制调用了。
以DualBlurRenderPass为例子
public class DualBlurRenderPass : BaseBlurRendererPassWithVolume<DualBlur>
{
protected override BlurRendererFeature.ProfileId ProfileId => BlurRendererFeature.ProfileId.DualBlur;
protected override string ShaderName => "KuanMi/DualBlur";
RTHandle[] m_Down;
RTHandle[] m_Up;
const int k_MaxPyramidSize = 16;
public DualBlurRenderPass()
{
m_Down = new RTHandle[k_MaxPyramidSize];
m_Up = new RTHandle[k_MaxPyramidSize];
}
public override void OnCameraSetup(CommandBuffer cmd, ref RenderingData renderingData)
{
base.OnCameraSetup(cmd, ref renderingData);
for (int i = 0; i < blurVolume.Iteration.value; i++)
{
RenderingUtils.ReAllocateIfNeeded(ref m_Down[i], descriptor, FilterMode.Bilinear, TextureWrapMode.Clamp,
name: "_Down" + i);
RenderingUtils.ReAllocateIfNeeded(ref m_Up[i], descriptor, FilterMode.Bilinear, TextureWrapMode.Clamp,
name: "_Up" + i);
descriptor.width /= 2;
descriptor.height /= 2;
}
}
public override void ExecuteWithCmd(CommandBuffer cmd, ref RenderingData renderingData)
{
var blurRadius = blurVolume.BlurRadius.value;
var iteration = blurVolume.Iteration.value;
m_Material.SetVector(BlitTextureSt, new Vector4(1, 1, 0, 0));
m_Material.SetFloat(BlurRadius, blurRadius);
Blitter.BlitCameraTexture(cmd, m_Renderer.cameraColorTargetHandle, m_Down[0],
RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store, m_Material, 1);
var lastDown = m_Down[0];
for (int i = 1; i < iteration; i++)
{
Blitter.BlitCameraTexture(cmd, lastDown, m_Down[i], RenderBufferLoadAction.DontCare,
RenderBufferStoreAction.Store, m_Material, 1);
lastDown = m_Down[i];
}
var lastUp = lastDown;
for (int i = iteration - 2; i >= 0; i--)
{
Blitter.BlitCameraTexture(cmd, lastUp, m_Up[i], RenderBufferLoadAction.DontCare,
RenderBufferStoreAction.Store, m_Material, 0);
lastUp = m_Up[i];
}
// Blit(cmd, lastUp, m_Renderer.cameraColorTargetHandle);
Blitter.BlitCameraTexture(cmd, lastUp, isMask ? m_MaskTexture : m_Renderer.cameraColorTargetHandle, RenderBufferLoadAction.DontCare,
RenderBufferStoreAction.Store, m_Material, 0);
}
public override void Dispose()
{
base.Dispose();
foreach (var rtHandle in m_Down)
{
rtHandle?.Release();
}
foreach (var rtHandle in m_Up)
{
rtHandle?.Release();
}
}
}
申请需要的两组上下采样的RT,先降采样,然后升采样,最后依据是否需要和遮罩混合决定是直接绘制到相机上还是遮罩上。
VolumeComponentEditor
这是控制在Volume组件里显示属性的类。
写法很套路化
[CustomEditor(typeof(MaskBlur))]
public class MaskBlurEditor : VolumeComponentEditor
{
SerializedDataParameter m_isMask;
SerializedDataParameter m_areaSmooth;
SerializedDataParameter m_maskColor;
......
public override void OnEnable()
{
var o = new PropertyFetcher<MaskBlur>(serializedObject);
m_isMask = Unpack(o.Find(x => x.isMask));
m_maskType = Unpack(o.Find(x => x.maskType));
m_maskColor = Unpack(o.Find(x => x.maskColor))
......
}
public override void OnInspectorGUI()
{
PropertyField(m_isMask);
PropertyField(m_maskType);
if(m_maskType.value.intValue == (int)MaskBlur.MaskType.Rectangle)
{
PropertyField(m_offset);
}
else if(m_maskType.value.intValue == (int)MaskBlur.MaskType.Circle)
{
PropertyField(m_center);
}
}
}
Shader
因为原仓库也是用hlsl,所以基本没什么变换,
顶点着色器仿照URP其他后处理,采用VERTEXID_SEMANTIC来计算裁切坐标。
DefaultVaryings defaultVert(DefaultAttributes IN)
{
DefaultVaryings output;
output.positionCS = GetFullScreenTriangleVertexPosition(IN.vertexID);
output.uv = output.positionCS.xy * 0.5 + 0.5;
#if UNITY_UV_STARTS_AT_TOP
output.uv.y = 1 - output.uv.y;
#endif
return output;
}
工具库
然后就是要考虑怎么让其他效果能方便的调用。
大致思路有两个,一个就通过RTID来在RendererFeature之间传递。
或者把全部Pass都提成工具类,在其他需要模糊效果的Pass中实例化然后调用。
最后还是觉得后者更靠谱一点。
所以把具体的模糊算法再抽象一层
public abstract class BaseTool
{
public abstract string ShaderName { get; }
public Material Material { get; protected set; }
internal static readonly int GoldenRot = Shader.PropertyToID("_GoldenRot");
internal static readonly int Params = Shader.PropertyToID("_Params");
protected ScriptableRenderPass _renderPass;
public BaseTool(ScriptableRenderPass renderPass)
{
_renderPass = renderPass;
Material = CoreUtils.CreateEngineMaterial(Shader.Find(ShaderName));
}
public abstract void OnCameraSetup(RenderTextureDescriptor descriptor);
public abstract void Execute(CommandBuffer cmd, RTHandle source, RTHandle target);
public virtual void Dispose()
{
CoreUtils.Destroy(Material);
}
}
把具体的参数的配置剥离开,相机视口的属性也剥离开。
这样就方便了在其他Pass中直接引用模糊的功能。
比如在之前写的体积光中
不直接绘制到视窗中,而是先绘制到一张RT上,把这张RT传给模糊工具,让其模糊后绘制到另一张RT上,然后再与视窗加法混合。
当然如果考虑性能,应该是能合并很多操作到一个Shader中的。但为了开发测试方便,这里先分开,真的有需要再汇总全部后处理。