【UnityShader】ガウシアンブラー【2】 #65
前回の成果
今回やること
前回に引き続き、ガウシアンブラーについて説明します。
今回はShaderについて解説するので、Shaderのソースを添付します。
ソースコード
Shader "Unlit/GaussianBlur" { Properties { _MainTex ("Texture", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 100 Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #pragma fragmentoption ARB_precision_hint_fastest #include "UnityCG.cginc" struct appdata { float4 vertex : POSITION; float2 uv : TEXCOORD0; }; struct v2f { float2 uv : TEXCOORD0; float4 vertex : SV_POSITION; }; sampler2D _MainTex; half4 _Offset; static const int samplingCount = 10; half _Weights[samplingCount]; v2f vert (appdata v) { v2f o; o.vertex = UnityObjectToClipPos(v.vertex); o.uv = v.uv; return o; } fixed4 frag (v2f i) : SV_Target { fixed4 col = 0; [unroll] for (int j = samplingCount - 1; j > 0; j--) { col += tex2D(_MainTex, i.uv - (_Offset.xy * j)) * _Weights[j]; } [unroll] for (int j = 0; j < samplingCount; j++) { col += tex2D(_MainTex, i.uv + (_Offset.xy * j)) * _Weights[j]; } return col; } ENDCG } } }
予約語
Shaderの中身の解説に入る前に、Unity側で定義済みの予約語があるのでそれについて説明します。
ARB_precision_hint_fastest
フラグメントの処理で、可能な限り精度を下げて実行時間を最小限に抑えたい時に使用するオプションとなります。
ARB_precision_hint_nicestという予約語もあり、こちらは精度を上げるオプションになります。
ARB_precision_hint_fastestと
ARB_precision_hint_nicestは同時に指定するとエラーとなってしまうので気をつけてください。
詳しくは以下サイト様に記載されています。
https://www.khronos.org/registry/OpenGL/extensions/ARB/ARB_fragment_program.txt
unroll
コンパイル後のコードの記述の仕方を変えるものです。
他の指定の仕方にloopがあるので、そちらの説明も以下の表で行います。
\ | unroll | loop |
---|---|---|
記述方法 | ループ内のコードがループ回数分記述 | アセンブラコードでループがそのまま記述 |
メモリサイズ | 大きくなる | 抑えられる |
実行速度 | 高速になる | 低速になる |
より詳しいことに関しましては以下サイト様をご参照ください。
Shaderの解説
予約語について理解できたので、Shaderの解説に移ります。
サンプリングする
[unroll] for (int j = samplingCount - 1; j > 0; j--) { col += tex2D(_MainTex, i.uv - (_Offset.xy * j)) * _Weights[j]; } [unroll] for (int j = 0; j < samplingCount; j++) { col += tex2D(_MainTex, i.uv + (_Offset.xy * j)) * _Weights[j]; }
まずは左右、次に上下のテクセルをサンプリングしています。
重みやオフセットに応じて色が変化するのがわかると思います。
また、重みとオフセットは今回C#にて渡しています。
C#のソースコード
using UnityEngine; public class GaussianBlur : MonoBehaviour { [SerializeField] private Texture _texture; [SerializeField] private Shader _shader; [SerializeField, Range(1f, 10f)] private float _offset = 1f; [SerializeField, Range(10f, 1000f)] private float _blur = 10f; private Material _material; private Renderer _renderer; private RenderTexture _rt1; private RenderTexture _rt2; private float[] _weights = new float[10]; private bool _isInitialized = false; private void Awake() { Initialize(); } private void OnValidate() { if (!Application.isPlaying) { return; } UpdateWeights(); Blur(); } private void Initialize() { if (_isInitialized) { return; } _material = new Material(_shader); _material.hideFlags = HideFlags.HideAndDontSave; _rt1 = RenderTexture.GetTemporary(_texture.width / 2, _texture.height / 2, 0, RenderTextureFormat.ARGB32); _rt2 = RenderTexture.GetTemporary(_texture.width / 2, _texture.height / 2, 0, RenderTextureFormat.ARGB32); _renderer = GetComponent<Renderer>(); UpdateWeights(); _isInitialized = true; } public void Blur() { if (!_isInitialized) { Initialize(); } Graphics.Blit(_texture, _rt1); _material.SetFloatArray("_Weights", _weights); float x = _offset / _rt1.width; float y = _offset / _rt1.height; _material.SetVector("_Offset", new Vector4(x, 0, 0, 0)); Graphics.Blit(_rt1, _rt2, _material); _material.SetVector("_Offset", new Vector4(0, y, 0, 0)); Graphics.Blit(_rt2, _rt1, _material); _renderer.material.mainTexture = _rt1; } private void UpdateWeights() { float total = 0; float d = _blur * _blur * 0.001f; for (int i = 0; i < _weights.Length; i++) { float x = 1.0f + i * 2f; float w = Mathf.Exp(-0.5f * (x * x) / d); _weights[i] = w; if (i > 0) { w *= 2.0f; } total += w; } for (int i = 0; i < _weights.Length; i++) { _weights[i] /= total; } } private void OnDestroy() { RenderTexture.ReleaseTemporary(_rt1); RenderTexture.ReleaseTemporary(_rt2); } }
予約語
こちらもShaderと同じように定義済みのものから解説していこうと思います。
OnValidate
Inspectorから自身の値が変更された場合に自動で呼ばれる関数となります。
Application.isPlaying
ビルドされている場合、trueを返すものになります。
OnDestroy()
破棄される、もしくはエディタの再生を停止した場合に呼ばれる関数となります。
C#の解説
では、C#側の解説をしていきます。
Initialize
private void Initialize() { if (_isInitialized) { return; } _material = new Material(_shader); _material.hideFlags = HideFlags.HideAndDontSave; _rt1 = RenderTexture.GetTemporary(_texture.width / 2, _texture.height / 2, 0, RenderTextureFormat.ARGB32); _rt2 = RenderTexture.GetTemporary(_texture.width / 2, _texture.height / 2, 0, RenderTextureFormat.ARGB32); _renderer = GetComponent<Renderer>(); UpdateWeights(); _isInitialized = true; }
ここでは、マテリアルとRenderTexureの準備を行っています。
RenderTextureを2つ使用しているのは、縦と横にブラーをするためです。
RenderTexture系の処理に関しましては、CommandBufferの際に似たような処理を行ったのでそちらを参考にしてください。
Blur
public void Blur() { if (!_isInitialized) { Initialize(); } Graphics.Blit(_texture, _rt1); _material.SetFloatArray("_Weights", _weights); float x = _offset / _rt1.width; float y = _offset / _rt1.height; _material.SetVector("_Offset", new Vector4(x, 0, 0, 0)); Graphics.Blit(_rt1, _rt2, _material); _material.SetVector("_Offset", new Vector4(0, y, 0, 0)); Graphics.Blit(_rt2, _rt1, _material); _renderer.material.mainTexture = _rt1; }
ここでShaderに重みを渡すことでブラーを行っています。
offsetは、参照するテクセルのオフセットになります。
仮にoffsetが10だとすると、10,20,30...のように参照します。
ただし、_offsetに入れる値が大きすぎると、参照する範囲が広くなってしまうので本来望んでいた結果と異なる可能性があります。
offsetの値が大きすぎた例
UpdateWeights
private void UpdateWeights() { float total = 0; float d = _blur * _blur * 0.001f; for (int i = 0; i < _weights.Length; i++) { float x = 1.0f + i * 2f; float w = Mathf.Exp(-0.5f * (x * x) / d); _weights[i] = w; if (i > 0) { w *= 2.0f; } total += w; } for (int i = 0; i < _weights.Length; i++) { _weights[i] /= total; } }
ここで重みを計算しています。
基本的にはガウス関数に基づいて計算しています。
if (i > 0) { w *= 2.0f; }
ここで2倍しているのは、左右と上下の2回重みを計算しているので2で乗算してあげることで重みを半分にしています。
重みはピクセルに乗算するので、半分にしないと本来の2倍の色になってしまいます。
2倍しなかった場合
for (int i = 0; i < _weights.Length; i++) { _weights[i] /= total; }
最後に重みの合計で除算することで、各重みを正規化しています。
OnDestroy
private void OnDestroy() { RenderTexture.ReleaseTemporary(_rt1); RenderTexture.ReleaseTemporary(_rt2); }
Initializeで生成したRenderTextureを解放しています。
結果
Inspectorの値を変更することで、ブラーがかかれば成功です。
元画像
ブラーをかけた時
Inspector
今回は以上となります。
ここまでご視聴ありがとうございました。