【Unity】「HIT」のログイン時のようなエフェクトをつくる

HITとは?

NEXONが出してるスマホゲームです。UE4製で綺麗なグラフィックのアクションゲームです。
mobile.nexon.co.jp

ログイン時のエフェクトってどんなの?

f:id:setchi_q:20170125221344g:plain

実装方針

まずこんな画像を用意します。
こういうのはパーリンノイズが向いてる気がするので、パーリンノイズで生成しました。
f:id:setchi_q:20170124011309p:plain
色を高さと見なして、こんな風にしたらそれっぽいのができそうですね!
f:id:setchi_q:20170124234952g:plain
(図は Processing で生成しました)
EffectVisualize.pde · GitHub

実装

シェーダの全体像です。

Shader "Custom/LoginEffectShader"
{
    Properties
    {
        [PerRendererData] _MainTex("Sprite Texture", 2D) = "white" {}
        _MaskTex("Mask Texture", 2D) = "white" {}
        _Height("Height", Float) = 0
    }

    SubShader
    {
        Tags
        {
            "Queue" = "Transparent"
            "IgnoreProjector" = "True"
            "RenderType" = "Transparent"
            "PreviewType" = "Plane"
            "CanUseSpriteAtlas" = "True"
        }

        Blend One OneMinusSrcAlpha

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            #pragma target 2.0
            #pragma multi_compile _ ETC1_EXTERNAL_ALPHA
            #include "UnityCG.cginc"

            struct appdata_t
            {
                float4 vertex   : POSITION;
                float4 color    : COLOR;
                float2 texcoord : TEXCOORD0;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct v2f
            {
                float4 vertex   : SV_POSITION;
                fixed4 color : COLOR;
                float2 texcoord  : TEXCOORD0;
                UNITY_VERTEX_OUTPUT_STEREO
            };

            v2f vert(appdata_t IN)
            {
                v2f OUT;
                UNITY_SETUP_INSTANCE_ID(IN);
                UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(OUT);
                OUT.vertex = UnityObjectToClipPos(IN.vertex);
                OUT.texcoord = IN.texcoord;
                return OUT;
            }

            sampler2D _MainTex;
            sampler2D _MaskTex;
            float _Height;

            fixed4 frag(v2f IN) : SV_Target
            {
                float maskHeight= tex2D(_MaskTex, IN.texcoord).r;
                if (maskHeight> _Height) {
                    discard;
                }
                return tex2D(_MainTex, IN.texcoord);
            }
                ENDCG
        }
    }
}

Properties ブロック内で、ノイズ画像の _MaskTex と現在の高さを表す _Height を定義します。

_MaskTex("Mask Texture", 2D) = "white" {}
_Height("Height", Float) = 0

フラグメントシェーダ内で、今見ている位置より高い位置のピクセルを破棄しています。

fixed4 frag(v2f IN) : SV_Target
{
    float maskHeight = tex2D(_MaskTex, IN.texcoord).r;
    if (maskHeight > _Height) {
        discard;
    }
    return tex2D(_MainTex, IN.texcoord);
}

あとは、C# スクリプトから適当に _Height を操作します。

using DG.Tweening;
using UnityEngine;

public class EffectTweener : MonoBehaviour
{
    [SerializeField]
    SpriteRenderer _spriteRenderer;

    Material _material;
    int _heightPropertyID;
    bool _isHidden = false;

    void Start()
    {
        _heightPropertyID = Shader.PropertyToID("_Height");
        _material = _spriteRenderer.material;
    }

    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            if (_isHidden)
            {
                Play(from: 0f, to: 1f);
            }
            else
            {
                Play(from: 1f, to: 0f);
            }
            _isHidden = !_isHidden;
        }
    }

    void Play(float from, float to)
    {
        DOTween.Kill(this);
        DOTween.To(
                () => from,
                a => _material.SetFloat(_heightPropertyID, a),
                to,
                1.5f)
            .SetId(this);
    }
}

ここまでの結果です。
f:id:setchi_q:20170125223215g:plain

仕上げ

フチに色を付けたり、Bloomで光らせたりして盛ります。

fixed4 frag(v2f IN) : SV_Target
{
    float maskHeight = tex2D(_MaskTex, IN.texcoord).r;
    if (maskHeight > _Height) {
        discard;
    }
    
    fixed4 color = tex2D(_MainTex, IN.texcoord);
    float edgeHeight = 0.015;
    return lerp(
        color,
        fixed4(0, 4, 2, 0),
        step(_Height - edgeHeight, maskHeight)
    );
}

f:id:setchi_q:20170124014044g:plain

まとめ

マスク画像を工夫すればもっと面白い表現ができそうです。
できればフラグメントシェーダ内の条件分岐をなくしたいです。




2017/01/26追記:シェーダーの改善

今回の記事のような処理は、αカットアウトシェーダを使うとよりシンプルに実装できると指摘をいただきました。
docs.unity3d.com

Unityのビルトインシェーダにあるαカットアウトシェーダ(Unlit/Transparent Cutout)を使って実装してみます。

仕組み

αカットアウトシェーダは名前の通り、画像のα値と閾値によってピクセルを破棄するか決定します。
元画像のα成分にマスク情報を埋め込んだ画像を用意します。
f:id:setchi_q:20170128154149p:plain
この画像を、Unlit/Transparent Cutout シェーダをセットしたマテリアルで描画して、_Cutoff プロパティを操作するとこのように動きます。
f:id:setchi_q:20170128154402g:plain
(前回と背景が変わっていますが、分かりやすさのためです。結果は前回とほぼ同じです)

フチに色を付ける

Unityのビルトインシェーダはここからダウンロードできます。
https://unity3d.com/jp/get-unity/download/archive

フチに色が付くように Unlit/Transparent Cutout の frag 関数内を改造したシェーダをつくりました。

fixed4 frag (v2f i) : SV_Target
{
    fixed4 col = tex2D(_MainTex, i.texcoord);
    clip(col.a - _Cutoff);
    UNITY_APPLY_FOG(i.fogCoord, col);

    float edgeHeight = 0.015;
    return lerp(
        col,
        fixed4(0, 4, 2, 1),
        step(col.a, _Cutoff + edgeHeight)
    );
}

改造したシェーダの全体像はこちらです。
https://gist.github.com/setchi/b5c9fd72c3cb5317dae44cb6f3eb7fef

実行結果です。
f:id:setchi_q:20170128154424g:plain

最初の実装と比較して

最初の実装では画像を二枚使用していたのに対し、一枚の画像で実装できました。
また、不要なピクセルをより手前(Alpha Test)の段階で破棄できて効率的になりました。