【Unity】RenderMeshIndirectとComputeShaderでまつぼっくり螺旋を描く with URP【C# / HLSL】

# 自己紹介
Unity公式オフラインイベント「U/DAY TOKYO」に参加して、モチベ爆上がりの民

https://x.com/KaidoAtsuya/status/1807741427318939859

U/DAYはアーカイブが公開されるらしいので気になる人は以下のツイートからどうぞ
（写真に私います）

https://x.com/unity_japan/status/1807699413181337948

# ほんだーい！
今回はDrawMesh系のメソッド群に代わる新たなメソッド群RenderMeshにおける`RenderMeshIndirect`にフォーカスしつつ、大学の講義でまつぼっくり螺旋というものを習ったのでそれを描画します。
あくまでもメインは`RenderMeshIndirect`です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3083521/7e005749-f643-311f-0793-e6e521a0d607.png)

## 注意
私の理解も浅いので、間違っている可能性もあるという前提で読んでください！
実装例としてみてくれると嬉しいです。

# 用語整理
## RenderMesh系APIとは？
Unityは基本的に「MeshRenderer」というコンポーネントを利用して、オブジェクトを描画していきます。
つまりゲームオブジェクト１つに対して、１つのオブジェクトを描画することが基本なわけです。

しかし、RenderMesh系のAPIはゲームオブジェクトなしで非常に高速にオブジェクトを描画できます。
詳しくは「Unity Technologies Japan」公式の動画がありますので、ご覧ください。

https://www.youtube.com/watch?v=d6i9hvJ-xcM

### RenderMeshIndirectとは？
RenderMesh系のメソッドの中でも特に自由度が高いものがRenderMeshIndirectです。
その代わりに独自のシェーダーと結構長めの準備が必要になってきます。

## ComputeShaderとは？
ComputeShaderは通常のC#スクリプトでCPUに処理させるようなものを、GPUに処理をさせる機能です。
GPUはCPUに比べ並列処理がとても強いため、状況によっては非常に有効なものとなります。

詳しくはゆっち～さんの[こちらの記事](https://blog.yucchiy.com/2019/01/03/tutorial-for-unity-compute-shader/)やインターネットで調べるとより詳細な話が出てきます。

### JobSystem vs. ComputeShader
少し話はそれますが、UnityにはJobSystemというCPUに並列処理をさせる強力な機能が搭載されています。
ComputeShaderとどちらを選択するべきなのか、結論から言うとCPUとGPUボトルネックになっていないほうに負荷がかかるようにだそうです。
ほかにも様々な考慮すべき点があるとされていますが、これが一番分かりやすいのではないでしょうか。
（私がテクスチャサイズによって選ぶべき理由を理解していないのもある）

https://forum.unity.com/threads/c-jobs-vs-compute-shaders.664456/

## まつぼっくり螺旋とは？
まつぼっくり螺旋と言っていますが、「アルキメデス螺旋」といったほうが恐らく一般的です。
（まつぼっくりの方がキャッチーじゃん？）
本題とそれ過ぎるので、以下の記事などをご覧ください。

https://neo.ac/column/9587/

# プログラムを書いていく
`RenderMeshIndirect`の引数には聞きなれない型が多くあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3083521/c4b2fe79-7eea-d75b-7599-20d07564abf2.png)

https://docs.unity3d.com/ScriptReference/Graphics.RenderMeshIndirect.html

`RenderParams`やら、`CommandBuffer`やら…ひとつずつ作っていきましょう。

## RenderParams
`RenderParams`はその名の通り、描画に関する情報を詰め込むクラスです。
マテリアルや、独自のシェーダーに渡したい情報などを設定していきます。

```Csharp:RenderParamsを準備
// PositionsはComputeShaderで計算している状態

var rp = new RenderParams(_material);

var positionsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, (int)_spiralLength, Marshal.SizeOf<float3>());
positionsBuffer.SetData(positions);

rp.matProps = new();
rp.matProps.SetBuffer(PositionsID, positionsBuffer);
```

という感じです。
前提が多く抜けているので、全てを理解する必要はありません。
理解すべきと考えられるものを解説します。

### コンストラクタ
コンストラクタでは、描画に使用するマテリアルを指定します。
マテリアルというか、シェーダーを指定する感覚に近いです。
なぜなら独自のシェーダーを持ったマテリアルを設定することになるからです。

### GraphicsBuffer
`GraphicsBuffer`はシェーダーに情報を送るためのバッファーです。
コンストラクタでは、情報を送るためのサイズなどを指定します。
（正直私もよく分かっていないところもあります。）

`Marshal.SizeOf<float3>()`というものは`sizeof(float)`と同様の働きです。
しかし、`float3`は`sizeof`が使えないので、このような書き方をします。

