最近开发碰到的需求:打开一个弹窗,只在弹窗覆盖的区域下方实现局部模糊,其他地方仍然保持清晰,弹窗的位置不固定。
先看下效果,白色方块是一个RawImage,用来表示一个弹窗,点击按钮,进行局部模糊。
原理是先对RawImage覆盖的区域进行截屏,然后用Shader做高斯模糊。实际开发中,在弹窗出现前,先对弹窗所覆盖区域进行局部截屏,然后把处理好的RenderTexture赋值给弹窗的背景。
截屏使用的代码参考这篇文章:Unity3D 局部截图、全屏截图、带UI截图三种方法
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
using UnityEngine.UI;
///
/// 对_rawImage所在区域截屏并模糊处理
///
public class 局部模糊 : MonoBehaviour
{
public RawImage _rawImage;
public void OnButtonClick()
{
RectTransform rectTransform = this.transform.GetComponent<RectTransform>();
BlurRegion(rectTransform);
}
///
/// uiRoot是界面根节点
///
private void BlurRegion(RectTransform uiRoot, int iterations = 3, float blurSpread = 2.0f, int downSample = 2)
{
//先将_rawImage的透明度改为0,不影响截屏
Color c = _rawImage.color;
c.a = 0;
_rawImage.color = c;
StartCoroutine(BlurRegionCoroutine(uiRoot, _rawImage, iterations, blurSpread, downSample));
}
///
/// 注意:根节点这里Canvas的Render mode为Overlay,其他模式,Canvas的宽高和屏幕宽高如果不一致,则需要转换
///
private IEnumerator BlurRegionCoroutine(RectTransform uiRoot, RawImage rawImage, int iterations, float blurSpread, int downSample)
{
yield return new WaitForEndOfFrame();
RectTransform imageRt = rawImage.rectTransform;
int imageWidth = (int)(imageRt.rect.width);
int imageHeight = (int)(imageRt.rect.height);
Texture2D texture2D = new Texture2D(imageWidth, imageHeight, TextureFormat.RGB24, false);
//计算rawImage作为uiRoot子物体时的局部坐标,因为rawImage可能是孙子节点,所以需要转换
Vector3 imagePos = uiRoot.InverseTransformPoint(rawImage.transform.position);
//计算rawImage左下角坐标,屏幕左下角为原点(0, 0)
Vector2 imagePivot = imageRt.pivot;
float leftBottomX = Screen.width * 0.5f - imageWidth * imagePivot.x + imagePos.x;
float leftBottomY = Screen.height * 0.5f - imageHeight * imagePivot.y + imagePos.y;
//从屏幕读取像素, leftBottomX,leftBottomY 是读取的初始位置,width,height是读取像素的宽度和高度
texture2D.ReadPixels(new Rect(leftBottomX, leftBottomY, imageWidth, imageHeight), 0, 0);
texture2D.Apply(false, true);
//使用《Shader入门精要》中用到的高斯模糊
Shader shader = AssetDatabase.LoadAssetAtPath<Shader>("Assets/Resources/Chapter12-GaussianBlur.shader");
if (shader == null || rawImage == null)
yield break;
var material = new Material(shader);
if (material == null)
yield break;
RenderImage(rawImage, iterations, blurSpread, downSample, material, texture2D);
Color c = rawImage.color;
c.a = 1;
rawImage.color = c;
}
private void RenderImage(RawImage rawImage, int iterations, float blurSpread, int downSample, Material material, Texture2D texture2D)
{
int rtW = texture2D.width / downSample;
int rtH = texture2D.height / downSample;
// 首先定义了第一个缓存buffer0,并把src中的图像缩放后存储到buffer0中。在迭代过程中,我们又定义了第二个缓存buffer1。
// 在执行第一个Pass时,输入是buffer0,输出是buffer1,完毕后首先把buffer0释放,再把结果值buffer1存储到buffer0中,
// 重新分配buffer1,然后再调用第二个Pass,重复上述过程。迭代完成后,buffer0将存储最终的图像
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(texture2D, buffer0);
for (int i = 0; i < iterations; i++)
{
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the vertical pass
Graphics.Blit(buffer0, buffer1, material, 0);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// Render the horizontal pass
Graphics.Blit(buffer0, buffer1, material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
var buffer = RenderTexture.GetTemporary(buffer0.descriptor); //最终效果
Graphics.Blit(buffer0, buffer);
RenderTexture.ReleaseTemporary(buffer0);
rawImage.texture = buffer;
}
}
高斯模糊Shader来自《Shader入门精要》,也一并粘贴过来
Shader "Unity Shaders Book/Chapter 12/Gaussian Blur" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader {
// CGINCLUDE类似于C++中头文件的功能。由于高斯模糊需要定义两个Pass,但它们使用的片元着色器代码
// 是完全相同的,使用CGINCLUDE可以避免我们编写两个完全一样的frag函数。
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
half4 _MainTex_TexelSize;
float _BlurSize;
struct v2f {
float4 pos : SV_POSITION;
half2 uv[5]: TEXCOORD0;
};
v2f vertBlurVertical(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
// 数组的第一个坐标存储了当前的采样纹理,而剩余的四个坐标则是高斯模糊中对邻域采样时使用的纹理坐标。
// 我们还和属性_BlurSize相乘来控制采样距离。
o.uv[0] = uv;
o.uv[1] = uv + float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[2] = uv - float2(0.0, _MainTex_TexelSize.y * 1.0) * _BlurSize;
o.uv[3] = uv + float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
o.uv[4] = uv - float2(0.0, _MainTex_TexelSize.y * 2.0) * _BlurSize;
return o;
}
v2f vertBlurHorizontal(appdata_img v) {
v2f o;
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
o.uv[0] = uv;
o.uv[1] = uv + float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[2] = uv - float2(_MainTex_TexelSize.x * 1.0, 0.0) * _BlurSize;
o.uv[3] = uv + float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
o.uv[4] = uv - float2(_MainTex_TexelSize.x * 2.0, 0.0) * _BlurSize;
return o;
}
fixed4 fragBlur(v2f i) : SV_Target {
float weight[3] = {0.4026, 0.2442, 0.0545};
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
for (int it = 1; it < 3; it++) {
sum += tex2D(_MainTex, i.uv[it*2-1]).rgb * weight[it];
sum += tex2D(_MainTex, i.uv[it*2]).rgb * weight[it];
}
return fixed4(sum, 1.0);
}
ENDCG
ZTest Always Cull Off ZWrite Off
Pass {
//定义名字,可以在其他shader中使用该pass
NAME "GAUSSIAN_BLUR_VERTICAL"
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
Pass {
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
}
FallBack "Diffuse"
}
目前模糊效果边缘比较硬,为了优化可以在RawImage上层添加一个RectMask2D,并设置Softness
而RawImage的RectTransform改成锚定RectMask2D的四条边,这样方便通过RectMask2D来修改范围。
最终效果