なんの記事?
複数パーツのSpriteで構成されたキャラクターを透過した時の問題と解消法についての記事です!
複数のSpriteでキャラクターを表現する場合、Rendererが重なることになるので、
透過をかけたときに重なったところが変に表示されてしまいます。
今回は、この問題を解消しつつ、更に各パーツをアニメーションさせる方法についての記事です。
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を作りましょう
InspcterからShaderを指定して、各Layerに画像を貼り付けてください。
Layer2~5の画像の描画位置は、Layer1のサイズを基準に取っています。
画像は、全て同じピクセル数で描き出して、不要部分は透過してください。
Rendererを用意して、半透明にしてみる
さっき作ったマテリアルをattachしてみましょう。
今回はMeshRendererを使用しています。
3DObject>Planeなどを作成して、回転させて使ってください。
(私はRotation x:90 y:180で使っています。スケールはxとzで調節します。)
Sprite Rendererでも同じ設定で動作します。
万一位置ずれなどが起きる場合は画像のImport Setting > Mesh TypeをFull Rectにしてみてください。
Sceneビューに5つのレイヤーの重なった像が表示されているかと思います。
Materialの表示欄からカラーの透明度を変更すると、全体が半透明になっていきます!
一旦ここまでで成功です! 続いてアニメーション周りの設定をしていきます。
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を作っておきましょう。
Animationを作ってみる
AnimationタブのAddPropertyから、RendererTexLayeredAnimation
の5つのLayerを追加してください。
それぞれのLayerに画像を指定してあげます。
1フレーム目には、元の状態の画像を登録してください。
全て登録すると、個別パーツをアニメーションさせながら、半透明が表現できる仕組みになりました!!
おわりに
今回は、2Dゲームを作る上で地味に苦戦した部分を記事にしてみました!!
実測はしていませんが、Rendererをまとめて、GPU内で処理を完結させるため、
おそらく重ね描画よりも軽くなると思います!
謝辞
偽典オーさまより素材をお借りました。ありがとうございます!
http://albireo.watson.jp/ukgk/freeshell.html
また、問題の解決にあたって、Unityゲーム開発者ギルドの皆様にたくさんのアドバイスを頂きました!
ありがとうございました!!