【Unity】複数枚の重なったSpriteを個別にアニメーションさせ、同時に違和感なく透過させる方法

# なんの記事？

**複数パーツのSpriteで構成されたキャラクターを透過した時の問題と解消法**についての記事です！
複数のSpriteでキャラクターを表現する場合、Rendererが重なることになるので、
透過をかけたときに重なったところが変に表示されてしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/65ef2a13-d55a-2c3e-8300-c3e89ba059ef.png)

今回は、この問題を解消しつつ、更に**各パーツをアニメーションさせる方法**についての記事です。

![animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/d8684d88-179e-2d94-6959-89aa63c09bdf.gif)


# Shaderを用意する
この問題は、異なるRendererが重なって、かつ別々に画像を描画しているために発生します。
そこで今回は、複数枚の画像を一括で処理してまとめたレンダリング結果を返すShaderを作ります！

基本的にはフラグメントシェーダーしかいじっていませんが、全体を載せておきます。

<details><summary><strong>LayeredShaderコード</strong></summary>
    
```hlsl
    Shader "Custom/LayeredShader"
    {
        Properties
        {
            //Layerの重ね順を表現するために、1から5の数字を付けています。5が一番上になります。
            //初期値blackは透明な黒の指定になります。
            _Layer5("Layer5", 2D) = "black" {}
            _Layer4("Layer4", 2D) = "black" {}
            _Layer3("Layer3", 2D) = "black" {}
            _Layer2("Layer2", 2D) = "black" {}
            _Layer1("Layer1", 2D) = "black" {}
            _Color("Color", Color) = (1,1,1,1)
        }
        SubShader
        {
            Tags
            {
                "Queue" = "Transparent"
                "RenderType" = "Transparent"
                "RenderPipeline" = "UniversalPipeline"
    
                "IgnoreProjector" = "True"
                "PreviewType" = "Plane"
            }
            
            Cull off
            ZWrite Off
            Blend SrcAlpha OneMinusSrcAlpha
    
            Pass
            {
                CGPROGRAM
                #pragma vertex vert
                #pragma fragment frag
    
                #include "UnityCG.cginc"
                
                sampler2D _Layer5;
                sampler2D _Layer4;
                sampler2D _Layer3;
                sampler2D _Layer2;
                sampler2D _Layer1;
                
                float4 _Layer5_ST;
                float4 _Layer4_ST;
                float4 _Layer3_ST;
                float4 _Layer2_ST;
                float4 _Layer1_ST;
                half4 _Color;
                
                struct appdata
                {
                    float4 vertex : POSITION;
                    float2 uv : TEXCOORD0;
                };
    
                struct v2f
                {
                    float2 uv : TEXCOORD0;
                    float4 vertex : SV_POSITION;
                };
                
                v2f vert (appdata v)
                {
                    v2f o;
                    o.vertex = UnityObjectToClipPos(v.vertex);
                    o.uv = TRANSFORM_TEX(v.uv, _Layer1);
    
                    return o;
                }
    
                float4 frag(v2f i) : COLOR
                {
                    //各レイヤーの描画情報を配列に格納します
                    fixed4 layers[5] = {
                        tex2D(_Layer1, i.uv),
                        tex2D(_Layer2, i.uv),
                        tex2D(_Layer3, i.uv),
                        tex2D(_Layer4, i.uv),
                        tex2D(_Layer5, i.uv)
                    };
                    
                    fixed4 c = float4(0, 0, 0, 0);
                    //不透明度の累乗を記録するための変数
                    float alphaAccum = 1;
    
                    //上のレイヤーから処理し始めます
                    for (int j = 4; j >= 0; --j) {
                        
                        fixed4 layer = layers[j];
                        
                        //記録された不透明度の累乗を使って、このピクセルの描画を決めます。
                        //例えば上のレイヤーが全て透明であった場合には、このレイヤーを、アルファ値を考慮して加算します。
                        //不透明度の累乗が0になった場合には、このピクセルは描画されません。
                        c.rgb += layer.rgb * layer.a * alphaAccum;
                        
                        //このピクセルにたいする、ここまでの 不透明度の累乗を記録します。
                        alphaAccum *= 1 - layer.a;
                    }
    
                    //値が1を超えないように制限します
                    c.rgb = min(c.rgb,1);
                    c.a = min(1 - alphaAccum,1);
    
                    //Colorプロパティで指定された色をかけます
                    return c * _Color;
                }
                ENDCG
            }
        }
    }
```

</details>

# Materialを用意する

作ったShaderを適用したMaterialを作りましょう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/7e3a7413-70a0-fdd5-5064-de046fe57a40.png)

InspcterからShaderを指定して、各Layerに画像を貼り付けてください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/e7759134-bf6f-11b1-ea03-21ef65ff6594.png)

:::note
Layer2~5の画像の描画位置は、Layer1のサイズを基準に取っています。
**画像は、全て同じピクセル数で描き出して**、不要部分は透過してください。
:::

# Rendererを用意して、半透明にしてみる


さっき作ったマテリアルをattachしてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/3583c4d8-e5ff-4627-00a3-c227b4330ade.png)

