14
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-12-22

経緯

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

selection-highlight.gif

これまでの記事で、ドット絵を深度バッファ付きで描画して、Deferredをカスタマイズして昼夜画像をそれぞれ異なるRenderTargetに同時に書き出し、夕方アニメーションが出来るようになりました。
この時ついでに、オブジェクトごとにIDColorなるものを割り当て、それを別のRenderTargetに書き出して、IDカラーマップ画像を作りました。このIDカラーマップ画像から指定座標の色を読みだして、IDColorが一致するオブジェクトを見つければ、ピクセル単位で正確な判定ができるはずです。
gbuffer-rt1.jpg

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

ご存知の方には今更ですが、Unityでマウスでオブジェクトを選択したい場合などには Ray/RaycastColliderを使うのが一般的だと思います。
参考:【Unity】オブジェクトをタッチしたことを判定する方法【2D・3D】

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

transpalency_hittest.png

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

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

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

座標位置のオブジェクト検索

実装

IDカラーマップを描きだす部分は、冒頭にもリンクを張った前回の記事で詳しく説明してますので、よろしければご覧ください。 Deferred レンダリングをカスタマイズして昼夜画像をそれぞれ異なるRenderTargetに同時に書き出すついでにIDカラーマップも描きだしてます。

G-Bufferの指定位置の色を取得する

参考までに、カメラに表示されるシーンの色を取得するのは、下記リンク先の内容ほぼそのままで出来ました。
unity マウスクリックした部分の色を抽出する|teratail

問題はどうやって特定の G-Buffer の内容を取り出すか・・・
ヒントは前回の記事でも引用したこちらの記事にありました。
UnityのDeferredでCommandBufferを利用してGBufferをいじってみる

CommandBuffer なるものを使えば、G-Buffer を RenderTarget に指定するのは容易そうです。しかも CopyTexture などという、まさにうってつけのメソッドがありました。

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

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オブジェクトを見つけます。

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 から取得した色が違うからで、どうやら**ガンマ補正**が影響してることがわかってきました。

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

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

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

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);
      }

ファイル全体

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

IDColorをVectorにする(カラースペース変換を完全無効化)

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

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

--- 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 型の IDColor フィールドを追加して、 Start() でシェーダープロパティーを設定するようにしました。

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;
     }

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

--- 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

選択したオブジェクトをハイライト表示

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

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

実装

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

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

--- 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);
     }     

ファイル全体

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

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

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 変数の追加です。

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を返します。

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);
}

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

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;
}

ファイル全体

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

結果

冒頭の画像のようになりました
selection-highlight.gif

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

pixel_selection.gif

[おまけ] 深度情報でアウトライン(輪郭線)を描く

さて、せっかく深度バッファが手近(?)にあるので、深度バッファを使ってオブジェクトの輪郭を強調してみたいと思います。
以前書いた「綺麗なアウトラインシェーダーを作る」の超簡易版ですね。

深度情報を複製

DayNightPixelArtsGBuffer.shaderでG-Bufferと一緒に深度バッファも書き出してるので、G-Bufferを読みだしてるDayNightPixelArtsLit.shaderで同じように深度バッファも読み出せる・・・と思ってましたが甘かったです。

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

以前 FrameDebugger で見た時、 CopyDepth っていうシェーダーが挟まってましたが、この前後で失われてしまうようです。
framedebugger_cpopydepth.jpg

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

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

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 関数を利用して、ある程度深度差に応じた値を返しています。

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;
}

ファイル全体

CalcEdgeStrength() を使って現在のピクセルのエッジ強度を edge 変数に代入し、colDayリニア焼きこみ処理時についでに edge をかけて、エッジ強度が強ければ暗くなるようにしてます。

結果

ピクセル等倍だとわかり辛いので、ビフォーアフターを切り替えたgifアニメーションさせてみました。控えめながら、建物と建物の境界がより明瞭になったのではないでしょうか。
outline_blink.gif

深度差が大きいほどエッジ強度が強くなってるということがわかりやすいように、昼画像の代わりに深度+アウトラインを描きだしてみました。
depth_edge.jpg

ところで、カメラには Quater View という自作コンポーネントが付いていて、1ピクセルを何倍で描画するか指定できるようになってるんですが、拡大処理はG-Bufferへの書き出し時に行われるので、深度アウトラインは影響されません。

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

scale_and_edge_width.jpg
ピクセル倍率:4 での描画結果はこんな感じです。
scale_and_edge_width_colored.jpg

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

3Dメッシュとの混成(2020/01/24 追記)

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

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

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;
      }

ファイル全体

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

ezgif.com-video-to-gif.giflighting.jpg

一方C#スクリプト側ではIDカラーマップ対応のために、もともと Shade4Sprite にだけあった IDColor を3Dメッシュにも関連付けるための基本クラスを用意します。Shade4Sprite はこのクラスを継承するようにします。

HasIDColor.cs
using UnityEngine;

public class HasIDColor : MonoBehaviour
{
    public Color32 IDColor;
}

cube_inspector.jpgmaterial_inspector.jpg

CaptureGBuffer クラスの FindObjectByIDColor() メソッド では検索対象として Shade4Sprite ではなく HasIDColor クラスを取得するように変更します。

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

まとめ

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?