径向模糊(URP下的实现)

前言

        最近项目上要用到径向模糊的效果,而我又不会。我看了网络上的文章,我就感觉前人之述备矣。不过为了我自己不忘记,因此我写了这篇文章来记录这次我所学到的东西。

环境

   Windows10
   Unity2022.3.8f1c1
   Universal RP 14.0.8
   Core RP Library 14.0.8

介绍

        径向模糊(Radial Blur),是一种从中心向外呈幅射状的逐渐模糊的效果。在游戏中常常用来模拟一些动感的效果。

流程

        径向模糊的特点是从某个像素为中心向外辐射状扩散,因此需要采样的像素在原像素和中间点像素的连线上,不同连线上的点不会相互影响。

   第一步:确定径向模糊的中心点
   第二步:计算采样像素与中心点的距离,根据距离确定偏移程度,即离中心点越远,偏移量越大。
   第三步:将采样点的颜色值做加权求和。
   第四步:将模糊的颜色和原本的颜色进行线性差值混合。

具体实现

        在开始实现前,请确保你的相机开启了后处理。

Shader部分

        我们先实现Shader部分。网上我搜到的shader实现都是使用了两个Pass做渲染,但我实在不明白这样做的必要性,所以我写在了一个Pass中。如果你想知道两个Pass的Shader是如何实现的,你可以看一下后面参考文章中的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
Shader "Game/OnePassRadialBlur"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" {}
_BlurFactor ("BlurFactor", Range(0,1)) = 0.01
_BlurCenter("BlurCenter",Vector)=(0.5,0.5,0,0)
[int]_LoopNum("_Loop",Range(1,10)) = 5
_Instensity("Instensity",Float) = 1
}
SubShader
{
Tags { "RenderType"="Opaque" }
ZTest Off
Cull Off
ZWrite Off

HLSLINCLUDE
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};

struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};

sampler2D _MainTex;

CBUFFER_START(UnityPerMaterial)
float _BlurFactor;
half2 _BlurCenter;
half _Instensity;
int _LoopNum;
CBUFFER_END

v2f vert (appdata v)
{
v2f o;
o.vertex = TransformObjectToHClip(v.vertex.xyz);
o.uv = v.uv;
return o;
}

half4 frag (v2f i) : SV_Target
{
half2 dir = _BlurCenter.xy - i.uv;
float dis = length(dir);
half4 col = tex2D(_MainTex, i.uv);
half4 mainCol = col;
//dir = dir * _BlurFactor;
dir = normalize(dir) * dis / _LoopNum * _BlurFactor;
for(int j = 1; j < _LoopNum;j++){
col += tex2D(_MainTex, i.uv + dir * j);
}
col /= _LoopNum;
col = lerp(mainCol,col,saturate(_Instensity * dis));
return col;
}

ENDHLSL

Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag

ENDHLSL
}
}
}

实际上在我所看到的文章中,大多数的采样方式是我代码中的注释的采样方式dir = dir * _BlurFactor;。这里我也不太理解,因为按照原理上说我们只采样目标点和中心点间的点。如果是简单的乘上_BlurFactor,在后面循环采样的时我们所采样的点可能就不会在目标点和中间点间。但是你会发现我仍然乘上了_BlurFactor,因为在实际操作的过程中,我发现还是要有一个参数来控制好其采样的大小。我们可以明显的发现如果不乘_BlurFactor,离中心点越远的目标点,其采样的长度更大。其表现如下:

如果我们乘_BlurFactor,并设其值为0.1。其效果如下:

而且加上_BlurFactor能让我们更好的控制整体的效果。当然如果你觉得我们采样的点是否在目标点和中间点间不重要,那么你可以直接使用dir = dir * _BlurFactor;来得到你想要的效果。最后我使用了一个_Instensity * dis来控制模糊。在我找到的参考文章中,有一篇给出了设定模糊范围的方法。但是这个方法我没有理解,所以我不写进去。有兴趣的话可以看一下这篇文章URP | 后处理-径向模糊

后处理脚本实现

        其实可以直接用Render Feature搞定的,但是这个大概率是在后处理的阶段做的。那我还是走一下URP的后处理流程,去实现一个后处理的脚本来控制最终的效果。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class RadialBlurSetting : VolumeComponent, IPostProcessComponent
{
public Vector2Parameter centerPoint = new Vector2Parameter(new Vector2(0.5f, 0.5f));
public MinFloatParameter instensity = new MinFloatParameter(0, 0);
public ClampedIntParameter loopNum = new ClampedIntParameter(5,1,10);
public ClampedFloatParameter blurFactor = new ClampedFloatParameter(0, 0, 1);
public MinIntParameter sampleScale = new MinIntParameter(1,1);
public bool IsActive()
{
return instensity.value > 0.000001f;
}
public bool IsTileCompatible()
{
return false;
}
}