:::note
今回はMeshRendererを使用しています。
3DObject>Planeなどを作成して、回転させて使ってください。
(私はRotation x:90 y:180で使っています。スケールはxとzで調節します。)
:::
:::note
Sprite Rendererでも同じ設定で動作します。
万一位置ずれなどが起きる場合は画像のImport Setting > Mesh TypeをFull Rectにしてみてください。
:::
Sceneビューに5つのレイヤーの重なった像が表示されているかと思います。
Materialの表示欄からカラーの透明度を変更すると、全体が半透明になっていきます！
一旦ここまでで成功です！　続いてアニメーション周りの設定をしていきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/a709391e-d07b-1f6b-3dc5-2f35a92ec7a5.png)


# Animatiorと連携するためのスクリプトを用意する

ExecuteAlwaysアトリビュートと、
MonobehaviourのOnDidApplyAnimationPropertiesメソッドを使用して、
EditorでもRuntimeでも、Animatiorに変化があったらMaterialを上書きするスクリプトを書きます。

<details><summary><strong>RendererTexLayeredAnimationコード</strong></summary>

```csharp
    using UnityEngine;

    //アニメーションのタイミングで、マテリアルのテクスチャを変更する Editor上でも起動させたいためExecuteAlwaysを使用する
    [ExecuteAlways]
    [RequireComponent(typeof(Renderer))]
    public class RendererTexLayeredAnimation : MonoBehaviour
    {
        //重ね順を表現するため、Layer5からLayer1の順に設定する
        public Sprite layer5;
        public Sprite layer4;
        public Sprite layer3;
        public Sprite layer2;
        public Sprite layer1;

        //スプライトが設定されているかどうかを判定する
        private bool IsSpriteSet => layer1 is not null || layer2 is not null || layer3 is not null ||
                                    layer4 is not null || layer5 is not null;

        //マテリアルへのアクセスが多いので、IDをキャッシュしておく
        private int _layer5ID = -1;
        private int Layer5ID => _layer5ID is -1 ? _layer5ID = Shader.PropertyToID("_Layer5") : _layer5ID;
 
        private int _layer4ID = -1;
        private int Layer4ID => _layer4ID is -1 ? _layer4ID = Shader.PropertyToID("_Layer4") : _layer4ID;
        
        private int _layer3ID = -1;
        private int Layer3ID => _layer3ID is -1 ? _layer3ID = Shader.PropertyToID("_Layer3") : _layer3ID;

        private int _layer2ID = -1;
        private int Layer2ID => _layer2ID is -1 ? _layer2ID = Shader.PropertyToID("_Layer2") : _layer2ID;
        
        private int _layer1ID = -1;
        private int Layer1ID => _layer1ID is -1 ? _layer1ID = Shader.PropertyToID("_Layer1") : _layer1ID;
    
        
        private Renderer _meshRenderer;

        private Renderer ThisMeshRenderer => _meshRenderer? _meshRenderer : _meshRenderer = GetComponent<Renderer>();

        private Material _material;

        //Editor上でも起動するため、.sharedMaterial;を使用する。
        private Material ThisMaterial => _material? _material : _material = ThisMeshRenderer.sharedMaterial;
    
        //OnDidApplyAnimationPropertiesを使うと、Animationに変化があったタイミングで呼ばれる
        void OnDidApplyAnimationProperties()
        {
            if ( ThisMaterial is null) return;
            if ( IsSpriteSet is false ) return;
        
            if(layer1 is not null) ThisMaterial.SetTexture( Layer1ID , layer1.texture  );
            if(layer2 is not null) ThisMaterial.SetTexture( Layer2ID , layer2.texture  );
            if(layer3 is not null) ThisMaterial.SetTexture( Layer3ID , layer3.texture  );
            if(layer4 is not null) ThisMaterial.SetTexture( Layer4ID , layer4.texture  );
            if(layer5 is not null) ThisMaterial.SetTexture( Layer5ID , layer5.texture  );

        }
        
    }
```

</details>

これを、MeshRendererと同じGameObjectにアタッチしておいてください。
Animatorも必要になるので、アタッチして、AnimationControllerを作っておきましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/b4b10cfb-cddf-dc2e-8839-bbce84757f4c.png)

# Animationを作ってみる
AnimationタブのAddPropertyから、```RendererTexLayeredAnimation```の5つのLayerを追加してください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/31c6eadf-b363-bac1-d434-cb55c6d24c38.png)

それぞれのLayerに画像を指定してあげます。
1フレーム目には、元の状態の画像を登録してください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/6a6a59fb-dafd-d8c8-630c-c01ccc283b51.png)

全て登録すると、個別パーツをアニメーションさせながら、半透明が表現できる仕組みになりました！！

![animation.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2955766/d8684d88-179e-2d94-6959-89aa63c09bdf.gif)

# おわりに
今回は、2Dゲームを作る上で地味に苦戦した部分を記事にしてみました！！
実測はしていませんが、Rendererをまとめて、GPU内で処理を完結させるため、
おそらく重ね描画よりも軽くなると思います！

# 謝辞
偽典オーさまより素材をお借りました。ありがとうございます！
http://albireo.watson.jp/ukgk/freeshell.html

また、問題の解決にあたって、Unityゲーム開発者ギルドの皆様にたくさんのアドバイスを頂きました！
ありがとうございました！！