Unityで手軽に2D擦りガラスシェーダー

# 擦りガラスUIは好きですか

ぼくは大好きです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124429/045e6c13-8670-adc6-f6d7-8a21632cccba.png)

やります。

## 方針
　描画中の画面をテクスチャとして取得できる**GrabPass**をつかい、そいつに[ガウシアンブラー](https://ja.wikipedia.org/wiki/%E3%82%AC%E3%82%A6%E3%82%B7%E3%82%A2%E3%83%B3%E3%81%BC%E3%81%8B%E3%81%97)をかけるシェーダーを書きます。

### ガウシアンブラー
　既にいろんなサイトにも書いてある超メジャーなアルゴリズムなので詳しくは説明しませんが、要は「描画対象のピクセルに対して、その周辺ピクセルの色を距離に応じた重み付けをしながら平均を取る」ということになります。そしてこの重みづけに**ガウス関数**を利用するのでガウシアンブラーと呼ばれるわけです。この関数を使うことによりとてもいい感じのぼかしが得られます。

### 描画負荷
　ガウシアンブラーを愚直に実装すると画面上のそれぞれのピクセルに対してその周辺ピクセルを参照することになるわけですから、ぼかしの強さを高めると、かなりの速さで計算負荷が高まります。例えばぼかし範囲20x20ならば各ピクセルに対し20x20=400回のサンプリングがかかります。ですので、計算負荷を減らす工夫が必要です。
　一般的には次のような方法で行われます。まずは全体に**横方向のみのぼかし**を掛け、その画面に**縦方向のぼかし**を掛けます。こうすることにより、サンプリング回数は20+20=40回まで減らすことができるわけです。ちなみにこれを行った場合も同じ描画結果が得られます。

　ここで、2回のぼかし処理をどのように行うかが問題になってきます。[このサイト](http://edom18.hateblo.jp/entry/2018/12/30/171214)では画面ではなく特定のテクスチャに対してガウシアンブラーをかける方法が紹介されており、スクリプトから手動でシェーダーを呼び出せる**Graphics.Blit**を使って、横方向と縦方向の二回分Graphics.Blitを呼ぶ実装になっています。しかし今回のように**GrabPass**を使う場合、その方法ではうまく動きません。
（スクリプトの呼び出しタイミングではGrabPassが動かないため。どうやらGrabPassはカメラでの描画でしか機能しないっぽいです。そもそもカメラの描画タイミング以外では「その時点でカメラが描画した画面」が存在しないので当然といえる）

　なので、GrabPassを使う以上は意地でも**スクリプト無しで**実装しなければなりません。そこで**マルチパスシェーダー**を書きます。

　マルチパスシェーダーと言っても大したことではありません。要はひとつのシェーダーに二つの処理をぶち込むというだけのことです。縦方向ぼかしと横方向ぼかしをそれぞれ書けばいいのです。

## まずはGrabPassの下準備
　まずは、GrabPassで画面をテクスチャとして取得してそのまま表示するシェーダーを書きます。ブラーシェーダーはこれを原型に書いていきます。

```ShaderLab
Shader "Custom/Blur"
{
	Properties
	{
		_MainTex("Texture", 2D) = "white" {}
	}
	SubShader
	{

		Tags{ "Queue" = "Transparent" }

		GrabPass
		{	
		}
	
		Pass
		{
			CGPROGRAM

			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
				fixed4 color : COLOR;
			};
		
			struct v2f
			{
				float4 grabPos : TEXCOORD0;
				float4 pos : SV_POSITION;
				float4 vertColor : COLOR;
			};

			v2f vert(appdata v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.grabPos = ComputeGrabScreenPos(o.pos);
				o.vertColor = v.color;
				return o;
			}
		
			sampler2D _GrabTexture;
		
			half4 frag(v2f i) : SV_Target
			{
				half4 col = tex2Dproj(_GrabTexture, i.grabPos);
				return col;
			}
			ENDCG
		}
	}
}
```

# 横方向ブラーを書く
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124429/3350cef1-d35a-6837-736c-8d2e34997b17.png)

### パラメーター
Propertiesにガウシアンブラーで周辺ピクセルを取得する半径をパラメーターとして追加します。

```ShaderLab
	Properties
	{
		_MainTex("Texture", 2D) = "white" {}
		_Blur("Blur", Float) = 10
	}
```

### フラグメントシェーダ―
肝です。

```ShaderLab
...（前略）
			sampler2D _GrabTexture;
			fixed4 _GrabTexture_TexelSize;

			float _Blur;
		
			half4 frag(v2f i) : SV_Target
			{
				float blur = _Blur;
				blur = max(1, blur);

				fixed4 col = (0, 0, 0, 0);
				float weight_total = 0;

				[loop]
				for (float x = -blur; x <= blur; x += 1)
				{
					float distance_normalized = abs(x / blur);
					float weight = exp(-0.5 * pow(distance_normalized, 2) * 5.0);
					weight_total += weight;
					col += tex2Dproj(_GrabTexture, i.grabPos + float4(x * _GrabTexture_TexelSize.x, 0, 0, 0)) * weight;
				}

				col /= weight_total;
				return col;
			}
（後略）...
```

#### 解説
肝なので解説します。

***
```ShaderLab
				[loop]
				for (float x = -blur; x <= blur; x += 1)
				{
```
ブラー半径に基づいて、現在位置を中心として左から右へピクセルを舐めていくループです。
***
```ShaderLab
					float distance_normalized = abs(x / blur);
```
ブラー中心からの距離を求めます。0～1の範囲に正規化しているので、ブラー半径が変わっても同じように処理できるようになります。
***
```ShaderLab
					float weight = exp(-0.5 * pow(distance_normalized, 2) * 5.0);
					weight_total += weight;
```
ガウシアンブラーのアルゴリズムに基づいた重みの計算です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124429/f93032d6-3a33-031d-8da2-af805bc50791.png)
ガウシアンブラーの重みに使う関数はこんな感じの形ですが、yは完全な0にはならないため、うまいこと係数を調整して形状を合わせなければなりません。大体ブラー半径の両端で重みが0.1以下になるくらいに調整した結果、5.0がかかっています。
計算した重みはあとで平均を取るときに使うので、weight_totalに加算しておきます。
***
```ShaderLab
                    col += tex2Dproj(_GrabTexture, i.grabPos + float4(x * _GrabTexture_TexelSize.x, 0, 0, 0)) * weight;
```
定義した\_GrabTexture_TexelSize.xyには、GrabPassで取得した画面のテクスチャのサイズの逆数が入ってます。これがまさに１ピクセルぶんのuv空間上の距離になるわけです。あとはこれをもとに\_GrabTextureを参照して、重みを掛けて加算すればいいのです。
***
```ShaderLab
                col /= weight_total;
```
あとは加算した色を重みの合計で割れば、重みを考慮した平均値が求められます。



