スポンサーリンク
最適化・パフォーマンス

UnityのCompute Shaderとは?GPU並列処理の基本と実装サンプルまとめ

最適化・パフォーマンス

「Compute Shaderって名前は聞くけど、結局なにができるものなの?」そんな疑問を持ってこのページにたどり着いた方も多いのではないでしょうか。

UnityでゲームやアプリのCPU負荷が気になり始めると、必ずと言っていいほど目にするのがこの「Compute Shader」という言葉です。でも、HLSLもGPUの仕組みもよく分からないまま説明を読むと、専門用語の多さに手が止まってしまうこと、ありますよね。

この記事では、自分にCompute Shaderが本当に必要なのかを見極めるところから、最小構成で実際に動かす手順、用途に合わせた実装方法の選び方、そしてつまずきやすいポイントの対処法まで、順番に整理していきます。


Compute Shaderは必要?Job Systemとの違い

結論から言うと、Compute Shaderが向いているのは「やることは単純だけど、数がとにかく多い処理」です。逆に、条件分岐が多かったり処理の内容が複雑だったりする場合は、Job SystemとBurst Compilerの組み合わせのほうが扱いやすいことが多いです。

たとえば、こんな判断基準で考えてみてください。

  • 大量のパーティクルや粒子の位置計算など、同じ計算を何千回も繰り返す → Compute Shader向き
  • 敵AIの索敵判定やゲームロジックのように、条件分岐が多く処理内容がその都度変わる → Job System向き
  • 低スペック端末やGPUの対応状況が不安な環境も視野に入れたい → Job System向き

Compute ShaderはGPUの大量のコアを使って単純な計算を並列にさばくことが得意な一方、GPU側で複雑な条件分岐を行うと逆に効率が落ちてしまうことがあります。反対にJob Systemは、CPU側で動くぶん柔軟なロジックを組みやすいという特徴があります。

「うちのプロジェクトは複雑な処理が多いかも」と感じた方は、先にCPU側の並列化から検討してみるのも一つの手です。以下の記事でJob SystemとBurst Compilerの使い方を詳しく解説しているので、あわせて確認してみてください。

「自分の処理は単純作業の繰り返しだ」と判断できた方は、次はいよいよ実装に進んでいきましょう。




最小構成で動かす3ステップ

Compute Shaderの実装は、実はやることを分解するとシンプルです。大きく分けると次の3ステップだけで最小構成が動きます。

  • ①カーネルを定義する(.computeファイルにGPU側の処理を書く)
  • ②C#側からバッファを用意してDispatchで実行する
  • ③GetDataで計算結果をC#側に取り出す

まずは.computeファイル側のイメージです。

#pragma kernel CSMain

RWStructuredBuffer<float> Result;

[numthreads(64,1,1)]

void CSMain (uint3 id : SV_DispatchThreadID) { Result[id.x] = id.x * 2.0; }

C#側はこんな流れになります。

GraphicsBuffer buffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, 64, sizeof(float));
int kernel = computeShader.FindKernel("CSMain");
computeShader.SetBuffer(kernel, "Result", buffer);
computeShader.Dispatch(kernel, 1, 1, 1);

float[] results = new float[64];
buffer.GetData(results);
buffer.Release();

この流れさえ押さえておけば、あとは処理内容やバッファの中身を変えていくだけです。ここから、つまずきやすい3つのポイントを整理していきます。

カーネル・スレッド・スレッドグループの違い

実装を始めると「カーネル」「スレッド」「スレッドグループ」という似たような言葉が出てきて混乱しやすいところです。役割で整理すると、こんなイメージになります。

  • カーネル:GPUで実行される処理のかたまり(先ほどのCSMainのような関数単位)
  • スレッド:カーネルを実行する最小の単位
  • スレッドグループ:スレッドを束ねたグループ。C#側からDispatchするときに、このグループ数を指定する

先ほどのコードのnumthreads(64,1,1)は「1つのスレッドグループの中に64個のスレッドを用意する」という意味です。X・Y・Zの3方向で指定できるので、2Dのテクスチャを扱うときはnumthreads(8,8,1)のように縦横で分けて考えると扱いやすくなります。

Dispatchは即座に実行されない

ここは特に誤解しやすいポイントです。C#側でDispatchを呼んだ瞬間に計算が終わっている、と思ってしまいがちですが、実際はそうではありません。

Dispatchメソッドは実行の完了を保証するものではなく、GL.Flush()を明示的に呼ばない限り、実際の実行タイミングはそのフレームの最後になります。そのため「Dispatchした直後にGetDataで結果が取れない」「値がおかしい」といった不具合が起きた場合、まずはこの実行タイミングのズレを疑ってみてください。

ComputeBufferとGraphicsBufferどちらを使う?

もう一つ迷いやすいのが、バッファの種類選びです。結論としては、これから新しく実装するなら基本的にGraphicsBufferを選んでおくのが無難です。

GraphicsBufferは頂点バッファやインデックスバッファとしても扱える汎用性の高さが特徴で、Compute Shader以外の用途にも使い回しやすくなっています。一方のComputeBufferは以前から使われてきたクラスで、Unityの公式ドキュメント上でも変わらず提供が続いています。

「ComputeBufferはいずれ廃止される」という情報を目にすることもありますが、現時点で明確に非推奨と明記されているわけではないため、必ずしも急いで書き換える必要はありません。ただ、これから新規で書くコードについては、汎用性の高いGraphicsBufferを選んでおくと安心です。




用途別で選ぶ描画方式の決め方

