はじめに
自作のEditor拡張でUVマップを表示しました。
そのときにComputeShaderを使用したのでその詳細を記していきます。
MeshDeleterWithTexture v0.5bでは
— がとーしょこら@VRC技術市でEditor拡張本を出品中 (@gatosyocora) September 19, 2019
* UVマップの表示
* ペンカーソルの表示
* 削除処理の高速化
などを更新します pic.twitter.com/wBZuvNcsbj
ComputeShaderとは
HLSLやGLSLといった描画用のシェーダーがありますが、ComputeShaderはGPUを使った数値計算をする(GPGPU)ためのシェーダーです。
GPUは単純な計算を並列実行できるので、処理によってはCPUに比べて高速に処理が実行できます。
Unityで使う場合には事前にC#のプログラム上で必要なデータや出力先を指定してComputeShaderを実行させます。
https://docs.unity3d.com/ja/2018.4/Manual/class-ComputeShader.html
メッシュのUVマップを取得する実際のコードを見ながら簡単に解説します。
実際のコード
実際のコードです。
今回のComputeShaderではポリゴン単位で並列で計算させています。
#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);
}
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の部分です。
[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頂点を特定するためのインデックスを取得します。
// 3角ポリゴンをつくる3頂点のインデックスを取得
int p1Index = Triangles[id.x * 3];
int p2Index = Triangles[id.x * 3 + 1];
int p3Index = Triangles[id.x * 3 + 2];
取得したインデックスを元にUV座標を取得してテクスチャ座標に変換します。
// 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頂点間に線を引くコードはこちらです。
// 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を取得します。
ComputeShader cs = Instantiate(Resources.Load<ComputeShader>("getUVMap")) as ComputeShader;
int kernel = cs.FindKernel("CSMain");
次に使用するデータを渡すためにBufferを確保して、データを設定します。
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)]
に対応しています。
cs.Dispatch(kernel, triangles.Length / 3, 1, 1);
これで計算結果がテクスチャとしてuvMapTexに出力されています。