-100p

-10p

+10p

+100p

Unity陽炎シェーダー

Unity uGUI用、陽炎シェーダー。真夏の空気感や熱波を表現。
(Heat Haze Shader for Unity uGUI)

関連ページ
ゆらぎシェーダー レンズフレアシェーダー の経験を経て、念願の陽炎シェーダーを作れた。
これで思い残すことはない。

陽炎シェーダー概要

陽炎シェーダーのサンプル動画は次の通り。

挙動自体はシンプルにゆらぎシェーダーとレンズフレアシェーダーを合体させたものになる。
特定のパラメータをOnにすることで、太陽光の明滅と画面のゆらぎのアニメーションを時間経過に従って自動で実行してくれる。
画面をスクロールさせるとゆらぎが実感しづらくなるので、もう少しゆらぎを大きくしても良いかも。
この辺りも後述のパラメータで弄ることができる。

今回コードを合体するだけなので簡単に作れると思いきや、シェーダー内の計算量が跳ね上がり大分画面が重くなってしまった。
なのでゆらぎ表現に使っていたノイズの計算式を削除し、下のようなノイズ画像を事前に作っておくことで負荷を軽減させている。
事前に作ったノイズ画像
実装に必要なノイズ画像、マテリアル、シェーダーをUnityPackageにまとめてこちら に上げてある。

陽炎シェーダーの実装の仕方

落としたHeatHaze.unitypackageをダブルクリックすると、UnitySample/U0039の下に必要な3つのファイルがインポートされる。
UnityPackageでインポートされるファイル群
HeatHazeマテリアルがfbm_noiseとHeatHazeシェーダーの2つを参照している。

HierarchyのUI Canvasの一番下にImage Componentを配置。ストレッチにして画面全体を覆うようにする。
このImageにインポートしたHeatHazeマテリアルをアタッチ、これで概ね実装終了になる。
陽炎シェーダーのHierarchy上の配置

プロパティを解説

陽炎シェーダーのプロパティは14種あり滅茶苦茶多い。
大きく分けてレンズフレアを弄る変数7種、ゆがみを弄る変数4種、それ以外の変数3種になる。
陽炎シェーダーのプロパティ

文章だと分かり辛いので以下動画で解説。

LensFlare

Degreeは光線の角度、FlareLenghtは光線の長さを長さを変えることができる。
FlareConvergeを弄ると光線を中央寄せにするか、画面端に寄せるかを設定できる。




FlareSizeはレンズフレアの大きさを、FlareHuePlusはレンズフレアの虹色の発色具合を変更することが出来る。
FlareLuminanceを弄るとレンズフレアの輝度の高さを変更できる。




UseFlareAutoAnimのトグルをOnにすると、UnityをPlay状態にしてるだけで自動でFlareLuminanceの値を弄り光を明滅させてくれる。
初期状態ではOffになっている。

Flactuation

Flactuationの方は機能がお互いに依存し合っていて説明が少し難しい。順不同で解説。

FluctScrollの値を増減させる事で、座標修正に使うノイズ画像を上下にスクロールさせ、ゆがみアニメーションを実行できる。
またゆがみアニメーションの際のゆがみの大きさを、FluctPowの数値を弄ることで変更できる。
下の動画はFluctPowが最小の時と最大の時の効果の違い。


UseFluctAutoAnimのトグルをOnにすると、UnityをPlay状態にしてるだけで自動でゆがみアニメーションを実行してくれる。
このトグルがOnの時は、FluctScrollへの入力は無視される。FluctPowの方はオートアニメーションの時もしっかり反映される。
初期状態ではトグルはOffになっている。


AnimSpeedの値を弄ると、UseFluctAutoAnimがOnの時のアニメーションのスピードを変更できる。
下の動画はAnimSpeedが最小の時と最大の時の効果の違い、最小だとほとんど動いてないように見えるかも。

Other

Brightnessで画面全体の輝度を変更できる。


OverrideRateで画面を単一色に塗りつぶす際の配合率を操作できる。
またOverrideColでその単一色を変更できる。

陽炎シェーダー、コード全文

