Unityの公式アドオンで提供されているポストプロセスには、一般的なぼかしエフェクトが含まれていません。トランジションやオプション表示といった画面の転換にブラーを使いたいというケースはけっこうあるように思います。今回はそんなブラーの実装を詳しく解説していこうと思います。
Contents
平均ブラーの原理
今回実装するブラーは、平均ブラーと呼ばれるものです。平均ブラーは、周囲のテクセルから色の平均値を計算するブラー方式です。中心となるテクセルから、周囲8ピクセル分のカラーを合算し、その平均を取る、という手順です。
一回目の平均を取った後に、テクスチャサイズを半分にして二回目の平均を取ります。その後、さらにテクスチャサイズを半分にして三回目の平均を取ります。そして、最後的にサイズが1/4になったテクスチャをもとのサイズに戻して完成です。
平均ブラーの欠点
平均ブラーの大きなデメリットは、ブラーサイズを大きくすると、オブジェクトの輪郭がダブって見えることです。このように、平均ブラーではあまり強いブラーをかけることができません。
C#でのオフスクリーン処理
平均ブラーでは、スクリプトによるオフスクリーン処理も重要です。先ほど説明した、平均を取るプロセスを3回呼び出しています。なお、このスクリプトは、カメラコンポーネントを持つゲームオブジェクトにアタッチしてください。
using UnityEngine;
// !Cameraコンポーネントを持つゲームオブジェクトにアタッチしてください!
// ExecuteInEditMode : プレイしなくても動作させる
// ImageEffectAllowedInSceneView: シーンビューにポストエフェクトを反映させる
[ExecuteInEditMode, ImageEffectAllowedInSceneView]
public class BasicBlurEffectInserter : MonoBehaviour
{
[SerializeField]
private Material _material;
private int resolution;
private Vector2 displaySize;
private void Awake()
{
resolution = Shader.PropertyToID("_Resolution"); //シェーダーのプロパティIDを検索
}
private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
displaySize.x = Screen.currentResolution.width; //ディスプレイの高さを取得
displaySize.y = Screen.currentResolution.height; //ディスプレイの幅を取得
_material.SetVector(resolution, displaySize); //シェーダーのプロパティに流し込み
//////////横幅を半分にしたレンダーテスクチャを作成(まだ、なにも描かれていない)
var rth = RenderTexture.GetTemporary(src.width / 2, src.height);
Graphics.Blit(src, rth, _material); //シェーダー処理を加えて、横半分のレンダーテクスチャにコピー
/////////
/////////先のテクスチャサイズから、さらに縦半分にしたレンダーテスクチャを作成(まだ、なにも描かれていない)
var rtv = RenderTexture.GetTemporary(rth.width, rth.height / 2);
Graphics.Blit(rth, rtv, _material); //シェーダー処理を加えて、縦半分のレンダーテクスチャにコピー
/////////
Graphics.Blit(rtv, dest, _material); //元サイズから1/4になったレンダーテクスチャを、元のサイズに戻す
RenderTexture.ReleaseTemporary(rtv); //テンポラリレンダーテスクチャの開放
RenderTexture.ReleaseTemporary(rth); //開放しないとメモリリークするので注意
}
}
OnRenderImage()は、Unityのフレーム描画サイクルの中で呼び出されるコールバックで、カメラのレンダリングが完了したときに発火します。後述するシェーダー処理を、Graphis.Blitというメソッドを通すことで適用しています。平均ブラーのポストプロセスでは、Graphics.Blitを三回呼び出しているので、ドローコールは三回分の負荷となっています。
シェーダー内でのブラー処理
フラグメントシェーダー内でのブラー処理は、以下の通りです。計算のスタートとなるテクセルを中心に、周囲8テクセル分を取得し、その平均を返します。_BlurLevelはマテリアルのプロパティから、_Resolutionは前述のC#スクリプトから動的に渡しています。
float _BlurLevel; //ブラーの強度
float2 _Resolution; //C#から渡ってくるディスプレイサイズ
fixed4 frag(v2f i) : SV_Target
{
float offsetU = _BlurLevel / _Resolution.x; //U方向のオフセットを計算
float offsetV = _BlurLevel / _Resolution.y; //V方向のオフセットを計算
float4 color = tex2D(_MainTex, i.uv);
color += tex2D(_MainTex, i.uv + float2(offsetU, 0.0f)); //右のテクセルのカラーをサンプリング
color += tex2D(_MainTex, i.uv + float2(-offsetU, 0.0f)); //左のテクセルのカラーをサンプリング
color += tex2D(_MainTex, i.uv + float2(0.0f, offsetV)); //下のテクセルのカラーをサンプリング
color += tex2D(_MainTex, i.uv + float2(0.0f, -offsetV)); //上のテクセルのカラーをサンプリング
color += tex2D(_MainTex, i.uv + float2(offsetU, offsetV)); //右下のテクセルのカラーをサンプリング
color += tex2D(_MainTex, i.uv + float2(offsetU, -offsetV)); //右上のテクセルのカラーをサンプリング
color += tex2D(_MainTex, i.uv + float2(-offsetU, offsetV)); //左下のテクセルのカラーをサンプリング
color += tex2D(_MainTex, i.uv + float2(-offsetU, -offsetV)); //左上のテクセルのカラーをサンプリング
color /= 9.0f; //8テクセル分の色が加算されているので、9で除算し平均化
return color;
}
シェーダー全体
ここまで解説してきたコードをまとめると、以下のようになります。なお、このシェーダーを一回だけ適用しても、効果的なブラーはかかりません。前述したC#スクリプトと合わせて使用する必要があるので、注意してください。
Shader "CAGraphicsAcademy/BasicBlur"
{
Properties{
[HideInInspector] _MainTex("Texture", 2D) = "white" {}
_BlurLevel("Blur Level", int) = 1
}
SubShader
{
Cull Off // カリングは不要
ZTest Always // ZTestは常に通す
ZWrite Off // ZWriteは不要
Tags { "RenderType" = "Opaque" }
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float _BlurLevel; //ブラーの強度
float2 _Resolution; //C#から渡ってくるディスプレイサイズ
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
float offsetU = _BlurLevel / _Resolution.x; //U方向のオフセットを計算
float offsetV = _BlurLevel / _Resolution.y; //V方向のオフセットを計算
float4 color = tex2D(_MainTex, i.uv);
color += tex2D(_MainTex, i.uv + float2(offsetU, 0.0f)); //右のテクセルのカラーをサンプリング
color += tex2D(_MainTex, i.uv + float2(-offsetU, 0.0f)); //左のテクセルのカラーをサンプリング
color += tex2D(_MainTex, i.uv + float2(0.0f, offsetV)); //下のテクセルのカラーをサンプリング
color += tex2D(_MainTex, i.uv + float2(0.0f, -offsetV)); //上のテクセルのカラーをサンプリング
color += tex2D(_MainTex, i.uv + float2(offsetU, offsetV)); //右下のテクセルのカラーをサンプリング
color += tex2D(_MainTex, i.uv + float2(offsetU, -offsetV)); //右上のテクセルのカラーをサンプリング
color += tex2D(_MainTex, i.uv + float2(-offsetU, offsetV)); //左下のテクセルのカラーをサンプリング
color += tex2D(_MainTex, i.uv + float2(-offsetU, -offsetV)); //左上のテクセルのカラーをサンプリング
color /= 9.0f; //8テクセル分の色が加算されているので、9で除算し平均化
return color;
}
ENDCG
}
}
}
まとめ
今回紹介した平均ブラーは、大きなブラーをかけられないというデメリットがあるものの、考え方が非常に簡単で、初めてポストプロセスを実装する方にもおすすめです。もうすこし実践的にきれいなブラーを実装したいという方は、ガウシアンブラーで調べてみてください。こちらは加重平均を用いたブラーなので、より自然なボケ味を表現することができます。