知識0からのUnityShader勉強

知識0からのUnityShader勉強

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

【UnityShader】CommandBufferでブラーをかける 【2】#67

前回の成果

ブラーをかけるシェーダー側の処理を理解した。

soramamenatan.hatenablog.com


今回やること

前回の続きから行っていきます。

edom18.hateblo.jp


ソースコード

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class BlurEffect : MonoBehaviour {
    [SerializeField]
    private Shader _shader;

    [SerializeField, Range(1f, 100f)]
    private float _offset = 1f;

    [SerializeField, Range(10f, 1000f)]
    private float _blur = 100f;

    [SerializeField, Range(0f, 1f)]
    private float _intencity = 0;

    [SerializeField]
    private CameraEvent _cameraEvent = CameraEvent.AfterImageEffects;

    private Material _material;

    private Dictionary<Camera, CommandBuffer> _cameras = new Dictionary<Camera, CommandBuffer>();

    private float[] _weights = new float[10];

    private int _copyTexID = 0;
    private int _blurredID1 = 0;
    private int _blurredID2 = 0;
    private int _weightsID = 0;
    private int _intencityID = 0;
    private int _offsetsID = 0;
    private int _grabBlurTextureID = 0;

    private void Awake() {
        MeshFilter filter = gameObject.AddComponent<MeshFilter>();
        filter.hideFlags = HideFlags.DontSave;
        MeshRenderer renderer = gameObject.AddComponent<MeshRenderer>();
        renderer.hideFlags = HideFlags.DontSave;

        _copyTexID = Shader.PropertyToID("_ScreenCopyTexture");
        _blurredID1 = Shader.PropertyToID("_Temp1");
        _blurredID2 = Shader.PropertyToID("_Temp2");
        _weightsID = Shader.PropertyToID("_Weights");
        _intencityID = Shader.PropertyToID("_Intencity");
        _offsetsID = Shader.PropertyToID("_Offset");
        _grabBlurTextureID = Shader.PropertyToID("_GrabBlurTexture");

        Transform parent = Camera.main.transform;
        transform.SetParent(parent);
        transform.localPosition = Vector3.forward;

        UpdateWeights();
    }

    private void Update() {
        foreach (var kv in _cameras) {
            kv.Value.Clear();
            BuildCommandBuffer(kv.Value);
        }
    }

    private void OnEnable() {
        Cleanup();
    }

    private void OnDisable() {
        Cleanup();
    }

    public void OnWillRenderObject() {
        if (!gameObject.activeInHierarchy || !enabled) {
            Cleanup();
            return;
        }

        if (_material == null) {
            _material = new Material(_shader);
            _material.hideFlags = HideFlags.HideAndDontSave;
        }

        Camera cam = Camera.current;
        if (cam == null) {
            return;
        }

#if UNITY_EDITOR
        if (cam == UnityEditor.SceneView.lastActiveSceneView.camera) {
            return;
        }
#endif

        if (_cameras.ContainsKey(cam)) {
            return;
        }

        CommandBuffer buf = new CommandBuffer();
        _cameras[cam] = buf;

        BuildCommandBuffer(buf);
        cam.AddCommandBuffer(_cameraEvent, buf);
    }

    private void BuildCommandBuffer(CommandBuffer buf) {
        buf.GetTemporaryRT(_copyTexID, -1, -1, 0, FilterMode.Bilinear);
        buf.Blit(BuiltinRenderTextureType.CurrentActive, _copyTexID);

        buf.GetTemporaryRT(_blurredID1, -2, -2, 0, FilterMode.Bilinear);
        buf.GetTemporaryRT(_blurredID2, -2, -2, 0, FilterMode.Bilinear);

        buf.Blit(_copyTexID, _blurredID1);

        buf.ReleaseTemporaryRT(_copyTexID);

        float x = _offset / Screen.width;
        float y = _offset / Screen.height;

        buf.SetGlobalFloatArray(_weightsID, _weights);
        buf.SetGlobalFloat(_intencityID, _intencity);

        buf.SetGlobalVector(_offsetsID, new Vector4(x, 0, 0, 0));
        buf.Blit(_blurredID1, _blurredID2, _material);

        buf.SetGlobalVector(_offsetsID, new Vector4(0, y, 0, 0));
        buf.Blit(_blurredID2, _blurredID1, _material);

        buf.SetGlobalTexture(_grabBlurTextureID, _blurredID1);
    }

    private void Cleanup() {
        foreach (var cam in _cameras) {
            if (cam.Key != null) {
                cam.Key.RemoveCommandBuffer(_cameraEvent, cam.Value);
            }
        }

        _cameras.Clear();
        Object.DestroyImmediate(_material);
    }

    private void UpdateWeights() {
        float total = 0;
        float d = _blur * _blur * 0.001f;

        for (int i = 0; i < _weights.Length; i++) {
            float r = 1.0f + 2.0f * i;
            float w = Mathf.Exp(-0.5f * (r * r) / d);
            _weights[i] = w;
            if (i > 0) {
                w *= 2.0f;
            }
            total += w;
        }

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

重みの計算等はガウシアンブラーの時と同じ処理となっています。


予約語

HideFlags.DontSave

オブジェクトが持つHigeFlagsに設定するものでビット演算で管理されています。
例を挙げると、オブジェクトをHierarchyに表示しなくすることもできます。

HideFlags.DontSaveは端的に言うとビルド時のオブジェクトの情報を保持しないものになります。

HideFlags.DontSaveは、DontSaveInBuildとDontSaveInEditorとDontUnloadUnusedAssetのフラグをあわせたものになります。

各フラグの説明は以下となります。

DontSaveInBuild

ビルドした際に、オブジェクトをシーンに保存しない

DontSaveInEditor

エディタ上でオブジェクトを保存しない

DontUnloadUnusedAsset

リソース管理からの除外

他にもフラグがあるので、そちらに関しましては以下サイト様に詳しく説明されているので参照してみてください。

qiita.com


OnWillRenderObject

対象となるRendererがカメラに映っているときに呼ばれる関数となります。
呼び出されるタイミングはUpdateの後 となります。


UnityEditor.SceneView.lastActiveSceneView.camera

シーンビューのカメラを取得するものになります。


DestroyImmediate

オブジェクトを破棄する関数になります。
同じ様な処理をする関数として、Destroyがあります。
Destroyは数フレーム待ってから破棄するのに対し、DestroyImmediateは即座に破棄します。
ただし、DestroyImmediateはUnity公式非推奨となっているので使い分けには気をつけてください。


ソースコードの解説

予約語の解説が終わったので、実際のソースコードの解説に移ります。


Awake関数

ここでシェーダーのPropertyIDの設定や重みなどの初期化を行っています。

MeshFilter filter = gameObject.AddComponent<MeshFilter>();
filter.hideFlags = HideFlags.DontSave;
MeshRenderer renderer = gameObject.AddComponent<MeshRenderer>();
renderer.hideFlags = HideFlags.DontSave;

上記で自身にMeshを設定しています。
理由としては、OnWillRenderObjectはRendererが付いていないと呼び出されないからになります。


OnWillRenderObject関数

ここでは、カメラに対してCommand Bufferを登録して変数に保持しています。
OnWillRenderObject関数で呼び出す理由としては、この関数はカメラ毎に呼び出されます。
ですので、全てのカメラに対してCamera.currentで登録及び、処理が出来るからになります。


BuildCommandBuffer関数

ここでRenderTextureの生成と、シェーダーに値を渡すことをしています。

流れは参考サイト様のものが分かりやすかったので引用させていただきます。

  1. 現在レンダリング済みの内容をテンポラリなRenderTextureに等倍でコピーする
  2. (1)でコピーしたものをさらに半分のサイズにコピーする
  3. (2)の半分サイズのテクスチャに対し、横方向のブラーをかける
  4. (3)の横方向ブラーの画像に対し、さらに縦方向のブラーをかける
  5. (4)の最終結果を、グローバルなテクスチャとして登録する

uGUIの背景をぼかしてオシャレに見せる - e.blog:より引用


Cleanup関数

カメラに設定したCommand Bufferとマテリアルを削除しています。

この関数が呼ばれるときは

となります。


UpdateWeights関数

シェーダーに渡す重みを計算している関数となります。
こちらは以前解説したので、そちらを参照してください。

soramamenatan.hatenablog.com


結果

UIと重なっているオブジェクトの部分だけブラーがかかっていれば成功です。

描画結果

f:id:soramamenatan:20200817165153p:plain

inspector

f:id:soramamenatan:20200817165157p:plain

また、オブジェクトを移動させても問題なくブラーがかかります。

f:id:soramamenatan:20200817165345g:plain


ソースコードにコメントを付与

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Rendering;

public class BlurEffect : MonoBehaviour {
    [SerializeField]
    private Shader _shader;

    [SerializeField, Range(1f, 100f)]
    private float _offset = 1f;

    [SerializeField, Range(10f, 1000f)]
    private float _blur = 100f;

    [SerializeField, Range(0f, 1f)]
    private float _intencity = 0;

    [SerializeField]
    private CameraEvent _cameraEvent = CameraEvent.AfterImageEffects;

    private Material _material;

    private Dictionary<Camera, CommandBuffer> _cameras = new Dictionary<Camera, CommandBuffer>();

    private float[] _weights = new float[10];

    private int _copyTexID = 0;
    private int _blurredID1 = 0;
    private int _blurredID2 = 0;
    private int _weightsID = 0;
    private int _intencityID = 0;
    private int _offsetsID = 0;
    private int _grabBlurTextureID = 0;

    /// <summary>
    /// 初期化
    /// </summary>
    private void Awake() {
        // OnWillRenderObjectで呼ばれるためにRendererをつける
        MeshFilter filter = gameObject.AddComponent<MeshFilter>();
        filter.hideFlags = HideFlags.DontSave;
        MeshRenderer renderer = gameObject.AddComponent<MeshRenderer>();
        renderer.hideFlags = HideFlags.DontSave;

        // propertyID制作
        _copyTexID = Shader.PropertyToID("_ScreenCopyTexture");
        _blurredID1 = Shader.PropertyToID("_Temp1");
        _blurredID2 = Shader.PropertyToID("_Temp2");
        _weightsID = Shader.PropertyToID("_Weights");
        _intencityID = Shader.PropertyToID("_Intencity");
        _offsetsID = Shader.PropertyToID("_Offset");
        _grabBlurTextureID = Shader.PropertyToID("_GrabBlurTexture");

        // mainカメラの子にすることで、mainカメラが消えたときに対応できるように
        Transform parent = Camera.main.transform;
        transform.SetParent(parent);
        transform.localPosition = Vector3.forward;

        // 重みの計算
        UpdateWeights();
    }

    /// <summary>
    /// 更新
    /// </summary>
    private void Update() {
        foreach (var kv in _cameras) {
            // CommandBufferの更新
            kv.Value.Clear();
            BuildCommandBuffer(kv.Value);
        }
    }

    /// <summary>
    /// アクティブ時
    /// </summary>
    private void OnEnable() {
        Cleanup();
    }

    /// <summary>
    /// 非アクティブ時
    /// </summary>
    private void OnDisable() {
        Cleanup();
    }

    /// <summary>
    /// Rendererがカメラに映っているときにカメラ毎に呼ばれる
    /// </summary>
    public void OnWillRenderObject() {
        // 自身が非アクティブならreturn
        if (!gameObject.activeInHierarchy || !enabled) {
            Cleanup();
            return;
        }

        // マテリアルの初期化
        if (_material == null) {
            _material = new Material(_shader);
            _material.hideFlags = HideFlags.HideAndDontSave;
        }

        // 現在のカメラを取得
        Camera cam = Camera.current;
        if (cam == null) {
            return;
        }

#if UNITY_EDITOR
        // カメラがシーンビューのものだった場合return
        if (cam == UnityEditor.SceneView.lastActiveSceneView.camera) {
            return;
        }
#endif

        // すでに処理済ならreturn
        if (_cameras.ContainsKey(cam)) {
            return;
        }

        // CommandBufferの生成
        CommandBuffer buf = new CommandBuffer();
        _cameras[cam] = buf;
        BuildCommandBuffer(buf);
        cam.AddCommandBuffer(_cameraEvent, buf);
    }

    /// <summary>
    /// CommandBufferの設定
    /// </summary>
    /// <param name="buf">CommandBuffer</param>
    private void BuildCommandBuffer(CommandBuffer buf) {
        // 現在のレンダリング結果をカメラと同じ解像度でRenderTextureにコピー
        buf.GetTemporaryRT(_copyTexID, -1, -1, 0, FilterMode.Bilinear);
        buf.Blit(BuiltinRenderTextureType.CurrentActive, _copyTexID);

        // カメラの半分の解像度で縦横の2枚のRenderTexture生成
        buf.GetTemporaryRT(_blurredID1, -2, -2, 0, FilterMode.Bilinear);
        buf.GetTemporaryRT(_blurredID2, -2, -2, 0, FilterMode.Bilinear);

        // 半分の解像度にする
        buf.Blit(_copyTexID, _blurredID1);

        // カメラと同じ解像度のものは不要なので破棄
        buf.ReleaseTemporaryRT(_copyTexID);

        // 縦横のオフセット
        float x = _offset / Screen.width;
        float y = _offset / Screen.height;
        buf.SetGlobalFloatArray(_weightsID, _weights);
        buf.SetGlobalFloat(_intencityID, _intencity);

        // 横方向ブラー
        buf.SetGlobalVector(_offsetsID, new Vector4(x, 0, 0, 0));
        buf.Blit(_blurredID1, _blurredID2, _material);

        // 縦方向ブラー
        buf.SetGlobalVector(_offsetsID, new Vector4(0, y, 0, 0));
        buf.Blit(_blurredID2, _blurredID1, _material);

        // 縦横ブラーをかけたテクスチャをシェーダーに適応
        buf.SetGlobalTexture(_grabBlurTextureID, _blurredID1);
    }

    /// <summary>
    /// 設定の削除
    /// </summary>
    private void Cleanup() {
        foreach (var cam in _cameras) {
            if (cam.Key != null) {
                cam.Key.RemoveCommandBuffer(_cameraEvent, cam.Value);
            }
        }
        _cameras.Clear();
        Object.DestroyImmediate(_material);
    }

    /// <summary>
    /// 重みの計算
    /// </summary>
    private void UpdateWeights() {
        float total = 0;
        float d = _blur * _blur * 0.001f;

        for (int i = 0; i < _weights.Length; i++) {
            float r = 1.0f + 2.0f * i;
            float w = Mathf.Exp(-0.5f * (r * r) / d);
            _weights[i] = w;
            // xとyがあるので2倍
            if (i > 0) {
                w *= 2.0f;
            }
            total += w;
        }

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

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