## Mesh
MeshはMeshですね…
MeshっていうのはMeshなんですよ…

## GraphicsBuffer
２度目の登場です。こちらのただこちらの`GraphicsBuffer`はやることが決められています。

```csharp:GraphicsBufferを準備する
var commandData = new GraphicsBuffer.IndirectDrawIndexedArgs[1];
commandData[0].indexCountPerInstance = _mesh.GetIndexCount(0);
commandData[0].baseVertexIndex = _mesh.GetBaseVertex(0);
commandData[0].startIndex = _mesh.GetIndexStart(0);
commandData[0].instanceCount = 10;

var indirectBuf = new GraphicsBuffer(GraphicsBuffer.Target.IndirectArguments, 1, GraphicsBuffer.IndirectDrawIndexedArgs.size);
indirectBuf.SetData(commandData);
```

ここではメッシュの設定や生成するインスタンスの個数を設定していきます。
基本的には上のコピペでOKで、`instanceCount`だけ調整してください。

## その他
その他は基本的にデフォルトで大丈夫です。

## シェーダーを用意する
そもそもMaterialを用意しなければなりません。
では、今回使ったシェーダープログラムを貼ります。

```HLSL
Shader "Custom/FibonacciSpiralShader"
{
    SubShader
    {
        Tags
        {
            "RenderType"="Opaque" "RenderPipeline"="UniversalRenderPipeline"
        }
        LOD 200

        Pass
        {
            HLSLPROGRAM
            
            #define UNITY_INDIRECT_DRAW_ARGS IndirectDrawIndexedArgs
            
            #pragma vertex vert
            #pragma fragment frag

            #pragma multi_compile_fog

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
            
            struct Varying
            {
                float4 vertex : POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Attributes
            {
                float4 pos : POSITION;
                float4 color : COLOR0;
            };

            CBUFFER_START(UnityPerMaterial);
            uniform StructuredBuffer<float3> _Positions;
            CBUFFER_END;
            
            Attributes vert(Varying i, uint instanceID : SV_InstanceID)
            {
                Attributes o;

                // 色を変える（Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlslにマクロが用意されてる）
                float colorValue = instanceID / 20.0f;
                float3 hsvColor = float3(colorValue, 1, 1);
                o.color = float4(HsvToRgb(hsvColor), 1);

                // サイズを変える
                i.vertex *= instanceID * 1.1 + 1;

                // ポジションを指定
                float4 wPos = TransformObjectToHClip(i.vertex + _Positions[instanceID]);
                o.pos = wPos;
                
                return o;
            }

            half4 frag(Attributes i) : SV_Target
            {
                return i.color;
            }
            
            ENDHLSL   
        }
    }
}
```
ポイントはVaryingの中に定義してある`UNITY_VERTEX_INPUT_INSTANCE_ID`です。
これによって、`vertex`の引数の中で`instanceID`が取得できるようになります。

また、`_Positions`という変数で`RenderParams`からの情報が入る形になっています。

