Unity UGUI高斯模糊背景效果简易解决方案
本文欢迎转载,转载请标明出处!
功能描述
在我们玩游戏的时候,经常会看到一些游戏会对UI做类似这样的处理,以《明日方舟》为例:

可以看到,新开的上层UI界面会以下层UI界面的模糊效果图做背景。这种模糊效果可以起到让玩家的注意力集中在上层UI界面的作用,倘若加入了透明度动画强化模糊过度也可以让UI界面的叠加看上去更加流畅和舒适。
这种便是本文想要实现的一个功能:UI背景高斯模糊。

这种效果的原理可以分为几个部分:
- 如何获取下层UI界面的画面抓取;
- 如何使用shader处理模糊效果;
- 如何统一管理界面的模糊功能;
- 功能拓展和坑点小汇总;
接下来笔者将针对上述几个部分逐一讲解并分享这个具体实现方法。
简易界面框架搭建
正式开始实现效果之前,我们需要准备好一个简单的Demo界面框架。笔者使用的是Unity2018.4.16版本,不过具体的实现基本上与版本没有太大关联,不要太低就行。
首先先搭建好一个简易的实现场景。用四个适当拉伸过并赋予不同颜色材质球的Cube组合出一个“游戏世界”场景,方便我们看具体的模糊效果。同时,我们也要养成一个比较好的习惯,将材质球和以后可能会用到的像sprite,shader和prefab等各自开一个文件夹存放。


接着是创建一个UI专用的摄像机。摄像机的参数如下:

特别需要注意的是摄像机的LayerMask要修改成只拍摄UI层。
然后是创建一个Canvas。将Canvas的RenderMode修改为ScreenSpace-Camera,然后将刚才新建的UI摄像机挂载到这个Canvas的RenderCamera上。
同时,修改CanvasScaler的UI Scale Mode为Scale With Screen Size,调整一个适合的分辨率并将Match调成0.5。

在Canvas中创建两个按钮,用于做点击按钮弹出界面的功能。

