-->

【Unity Shader】UI発光シェーダー

【Unity Shader】UI発光シェーダー
 ※コードが古くなっていたのでURP用にコードを改修しました。(2026/04/14) 


UIに対する発光シェーダーを探してたものの、精度が低かったり結局PostProcessに頼ってるのしかなくて満足いきませんでした。
それで、 BlurシェーダーとBrightnessシェーダーを組み合わせれば自分でも作れるのではと試行錯誤してたら結構良いのが作れました。

URP + uGUI環境では、シェーダー単体でマルチパスを実装するのが不可能になりました。
このためC#側のRenderTextureの実装も追加され、依然と比べるとやや内容が複雑になっています。

発光シェーダー概要

下が今回の発光シェーダーの動画です。
 Luminance 
の値を上げると段々画像の輝度が上がっていき、最終的に真っ白になります。


普通の方法ではuGUIに対してマルチパスは実装できないので、Pass1, 2でBlur > その後Pass0でBrightnessという特殊な遷移になっています。
シェーダー側とC#側の長々としたコードをコピペする必要がありますが、その後の実装自体は割とシンプルです。

シェーダー側の実装

下がシェーダー側のコード全文になります。
適当な.shaderファイルを作成し、中に全てコピペしてください。
コードの意味は理解しなくても大丈夫ですが、 UIシェーダー基礎 マルチパス記事を見ると理解しやすいです。
Copy Code