# 全てのソースコード
```csharp:FibonacciSpiral.cs
using System.Runtime.InteropServices;
using Unity.Mathematics;
using UnityEngine;

namespace ComputeShading.FibonacciSpiral
{
    public class FibonacciSpiral : MonoBehaviour
    {
        [SerializeField] private ComputeShader _computeShader;

        [SerializeField] private float _theta;
        [SerializeField] private float _radius;
        [SerializeField] private uint _spiralLength;

        [SerializeField] private Mesh _mesh;
        [SerializeField] private Material _material;
        
        private int _kernelIndex;

        private GraphicsBuffer.IndirectDrawIndexedArgs[] _commandData;
        
        private static readonly int Theta = Shader.PropertyToID("theta");
        private static readonly int R = Shader.PropertyToID("r");
        private static readonly int BufferID = Shader.PropertyToID("buffer");
        private static readonly int PositionsID = Shader.PropertyToID("_Positions");
        private static readonly int CountID = Shader.PropertyToID("_Count");

        private void Start()
        {
            // ComputeShaderの用意
            _kernelIndex = _computeShader.FindKernel("CalculateFibonacciSpiral");
            
            // RenderMeshIndirectの用意
            _commandData = new GraphicsBuffer.IndirectDrawIndexedArgs[1];
            _commandData[0].indexCountPerInstance = _mesh.GetIndexCount(0);
            _commandData[0].baseVertexIndex = _mesh.GetBaseVertex(0);
            _commandData[0].startIndex = _mesh.GetIndexStart(0);
        }

        private void Update()
        {
            _computeShader.SetFloat(Theta, _theta);
            _computeShader.SetFloat(R, _radius);
            
            var positions = new float3[_spiralLength];
            var buffer = new ComputeBuffer((int)_spiralLength, Marshal.SizeOf(typeof(float2)));
            _computeShader.SetBuffer(_kernelIndex, BufferID, buffer);
            
            uint sizeX, sizeY, sizeZ;
            _computeShader.GetKernelThreadGroupSizes(
                _kernelIndex,
                out sizeX,
                out sizeY,
                out sizeZ
            );

            var threadGroupX = (int)System.Math.Ceiling(_spiralLength / (float)sizeX);
            _computeShader.Dispatch(_kernelIndex, threadGroupX, 1, 1);
            
            var result = new float2[_spiralLength];
            buffer.GetData(result);

            for (var i = 0; i < _spiralLength; i++)
            {
                positions[i] = new float3(result[i].x, result[i].y, 0);
            }
            
            var rp = new RenderParams(_material);
            
            var positionsBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, (int)_spiralLength, Marshal.SizeOf<float3>());
            positionsBuffer.SetData(positions);
            
            rp.matProps = new();
            rp.matProps.SetBuffer(PositionsID, positionsBuffer);
            rp.matProps.SetInt(CountID, (int)_spiralLength);
            
            rp.material.SetInt("_InstanceCount", (int)_spiralLength);
            
            _commandData[0].instanceCount = _spiralLength;
         
            var indirectBuf = new GraphicsBuffer(GraphicsBuffer.Target.IndirectArguments, 1, GraphicsBuffer.IndirectDrawIndexedArgs.size);
            indirectBuf.SetData(_commandData);
            
            Graphics.RenderMeshIndirect(rp, _mesh, indirectBuf);
        }
    }
}
```

```HLSL:FibbonacciSpiral.compute
#pragma kernel CalculateFibonacciSpiral

#define PI 3.14159265

RWStructuredBuffer<float2> buffer;
float theta;
float r;

[numthreads(4, 1, 1)]
void CalculateFibonacciSpiral(uint3 dispatchThreadID : SV_DispatchThreadID)
{
    float rad = theta * dispatchThreadID.x * PI / 180;
    
    buffer[dispatchThreadID.x].x = r * dispatchThreadID.x * cos(rad);
    buffer[dispatchThreadID.x].y = r * dispatchThreadID.x * sin(rad);
}
```

```HLSL:FibbonacciSpiralShader.shader
Shader "Custom/FibonacciSpiralShader"
{
    SubShader
    {
        Tags
        {
            "RenderType"="Opaque" "RenderPipeline"="UniversalRenderPipeline"
        }
        LOD 200

        Pass
        {
            HLSLPROGRAM
            
            #define UNITY_INDIRECT_DRAW_ARGS IndirectDrawIndexedArgs
            
            #pragma vertex vert
            #pragma fragment frag

            #pragma multi_compile_fog

            #include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
            #include "Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlsl"
            
            struct Varying
            {
                float4 vertex : POSITION;
                UNITY_VERTEX_INPUT_INSTANCE_ID
            };

            struct Attributes
            {
                float4 pos : POSITION;
                float4 color : COLOR0;
            };

            CBUFFER_START(UnityPerMaterial);
            uniform StructuredBuffer<float3> _Positions;
            CBUFFER_END;
            
            Attributes vert(Varying i, uint instanceID : SV_InstanceID)
            {
                Attributes o;

                // 色を変える（Packages/com.unity.render-pipelines.core/ShaderLibrary/Color.hlslにマクロが用意されてる）
                float colorValue = instanceID / 20.0f;
                float3 hsvColor = float3(colorValue, 1, 1);
                o.color = float4(HsvToRgb(hsvColor), 1);

                // サイズを変える
                i.vertex *= instanceID * 1.1 + 1;

                // ポジションを指定
                float4 wPos = TransformObjectToHClip(i.vertex + _Positions[instanceID]);
                o.pos = wPos;
                
                return o;
            }

            half4 frag(Attributes i) : SV_Target
            {
                return i.color;
            }
            
            ENDHLSL   
        }
    }
}
```

# まとめ
RenderMeshIndirectとComputeShaderで良い感じにアルキメデス螺旋を作れました。
やったね！！

## ちなみに
今回登場した、ゆっち～さんとUnity Technologies Japan公式の動画に出演した方全員とU/DAYでお話しできたので、ハッピーすぎます。

## Computer Shader × RenderMesh(DrawMesh)
ほかにもComputer ShaderとRenderMesh系APIで遊んでいるリポジトリがあります。
どうぞ、ご覧ください。

https://github.com/AtsuAtsu0120/ComputeSharding