こんにちは、エンジニアのオオバです。

お悩みさん
お悩みさん
  • UnityでBloomを作ってみたい
  • ポストエフェクトって難しい?
  • オオバ
    オオバ
    本記事ではこれらの悩みを解決します。

    今回はUnityでポストエフェクト「Bloom(ブルーム)」を作ってみます。Bloomとは画面内の輝度(明るさ)抽出して物体から光が漏れるような画面効果です。

    Bloom (ブルーム) エフェクトは、画像の明るい領域の境界から伸びる光のフリンジを生成し、カメラでシーンをとらえるときに、非常に明るい光があふれ出ているような効果を作ります。

    引用 : Bloom - Unity マニュアルより

    個人的にはBloomは画作りには欠かせないエフェクトの1つです。では、どのように実装するか解説していきます。

    →11万文字で徹底解説した「DOTweenの教科書」Unityアニメーションの超効率化ツールはこちら

    2パス使ってBloomを実装する

    Bloomは2パス使います。パスとは描画する回数です。1回目に明るいところを取得。2回目に元画像と合成するのです。

    【Unity】ポストエフェクト「Bloom(ブルーム)」の作り方_0

    これが何もしていない状態の画面の見え方です。ここから2パス使ってBloomを適用していきます。

    1パス目 明るい所を抽出する

    【Unity】ポストエフェクト「Bloom(ブルーム)」の作り方_1

    1Pass目で、明るいところを取得した画像を生成します。最終的には元画像に加算するため、しきい値を越えなければ「黒」にしておきます。

    1パス目のサンプルコードはこちらです。

    fixed4 frag0 (v2f_img i) : SV_Target  
    {
        fixed4 col = tex2D(_MainTex, i.uv);  
        // ピクセルの明るさ  
        float bright = (col.r + col.g + col.b)/3;  
        // 0 or 1  
        float tmp = step(_Threshold, bright);  
        return tex2D(_MainTex, i.uv) * tmp * _Strength;  
    }
    

    ポイントは明るさ(bright)をしきい値元に黒くするところです。重要なのは 「step関数」

    step関数は 第1引数の値 > 第2引数の値 ? 1 : 0 と、「0」または「1」を返却します。しきい値(_Threshold)とピクセルの明るさ(bright)を代入ししています。

    2パス目 ぼかして元画像と合成

    2パス目は2つやることがあります。まず、1パス目で作成した明るいところを抽出した画像を ぼかし ます。そして、ぼかした画像を元画像に 加算合成 するのです。

    1パス目の画像をぼかす

    1パス目に作った明るいところを抽出した画像をぼかします。すると以下のような画像ができあがります。

    【Unity】ポストエフェクト「Bloom(ブルーム)」の作り方_2

    ぼかしている部分のシェーダーはこちら。

    fixed4 result;  
    // ぼかし  
    for (float x = 0; x < _Blur; x++)  
    {
        float xx = i.uv.x + (x - _Blur/2) * u;  
    
        for (float y = 0; y < _Blur; y++)  
        {
            float yy = i.uv.y + (y - _Blur/2) * v;  
            fixed4 smp = tex2D(_Tmp, float2(xx, yy));  
            result += smp;  
        }
    }
    result /= _Blur * _Blur;  
    

    全文は記事後半「サンプルコード全文」で公開しているので参考にしてみてください。

    1パス目の画像を元画像と合成して完成

    ぼかした画像をもとの画像に加算合成すれば完成です。以下のように1パス目の結果(result)を加算します。

    return tex2D(_MainTex, i.uv) + result;  
    

    すると、以下のように明るい部分から光が漏れたような表現ができ上がります。

    【Unity】ポストエフェクト「Bloom(ブルーム)」の作り方_3

    RenderTextureの解放に注意

    一時的に生成したRenderTextureの解放でハマりました。 RenderTexture.GetTemporary関数で生成したRenderTextureは Releaseメソッドでは解放できません。
    Releaseメソッドではなく、 ReleaseTemporary を使いましょう。

    💻ソースコード : RenderTextureが解放されない
    var tmp = RenderTexture.GetTemporary(100,100);  
    tmp.Release();  //・・・☓ 解放されない  
    
    💻ソースコード : RenderTextureが解放される
    var tmp = RenderTexture.GetTemporary(100,100);  
    RenderTexture.ReleaseTemporary(tmp);  //・・・○ 解放される  
    

    まとめ

    この記事では明るいところから光が漏れるようなポストエフェクト「Bloom」について解説してきました。

    【Unity】ポストエフェクト「Bloom(ブルーム)」の作り方_4

    複数のパスを使って表現する方法も理解できたのではないでしょうか。このように一度では描画できないものを回数を分けて描画するテクニックはよく登場するので覚えておきましょう。

    しかし、 パスが増えることはその分負荷も上がります。 できる限り少ないパスで描画できる方がよいです。

    また、今回は明るさの定義をRGBの合算値としましたが、輝度という値を持たせることも可能です。慣れてきたら輝度を使った本物のBloomに挑戦してみてください。

    サンプルコード全文

    Shader "Hidden/Bloom"  
    {
        Properties  
        {
            _MainTex ("Texture", 2D) = "white" {}  
        }
    
        SubShader  
        {
            Cull Off ZWrite Off ZTest Always  
    
            CGINCLUDE  
            #include "UnityCG.cginc"  
            sampler2D _MainTex;  
            sampler2D _Tmp;  
            float _Strength;  
            float _SamplerCnt;  
            float _Blur;  
            float _Threshold;  
    
            fixed4 frag0 (v2f_img i) : SV_Target  
            {
                fixed4 col = tex2D(_MainTex, i.uv);  
                // ピクセルの明るさ  
                float bright = (col.r + col.g + col.b)/3;  
                // 0 or 1  
                float tmp = step(_Threshold, bright);  
                return tex2D(_MainTex, i.uv) * tmp * _Strength;  
            }
    
            fixed4 fragBlur (v2f_img i) : SV_Target  
            {
                float u = 1 / _ScreenParams.x;  
                float v = 1 / _ScreenParams.y;  
    
                fixed4 result;  
                // ぼかし  
                for (float x = 0; x < _Blur; x++)  
                {
                    float xx = i.uv.x + (x - _Blur/2) * u;  
    
                    for (float y = 0; y < _Blur; y++)  
                    {
                        float yy = i.uv.y + (y - _Blur/2) * v;  
                        fixed4 smp = tex2D(_Tmp, float2(xx, yy));  
                        result += smp;  
                    }
                }
    
                result /= _Blur * _Blur;  
                return tex2D(_MainTex, i.uv) + result;  
            }
    
            ENDCG  
    
            // Pass 0 bright sampling  
            Pass  
            {
                CGPROGRAM  
                #pragma vertex vert_img  
                #pragma fragment frag0  
                ENDCG  
            }
    
            // Pass 1 blur & 合成  
            Pass  
            {
                CGPROGRAM  
                #pragma vertex vert_img  
                #pragma fragment fragBlur  
                ENDCG  
            }
        }
    }
    
    using System.Collections;  
    using System.Collections.Generic;  
    using UnityEngine;  
    
    public class Bloom : AbstractImageEffect  
    {
        // Bloomの強度  
        [Range(0,1f)] public float strength = 0.3f;  
        [Range(1,12)] public int samplerCnt = 6;  
        // ブラーの強度  
        [Range(1,64)] public int blur = 20;  
        // 明るさのしきい値  
        [Range(0,1f)] public float threshold = 0.3f;  
        // RenderTextureサイズの分母  
        [Range(1,12)] public int ratio = 1;  
    
        protected override void _OnRenderImage (RenderTexture src, RenderTexture dest)  
        {
            RenderTexture tmp = RenderTexture.GetTemporary (src.width / ratio, src.height / ratio, 0, RenderTextureFormat.ARGB32);  
            tmp.filterMode = FilterMode.Bilinear;  
    
            m_mat.SetFloat ("_SamplerCnt", samplerCnt);  
            m_mat.SetFloat ("_Strength", strength);  
            m_mat.SetFloat ("_Threshold", threshold);  
            m_mat.SetFloat ("_Blur", blur);  
            m_mat.SetTexture ("_Tmp", tmp);  
            Graphics.Blit (src, tmp, m_mat, 0);  
    
            // ぼかし + 合成  
            Graphics.Blit (src, dest, m_mat, 1);  
    
            RenderTexture.ReleaseTemporary (tmp);  
        }
    }
    
    using UnityEngine;  
    /// <summary>  
    /// イメージエフェクトをサクッと試す用ベースクラス  
    /// </summary>  
    [ExecuteInEditMode]  
    public abstract class AbstractImageEffect : MonoBehaviour  
    {
        Shader m_shader;  
        protected Material m_mat;  
        public string shaderName;  
    
        void OnRenderImage(RenderTexture src, RenderTexture dest)  
        {
            if (m_mat == null)  
            {
                m_shader = Shader.Find(shaderName);  
                m_mat = new Material(m_shader);  
                m_mat.hideFlags = HideFlags.DontSave;  
            }
            _OnRenderImage(src, dest);  
        }
        protected virtual void _OnRenderImage (RenderTexture src, RenderTexture dest){Graphics.Blit(src, dest, m_mat);}  
    }
    

    「Unity初心者大学」というUnity初心者向けのYouTube始めました!!
    ぜひチャンネル登録をお願いします!

    最後まで読んでいただきありがとうございました!
    すばらしいポストエフェクトライフをお過ごしください。

    オススメ記事