Unityでシェーダーを使ってミニマップを自動生成する

# はじめに

ステージ内を自由に動き回れるようなゲームでは大体プレイヤーがステージのどこにいるのかを表す、ステージを俯瞰したマップが付いています。
カービーのエアライドのこれみたいな。
![kirby_airride_map.jpg](https://qiita-image-store.s3.amazonaws.com/0/76085/aed0a7f1-4237-96a4-c527-3ec5f82713b3.jpeg)

こんな感じのステージ俯瞰マップが作りたくありませんか？
私は作りたいと思ったので作りました。のでその作り方を書いていきます。

# ミニマップ(ステージ俯瞰マップ)

ステージ俯瞰マップと呼ぶのも面倒くさいので、以降ミニマップと呼ぶことにします。
ミニマップを作成するには、当然ながらそのベースとなるステージの俯瞰画像が必要です。
俯瞰画像を作成する手法として確実なのは、手動でマップ画像を作成することでしょう。見栄えや細部の調整のしやすさ的には一番良さそうです。しかし、ステージの形状が変わるたびにステージごとにマップ画像を作成するのは大変です。現在のステージ形状に合わせて自動でマップ画像が生成されればそれに越したことはありません。

マップ画像の自動生成方法として一番簡単なのは、メインカメラとは別にマップ生成用にカメラを置き、そのカメラでステージを上から見たものをマップとして使う方法です。
しかしステージを単純に上から見たものをマップとして使うこの方法は、ステージの立体構造が全く反映されないので分かりづらいです。

例えばこんな構造のステージがあったとして
![stage.jpg](https://qiita-image-store.s3.amazonaws.com/0/76085/8539fe0e-fc5a-5881-a823-3448f4325d44.jpeg)

単純に俯瞰カメラから得た画像は以下のようになります。
![map1.jpg](https://qiita-image-store.s3.amazonaws.com/0/76085/b35aa29d-5a9b-461f-0d9f-c115cee525b9.jpeg)

水色の立体的に交差している部分が、この画像では同じ色で重なってしまい分かりません。
こんな感じで、立体構造も付加した画像を生成すれば少し分かりやすくなります。
![map2.jpg](https://qiita-image-store.s3.amazonaws.com/0/76085/d5f336d5-dd0f-bfa4-7f86-735831b4c19d.jpeg)

そこでこの記事では以下の三つを目的として、俯瞰マップ自動生成の方法を考えてみます。

- 自動生成：カメラとシェーダーを使ってマップ画像を生成する
- ステージの全景を反映：ステージを俯瞰した状態の色合いを反映する
- ステージの立体構造を反映：ステージの立体構造を傾きで表示する

# マップ画像生成シェーダー

ステージ俯瞰画像を作成するにあたり、今回はマップ用カメラを用意してマップ画像を生成します。そのうえで上記の目的を果たすためにシェーダーを使用し、深度テクスチャで傾斜を求めてカメラ画像に合成します。

まずシェーダーのコードはこんな感じです。このコードはbokkuri_orzさんの記事[【Unity3D】 デプスバッファを利用する](https://qiita.com/bokkuri_orz/items/08cbaeae6a34fed7f903)を参考に、今回の目的に合わせて作成しました。

```cpp:Minimap.shader
Shader "Custom/Minimap" {
	Properties {
		_MainTex ("Main Texture", 2D) = "white" {}
		_MainTexRatio("Main Texture Ratio", Range(0, 1)) = 1
		_BaseColor ("Base Color", Color) = (0,0,0,1)
		_MapColor ("Map Color", Color) = (1,1,1,1)
		_MapMagnification ("Map Magnification", Range(0.1, 100000)) = 10000
		_Thick("Thick", Range(0.1, 5)) = 1
	}

	SubShader {
		Tags { "RenderType"="Opaque" }
		ZTest Off
		ZWrite Off
		Lighting Off
		AlphaTest Off

		Pass {
			CGPROGRAM
			#pragma vertex vert
			#pragma fragment frag
			#include "UnityCG.cginc"

			sampler2D _MainTex;
			float _MainTexRatio;
			float4  _MainTex_ST;
			float _MapMagnification;
			float4 _MapColor;
			float4 _BaseColor;
			float _Thick;

			sampler2D _CameraDepthTexture;
			float4  _CameraDepthTexture_TexelSize;

			struct appdata_t {
				float4 vertex : POSITION;
				float2 texcoord : TEXCOORD0;
			};
			struct v2f {
				float4 vertex : SV_POSITION;
				half2 texcoord : TEXCOORD0;
			};

			v2f vert(appdata_t v)
			{
				v2f o;
				o.vertex = UnityObjectToClipPos(v.vertex);
				o.texcoord = TRANSFORM_TEX(v.texcoord, _MainTex);
				return o;
			}

			fixed4 frag(v2f input) : Color
			{
				float tx = _CameraDepthTexture_TexelSize.x * _Thick;
				float ty = _CameraDepthTexture_TexelSize.y * _Thick;

				float col00 = Linear01Depth(tex2D(_CameraDepthTexture, input.texcoord + half2(-tx, -ty)).r);
				float col10 = Linear01Depth(tex2D(_CameraDepthTexture, input.texcoord + half2(  0, -ty)).r);
				float col01 = Linear01Depth(tex2D(_CameraDepthTexture, input.texcoord + half2(-tx,   0)).r);
				float col11 = Linear01Depth(tex2D(_CameraDepthTexture, input.texcoord + half2(  0,   0)).r);
				float val = (abs(col00 - col11) + abs(col10 - col01))/2;

				float4 main_col = tex2D(_MainTex, input.texcoord);
				float map_ratio = min(val*_MapMagnification,1);
				float4 map_color = _MapColor * map_ratio;				
				map_color.a = 1;
				fixed4 col = (_BaseColor*(1 - _MainTexRatio) + main_col*_MainTexRatio)*(1 - map_ratio) + map_color*map_ratio;
				return col;
			}
			ENDCG
		}
	} 
}
```

[元記事](https://qiita.com/bokkuri_orz/items/08cbaeae6a34fed7f903)からの変更点は、輪郭だけでなく傾斜を描画するようにした点、シェーダー内でカメラ画像との合成を行うようにした点です。

frag関数内ではカメラから得た画像に深度マップを使用してその一の傾きの情報を追加しています。傾きはその近隣の深度の変化から近似します。
具体的には、現在の位置、その左、左上、上のピクセルから成る正方形を考え、その対角線の両端に位置する頂点での深度の差の平均を傾斜として扱います。コード中の以下の部分です。

```c++
float tx = _CameraDepthTexture_TexelSize.x * _Thick;
float ty = _CameraDepthTexture_TexelSize.y * _Thick;

float col00 = Linear01Depth(tex2D(_CameraDepthTexture, input.texcoord + half2(-tx, -ty)).r);
float col10 = Linear01Depth(tex2D(_CameraDepthTexture, input.texcoord + half2(  0, -ty)).r);
float col01 = Linear01Depth(tex2D(_CameraDepthTexture, input.texcoord + half2(-tx,   0)).r);
float col11 = Linear01Depth(tex2D(_CameraDepthTexture, input.texcoord + half2(  0,   0)).r);
float val = (abs(col00 - col11) + abs(col10 - col01))/2;
```

[元記事](https://qiita.com/bokkuri_orz/items/08cbaeae6a34fed7f903)では輪郭を求めることが目的だったため閾値以上の傾きがある場所に輪郭を描画していましたが、今回は傾きの大きさ自体を表示するのが目的のため、求めた値の絶対値を使用し、その絶対値の大きさに応じて色を付けます。
それを指定された比率で実際のカメラから得られた描画色と混合し、マップ画像を生成します。

インスペクタで設定できるシェーダーのプロパティは以下の通りです。

- Main Texture: 入力となるカメラ画像
- Main Texture Ratio: マップにカメラ画像を混合する比率
- Base Color: マップに加算される基本色
- Map Color: ステージの傾斜や輪郭などの色
- Map Magnification: ステージの傾斜や輪郭の強調度合い
- Thick: 輪郭の太さ

# マップ用カメラスクリプト

次は、上記のシェーダーからマップ画像を生成するためのカメラにアタッチするスクリプトです。

```cs:MinimapCamera.cs
using System.Collections;
using UnityEngine;

#if UNITY_EDITOR
using UnityEditor;
#endif

/// <summary>
/// ミニマップ作成用カメラ。
/// パフォーマンス改善のために、普段はCameraをDisableにして描画を行っていない。
/// </summary>
[ExecuteInEditMode]
public class MinimapCamera : MonoBehaviour {
	/// <summary>
	/// 自分のカメラを取得する
	/// </summary>
	public Camera myCamera {
		get {
			if (!_myCamera) {
				_myCamera = GetComponent<Camera>();
			}
			return _myCamera;
		}
	}

	/// <summary>
	/// ミニマップテクスチャを更新する
	/// 1フレームだけ自分をカメラにしてカメラからテクスチャに書き込む
	/// </summary>
	public void UpdateMinimapTexture() {
		//エディター上でEditModeの時は無効化しない
#if UNITY_EDITOR
		if (!EditorApplication.isPlaying) { return; }
#endif
		if (gameObject.activeInHierarchy) {
			myCamera.enabled = true;
			StartCoroutine(CrtDisableCameraAfterAFrame());
		}
	}

	[SerializeField, Tooltip("ミニマップに適用するマテリアル")]
	private Material _minimapMaterial;

	private Camera _myCamera;

	private void Awake() {
		myCamera.depthTextureMode = DepthTextureMode.Depth;
	}

	private void OnEnable() {
		UpdateMinimapTexture();
	}

	private void OnRenderImage(RenderTexture source, RenderTexture destination) {
		Graphics.Blit(source, destination, _minimapMaterial);
	}

	/// <summary>
	/// 1フレーム後にカメラを無効にする
	/// </summary>
	/// <returns></returns>
	private IEnumerator CrtDisableCameraAfterAFrame() {
		yield return null;
		myCamera.enabled = false;
	}
}
```

まず、カメラの初期化としてミニマップ用カメラで深度テクスチャを使用する設定を行います。これにより、シェーダー内で_CameraDepthTextureとそれに関連する変数が使用可能になります。

```cs
private void Awake() {
	myCamera.depthTextureMode = DepthTextureMode.Depth;
}
```

次に描画です。
描画はのUnityコールバックメソッドである[OnRenderImageメソッド](https://docs.unity3d.com/jp/current/ScriptReference/Camera.OnRenderImage.html)で行います。
このメソッドはカメラにアタッチされたスクリプトにおいて描画が行われた直後に呼び出されるメソッドで、カメラの描画における動作をカスタマイズすることができます。

今回はミニマップシェーダーが設定されたマテリアルを使用して、マップカメラの画像からマップ画像を生成します。
マップの描画には[Graphics.Blitメソッド](https://docs.unity3d.com/ja/2018.2/ScriptReference/Graphics.Blit.html)を使用します。このメソッドは指定されたマテリアルを使用して、sourceとして指定されたテクスチャに描画を行いdestinationとして指定されたテクスチャにコピーする関数です。

```cs
private void OnRenderImage(RenderTexture source, RenderTexture destination) {
	Graphics.Blit(source, destination, _minimapMaterial);
}
```

あとこれは必須ではありませんが、ステージの形状が変化しないのなら常にマップの描画を行うのはパフォーマンス的に無駄です。
マップカメラが生成されてすぐにマップ画像の生成を行い、その後はカメラを無効化しておくことで描画を一回で済ますことができます。
具体的にはOnEnable時に描画を行います。OnEnable時にカメラを有効化し、1フレーム後にまた無効化しています。ステージの形状がプレイ中にどんどん変わっていくような場合にはこの処理を省いて、随時、または一定間隔で描画するようにするのが良いと思います。
また、エディタ上ですぐに最新のミニマップが確認できるよう、ExecuteInEditMode属性をつけています。

# マップ用カメラ設定

最後にミニマップ用カメラの設定です。

まず描画先となるRenderTextureを作成します。RenderTextureはプロジェクトウインドウ内の右クリックメニューで「Create->Render texture」を選択して作成できます。
作成したRender Textureは以下のような設定にします。Sizeを作成したいマップ画像のサイズに設定してください。
![render_texture.jpg](https://qiita-image-store.s3.amazonaws.com/0/76085/6c122f8e-7cd1-54a1-135d-da58460e2f14.jpeg)

次にマテリアルです。
今回は作成したMinimapシェーダーを用いて描画を行うため、シェーダーとしてMinimapシェーダーを選択します。
Main Textureにはカメラによる描画結果がスクリプト内で渡されるため、ここで設定する必要はありません。
その他のパラメータはそれぞれ好みで調整してください。
![material.jpg](https://qiita-image-store.s3.amazonaws.com/0/76085/a548a4d4-eac7-3d12-7f27-ba7a264d3782.jpeg)

そして、ミニマップ用カメラをシーン内に作成し、以下の設定を行います。

1. 上に書いたミニマップカメラ用コンポーネント(MinimapCamera)をアタッチする
2. MinimapCameraコンポーネントのMinimap Materialプロパティに作成したミニマップ用マテリアルを設定する
3. CameraコンポーネントProjevtionをOrthgraphicに設定する
4. カメラコンポーネントのTarget Textureプロパティに作成したRender Textureを設定する
5. カメラの位置と向き、CameraコンポーネントのSizeプロパティを、ステージがちょうど入るように調整する
6. 地形や地面オブジェクトに専用のレイヤー(Groundなど)を割り当てる
7. CameraコンポーネントのCulling Maskプロパティで地面レイヤーのみを対象に設定する

3は、マップに不必要な遠近感を出さないための設定です。7は、マップにステージ以外のオブジェクトを表示しないための設定です。
結果として以下のような状態になります。
![minimap_camera.jpg](https://qiita-image-store.s3.amazonaws.com/0/76085/992ff910-e53c-795d-354d-7c633fa5c1b0.jpeg)

これにより作成したRender Textureに初めに張った俯瞰マップ画像が描画されます。
![map2.jpg](https://qiita-image-store.s3.amazonaws.com/0/76085/d5f336d5-dd0f-bfa4-7f86-735831b4c19d.jpeg)

これをuGUIに張り付けることでミニマップを作成することができます。

# 最後に

この記事ではミニマップ画像生成までしか行っていませんが、これにプレイヤーの位置やゲーム内で重要なオブジェクトのマーカーを配置することで、自分のいる位置や重要なオブジェクトの位置が分かるようになります。私が現在作成しているゲーム内での使用例がこんな感じです。
ステージの俯瞰画像にプレイヤーと重要オブジェクトの位置を示すマーカーを配置しています。
![ctm_map.jpg](https://qiita-image-store.s3.amazonaws.com/0/76085/2c5bc608-bec1-b825-0234-969c2bb513c8.jpeg)

今回は深度テクスチャから傾斜を求めマップに合成しましたが、ここで紹介した実装だと傾斜が同じなのにもかかわらず意図せず色に違いができている部分があります。実際の傾斜が同じでも深度が異なると違う傾斜の値が求まっているようで、Linear01Depthで求めた値がカメラからの距離に対して一次関数の関係になっているわけではないことが原因のようです。
[リファレンス](https://docs.unity3d.com/ja/current/Manual/SL-DepthTextures.html)にはLinear01Depthは「直線デプス」を返すと書いてあったので、てっきりカメラからの距離と戻り値の深度値は一次関数の関係になっていると思っていました。

私の使い方の間違いなのか、解釈の間違いでそういう仕様なのか、Linear01Depthの問題以前に私の計算式の間違いなのかわからないので、解決出来たら更新しようと思います。

シェーダーに関して理解不足な点があるかもしれないので、もし間違っている点やより良い方法などがあればコメントで指摘していただけると助かります。

# 参考文献

- [LIGHT11「【Unity】【シェーダ】カメラからの深度情報を用いてアウトラインを描画する」](http://light11.hatenadiary.com/entry/2018/05/08/215519)
- [LIGHT11「【Unity】【シェーダ】カメラから見た深度を描画する」](http://light11.hatenadiary.com/entry/2018/05/08/012149)
- [Qiita「【Unity3D】 デプスバッファを利用する」](https://qiita.com/bokkuri_orz/items/08cbaeae6a34fed7f903)
- [Unityスクリプトリファレンス「Camera.OnRenderImage」](https://docs.unity3d.com/ja/current/Manual/SL-DepthTextures.html)
- [Unityスクリプトリファレンス「Depth Texture の使用」](https://docs.unity3d.com/jp/current/ScriptReference/Camera.OnRenderImage.html)
- [Unityスクリプトリファレンス「Graphics.Bilt」](https://docs.unity3d.com/ja/current/ScriptReference/Graphics.Blit.html)
- [凹みTips「Unity のソフトパーティクルのシェーダについて調べてみた」](http://tips.hecomi.com/entry/2018/09/15/014050)