如何获取下层UI界面的画面抓取
抓取画面的方式,比较实用的方法是RenderTexture(渲染纹理,后面简称RT)渲染摄像机拍摄到的画面,并对这张RT进行处理。Shader中有GrabPass抓取屏幕的方法但是这个方法极其不推荐使用,因为这个方法在移动端会有大概率不能正常运作,同时性能开销也比较大。
不过这边的抓取并不是创建一张RT然后放在UI摄像机中直接输出,而是使用MonoBehaviour的OnRenderImage()对当前摄像机拍摄的画面方法进行模糊处理并输出,也就是俗称的屏幕后处理。
// src为当前摄像机拍摄到的RT dest为目标输出的RT
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
// 通过这个函数进行渲染,只不过我们要用的是它的其他重载,可以添加材质球的那个
// 第二个参数不一定要是dest,也可以是其他RT
// 但是在OnRenderImage作用域内,输出dest的渲染必须是最后一次Blit,否则会有Warning警告
Graphics.Blit(src, dest);
}
我们新建一个类:ScreenBlurEffect 。这个类就是我们进行后处理的类,把它挂在UI摄像机下,并先写入下面这段代码:
using System;
using UnityEngine;
// 这段代码保证了挂载的时候一定要有Camera组件
[RequireComponent(typeof(Camera))]
public class ScreenBlurEffect : MonoBehaviour
{
RenderTexture final_blur_rt;
// 模糊后处理的主要方法
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
int width = src.width;
int height = src.height;
// 将当前摄像机画面渲染到目标RT上
final_blur_rt = RenderTexture.GetTemporary(width, height, 0);
Graphics.Blit(src, final_blur_rt);
// 我们只是想获得摄像机的画面,所以完事之后别忘了把画面正常输出出去
Graphics.Blit(src, dest);
}
}
这样我们就可以获得当前摄像机拍摄到的画面了(虽然这段代码很简单粗糙)。
如何使用shader处理模糊效果
模糊效果我们一般使用的是高斯模糊。
这块的内容网上搜就可以搜到很多教程及源码,这边就简单介绍介绍:
高斯模糊的处理流程简单来说就是对目标图中每个像素周边的n个像素进行采样后,通过特定的权值与当前像素和周边像素的颜色进行相乘累加,并以这个颜色作为最终颜色输出。由于图像中每个像素最终输出都会受到周边像素的影响,所以像素之间的颜色过度就会变得更加“平滑”,从而达成模糊的效果。
我们新建一个Shader文件:BlurShader,并创建一个新的材质球将shader挂在上面,代码如下:
Shader "Unlit/BlurShader"
{
Properties
{
_MainTex ("Texture", 2D) = "white" {
} // 主纹理,我们进行模糊处理的对象就是它
_BlurSize("BlurSize", Range(0, 127)) = 1.0 // 对周边采样的偏移量
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
fixed _BlurSize;
struct v2f{
float4 pos : SV_POSITION;
half2 uv[5] : TEXCOORD0;
};
// 水平uv数据扩展采样
v2f vert_hor(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
// 下标从0开始,分别取到当前像素,偏移1单位和2单位的uv位置
o.uv[0] = uv;
o.uv[1] = uv + half2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[2] = uv - half2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[3] = uv + half2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
o.uv[4] = uv - half2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
return o;
}
// 水平uv数据扩展采样
v2f vert_ver(appdata_img v)
{
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
// 下标从0开始,分别取到当前像素,偏移1单位和2单位的uv位置
o.uv[0] = uv;
o.uv[1] = uv + half2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[2] = uv - half2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[3] = uv + half2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
o.uv[4] = uv - half2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
return o;
}
// 处理模糊片元
fixed4 frag(v2f i) : SV_TARGET
{
// 模糊算子,分别决定了当前像素,上下(左右)偏移1个单位和2个单位的计算权重
// 算子决定了模糊的质量,算子越大越复杂效果越好,当然性能上就要差一些
half weight[3] = {
0.4026, 0.2442, 0.0545};
// 当前像素片元颜色(乘以权重)
fixed3 color = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
// 根据权重叠加上下(左右)像素颜色
color += tex2D(_MainTex, i.uv[1]).rgb * weight[1];
color += tex2D(_MainTex, i.uv[2]).rgb * weight[1];
color += tex2D(_MainTex, i.uv[3]).rgb * weight[2];
color += tex2D(_MainTex, i.uv[4]).rgb * weight[2];
return fixed4(color, 1.0);
}
ENDCG
Cull Off
ZWrite Off
Pass // 0:处理水平模糊
{
Name "BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vert_hor
#pragma fragment frag
ENDCG
}
Pass // 1:处理垂直模糊
{
Name "BLUR_VERTICAL"
CGPROGRAM
#pragma vertex vert_ver
#pragma fragment frag
ENDCG
}
}
Fallback Off
}

修改之前的ScreenBlurEffect脚本,修改后的代码如下:
using System;
using UnityEngine;
// 这段代码保证了挂载的时候一定要有Camera组件
[RequireComponent(typeof(Camera))]
public class ScreenBlurEffect : MonoBehaviour
{
// 预先定义shader渲染用的pass
const int BLUR_HOR_PASS = 0;
const int BLUR_VER_PASS = 1;
bool is_support; // 判断当前平台是否支持模糊
RenderTexture final_blur_rt;
RenderTexture temp_rt;
[SerializeField]
public Material blur_mat; // 模糊材质球
// 外部参数
[Range(0, 127)]
float blur_size = 1.0f; // 模糊额外散步大小
[Range(1, 10)]
public int blur_iteration = 4; // 模糊采样迭代次数
public float blur_spread = 1; // 模糊散值
int cur_iterate_num = 1; // 当前迭代次数
public int blur_down_sample = 4; // 模糊初始降采样比率
public bool render_blur_effect = false; // 是否开始渲染模糊效果
void Awake()
{
is_support = SystemInfo.supportsImageEffects;
}
void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if(is_support && blur_mat != null && render_blur_effect){
// 首先对输出的结果做一次降采样,也就是降低分辨率,减小RT图的大小
int width = src.width / blur_down_sample;
int height = src.height / blur_down_sample;
// 将当前摄像机画面渲染到被降采样的RT上
final_blur_rt = RenderTexture.GetTemporary(width, height, 0);
Graphics.Blit(src, final_blur_rt);
cur_iterate_num = 1; // 初始化迭代
while(cur_iterate_num <= blur_iteration)
{
blur_mat.SetFloat("_BlurSize", (1.0f + cur_iterate_num * blur_spread) * blur_size); // 设置模糊扩散uv偏移
temp_rt = RenderTexture.GetTemporary(width, height, 0);
// 使用blit的其他重载,针对对应的材质球和pass进行渲染并输出结果
Graphics.Blit(final_blur_rt, temp_rt, blur_mat, BLUR_HOR_PASS);
Graphics.Blit(temp_rt, final_blur_rt, blur_mat, BLUR_VER_PASS);
RenderTexture.ReleaseTemporary(temp_rt); // 释放临时RT
cur_iterate_num ++;
}
Graphics.Blit(final_blur_rt, dest);
RenderTexture.ReleaseTemporary(final_blur_rt); // final_blur_rt作用已经完成,可以回收了
}
else{
Graphics.Blit(src, dest);
}
}
}

