[Unity] クォータービューのドット絵に深度バッファを適用する（重なり順解決）

# 前置き
![withTerrain.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/cbaed478-af0f-1a2f-a4ea-4257e02e86cf.jpeg)![sceneViewAni.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/81f7e3a2-8375-16ff-3c6d-400acb94ea76.gif)

**クオータービュー**ゲームってご存知ですか？
自分はあんまり最近のゲーム知らなくて、古い例しか出てきませんが、「A列車で行こう4」とか「SimCity 2000」のような、**等角投影図風に描いたドット絵**を配置したゲームのことです。見下ろし型とか、2.5Dとか、鳥瞰図なんて言われる場合もありますね（これらは必ずしもクオータービューだけを指す言葉じゃないですが）。英語だと isometric view と呼ぶようです。

3Dのような奥行きのある絵ですが、実体は二次元（Sprite）なので**描画順(z-オーダー)**で重なりを制御する必要があります。
## 唐突ですがここで問題。

![greenFrame.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/736140d6-42d8-883c-f4af-d0f51d8628d3.png)![orangeSteps.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/07fd38c7-b58e-50fc-c7f9-692c4a6fa991.png)![purpleCargo.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/959d26cf-593d-6d36-4d1a-2b94cc00ec19.png)

上に挙げた**緑**、**橙**、**紫**のドット絵それぞれから３つの Sprite を作って、下の画像を作るにはどういう描画順で描画すればいいでしょうか？

![Image10000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/a729f893-4430-f5e9-e54f-870e80ba1eb0.png)

答えは、「（※この問題の前提では）**不可能**」です。
それぞれの重なってる部分を見てください。**橙＞紫**、**紫＞緑**、**緑＞橙**となるようなｚオーダーの組み合わせは作れません。方程式で言うなら「解なし」です。

こういう絵を作りたいときに、他の人たちはどうやっているかというと、**Sprite をタイルなどに分割して描く**（もちろん、その時分割部分ごとに適切な順番になるようにする）のが一般的のようです。

https://gamedevelopment.tutsplus.com/tutorials/creating-isometric-worlds-a-primer-for-game-developers--gamedev-6511
https://blog.pocketcitygame.com/cheating-at-z-depth-sprite-sorting-in-an-isometric-game/
http://ashley.baldock.me/tilecutter