Shader "UI/Glow3Pass"
{
    Properties 
    {
        [HideInInspector] _MainTex("-", 2D) = "white"{} //uGUI及びGraphics.Blitが標準で扱うテクスチャ
        [HideInInspector] _CustomTex("-", 2D) = "white"{}
        [HideInInspector] _Luminance("-", float) = 0
        [HideInInspector] _BlurCount("-", float) = 0
        [HideInInspector] _BlurDistance("-", float) = 0          
        [HideInInspector] _BlurPower("-", float) = 0
    }

    SubShader
    {
        Tags
        {
            "RenderPipeline" = "UniversalPipeline"
            "Queue" = "Transparent" 
        }
        Cull Off
        ZWrite Off

        HLSLINCLUDE      
            #pragma vertex vert
            #pragma fragment frag
            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"

            //uGUI Image > Source Imageにアタッチされた画像を参照できるようにする
            TEXTURE2D(_MainTex);        
            SAMPLER(sampler_MainTex);            

            //最終的に画面に出力されるテクスチャを宣言、中身のデータはC#側から渡される
            TEXTURE2D(_CustomTex);        
            SAMPLER(sampler_CustomTex);
            //C#から渡された_CustomTexのサイズを参照できるようにする
            float4 _CustomTex_TexelSize;    

            //SRP Batcherを適用、動的に変更される数値はこの中に定義
            CBUFFER_START(UnityPerMaterial)
                half _Luminance;           
                int _BlurCount;
                half _BlurDistance;
                half _BlurPower;                
            CBUFFER_END

            static const half MaxLuminance = 10;
            static const half HalfLuminance = MaxLuminance / 2;
            static const half MaxBlurPower = 10;
            
            struct Attributes
            {
                half2 uv : TEXCOORD0;
                float4 positionOS : POSITION;
                half4 color : COLOR;
            };

            struct Varyings
            {
                half2 uv : TEXCOORD0;
                float4 positionCS : SV_POSITION;
                half4 color : COLOR;
            };

            Varyings vert (Attributes input)
            {
                Varyings output;
                output.positionCS = TransformObjectToHClip(input.positionOS.xyz);
                output.uv = input.uv;
                output.color = input.color;
                return output;
            }

            //第二引数で指定した方向に画像をぼかす
            half4 executeBlur(half2 uv, half2 direction)
            {
                //_BlurDistanceに関しては_Luminanceの値も影響させる
                //_Luminanceの値が大きいほど、一定程度までBlurの距離もどんどん伸びる
                half clampLuminance = clamp(_Luminance, 0, HalfLuminance) / HalfLuminance;
                half fixBlurDistance = _BlurDistance * clampLuminance;

                half4 color = 0;
                for (int shift = -_BlurCount; shift <= _BlurCount; shift++)  
                {  
                    half2 offset = direction * shift * fixBlurDistance * _CustomTex_TexelSize;
                    color += SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, uv + offset); 
                }  
                //加算した回数分だけ割って平均色を出す
                color /= (_BlurCount * 2 + 1);  
                //アルファ値に関してはBlorPowerに合わせて適切に弱める
                color.a *= (_BlurPower / MaxBlurPower);
                return color;
            }

            //直線的なoriginalColor > finalColorの上昇から、Easeをかけた曲線的な上昇に変換する
            half3 cubicIn(half curParam, half maxParam, half3 originCol, half3 finalCol)
            {
                finalCol -= originCol;
                curParam /= maxParam;
                return finalCol * curParam * curParam * curParam + originCol;
            }            
        ENDHLSL    

        //Pass0: 一番上のPassはuGUIのCanvasRendererから自動的に呼び出される、処理的には下のPass1,2より後に実行される
        Pass
        {
            //一番最後は背景とブレンドして透過処理を行う
            Blend SrcAlpha OneMinusSrcAlpha    

            HLSLPROGRAM
                //オリジナルの画像を上に被せた上で、全てのピクセル色の輝度を上げる
                half4 frag (Varyings input) : SV_Target
                {    
                    //CanvasRendererから呼ばれた際は、_MainTexにはPass1, 2実行前の純粋な元の画像が代入されている
                    //この_MainTexから指定したUV座標のピクセル色を取得
                    half4 original = SAMPLE_TEXTURE2D(_MainTex, sampler_MainTex, input.uv);

                    //Pass1,2でBlur処理を通した画像から、指定したUV座標のピクセル色を取得
                    half4 blurred = SAMPLE_TEXTURE2D(_CustomTex, sampler_CustomTex, input.uv);
                    
                    //Blur画像の上に元の画像を重ねる
                    //例えば指定UV座標のalphaが1なら、完全に元の画像のピクセルを描画、0なら完全なBlur画像を描画
                    //0.5なら両者の中間のピクセル色が描画される
                    half4 combine = lerp(blurred, original, original.a); 
                    
                    //まず掛け算を使って、RGBの値をそれぞれの強度に応じて相応に増加させる
                    //この際輝度の上がり方が急すぎるので、Easeを使って少し序盤の輝度を上昇力を弱める
                    half3 finalCol = combine.rgb + (combine.rgb * MaxLuminance * 2);
                    combine.rgb = cubicIn(_Luminance, MaxLuminance, combine.rgb, finalCol);
                    //_Luminanceが7を超えてからは、足し算で純粋にRGBを増やし白飛びさせる
                    half addWhite = saturate((_Luminance - 7) / 3);
                    combine.rgb += addWhite;

                    return combine;
                }
            ENDHLSL
        }

        //Pass1: C#から明示的に呼び出す
        Pass
        {
            //背景は考慮せず画像単体で描画処理
            Blend One Zero   

            HLSLPROGRAM
                //x軸にBlurをかける
                half4 frag (Varyings input) : SV_Target
                {    
                    return executeBlur(input.uv, half2(1, 0));
                }
            ENDHLSL
        }        

        //Pass2: C#から明示的に呼び出す
        Pass
        {
            //背景は考慮せず画像単体で描画処理            
            Blend One Zero            

            HLSLPROGRAM
                //y軸にBlurをかける
                half4 frag (Varyings input) : SV_Target
                {    
                    return executeBlur(input.uv, half2(0, 1)); 
                }
            ENDHLSL
        }        
    }
}