修改完代码别忘记把blurMat材质球挂载上去
is_support 的作用是获取当前平台是否支持后处理效果,在Awake或者Start时获取都可以。
BLUR_HOR_PASS 和 BLUR_VER_PASS 这两个int字段代表我们在渲染RT时使用的目标材质球中Shader使用哪个Pass。可以看到,代码中的Graphics.Blit出现了使用blurMat材质球的重载,材质参数后面跟上的这个int值代表了使用哪个Pass(Shader中的Pass从0开始计数)

(PS:当然如果直接使用0和1赋值也是可以的,只不过我们在写代码的时候,还是尽量避免写一些难以理解的“魔法数字”,可以事先声明就声明,后面假设改了shader的pass也可以通过改一个地方就完成修改,而不会因为单纯写数字而出现修改疏漏。)
一次模糊的效果是有限的,我们可以进行一次模糊之后,对被模糊的图继续执行模糊。同时,扩大模糊的采样范围,也可以加强模糊的效果。当然,每一次模糊都会执行两次Pass,所以我们要掂量性能和效果之间的平衡,并不是重复模糊次数越多越好。我们可以通过自己调试不同的模糊次数和模糊范围来获得一个相对满意的效果。
此外,我们要尽量避免重复创建一次性临时RT。因此,我们在最开始的时候声明了一个临时RT temp_rt,在OnRenderImage中循环利用,而不是等到需要的时候才声明。在重新写入新的渲染数据前,也需要先释放掉temp_rt中的数据再重新渲染进去。
最后,我们获得了经过几次模糊后的模糊RT final_blur_rt,将其输出到dest。
运行游戏,在Inspector把ScreenBlurEffect的render_blur_effect置为true,就可以看到模糊效果了。


现在我们只是实现了实时模糊界面的效果,要实现模糊UI背景的效果还需要将模糊后的RT输出到某个界面的图片组件上。
我们需要修改原先的模糊逻辑,把模糊后的RT图单独保存,并且不要改变原先的输出画面。同时,假设我们对不同界面的模糊有不同的需求的话,需要预留控制模糊参数的逻辑。最后就是这个模糊效果只需要在模糊的时候才需要打开,其余的时间不让它运行,减少无谓的渲染次数。
首先是用了一个新的类BlurData来控制模糊的具体参数,并且将代码设计为调用EnableBlurRender接口激活脚本渲染模糊。修改后的ScreenBlurEffect类代码如下:
using System;
using UnityEngine;
public class BlurData{
public float blur_size;
public int blur_iteration;
public int blur_down_sample;
public float blur_spread;
}
[RequireComponent(typeof(Camera))]
public class ScreenBlurEffect : MonoBehaviour
{
// 预先定义shader渲染用的pass
const int BLUR_HOR_PASS = 0;
const int BLUR_VER_PASS = 1;
bool is_support; // 判断当前平台是否支持模糊
RenderTexture final_blur_rt;
RenderTexture temp_rt;
[SerializeField]
public Material blur_mat; // 模糊材质球
// 外部参数
[Range


4万+

被折叠的 条评论
为什么被折叠?



