複数の画像をパラパラマンガのように切り替えてアニメーションしているように見せかける手法を、Unityではスプライトアニメーションといいます。今回は、こちらのシェーダーの作り方についてご紹介しようと思います。
Contents
テクスチャを貼る
スプライトアニメーションをするために、テクスチャをメッシュに張り付ける必要があります。この部分は、UVスクロールを実装する工程とほとんど変わらないので、こちらの記事の前半をご覧ください。
UVを拡大する
次に、メッシュに張り付けたテクスチャを拡大する必要があります。スプライトアニメーションに使用するテクスチャは、同じ解像度の画像が敷き詰められている形になっているので、その一枚分まで拡大して上げる必要があります。
では、どうやってUVを拡大するかというと、行列で乗算します。必要な行列は以下の通りです。
$\begin{pmatrix} x’ \\ y’ \\ \end{pmatrix} = \begin{pmatrix} s_{x} & 0 \\ 0 & s_{y} \\ \end{pmatrix} \begin{pmatrix} x \\ y \\ \end{pmatrix}$
なお、$S_{x}$と$S_{y}$に入る数は、
拡大:$0 < S_{x} < 1$
縮小:$1 < S_{x}$
となっています。スプライトアニメーションで使用するテクスチャは縦横で分割されているので、たとえば4分割されていると、
$\begin{pmatrix} x’ \\ y’ \\ \end{pmatrix} = \begin{pmatrix} \frac{1}{4} & 0 \\ 0 & \frac{1}{4} \\ \end{pmatrix} \begin{pmatrix} x \\ y \\ \end{pmatrix}$
という感じになります。この分割数をインスペクターから設定できるようにしたコードが以下の通りです。
Properties
{
_Divided("Divided", float) = 1
}
float _Divided;
fixed4 frag(v2f i) : SV_Target
{
half2x2 scaleMatrix = half2x2(1 / _Divided, 0, 0, 1 / _Divided);
i.uv = mul(i.uv, scaleMatrix);
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
すると、このように拡大縮小を自在に操作できるようになります。
注意点として、シェーダー(HLSL)における乗算において、*とmul(a, b)は意味が違います。前者は同じ成分同士しか掛け算されません。たとえば、
$v_{A} = \begin{pmatrix} \ A_{11} & A_{12} \\ A_{21} & A_{22} \\ \end{pmatrix}$
$v_{B} = \begin{pmatrix} \ B_{11} & B_{12} \\ B_{21} & B_{22} \\ \end{pmatrix}$
$v_{A} * v_{B} = \begin{pmatrix} \ A_{11} * B_{11} & A_{12} * B_{12} \\ A_{21} * B_{21} & A_{22} * B_{22} \\ \end{pmatrix}$
という計算になります。なので行列積を計算する場合はmulを使うということを覚えておきましょう。
パラパラ切り替わるUVスクロール実装
スプライトアニメーションでは、普通のUVスクロールとは違ったテクスチャ座標の切り替えが必要です。下の画像のように、瞬時に座標が切り替わらないとアニメーションとして成立しません。
このUVの切り替えを実装した部分が、以下のコードです。私はintで割ってしまったのですが、この方法以外にもfloorという関数を使っても同じようなことが再現できます。
float _Divided; //分割数用の変数
int _StartIndex; //スクロールをスタートさせる地点用の変数
fixed4 frag(v2f i) : SV_Target
{
half2x2 scaleMatrix = half2x2(1 / _Divided, 0, 0, 1 / _Divided);
//UVにオフセット値を加算
i.uv += float2(_StartIndex % (int)_Divided, _StartIndex / (int)_Divided);
i.uv = mul(i.uv, scaleMatrix);
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
開始地点を反転する
インスペクターにトグルを表示する
さて、ここまで紹介した機能だけでも十分にスプライトアニメーションとしての要件は満たしているのですが、すこし都合の悪い部分が残っています。それは、テクスチャの中でアニメーションする方向が変えられないという点です。
次は、インスペクターからアニメーション方向を切り替える機能を実装してみます。まず初めに、インスペクターにトグルボタンを表示してみましょう。トグルボタンの表示は、以下のようにプロパティブロックに[Toggle(好きな変数名)]というアトリビュートを追加することで、float型の変数がbool型のように扱われます。
Properties
{
[Toggle(FRIP_START_POS)] _FlipStartPos("Flip Start Pos", float) = 0
}
Pass
{
CGPROGRAM
#pragma shader_feature FRIP_START_POS
------略------
ENDCG
}
トグルによって分岐させる
トグルを表示させたので、このトグルを実際に分岐に使ってみます。一般的なスクリプトだとif()を使うことで分岐させますが、シェーダーにおいては条件付きコンパイルのように書く必要があります。今回は右左の反転しか想定していませんが、同じような方法で上下反転も実装できます。
fixed4 frag(v2f i) : SV_Target
{
#ifdef FRIP_START_POS
i.uv += float2(-(_StartIndex + 1) % (int)_Divided, _StartIndex / (int)_Divided);
#else
i.uv += float2(_StartIndex % (int)_Divided, _StartIndex / (int)_Divided);
#endif
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
アルファが付いている場合
最後にアルファによるカットアウトを実装します。スプライトアニメーションでは、下のようなアルファ付き画像を使うことが多いです。
ここまで紹介したコードには、アルファをくり抜く機能は実装していませんでした。アルファをカットアウトするには、SubShaderブロックにBlendとBlendOpを追加します。
SubShader
{
Tags { "RenderType" = "Transparent" }
LOD 100
Blend SrcAlpha OneMinusSrcAlpha
BlendOp Add
}
シェーダー全体
ここまで紹介したコードをすべてまとめたシェーダーの全容は、以下の通りです。
Shader "CAGraphicsAcademy/SpriteAnimation"
{
Properties
{
_MainTex("Texture", 2D) = "white" {}
_Divided("Divided", float) = 1
_StartIndex("StartIndex", float) = 1
[Toggle(FRIP_START_POS)] _FlipStartPos("Flip Start Pos", float) = 0
}
SubShader
{
Tags { "RenderType" = "Transparent" }
LOD 100
Blend SrcAlpha OneMinusSrcAlpha // アルファ合成
BlendOp Add
Pass
{
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma shader_feature FRIP_START_POS
#include "UnityCG.cginc"
struct appdata
{
float4 vertex : POSITION;
float2 uv : TEXCOORD0;
};
struct v2f
{
float2 uv : TEXCOORD0;
float4 vertex : SV_POSITION;
};
sampler2D _MainTex;
float4 _MainTex_ST;
float4 _MainTex_TexelSize;
float _Divided;
int _StartIndex;
v2f vert(appdata v)
{
v2f o;
o.vertex = UnityObjectToClipPos(v.vertex);
o.uv = TRANSFORM_TEX(v.uv, _MainTex);
return o;
}
fixed4 frag(v2f i) : SV_Target
{
half2x2 scaleMatrix = half2x2(1 / _Divided, 0, 0, 1 / _Divided);
#ifdef FRIP_START_POS
i.uv += float2(-(_StartIndex + 1) % (int)_Divided, _StartIndex / (int)_Divided);
#else
i.uv += float2(_StartIndex % (int)_Divided, _StartIndex / (int)_Divided);
#endif
i.uv = mul(i.uv, scaleMatrix);
fixed4 col = tex2D(_MainTex, i.uv);
return col;
}
ENDCG
}
}
}
まとめ
3Dゲームだとあまり登場シーンがないスプライトアニメーションですが、2Dゲームだとかなり表現の幅を広げてくれる武器となります。そもそも、連番画像を作るのが結構難しいのですが、表現をリッチにする手法として取り入れてみてはいかがでしょうか。