[Unity]uGUIをBloomを使わず光らせる

画面の見栄えを良くするテクニックはいろいろありますが、パッと見の印象が大きく変えたい！となったらやっぱり**`Bloom`**が一番効果が高いイメージがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/a0fec4bf-2fc3-79c9-f9a0-5c8955f186a2.png)
↑なんかすごい（小並感）

ただ、**「UIを光らせたい」**となったときにCanvasにBloomを反映させるのはけっこう乱暴だと思ってて、現実的じゃないという認識です。
ビジュアルの調整が難しいということもあるんですが、そもそも3Dモデルを映すカメラにはBloom以外にブラーなどのポストエフェクトをかけることもあるだろうから、それを配慮すると **3DカメラとUIカメラのそれぞれでBloomをかけないといけない** ということになってしまい、負荷的に超キツい。

ということで、 **ポストエフェクト無しでBloomみたいな効果** を出せるか挑戦してみました。

**■おまけ**
**[[Unity]uGUIをBloomを使って光らせ、EmissionColorも設定できるようにする](https://qiita.com/flankids/private/eae9d28fc96ca2bd39c4)**
一応普通にBloom使って光らせる方法もまとめました。


# こんな感じになりました
https://twitter.com/flankids/status/1398988487135617024

必要な機能はできました。
自分のセンスではちょっと良いビジュアルは作れなかったですが・・・**必要な機能はできました**（強調）

# なにをしてるのか
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/b13a782f-9119-5eec-537f-df1809706f67.png)

こういう見た目を作るために・・・

![スクリーンショット 2021-05-30 22.42.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/9ce92c72-c0c1-8845-a00d-1adc6543c28b.png)

ベースのImageと、発光っぽい表示用のImageの**2つの画像を重ねています**。
光るということはつまり加算合成なので、ぼかしの入った方は**加算合成**で表示されるようにしています。

この方法で光る機能を提供するために

* UIの加算合成シェーダー
* ぼかし画像の自動生成機能

を作り、それぞれをカジュアルに使えるように整理しました。
順番に説明します。

# UIの加算合成シェーダー

UIの標準シェーダーを複製して、加算合成されるようなシェーダーにカスタムします。
まず下記ページから、自身のUnityバージョン＆OSのビルドインシェーダーをダウンロードします。

https://unity3d.com/jp/get-unity/download/archive

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/b7e85cbd-5844-42db-7685-2a0d01b83bdb.png)

ダウンロードしたファイルを解凍し、UIの標準シェーダーである`UI-Default.shader`を探します。
これをコピーして、ファイル名は`UI-Addtive.shader`としてプロジェクトに入れましょう。

![スクリーンショット 2021-05-27 22.06.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/44691cae-6a59-730c-9178-2dd03e3664bc.png)

## UI-Addtive.shaderを編集

```UI-Addtive.shader
ZTest [unity_GUIZTestMode]
Blend One One // ← 加算合成モードにする
ColorMask [_ColorMask]
```

デフォルトだと`Blend One OneMinusSrcAlpha`となっている行を、`Blend One One`に変更して、加算合成で下の画像とブレンドされるようにします。
これだけ！

