LoginSignup
11
4

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-02-17

はじめに

自作のEditor拡張でUVマップを表示しました。
そのときにComputeShaderを使用したのでその詳細を記していきます。

ComputeShaderとは

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

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

実際のコード

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

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);
}
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の部分です。

[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に出力されています。

11
4
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
4