Unityでシェーダーに渡すパラメーターを頂点情報に含めることでsetpass callを減らす試み

この記事は[PONOS Advent Calendar 2024](https://qiita.com/advent-calendar/2024/ponos) 6日目の記事です。
前回は[@nissy_gp](https://qiita.com/nissy_gp)さんでした。

---
今回使用する画像は[いらすとや](https://www.irasutoya.com/)からお借りしています。

---

## 目的
Unityでオブジェクトを大量に描画するときに、できるだけ単一のマテリアルで描画することでsetpass callを減らし描画負荷を減らすことを試みるかと思います。
しかし「オブジェクトごとに別パラメーターで描画したい」みたいなことがあると思います。そうなるとマテリアルに`material.SetFloat("key" value)`のような処理でパラメーターを渡してやる必要があり、それによってマテリアルが複製されSetPass Callsが増えて負荷が増えます。
今回の記事はSetPass Callsを増やさずにシェーダーにパラメーターを渡すことを目的としています。

## 方法
いくつか方法が考えられると思いますが今回は頂点情報(UV)で渡す方法を考えます。

## 実装
今回は単一の画像を描画するSprite Rendererのようなものを実装します。
以下のように同じ画像のパラメーター違いを3つ描画します。
- 普通に描画
- グレースケールで描画
- 色を変更して描画

![スクリーンショット 2024-12-04 12.13.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98727/e963daf6-5c45-6ede-fb6d-55a3a17bf68b.png)

実現のためにはSpriteを描画するC#スクリプトの実装とそれに対応するシェーダーの追加が必要です。


### C#スクリプトの実装
コード全ては長いので折り畳みで書いてあります。
頂点4つのメッシュを作成し描画するコンポーネントです。

<details><summary>コード全文</summary>

```csharp
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace Block
{
    [RequireComponent(typeof(MeshRenderer), typeof(MeshFilter))]
    [ExecuteAlways]
    public class CustomSpriteRenderer : MonoBehaviour
    {
        static readonly Vector3[] MeshVertices = new Vector3[] {
                new Vector3(-1, -1, 0),
                new Vector3(-1, 1, 0),
                new Vector3(1, 1, 0),
                new Vector3(1, -1, 0),
            };
        static readonly Vector2[] UV = new Vector2[] {
                new Vector2(0, 0),
                new Vector2(0, 1),
                new Vector2(1, 1),
                new Vector2(1, 0),
            };
        static readonly int[] VertexIndeces = new int[] { 0, 1, 2, 3, 0, 2 };
        
        [SerializeField]
        Material material = null;
        
        bool isDirty = false;

        [field: SerializeField]
        public Color Color { get; private set; } = Color.white;

        [field: SerializeField]
        public bool IsGrayScale { get; private set; } = false;

        Mesh mesh = null;
        
        MeshRenderer meshRenderer = null;

        List<Color> colors = new() { Color.white, Color.white, Color.white, Color.white };

        List<Vector2> uv0 = UV.ToList(); 

        List<Vector4> uv1 = new (){ Vector4.zero, Vector4.zero, Vector4.zero, Vector4.zero, };
        
        void OnEnable()
        {
            TryGetComponent(out meshRenderer);
            if (!Application.isPlaying && mesh != null)
            {
                DestroyImmediate(mesh);
                mesh = null;
            }

            if (mesh == null)
            {
                mesh = GenerateMesh();
                meshRenderer.sharedMaterials = new Material[] { material };
                TryGetComponent<MeshFilter>(out var meshFilter);
                meshFilter.sharedMesh = mesh;
                SetColor(Color);
                SetGrayScale(IsGrayScale);
                mesh.SetUVs(0, uv0);
                mesh.SetUVs(1, uv1);
                mesh.SetColors(colors);
            }
            
            isDirty = true;
        }
        
        void OnValidate()
        {
            if (meshRenderer.sharedMaterial != material)
            {
                meshRenderer.sharedMaterial = material;
            }
            SetColor(Color);
            SetGrayScale(IsGrayScale);
            isDirty = true;
        }
        
        void LateUpdate()
        {
            if (!isDirty) { return; }
            mesh.SetColors(colors);
            mesh.SetUVs(0, uv0);
            mesh.SetUVs(1, uv1);
        }

        static Mesh GenerateMesh()
        {
            var mesh = new Mesh();
            mesh.vertices = MeshVertices;
            mesh.subMeshCount = 1;
            mesh.SetTriangles(VertexIndeces, 0);
            return mesh;
        }
        
        public void SetColor(Color color)
        {
            Color = color;
            colors[0] = color;
            colors[1] = color;
            colors[2] = color;
            colors[3] = color;
            isDirty = true;
        }
        
        public void SetGrayScale(bool isGrayScale)
        {
            IsGrayScale = isGrayScale;
            var v = uv1[0];
            v.x = isGrayScale ? 1.0f : 0.0f;
            uv1[0] = v;
            uv1[1] = v;
            uv1[2] = v;
            uv1[3] = v;
            isDirty = true;
        }
    }
}
```
</details>

エディタで実行しなくても確認できるようにするために処理が複雑になっている箇所があります。
重要なのは以下の処理です。
```csharp
        void LateUpdate()
        {
            if (!isDirty) { return; }
            mesh.SetColors(colors);
            mesh.SetUVs(0, uv0);
            mesh.SetUVs(1, uv1);
        }

        public void SetColor(Color color)
        {
            Color = color;
            colors[0] = color;
            colors[1] = color;
            colors[2] = color;
            colors[3] = color;
            isDirty = true;
        }

        public void SetGrayScale(bool isGrayScale)
        {
            // uv.xにグレースケール化するかどうかの値をセットする
            IsGrayScale = isGrayScale;
            var v = uv1[0];
            v.x = isGrayScale ? 1.0f : 0.0f;
            uv1[0] = v;
            uv1[1] = v;
            uv1[2] = v;
            uv1[3] = v;
            isDirty = true;
        }
```
色は頂点カラーとして、
グレースケール化するかどうかの情報を`uv1`の`x`としてセットしています。

### シェーダー
今回はShader Graphでの実装です。
uvのchannle UV1のxが0でなければグレースケールで描画するようにしています。
![スクリーンショット 2024-12-04 13.37.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98727/332903a4-2781-cbf4-d4a8-2fc08e3dbb97.png)


## 結果
以下の画像から1つの描画でも3つの描画でもSetPass Callsが変化せず、Batch処理が有効になっており、1度の処理で描画できていることがわかります。
![スクリーンショット 2024-12-04 13.47.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98727/7fb2f5c0-6598-3f76-4e35-e2040a225b77.png)
![スクリーンショット 2024-12-04 13.47.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98727/476d93a4-da25-8f56-d1b0-c9ab97457c38.png)
![スクリーンショット 2024-12-04 12.14.23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/98727/1b5d56ee-915b-e87c-ecba-2f1cdc405e48.png)

## メリット
パラメーター違いの描画を行なってもSetPass Callsが増えない

## デメリット
- シェーダーが分かり辛くなる
- 頂点情報が増える

頂点がいくつでもUV1に同じ情報を入れている関係で、頂点数が増えれば増えるほど2番目のデメリットが大きなデメリットとなります。

## 結論
目的は果たせるもののこのまま使用するのは推奨できない。
頂点数が少ないものであれば場合によっては使用できる。

---
次回は [@nissy_gp](https://qiita.com/nissy_gp) さんの「【初心】プログラマが問題を調査/解決する時に大事にしてほしいこと１０ヶ条」です。