YAMADA TAISHI’s diary

ゲームについてとか私の日記とか。このブログのあらゆるコードは好きにどうぞ。利用規約があるものは記事内のGitHubのRepositoryのリンクで貼られていると思うので、そちらを参照ください。

Unityでルール画像を使ってフェードイン、フェードアウトさせる

こんにちは、やまだたいし( やまだ たいし (@OrotiYamatano) | Twitter )です。
前に社内勉強会があり、その際にShaderのプレゼンをしたのですが、業務にも使えそうなShaderの使い方を教えて欲しいと言われたので
今回は簡単なフェードイン、フェードアウトのリポジトリを作ったのでソレの解説です。

github.com

目次


前提


利用プラグイン

UniRxとDOTweenの無料のものを使っています。
どちらともライセンス表記が必須です。

Unityバージョン

Unity2019.4.16fで動作確認。多分DOTweenとUniRxが動けば、ほぼどのバージョンでも動くと思います。

リポジトリ


github.com

サンプル


ボタンを押すとフェードイン、フェードアウトをトグルするものを作りました。

f:id:OrotiYamatano:20210124215702g:plain

Shaderの解説


f:id:OrotiYamatano:20210124235235p:plain

Disolve(RGB)にルール画像を設定するとその設定されたルール画像の輝度に応じて、フェードされます。

edoさんのディゾルブShaderを参考にしました。

qiita.com

コードは以下の通り。

// Upgrade NOTE: replaced 'mul(UNITY_MATRIX_MVP,*)' with 'UnityObjectToClipPos(*)'

Shader "Custom/Dissolve" {
    Properties {
        _Color ("Main Color", Color) = (.5,.5,.5,1)
        _MainTex ("Base (RGB)", 2D) = "white" {}
        _DissolveTex ("Desolve (RGB)", 2D) = "white" {}
        _CutOff("Cut off", Range(0.0, 1.0)) = 0.0
    }

    SubShader {
        Tags { "RenderType"="Opaque" "Queue"="Transparent" }
        Pass {
            Name "BASE"
            Cull Off

            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;

            sampler2D _DissolveTex;
            float4 _DissolveTex_ST;

            float4 _Color;
            float _CutOff;
            struct appdata {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float2 texcoord : TEXCOORD0;
                float2 dissolvecoord : TEXCOORD1;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos (v.vertex);
                o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
                o.dissolvecoord = TRANSFORM_TEX(v.texcoord, _DissolveTex);
                UNITY_TRANSFER_FOG(o,o.pos);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                fixed4 col = _Color * tex2D(_MainTex, i.texcoord);
                fixed a = Luminance(tex2D(_DissolveTex, i.dissolvecoord).xyz);
                if (_CutOff > a) {
                    discard;
                }

                return col;
            }
            ENDCG           
        }

        Pass {
            Tags { "LightMode" = "ForwardBase" }

            Name "Add"
            Cull Off
            Blend One One

            CGPROGRAM
            #pragma vertex vert

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;

            sampler2D _DissolveTex;
            float4 _DissolveTex_ST;

            float4 _Color;
            float _CutOff;
            float _Width;

            struct appdata {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float2 dissolvecoord : TEXCOORD0;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos (v.vertex);
                o.dissolvecoord = TRANSFORM_TEX(v.texcoord, _DissolveTex);
                return o;
            }
            ENDCG           
        }

        Pass {
            Tags { "LightMode" = "ForwardBase" }

            Name "Add"
            Cull Off
            Blend One One

            CGPROGRAM
            #pragma vertex vert

            #include "UnityCG.cginc"

            sampler2D _MainTex;
            float4 _MainTex_ST;

            sampler2D _DissolveTex;
            float4 _DissolveTex_ST;

            float4 _Color;
            float _CutOff;
            float _Width;

            struct appdata {
                float4 vertex : POSITION;
                float2 texcoord : TEXCOORD0;
                float3 normal : NORMAL;
            };

            struct v2f {
                float4 pos : SV_POSITION;
                float2 dissolvecoord : TEXCOORD0;
            };

            v2f vert (appdata v)
            {
                v2f o;
                o.pos = UnityObjectToClipPos (v.vertex);
                o.dissolvecoord = TRANSFORM_TEX(v.texcoord, _DissolveTex);
                return o;
            }

            ENDCG           
        }
    } 

    Fallback "VertexLit"
}

