[Unity] マウス位置のドット絵オブジェクトをピクセル単位で選択＆ハイライト表示

# 経緯
複数のドット絵オブジェクトで構成されたゲームシーンで、マウスでクリックした**指定座標位置にあるオブジェクトを、ピクセル単位で正確に判定したい**と思いました。また判定で見つけたオブジェクトを**ハイライト表示（色フィルターや輪郭を付けたり）したい**と思いました。

![selection-highlight.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/c8e25eb7-50e0-a4fc-e199-229045d2d31e.gif)

これまでの記事で、**[ドット絵を深度バッファ付きで描画](https://qiita.com/Shinoda_Naoki/items/fccdc137747ea4301578)**して、**[Deferredをカスタマイズして昼夜画像をそれぞれ異なるRenderTargetに同時に書き出し](https://qiita.com/Shinoda_Naoki/items/31bde73218b0295b8778)**、夕方アニメーションが出来るようになりました。
この時ついでに、オブジェクトごとに`IDColor`なるものを割り当て、それを別のRenderTargetに書き出して、IDカラーマップ画像を作りました。このIDカラーマップ画像から指定座標の色を読みだして、`IDColor`が一致するオブジェクトを見つければ、**ピクセル単位で正確な判定ができる**はずです。
![gbuffer-rt1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/2642901f-2018-7c53-a0bd-50ea3067d754.jpeg)

## なぜIDカラーマップが必要か？

ご存知の方には今更ですが、Unityでマウスでオブジェクトを選択したい場合などには [Ray/Raycast](https://docs.unity3d.com/ja/2017.4/ScriptReference/Physics.Raycast.html) と[Collider](https://docs.unity3d.com/jp/current/ScriptReference/Collider.html)を使うのが一般的だと思います。
参考：[【Unity】オブジェクトをタッチしたことを判定する方法【2D・3D】](https://pengoya.net/unity/object-touch/)

しかし、この方法だとテクスチャの透過ピクセルを無視できないので、複数のオブジェクトが重なっている場合には正確な判定が出来ません。もしやるなら、Raycastで見つけたオブジェクトから**テクスチャのUV座標を逆算して、その位置のテクスチャーが透過色かどうかを調べる**必要があります。もし透過色だったらさらに奥のオブジェクトにRayを飛ばしていって、**透過色でないテクスチャに当たるか、何も当たらなくなるまで繰り返す**必要がありそうです。

![transpalency_hittest.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/f83af62b-d38a-a0ab-0334-dd73dfd982cf.png)

これはどう考えても面倒くさい実装になりそうだし、テクスチャのピクセルを読むためにはテクスチャを[Rear/Write Enadable](https://docs.unity3d.com/ja/2017.4/Manual/TextureTypes.html)に設定する必要があるし、パフォーマンスも悪そうだと思いました。

先に書いたように、描画オブジェクトごとに`IDColor`を割り当て、それをメインのフラグメントシェーダーで**通常映像と同時に別のRenderTargetに書き出して、IDカラーマップ画像を作れば**、そこから指定座標の色を読み込んで、スクリプト側で`IDColor`に一致するオブジェクトを探す方が、必ず一度で済むし、コードも単純で正確で速い（※計測してないので、憶測ですが）でしょう。

さらにIDカラーマップ画像は、**特定のオブジェクトをハイライト表示**（明るく色をつけたり、輪郭線を付けたり）するときにも使えて一石二鳥です。

# 座標位置のオブジェクト検索
## 実装
IDカラーマップを描きだす部分は、冒頭にもリンクを張った[前回の記事](https://qiita.com/Shinoda_Naoki/items/31bde73218b0295b8778)で詳しく説明してますので、よろしければご覧ください。 Deferred レンダリングをカスタマイズして昼夜画像をそれぞれ異なるRenderTargetに同時に書き出すついでにIDカラーマップも描きだしてます。

### G-Bufferの指定位置の色を取得する
参考までに、**カメラに表示されるシーンの色を取得する**のは、下記リンク先の内容ほぼそのままで出来ました。
[unity　マウスクリックした部分の色を抽出する｜teratail](https://teratail.com/questions/208439)

問題は**どうやって特定の G-Buffer の内容を取り出す**か・・・
ヒントは前回の記事でも引用したこちらの記事にありました。
[UnityのDeferredでCommandBufferを利用してGBufferをいじってみる](https://matcha-choco010.net/2018/09/16/unity%E3%81%AEdeferred%E3%81%A7commandbuffer%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%A6g-buffer%E3%82%92%E3%81%84%E3%81%98%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%8B/)

[CommandBuffer](https://docs.unity3d.com/ja/2017.4/ScriptReference/Rendering.CommandBuffer.html) なるものを使えば、G-Buffer を RenderTarget に指定するのは容易そうです。しかも **[CopyTexture](https://docs.unity3d.com/ja/2017.4/ScriptReference/Rendering.CommandBuffer.CopyTexture.html)** などという、まさにうってつけのメソッドがありました。

二つの記事を参考にできたのがこちらです。

```csharp:CaptureGBuffer.cs
public class CaptureGBuffer : MonoBehaviour
{
    private CommandBuffer buf;

    public Color32 color;
    private Texture2D texture;
    private RenderTexture renderTexture;

    // Start is called before the first frame update
    void Start()
    {
        texture = new Texture2D(1, 1, TextureFormat.RGBAFloat, false);
        renderTexture = new RenderTexture(1, 1, 0, RenderTextureFormat.ARGB32);
        renderTexture.filterMode = FilterMode.Point;

        buf = new CommandBuffer();
        buf.name = "GBuffer Test";
        foreach (var cam in Camera.allCameras)
        {
            if (!cam)
            {
                break;
            }
            cam.AddCommandBuffer(CameraEvent.AfterGBuffer, buf);
        }

#if UNITY_EDITOR
        var sceneViewCameras = SceneView.GetAllSceneCameras();
        foreach (var cam in sceneViewCameras)
        {
            if (!cam)
            {
                break;
            }
            cam.AddCommandBuffer(CameraEvent.AfterGBuffer, buf);
        }
#endif
    }

    Vector2Int getNormalizedMousePos()
    {
        Vector2 pos = Input.mousePosition;        

        float x = Mathf.Clamp(pos.x, 0.0f, Screen.width - 1);
        float y = Mathf.Clamp(pos.y, 0.0f, Screen.height - 1);
        return new Vector2Int((int)x, (int)y);
    }

    // Update is called once per frame
    void Update()
    {
        if (!Input.GetMouseButtonDown(0)) return;

        RenderTexture.active = renderTexture;        
        texture.ReadPixels(new Rect(0, 0, 1, 1), 0, 0);
        color = texture.GetPixel(0, 0);
        Debug.Log($"Color:{color}");
    }
    
    void OnPostRender()
    {
        buf.Clear();

        RenderTargetIdentifier src = new RenderTargetIdentifier(BuiltinRenderTextureType.GBuffer1);
        RenderTargetIdentifier dst = renderTexture;
        Vector2Int pos = getNormalizedMousePos();
        buf.CopyTexture(src, 0, 0, pos.x, pos.y, 1, 1, dst, 0, 0, 0, 0);
    }

    private void OnDestroy()
    {
        renderTexture.Release();
        buf.Release();
    }
}
```

### IDColorに合うオブジェクトを探す

色が取得できたなら後は難しいところはありません。シーン中のゲームオブジェクトから同じIDカラーを持つShedオブジェクトを見つけます。

```csharp:CaptureGBuffer.cs
    void Update()
    {
        if (!Input.GetMouseButtonDown(0)) return;

        RenderTexture.active = renderTexture;        
        texture.ReadPixels(new Rect(0, 0, 1, 1), 0, 0);
        color = texture.GetPixel(0, 0);
        var seleted = FindObjectByIDColor(color);
        Debug.Log($"Color:{color}, Selection:{(seleted != null ? seleted.name:"N/A")}");
        if (seleted == null) return;
        Shader.SetGlobalVector("HighlightID", (Color)color);
    }

    private Shed4Sprite FindObjectByIDColor(Color32 idCol)
    {
        var structures = GameObject.FindObjectsOfType<Shed4Sprite>();
        var found = Array.Find(structures, shed => idCol.Equals((Color32)shed.material.GetColor("_IDColor")));
        return found;
    }
```

本格的に使うなら、毎回全検索せずにハッシュテーブルなどでIDカラーとオブジェクトの紐づけを管理する方がいいのでしょうが、今はとりあえず妥協してます。

ところが、**これでは上手く同じIDカラーを見つけられない場合がある**ことがわかりました。
なぜ見つからないかと言えば、**シェーダーに指定した色と、RenderTarget から取得した色が違う**からで、どうやら**[ガンマ補正](https://docs.unity3d.com/jp/460/Manual/LinearLighting.html)**が影響してることがわかってきました。

そこで、ガンマ補正を元に戻す方法を検討してみました。

### 【没案】IDColor のカラースペース変換を逆変換する

前述の公式ドキュメントを見るとリニアライティングに変更すればいいのかな？とも思いますが、設定が面倒で制約も多そうなので躊躇われます。
そこで、ググって見つけた記事（[※１](https://github.com/EsotericSoftware/spine-runtimes/issues/1301
https://teratail.com/questions/163790)、[※２](https://teratail.com/questions/163790)）を参考に試行錯誤した結果、以下のようにしたら（少なくとも今までうまく一致しなかったケースにおいては）色の違いがなくなりました。

```glsl:DayNightPixelArtsGBuffer.shader
      inline float3 linearToSrgb(float3 c)
      {
          return lerp(c * 12.92, 1.055 * pow(c, 1.0 / 2.4) - 0.055, step(0.0031308, c));
      }

      void frag (in v2f i, out flagout o)
      {
        //..中略..//

        o.gBuffer1 = float4(GammaToLinearSpace(linearToSrgb(_IDColor.rgb)),_IDColor.a);
      }
```
[ファイル全体](https://github.com/ShinodaNaoki/isometricPixelarts/blob/93a55986605ccc611b0bc39abe360430498ee779/Assets/Assets/Shader/DayNightPixelArtsGBuffer.shader)

うーん、なんでガンマからリニアの変換だけじゃ足りないんですかね？**sRGBってなんなの？**なんか直感にそぐわない変換式で困惑しました。
それでなくとも、**IDとしての性質上、少数部を超える誤差は致命的**なので、powとか使ったややこしい計算が**本当にどんな色も誤差なく逆変換できるのか心配**です。

### IDColorをVectorにする（カラースペース変換を完全無効化）

・・・しばらく思い悩んでいたら、はたと気づきました。
そういえば法線マップはVectorを画像化しています。別に**Vectorを色として描きだしてもいい**んじゃないの？そうすれば**カラースペース変換から逃れられるんじゃないの？**

まず、シェーダーのプロパティを Color から Vector に変更しました。

```diff
--- a/Assets/Assets/Shader/DayNightPixelArtsGBuffer.shader
+++ b/Assets/Assets/Shader/DayNightPixelArtsGBuffer.shader
@@ -5,7 +5,7 @@
     [NoScaleOffset] _DayTex ("Day Texture", 2D) = "white" {}
     [NoScaleOffset] _NightTex ("Night Texture", 2D) = "black" {}
     _Transpalent ("Transpalent Color", Color) = (1,0,1,1)
-    _IDColor ("Color fo source object ID", Color) = (0,0,0,0)
+    _IDColor ("Color fo source object ID", Vector) = (0,0,0,0)
   }
   SubShader
   {

```

**視覚的に確認したり、16進数で指定するのに都合がいい**のでゲームオブジェクトにつける Shed コンポーネントに [Color32](https://docs.unity3d.com/ja/2017.4/ScriptReference/Color32.html) 型の IDColor フィールドを追加して、 Start() でシェーダープロパティーを設定するようにしました。

```diff:Shed4Sprite.cs
--- a/Assets/Assets/Scripts/Shed4Sprite.cs
+++ b/Assets/Assets/Scripts/Shed4Sprite.cs
@@ -46,6 +46,8 @@ public class Shed4Sprite : MonoBehaviour //opposite -> courtyard
     // ドット絵を保持するマテリアル
     public Material material;
 
+    public Color32 IDColor;
+
     // マテリアルのメインテクスチャサイズ
     private Vector2Int texSize;
 
@@ -118,6 +120,8 @@ public class Shed4Sprite : MonoBehaviour //opposite -> courtyard
         newMaterial.SetTexture("_MainTex", sprite4Day.texture);
         newMaterial.SetTexture("_DayTex", sprite4Day.texture);
         newMaterial.SetTexture("_NightTex", sprite4Night.texture);
+        Vector4 vect = (Color)IDColor;
+        newMaterial.SetVector("_IDColor", vect);
         GetComponent<MeshFilter>().sharedMesh = mesh;
         GetComponent<MeshRenderer>().material = newMaterial;
     }
```

このクラスはエディタ拡張使ってるので、あわせてエディタクラスも修正（単にフィールド追加しただけ）。

```diff
--- a/Assets/Assets/Scripts/Editor/Shed4SpriteEditor.cs
+++ b/Assets/Assets/Scripts/Editor/Shed4SpriteEditor.cs
@@ -15,6 +15,7 @@ public class Shed4SpriteEditor : Editor
     SerializedProperty pivot;
     SerializedProperty size;
     SerializedProperty autoAdjust;
+    SerializedProperty idColor;
 
     void OnEnable()
     {
@@ -25,6 +26,7 @@ public class Shed4SpriteEditor : Editor
         pivot = serializedObject.FindProperty("pivot");
         size = serializedObject.FindProperty("size");
         autoAdjust = serializedObject.FindProperty("autoSizeAdjust");
+        idColor = serializedObject.FindProperty("IDColor");
     }
 
     public override void OnInspectorGUI()
@@ -42,6 +44,7 @@ public class Shed4SpriteEditor : Editor
         EditorGUILayout.PropertyField(sprite4Day);
         EditorGUILayout.PropertyField(sprite4Night);
         EditorGUILayout.PropertyField(material);
+        EditorGUILayout.PropertyField(idColor);
 
         EditorGUILayout.PropertyField(autoAdjust);
         if (autoAdjust.boolValue)
```

## 結果
`#000001` とか `#FFFFFE` みたいな極端？な値も試してみましたが、うまく判定できてるようです。
![selection_console_log.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/711329c5-9774-8039-45ac-1d271900531d.jpeg)

# 選択したオブジェクトをハイライト表示
次に、選択されたオブジェクトをゲーム画面上で**色や輪郭を付けて、わかりやすく目立たせる**ようにしたいと思います。IDカラーマップを参照して、指定した値に一致する場合にのみピクセルの色を加工したり、境界部分をハイライトカラーで塗りつぶしたりしてみます。

とりあえず、マウスクリックしたオブジェクトをハイライト表示させてみますが、仕組み的には目立たせたいIDカラーをシェーダー渡すだけなので、応用すればゲームイベントが発生してるオブジェクトは別の色でハイライト表示したり（しかも同時に複数選択も）、できるようになるはずです。

## 実装
### 対象のIDColorをシェーダーに渡す

ハイライト効果の描画はビルトインの Deferred Lighting を置き換えたシェーダーでやりますので、通常の方法ではプロパティー設定ができません（少なくとも私が調べた範囲では）。ですのでちょっとオーバヘッドが気になりますが、[SetGlobalVector](https://docs.unity3d.com/jp/460/ScriptReference/Shader.SetGlobalVector.html)で全シェーダーからアクセス可能な変数として登録します。

```diff
--- a/Assets/Assets/Scripts/CaptureGBuffer.cs
+++ b/Assets/Assets/Scripts/CaptureGBuffer.cs
@@ -19,6 +19,7 @@ public class CaptureGBuffer : MonoBehaviour
         texture = new Texture2D(1, 1, TextureFormat.RGBAFloat, false);
         renderTexture = new RenderTexture(1, 1, 0, RenderTextureFormat.ARGB32);
         renderTexture.filterMode = FilterMode.Point;
+        Shader.SetGlobalVector("HighlightID", Color.black);
 
         buf = new CommandBuffer();
         buf.name = "GBuffer Test";
@@ -72,6 +73,7 @@ public class CaptureGBuffer : MonoBehaviour
         var seleted = FindObjectByIDColor(color);
         Debug.Log($"Color:{color}, Selection:{(seleted != null ? seleted.name:"N/A")}");
         if (seleted == null) return;
+        Shader.SetGlobalVector("HighlightID", (Color)color);
     }     
```

[ファイル全体](https://github.com/ShinodaNaoki/isometricPixelarts/blob/a9894150794eb8826f5444aad5ce87e3e615b821/Assets/Assets/Scripts/CaptureGBuffer.cs)

最初の行は、初期化時に無選択状態にリセットするものです。黒`#00000000`はIDカラーとして使わないという自分ルールを設けました。

### シェーダーでハイライト表示

```diff:DayNightPixelArtsLit.shader
 Shader "Hidden/DayNightPixelArtsLit"
 {
+Properties
+{
+  HighlightColor ("Color fo source object ID", Color) = (0,1,1,1)
+}
 SubShader {
 // Pass 1: Lighting pass
 //  LDR case - Lighting encoded into a subtractive ARGB8 buffer
@@ -31,6 +35,9 @@ sampler2D _CameraGBufferTexture2;
 sampler2D _CameraGBufferTexture3;
 sampler2D _DepthTexture;
+ 
+float4 HighlightID;
+half4 HighlightColor;

+
+inline float _idMapEdge(float2 uv, float x, float y) {
+  float2 uv2 = float2(uv.x + x / _ScreenParams.x, uv.y + y / _ScreenParams.y);
+  half4 pix = tex2D (_CameraGBufferTexture1, uv2);
+  return step(length(pix - HighlightID),0.001);
+}
+
+float IsHighlightEdge (float2 uv)
+{
+    float e1 = max(_idMapEdge(uv, +1, 0), _idMapEdge(uv, -1, 0));
+    float e2 = max(_idMapEdge(uv, 0, +1), _idMapEdge(uv, 0, -1));
+    
+    return max(e1, e2);
+}

 half4 CalculateLight (unity_v2f_deferred i)
 {
     float2 uv = i.uv.xy / i.uv.w;
     half4 colDay = tex2D (_CameraGBufferTexture0, uv);
-    half4 normal = tex2D (_CameraGBufferTexture1, uv);
-    half4 idCol = tex2D (_CameraGBufferTexture2, uv);
+    half4 idCol = tex2D (_CameraGBufferTexture1, uv);
+    half4 normal = tex2D (_CameraGBufferTexture2, uv);
     half3 colNight = tex2D (_CameraGBufferTexture3, uv);    
-
-    float edge =  1 - CalcEdgeStrength (colDay.a, uv);   
+    float edge =  1 - CalcEdgeStrength (colDay.a, uv);
     // apply color burn effect to the day color.
     fixed3 colBurn = (1 - _LightColor .rgb) * _LightColor.a;
     colDay = fixed4(colDay.rgb - colBurn, 1) * edge;
-    return half4(lerp(colNight, colDay.rgb, 1 - _LightColor.a), 1);
+    half4 result = half4(lerp(colNight, colDay.rgb, 1 - _LightColor.a), 1);
+    float highlight =  length(idCol - HighlightID) < 0.001 ? 0.25 : IsHighlightEdge (uv);
+    result = lerp(result, HighlightColor, highlight);
+    return result;
 }
 
 #ifdef UNITY_HDR_ON
```

idCol と normal の取得先のGBufferが変わってるのは、元々指定を間違ってただけです（まだ使ってないので気づかなかった）。それ以外の重要なところを以下に説明します。

まず、選択したIDカラーを受け取る `HighlightID` と、ハイライトカラーを設定する `HighlightColor` 変数の追加です。

```glsl:DayNightPixelArtsLit.shader
Properties
{
  HighlightColor ("Color fo source object ID", Color) = (0,1,1,1)
}

float4 HighlightID;
half4 HighlightColor;
```
ビルトインを置き換えたシェーダーのプロパティーをスクリプトから設定する方法はわかりませんが、初期値を入れておくことができるのでプロパティーにも記述してます。
他方 `HighlightID` は SetGlobalVector から設定するので、プロパティーに宣言してはいけません。**プロパティーに宣言した変数は SetGlobalXxx 系メソッドによって反映されませんので注意**。

次にIDカラーマップの指定色との境界検知のメソッドです。 `IsHighlightEdge` は境界の外側1pixel 以内だったら1、それ以外だったら0を返します。

```glsl:DayNightPixelArtsLit.shader
inline float _idMapEdge(float2 uv, float x, float y) {
  float2 uv2 = float2(uv.x + x / _ScreenParams.x, uv.y + y / _ScreenParams.y);
  half4 pix = tex2D (_CameraGBufferTexture1, uv2);
  return step(length(pix - HighlightID),0.001);
}

float IsHighlightEdge (float2 uv)
{
    float e1 = max(_idMapEdge(uv, +1, 0), _idMapEdge(uv, -1, 0));
    float e2 = max(_idMapEdge(uv, 0, +1), _idMapEdge(uv, 0, -1));
    
    return max(e1, e2);
}
```

そして上記メソッドを使って、出力カラーを調整する部分は以下のようになります。

```glsl:DayNightPixelArtsLit.shader
half4 CalculateLight (unity_v2f_deferred i)
{
    // ..前略..

    half4 result = half4(lerp(colNight, colDay.rgb, 1 - _LightColor.a), 1);
    float highlight =  length(idCol - HighlightID) < 0.001 ? 0.25 : IsHighlightEdge (uv);
    result = lerp(result, HighlightColor, highlight);
    return result;
}

```
[ファイル全体](https://github.com/ShinodaNaoki/isometricPixelarts/blob/day-night_deferred_redering/Assets/Assets/Resources/DayNightPixelArtsLit.shader)

`highlight` 変数は `idCol` と `HighlightID` がほぼ同じ（※float演算なので誤差を許容する）なら 0.25、それ以外なら `IsHighlightEdge()` の結果、境界ピクセルなら1、そうでなければ0が入ります。
あとは、`result` にハイライト適用前の出力カラーが履いていますので、 lerp関数を使って`highlight` 変数の値に応じて `HighlightColor` と混ぜ合わせています。 

## 結果
冒頭の画像のようになりました
![selection-highlight.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/c8e25eb7-50e0-a4fc-e199-229045d2d31e.gif)

本来ならテクスチャが透明なだけで、手前に mesh が存在するようなオブジェクト同士の境界付近でも正確に奥のオブジェクトを選択できていますね！

![pixel_selection.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/3d882857-82e7-c47b-044f-2e2f92a91cc8.gif)

# [おまけ] 深度情報でアウトライン(輪郭線)を描く
さて、せっかく深度バッファが手近(?)にあるので、深度バッファを使ってオブジェクトの輪郭を強調してみたいと思います。
以前書いた「[綺麗なアウトラインシェーダーを作る](https://qiita.com/Shinoda_Naoki/items/734fee861fe8abfca228)」の超簡易版ですね。

## 深度情報を複製
[DayNightPixelArtsGBuffer.shader](https://github.com/ShinodaNaoki/isometricPixelarts/blob/day-night_deferred_redering/Assets/Assets/Shader/DayNightPixelArtsGBuffer.shader)でG-Bufferと一緒に深度バッファも書き出してるので、G-Bufferを読みだしてる[DayNightPixelArtsLit.shader](https://github.com/ShinodaNaoki/isometricPixelarts/blob/day-night_deferred_redering/Assets/Assets/Resources/DayNightPixelArtsLit.shader)で同じように深度バッファも読み出せる・・・と思ってましたが甘かったです。

DayNightPixelArtsLit.shader でも**普通に深度バッファにアクセスできるのですが、中身は空っぽになってしまている**（正確にはグレーの小さなテクスチャになってる）ようです。

以前 FrameDebugger で見た時、 CopyDepth っていうシェーダーが挟まってましたが、この前後で失われてしまうようです。
![framedebugger_cpopydepth.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/de038282-c34d-3aa2-8b0e-704f0114169d.jpeg)

CopyDepth シェーダーは置き換え可能なビルトインシェーダーではないし、そもそも何かしら技術的に制約があるからコピーしてるんだろうと思われますから、DayNightPixelArtsGBuffer.shader で書いた深度バッファそのものにアクセスするのは諦めたほうがよさそうです。

そこで、深度バッファは１チャネルあれば十分なので、昼画像用G-Bufferのアルファチャネルを使うことにしました。
こちらは各ゲームオブジェクトのシェーダーです。

```diff:DayNightPixelArtsGBuffer.shader
      void frag (in v2f i, out flagout o)
      {
        // ..中略..


-       o.gBuffer0 = colDay;
+       o.gBuffer0 = fixed4(colDay.rgb, i.position.z);
        o.gBuffer3 = colNight;
        o.gBuffer2 = float4(i.normal, 0) * 0.5 + float4(0.5, 0.5, 0.5, 0);
        o.gBuffer1 = _IDColor;
        o.depth = i.position.z;
      }
```
既存の深度バッファの書き出しに加えて、`gBuffer0` にも `position.z` を書き込んでいます。

## 最終合成(deferred lighting)シェーダー修正

そして、こちらが Deferred Lighting シェーダー側です。境界線を描くということで、上で紹介したハイライト表示の輪郭線の時とよく似たメソッドを追加してます。ただしこちらは 0/1 ではなく smoothstep 関数を利用して、**ある程度深度差に応じた値**を返しています。

```glsl:DayNightPixelArtsLit.shader
inline float _depthEdge(float2 uv, float base, float x, float y) {
  float2 uv2 = float2(uv.x + x / _ScreenParams.x, uv.y + y / _ScreenParams.y);
  half4 pix = tex2D (_CameraGBufferTexture0, uv2);
  return smoothstep(0, 0.1, pix.a - base);
}

float CalcEdgeStrength (float d, float2 uv)
{
    float e1 = max(_depthEdge(uv, d, +1, 0), _depthEdge(uv, d, -1, 0));
    float e2 = max(_depthEdge(uv, d, 0, +1), _depthEdge(uv, d, 0, -1));
    
    return max(e1, e2) * 0.75;
}

half4 CalculateLight (unity_v2f_deferred i)
{
    // ..中略..

    float edge =  1 - CalcEdgeStrength (colDay.a, uv);
    // apply color burn effect to the day color.
    fixed3 colBurn = (1 - _LightColor .rgb) * _LightColor.a;
    colDay = fixed4(colDay.rgb - colBurn, 1) * edge;
    half4 result = half4(lerp(colNight, colDay.rgb, 1 - _LightColor.a), 1);

    // ..中略..

    return result;
}
```
[ファイル全体](https://github.com/ShinodaNaoki/isometricPixelarts/blob/day-night_deferred_redering/Assets/Assets/Resources/DayNightPixelArtsLit.shader)

`CalcEdgeStrength()` を使って現在のピクセルのエッジ強度を `edge` 変数に代入し、`colDay` の[リニア焼きこみ処理](https://qiita.com/Shinoda_Naoki/items/36ad022200616c229936#%E3%83%AA%E3%83%8B%E3%82%A2%E7%84%BC%E3%81%8D%E3%81%93%E3%81%BFlinear-burn)時についでに `edge` をかけて、エッジ強度が強ければ暗くなるようにしてます。


## 結果
ピクセル等倍だとわかり辛いので、ビフォーアフターを切り替えたgifアニメーションさせてみました。控えめながら、建物と建物の境界がより明瞭になったのではないでしょうか。
![outline_blink.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/dedce3d0-ec2d-9c3b-f983-97fb664e1ae4.gif)

**深度差が大きいほどエッジ強度が強くなってる**ということがわかりやすいように、昼画像の代わりに深度＋アウトラインを描きだしてみました。
![depth_edge.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/baeb27a6-6935-4c61-bb9a-326aef3b824d.jpeg)

ところで、カメラには [Quater View](https://github.com/ShinodaNaoki/isometricPixelarts/blob/5a4229fefb7ab563f03d07a7c202fe7b4de7108d/Assets/Assets/Scripts/QuaterView.cs) という自作コンポーネントが付いていて、**１ピクセルを何倍で描画するか指定できる**ようになってるんですが、拡大処理はG-Bufferへの書き出し時に行われるので、深度アウトラインは影響されません。

ですからピクセル倍率を上げても、アウトラインは常に1ピクセル幅で描画されます。
（ちなみにハイライトの輪郭も同じ仕組みなので、常に1ピクセル幅です）

![scale_and_edge_width.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/6b65858a-500d-371b-33c4-091c24fca032.jpeg)
ピクセル倍率:4 での描画結果はこんな感じです。
![scale_and_edge_width_colored.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/3549829b-d8fc-b10f-0c33-8481c9b9f5ee.jpeg)

アウトラインまで4倍になると画面がうるさくなりそうなので、これぐらいで良いかなと思ってます。

## 3Dメッシュとの混成（2020/01/24 追記）
元々この手法のメリットの一つとして **ドット絵と通常の3Dオブジェクトの共存** が挙げられたのですが、今回作成した機能を3Dオブジェクトにも適用できるように、追加拡張を行いました。

DayNightPixelArtsGBuffer.shader をベースに 3D メッシュ用のシェーダーを作ります。主なポイントは、1. 昼用・夜用テクスチャの区別がない事、2. フラグメントシェーダーで **簡易ライティング処理** をしてることです。

```glsl:DayNight3DMeshGBuffer.shader
  Properties
  {
    _MainTex ("Day Texture", 2D) = "white" {}
    _IDColor ("Color for source object ID", Vector) = (0,0,0,0)
  }

  // ...中略...

      sampler2D _MainTex;
      float4 _MainTex_ST;
      float4 _IDColor;

  // ...中略...

      void frag (in v2f i, out flagout o)
      {
         half NdotL = max(0, dot (i.normal, _WorldSpaceLightPos0));
        
        fixed4 colDay = tex2D(_MainTex, i.uv) * lerp(NdotL, 1, 0.75);

        // use 25% brightness of day texture, if night texture is disabled.
        fixed4 colNight = colDay * 0.25;

        o.gBuffer0 = fixed4(colDay.rgb, i.position.z);
        o.gBuffer3 = fixed4(colNight.rgb, 0.1);
        o.gBuffer2 = float4(i.normal, 0) * 0.5 + float4(0.5, 0.5, 0.5, 0);
        o.gBuffer1 = _IDColor;
        o.depth = i.position.z;
      }
```
[ファイル全体](https://github.com/ShinodaNaoki/isometricPixelarts/blob/4d3e993dda80a2fdb6c68f6dc9069c70d982245a/Assets/Assets/Shader/DayNight3DMeshGBuffer.shader)

Deferred レンダリングを独自にカスタマイズしてるので、**本来の Deferred ライティングは適用できません**。フラグメントシェーダー(frag) の冒頭で `NdotL` に照明と法線の内積を計算して、`colDay` に掛けることで、法線ベクトルに応じた簡単な陰影を付けています。
ドット絵と共存させるような3Dオブジェクトに、そこまで高度なライティングは要求されないだろうし、夕焼けアニメーションにいい感じに調和させるためにも、必要な処理です。
なお**陰影は光源ベクトルに依存するので、ドット絵の仮想的な光源方向に合わせる**必要があります。今回は下記のように設定してみました。

![ezgif.com-video-to-gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/ebcdaff4-fe8c-8a4b-db30-413ea11f4da4.gif)![lighting.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/ce8f0716-264d-e28e-f109-af9196b97c4f.jpeg)


一方C#スクリプト側ではIDカラーマップ対応のために、もともと [Shade4Sprite](https://github.com/ShinodaNaoki/isometricPixelarts/blob/4d3e993dda80a2fdb6c68f6dc9069c70d982245a/Assets/Assets/Scripts/Shed4Sprite.cs#L7) にだけあった IDColor を3Dメッシュにも関連付けるための基本クラスを用意します。Shade4Sprite はこのクラスを継承するようにします。

```csharp:HasIDColor.cs
using UnityEngine;

public class HasIDColor : MonoBehaviour
{
    public Color32 IDColor;
}
```

![cube_inspector.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/21903717-b5d3-9f6c-d6fb-e6047c55f0a1.jpeg)![material_inspector.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/8d27d0ff-9c9a-675c-1956-3a1ab050e054.jpeg)


[CaptureGBuffer クラスの FindObjectByIDColor() メソッド](https://github.com/ShinodaNaoki/isometricPixelarts/blob/4d3e993dda80a2fdb6c68f6dc9069c70d982245a/Assets/Assets/Scripts/CaptureGBuffer.cs#L90-L96) では検索対象として Shade4Sprite ではなく HasIDColor クラスを取得するように変更します。

```csharp:CaptureGBuffer.cs
    private HasIDColor FindObjectByIDColor(Color32 idCol)
    {
        var structures = GameObject.FindObjectsOfType<HasIDColor>();

        var found = Array.Find(structures, shed => idCol.Equals(shed.IDColor));
        return found;
    }
```

これで、3DオブジェクトにもIDカラーを割り当て、クリックで選択できるようになりました。
![cube_selection.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/821f9130-3237-e4cb-7080-4bce18021c46.jpeg)


# まとめ
- [前回]Deferred レンダリングのカスタマイズによって、描画時にIDカラーマップを作成した
- IDカラーマップを使って、ピクセル単位で正確なオブジェクト選択ができた
    - ガンマ補正されたくないときはVector型でシェーダーに渡すとよい
- IDカラーマップを使って、特定のオブジェクトをハイライト表示することができた
- ついでに深度エッジで輪郭線描画もできた
    - オリジナルの深度バッファにはアクセスできなかったが、代わりにGBuffer0のアルファチャネルを深度バッファの複製として使った