这个就没有什么好说的,无非就是按照自己的要求设定值罢了。这里我多设置了一个sampleScale的值,这里是为了后面Render Feature中控制_MainTex传输的大小。且他也可以照成一定的模糊效果。一开始我加这个是因为有人说这样就不用传一个屏幕大小的RT(Render Texture),从而可以节省性能。但是URP原本就会存储一张屏幕的RT,所以我突然感觉没必要了。但是他的确可以加深模糊,所以我并没有删除掉。

Render Feature实现

        Render Feature在实现上也简单,我先上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.Universal;

public class RadialBlur : ScriptableRendererFeature
{
class RadialBlurRenderPass : ScriptableRenderPass
{
public float blurFactor
{
set
{
material.SetFloat("_BlurFactor", value);
}
}
public Vector2 blurCenter
{
set
{
material.SetVector("_BlurCenter", value);
}
}
public int loopNum
{
set
{
material.SetInt("_LoopNum", value);
}
}
public float instensity
{
set
{
material.SetFloat("_Instensity", value);
}
}

public int sampleScale;

public bool Ready { get; private set; }

private Material material;

private int blurTexId;

public RadialBlurRenderPass()
{
Ready = false;
var blurShader = Shader.Find("Game/OnePassRadialBlur");
if (blurShader == null)
{
Debug.Log("找不到Shader:Game/OnePassRadialBlur");
return;
}
material = CoreUtils.CreateEngineMaterial(blurShader);

blurTexId = Shader.PropertyToID("_RadialBlurTex");
Ready = true;

sampleScale = 1;
}

public override void Configure(CommandBuffer cmd, RenderTextureDescriptor cameraTextureDescriptor)
{
//根据缩放值来得到 RT
cmd.GetTemporaryRT(blurTexId,cameraTextureDescriptor.width / sampleScale, cameraTextureDescriptor.height / sampleScale
,cameraTextureDescriptor.depthBufferBits,FilterMode.Bilinear,cameraTextureDescriptor.colorFormat);
}

public override void Execute(ScriptableRenderContext context, ref RenderingData renderingData)
{
var cmd = CommandBufferPool.Get(nameof(RadialBlurRenderPass));
cmd.Clear();

var renderer = renderingData.cameraData.renderer;
if (renderer.cameraColorTargetHandle.rt != null)
{
cmd.Blit(renderer.cameraColorTargetHandle, blurTexId);
// 自动将blurTexId设定到_MainTex中,只特殊名称的才可以
cmd.Blit(blurTexId, renderer.cameraColorTargetHandle, material, 0);
}
context.ExecuteCommandBuffer(cmd);
CommandBufferPool.Release(cmd);
}

public override void FrameCleanup(CommandBuffer cmd)
{
cmd.ReleaseTemporaryRT(blurTexId);
}
}

RadialBlurRenderPass m_ScriptablePass;

[SerializeField]
private RenderPassEvent passEvent;

/// <inheritdoc/>
public override void Create()
{
m_ScriptablePass = new RadialBlurRenderPass();

m_ScriptablePass.renderPassEvent = passEvent;
}

public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData)
{
RadialBlurSetting setting = VolumeManager.instance.stack.GetComponent<RadialBlurSetting>();
// 可以获取到设置且设置是开启的、m_ScriptablePass是准备好的、相机也启用后处理,在这样的情况下才能进入队列渲染
if (setting != null && setting.IsActive() && m_ScriptablePass.Ready && renderingData.cameraData.postProcessEnabled)
{
m_ScriptablePass.loopNum = setting.loopNum.value;
m_ScriptablePass.instensity = setting.instensity.value;
m_ScriptablePass.blurCenter = setting.centerPoint.value;
m_ScriptablePass.blurFactor = setting.blurFactor.value;
m_ScriptablePass.sampleScale = setting.sampleScale.value;

renderer.EnqueuePass(m_ScriptablePass);
}
}
}

在RadialBlurRenderPass中,一开始我们要先获取到Shader,并用其创建出材质。之后我们要用这个材质进行后处理渲染。如果这时候出现问题,那后面渲染也是有问题的,所以我设定了一个Ready来保证只有其正确后才可以进入AddRenderPasses的逻辑中。当然除了判断这个外,我这里还判断是否有存在RadialBlurSetting且其是否是开启的和相机是否开启了后处理效果。如果条件符合,那么就设定材质的值并将其加入到渲染队列中。

项目配置

        在完成上面的代码后,我们在场景中的Volume组件上添加RadialBlurSetting。在2022.3.8f1c1版本的Unity中创建一个默认的场景时会带有一个名为Global Volume的对象。大家也可以在自己的场景中找找。如果找不到,我们可以直接创建一个对象并把Volume组件添加上去。然后再在Volume组件上添加我们的脚本。然后我们找到一下自己的URP设置,并在设置上添加我们刚刚写好的RadialBlur

参考文章

闲言碎语

        我本来是不想写这篇文章。但是我想到以后可能会用上,所以还是写了。我以后要找这类文章就来自己的博客上找了。(* ̄︶ ̄)