ComputeShaderを使ってUVマップ画像を作成する

# はじめに
自作のEditor拡張でUVマップを表示しました。
そのときにComputeShaderを使用したのでその詳細を記していきます。
<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">MeshDeleterWithTexture v0.5bでは<br>* UVマップの表示<br>* ペンカーソルの表示<br>* 削除処理の高速化<br>などを更新します <a href="https://t.co/wBZuvNcsbj">pic.twitter.com/wBZuvNcsbj</a></p>&mdash; がとーしょこら@VRC技術市でEditor拡張本を出品中 (@gatosyocora) <a href="https://twitter.com/gatosyocora/status/1174623491355578368?ref_src=twsrc%5Etfw">September 19, 2019</a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

# ComputeShaderとは
HLSLやGLSLといった描画用のシェーダーがありますが、ComputeShaderはGPUを使った数値計算をする(GPGPU)ためのシェーダーです。
GPUは単純な計算を並列実行できるので、処理によってはCPUに比べて高速に処理が実行できます。
Unityで使う場合には事前にC#のプログラム上で必要なデータや出力先を指定してComputeShaderを実行させます。
https://docs.unity3d.com/ja/2018.4/Manual/class-ComputeShader.html

メッシュのUVマップを取得する実際のコードを見ながら簡単に解説します。

# 実際のコード
実際のコードです。
今回のComputeShaderではポリゴン単位で並列で計算させています。

```glsl:getUVMap.compute
﻿#pragma kernel CSMain

// 出力先テクスチャ
RWTexture2D<float4> UVMap;

// 入力データ
StructuredBuffer<float2> UVs;
StructuredBuffer<int> Triangles;
int Width;
int Height;

CGPROGRAM
//　2点間に線を引く
void drawline(uint2 p1, uint2 p2, float4 color) {
	int2 diffp12 = int2(p2.x-p1.x, p2.y-p1.y);
	float distp12 = distance(p1, p2);
	for (int i = 0; i < distp12; i++) 
	{
		UVMap[p1 + diffp12 / distp12 * i] = color;	
	}
}
ENDCG

[numthreads(1,1,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    // 3角ポリゴンをつくる3頂点のインデックスを取得
	int p1Index = Triangles[id.x * 3];
	int p2Index = Triangles[id.x * 3 + 1];
	int p3Index = Triangles[id.x * 3 + 2];

    // 3頂点に対応したuv座標を取得
	float2 uv1 = UVs[p1Index];
	float2 uv2 = UVs[p2Index];
	float2 uv3 = UVs[p3Index];

    // テクスチャの座標に変換
	uint2 p1Pos = uint2(uv1.x * Width, uv1.y * Height);
	uint2 p2Pos = uint2(uv2.x * Width, uv2.y * Height);
	uint2 p3Pos = uint2(uv3.x * Width, uv3.y * Height);

	float4 color = float4(1, 1, 1, 1);

    // 3頂点が示すテクスチャ上の点間に線を引く
	drawline(p1Pos, p2Pos, color);
	drawline(p2Pos, p3Pos, color);
	drawline(p3Pos, p1Pos, color);
}
```

```csharp:cprog.cs
private Texture2D GetUVMap(Mesh mesh, int subMeshIndex, Texture2D texture)
{
    var triangles = mesh.GetTriangles(subMeshIndex);
    var uvs = mesh.uv;

    if (uvs.Count() <= 0) return null;

    ComputeShader cs = Instantiate(Resources.Load<ComputeShader>("getUVMap")) as ComputeShader;
    int kernel = cs.FindKernel("CSMain");

    RenderTexture uvMapRT = new RenderTexture(texture.width, texture.height, 0);
    uvMapRT.enableRandomWrite = true;
    uvMapRT.Create();

    var triangleBuffer = new ComputeBuffer(triangles.Count(), sizeof(int));
    var uvBuffer = new ComputeBuffer(uvs.Count(), Marshal.SizeOf(typeof(Vector2)));
    triangleBuffer.SetData(triangles);
    uvBuffer.SetData(uvs);

    cs.SetTexture(kernel, "UVMap", uvMapRT);
    cs.SetInt("Width", texture.width);
    cs.SetInt("Height", texture.height);
    cs.SetBuffer(kernel, "Triangles", triangleBuffer);
    cs.SetBuffer(kernel, "UVs", uvBuffer);

    cs.Dispatch(kernel, triangles.Length / 3, 1, 1);

    triangleBuffer.Release();
    uvBuffer.Release();

    var uvMapTex = new Texture2D(texture.width, texture.height, TextureFormat.RGB24, false);
    uvMapTex.name = texture.name;

    // RenderTextureからTexture2Dに変換
    var original = RenderTexture.active;
    RenderTexture.active = uvMapRT;
    uvMapTex.ReadPixels(new Rect(0, 0, uvMapRT.width, uvMapRT.height), 0, 0);
    uvMapTex.Apply();
    RenderTexture.active = original;

    uvMapRT.Release();

    return uvMapTex;
}
```

