Unity Shader系列文章:Unity Shader目錄-初級篇
Unity Shader系列文章:Unity Shader目錄-中級篇
效果:

卷積的另一個常見應(yīng)用一高斯模糊。模糊的實現(xiàn)有很多方法,例如均值模糊和中值模糊。均值模糊同樣使用了卷積操作,它使用的卷積核中的各個元素值都相等,且相加等于1, 也就是說,卷積后得到的像素值是其鄰域內(nèi)各個像素值的平均值。而中值模糊則是選擇鄰域內(nèi)對所有像素排序后的中值替換掉原顏色。 一個更高級的模糊方法是高斯模糊。
高斯模糊同樣利用了卷積計算 ,它使用的卷積核名為高斯核 。高斯核是一個正方形大小的濾波核(如下圖),其中每個元素的計算都是基于下面的高斯方程:
其中, 是標(biāo)準(zhǔn)方差(一般取值為 1), 分別對應(yīng)了當(dāng)前位置到卷積核中心的整數(shù)距離。要構(gòu)建一個高斯核 ,我們只需要計算高斯核中各個位置對應(yīng)的高斯值 為了保證濾波后的圖像不會變暗,我 需要對高斯核中的權(quán)重進行歸 化,即讓每個權(quán)重除以所有權(quán)重的和,這樣可以保證所有權(quán)重的和為 。因此 高斯函數(shù)中 前面的系數(shù)實際不會對結(jié)果有任何影響。
高斯方程很好地模擬了鄰域每個像素對當(dāng)前處理像素的影響程度一距離越近,影響越大。高斯核的維數(shù)越高,模糊程度越大。使用一個 NxN 的高斯核對圖像進行卷積濾波,就需要 NxNxWxH(W和H分別是圖像的寬和高)次紋理采樣。當(dāng)N的大小不斷增加時,采樣次數(shù)會變得非常巨大幸運的是,我們可以把這個二維高斯函數(shù)拆分成兩個一維函數(shù)。也就是說,我們可以使用兩個一維的高斯核(如圖)先后對圖像進行濾波,它們得到的結(jié)果和直接使用二維高斯核是一樣的,但采樣次數(shù)只需要 2xNxWxH 。我們可以進一步觀察到,兩個一維高斯核中包含了很多重復(fù)的權(quán)重。對于一個大小為 的一維高斯核,我們實際只需要記錄3個權(quán)重值即可。

