[Unity]uGUIをBloomを使って光らせ、EmissionColorも設定できるようにする

※本記事は**「[[Unity]uGUIをBloomを使わず光らせる](https://qiita.com/flankids/items/416ed19dae906680101a)」**のおまけ記事です

3Dモデルを光らせたい！
と思った時、Bloomをかけてエミッションマップ設定したり、イイカンジのシェーダーで便利にしたりという情報は調べるとすぐ出てきてとてもありがたいなと思います。

ただ、個人的に**UIをBloomで光らせる方法**についての情報は少ないな〜と思ったので簡単に整理しました。

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

# 筆者の環境
* Unity 2020.2.4f1
* Post Processing 3.0.3
* MacOS 10.15.7

# Bloomを設定し、Canvasにも影響が反映されるようにする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/f715d6ab-3b71-b06e-15e0-527fedb69689.png)

Package Manager で `Post Processing` をインストール。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/3e6b9873-4bba-de55-b296-71d821109f17.png)

UIを表示するカメラに `Post-process Layer` コンポーネントを付与。
`Trigger` にカメラの参照を与え、`Layer`にポストエフェクトの影響を与えるレイヤーを設定（今回はUIにでもしておきましょう）。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/749336c1-a343-d47a-48df-5422daadb6dd.png)
UIを表示するCanvasがカメラのポストエフェクト影響下に置かれるように、

* `Render Mode`を「Screen Space - Camera」に設定
* `Render Camera`に先程のカメラの参照を設定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/d528c8e6-cb61-af9b-4592-cc8d0289526a.png)

シーンに新しくGameObjectを作成し、`Post-process Volume`コンポーネントを付与。

* さきほど`Post-process Layer`でポストエフェクトの影響下に置いたレイヤー（UIレイヤー）をオブジェクトに設定
* `Profile`の「New」ボタンを押して設定ファイルを追加
* 「Add effect...」ボタンを押して「Unity > Bloom」を選択

これでBloomが画面に設定され、Canvasもポストエフェクトの影響下に置かれました。
試しにBloomの`Intensity`（輝度）を`15`にしてみます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/5c03c476-51aa-0444-e576-9db3ba02d7c5.png)

光りました。

# 個別にUIの発光具合、発光色を設定できるようにする

とはいえ画面全体でしか光り具合を調整できないのは困るので、UIに`EmissionColor`（発光色）を定義して、個別に光り具合を設定できるようにしてみます。

まず下記ページから、自身の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-Glow.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-Glow.shaderを編集
※記事の最後に全文を記載します

```UI-Glow.shader
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)
 
Shader "UI/Glow" // ←名前変更
{
```
シェーダー名を変更

```UI-Glow.shader
_StencilWriteMask ("Stencil Write Mask", Float) = 255
_StencilReadMask ("Stencil Read Mask", Float) = 255

_ColorMask ("Color Mask", Float) = 15
[HDR] _EmissionColor ("Emission", Color) = (0, 0, 0, 0) // ← パラメータ追加

~~~中略~~~

float _UIMaskSoftnessX;
float _UIMaskSoftnessY;
float3 _EmissionColor; // ← メンバ定義
```
発光色である`_EmissionColor`を定義

```UI-Glow.shader
OUT.color = v.color * _Color;
OUT.color.rgb += _EmissionColor; // ← _EmissionColorを加算合成
return OUT;
```

フラグメントシェーダーの記述で、_EmissionColorを加算合成
これでシェーダー完成です。

## シェーダーを反映

プロジェクト内に新たにマテリアルを作成。（ひとまず名前は「UI-Glow」としておきます）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/3b7ff2f4-b47c-23e4-125f-b435ccfbdbcc.png)

UI-Glowマテリアルを選択し、Inspector上でShaderを「UI > Glow」とすると、シェーダーが設定されます。
（マテリアルファイルにシェーダーファイルをドラッグ＆ドロップでも設定可能です）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/94c98521-7ca2-1ee6-aa71-42705c0dd139.png)

このマテリアルをImageオブジェクトに設定します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/3be24290-c0f7-619a-3f49-0ece42559920.png)

