使い古されて今更感ある内容ですが、ノベルゲームでよくあるトランジション演出についてです。
トランジション演出というのは以下の画像のような場面転換で利用されるものです。
今回はuGUIで実装しますが、考え方はどのような環境でも変わりません。
※UnityちゃんはUnity-chanライセンス( © Unity Technologies Japan/UCL )です。
理屈
これをプログラムで黒いメッシュを制御して……となるととてもめんどくさいです。
古からこの演出には定石があります。
それはこの動きを画像として用意するというものです。
用意するものはこのような白黒画像です。
上のgifアニメと比べるとわかりますが、白い部分から黒い部分に向けて画面が消えていきます。
実装は簡単です。この画像を最前面にα画像として表示し、α値を時間に伴って増減させるだけです。
最初はこの画像の全体が見えないので、αを-1させます。
最終的には画像全体が見えるようになるのでαを+1させます。
後はこの-1~+1の間を補間すれば上のgifのような演出になります。
実装内容まとめ
順に実装すべき内容を書きます。
トランジション画像を最前面に表示する
まずは画像が画面に出せなければいけません。
uGUIではImageを使えば出ますが、白黒をαとして出したい上に、後々の演出をいれたいのでシェーダーは自作が必要です。
時間によって画像のα値を増減する
時間経過に伴ってα値を-1~+1する処理が必要です。
時間を制御するスクリプト
C#で時間に伴ってシェーダー定数を変更する処理が必要です。
以上
これだけです。
トランジション画像を最前面に表示する
ではまず画像を表示します。
グレースケールで作ったトランジション画像をプロジェクトに追加します。
インスペクターでuGUI用の設定を施します。
Texture TypeをSpriteにし、sRGBのチェックを外し、Alpha SourceにFrom Gray Scaleを選択します。
後はお好みで大丈夫です。
これをImageに設定します。何とも言えない画像が表示されると思います。
次にこれを表示する為のシェーダーを用意します。と言っても、Unity特有のルールなんかも多いのでもともとあったuGUI用のシェーダーを改造したものです。
Shader "Unlit/transition" { Properties { [PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {} _Color("Tint", Color) = (1,1,1,1) } SubShader { Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" "PreviewType" = "Plane" "CanUseSpriteAtlas" = "True" } Cull Off Lighting Off ZWrite Off ZTest[unity_GUIZTestMode] Fog{ Mode Off } Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata_t { float4 vertex : POSITION; float2 texcoord : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; half2 texcoord : TEXCOORD0; }; fixed4 _Color; sampler2D _MainTex; // 頂点シェーダーの基本 v2f vert(appdata_t IN) { v2f OUT; OUT.vertex = UnityObjectToClipPos(IN.vertex); OUT.texcoord = IN.texcoord; #ifdef UNITY_HALF_TEXEL_OFFSET OUT.vertex.xy += (_ScreenParams.zw - 1.0) * float2(-1,1); #endif return OUT; } // 通常のフラグメントシェーダー fixed4 frag(v2f IN) : SV_Target { half alpha = tex2D(_MainTex, IN.texcoord).a; return fixed4(_Color.r, _Color.g, _Color.b, alpha); } ENDCG } } FallBack "UI/Default" }
マテリアルに指定された色と、テクスチャのαで描画するだけのものです。9割9分が決まり文句です。
色を決める部分はfrag関数です。
tex2Dでテクスチャの色を取得し、返り値(実際に描画される色)には_Colorのrgbとテクスチャのαを指定しています。
_Colorはマテリアルに設定されているシェーダー定数です。(インスペクターでいじれるやつ)
シェーダーはマテリアルに付けなければ意味がないので、これ用のマテリアルも用意します。
使ったシェーダーをマテリアルに設定します。
作ったマテリアルをImageに設定します。
これで黒~透明の画像が表示されるようになりました。
時間によって画像のα値を増減する
さて、この画像のα値を時間によって増減させます。
増減させる箇所はシェーダーのfrag関数のalphaの部分です。
時間は0~1が渡されるとして、時間が0の時は-1、1の時は+1するように変更します。
時間変数_Alphaを用意してfragでalphaに足してやります。
Shader "Unlit/transition" { Properties { [PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {} _Color("Tint", Color) = (1,1,1,1) _Alpha ("Time", Range(0, 1)) = 0 } SubShader { Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" "PreviewType" = "Plane" "CanUseSpriteAtlas" = "True" } Cull Off Lighting Off ZWrite Off ZTest[unity_GUIZTestMode] Fog{ Mode Off } Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata_t { float4 vertex : POSITION; float2 texcoord : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; half2 texcoord : TEXCOORD0; }; fixed4 _Color; fixed _Alpha; sampler2D _MainTex; // 頂点シェーダーの基本 v2f vert(appdata_t IN) { v2f OUT; OUT.vertex = UnityObjectToClipPos(IN.vertex); OUT.texcoord = IN.texcoord; #ifdef UNITY_HALF_TEXEL_OFFSET OUT.vertex.xy += (_ScreenParams.zw - 1.0) * float2(-1,1); #endif return OUT; } // 通常のフラグメントシェーダー fixed4 frag(v2f IN) : SV_Target { half alpha = tex2D(_MainTex, IN.texcoord).a; alpha = saturate(alpha + (_Alpha * 2 - 1)); return fixed4(_Color.r, _Color.g, _Color.b, alpha); } ENDCG } } FallBack "UI/Default" }
変っている点は_Alphaを追加している部分とfrag関数のalphaに_Alphaの値を足している部分です。
ちなみにsaturateは引数が0以下なら0に、1以上なら1にする関数です。
_Alpha * 2 – 1ということで、0なら-1、1なら+1になります。αに足している為、_Alphaが0の時はすべてのテクセルが「0 * 2 – 1 = -1」されて0の完全透明に。
_Alphaが1だと「1 * 2 – 1 = +1」されて1の完全不透明になります。
基本のシェーダーはこれだけで終わりです。後は演出の内容によって好きにカスタマイズしてください。
時間を制御するスクリプト
ではこのシェーダーの_Alphaを0~1に変更するスクリプトを用意して動かしてみます。
using System.Collections; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; public class Transition : MonoBehaviour { [SerializeField] private Material _transitionIn; void Start() { StartCoroutine( BeginTransition() ); } IEnumerator BeginTransition() { yield return Animate( _transitionIn, 1 ); } /// <summary> /// time秒かけてトランジションを行う /// </summary> /// <param name="time"></param> /// <returns></returns> IEnumerator Animate(Material material, float time) { GetComponent<Image>().material = material; float current = 0; while (current < time) { material.SetFloat( "_Alpha", current / time ); yield return new WaitForEndOfFrame(); current += Time.deltaTime; } material.SetFloat( "_Alpha", 1 ); } }
マテリアルは外部から設定できるようにしました。
これをトランジション画像が設定されたImageに設定する事でアニメーションが行われます。
中身は特に難しいこともなく、コルーチンで時間を増やしているだけです。
ちなみに最後にちゃんと_Alphaに1を入れてやらないと時間の具合によっては中途半端な値になっていることがあります。
これを実行するとちゃんと暗転します。
基本はここまであとはαの変化の仕方を変えることで、直線的でない色の変化や、明転等が表現できます。
明転
先ほどは暗転を作りましたが、これでは最初のgifのように、暗くなった後明転して次場面が表示されません。
というわけで明転用シェーダを作ります。ただこれもそこまで難しくありません。
暗転とは逆に黒から白に向けて色が出るので、α値を逆にして、時間によってαが減ればいいわけです。
Shader "Unlit/rtransition" { Properties { [PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {} _Color("Tint", Color) = (1,1,1,1) _Alpha ("Time", Range(0, 1)) = 0 } SubShader { Tags { "Queue" = "Transparent" "IgnoreProjector" = "True" "RenderType" = "Transparent" "PreviewType" = "Plane" "CanUseSpriteAtlas" = "True" } Cull Off Lighting Off ZWrite Off ZTest[unity_GUIZTestMode] Fog{ Mode Off } Blend SrcAlpha OneMinusSrcAlpha Pass { CGPROGRAM #pragma vertex vert #pragma fragment frag #include "UnityCG.cginc" struct appdata_t { float4 vertex : POSITION; float2 texcoord : TEXCOORD0; }; struct v2f { float4 vertex : SV_POSITION; half2 texcoord : TEXCOORD0; }; fixed4 _Color; uniform fixed _Alpha; sampler2D _MainTex; // 頂点シェーダーの基本 v2f vert(appdata_t IN) { v2f OUT; OUT.vertex = UnityObjectToClipPos(IN.vertex); OUT.texcoord = IN.texcoord; #ifdef UNITY_HALF_TEXEL_OFFSET OUT.vertex.xy += (_ScreenParams.zw - 1.0) * float2(-1,1); #endif return OUT; } // 通常のフラグメントシェーダー fixed4 frag(v2f IN) : SV_Target { half alpha = tex2D(_MainTex, IN.texcoord).a; alpha = saturate(1 - alpha - (_Alpha * 2 - 1)); return fixed4(_Color.r, _Color.g, _Color.b, alpha); } ENDCG } } FallBack "UI/Default" }
シェーダーの名前とfrag関数しか変えていません。
変更内容は先ほどのとおり、1からalphaを引くことでα値を逆にしています。
その後、先ほどまで足していた_Alphaを引くようにしています。
これで真っ暗から明転します。
暗転 → 明転
暗転あとに明転させると先ほどのgifのようになります。
ただし、注意点として暗転と明転の間に場面を切り替える処理が必要です。
今回はUnityEventで外部から設定できるようにしています。
using System.Collections; using UnityEngine; using UnityEngine.Events; using UnityEngine.UI; public class Transition : MonoBehaviour { [SerializeField] private Material _transitionIn; [SerializeField] private Material _transitionOut; [SerializeField] private UnityEvent OnTransition; [SerializeField] private UnityEvent OnComplete; void Start() { StartCoroutine( BeginTransition() ); } void Update() { } IEnumerator BeginTransition() { yield return Animate( _transitionIn, 1 ); if ( OnTransition != null ) { OnTransition.Invoke(); } yield return new WaitForEndOfFrame(); yield return Animate( _transitionOut, 1 ); if ( OnComplete != null ) { OnComplete.Invoke(); } } /// <summary> /// time秒かけてトランジションを行う /// </summary> /// <param name="time"></param> /// <returns></returns> IEnumerator Animate(Material material, float time) { GetComponent<Image>().material = material; float current = 0; while (current < time) { material.SetFloat( "_Alpha", current / time ); yield return new WaitForEndOfFrame(); current += Time.deltaTime; } material.SetFloat( "_Alpha", 1 ); } }
BeginTransitionで先ほどは暗転だけだったのが、その後に明転させています。
また、間でUnityEventを発火しています。
※UnityEventはインスペクターでGameObjectとそのメソッドを設定できます。
今回はGameObjectの表示・非表示を切り替えるだけなので、特にスクリプトは用意していません。
OnTransitionに転換前のImageがあるGameObjectを指定し、メソッドにSetActiveを指定。パラメータである下のチェックを外します。
もう一つは転換後のImageがあるGameObjectを指定し、同様にSetActiveを指定。こちらは表示させるのでチェックを付けます。
今回はこれだけです。
※もちろんもっと複雑な場面転換が必要なら適宜実装が必要です。
画像さえ用意すれば……
画像さえ用意すればなんでもできます。
例えば適当にフォトショップのグラデーションで作ったコレとかもできます。
こんな感じです。
まとめ
理屈は大して難しくなく、画面の一番手前に画像を一枚だすだけで良いというお手軽さが結構使い勝手が良いです。
今回はuGUIでしましたが、普通のメッシュでも同じ理屈で出来ますし、ポストエフェクトとして実装する事も出来ます。