# 処理の解説
### compute shader
このComputeShaderで実行される部分はCSMainの部分です。

```glsl:
[numthreads(1,1,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
```
上についているnumthreadsは処理単位のブロックみたいなものですが、今回は特に考えないのですべて1にしています。

そうした場合、引数のidには並列処理ごとに割り振られた異なるidが入力されます。
今回は`[numthreads(1,1,1)]`としています。
また、後述のC#のコードでは`cs.Dispatch(kernel, triangles.Length / 3, 1, 1);`で実行しています。
この`(1, 1, 1)`と`triangles.Length / 3, 1, 1`がuint3型であるidのx, y, zに対応しています。
これらを見るとxのみが1より大きな値になっており, この数だけ並列で実行されます。
よってid.xが異なる値でyとzはすべて同じ値になっています。

そのidを元にメッシュの3頂点を特定するためのインデックスを取得します。

```csharp

// 3角ポリゴンをつくる3頂点のインデックスを取得
int p1Index = Triangles[id.x * 3];
int p2Index = Triangles[id.x * 3 + 1];
int p3Index = Triangles[id.x * 3 + 2];
```

取得したインデックスを元にUV座標を取得してテクスチャ座標に変換します。

```csharp

// 3頂点に対応したuv座標を取得
float2 uv1 = UVs[p1Index];
float2 uv2 = UVs[p2Index];
float2 uv3 = UVs[p3Index];

// テクスチャの座標に変換
uint2 p1Pos = uint2(uv1.x * Width, uv1.y * Height);
uint2 p2Pos = uint2(uv2.x * Width, uv2.y * Height);
uint2 p3Pos = uint2(uv3.x * Width, uv3.y * Height);
```

そして、2頂点間に線を引いていきます。

```
drawline(p1Pos, p2Pos, color);
drawline(p2Pos, p3Pos, color);
drawline(p3Pos, p1Pos, color);
```

2頂点間に線を引くコードはこちらです。

```csharp

//　2点間に線を引く
void drawline(uint2 p1, uint2 p2, float4 color) {
    int2 diffp12 = int2(p2.x-p1.x, p2.y-p1.y);
    float distp12 = distance(p1, p2);
    for (int i = 0; i < distp12; i++) 
    {
        UVMap[p1 + diffp12 / distp12 * i] = color;  
    }
}
```

### csharp
C#コード側ではこのComputeShaderに必要なデータを渡して、実行させています。

始めに使用するComputeShaderをResourcesフォルダから読み込んで、実行するKernelを取得します。

```csharp

ComputeShader cs = Instantiate(Resources.Load<ComputeShader>("getUVMap")) as ComputeShader;
int kernel = cs.FindKernel("CSMain");
```

次に使用するデータを渡すためにBufferを確保して、データを設定します。

```csharp

var triangleBuffer = new ComputeBuffer(triangles.Count(), sizeof(int));
var uvBuffer = new ComputeBuffer(uvs.Count(), Marshal.SizeOf(typeof(Vector2)));
triangleBuffer.SetData(triangles);
uvBuffer.SetData(uvs);

cs.SetTexture(kernel, "UVMap", uvMapRT);
cs.SetInt("Width", texture.width);
cs.SetInt("Height", texture.height);
cs.SetBuffer(kernel, "Triangles", triangleBuffer);
cs.SetBuffer(kernel, "UVs", uvBuffer);
```

そして、ComputeShaderを実行します。
`Dispatch(kernel, x, y, z)`はComputeShaderの`[numthreads(x, y, z)]`に対応しています。

```csharp

cs.Dispatch(kernel, triangles.Length / 3, 1, 1);
```

これで計算結果がテクスチャとしてuvMapTexに出力されています。