シェーダーは直接uGUI Imageにアタッチできないので、マテリアルを作成する必要があります。
しかし今回の場合、後述のC#側のコードが自動でそれをやってくれます。

C#側の実装

下がC#側のコード全文です。
このC#のコードから、Blitを使ってさきほどのシェーダーのPassを呼び出します。
Copy Code

using System;
using UnityEngine;
using UnityEngine.UI;

[ExecuteAlways]
/// <summary>
/// RenderTextureをシェーダーに流し込んで画像を発光させる処理
/// </summary>
public class Glow3PassController : MonoBehaviour
{
    [Serializable]
    /// <summary>
    /// シェーダーに渡すパラメータはこちらで管理
    /// </summary>
    private class Parameters
    {
        [SerializeField, Range(0, 10)] private float luminance = 5;
        public float Luminance => luminance;
        [SerializeField, Range(0, 10)] private int blurCount = 5;
        public int BlurCount => blurCount;
        [SerializeField, Range(0, 10)] private float blurDistance = 3;
        public float BlurDistance => blurDistance;
        [SerializeField, Range(0, 10)] private float blurPower = 5;    
        public float BlurPower => blurPower;        

        // 変更があったときだけ更新するためのキャッシュ
        private float _lastLuminance;
        private int _lastBlurCount;  
        private float _lastBlurPower;  
        private float _lastBlurDistance;

        /// <summary>
        /// インスペクターのパラメータに変化があった場合trueを返す
        /// </summary>
        public bool CahngedParameter => !Mathf.Approximately(luminance, _lastLuminance) || blurCount != _lastBlurCount ||
                                        !Mathf.Approximately(blurPower, _lastBlurPower) || !Mathf.Approximately(blurDistance, _lastBlurDistance);        

        /// <summary>
        /// キャッシュに最新のパラメーター値を反映させる
        /// </summary>
        public void UpdateParameter()
        {
            _lastLuminance = luminance;
            _lastBlurCount = blurCount;
            _lastBlurPower = blurPower;
            _lastBlurDistance = blurDistance;
        }
    }

    [SerializeField] private Image targetImage;
    [SerializeField] private Shader targetShader;
    [SerializeField] private Parameters parameters;

    private Texture2D sourceTexture;
    private Material createdMaterial;
    private RenderTexture tmpRenderTexture1;
    private RenderTexture tmpRenderTexture2;

    /// <summary>
    /// 初期化処理の必要性がある場合trueを返す
    /// </summary>
    private bool NeedsInitialization => sourceTexture == null || sourceTexture != targetImage.sprite.texture || 
                                        createdMaterial == null || targetImage.material != createdMaterial ||
                                        tmpRenderTexture1 == null || tmpRenderTexture2 == null;

    /// <summary>
    /// 初期化処理
    /// </summary>
    private void Init()
    {
        Release();

        sourceTexture = targetImage.sprite.texture;
        createdMaterial = new (targetShader);
        createdMaterial.hideFlags = HideFlags.DontSave;
        targetImage.material = createdMaterial;            

        //処理負荷軽減のため、大元の画像サイズから1/2のサイズでtmpRenderTexture1, 2を作成
        //tmpRenderTexture1, 2は両方ともぼかし処理のための画像なので、サイズが小さくても見た目に違和感は出ない
        tmpRenderTexture1 = new RenderTexture(sourceTexture.width / 2, sourceTexture.height /2, 0);
        tmpRenderTexture2 = new RenderTexture(sourceTexture.width / 2, sourceTexture.height / 2, 0);

        //事前にBlitで更新される予定のtmpRenderTexture2を、_CustomTexの名前でシェーダー側と紐づけておく
        //後はCanvasRender内の処理で自動的にPass0を実行させて、その_CustomTexを画像に反映させる
        targetImage.material.SetTexture("_CustomTex", tmpRenderTexture2);

        //初期化後は問答無用でUpdateTextureを呼ぶ
        UpdateTexture();
    }