この状態で、UI-Glowの「Emission Color」を変化させると光具合が変化します！
※さっきデフォルトのシェーダーのUIを光らせるために極端に高くしてた、BloomのIntensityを下げておきましょう（`1`で十分なはず）

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

色も輝きも自由自在。

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

こんな感じで、模様として機能させることもできます！
これで完成です。

# 扱いやすくするコンポーネント作成（おまけ）

発光色はUIオブジェクトごとに設定したいので、マテリアルもその分必要になります。
その度マテリアル作成してたら大変なので、自動的に作成＆割当してくれるコンポーネントを作りました。
また、いちいちマテリアルファイルを選択してEmissionColor設定するのも面倒なので、同一オブジェクトのInspector上でImageのカラーも発光色も操作できるようにしています。

```ImageGlow.cs
using UnityEngine;
using UnityEngine.UI;

[RequireComponent(typeof(Image))]
public class ImageGlow : MonoBehaviour
{
    /// <summary> 発光色のシェーダーパラメータ </summary>
    private static readonly int PROPERTY_EMISSION_COLOR = Shader.PropertyToID("_EmissionColor");

    /// <summary> 発光色（HDR） </summary>
    [ColorUsage(false, true)]
    public Color EmissionColor;

    /// <summary> マテリアル </summary>
    private Material _mat;

    /// <summary> マテリアルアクセサ </summary>
    private Material Mat
    {
        get
        {
            if (_mat != null)
            {
                return _mat;
            }
            // マテリアルがない時に呼び出されたら初期化
            Initialize();
            return _mat;
        }
    }

    /// <summary>
    /// 初期化
    /// </summary>
    private void Initialize()
    {
        Image image = GetComponent<Image>();
        // UI/Glowシェーダーでマテリアル生成
        Shader uiGlowShader = Shader.Find("UI/Glow");
        if (image.material == null || image.material.shader != uiGlowShader)
        {
            _mat = new Material(uiGlowShader);
        }
        else
        {
            _mat = new Material(image.material);
        }
        _mat.name = "UI-Glow (Instance)";
        image.material = _mat;
    }

    /// <summary>
    /// 更新
    /// </summary>
    private void Update()
    {
        Mat.SetColor(PROPERTY_EMISSION_COLOR, EmissionColor);
    }

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

    /// <summary>
    /// 削除時に呼び出し
    /// </summary>
    private void OnDestroy()
    {
        // 生成したマテリアルがリークしないように明示的に削除
        if(_mat != null)
        {
            if (Application.isPlaying)
            {
                Destroy(_mat);
            }
            else
            {
                DestroyImmediate(_mat);
            }
        }
    }
}
```

`Shader.Find`で今回作ったシェーダーを取得する処理が含まれています。
このメソッドでカスタムシェーダーを取得するために、`Project Settings`の`Graphics`で、`Always Included Shaders`に、今回作った「UI/Glow」を追加してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/184294/b9c3054e-a3c7-7d24-d780-544a4c87915e.png)
その上で、この`ImageGlow`コンポーエントをImageオブジェクトにアタッチすると、下記動画のように扱いが楽になります。お好みでお試しください

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

# 課題

**① マテリアルがとにかく増える**
発光するUIごとにマテリアルが必要になる関係上、光るものを増やすとどんどんマテリアル数が増えていきます。
これは単純にメモリを圧迫するということもあるんですが、バッチがいちいち切られて**ドローコール数が爆増**する原因になりかねません。
ImageのColorみたいに1つのマテリアルで各オブジェクトのEmissinColorを変えられればマシになるんですが、どうすればいいんだっけ・・・（ImageのColorで出来てるならできるはずなんだけども）
誰か教えてください！


**② Bloomはやっぱり重いよね**
それ今言う？って感じなのですが。
実はもともとこの話がしたかったので課題というより本題でもあるんですが、UIにBloomを反映させるのってけっこう乱暴だと思ってて、現実的じゃないという認識です。
3Dモデルを映すカメラにはBloom以外にブラーなどのポストエフェクトをかけることもあるだろうし、そう考えると今回の例みたいに１つのカメラで済まなくなって、**3DカメラとUIカメラのそれぞれでBloomをかけないといけない**ということになってしまいます。それは負荷的にキツい。