# 縦方向ブラーを書く

縦方向ブラーを追加します。**横方向ブラーを掛けた結果の画像に、さらに縦方法ブラーを掛ける必要がある**ので、GrabPassとPassをひとつずつ**書き足し**ます。Passの中身はほとんど横方向ブラーと同じなのでちょっと冗長な感じがしますが、こうせざるを得ないのであきらめて書きます。

```ShaderLab
...（前略）
				col /= weight_total;
				return col;
			}
			ENDCG
		}

		GrabPass
		{	
		}
	
		Pass
		{
			CGPROGRAM

			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
				fixed4 color : COLOR;
			};
		
			struct v2f
			{
				float4 grabPos : TEXCOORD0;
				float4 pos : SV_POSITION;
				float4 vertColor : COLOR;
			};

			v2f vert(appdata v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.grabPos = ComputeGrabScreenPos(o.pos);
				o.vertColor = v.color;
				return o;
			}
		
			sampler2D _GrabTexture;
			fixed4 _GrabTexture_TexelSize;

			float _Blur;
		
			half4 frag(v2f i) : SV_Target
			{
				float blur = _Blur;
				blur = max(1, blur);

				fixed4 col = (0, 0, 0, 0);
				float weight_total = 0;

				[loop]
				for (float y = -blur; y <= blur; y += 1)
				{
					float distance_normalized = abs(y / blur);
					float weight = exp(-0.5 * pow(distance_normalized, 2) * 5.0);
					weight_total += weight;
					col += tex2Dproj(_GrabTexture, i.grabPos + float4(0, y * _GrabTexture_TexelSize.y, 0, 0)) * weight;
				}

				col /= weight_total;
				return col;
			}
			ENDCG
		}

	}
}

```

