擦りガラスUIは好きですか
やります。
方針
描画中の画面をテクスチャとして取得できるGrabPassをつかい、そいつにガウシアンブラーをかけるシェーダーを書きます。
ガウシアンブラー
既にいろんなサイトにも書いてある超メジャーなアルゴリズムなので詳しくは説明しませんが、要は「描画対象のピクセルに対して、その周辺ピクセルの色を距離に応じた重み付けをしながら平均を取る」ということになります。そしてこの重みづけにガウス関数を利用するのでガウシアンブラーと呼ばれるわけです。この関数を使うことによりとてもいい感じのぼかしが得られます。
描画負荷
ガウシアンブラーを愚直に実装すると画面上のそれぞれのピクセルに対してその周辺ピクセルを参照することになるわけですから、ぼかしの強さを高めると、かなりの速さで計算負荷が高まります。例えばぼかし範囲20x20ならば各ピクセルに対し20x20=400回のサンプリングがかかります。ですので、計算負荷を減らす工夫が必要です。
一般的には次のような方法で行われます。まずは全体に横方向のみのぼかしを掛け、その画面に縦方向のぼかしを掛けます。こうすることにより、サンプリング回数は20+20=40回まで減らすことができるわけです。ちなみにこれを行った場合も同じ描画結果が得られます。
ここで、2回のぼかし処理をどのように行うかが問題になってきます。このサイトでは画面ではなく特定のテクスチャに対してガウシアンブラーをかける方法が紹介されており、スクリプトから手動でシェーダーを呼び出せるGraphics.Blitを使って、横方向と縦方向の二回分Graphics.Blitを呼ぶ実装になっています。しかし今回のようにGrabPassを使う場合、その方法ではうまく動きません。
(スクリプトの呼び出しタイミングではGrabPassが動かないため。どうやらGrabPassはカメラでの描画でしか機能しないっぽいです。そもそもカメラの描画タイミング以外では「その時点でカメラが描画した画面」が存在しないので当然といえる)
なので、GrabPassを使う以上は意地でもスクリプト無しで実装しなければなりません。そこでマルチパスシェーダーを書きます。
マルチパスシェーダーと言っても大したことではありません。要はひとつのシェーダーに二つの処理をぶち込むというだけのことです。縦方向ぼかしと横方向ぼかしをそれぞれ書けばいいのです。
まずはGrabPassの下準備
まずは、GrabPassで画面をテクスチャとして取得してそのまま表示するシェーダーを書きます。ブラーシェーダーはこれを原型に書いていきます。
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
}
}
}
横方向ブラーを書く
パラメーター
Propertiesにガウシアンブラーで周辺ピクセルを取得する半径をパラメーターとして追加します。
Properties
{
_MainTex("Texture", 2D) = "white" {}
_Blur("Blur", Float) = 10
}
フラグメントシェーダ―
肝です。
...(前略)
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;
}
(後略)...
解説
肝なので解説します。
[loop]
for (float x = -blur; x <= blur; x += 1)
{
ブラー半径に基づいて、現在位置を中心として左から右へピクセルを舐めていくループです。
float distance_normalized = abs(x / blur);
ブラー中心からの距離を求めます。0~1の範囲に正規化しているので、ブラー半径が変わっても同じように処理できるようになります。
float weight = exp(-0.5 * pow(distance_normalized, 2) * 5.0);
weight_total += weight;
ガウシアンブラーのアルゴリズムに基づいた重みの計算です。
ガウシアンブラーの重みに使う関数はこんな感じの形ですが、yは完全な0にはならないため、うまいこと係数を調整して形状を合わせなければなりません。大体ブラー半径の両端で重みが0.1以下になるくらいに調整した結果、5.0がかかっています。
計算した重みはあとで平均を取るときに使うので、weight_totalに加算しておきます。
col += tex2Dproj(_GrabTexture, i.grabPos + float4(x * _GrabTexture_TexelSize.x, 0, 0, 0)) * weight;
定義した_GrabTexture_TexelSize.xyには、GrabPassで取得した画面のテクスチャのサイズの逆数が入ってます。これがまさに1ピクセルぶんのuv空間上の距離になるわけです。あとはこれをもとに_GrabTextureを参照して、重みを掛けて加算すればいいのです。
col /= weight_total;
あとは加算した色を重みの合計で割れば、重みを考慮した平均値が求められます。
縦方向ブラーを書く
縦方向ブラーを追加します。横方向ブラーを掛けた結果の画像に、さらに縦方法ブラーを掛ける必要があるので、GrabPassとPassをひとつずつ書き足します。Passの中身はほとんど横方向ブラーと同じなのでちょっと冗長な感じがしますが、こうせざるを得ないのであきらめて書きます。
...(前略)
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
}
}
}
全文
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
ShaderLab: GrabPass
https://docs.unity3d.com/ja/current/Manual/SL-GrabPass.html