なので、ポストエフェクト無しでBloomみたいな効果をつけられないか？という話を、この記事の本体である下記の記事で書いています。
よろしければご覧ください！

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

今回はここまで！

# UI-Glow.shader全文

```UI-Glow.shader
// Unity built-in shader source. Copyright (c) 2016 Unity Technologies. MIT license (see license.txt)

Shader "UI/Glow"
{
    Properties
    {
        [PerRendererData] _MainTex ("Sprite Texture", 2D) = "white" {}
        _Color ("Tint", Color) = (1,1,1,1)

        _StencilComp ("Stencil Comparison", Float) = 8
        _Stencil ("Stencil ID", Float) = 0
        _StencilOp ("Stencil Operation", Float) = 0
        _StencilWriteMask ("Stencil Write Mask", Float) = 255
        _StencilReadMask ("Stencil Read Mask", Float) = 255

        _ColorMask ("Color Mask", Float) = 15

        [HDR] _EmissionColor ("Emission", Color) = (0, 0, 0, 0)

        [Toggle(UNITY_UI_ALPHACLIP)] _UseUIAlphaClip ("Use Alpha Clip", Float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue"="Transparent"
            "IgnoreProjector"="True"
            "RenderType"="Transparent"
            "PreviewType"="Plane"
            "CanUseSpriteAtlas"="True"
        }

        Stencil
        {
            Ref [_Stencil]
            Comp [_StencilComp]
            Pass [_StencilOp]
            ReadMask [_StencilReadMask]
            WriteMask [_StencilWriteMask]
        }

        Cull Off
        Lighting Off
        ZWrite Off
        ZTest [unity_GUIZTestMode]
        Blend One OneMinusSrcAlpha
        ColorMask [_ColorMask]

        Pass
        {
            Name "Default"
        CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0

            #include "UnityCG.cginc"
            #include "UnityUI.cginc"

            #pragma multi_compile_local _ UNITY_UI_CLIP_RECT
            #pragma multi_compile_local _ UNITY_UI_ALPHACLIP

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color    : COLOR;
                float2 texcoord  : TEXCOORD0;
                float4 worldPosition : TEXCOORD1;
                half4  mask : TEXCOORD2;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            sampler2D _MainTex;
            fixed4 _Color;
            fixed4 _TextureSampleAdd;
            float4 _ClipRect;
            float4 _MainTex_ST;
            float _UIMaskSoftnessX;
            float _UIMaskSoftnessY;

            float3 _EmissionColor;

            v2f vert(appdata_t v)
            {
                v2f OUT;
                UNITY_SETUP_INSTANCE_ID(v);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                float4 vPosition = UnityObjectToClipPos(v.vertex);
                OUT.worldPosition = v.vertex;
                OUT.vertex = vPosition;

                float2 pixelSize = vPosition.w;
                pixelSize /= float2(1, 1) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));

                float4 clampedRect = clamp(_ClipRect, -2e10, 2e10);
                float2 maskUV = (v.vertex.xy - clampedRect.xy) / (clampedRect.zw - clampedRect.xy);
                OUT.texcoord = TRANSFORM_TEX(v.texcoord.xy, _MainTex);
                OUT.mask = half4(v.vertex.xy * 2 - clampedRect.xy - clampedRect.zw, 0.25 / (0.25 * half2(_UIMaskSoftnessX, _UIMaskSoftnessY) + abs(pixelSize.xy)));

                OUT.color = v.color * _Color;
                OUT.color.rgb += _EmissionColor;
                return OUT;
            }

            fixed4 frag(v2f IN) : SV_Target
            {
                half4 color = IN.color * (tex2D(_MainTex, IN.texcoord) + _TextureSampleAdd);

                #ifdef UNITY_UI_CLIP_RECT
                half2 m = saturate((_ClipRect.zw - _ClipRect.xy - abs(IN.mask.xy)) * IN.mask.zw);
                color.a *= m.x * m.y;
                #endif

                #ifdef UNITY_UI_ALPHACLIP
                clip (color.a - 0.001);
                #endif

                color.rgb *= color.a;

                return color;
            }
        ENDCG
        }
    }
}
```