-100p

-10p

+10p

+100p

Unity溶解シェーダー

UnityでuGUI用溶解シェーダー。画像が徐々に溶けていくような演出。
(Dissolve Shader for Unity uGUI to gradual melting of images)

関連ページ 参考URL
前回 ドット欠けシェーダー を作った際に、ランダムノイズという関数について学習した。
この関数をパーリンノイズという複雑な関数に入れ替えてみた所、画像が溶け出すような、世間で言うDissolve Shaderが簡単に作れた。

今回は こちらの記事 こちらの記事 を参考しにしている。パーリンノイズの関数内で何が行われてるかは正直理解してない。

溶解シェーダー、コード全文

下は溶解シェーダーのサンプル動画。

以下コード全文。uGUI Imageへの反映方法は こちらの記事 参照。
Shader "UI/Dissolve"
{
    Properties 
    {
        _DissolvePow ("DissolvePow", Range(0.0, 1.0)) = 0 
//溶けていく力、1で完全に消滅する

        _Spot ("Spot", Range(1, 50)) = 10 
//溶けていく際の穴の数

        _Roughness ("Roughness", Range(1, 20.0)) = 1 
//欠けていくドットの粗さ

    }

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

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            struct appdata
            {
                fixed2 uv : TEXCOORD0;
                fixed4 vertex : POSITION;
                fixed4 color : COLOR;
            };

            struct v2f
            {
                fixed2 uv : TEXCOORD0;
                fixed4 vertex : POSITION;
                fixed4 color : COLOR;
            };

            sampler2D _MainTex;
            
//アタッチされたSpriteのサイズを取得(x=1.0/width, y=1.0/height, z=width, w=height)

            float4 _MainTex_TexelSize;
   
            float _DissolvePow;
            float _Spot;
            float _Roughness;

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

            
//2次元 疑似ランダム(入力は2次元です)

            float2 random2(fixed2 st) 
            {
                st = fixed2(dot(st, float2(127.1, 311.7)),
                    dot(st, fixed2(269.5, 183.3)));
                return -1.0 + 2.0 * frac(sin(st) * 43758.5453123);
            }

            
//パーリンノイズ (上記のrandom2を使います)

            float perlinNoise(fixed2 st)
            {
                fixed2 p = floor(st);
                fixed2 f = frac(st);
                fixed2 u = f * f * (3.0 - 2.0 * f);

                float v00 = random2(p + fixed2(0, 0));
                float v10 = random2(p + fixed2(1, 0));
                float v01 = random2(p + fixed2(0, 1));
                float v11 = random2(p + fixed2(1, 1));

                return lerp(lerp(dot(v00, f - fixed2(0, 0)), dot(v10, f - fixed2(1, 0)), u.x),
                    lerp(dot(v01, f - fixed2(0, 1)), dot(v11, f - fixed2(1, 1)), u.x),
                    u.y) + 0.5f;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = tex2D(_MainTex, i.uv);

                
//アタッチされたSpriteの縦横比をratioに保存

                fixed ratio = _MainTex_TexelSize.z / _MainTex_TexelSize.w;
                
//_Roughnessの入力値を実際に使う値まで細かくする

                fixed fixRough = _Roughness * 0.001f;

                
//対象ピクセルのx座標とy座標を保存、ただしfixRoughの値に応じて座標の取得が大雑把になる

                
//またy座標にはratioを追加で掛けることで、座標を大雑把にした時の値に補正を乗せて欠けるドットを正方形に修正する

                fixed roughX = floor(i.uv.x / fixRough) * fixRough;
                fixed roughY = floor(i.uv.y / (fixRough * ratio)) * (fixRough * ratio);
                fixed2 roughUv = fixed2(roughX, roughY);

                
//パーリンノイズにより、指定座標に対し(ほぼ)0~1のランダムな値が返ってくる

                fixed rdm = perlinNoise(roughUv * _Spot);
                
//パーリンノイズから_DisolvePowを引き、その値が-0.1未満になった時対象ドットを消滅させる

                fixed doVanish = step(-0.1, rdm - _DissolvePow * 1.2);

                col *= i.color;
                col.a = col.a * doVanish;

                return col;
            }
            ENDCG
        }
    }
}

プロパティを解説

用意してあるプロパティは3つ。
溶解シェーダーのプロパティ
 DissolvePow 
: 溶けていく力。0から1まで設定可能で、0で全て表示、1で全てのピクセルが消える。
 Spot 
: 溶けていく際の穴の数、大きいほど穴の数が爆発的に増えていく。
 Roughness 
: DissolvePowを上げていく際の、欠けていくドットの粗さ。1から20まで設定可能。
Roughnessを上げる際、ドット欠けシェーダーと同様にImageの設定によっては欠けていくドットが長方形になる。
またRoughnessの値は大きくしすぎるとDissolveの良さが失われるので、最大値が少し抑え目にしてある。

コードを少し解説

ランダムノイズ関数をパーリンノイズ関数に変えてるだけなので、あんまり解説するところがない。
違うところだけ少し解説。なおドット欠けシェーダーのコードの理解を前提としている。

                
//パーリンノイズにより、指定座標に対し(ほぼ)0~1のランダムな値が返ってくる

                fixed rdm = perlinNoise(roughUv * _Spot);
                
//パーリンノイズから_DisolvePowを引き、その値が-0.1未満になった時対象ドットを消滅させる

                fixed doVanish = step(-0.1, rdm - _DissolvePow * 1.2);
step関数内の引数で、-0.1やら* 1.2やらのマジックナンバーが使われている。
これは指定した_Spotの値によっては、perlinNoise関数から返ってくる値が0をわずかに割ってマイナス値が返ってきてしまうため。

stepの第一引数を0にしてしまうと、たとえ_DissolvePowが0でも最初からいくつかピクセルが欠けてる状態になってしまう。
第一引数を-0.1にすることで、わずかにマイナスに転んでしまったrdmの値にも対応している。
しかし第一引数を-0.1にした結果、今度は逆に_DissolvePowを最大値の1にしても画像が全て消えない状態になる。
このため第二引数の_DissolvePowを1.2倍にして無理やり解決してる。

抜本的な治療にはperlinNoise関数内を真面目に解析する必要があるが、そこまでの気力が起きなかった。
1
1

-100p

-10p

+10p

+100p