具體實現(xiàn)思路:
將先后調(diào)用兩個 Pass,第一個 Pass 將會使用豎直方向的一維高斯核對圖像進行濾波,第二個 Pass 再使用水平方向的維高斯核對圖像進行濾波,得到最終的目標(biāo)圖像。在實現(xiàn)中,還將利用圖像縮放來進一步提高性能,并通過調(diào)整高斯濾波的應(yīng)用次數(shù)來控制模糊程度(次數(shù)越多,圖像越模糊)。
ScreenPostEffectsBase基類代碼:
using UnityEngine;
/// <summary>
/// 屏幕后處理效果基類
/// </summary>
[ExecuteInEditMode]
[RequireComponent(typeof(Camera))]
public class ScreenPostEffectsBase : MonoBehaviour
{
public Shader Shader;
public Material Material
{
get
{
return CheckAndCreateMaterial();
}
}
private Material _material;
protected void Start()
{
CheckResources();
}
/// <summary>
/// 檢查資源
/// </summary>
protected void CheckResources()
{
if (!CheckSupport())
{
NotSupported();
}
}
/// <summary>
/// 檢查支持
/// </summary>
/// <returns></returns>
protected bool CheckSupport()
{
bool isSupported = SystemInfo.supportsImageEffects;
return isSupported;
}
/// <summary>
/// 不支持
/// </summary>
protected void NotSupported()
{
enabled = false;
}
/// <summary>
/// 檢查和創(chuàng)建Material
/// </summary>
/// <returns></returns>
protected Material CheckAndCreateMaterial()
{
if (!Shader || !Shader.isSupported)
{
return null;
}
if (_material && _material.shader == Shader)
{
return _material;
}
_material = new Material(Shader);
_material.hideFlags = HideFlags.DontSave;
return _material;
}
}
ScreenGaussianBlur派生類代碼:
using UnityEngine;
/// <summary>
/// 屏幕后處理效果:高斯模糊
/// </summary>
public class ScreenGaussianBlur : ScreenPostEffectsBase
{
/// <summary>
/// 迭代此時
/// </summary>
[Range(0, 4)]
public int Iterations = 2;
/// <summary>
/// 模糊擴散
/// </summary>
[Range(0.2f, 3)]
public float BlurSpread = 1;
/// <summary>
/// 縮放系數(shù)
/// </summary>
[Range(1, 8)]
public int DownSample = 2;
// // 第一個版本,最簡單的OnRenderlmage實現(xiàn):
// private void OnRenderImage(RenderTexture src, RenderTexture dest)
// {
// if (Material != null)
// {
// int rtW = src.width;
// int rtH = src.height;
// // 利用 RenderTexture GetTemporary 函數(shù)分配了一塊與屏幕圖像大小相同的緩沖區(qū)。
// // 這是因為 高斯模糊需要調(diào)用兩個Pass, 我們需要使用一塊中間緩存來存儲第一個Pass執(zhí)行完畢后得到的模糊結(jié)果。
// RenderTexture buffer = RenderTexture.GetTemporary(rtW, rtH, 0);
// // 使用 Shader 中的第1個Pass (即使用豎直方向的一維高斯核進行濾波)對src進行處理,并將結(jié)果存儲在了buffer中。
// Graphics.Blit(src, buffer, Material, 0);
// // 然后,再使用Shader中的第2個Pass (即使用水平方向的一維高斯核進行濾波)對buffer進行處理,返回最終屏幕圖像。
// Graphics.Blit(buffer, dest, Material, 1);
// // 最后 還需要釋放之前分配的緩存。
// RenderTexture.ReleaseTemporary(buffer);
// }
// else
// {
// Graphics.Blit(src, dest);
// }
// }
// // 第二個版本實現(xiàn):利用縮放對圖像進行降采樣,從而減少需要處理的像素 提高性能。
// private void OnRenderImage(RenderTexture src, RenderTexture dest)
// {
// if (Material)
// {
// int rtw = src.width / DownSample;
// int rtH = src.height / DownSample;
// // 使用了小于原屏幕分辨率的尺寸
// RenderTexture buffer = RenderTexture.GetTemporary(rtw, rtH, 0);
// // 并將該臨時渲染紋理的濾波模式設(shè)置為雙線性
// buffer.filterMode = FilterMode.Bilinear;
// // 這樣,在調(diào)用第一個 Pass 時,我們需要處理的像素個數(shù)就是原來的幾分之一。
// // 對圖像進行降采樣不僅可以減少需要處理的像素個數(shù),提高性能,而且適當(dāng)?shù)慕挡蓸油€可以得到更好的模糊效果。
// // 盡管 downSample 值越大,性能越好,但過大的 downSample 可能會造成圖像像素化。
// Graphics.Blit(src, buffer, Material, 0);
// Graphics.Blit(buffer, dest, Material, 1);
// RenderTexture.ReleaseTemporary(buffer);
// }
// else
// {
// Graphics.Blit(src, dest);
// }
// }
// 第三個版本實現(xiàn):考慮了高斯模糊的迭代次數(shù),何利用兩個臨時緩存在迭代之間進行交替的過程。
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
if (Material)
{
int rtW = src.width / DownSample;
int rtH = src.height / DownSample;
// 定義了第一個緩存 bufferO, 并把 src 中的圖像縮放后存儲到 bufferO 中
RenderTexture buffer0 = RenderTexture.GetTemporary(rtW, rtH, 0);
buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(src, buffer0);
for (var i = 0; i < Iterations; i++)
{
Material.SetFloat("_BlurSize", 1 + i * BlurSpread);
// 定義了第二個緩存 bufferl
RenderTexture buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 執(zhí)行第一個 Pass 時,輸入 bufferO, 輸出是 bufferl
Graphics.Blit(buffer0, buffer1, Material, 0);
// 完畢后首先把 bufferO 釋放,再把結(jié)果值 buffer 存儲到 bufferO 中,重新分配 bufferl
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(rtW, rtH, 0);
// 調(diào)用第二個Pass, 重復(fù)上述過程
Graphics.Blit(buffer0, buffer1, Material, 1);
// 完畢后再把 bufferO 釋放,再把結(jié)果值 buffer 存儲到 bufferO 中
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
// 迭代完成后 bufferO 將存儲最終的圖像,將結(jié)果顯示到屏幕上
Graphics.Blit(buffer0, dest);
// 釋放緩存
RenderTexture.ReleaseTemporary(buffer0);
}
else
{
Graphics.Blit(src, dest);
}
}
}
Shader代碼:
// 高斯模糊
Shader "Custom/GaussianBlur"
{
Properties
{
_MainTex ("MainTex", 2D) = "white" { }
_BlurSize ("Blur Size", Float) = 1 // 控制采樣距離,在高斯核維數(shù)不變的情況下, BlurSize 越大,模糊程度越高
}
SubShader
{
CGINCLUDE
#include "UnityCG.cginc"
sampler2D _MainTex;
// _MainTex紋理的紋素大小,例如521x512大小的紋理,值約為0.001953,即1/512
half4 _MainTex_TexelSize;
float _BlurSize;
// 頂點著色器傳遞給片元著色器的數(shù)據(jù)
struct v2f
{
float4 pos: SV_POSITION; // 裁剪空間下的頂點坐標(biāo)
half2 uv[5]: TEXCOORD0; // 數(shù)組的第一個坐標(biāo)存儲了當(dāng)前的采樣紋理,而剩余的四個坐標(biāo)則是高斯模糊中對鄰域采樣時使用的紋理坐標(biāo)
};
// 頂點著色器,豎直模糊處理
v2f vertBlurVertical(appdata_img v)
{
v2f o;
// 將頂點坐標(biāo)從模型空間變換到裁剪空間
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
// 豎直的5個相鄰的像素
o.uv[0] = uv;
o.uv[1] = uv + float2(0, _MainTex_TexelSize.y * 1) * _BlurSize;
o.uv[2] = uv - float2(0, _MainTex_TexelSize.y * 1) * _BlurSize;
o.uv[3] = uv + float2(0, _MainTex_TexelSize.y * 2) * _BlurSize;
o.uv[4] = uv - float2(0, _MainTex_TexelSize.y * 2) * _BlurSize;
return o;
}
// 頂點著色器,水平模糊處理
v2f vertBlurHorizontal(appdata_img v)
{
v2f o;
// 將頂點坐標(biāo)從模型空間變換到裁剪空間
o.pos = UnityObjectToClipPos(v.vertex);
half2 uv = v.texcoord;
// 水平的5個相鄰的像素
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
{
// 一個 sxs 的二維高斯核可以拆分成兩個大小為 5 的一維高斯核, 并且由千它的對稱性,我們只需要記錄3個高斯權(quán)重,
float weight[3] = {
0.4026, 0.2442, 0.0545
};
// 將結(jié)果值 sum 初始化為當(dāng)前的像素值乘以它的權(quán)重值
fixed3 sum = tex2D(_MainTex, i.uv[0]).rgb * weight[0];
// 根據(jù)對稱性,進行了兩次迭代,每次迭代包含了兩次紋理采樣,并把像素值和權(quán)重相乘后的結(jié)果疊加到 sum 中
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
{
// Pass名字,這樣別的Shader可以通過此名字調(diào)用此Pass
// 如: UsePass "Cusstom/GaussianBlur/GAUSSIAN_BLUR_VERTICAL"
NAME "GAUSSIAN_BLUR_VERTICAL"
CGPROGRAM
#pragma vertex vertBlurVertical
#pragma fragment fragBlur
ENDCG
}
Pass
{
// Pass名字,這樣別的Shader可以通過此名字調(diào)用此Pass
// 如: UsePass "Cusstom/GaussianBlur/GAUSSIAN_BLUR_HORIZONTAL"
NAME "GAUSSIAN_BLUR_HORIZONTAL"
CGPROGRAM
#pragma vertex vertBlurHorizontal
#pragma fragment fragBlur
ENDCG
}
}
FallBack "Diffuse"
}