本日はUnity枠です。
筆者はシェーダーに関してはある一定以上の理解と構築力があると思っていますが、近年用いられているコンピュートシェーダーに関しては苦手意識を持っていました。
今回はコンピュートシェーダーを使用した簡単な実装を行っていきます。
〇環境
・Unity6000.0.2f1
・Windows11PC
〇コンピュートシェーダーとは?
コンピュートシェーダー(Compute Shader)はGPU上で実行されるプログラムで、GPGPU=GPUでの汎用的計算=シェーダーとして以外のGPUの利用用途として用いられています。
GPUでは一般的に描画のプロセスが行われていますが、近年のAIなどにみられるように物理シミュレーション、画像処理、機械学習などグラフィックの描画以外の計算をCPUではなくGPUの膨大な並列演算機能を用いて行うというものがGPGPUであり、この処理プログラムがコンピュートシェーダーです。
また、この時GPUのメモリであるVRAMを使用することができ、データの読み書きが可能です。
近年ジオメトリシェーダーステージで従来行っていた処理を、コンピュートシェーダーで置き換え、ジオメトリシェーダーステージを使用しないという実装が増えています。
またDirectX以外のグラフィックスAPIではMetalなどジオメトリシェーダーをそもそもサポートしない場合も増えています。
この理由は汎用性の向上、パフォーマンスの向上、メモリ管理に加えコンピュートシェーダーがレンダリングパイプラインの外で実行されるという点があります。
ジオメトリシェーダーの特徴であった頂点の増減などもコンピュートシェーダーを使用することで可能なため従来のジオメトリシェーダーに置き換えが始めっています。
〇Unityでコンピュートシェーダーを使った簡単な実装を行う
今回はコンピュートシェーダーを使って何ができるのかという点を見るために3Dモデルの頂点がY座標でマイナス値にある数を取得するということを行っていきます。
〇コンピュートシェーダーを実行するために必要な条件
コンピュートシェーダーはProjectウィンドウで右クリックCreate→Shader→Compute Shaderから作成することが可能です。


しかしC#のようにオブジェクトに直接アタッチすることはできません。(Monobehaviorを継承していないどころかC#ではないので)
そのためオブジェクト側にアタッチしてコンピュートシェーダーとつなぎの役割となるC#スクリプトが必要です。
〇Y座標がマイナスの頂点を取得する
まずはコンピュートシェーダー側の処理です。
コンピュートシェーダーは基本的にHLSL言語で記述されます。
// CountTrianglesBelowY0.compute
#pragma kernel CSMain
struct Vertex {
float3 position;
};
StructuredBuffer<Vertex> vertexBuffer;
StructuredBuffer<int> indexBuffer;
RWStructuredBuffer<int> countBuffer;
float4x4 localToWorldMatrix; // ワールド座標変換行列
[numthreads(1, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
// 面のインデックスを3つずつ取得
int index0 = indexBuffer[id.x * 3 + 0];
int index1 = indexBuffer[id.x * 3 + 1];
int index2 = indexBuffer[id.x * 3 + 2];
float3 v0 = vertexBuffer[index0].position;
float3 v1 = vertexBuffer[index1].position;
float3 v2 = vertexBuffer[index2].position;
// ローカル座標をワールド座標に変換
v0 = mul(localToWorldMatrix, float4(v0, 1.0)).xyz;
v1 = mul(localToWorldMatrix, float4(v1, 1.0)).xyz;
v2 = mul(localToWorldMatrix, float4(v2, 1.0)).xyz;
// Y座標が0以下の頂点が含まれるかチェック
if (v0.y <= 0 || v1.y <= 0 || v2.y <= 0)
{
// 面のカウンタをインクリメント
InterlockedAdd(countBuffer[0], 1);
}
}
冒頭部のpragma kernelはカーネルの定義です。
#pragma kernel CSMain
カーネル(Kernel)とはOSの中核をなすコンポーネントであり、ハードウェアとソフトウェアの受け渡しを行いシステム全体をつないでいます。
コンピュートシェーダーでカーネルの定義が行われるのはGPU上で並列に実行される計算の単位を明示して定義するためです。
具体的にはGPUがどの関数をへ入れるに実行するのかを指定します。
通常のシェーダー同様、CSMain関数に処理内容が定義されており、numthreadsアトリビュートがスレッドグループ内で実行されるスレッドの数を指定しています。
[numthreads(1, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
// 面のインデックスを3つずつ取得
int index0 = indexBuffer[id.x * 3 + 0];
int index1 = indexBuffer[id.x * 3 + 1];
int index2 = indexBuffer[id.x * 3 + 2];
float3 v0 = vertexBuffer[index0].position;
float3 v1 = vertexBuffer[index1].position;
float3 v2 = vertexBuffer[index2].position;
// ローカル座標をワールド座標に変換
v0 = mul(localToWorldMatrix, float4(v0, 1.0)).xyz;
v1 = mul(localToWorldMatrix, float4(v1, 1.0)).xyz;
v2 = mul(localToWorldMatrix, float4(v2, 1.0)).xyz;
// Y座標が0以下の頂点が含まれるかチェック
if (v0.y <= 0 || v1.y <= 0 || v2.y <= 0)
{
// 面のカウンタをインクリメント
InterlockedAdd(countBuffer[0], 1);
}
}
上記の場合は1つのスレッドグループに対して1×1×1のスレッドが実行されています。
つまり実質1つのスレッドのみ実行されているということです。
〇スレッドとは?
スレッドとはプログラムの実行単位を指します。
非同期処理などで用いられるマルチスレッドなどのスレッドです。
コンピュートシェーダーの場合はスレッドはGPU上で実行される計算最小単位を指し、書くスレッドは特定の計算処理を担当しながら最終的な処理を行います。
スレッドは3次元のグリッドとして構築されており、並列に実行されています。
今回は概要をつかむため細かい処理は次回の記事で行います。
〇参考
https://qiita.com/satoruhiga/items/b048e3ac17fac5f5a817
https://ics.media/entry/18467/
https://blog.yucchiy.com/2019/01/03/tutorial-for-unity-compute-shader/