Compute Shaderで計算した結果を、実際に画面へ描画する方法はひとつではありません。作りたいものによって適した方式が変わるので、ここでは代表的な4パターンを整理しておきます。

作りたいもの向いている方式
ライフゲームや画像処理などのグリッド計算RenderTexture(2D)
流体シミュレーションやボクセル表現RenderTexture(3D)
2DパーティクルシステムGraphicsBuffer + DrawProcedural
大量の3Dパーティクルや群れ(Boids)の表現GraphicsBuffer + DrawMeshInstancedIndirect

迷ったときは、次の2つの軸で考えてみるとイメージしやすくなります。

  • 2Dか3Dか:平面的なグリッド計算なら2D系、奥行きのある空間表現なら3D系
  • 個数の規模:数百個程度ならRenderTexture方式でも十分ですが、数千〜数万単位のオブジェクトを一括描画したい場合はDrawMeshInstancedIndirectのような、1回のドローコールでまとめて描画できる方式が向いています

たとえば、画面いっぱいに舞う花びらのような数百枚程度の表現ならGraphicsBuffer + DrawProceduralで十分ですが、戦場を埋め尽くすような大群衆やBoidsシミュレーションになると、DrawMeshInstancedIndirectを使わないとドローコールの数が膨れ上がり、かえって重くなってしまうことがあります。

「なんとなく重そうだから」で方式を選ぶのではなく、表現したいものの次元と個数の規模から逆算して選ぶと、余計な作り直しを避けやすくなります。

方式が決まったら、次は実装したあとに詰まりやすいポイントを見ていきましょう。




失敗しないための注意点とデバッグ法

Compute Shaderは実装できても、意外と厄介なのが「うまくいっているかどうかが目で見て分かりにくい」という点です。通常のシェーダーと違って画面に直接色を出す処理ではないため、正しく動いているのか、値がおかしいのかがぱっと判断しづらいんですよね。

そんなときにおすすめなのが、デバッグ専用のバッファを用意する方法です。

  • 検証したい値を格納するためのRWStructuredBuffer<float>を用意する
  • シェーダー内の確認したい箇所でその値を書き込む
  • C#側でGetDataを使って値を取得する
  • 取得した値をDebug.Logで出力して中身を確認する

「計算結果がおかしい」「思った通りに動かない」というときは、まずこの方法で実際にGPU側で何が起きているかを可視化してみると、原因の切り分けがぐっとしやすくなります。

もうひとつ、実装時に見落としがちなのがバッファの解放です。GraphicsBufferComputeBufferは使い終わったら必ずReleaseを呼んで解放する必要があります。

これを忘れてしまうと、GPU側のメモリが解放されないまま残り続けてしまい、少しずつメモリを圧迫していきます。シーンを何度も切り替えるようなゲームだと、プレイを続けるうちにじわじわと動作が重くなっていく、という形で表面化することもあります。

特に、毎フレームバッファを新しく生成してしまっているケースは要注意です。生成と解放のタイミングがずれていないか、一度コードを見直してみてください。バッファの管理やメモリまわりの最適化についてさらに詳しく知りたい方は、こちらの記事もあわせて参考にしてみてください。

デバッグの方法とバッファ管理さえ押さえておけば、Compute Shaderで「動いているのか分からない」「なんとなく重い」という状態にはまりにくくなります。




まとめ

Compute Shaderは、単純だけど数の多い処理をGPUに任せることで、CPU側の負担をぐっと軽くしてくれる仕組みです。まずは自分の処理が向いているかどうかを見極め、最小構成で一度動かしてみることが、理解への一番の近道になります。

用途に合った描画方式を選び、デバッグ用バッファとバッファの解放を意識しておけば、実装中に「なぜか動かない」「なぜか重い」といった状況にも落ち着いて対処できるはずです。まずは小さな処理から、実際にコードを書いて試してみてください。


よくある質問(FAQ)

Q
WebGLでもCompute Shaderは使えますか?
A

Unity 6以降であれば、WebGPUという仕組みを経由することでWebGL向けビルドでもCompute Shaderを利用できるようになっています。ただし、これは実験的な機能という位置づけで、対応しているブラウザやデバイスがまだ限られている点には注意が必要です。

また、有効化するための設定手順はUnityのバージョンによって異なることがあり、Player設定の画面上で完結する場合もあれば、プロジェクト設定ファイルを直接編集する必要がある場合もあります。WebGL向けに導入を検討する際は、自分が使っているUnityバージョンの公式マニュアルを確認しながら進めるのが確実です。

Q
Compute Shaderは全てのプラットフォームで使えますか?
A

Compute Shader自体は幅広いプラットフォームに対応していますが、実行するデバイスやグラフィックスAPIによっては対応していない場合もあります。実装前に、コード内でSystemInfo.supportsComputeShadersを使って対応状況を確認しておくと安心です。

この判定を入れておけば、万が一対応していない環境でアプリが動いた場合でも、いきなりエラーで落ちるのではなく、代替の処理に切り替えるといった対応がしやすくなります。

Q
URPとBuilt-inで書き方は変わりますか?
A

カーネルの定義やDispatchの呼び出しといったCompute Shaderの基本部分は、URPでもBuilt-inでも共通です。差が出てくるのは、計算した結果を画面に表示するために別のシェーダーへ渡す部分になります。

レンダーパイプラインごとにシェーダーの書き方や渡し方の作法が異なるため、表示用シェーダーを組む段階になったら、自分が使っているパイプラインに合わせた書き方になっているかを確認してみてください。

※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。

※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。

スポンサーリンク