知識0からのUnityShader勉強

知識0からのUnityShader勉強

UnityのShaderをメインとして、0から学んでいくブログです。

【UnityShader】ガウシアンブラー【2】 #65

前回の成果

soramamenatan.hatenablog.com


今回やること

前回に引き続き、ガウシアンブラーについて説明します。
今回は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
記述方法 ループ内のコードがループ回数分記述 アセンブラコードでループがそのまま記述
メモリサイズ 大きくなる 抑えられる
実行速度 高速になる 低速になる

より詳しいことに関しましては以下サイト様をご参照ください。

blog.livedoor.jp

wlog.flatlib.jp


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の際に似たような処理を行ったのでそちらを参考にしてください。

soramamenatan.hatenablog.com


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の値が大きすぎた例

f:id:soramamenatan:20200809132121p:plain


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倍しなかった場合

f:id:soramamenatan:20200809132957p:plain


for (int i = 0; i < _weights.Length; i++) {
    _weights[i] /= total;
}

最後に重みの合計で除算することで、各重みを正規化しています。


OnDestroy

private void OnDestroy() {
    RenderTexture.ReleaseTemporary(_rt1);
    RenderTexture.ReleaseTemporary(_rt2);
}

Initializeで生成したRenderTextureを解放しています。


結果

Inspectorの値を変更することで、ブラーがかかれば成功です。

元画像

f:id:soramamenatan:20200809133324p:plain

ブラーをかけた時

f:id:soramamenatan:20200809133320p:plain

Inspector

f:id:soramamenatan:20200809133329p:plain


今回は以上となります。
ここまでご視聴ありがとうございました。