メタボールパズルゲームを作る -メタボール編-
unity1weekお疲れさまでした.今回は私は以下のようなゲームを作成したので遊んだことのない人は是非プレイしてみてください.
ちなみにツイートのいいね数が150を超えていて地味にうれしいです.
こんな感じのゲームを作ったぞ!
あと30分で公開だからよろしくな!
オンライン対戦もできるぞPC、AndroidのChromeでは動作確認済み#unity1week https://t.co/Ma6K1aXMBi pic.twitter.com/bLyVBHX6jG
— さっくん (@mo_takusan9922) 2019年3月17日
本記事ではゲームで使用した主要な技術について軽く解説していきます.なお,サンプルプロジェクトも用意しておいたので気になる方は以下のリンクをどうぞ.
2次元のメタボールを描画する
さて,このゲームで最も特徴的なのはメタボールだと思います.メタボールとは,2次元のグラフィクスにおいては滑らかな曲線を描画するための技術です.ゲームキャラクター(メタボ~ル)はこの技術によってスライムのような滑らかな表現を表すことができるようになっています.
以降はメタボールの数式的な理解,及びシェーダ上での実装を順に見ていきます.
メタボールを数式で表す
まずは,数式を交えながらメタボールの表現方法を述べていきます.
メタボールは一言でいうと,あるピクセル上の点(x,y)において,密度関数f_i(x,y)(i=0,…,N)の重ね合わせが閾値tを超えた際に色を塗り,そうでない場合は地の色を塗ると言った処理を全ピクセルに渡って行う操作によって有機的な形状を描画するグラフィクス技法のことです..
具体例を見てみましょう.N=2とし,密度関数が次のように表されるとします.
\begin{eqnarray} f_0(x,y) &=& \exp(-(x^2+y^2)), f_1(x,y) &=& \exp(-((x+2)^2+y^2)) \end{eqnarray}
f_0をグラフにすると次のようになります.なお,f_1に関しては単にf_0の中心位置を負の方向に2だけ移動したものになります.
分かりやすいように等高線で表すと以下のようになります.
例えば最も濃い部分から3つ目のオレンジ色の部分を閾値tとすれば,このf_0によって円形の画像が描画されることが分かります.
そして,これらの密度関数を重ねあわせると次のようになります.
等高線では以下のように表されます.
もうメタボールが見えましたね.この場合,オレンジ色の部分は次の式を満たしていると言えます.
f_0(x,y) + f_1(x,y) > t
『つながるメタボ~ル』ではキャラクターの位置を中心としたf_0のような関数を定義してやることで滑らかな表現を実現しているのです.
シェーダで実装する
それでは具体的な実装を行っていきます.今回の実装では次の2ステップに分解ができます.
- スプライトの描画
- 全体にイメージエフェクトを適応する
1.のスプライトの描画は前節で言う,密度関数の定義に当たります.スプライトの描画は4点の頂点で正方形を表しその内部に色を載せていきます.すなわち,頂点シェーダで中心位置を移動(実際にはtransformで調整しています)し,フラグメントシェーダで密度関数の本体を書くことになります.
そして,2.の操作は閾値を超えているかどうか判定をする操作に当たります.
それでは実際のプログラムを見ていきましょう.
Shader "Custom/Metaball" | |
{ | |
Properties | |
{ | |
_MainTex ("Texture", 2D) = "white" {} | |
_Strength ("Strength", float) = 1 | |
} | |
SubShader | |
{ | |
Tags | |
{ | |
"Queue" = "Transparent" | |
"IgnoreProjector" = "True" | |
"RenderType" = "Transparent" | |
"PreviewType" = "Plane" | |
"CanUseSpriteAtlas" = "False" | |
} | |
LOD 100 | |
Pass | |
{ | |
Blend One One | |
CGPROGRAM | |
#pragma vertex vert | |
#pragma fragment frag | |
#include "UnityCG.cginc" | |
struct appdata | |
{ | |
float4 vertex : POSITION; | |
float4 color : COLOR; // 頂点カラーで色を指定することで,バッチ数を減らすことが可能 | |
float2 uv : TEXCOORD0; | |
}; | |
struct v2f | |
{ | |
float2 uv : TEXCOORD0; | |
float4 color : COLOR; | |
float4 vertex : SV_POSITION; | |
}; | |
sampler2D _MainTex; | |
float _Strength; | |
v2f vert (appdata v) | |
{ | |
v2f o; | |
o.vertex = UnityObjectToClipPos(v.vertex); | |
o.uv = v.uv; | |
o.color = v.color; | |
return o; | |
} | |
float4 frag (v2f i) : SV_Target | |
{ | |
float4 col = i.color; | |
float2 diff = i.uv - 0.5; | |
col.a = _Strength * pow(2, -20 * length(diff)); | |
col.rgb = col.rgb * col.a; | |
return col; | |
} | |
ENDCG | |
} | |
} | |
} |
長々と書いてありますが,注目すべきは2点のみです.
まずは23行目です.ここでは透明なオブジェクトをどのようにブレンドしていくかを決定します.今回はメタボールの密度値(密度関数からの出力)をα値に格納するため,適切にブレンド方法を決める必要があります.今回の描画では,地の値をそのまま保存しながら描画しなければならないため,One One
で指定します.
そしてもう一つの注目点はフラグメントシェーダです.58行目でスプライトの中心位置からの距離を計算し,59行目で計算した密度関数の出力結果をα値に書き込んでいます.なお,使用する密度関数の形式には特に規定がないため,前節のものとは異なる数式を使用しています(2の累乗数は,プロセッサによっては特殊関数が用意されているためですが,アセンブリが最適化されているかは確認していません).最後に60行目では色をうまくブレンドする際に利用に密度値を参照するために残しておきます.α値に直接密度値を書き込んでいるので必要ないように感じますが,α値は後で上書きしてしまうため,色もうまくブレンドするにはこのようにする必要があるのです.
続いて,イメージエフェクト用のシェーダです.
Shader "Hidden/MetaballEffect" | |
{ | |
Properties | |
{ | |
_MainTex ("Texture", 2D) = "white" {} | |
} | |
SubShader | |
{ | |
// No culling or depth | |
Cull Off ZWrite Off ZTest Always | |
Pass | |
{ | |
CGPROGRAM | |
#pragma vertex vert | |
#pragma fragment frag | |
#include "UnityCG.cginc" | |
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 = v.uv; | |
return o; | |
} | |
sampler2D _MainTex; | |
float _Cut; | |
float4 frag (v2f i) : SV_Target | |
{ | |
float4 col = tex2D(_MainTex, i.uv); | |
if (col.a < _Cut) | |
{ | |
return 0; | |
} | |
col.rgb = col.rgb / col.a; | |
col.a = 1; | |
return col; | |
} | |
ENDCG | |
} | |
} | |
} |
こちらはフラグメントシェーダにのみ注目してください.46行目が核となる部分です._Cut
が閾値を表しこの値を超えないピクセルについては黒く塗りつぶします.
Unityで実装する上での注意
以上でメタボールを描画することができるのですが,一つ注意が必要です.それはUnityは基本的に各色を8bitで表し,0~1を256段階に区切った色しか出力できないという点です.もしも1を超えてしまう場合は,1にクリップされて出力されます.今回算出する密度値は重なりが大きいと1を超えかねないため,何も対策をしないと以下のように一部分が白くなり,光ったような表現になってしまうのです.
これを防ぐためにはおそらくレンダーテクスチャに描画するしかありません(CameraのAllow HDR
をtrue
にするだけで良さそうですがうまいこといきませんでした).レンダーテクスチャでは色のフォーマットを8bitだけではなく,16bitまたは32bitにすることができます.これによって,値がクリップされることをできるだけ防止することができます.したがって,実際に試してみたい方は次のようにレンダーテクスチャを作成し,カメラの描画先をレンダーテクスチャに設定してください.
レンダーテクスチャを表示するためにはuGUIのRawImageを使用します.
すると,次のようにきれいなメタボールを描画できるというわけです.
最後に
以上でUnity上でのメタボールの実装は完了です.『つながるメタボ~ル』の実装には他にも様々な要素がありますが,今回は疲れたのでここで終了にします.
最後まで読んでいただき,ありがとうございました.また,実装についてアドバイスをして頂いたchokopan先輩,ありがとうございました.
気が向いたらphotonについての記事も書いていこうと思いますのでその時はまたよろしくお願いします.