一、降采样与高斯模糊的原理
1.1 图像的降采样
降采样(Downsample)也称下采样(Subsample),按字面意思理解即是降低采样频率。对于一幅N*M的图像来说,如果降采样系数为k,则降采样即是在原图中每行每列每隔k个点取一个点组成一幅图像的一个过程。
降采样系数K值越大,则需要处理的像素点越少,运行速度越快。
1.2 高斯模糊原理
高斯模糊(Gaussian Blur),也叫高斯平滑,高斯滤波,其通常用它来减少图像噪声以及降低细节层次,常常也被用于对图像进行模糊。
通俗的讲,高斯模糊就是对整幅图像进行加权平均的过程,每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。高斯模糊的具体操作是:用一个模板(或称卷积、掩模)扫描图像中的每一个像素,用模板确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值。
高斯分布的数学表示如下:
其中,x为到像素中心的距离,σ为标准差。
高斯分布(正态分布曲线)
说明一下高斯模糊的几个要点:
- 从数学的角度来看,图像的高斯模糊过程就是图像与正态分布做卷积。
- 由于正态分布又叫作高斯分布,所以这项技术就叫作高斯模糊。
- 高斯模糊能够把某一点周围的像素色值按高斯曲线统计起来,采用数学上加权平均的计算方法得到这条曲线的色值
- 所谓"模糊",可以理解成每一个像素都取周边像素的平均值。
- 图像与圆形方框模糊做卷积将会生成更加精确的焦外成像效果。由于高斯函数的傅立叶变换是另外一个高斯函数,所以高斯模糊对于图像来说就是一个低通滤波器。
二、高斯模糊特效在Unity的实现
Unity中的屏幕特效,通常分为两部分来实现:
- Shader代码实现部分
- C#/javascript代码实现部分
上述两者结合起来,便可以在Unity中实现具有很强可控性和灵活性的屏幕后期特效。
实现思路类似Standard Assets/Image Effect中的Blur
2.1 Shader部分
书写思路方面,采用了3个通道(Pass),分别是:
- 通道0:降采样通道。
- 通道1:垂直方向模糊处理通道。
- 通道2:水平方向模糊处理通道。
而三个通道中共用的变量、函数和结构体的代码位于CGINCLUDE和ENDCG之间。
源码如下:
Shader "Shader/RapidBlurEffect"
{
//-----------------------------------【属性 || Properties】------------------------------------------
Properties
{
//主纹理
_MainTex("Base (RGB)", 2D) = "white" {}
}
//----------------------------------【子着色器 || SubShader】---------------------------------------
SubShader
{
ZWrite Off
Blend Off
//---------------------------------------【通道0 || Pass 0】------------------------------------
//通道0:降采样通道 ||Pass 0: Down Sample Pass
Pass
{
ZTest Off
Cull Off
CGPROGRAM
//指定此通道的顶点着色器为vert_DownSmpl
#pragma vertex vert_DownSmpl
//指定此通道的像素着色器为frag_DownSmpl
#pragma fragment frag_DownSmpl
ENDCG
}
//---------------------------------------【通道1 || Pass 1】------------------------------------
//通道1:垂直方向模糊处理通道 ||Pass 1: Vertical Pass
Pass
{
ZTest Always
Cull Off
CGPROGRAM
//指定此通道的顶点着色器为vert_BlurVertical
#pragma vertex vert_BlurVertical
//指定此通道的像素着色器为frag_Blur
#pragma fragment frag_Blur
ENDCG
}
//---------------------------------------【通道2 || Pass 2】------------------------------------
//通道2:水平方向模糊处理通道 ||Pass 2: Horizontal Pass
Pass
{
ZTest Always
Cull Off
CGPROGRAM
//指定此通道的顶点着色器为vert_BlurHorizontal
#pragma vertex vert_BlurHorizontal
//指定此通道的像素着色器为frag_Blur
#pragma fragment frag_Blur
ENDCG
}
}
//-------------------------CG着色语言声明部分 || Begin CG Include Part----------------------
CGINCLUDE
//【1】头文件包含 || include
#include "UnityCG.cginc"
//【2】变量声明 || Variable Declaration
sampler2D _MainTex;
//UnityCG.cginc中内置的变量,纹理中的单像素尺寸|| it is the size of a texel of the texture
uniform half4 _MainTex_TexelSize;
//C#脚本控制的变量 || Parameter
uniform half _DownSampleValue;
//【3】顶点输入结构体 || Vertex Input Struct
struct VertexInput
{
//顶点位置坐标
float4 vertex : POSITION;
//一级纹理坐标
half2 texcoord : TEXCOORD0;
};
//【4】降采样输出结构体 || Vertex Input Struct
struct VertexOutput_DownSmpl
{
//像素位置坐标
float4 pos : SV_POSITION;
//一级纹理坐标(右上)
half2 uv20 : TEXCOORD0;
//二级纹理坐标(左下)
half2 uv21 : TEXCOORD1;
//三级纹理坐标(右下)
half2 uv22 : TEXCOORD2;
//四级纹理坐标(左上)
half2 uv23 : TEXCOORD3;
};
//【5】准备高斯模糊权重矩阵参数7x4的矩阵 || Gauss Weight
static const half4 GaussWeight[7] =
{
half4(0.0205,0.0205,0.0205,0),
half4(0.0855,0.0855,0.0855,0),
half4(0.232,0.232,0.232,0),
half4(0.324,0.324,0.324,1),
half4(0.232,0.232,0.232,0),
half4(0.0855,0.0855,0.0855,0),
half4(0.0205,0.0205,0.0205,0)
};
//【6】顶点着色函数 || Vertex Shader Function
VertexOutput_DownSmpl vert_DownSmpl(VertexInput v)
{
//【6.1】实例化一个降采样输出结构
VertexOutput_DownSmpl o;
//【6.2】填充输出结构
//将三维空间中的坐标投影到二维窗口
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
//对图像的降采样:取像素上下左右周围的点,分别存于四级纹理坐标中
o.uv20 = v.texcoord + _MainTex_TexelSize.xy* half2(0.5h, 0.5h);;
o.uv21 = v.texcoord + _MainTex_TexelSize.xy * half2(-0.5h, -0.5h);
o.uv22 = v.texcoord + _MainTex_TexelSize.xy * half2(0.5h, -0.5h);
o.uv23 = v.texcoord + _MainTex_TexelSize.xy * half2(-0.5h, 0.5h);
//【6.3】返回最终的输出结果
return o;
}
//【7】片段着色函数 || Fragment Shader Function
fixed4 frag_DownSmpl(VertexOutput_DownSmpl i) : SV_Target
{
//【7.1】定义一个临时的颜色值
fixed4 color = (0,0,0,0);
//【7.2】四个相邻像素点处的纹理值相加
color += tex2D(_MainTex, i.uv20);
color += tex2D(_MainTex, i.uv21);
color += tex2D(_MainTex, i.uv22);
color += tex2D(_MainTex, i.uv23);
//【7.3】返回最终的平均值
return color / 4;
}
//【8】顶点输入结构体 || Vertex Input Struct
struct VertexOutput_Blur
{
//像素坐标
float4 pos : SV_POSITION;
//一级纹理(纹理坐标)
half4 uv : TEXCOORD0;
//二级纹理(偏移量)
half2 offset : TEXCOORD1;
};
//【9】顶点着色函数 || Vertex Shader Function
VertexOutput_Blur vert_BlurHorizontal(VertexInput v)
{
//【9.1】实例化一个输出结构
VertexOutput_Blur o;
//【9.2】填充输出结构
//将三维空间中的坐标投影到二维窗口
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
//纹理坐标
o.uv = half4(v.texcoord.xy, 1, 1);
//计算X方向的偏移量
o.offset = _MainTex_TexelSize.xy * half2(1.0, 0.0) * _DownSampleValue;
//【9.3】返回最终的输出结果
return o;
}
//【10】顶点着色函数 || Vertex Shader Function
VertexOutput_Blur vert_BlurVertical(VertexInput v)
{
//【10.1】实例化一个输出结构
VertexOutput_Blur o;
//【10.2】填充输出结构
//将三维空间中的坐标投影到二维窗口
o.pos = mul(UNITY_MATRIX_MVP, v.vertex);
//纹理坐标
o.uv = half4(v.texcoord.xy, 1, 1);
//计算Y方向的偏移量
o.offset = _MainTex_TexelSize.xy * half2(0.0, 1.0) * _DownSampleValue;
//【10.3】返回最终的输出结果
return o;
}
//【11】片段着色函数 || Fragment Shader Function
half4 frag_Blur(VertexOutput_Blur i) : SV_Target
{
//【11.1】获取原始的uv坐标
half2 uv = i.uv.xy;
//【11.2】获取偏移量
half2 OffsetWidth = i.offset;
//从中心点偏移3个间隔,从最左或最上开始加权累加
half2 uv_withOffset = uv - OffsetWidth * 3.0;
//【11.3】循环获取加权后的颜色值
half4 color = 0;
for (int j = 0; j< 7; j++)
{
//偏移后的像素纹理值
half4 texCol = tex2D(_MainTex, uv_withOffset);
//待输出颜色值+=偏移后的像素纹理值 x 高斯权重
color += texCol * GaussWeight[j];
//移到下一个像素处,准备下一次循环加权
uv_withOffset += OffsetWidth;
}
//【11.4】返回最终的颜色值
return color;
}
//-------------------结束CG着色语言声明部分 || End CG Programming Part------------------
ENDCG
FallBack Off
}
2.2 c#部分
源码如下:
using UnityEngine;
using System.Collections;
//设置在编辑模式下也执行该脚本
[ExecuteInEditMode]
//添加选项到菜单中
[AddComponentMenu("Shader/RapidBlurEffect")]
public class RapidBlurEffect : MonoBehaviour
{
//-------------------变量声明部分-------------------
#region Variables
//指定Shader名称
private string ShaderName = "Shader/RapidBlurEffect";
//着色器和材质实例
public Shader CurShader;
private Material CurMaterial;
//几个用于调节参数的中间变量
public static int ChangeValue;
public static float ChangeValue2;
public static int ChangeValue3;
//降采样次数
[Range(0, 6), Tooltip("[降采样次数]向下采样的次数。此值越大,则采样间隔越大,需要处理的像素点越少,运行速度越快。")]
public int DownSampleNum = 2;
//模糊扩散度
[Range(0.0f, 20.0f), Tooltip("[模糊扩散度]进行高斯模糊时,相邻像素点的间隔。此值越大相邻像素间隔越远,图像越模糊。但过大的值会导致失真。")]
public float BlurSpreadSize = 3.0f;
//迭代次数
[Range(0, 8), Tooltip("[迭代次数]此值越大,则模糊操作的迭代次数越多,模糊效果越好,但消耗越大。")]
public int BlurIterations = 3;
#endregion
//-------------------------材质的get&set----------------------------
#region MaterialGetAndSet
Material material
{
get
{
if (CurMaterial == null)
{
CurMaterial = new Material(CurShader);
CurMaterial.hideFlags = HideFlags.HideAndDontSave;
}
return CurMaterial;
}
}
#endregion
#region Functions
//-----------------------------------------【Start()函数】---------------------------------------------
// 说明:此函数仅在Update函数第一次被调用前被调用
//--------------------------------------------------------------------------------------------------------
void Start()
{
//依次赋值
ChangeValue = DownSampleNum;
ChangeValue2 = BlurSpreadSize;
ChangeValue3 = BlurIterations;
//找到当前的Shader文件
CurShader = Shader.Find(ShaderName);
//判断当前设备是否支持屏幕特效
if (!SystemInfo.supportsImageEffects)
{
enabled = false;
return;
}
}
//-------------------------------------【OnRenderImage()函数】------------------------------------
// 说明:此函数在当完成所有渲染图片后被调用,用来渲染图片后期效果
//--------------------------------------------------------------------------------------------------------
void OnRenderImage(RenderTexture sourceTexture, RenderTexture destTexture)
{
//着色器实例不为空,就进行参数设置
if (CurShader != null)
{
//【0】参数准备
//根据向下采样的次数确定宽度系数。用于控制降采样后相邻像素的间隔
float widthMod = 1.0f / (1.0f * (1 << DownSampleNum));
//Shader的降采样参数赋值
material.SetFloat("_DownSampleValue", BlurSpreadSize * widthMod);
//设置渲染模式:双线性
sourceTexture.filterMode = FilterMode.Bilinear;
//通过右移,准备长、宽参数值
int renderWidth = sourceTexture.width >> DownSampleNum;
int renderHeight = sourceTexture.height >> DownSampleNum;
// 【1】处理Shader的通道0,用于降采样 ||Pass 0,for down sample
//准备一个缓存renderBuffer,用于准备存放最终数据
RenderTexture renderBuffer = RenderTexture.GetTemporary(renderWidth, renderHeight, 0, sourceTexture.format);
//设置渲染模式:双线性
renderBuffer.filterMode = FilterMode.Bilinear;
//拷贝sourceTexture中的渲染数据到renderBuffer,并仅绘制指定的pass0的纹理数据
Graphics.Blit(sourceTexture, renderBuffer, material, 0);
//【2】根据BlurIterations(迭代次数),来进行指定次数的迭代操作
for (int i = 0; i < BlurIterations; i++)
{
//【2.1】Shader参数赋值
//迭代偏移量参数
float iterationOffs = (i * 1.0f);
//Shader的降采样参数赋值
material.SetFloat("_DownSampleValue", BlurSpreadSize * widthMod + iterationOffs);
// 【2.2】处理Shader的通道1,垂直方向模糊处理 || Pass1,for vertical blur
// 定义一个临时渲染的缓存tempBuffer
RenderTexture tempBuffer = RenderTexture.GetTemporary(renderWidth, renderHeight, 0, sourceTexture.format);
// 拷贝renderBuffer中的渲染数据到tempBuffer,并仅绘制指定的pass1的纹理数据
Graphics.Blit(renderBuffer, tempBuffer, material, 1);
// 清空renderBuffer
RenderTexture.ReleaseTemporary(renderBuffer);
// 将tempBuffer赋给renderBuffer,此时renderBuffer里面pass0和pass1的数据已经准备好
renderBuffer = tempBuffer;
// 【2.3】处理Shader的通道2,竖直方向模糊处理 || Pass2,for horizontal blur
// 获取临时渲染纹理
tempBuffer = RenderTexture.GetTemporary(renderWidth, renderHeight, 0, sourceTexture.format);
// 拷贝renderBuffer中的渲染数据到tempBuffer,并仅绘制指定的pass2的纹理数据
Graphics.Blit(renderBuffer, tempBuffer, CurMaterial, 2);
//【2.4】得到pass0、pass1和pass2的数据都已经准备好的renderBuffer
// 再次清空renderBuffer
RenderTexture.ReleaseTemporary(renderBuffer);
// 再次将tempBuffer赋给renderBuffer,此时renderBuffer里面pass0、pass1和pass2的数据都已经准备好
renderBuffer = tempBuffer;
}
//拷贝最终的renderBuffer到目标纹理,并绘制所有通道的纹理到屏幕
Graphics.Blit(renderBuffer, destTexture);
//清空renderBuffer
RenderTexture.ReleaseTemporary(renderBuffer);
}
//着色器实例为空,直接拷贝屏幕上的效果。此情况下是没有实现屏幕特效的
else
{
//直接拷贝源纹理到目标渲染纹理
Graphics.Blit(sourceTexture, destTexture);
}
}
//-----------------------------------------【OnValidate()函数】--------------------------------------
// 说明:此函数在编辑器中该脚本的某个值发生了改变后被调用
//--------------------------------------------------------------------------------------------------------
void OnValidate()
{
//将编辑器中的值赋值回来,确保在编辑器中值的改变立刻让结果生效
ChangeValue = DownSampleNum;
ChangeValue2 = BlurSpreadSize;
ChangeValue3 = BlurIterations;
}
//-----------------------------------------【Update()函数】--------------------------------------
// 说明:此函数每帧都会被调用
//--------------------------------------------------------------------------------------------------------
void Update()
{
//若程序在运行,进行赋值
if (Application.isPlaying)
{
//赋值
DownSampleNum = ChangeValue;
BlurSpreadSize = ChangeValue2;
BlurIterations = ChangeValue3;
}
//若程序没有在运行,去寻找对应的Shader文件
#if UNITY_EDITOR
if (Application.isPlaying != true)
{
CurShader = Shader.Find(ShaderName);
}
#endif
}
//-----------------------------------------【OnDisable()函数】---------------------------------------
// 说明:当对象变为不可用或非激活状态时此函数便被调用
//--------------------------------------------------------------------------------------------------------
void OnDisable()
{
if (CurMaterial)
{
//立即销毁材质实例
DestroyImmediate(CurMaterial);
}
}
#endregion
}