文章內使用Unity 2019 LTS
模糊(Blur),不只是在遊戲中,在一般的網頁或APP仔細觀察的話,都會有這個視覺效果,今天(Day26)就來介紹「模糊」這怎麼做出來的吧!
這是最常見的模糊方式,比起電腦圖學,更會在圖像處理找到這個詞。
要完成這個功能,我們需要兩個東西:
不需要完全知道這兩個在數學的定義是甚麼,我們只需要知道它們做了甚麼,請看下圖:
上圖提到的「捲積核」,就是濾波核心,而上面整個操作就是「旋積」。
在5x5的圖像中,假設每一個方格為一個像素(註1),假設3x3濾波核心每個一格的權重皆為1
,那在進行旋積操作時,就是把核心對應到該圖的像素上,該點像素的值乘上對應到的核心權重,之後把這9個值相加,除以9,最後得到的就是中間「紅色方框」的新像素。
下面的影片認為更好的解釋,「旋積」是甚麼
註1: 會說假設是因為,不一定一次取樣一個像素,可以是N個,但每個方格取樣數量是一樣的。
先看左邊,下面為一個5x5的核心,對於一個畫面來說,我們要計數量就會變成N x N x W x H,但我們可以像右邊一樣,把二維變成一維的,也就是縱向與橫向的分開算,這樣計算量就會變成 2 x N x W x H
下面是Shader
Shader "Learning/Blur" {
Properties {
_MainTex ("MainTex", 2D) = "white" {}
// 一次取樣的大小
_BlurSize ("Blur Size", Float) = 1.0
}
SubShader {
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;
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;
// uv[0] 對應到上面所說的「紅色方框」
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
// 利用2個Pass處理,縱向與橫向
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
}
}
}
以下是Script
using UnityEngine;
[RequireComponent(typeof(Camera))]
public class Blur : MonoBehaviour
{
public Shader BlurShader = null;
private Material material;
[Range(0, 4)]
public int iterations = 3;
[Range(0.2f, 5.0f)]
public float blurSpread = 0.6f;
private void Start() {
if (!BlurShader.isSupported)
{
return;
}
else{
material = new Material(BlurShader);
material.hideFlags = HideFlags.DontSave;
}
}
private void OnRenderImage(RenderTexture src, RenderTexture dest) {
if (material != null) {
// 呼叫RenderTexture.GetTemporary是因為我們用了兩個pass
RenderTexture buffer0 = RenderTexture.GetTemporary(src.width, src.height, 0);
buffer0.filterMode = FilterMode.Bilinear;
Graphics.Blit(src, buffer0);
for (int i = 0; i < iterations; i++) {
material.SetFloat("_BlurSize", 1.0f + i * blurSpread);
// 存取第一個Pass的結果
RenderTexture buffer1 = RenderTexture.GetTemporary(src.width, src.height, 0);
// 縱向(Vertical)處理
Graphics.Blit(buffer0, buffer1, material, 0);
// 記得釋放
RenderTexture.ReleaseTemporary(buffer0);
// 存取第二個Pass的結果
buffer0 = buffer1;
buffer1 = RenderTexture.GetTemporary(src.width, src.width, 0);
// 橫向(Horizontal)處理
Graphics.Blit(buffer0, buffer1, material, 1);
RenderTexture.ReleaseTemporary(buffer0);
buffer0 = buffer1;
}
// 最後才處理完,返回結果
Graphics.Blit(buffer0, dest);
RenderTexture.ReleaseTemporary(buffer0);
} else {
Graphics.Blit(src, dest);
}
}
}