一応陽炎シェーダーのコード全文もここに掲載。
Shader "UI/HeatHaze"
{
    Properties
    {
        [HideInInspector]_MainTex("-",2D)="white"{} 
        _NoiseTex("NoiseTex", 2D) = "white"{} 
        
        [Header(LensFlare)]
        [Space(10)]
        _Degree("Degree", Range(0, 359)) = 0 
//光線の角度を指定

        _FlareLength("FlareLength", Range(0, 1)) = 0.8 
//光線の長さを指定   

        _FlareConverge("FlareConverge", Range(0, 1)) = 1 
//大きいほど光が端に寄り小さいほど中央に寄る


        [Space(10)]
        _FlareSize("FlareSize", Range(0.1, 2)) = 0.9 
//レンズフレアの大きさ

        _FlareHuePlus("FlareHuePlus", Range(-1, 1)) = 0 
//レンズフレアの虹色を変化させる

        _FlareLuminance("FlareLuminance", Range(0.1, 2)) = 0.8 
//レンズフレアの光の強さ

        [Space(10)]
        [Toggle] _UseFlareAutoAnim("UseFlareAutoAnim", float) = 0 
//経過時間によって自動で_FlareLuminanceを変化させるか


        [Header(Flactuation)]
        [Space(10)]
        _FluctPow("FluctPow", Range(0.01, 0.05)) = 0.02 
//歪みの強さ

        _AnimSpeed("AnimSpeed", Range(0.1, 1)) = 0.4 
//歪みが変化するスピード

        _FluctScroll("FluctScroll", float) = 0 
//参照するノイズ画像の座標をずらす、この値を変化させると画面がゆらぐように見える

        [Space(10)]
        [Toggle] _UseFluctAutoAnim("UseFluctAutoAnim", float) = 0 
//経過時間によって自動で_FluctScrollを変化させるか


        [Header(Other)]
        [Space(10)]
        _Brightness("Brightness", Range(1, 2)) = 1.3 
//画面全体の輝き

        _OverrideCol("OverrideCol", Color) = (1, 0, 0, 1) 
//画面全体を単一色で上書きする際の色

        _OverrideRate("OverrideRate", Range(0, 1)) = 0.15 
//画面全体を単一色で上書きする配合率

    }

    SubShader
    {
        Tags 
        { 
            "Queue" = "Transparent"
            "RenderType"="Transparent"
        }
        Cull Off
        ZWrite Off
        Blend SrcAlpha OneMinusSrcAlpha

        CGINCLUDE

        #pragma vertex vert
        #pragma fragment frag
        #include "UnityCG.cginc"

        struct appdata
        {
            float4 vertex : POSITION;
            float2 uv : TEXCOORD0;          
        };

        struct v2f
        {
            float4 vertex : SV_POSITION;
            float2 uv : TEXCOORD0;            
        };

        sampler2D _MainTex;
        sampler2D _NoiseTex;        
        sampler2D _GrabTexture;

        
//LensFlare

        fixed _Degree;
        fixed _FlareLength;             
        fixed _FlareConverge;

        fixed _FlareSize;
        fixed _FlareLuminance;
        fixed _FlareHuePlus;

        float _UseFlareAutoAnim;

        
//Flactuation

        float _FluctPow;
        float _AnimSpeed;
        float _FluctScroll;

        float _UseFluctAutoAnim;      

        
//Other

        fixed _Brightness;
        fixed4 _OverrideCol;
        fixed _OverrideRate;

        v2f vert(appdata v)
        {
            v2f o;
            o.vertex = UnityObjectToClipPos(v.vertex);
            o.uv = ComputeGrabScreenPos(o.vertex);
            return o;
        }

        
//光線の長さを修正

        fixed2 reflectLength(fixed baseX, fixed baseY)
        {
            fixed remainX = saturate(0.5 - baseX) * (1 - _FlareLength);
            fixed remainY = saturate(0.5 - baseY) * (1 - _FlareLength);
            return fixed2(baseX + remainX, baseY + remainY);
        }

        
//光線を適切に中央に寄せる

        fixed2 reflectConverge(fixed baseX, fixed baseY)
        {
            return fixed2(baseX * _FlareConverge, baseY * _FlareConverge);
        }

        
//_Degreeの入力値に合わせて座標を適切に変換

        
//baseX = 右上にレンズフレアがある時のx軸のベース座標

        
//baseY = 右上にレンズフレアがある時のy軸のベース座標

        fixed2 reflectDegree(fixed baseX, fixed baseY)
        {
            
//_Degree=0が右上になってるので、少し計算が特殊になってる

            
//画面を左45度傾けたとして、_Degree値が中心から右に寄ってるか。180で割るとバグるので179.9で代替え

            fixed isRight = step(_Degree / 179.9, 1);
            
//画面を左45度傾けたとして、_Degree値が中心から上に寄ってるか。90で割るとバグるので89.9で代替え

            fixed isUp = isRight * step(_Degree % 179.9 / 89.9, 1) + (1 - isRight) * (1 - step(_Degree % 179.9 / 89.9, 1));

            
//画面を4つの区間に分割

            fixed isRightUp = isRight * isUp;
            fixed isRightDown = isRight * (1 - isUp);
            fixed isLeftDown = (1 - isRight) * (1 - isUp);
            fixed isLeftUp = (1 - isRight) * isUp;

            
//0~1の値を返す。_Degreeが45, 135, 225, 315の時transは(ほぼ)ゼロになる

            fixed trans = (45 - _Degree % 89.9) / 45;

            
//x軸の座標を修正

            fixed fixX = isRightUp * baseX +
                         isRightDown * baseX * trans +
                         isLeftDown * -baseX +
                         isLeftUp * -baseX * trans;
            
//y軸の座標を修正

            fixed fixY = isRightUp * baseY * trans +
                         isRightDown * -baseY +
                         isLeftDown * -baseY * trans +
                         isLeftUp * baseY;

            return fixed2(fixX, fixY);
        }

        
//レンズフレアの座標修正

        fixed2 fixFlarePos(fixed x, fixed y)
        {
            fixed2 fixUv = reflectConverge(x, y);
            fixUv = reflectLength(fixUv.x, fixUv.y);
            fixUv = reflectDegree(fixUv.x, fixUv.y);
            return fixUv;
        }

        
//レンズフレアの大きさを修正

        fixed2 reflectSize(fixed size)
        {
            return size * _FlareSize ;
        }

        
//floatをRGBに変換

        fixed3 HUEtoRGB(in float H)
        {
            float R = abs(H * 6 - 3) - 1;
            float G = 2 - abs(H * 6 - 2);
            float B = 2 - abs(H * 6 - 4);
            return saturate(float3(R, G, B));
        }
        
        
//レンズフレアの円を作成する

        
//shiftX = 画面中央からどれだけズレるか、-0.5~0.5の値を受け入れる

        
//shiftY = 画面中央からどれだけズレるか、-0.5~0.5の値を受け入れる

        fixed4 createFlare(v2f IN, fixed size, fixed shiftX, fixed shiftY, 
            fixed hueBlendRate, fixed overrideRate, fixed hueOffset,
            fixed inStartAlpha, fixed inRangeAlpha, fixed outStartAlpha, fixed outRangeAlpha)
        {
            size = reflectSize(size);

            fixed4 color = (tex2D(_MainTex, IN.uv));
            fixed offset = max(0, hueOffset + _FlareHuePlus);
            fixed3 hueColor;
        
            fixed outAlpha = 1;
            fixed inAlpha = 1;

            
//渡されたuv座標を作成する円の座標に改変。座標は画面中央を0とし、xもyも最小値が-1、最大値が1になる

            fixed fixX = abs(IN.uv.x - 0.5 - shiftX) * 2;
            fixed fixY = abs(IN.uv.y - 0.5 + shiftY) * 2;
            
//解像度の縦横比に関わらず円を正円にするための処理と、円の大きさの修正

            fixed minScreen = min(_ScreenParams.x, _ScreenParams.y);
            fixX = fixX * (minScreen / _ScreenParams.y) / size;
            fixY = fixY * (minScreen / _ScreenParams.x) / size;
            
//画面中央からの直線距離を取得

            fixed atan = sqrt(fixX * fixX + fixY * fixY);

            
//距離からHUE値を取得

            fixed fixHue = abs(atan + offset);
            
//HUEをRGBに変換しを虹色として円に反映

            hueColor= (HUEtoRGB((abs(fixHue)) % 1) / hueBlendRate);
            color.xyz *= 1 / hueColor;

            
//円の中心から外に向かうアルファ値を取得

            fixed fixInStartAlpha = inStartAlpha - hueBlendRate;
            inAlpha = saturate(atan - fixInStartAlpha);
            inAlpha = saturate(inAlpha / inRangeAlpha);
            
//円の輪郭付近のアルファ値を取得

            fixed fixOutStartAlpha = outStartAlpha - (hueBlendRate / 2) ;
            outAlpha = saturate(atan - fixOutStartAlpha);
            outAlpha = 1 - saturate(outAlpha / outRangeAlpha);

            
//変数を4つかけて最終的なアルファ値を決定

            fixed luminance = _UseFlareAutoAnim * (_FlareLuminance + sin(_Time.y) * 0.2) + 
                              (1 - _UseFlareAutoAnim) * _FlareLuminance;
            color.a = outAlpha * inAlpha * overrideRate * luminance;

            return color;
        }

        ENDCG

        
//画面全体のゆらぎを実行

        GrabPass{}            
        Pass
        {
            CGPROGRAM
            fixed4 frag (v2f i) : SV_Target
            {
                float2 nUv = i.uv;

                
//UseFlareAutoAnimがOnの時は時間経過によってノイズの位置をずらす、Offの時はFluctScrollを参照する

                fixed shift = ((_UseFluctAutoAnim * _Time.y) + ((1 - _UseFluctAutoAnim) * _FluctScroll)) * _AnimSpeed;
                
//ノイズ画像をループさせる時に途切れ感を失くすために、サイン波を利用する

                
//まずsin関数にuv座標のy軸とshift値を合計したものを渡し、数値を-1~1の範囲に収める

                
//それをuv座標用に0~1の範囲に圧縮し、y軸に上書き

                nUv.y = (sin(nUv.y + shift) + 1) / 2;

                
//tex2Dを使うことで、指定テクスチャの指定uv座標のカラー値を取得

                fixed4 uvNoise= tex2D(_NoiseTex, nUv);
                
//カラーのRGB値を座標の修正値として無理やり変形させる処理

                
//ノイズ画像はモノクロのため、黒=(0, 0, 0)、白=(1, 1, 1)のようにRGB値は全て一緒になる

                
//どこから取得してもいいがここではrから値を取得、更に値の範囲を0~1から-1~1に拡大し座標の修正値とする

                fixed noise = uvNoise.r * 2 - 1;

                
//画面の上下端では歪みを実行させないためのnoise配合率を算出

                fixed isUvBotom = step(i.uv.y, 0.1);
                fixed rate1 = isUvBotom * (i.uv.y / 0.1) + (1 - isUvBotom) * 1;
                fixed isUvUp = 1 - step(i.uv.y, 0.9);
                fixed rate2 = isUvUp * ((0.1 - (i.uv.y - 0.9)) / 0.1) + (1 - isUvUp) * 1;
                fixed noiseRate = rate1 * rate2;

                
//noiseの値は-1~1の範囲に収まっている

                
//これに_FluctPowを掛けると、最小で-0.01~0.01、最大で-0.1~0.1の範囲に収まる

                
//その修正された値で、対象画像のuv座標のy軸だけ移動させ、描画するピクセルをズラす

                i.uv.y += noiseRate * (noise * _FluctPow);

                
//Grabした画像に変形させたuv座標を適用させ歪ませる

                fixed4 col = tex2D(_GrabTexture, i.uv);
                return col;
            }
            ENDCG
        }        

        
//虹リング大の描写

        Pass
        {
            CGPROGRAM
            fixed4 frag(v2f IN) : SV_Target
            {
                fixed2 fixPos = fixFlarePos(0.35, 0.35);
                fixed4 color = createFlare(IN, 0.4, fixPos.x, fixPos.y, 0.1, 0.25, 12.8, 0, 0, 0.95, 0.1);
                return color;
            }
            ENDCG
        } 

        
//虹リング小の描写

        Pass
        {
            CGPROGRAM
            fixed4 frag(v2f IN) : SV_Target
            {
                fixed2 fixPos = fixFlarePos(0.17, 0.17);
                fixed4 color = createFlare(IN, 0.23, fixPos.x, fixPos.y, 0.1, 0.2, 14.9, 0, 1, 0.8, 0.2);
                return color;
            }
            ENDCG
        }

        
//虹リング中の描写

        Pass
        {
            CGPROGRAM
            fixed4 frag(v2f IN) : SV_Target
            {
                fixed2 fixPos = fixFlarePos(0.23, 0.23);
                fixed4 color = createFlare(IN, 0.38, fixPos.x, fixPos.y, 0.55, 0.35, 4.65, 0.2, 0.8, 0.9, 0.1);
                return color;
            }
            ENDCG
        }          
        
        
//白色発光大の描写

        Pass
        {
            CGPROGRAM
            fixed4 frag(v2f IN) : SV_Target
            {
                fixed2 fixPos = fixFlarePos(0.47, 0.47);
                fixed4 color = createFlare(IN, 0.6, fixPos.x, fixPos.y, 1, 0.9, 0.3, 0, 0, 0.8, 0.7);
                return color;
            }
            ENDCG
        }

        
//白色発光中の描写

        Pass
        {
            CGPROGRAM
            fixed4 frag(v2f IN) : SV_Target
            {
                fixed2 fixPos = fixFlarePos(0.26, 0.26);
                fixed4 color = createFlare(IN, 0.3, fixPos.x, fixPos.y, 1, 0.5, 1.5, 0, 0, 0.9, 0.15);
                return color;
            }
            ENDCG
        }

        
//白色発光小の描写   

        Pass
        {
            CGPROGRAM
            fixed4 frag(v2f IN) : SV_Target
            {
                fixed2 fixPos = fixFlarePos(0.12, 0.12);
                fixed4 color = createFlare(IN, 0.15, fixPos.x, fixPos.y, 0.9, 0.3, 2, 0, 0, 0.9, 0.1);
                return color;
            }
            ENDCG
        }           
        
        
//画面全体を発光させる処理 

        GrabPass{}     
        Pass
        {
            CGPROGRAM
            fixed4 frag(v2f IN) : SV_Target
            {
                fixed4 color = (tex2D(_GrabTexture, IN.uv));
                color = color * _Brightness;
                color = (1 - _OverrideRate) * color + (_OverrideRate) * _OverrideCol;
                return color;
            }
            ENDCG
        }        
    }
}
0
0

-100p

-10p

+10p

+100p