fixed a = Luminance(tex2D(_DissolveTex, i.dissolvecoord).xyz);

の部分で_DissolveTexの輝度を計算しています。

_CutOffより小さい場合、描画せずに処理を中断しています。

トランジションの処理の解説


コード

        public class Transition : MonoBehaviour,IUtilTransition{
            [SerializeField] private Material transitionMaterial;
            public delegate void Callback();
            private Callback callbackComplete;
            private static readonly int Property = Shader.PropertyToID("_CutOff");
            private float TransitionRate { get; set; }
            private bool IsFade { get; set; }

            private void Start() {
               if (transitionMaterial == null) {
                   Debug.LogError("Transition.cs:マテリアルが設定されてない");
               }
               this.ObserveEveryValueChanged(x => TransitionRate).Subscribe(_ => {
                       transitionMaterial.SetFloat(Property, TransitionRate);
                   }
               ).AddTo(this);

            }
            
            private void OnApplicationQuit(){
                transitionMaterial.SetFloat(Property, 1.0f);
            }

            public bool IsActiveFade() {
               return IsFade;
            }

            /// <summary>
            /// カットアウトさせる
            /// </summary>
            /// <param name="startVal"></param>
            /// <param name="endVal"></param>
            /// <param name="duration"></param>
            public IUtilTransition Fade(float startVal,float endVal,float duration) {
                IsFade = true;
                TransitionRate = startVal;
                DOTween
                    .To(() => TransitionRate, (x) => TransitionRate = x, endVal, duration)
                    .SetEase(Ease.InOutSine)
                    .OnComplete(() => {
                        IsFade = false;
                        callbackComplete?.Invoke();
                    });
                return this;
            }

            /// <summary>
            /// 完了
            /// </summary>
            /// <param name="action"></param>
            /// <returns></returns>
            public IUtilTransition Complete(Callback action) {
                this.callbackComplete = action;
                return this;
            }

            /// <summary>
            /// フェードアウト
            /// </summary>
            public IUtilTransition FadeOut() {
                return Fade( 0, 1,0.2f);
            }


            /// <summary>
            /// フェードイン
            /// </summary>
            public IUtilTransition FadeIn() {
                return Fade(1, 0,0.2f);
            }
        }

TransitionRateの値が変わったらマテリアルに対して
カットオフの値を設定することで段々と透過するようにしています。

DoTweenのOnCompleteが呼ばれた際に続けてデリゲートでcallbackCompleteを呼ぶことでフェードの終了後にCompleteで処理を行うことが出来ます。

トランジション処理を呼び出す側


コード

public class PressButton : MonoBehaviour {
    [SerializeField] 
    private TransitionFade.Transition transition;
    private IUtilTransition fadeObj;   //Unityは早くインターフェースでSerializeField使えるようにして欲しい……
    private bool isDoneFade;

    private void Start() {
        Button btn = GetComponent<Button>();
        fadeObj = transition;
        
        //ボタンが押されなおかつ、フェード中ではない?
        btn.OnClickAsObservable().Where(_ =>!fadeObj.IsActiveFade()).Subscribe(_ => {
            if (isDoneFade) {   
                //フェードが完了してたら、フェードインする
                fadeObj.FadeIn().Complete(() => {
                    isDoneFade = false;
                    Debug.Log("フェードイン終了");
                });
                return;
            }

            //フェードが行われてなかったら、フェードアウトする
            fadeObj.FadeOut().Complete(() => {
                Debug.Log("フェードアウト終了");
                isDoneFade = true;
            });
        });
    }

}

別にインターフェースを使わなくても良いんですが、一応定義しておいたのでインターフェースの変数で色々処理をしています。

処理は注釈通りです。
UniRxの処理が重なって分かりづらい方も居ると思いますので念の為解説すると
ボタンが押された時にフェード処理の場合tureを返すfadeObj.IsActiveFade()という関数を見た上で
フェード中でなければフェードインもしくはフェードアウトを行う用にする処理しているというのが上記のスクリプトの内容になっています。

フェードするスピードや始まりの値を変えたい場合、FadeInやFadeOut関数を呼ばす直接Fade関数を呼んで、各値を設定してもFadeが出来ます。

まとめ


簡単なShaderのスクリプトでフェードイン・フェードアウトを実現しました。
Scene遷移時などパッと画面が変わってしまうと味気ないので、黒い画像へのフェードを挟むだけでもある程度見た目が良くなると思います。
参考になればと思います。以上です。