1
3

More than 1 year has passed since last update.

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

Last updated at Posted at 2023-04-16

なんの記事?

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

image.png

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

animation.gif

Shaderを用意する

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

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

LayeredShaderコード
    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
            }
        }
    }

Materialを用意する

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

image.png

InspcterからShaderを指定して、各Layerに画像を貼り付けてください。
image.png

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

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

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

image.png

今回はMeshRendererを使用しています。
3DObject>Planeなどを作成して、回転させて使ってください。
(私はRotation x:90 y:180で使っています。スケールはxとzで調節します。)

Sprite Rendererでも同じ設定で動作します。
万一位置ずれなどが起きる場合は画像のImport Setting > Mesh TypeをFull Rectにしてみてください。

Sceneビューに5つのレイヤーの重なった像が表示されているかと思います。
Materialの表示欄からカラーの透明度を変更すると、全体が半透明になっていきます!
一旦ここまでで成功です! 続いてアニメーション周りの設定をしていきます。
image.png

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

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

RendererTexLayeredAnimationコード
    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  );

        }
        
    }

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

image.png

Animationを作ってみる

AnimationタブのAddPropertyから、RendererTexLayeredAnimationの5つのLayerを追加してください。

image.png

それぞれのLayerに画像を指定してあげます。
1フレーム目には、元の状態の画像を登録してください。
image.png

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

animation.gif

おわりに

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

謝辞

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

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

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3