    /// <summary>
    /// 最初にUnityが1回だけ呼ぶ処理
    /// </summary>
    private void Start()
    {
        if (NeedsInitialization)
        {
            Init();
        }
    }

    /// <summary>
    /// Unityから毎フレーム呼ばれる処理
    /// </summary>
    private void Update()
    {
        if (targetImage == null || targetImage.sprite == null || targetShader == null)
        {
            return;    
        }

        if (NeedsInitialization)
        {
            Init();         
        }
        else if(parameters.CahngedParameter)
        {
            UpdateTexture();
        }
    }

    /// <summary>
    /// 対象のシェーダーのPass1, 2を実行させる
    /// </summary>
    private void UpdateTexture()
    {
        parameters.UpdateParameter();

        //輝度をセット
        createdMaterial.SetFloat("_Luminance", parameters.Luminance);
        //Blur設定をセット
        createdMaterial.SetInt("_BlurCount", parameters.BlurCount);
        createdMaterial.SetFloat("_BlurPower", parameters.BlurPower);
        createdMaterial.SetFloat("_BlurDistance", parameters.BlurDistance);         

        //第1引数の画像を元に、Blitでシェーダー内のPass1を実行させ、結果を第2引数に書き込む
        //Blitの仕様上、この書き込み処理は、第3引数のMaterial内の_MainTexを通して行われる
        Graphics.Blit(sourceTexture, tmpRenderTexture1, createdMaterial, 1);
        //Pass1の結果を元にPass2を実行
        Graphics.Blit(tmpRenderTexture1, tmpRenderTexture2, createdMaterial, 2);
    }

    /// <summary>
    /// 作成したRenderTextureやMaterialのメモリを解放する
    /// </summary>
    private void Release()
    {
        if (tmpRenderTexture1 != null)
        {
            tmpRenderTexture1.Release();
            tmpRenderTexture2.Release();            
        }
        if (createdMaterial != null)
        {
            DestroyImmediate(createdMaterial);  
        }
    }

    /// <summary>
    /// このGameObjectを非表示にした時の処理
    /// </summary>
    private void OnDisable()
    {
        Release();
    }
}

作成した
 Glow3PassController 
を何かしらのGameObjectにアタッチしてください。
そこから、インスペクターの
 TargetImage 
に対してシェーダーを適用させたい画像をアタッチしてください。
また
 TargetrSahder 
には上で追加したシェーダーコードのファイルをアタッチします。


これで実装は完了です。

パラメーターの解説

用意してあるパラメーターは4つです。
マテリアルのプロパティを直接弄っても正しく動作しないので、パラメーターの変更はC#から行ってください。

Luminance

冒頭の概要で紹介した、対象画像の光具合を調整する値です。0~7までは乗算で画像の輝度が上がっていきます。
7~10にかけては更に加算が加わって、画像の白飛びが著しくなります。

BlurCount

画像を何回サンプリングしてブレさせる(Blurをかける)かを指定します。
数字が増えるほど画像のアウトラインがブレて外側に広がっていきます。
処理速度の効率化のことを考えると、この値はなるべく抑えて、他のBlurDistanceやBlurPowerでブレ加減を調整した方が理想的です。

BlurDistance

画像をサンプリングする際に、どれだけUV座標をズラしてピクセル色を取得するかを指定します。
こちらも値が増えるほど、アウトラインが外側に広がっていきます。

BlurPower

外側に拡張されたアウトラインの、色の透明度の強さに変更を加えます。
この値が増えるほど、ブレた画像がくっきりと写ります。

Blur処理に関する注意点

Blurで拡張されるアウトラインは指定した画像の外までは伸びないので、あらかじめ周囲に透明部分のマージンを作った画像を用意します。
また画像素材の
 Mesh Type 
 Full Rect 
に設定します。
MeshType変更のサンプル画像
2
2