# やりたいこと
クオータービューのドット絵ゲームを作るとき、 **スプライトを分割せずに重なり順を解決したい**。そのためにドット絵に深度バッファを適用したい。
そこで下記の図のように3Dオブジェクトにドット絵を正射影表示してやろうと思いました。
![](http://blog-imgs-54.fc2.com/q/o/o/qoofast/cubeprojectionmap.gif)
これができれば、重なり順解決だけでなく他の**3Dオブジェクト（例えば Terrain）とドット絵を上手い具合に共存させる**こともできます。

実は Unity ではないですが、既に[**このアイデアを実現してる記事**](http://qoofast.blog76.fc2.com/blog-entry-23.html)があります。上記の図もそこからの拝借です。今回はこれを Unity に移植してみました。

# テクスチャを立体に正射影する
## マテリアル
![materialG01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/d945ac71-7911-a4a8-7e21-647a9ec48e2a.jpeg)


Unity使ってる諸氏には釈迦に説法かもしれませんが、**テクスチャを2の累乗サイズに調整**しないといけません。普段はUnityが勝手にやってくれるのですが、それに任せてるとテクスチャ上の**ドット絵の切り出し位置指定がややこしくなる**ので、それを避けるためです。
![resizedGreen.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/461c5382-d1e6-b829-dbfa-a2ee1112cc70.jpeg)![resizedOrange.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/69751e95-683d-8af0-a54c-52538a8aa546.jpeg)![resizedPurple.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/d20c7b83-3ba9-f694-1108-69d5e91e7f18.jpeg)

シェーダーは透過ができてUnlitでTextureを指定できればよいので、 Sprite/Default か UI/Default を使えます。
ですが、将来の拡張を見越して、一応独自のシェーダーを用意しておきました。

```GLSL:PseudoSprite.shader
Shader "Custom/SeudoSprite"
{
    Properties
    {
        _MainTex ("Texture", 2D) = "white" {}
    }
    SubShader
    {
        Tags { "RenderType"="Opaque" }
        LOD 100

        Pass
        {
            CGPROGRAM
            #pragma vertex vert
            #pragma fragment frag
            // make fog work
            #pragma multi_compile_fog

            #include "UnityCG.cginc"

            struct appdata
            {
                float4 vertex : POSITION;
                float2 uv : TEXCOORD0;
            };

            struct v2f
            {
                float2 uv : TEXCOORD0;
                UNITY_FOG_COORDS(1)
                float4 vertex : SV_POSITION;
            };

            sampler2D _MainTex;
            float4 _MainTex_ST;

            v2f vert (appdata v)
            {
                v2f o;
                o.vertex = UnityObjectToClipPos(v.vertex);
                o.uv = TRANSFORM_TEX(v.uv, _MainTex);
                UNITY_TRANSFER_FOG(o,o.vertex);
                return o;
            }

            fixed4 frag (v2f i) : SV_Target
            {
                // sample the texture
                fixed4 col = tex2D(_MainTex, i.uv);
                if(col.a <= 0.2) discard;
                // apply fog
                UNITY_APPLY_FOG(i.fogCoord, col);
                return col;
            }
            ENDCG
        }
    }
}
```
テンプレで生成されるUnlitシェーダーに透過処理を付け加えただけの単純なものです。

```GLSL
    if(col.a <= 0.2) discard;
```

## メッシュ生成コンポーネント
スプライトに相当する役割のオブジェクトとして、メッシュ生成用コンポーネントをEmptyなgameObjectにつけて使います。
ちなみに Shed （シェド）は小屋という意味で名づけました。
![prefabG01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/a22c2833-aa3a-d24d-cc1d-2c36e1a94575.jpeg)
上の図のように Mesh Renderer, Mesh Filter も追加してください。設定を弄る必要はありません。

以下ソース

```csharp:Shed.cs
#if UNITY_EDITOR
using UnityEditor;
#endif
using UnityEngine;

[ExecuteInEditMode]
public class Shed : MonoBehaviour
{
    private bool needRestruct = true;

#if UNITY_EDITOR
    void OnEnable()
    {
        SceneView.onSceneGUIDelegate += this.OnEditorUpdate;
        needRestruct = true;
    }

    void OnDisable()
    {
        SceneView.onSceneGUIDelegate -= this.OnEditorUpdate;
    }

    void OnEditorUpdate(SceneView vw)
    {
        Start();
    }
#endif

    [SerializeField, Header("ドット絵の3Dサイズ")]
    private Vector3Int size;

    [SerializeField, Header("テクスチャの最手前座標")]
    private Vector2Int pivot;

    // ドット絵を保持するマテリアル
    public Material material;

    // マテリアルのメインテクスチャサイズ
    private Vector2Int texSize;

    private void Awake()
    {
        // なぜか Start/Awake の両方から Mesh初期化しないと、シーン再生時に表示されない
        Start();
    }

    void Start()
    {
        if (!needRestruct) return;
        needRestruct = true;

        Mesh mesh = InitializeCube();
        texSize = GetTextureSize();
        GetComponent<MeshFilter>().sharedMesh = mesh;
        GetComponent<MeshRenderer>().material = material;
    }

    /// <summary>
    /// テクスチャサイズを取得
    /// </summary>
    /// <returns></returns>
    private Vector2Int GetTextureSize()
    {
        Debug.Assert(material.mainTexture != null, "Main texture is NULL!");

        var tex = material.mainTexture;

        return new Vector2Int(tex.width, tex.height);
    }

    /// <summary>
    /// pivotからの相対位置をUV座標に変換する
    /// </summary>
    /// <param name="offestX"></param>
    /// <param name="offsetY"></param>
    /// <returns></returns>    
    private Vector2 ToUV(float offestX, float offsetY)
    {
        return new Vector2((pivot.x + offestX) / texSize.x, (texSize.y - pivot.y + offsetY) / texSize.y);
    }

    /// <summary>
    /// メッシュを生成する
    /// </summary>
    /// <returns></returns>
    private Mesh InitializeCube()
    {
       
        Mesh mesh = new Mesh();

        /*
         *  　6
         * 5＜ ＞3
         * |　1　|
         * 4＜|＞2
         *  　0
         */
        var vertices = new Vector3[] {            
            new Vector3(0, 0, 0), // 0:pivot
            new Vector3(0, size.y, 0), // 1:nearest top
            new Vector3(0, 0, size.z), // 2:bottom right
            new Vector3(0, size.y, size.z), // 3:top right
            new Vector3(size.x, 0, 0), // 4:bottom left
            new Vector3(size.x, size.y, 0), // 5:top left
            new Vector3(size.x, size.y, size.z), // 6:most far top
        };

        var harfX = size.x * 0.5f;
        var harfZ = size.z * 0.5f;
        var uv = new Vector2[] {
            ToUV(0, 0), // 0:pivot
            ToUV(0, size.y), // 1:nearest top
            ToUV(-size.z, harfZ), // 4:bottom left
            ToUV(-size.z, harfZ + size.y), // 5:top left
            ToUV(size.x, harfX), // 2:bottom right
            ToUV(size.x, harfX + size.y),  // 3:top right
            ToUV(size.x - size.z, harfX + harfZ + size.y), // 6:most far top
        };

        var triangles = new int[] {
            0,3,1, 0,2,3, // Right Surface 
            5,4,0, 0,1,5, // Left Surface 
            1,3,5, 3,6,5, // Upper Surface
        };

        mesh.vertices = vertices;
        mesh.uv = uv;
        mesh.triangles = triangles;

        return mesh;
    }
}
```

`#if UNITY_EDITOR`~`#endif`ブロックは、Sceneビューでもプレビューできるようにするためのものです。

上のソースだと行間が伸びて若干見辛いですがVS上ではコメントの図はこんな感じです。
![vertIndex.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/a01684f7-a4f0-e87b-b631-b070f6ba08c0.jpeg)
0番が pivot で、1番が手前上面、6番が最奥上面… という風に割り振ってます。
基本的に一方向から見ることしか想定してませんから、裏側のメッシュは作りません。
Unity に合わせて一部書き換えましたが、やってることは参照元記事にある`InitializeCube`メソッドとほぼ同じです。
（オリジナルはオブジェクトの配置位置を反映してましたが、Unityでは親のtransformで動かせば良いので０起点に固定してます。）
テクスチャサイズが2の累乗に拡大されているので、 ToUV() メソッドでは pivot に相当するピクセルから必要な範囲だけを取り出せるように補正しています。このため**アトラス画像のように同じ画像に複数のドット絵を入れて使うことも可能**になっています。

## カメラ
![Camera01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/a76c535b-8f65-04d1-e063-ddb3eb41558c.jpeg)

これも2Dシーンなら基本中の基本、釈迦に説法かもしれませんが、パース(遠近法)を適用しないようにカメラを Orthographic に設定します。
Transform はお好みで調整してください、Clear Frags を Solid Color にしましたが、それ以外はデフォルト設定です。

## 途中結果１
ここまでの段階でこうなります。ぱっと見た感じでは既にお題の絵は出来ていますが、**ドット絵が綺麗に出ていません**。
![sceneView01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/43354df9-e5ea-52f1-5cb6-6e1dc5df6695.jpeg)


# ドット絵を綺麗に表示 PixelPerfect
わざわざドット絵を使うからには、**ドットが潰れたり引き伸ばされたりボヤけたりせず、元の品質を保って表示したい**ですね。（これを英語では **PixelPerfect** と言うようです。）

## テクスチャのアンチエイリアスを解除

![texFilterMode.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/9fe2da03-93c1-d3c4-2f3d-6ad5b77ea7c3.jpeg)
デフォルトだとテクスチャにアンチエイリアスがかかってぼやけてしまうので、上のように **Filter Mode を Point に設定して解除**します。
また、マッピングが適切に出来ていれば大差ないのですが、 Wrap Mode も Repeat から Clamp に変更しています。

**Bilinear** [初期設定]
|![bilinear.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/115f2b53-65b3-3b6c-02fa-fc0aaeb13c39.jpeg)

**Point(No filter)**
![point.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/3f11c5b3-e510-1449-a687-3e00025454b5.jpeg)
※上記キャプチャはいずれも倍率２

## プロジェクション行列の調整
カメラのプロジェクション(射影)行列の調整を行う QuaterView コンポーネントを作成します。
個人的には**これこそが元記事の肝**だと思ってます。【以下引用】
> ![](http://blog-imgs-54.fc2.com/q/o/o/qoofast/cubepixelsize.gif)
>ちょっと注意が必要なのは、xとzは直方体の辺に沿った長さではなく、画像の横幅に占めるドット数で指定すること。
例えば、~~右の絵~~(※上の絵)なら size = new Vector(24,18,20); となるだろう。

>なぜこんな仕様なのかというと、直方体の辺に沿った長さで指定すると x=20*√3, z=24*√3 になるわけだが、√3なんて**無理数を使うと小数点誤差を避けようもなく、ドット絵が歪む原因になる**から避けたいのだ。

というわけで、ドット絵のピクセル単位でサイズ指定できて、スクリーンのドットに綺麗に射影するための変換行列を作ります。

```csharp:QuaterView
public class QuaterView : MonoBehaviour
{
    private new Camera camera;

    [SerializeField, Header("カメラが移す中心地")]
    private Vector3Int lookAt;

    [SerializeField, Header("中心地の画面内上下位置(0～1)")]
    private float vertFocusPos;

    [SerializeField, Header("カメラを離す距離")]
    private int distance = 256;

    [SerializeField, Header("ピクセル倍率"), Min(1)]
    private int zoom = 1;


    void Awake()
    {
        camera = gameObject.GetComponent<Camera>();
        Assert.IsNotNull(camera);
        AdjustCamera();
    }

    void OnValidate()
    {
        Awake();
    }

    private float CalcOrthoSize()
    {
        // 画面の高さの半分＝等倍サイズ
        return camera.pixelHeight / (zoom * 2f);
    }

    private void AdjustCamera()
    {
        camera.transform.rotation = Quaternion.identity;
        camera.transform.position = Vector3.zero;
        camera.ResetProjectionMatrix();

        int depth = (lookAt.x + lookAt.z) / 2;
        Vector3Int pos = new Vector3Int(lookAt.x - lookAt.z, lookAt.y + depth, lookAt.y - depth);

        var matrix = new Matrix4x4()
        {
            m00 = 1.0f, m01 = 0.0f, m02 =-1.0f, m03 = 0.5f - pos.x,
            m10 = 0.5f, m11 = 1.0f, m12 = 0.5f, m13 = 0.0f - pos.y,
            m20 =-0.5f, m21 = 1.0f, m22 =-0.5f, m23 = 0.0f - pos.z - distance,
            m30 = 0.0f, m31 = 0.0f, m32 = 0.0f, m33 = 1.0f            
        };

        camera.worldToCameraMatrix = matrix;
        camera.orthographicSize = CalcOrthoSize();
       
    }
}
```

下の方に出てくる matrix は元記事のこの行列を Unity 用に変換したもので、**z軸の符号を反転**しています。また使用するクラスの違いから**パラメーターの表記順が行と列入れ替わってる**ことに注意してください。
![xnaMatrix.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/2579c905-7948-4d07-3529-3aa7928eb196.jpeg)
参考: http://logicalbeat.jp/blog/929/

この行列は camera の projectionMatrix ではなく worldToCameraMatrix にセットします。
projectionMatrix は標準的な Orthographic を使います。ピクセル等倍なサイズにするには、
```camera.orthographicSize = camera.pixelHeight / 2;```
に設定すればよいことがわかりました。
参考: http://light11.hatenadiary.com/entry/2019/04/17/012544

### カメラの回転も不要に
元記事には具体的には書いてないですが、この変換行列は回転も含んでます。
上の方で貼ったカメラの Inspector は transform/rotation が `x=22.5, y=45, z=0` となっていますが、 **QuaterView を適用したカメラは回転は不要**です。これも**小数点丸め誤差をなくすのに有用**だと思います。
![quarterViewCamera.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/a2c99889-4d43-5e1c-2862-111403189470.jpeg)
![projection.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/79011b58-463f-2e96-6765-0185a6ebd4e3.jpeg)
なお、 worldToCameraMatrix をセットすると transform 自体が無効になるので、**QuaterView の lookAt でワールドの表示目標位置を設定できる**ようにしています。Distanceはちょっとイジっただけだと見た目の変化はないですが、目標位置とカメラの距離を示します。 near/far plane の適用範囲をシフトさせる効果があります。

## 途中結果２
ドットがわかるように表示倍率x2にしてます。
![fixMatrix.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/8df6c0f8-9c78-3f83-bfa5-a856f37d069a.jpeg)

かなりいい感じですが、よく見るとまだドットの歪みがあるようです。
解りやすくするため、元記事のようにディザテクスチャを貼ってみました。
![textureCompressed.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/47ca83c8-4ff7-d254-5cc3-dee796159999.jpeg)

なぜか**境界付近の色がおかしい**ですね。

## テクスチャの圧縮設定
調べてみたところ、**境界付近の色がおかしいのはテクスチャの圧縮によるもの**だとわかりました。
作成したばかりのテクスチャはデフォルトでは左のように Format が `RGBA Compressed DXT5` になっています（最下部のグレーアウト部分）。これを右のように**`RGBA 32 bit`**にしました。ついでにMax Sizeもテクスチャに合わせてます。

|Before|After|
|:--:|:--:|
|![texDefaultComp.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/1cd35b7d-07d0-d538-7b78-b143efe9fe1f.jpeg)|![texSetNoComp.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/f82f26eb-c752-0879-98bf-3df56770dde5.jpeg)
|

## ポリゴン境界の拡張
色がおかしいのは直りましたが、よく見ると**斜めの線が途切れ途切れ**になっています。
![needMapping.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/8dda54b4-2792-5162-8559-a2784136969a.jpeg)
これは、左右側面のポリゴンの高さが足りないせいだと思われるので、**ポリゴンを上下に 0.5f づつ拡張**してみました。UVも同じように拡張しています。
![polyExpand.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/7a9664a7-7041-bccc-1ddd-043e908a268d.gif)

```csharp:Shed.cs
        var vertices = new Vector3[] {            
            new Vector3(0, -0.5f, 0), // 0:pivot
            new Vector3(0, size.y+0.5f, 0), // 1:nearest top
            new Vector3(0, -0.5f, size.z), // 2:bottom right
            new Vector3(0, size.y+0.5f, size.z), // 3:top right
            new Vector3(size.x, -0.5f, 0), // 4:bottom left
            new Vector3(size.x, size.y+0.5f, 0), // 5:top left
            new Vector3(size.x, size.y+0.5f, size.z), // 6:most far top
        };

        var harfX = size.x * 0.5f;
        var harfZ = size.z * 0.5f;
        var uv = new Vector2[] {
            ToUV(0, -0.5f), // 0:pivot
            ToUV(0, size.y+0.5f), // 1:nearest top
            ToUV(-size.z, harfZ-0.5f), // 4:bottom left
            ToUV(-size.z, harfZ + size.y+0.5f), // 5:top left
            ToUV(size.x, harfX-0.5f), // 2:bottom right
            ToUV(size.x, harfX + size.y+0.5f),  // 3:top right
            ToUV(size.x - size.z, harfX + harfZ + size.y+0.5f), // 6:most far top
        };
```

これで綺麗に表示されました。文句なしの Pixel Perfect です。
![allAdjusted.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/734f9de9-7ec6-e7ec-24db-e47a806f740f.jpeg)

# 最終結果
元のシーンも調整してこのようになりました。**ドットに歪みなく描画され、深度バッファを適用して適切な重なり順が実現できています**。
![finalResult.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/61b0eff2-5822-bfe0-5a3f-24085a6b896e.jpeg)

# 3Dオブジェクトとの共演
深度バッファを適用してるので、**他の３Dオブジェクトと混在させたシーンが作れます**。
例えば地形(Terrain)と組み合わせたり。
![withTerrain.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/cbaed478-af0f-1a2f-a4ea-4257e02e86cf.jpeg)
イーサンくんと組み合わせたり。
![ethanWalk.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/473bf44f-a311-a4e2-8f35-dcf0c6c203a5.jpeg)
↑足は紫の物体の向こう側にあるのに、右手は紫の物体の上にあるのがおわかりいただけるでしょうか。

## 縦横比補正
ところで、よく見ると３Dオブジェクトは若干縦に潰れた感じになります。
これはドット絵に都合のよいように強引な射影変形をしてるためですので、以下のように**アスペクト補正用のゲームオブジェクトの下に3Dオブジェクトを入れてやる**とよいと思います。
![adjust3D.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/c3fbfc53-0e75-7d4e-dec7-b66b57cbaed7.jpeg)
ここで**調整用オブジェクト(adjust3D)は縦を 1.155 倍**にしていることに注目してください。
1.155という数値はまったく無根拠ではなくて、これはx,z軸方向のドットが本来 √3/2 の距離を１ピクセルに変換していることから、逆数である `2/√3 ≒ 1.155` をy軸方向に適用したものです。

|無調整|アスペクト調整済|
|:--:|:--:|
|![ethanNoAdjust.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/bf370c73-2e00-8291-c158-820d91802a91.jpeg)|![ethanAdjusted.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/bb74dad2-e2ca-829e-8114-fae0146250be.jpeg)|

ご覧のように回転させても違和感ないですね！
![rotateEthan.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75035/d4c4f212-7578-a107-68fd-e7a0b273c7df.gif)

# まとめ
* 等角投影法のドット絵を立体に正射影して深度バッファを適用することができた
* worldToCameraMatrix を調整して Pixel Perfect にできた
* Pixel Perfect のために必要なテクスチャ設定がわかった
* 縦横比調整して3Dモデルとの併用もできた

プロジェクトファイル一式は [GitHub](https://github.com/ShinodaNaoki/isometricPixelarts) にあります。

# 参考
【元ネタ】http://qoofast.blog76.fc2.com/blog-entry-23.html

**カスタムmesh作成**
http://nn-hokuson.hatenablog.com/entry/2018/02/13/200114

**テクスチャ設定**
http://baba-s.hatenablog.com/entry/2018/01/31/213000

**射影行列とカメラ周り**
http://logicalbeat.jp/blog/929/
https://note.mu/fuqunaga/n/n08d81b185514
http://light11.hatenadiary.com/entry/2019/04/17/012544
https://github.com/cmilr/DeadSimple-Pixel-Perfect-Camera