できましたか？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124429/35534e1c-0382-df88-c7bc-69b2bdc490ed.png)

# 全文

```ShaderLab
Shader "Custom/Blur"
{
	Properties
	{
		_MainTex("Texture", 2D) = "white" {}
		_Blur("Blur", Float) = 10
	}
	SubShader
	{

		Tags{ "Queue" = "Transparent" }

		GrabPass
		{	
		}
	
		Pass
		{
			CGPROGRAM

			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
				fixed4 color : COLOR;
			};
		
			struct v2f
			{
				float4 grabPos : TEXCOORD0;
				float4 pos : SV_POSITION;
				float4 vertColor : COLOR;
			};

			v2f vert(appdata v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.grabPos = ComputeGrabScreenPos(o.pos);
				o.vertColor = v.color;
				return o;
			}
		
			sampler2D _GrabTexture;
			fixed4 _GrabTexture_TexelSize;

			float _Blur;
		
			half4 frag(v2f i) : SV_Target
			{
				float blur = _Blur;
				blur = max(1, blur);

				fixed4 col = (0, 0, 0, 0);
				float weight_total = 0;

				[loop]
				for (float x = -blur; x <= blur; x += 1)
				{
					float distance_normalized = abs(x / blur);
					float weight = exp(-0.5 * pow(distance_normalized, 2) * 5.0);
					weight_total += weight;
					col += tex2Dproj(_GrabTexture, i.grabPos + float4(x * _GrabTexture_TexelSize.x, 0, 0, 0)) * weight;
				}

				col /= weight_total;
				return col;
			}
			ENDCG
		}
		GrabPass
		{	
		}
	
		Pass
		{
			CGPROGRAM

			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"

			struct appdata
			{
				float4 vertex : POSITION;
				float2 uv : TEXCOORD0;
				fixed4 color : COLOR;
			};
		
			struct v2f
			{
				float4 grabPos : TEXCOORD0;
				float4 pos : SV_POSITION;
				float4 vertColor : COLOR;
			};

			v2f vert(appdata v)
			{
				v2f o;
				o.pos = UnityObjectToClipPos(v.vertex);
				o.grabPos = ComputeGrabScreenPos(o.pos);
				o.vertColor = v.color;
				return o;
			}
		
			sampler2D _GrabTexture;
			fixed4 _GrabTexture_TexelSize;

			float _Blur;
		
			half4 frag(v2f i) : SV_Target
			{
				float blur = _Blur;
				blur = max(1, blur);

				fixed4 col = (0, 0, 0, 0);
				float weight_total = 0;

				[loop]
				for (float y = -blur; y <= blur; y += 1)
				{
					float distance_normalized = abs(y / blur);
					float weight = exp(-0.5 * pow(distance_normalized, 2) * 5.0);
					weight_total += weight;
					col += tex2Dproj(_GrabTexture, i.grabPos + float4(0, y * _GrabTexture_TexelSize.y, 0, 0)) * weight;
				}

				col /= weight_total;
				return col;
			}
			ENDCG
		}

	}
}
```

## 注意点
今回のぼかし処理は画面全体ではなく特定領域のみに対して行っているため、領域の境界付近で正しい結果が得られない場合があります。実用の際はご留意ください。

## 参考
Unityでガウシアンブラーを実装する - e.blog
[http://edom18.hateblo.jp/entry/2018/12/30/171214](http://edom18.hateblo.jp/entry/2018/12/30/171214)

ShaderLab: GrabPass
[https://docs.unity3d.com/ja/current/Manual/SL-GrabPass.html](https://docs.unity3d.com/ja/current/Manual/SL-GrabPass.html)