均值模糊(Box Blur)
概述
由于公司手游项目需求,需要一个适合手机平台的模糊效果,同时需要开放一个参数便于调节模糊值。我首先想到的就是ps里面的均值模糊。查资料可以知道均值模糊是一种快速的图像模糊技术,相比与传统的卷积模糊(如高斯模糊),均值模糊可以更加有效率的完成对图像模糊。在unity官方自带imageeffect包也有一个blur的屏幕特效,用的就是均值模糊算法,只不过他只采样了离原像素上下左右模糊半径(Blur Spread)距离的四个像素进行平均处理。然后做迭代(Iterations)处理,即将模糊后的图像再次采样模糊,迭代次数越高越模糊,同时也会产生更多的drawcall,本文的效果就是从这个例子简化而来,使之适合手游项目,并且傻瓜式方便的调节参数。
原理
均值模糊的原理是通过图形滤波器来把一个像素和周围的像素一起求平均值,比如一个三阶的图像滤波器构造其实就是一个3*3的数组(n阶数组中的元素成为滤波器的系数和滤波器的权重,n称为滤波器的阶),相当于把一个像素和周围8个像素相加在一起再除以9求平均值,等于把一个像素和周围的像素搅拌在一起,自然就模糊了。均值滤波器与高斯模糊的滤波器不同的地方,就是采样的像素权重是相等的,因此效果相对而言要比高斯模糊差,但速度却要快一些。本例其实只采样了四个像素求和做平均,然后通过迭代多次采样,实现比较好的模糊效果。下面是一个三阶的均值滤波器:
[1,1,1]
1/9 [1,1,1]
[1,1,1]
本例采用的滤波器:
[0,1,0]
1/4 [1,0,1]
[0,1,0]
Shader代码实现
shader代码部分相对比较简单,首先需要定义两个内部变量,_MainTex_TexelSize和_BlurOffsets,这两个变量都属于unity的黑魔法,官方文档并没有详细说明,_MainTex_TexelSize的解释可以参考这里,_BlurOffsets这个参数是用来接收从C#脚本里面穿过来的参数,即模糊半径。
half4 _MainTex_TexelSize;
half4 _BlurOffsets;
在顶点输入结构体里面定义一个一维四阶数组用来存储uv坐标:
struct v2f {
float4 vertex : SV_POSITION;
half2 texcoord : TEXCOORD0;
half2 taps[4] : TEXCOORD1;
};
在vert函数里面将uv坐标进行偏移获得原像素上下左右偏移_BlurOffsets像素,并存储在taps[]数组里面,传给frag函数
v2f vert (appdata_t v)
{
v2f o;
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
o.texcoord = v.texcoord - _BlurOffsets.xy * _MainTex_TexelSize.xy;//将C#脚本传过来的图像偏移回原位置
o.taps[0] = o.texcoord + _MainTex_TexelSize * _BlurOffsets.xy;
o.taps[1] = o.texcoord - _MainTex_TexelSize * _BlurOffsets.xy;
o.taps[2] = o.texcoord + _MainTex_TexelSize * _BlurOffsets.xy * half2(1,-1);
o.taps[3] = o.texcoord - _MainTex_TexelSize * _BlurOffsets.xy * half2(1,-1);
return o;
}
在frag函数使用vert函数传过来的uv坐标数组采样图像然后进行叠加后平均化,以达到模糊效果:
fixed4 frag (v2f i) : SV_Target
{
half4 color = tex2D(_MainTex, i.taps[0]);
color += tex2D(_MainTex, i.taps[1]);
color += tex2D(_MainTex, i.taps[2]);
color += tex2D(_MainTex, i.taps[3]);
return color * 0.25;
}
Shader完整代码
VF版本代码01
Shader "PengLu/ImageEffect/Unlit/BlurBox" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
}
SubShader {
Pass {
ZTest Always ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata_t {
float4 vertex : POSITION;
float2 texcoord : TEXCOORD0;
};
struct v2f {
float4 vertex : SV_POSITION;
half2 texcoord : TEXCOORD0;
half2 taps[4] : TEXCOORD1;
};
sampler2D _MainTex;
half4 _MainTex_TexelSize;
half4 _BlurOffsets;
v2f vert (appdata_t v)
{
v2f o;
o.vertex = mul(UNITY_MATRIX_MVP, v.vertex);
o.texcoord = v.texcoord - _BlurOffsets.xy * _MainTex_TexelSize.xy;
o.taps[0] = o.texcoord + _MainTex_TexelSize * _BlurOffsets.xy;
o.taps[1] = o.texcoord - _MainTex_TexelSize * _BlurOffsets.xy;
o.taps[2] = o.texcoord + _MainTex_TexelSize * _BlurOffsets.xy * half2(1,-1);
o.taps[3] = o.texcoord - _MainTex_TexelSize * _BlurOffsets.xy * half2(1,-1);
return o;
}
fixed4 frag (v2f i) : SV_Target
{
half4 color = tex2D(_MainTex, i.taps[0]);
color += tex2D(_MainTex, i.taps[1]);
color += tex2D(_MainTex, i.taps[2]);
color += tex2D(_MainTex, i.taps[3]);
return color * 0.25;
}
ENDCG
}
}
Fallback off
}
C#脚本代码
C#脚本相对也比较简单,这个脚本主要负责把抓取屏幕并传递参数给shader进行模糊处理,这个脚本是从官方的简化而来,将迭代的次数固定为2,只留下模糊半径一个参数调节。关键的两个函数需要注意下:
Graphics.BlitMultiTap (source, dest, material,
new Vector2(-off, -off),
new Vector2(-off, off),
new Vector2( off, off),
new Vector2( off, -off)
);
RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
Graphics.BlitMultiTap函数官方解释在这里 ,大概意思是传递多个位图块给shader进行处理,每个位图块有各自的偏移(由vector[]数组决定)。但实际上我测试发现,这个函数传递的位图块是配合固定管线编程使用的,在5.0以下的blur特效shader代码里有以下代码:上面那个函数生成的四个纹理块一次对应下面四个_MainTex,shader代码里面并没有偏移,都是从Graphics.BlitMultiTap传递过来。
SubShader {
Pass {
ZTest Always Cull Off ZWrite Off Fog { Mode Off }
SetTexture [_MainTex] {constantColor (0,0,0,0.25) combine texture * constant alpha}
SetTexture [_MainTex] {constantColor (0,0,0,0.25) combine texture * constant + previous}
SetTexture [_MainTex] {constantColor (0,0,0,0.25) combine texture * constant + previous}
SetTexture [_MainTex] {constantColor (0,0,0,0.25) combine texture * constant + previous}
}
}
而在unity5.0的官方blur shader里面已经将固定管线的相关代码删除,而对vert&frag shader编程,它实际上传递了一个uv坐标偏移值为(off,off)的位图块给shader,并将偏移值传给内置变量_BlurOffsets,我们在相关代码里要重新做偏移,才会有偏移效果。因此Graphics.BlitMultiTap我们可以改成:
Graphics.BlitMultiTap (source, dest, material,new Vector2(off, off) );
RenderTexture.GetTemporary这个函数在这里主要是用来重新设定抓取的屏幕图像的长宽,在本例中我们将长宽设置成原来的8分之1后然后再传递给shader处理,可以是采样计算消耗降低到原来的64分之1,只是因此多消耗一个drawcall。由于还迭代了两次,最后这个屏幕特效需要消耗4个drawcall,当然由于做了个判断,当Blur Size为0时,只消耗1个drallcall,不做模糊处理。
完整C#脚本如下:
using UnityEngine;
using System.Collections;
using System;
[ExecuteInEditMode]
[AddComponentMenu ("PengLu/ImageEffect/Blurbox")]
public class ImageEffect_BlurBox : MonoBehaviour {
#region Variables
public Shader BlurBoxShader = null;
private Material BlurBoxMaterial = null;
[Range(0.0f, 1.0f)]
public float BlurSize = 0.5f;
#endregion
#region Properties
Material material
{
get
{
if(BlurBoxMaterial == null)
{
BlurBoxMaterial = new Material(BlurBoxShader);
BlurBoxMaterial.hideFlags = HideFlags.HideAndDontSave;
}
return BlurBoxMaterial;
}
}
#endregion
// Use this for initialization
void Start () {
BlurBoxShader = Shader.Find("PengLu/ImageEffect/Unlit/BlurBox");
// Disable if we don't support image effects
if (!SystemInfo.supportsImageEffects)
{
enabled = false;
return;
}
// Disable the image effect if the shader can't
// run on the users graphics card
if (!BlurBoxShader || !BlurBoxShader.isSupported)
enabled = false;
return;
}
public void FourTapCone (RenderTexture source, RenderTexture dest,int iteration)
{
float off = BlurSize*iteration+0.5f;
Graphics.BlitMultiTap (source, dest, material,
new Vector2(-off, -off),
new Vector2(-off, off),
new Vector2( off, off),
new Vector2( off, -off)
);
}
private void DownSample4x (RenderTexture source, RenderTexture dest)
{
float off = 1.0f;
// Graphics.Blit(source, dest, material);
Graphics.BlitMultiTap (source, dest, material,
new Vector2(off, off),
new Vector2(-off, off),
new Vector2( off, off),
new Vector2( off, -off)
);
}
void OnRenderImage (RenderTexture sourceTexture, RenderTexture destTexture)
{
if(BlurSize != 0 && BlurBoxShader != null){
int rtW = sourceTexture.width/8;
int rtH = sourceTexture.height/8;
RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
DownSample4x (sourceTexture, buffer);
for(int i = 0; i < 2; i++)
{
RenderTexture buffer2 = RenderTexture.GetTemporary(rtW, rtH, 0);
FourTapCone (buffer, buffer2,i);
RenderTexture.ReleaseTemporary(buffer);
buffer = buffer2;
}
Graphics.Blit(buffer, destTexture);
RenderTexture.ReleaseTemporary(buffer);
}
else{
Graphics.Blit(sourceTexture, destTexture);
}
}
// Update is called once per frame
void Update () {
#if UNITY_EDITOR
if (Application.isPlaying!=true)
{
BlurBoxShader = Shader.Find("PengLu/ImageEffect/Unlit/BlurBox");
}
#endif
}
public void OnDisable () {
if (BlurBoxMaterial)
DestroyImmediate (BlurBoxMaterial);
}
}