iT邦幫忙

第 12 屆 iT 邦幫忙鐵人賽

DAY 26
0
自我挑戰組

初見Unity Shader系列 第 26

畫面模糊效果

文章內使用Unity 2019 LTS

模糊(Blur),不只是在遊戲中,在一般的網頁或APP仔細觀察的話,都會有這個視覺效果,今天(Day26)就來介紹「模糊」這怎麼做出來的吧!

目標

  • 模糊效果

高斯模糊

這是最常見的模糊方式,比起電腦圖學,更會在圖像處理找到這個詞。

要完成這個功能,我們需要兩個東西:

  1. 旋積(Convolution)
  2. 濾波核心(Filter Kernel)

不需要完全知道這兩個在數學的定義是甚麼,我們只需要知道它們做了甚麼,請看下圖:

from candycat1992/Unity_Shaders_Book

上圖提到的「捲積核」,就是濾波核心,而上面整個操作就是「旋積」。

在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);
		}
    }
}

Reference


上一篇
Unity中存取相機視角的方法
下一篇
Gamma校正
系列文
初見Unity Shader30

尚未有邦友留言

立即登入留言