* 参考：**[Unityで加算合成とかしたい](https://qiita.com/tetr4lab/items/3b9bba563e66d244916e)**（@tetr4lab
）

**新規で`UI-Addtive`と名前をつけたマテリアルを作り、このシェーダーを反映しておきましょう。**
このマテリアルをUIのマテリアルに設定すると、UIが加算合成表示になります。

**また、この後このマテリアルをスクリプトから読み込むため、`Resources`フォルダに入れておいてください。**

# ぼかし画像の自動生成機能

**[Unityでブラー画像動的生成](https://qiita.com/divideby_zero/items/4c02177a56f7d500d4c0)**（@divideby_zero）の記事を参考に（というかほぼそのままパクリ）させていただきました！
ガウシアンフィルタでぼかしを効かせた画像を生成しています。

Imageオブジェクトにアタッチして使用する、`ImageFakeBloom.cs`というコンポーネントを作ります。
機能としては

* 子オブジェクトに自動でImageオブジェクトを追加
* 子のImageオブジェクトに自動で`UI-Addtive`マテリアルを付与
* Imageオブジェクトに設定されている画像のぼかし画像を自動生成＆子のImageオブジェクトに設定
* エディタモードでも各種設定の反映を確認できる
* 任意でぼかし画像をプロジェクトに追加

というものです。

## ImageFakeBloom.cs

``` ImageFakeBloom.cs
using UnityEngine;
using UnityEngine.UI;
using System.Linq;
#if UNITY_EDITOR
using UnityEditor;
using System.IO;
#endif

[ExecuteInEditMode, RequireComponent(typeof(Image))]
public class ImageFakeBloom : MonoBehaviour
{
    [SerializeField]
    private Image _image;

    [SerializeField]
    private Image _glowImage;

    /// <summary> 発光色 </summary>
    [SerializeField]
    private Color _glowColor = Color.white;

    /// <summary> 上乗せ画像のぼかし距離 </summary>
    [SerializeField]
    private float _blurSig = 5f;

    // ぼかし画像の再生成判定用
    private float _preBlurSig;
    private Sprite _preOrifinSprite;

    /// <summary>
    /// 起動時
    /// </summary>
    private void Awake()
    {
        UpdateGlow();
    }

    /// <summary>
    /// Inspector上の値が変更されたときに呼び出し
    /// </summary>
    private void OnValidate()
    {
        UpdateGlow();
    }

    /// <summary>
    /// 初期化
    /// </summary>
    private void Initialize()
    {
        _image = GetComponent<Image>();

        // 上乗せする発光表現用のImageの生成
        _glowImage = new GameObject("Glow", typeof(Image)).GetComponent<Image>();
        // 加算合成マテリアルを設定
        _glowImage.material = Resources.Load<Material>("UI-Additive");
        _glowImage.transform.SetParent(_image.transform, false);
        _glowImage.gameObject.layer = _image.gameObject.layer;
        _glowImage.rectTransform.sizeDelta = _image.rectTransform.sizeDelta;

        _preBlurSig = _blurSig;
        _preOrifinSprite = _image.sprite;
    }

    /// <summary>
    /// 発光表現用のぼかし画像の更新
    /// </summary>
    private void UpdateGlow()
    {
        if (_image == null)
        {
            // ベースのImageを取得していなかったら初期化
            Initialize();
        }

        if (_image.sprite == null)
        {
            // ベースのImageの画像が設定されていなかったら何もしない
            _glowImage.sprite = null;
            return;
        }

        _glowImage.color = _glowColor;

        if (_glowImage.sprite != null && _preBlurSig == _blurSig && _preOrifinSprite == _image.sprite)
        {
            // ぼかし距離とベースImageに変更がなければぼかし画像の再生成をしない
            return;
        }

        Sprite preGlowSprite = _glowImage.sprite;

        Texture2D blurTex = CreateBlurTexture(_image.sprite.texture, _blurSig);
        Sprite blurSprite = Sprite.Create(blurTex, _image.sprite.rect, _image.rectTransform.pivot);
        _glowImage.sprite = blurSprite;
        _glowImage.rectTransform.sizeDelta = _image.rectTransform.sizeDelta;

#if UNITY_EDITOR
        // エディターでのみ分かりやすいようにスプライト名をつける
        blurSprite.name = _image.sprite.name + " blur";
#endif

        // 使わなくなったスプライトを破棄
        DestroySprite(preGlowSprite);

        _preBlurSig = _blurSig;
        _preOrifinSprite = _image.sprite;
    }

    /// <summary>
    /// 更新
    /// </summary>
    private void Update()
    {
        _glowImage.rectTransform.sizeDelta = _image.rectTransform.sizeDelta;
        _glowImage.color = _glowColor;
    }

    /// <summary>
    /// 削除時に呼び出し
    /// </summary>
    private void OnDestroy()
    {
        DestroySprite(_glowImage.sprite);
    }

    /// <summary>
    /// スプライトの破棄
    /// </summary>
    private void DestroySprite(Sprite sprite)
    {
        if (sprite == null)
        {
            return;
        }

        if (Application.isPlaying)
        {
            Destroy(sprite.texture);
            Destroy(sprite);
        }
        else
        {
            // エディターモードでは即時破棄
            DestroyImmediate(sprite.texture);
            DestroyImmediate(sprite);
        }
    }

    /// <summary>
    /// ぼかし画像を生成
    /// https://qiita.com/divideby_zero/items/4c02177a56f7d500d4c0
    /// </summary>
    /// <param name="sig">ぼかし距離</param>
    private static Texture2D CreateBlurTexture(Texture2D tex, float sig)
    {
        sig = Mathf.Max(sig, 0f);
        int W = tex.width;
        int H = tex.height;
        int Wm = (int)(Mathf.Ceil(3.0f * sig) * 2 + 1);
        int Rm = (Wm - 1) / 2;

        //フィルタ
        float[] msk = new float[Wm];

        sig = 2 * sig * sig;
        float div = Mathf.Sqrt(sig * Mathf.PI);

        //フィルタの作成
        for (int x = 0; x < Wm; x++)
        {
            int p = (x - Rm) * (x - Rm);
            msk[x] = Mathf.Exp(-p / sig) / div;
        }

        var src = tex.GetPixels(0).Select(x => x.a).ToArray();
        var tmp = new float[src.Length];
        var dst = new Color[src.Length];

        //垂直方向
        for (int x = 0; x < W; x++)
        {
            for (int y = 0; y < H; y++)
            {
                float sum = 0;
                for (int i = 0; i < Wm; i++)
                {
                    int p = y + i - Rm;
                    if (p < 0 || p >= H) continue;
                    sum += msk[i] * src[x + p * W];
                }
                tmp[x + y * W] = sum;
            }
        }
        //水平方向
        for (int x = 0; x < W; x++)
        {
            for (int y = 0; y < H; y++)
            {
                float sum = 0;
                for (int i = 0; i < Wm; i++)
                {
                    int p = x + i - Rm;
                    if (p < 0 || p >= W) continue;
                    sum += msk[i] * tmp[p + y * W];
                }
                dst[x + y * W] = new Color(1, 1, 1, sum);
            }
        }

        var createTexture = new Texture2D(W, H);
        createTexture.SetPixels(dst);
        createTexture.Apply();

        return createTexture;
    }

#if UNITY_EDITOR
    [CustomEditor(typeof(ImageFakeBloom))]
    public class ImageFakeBloomEditor : Editor
    {
        public override void OnInspectorGUI()
        {
            base.OnInspectorGUI();

            ImageFakeBloom target = this.target as ImageFakeBloom;
            if(target._glowImage == null || target._glowImage.sprite == null)
            {
                return;
            }

            // 発光表現用に生成された画像をプロジェクトに保存するボタン
            if (GUILayout.Button("発光画像を保存"))
            {
                SaveBlurSprite(target._glowImage.sprite);
            }
        }

        /// <summary>
        /// 渡されたSpriteをプロジェクトに保存
        /// </summary>
        private void SaveBlurSprite(Sprite sprite)
        {
            string path = EditorUtility.SaveFilePanelInProject("発光画像を保存", sprite.name, "png", "保存する画像名を入力してください");
            if (path.Length == 0)
            {
                // 保存キャンセル
                return;
            }

            byte[] pngData = sprite.texture.EncodeToPNG();
            if (pngData == null)
            {
                Debug.LogError("画像データの取得に失敗しました");
                return;
            }

            File.WriteAllBytes(path, pngData);
            AssetDatabase.Refresh();

            // テクスチャタイプをSpriteに変更
            TextureImporter textureImporter = AssetImporter.GetAtPath(path) as TextureImporter;
            if (textureImporter != null)
            {
                textureImporter.textureType = TextureImporterType.Sprite;
                textureImporter.SaveAndReimport();
            }
        }
    }
#endif
}
```

このスクリプトをImageオブジェクトにアタッチして使ってください。

# 使い方

`Glow Color`を変更すると発光色が変化します。
`Blur Sig`の値を大きくすると、より広い範囲で発光するような見栄えになります。
`発光画像を保存`ボタンで、自動生成されたぼかし画像をプロジェクトに保存することもできます。
※保存していなければ、ぼかし画像は実行時に生成され、オブジェクト破棄時に削除されます

https://twitter.com/flankids/status/1399020259776991236

光ってるように見えますかね？どうでしょう

# 課題

**① そのまま使うとドローコールが増えやすい**
この仕組みで発光したUIをたくさん配置すると、標準のUIマテリアルと`UI-Addtive`マテリアルが交互に並ぶことになり、**ドローコールが無闇に増えてしまいます**。
また、ぼかし画像を自動生成に頼るとアトラスにも所属されないので、バッチされません。

* 自動生成されたぼかし画像が所属するアトラスを自動的に生成＆追加
* ぼかしUIは直接ベースのUIの子に入れず、ぼかしUI用のレイヤー（親オブジェクト）にまとめる
    * かつベースのUIのサイズや位置は追従する

ということができれば大量に表示してもドローコールを抑えられるんですが・・・むずかしい

**② Imageのトリミングやマスクに対応していない**
例えばHPゲージにこのなんちゃってBloomを使うと、発光部分がスパッと切られた表示になって偽の発光であることがバレバレでダサいです。
これは・・・解決できるんだろうか・・・

課題①は特に大きな問題なので、実用化にはまだちょっと時間がかかりそう。
アイデアやもっと良い方法を是非教えてください！

今回はここまで。