スポンサーリンク
Unity C#・スクリプト実装

UnityでGPUを活用するCompute Shaderの基礎と実装例

Unity C#・スクリプト実装

1. はじめに

Unityでゲームを開発していると、「もっと処理を速くしたい!」と思う瞬間はありませんか?
たとえば大量のエフェクト、物理シミュレーション、AIの行動計算など、CPUだけではどうしても重くなってしまう処理があります。そこで活躍するのがCompute Shader(コンピュートシェーダー)です。

Compute ShaderはGPUの並列処理性能を直接活用できる仕組みで、Unityでも利用可能です。通常のシェーダーとは異なり、画面描画に縛られず自由にデータ処理ができるため、「描画+計算」を効率的に分担させられます。結果として、ゲームの表現力と処理速度を同時に向上できるのが魅力です。

この記事では、Compute Shaderの基礎から実際の実装例、さらに最適化のテクニックまで、初心者でも分かりやすい形で解説していきます。GPUプログラミングの世界に一歩踏み出したい方は、ぜひ最後まで読んでみてくださいね。

なお、より深く学びたい方には書籍「Unityゲーム プログラミング・バイブル 2nd Generation」もおすすめです。実践的なサンプルと解説が豊富で、GPU処理や最適化の理解にも役立ちます。
Amazonでチェックする|✅ 楽天でチェックする


2. Compute Shaderとは?

2-1. 定義と役割

Compute Shader(コンピュートシェーダー)は、GPU上で実行されるプログラムの一種です。
通常のシェーダー(頂点シェーダーやフラグメントシェーダー)は画面描画の流れに沿って動作しますが、Compute Shaderは描画処理に依存せず、純粋な計算処理をGPUで並列実行できるのが大きな特徴です。

そのため、大量のデータ処理や物理演算、パーティクルシステム、ポストエフェクトなど、CPUでは負荷の大きい処理をオフロードするのに最適です。

2-2. Unityで使うメリット

  • 超並列処理による高速化: 数万~数百万のスレッドを同時に実行可能
  • 描画以外の処理に活用: AIの行動計算やシミュレーション、ノイズ生成など
  • URP/HDRPとの相性: ポストプロセスやカスタムエフェクトの実装が容易

2-3. 技術的基盤

UnityのCompute Shaderは、DirectX 11以降のDirectComputeやOpenGLのCompute機能と似た技術基盤を持ちます。HLSL(High Level Shader Language)をベースに記述し、プロジェクトでは.computeファイルとして管理されます。

スクリプトからは ComputeShader クラスを通じてアクセスし、FindKernel()Dispatch()を使って処理を呼び出します。また、ComputeBufferを用いてデータをGPUに送受信するのが一般的な流れです。

2-4. こんな人におすすめ

– GPUを活用してゲームのパフォーマンスを上げたい
– パーティクルやポストエフェクトをよりリアルにしたい
– 機械学習や数値シミュレーションをUnity上で実装してみたい




3. 実装の基本手順

ここからは、Compute Shaderを実際に使うための基本的な流れを整理していきます。
Compute Shaderは「GPU側(.computeファイル)」と「CPU側(C#スクリプト)」の2つを組み合わせて動作させます。

3-1. GPU側(.computeファイル)の書き方

  1. カーネルの宣言
    #pragma kernel CSMain のように処理を実行する関数を定義します。
  2. スレッド数の指定
    [numthreads(1, 1, 1)] でスレッドグループ内の並列数を決定します。
  3. バッファの定義
    RWStructuredBuffer<float3> Result; のように、処理結果を格納するバッファを宣言します。
  4. 処理内容の記述
    SV_DispatchThreadID を使って現在のスレッドIDを取得し、配列インデックスを計算します。
  5. 結果の書き込み
    例:Result[index] = float3(id.x / 255.0, id.y / 255.0, 1.0);

3-2. CPU側(C#スクリプト)の実装

  1. カーネルインデックスを取得
    int kernel = computeShader.FindKernel("CSMain");
  2. ComputeBufferを作成
    new ComputeBuffer(count, Marshal.SizeOf<Vector3>());
  3. バッファをシェーダーに結合
    computeShader.SetBuffer(kernel, "Result", buffer);
  4. シェーダーを実行
    computeShader.Dispatch(kernel, 256, 256, 1);
  5. 結果を取得
    buffer.GetData(data); を使い、CPU側に計算結果を転送します。
  6. 後始末
    buffer.Release(); を忘れずに呼び出しましょう。

このように「GPUで計算 → CPUに結果を渡す」という流れを意識すれば、基本的なCompute Shaderをすぐに試すことができます。




4. 実装例:256×256のグリッドをGPUで計算する

ここでは、256×256 の格子状データを Compute Shader で色(float3)に変換し、CPU側へ取り出す最小構成の例を作ります。
「GPUで並列計算 → CPUで結果確認」という一連の流れがつかめます。

4-1. Compute Shader(.compute)の作成

まずはGPU側の処理を書く.computeファイルを用意します。ここで各スレッドがどの配列要素を計算し、どのように結果を書き込むかを定義します。

手順

  1. プロジェクトウィンドウを右クリック「Create」→「Shader」→「Compute Shader」を選び、GridColor.compute と名前を付けます。
  2. 作成したファイルをダブルクリックで開き、下記コードに置き換えます。
// GridColor.compute
#pragma kernel CSMain

// CPUから渡すサイズ情報
uint _Width;
uint _Height;

// 結果を書き込むバッファ(float3: RGB)
RWStructuredBuffer&lt;float3&gt; Result;

// スレッドグループ内のスレッド数(8×8×1)
// → Dispatch 側は「グループ数」を指定します(幅/8, 高さ/8)
[numthreads(8, 8, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
    // 範囲外アクセス防止
    if (id.x &gt;= _Width || id.y &gt;= _Height) return;

    // 2次元座標 → 1次元インデックス
    uint index = id.x + id.y * _Width;

    // 0..1に正規化してグラデーション色を作成
    float r = (float)id.x / max(1, (_Width  - 1));
    float g = (float)id.y / max(1, (_Height - 1));
    float b = 1.0;

    // 結果を書き込み(GPU→CPUで後で読む)
    Result[index] = float3(r, g, b);
}

ポイント

  • #pragma kernel:呼び出す関数名(ここでは CSMain)を宣言します。
  • [numthreads(8,8,1)]:1グループ内の並列スレッド数です。後で Dispatch() ではピクセル数ではなくグループ数を指定します(例:幅256なら 256/8=32)。
  • SV_DispatchThreadID:ワールド座標系のスレッドID。画像のピクセル座標のように扱えます。
  • 境界チェック:端数切り上げでDispatchした際も安全にするため、id.x >= _Width などのチェックを必ず入れましょう。
  • RWStructuredBuffer<float3>:ここに計算結果を格納します。CPU側の ComputeBuffer と名前でバインド(SetBuffer)します。

ここまでで、GPU側の「色配列を計算して書き込む」最小限のカーネルが完成です。次の章(4-2)で、C#からこのカーネルへサイズやバッファを渡し、Dispatch で実行して結果を受け取ります。

4-2. C#スクリプトの作成と実行

  1. プロジェクトウィンドウを右クリック「Create」→「C# Script」を選んで、新しいスクリプトを作成し、ComputeGridExample と名前を付けます。
  2. 内容を以下に置き換えます:
using System;
using UnityEngine;

public class ComputeGridExample : MonoBehaviour
{
    [SerializeField] private ComputeShader computeShader;

    private ComputeBuffer _buffer;
    private Vector3[] _cpuData;

    const int Width  = 256;
    const int Height = 256;
    const int Threads = 8; // numthreads と揃える

    void Start()
    {
        // カーネル取得
        int kernel = computeShader.FindKernel("CSMain");

        // 出力用バッファ(要素数×1要素のバイトサイズ)
        int count  = Width * Height;
        int stride = sizeof(float) * 3; // Vector3 = 3 floats
        _buffer    = new ComputeBuffer(count, stride);

        // CPU側の受け皿
        _cpuData = new Vector3[count];

        // シェーダーへパラメータをセット
        computeShader.SetInt("_Width",  Width);
        computeShader.SetInt("_Height", Height);
        computeShader.SetBuffer(kernel, "Result", _buffer);

        // Dispatch(グループ数)。各軸で Width/Threads を切り上げ
        int groupX = Mathf.CeilToInt((float)Width  / Threads);
        int groupY = Mathf.CeilToInt((float)Height / Threads);
        computeShader.Dispatch(kernel, groupX, groupY, 1);

        // GPU→CPUへ転送
        _buffer.GetData(_cpuData);

        // 確認用ログ(先頭/中央/末尾の一部だけ)
        Debug.Log($"Result[0]     = {_cpuData[0]}");
        Debug.Log($"Result[mid]   = {_cpuData[(Height/2)*Width + (Width/2)]}");
        Debug.Log($"Result[last]  = {_cpuData[count-1]}");
    }

    void OnDestroy()
    {
        _buffer?.Release();
        _buffer = null;
    }
}

実行手順:

  • 空のGameObjectを作成し、ComputeGridExampleドラッグ&ドロップでアタッチします。
  • Inspector の Compute Shader 欄に先ほどの GridColor.compute を割り当てます。
  • 再生して Console にベクトル値が出力されればOK。色の配列がCPU側へ届いています。

開発効率&デバッグを底上げするおすすめツール

4-3. よくあるハマりどころ

  • strideのミス: ComputeBuffer(count, stride)stride は「1要素のバイト数」。Vector3 なら sizeof(float)*3(=12)を指定します。
  • numthreadsとDispatchの不一致: [numthreads(8,8,1)] にしたら、Dispatch(width/8, height/8, 1) のように「グループ数」で呼びます(端数は切り上げ)。
  • Release忘れ: ComputeBuffer はGCされません。OnDestroy()Release() を必ず呼びます。

このサンプルをベースに、Result をテクスチャへ書き出したり、URPのポストプロセスへ応用したりと、さまざまな拡張が可能です。次章では、ガウシアンブラーを題材にした最適化テクニックを紹介します。


5. 最適化テクニック(ガウシアンブラーの実践)

ここからは、ガウシアンブラー(横・縦の2パス)を例に、Compute Shaderのチューニング手順を段階的に紹介します。目的は「どこを触ると速くなるのか」を具体的に掴むこと。
計測ツール(PIX / RenderDoc / Nsight など)で実測→修正→再計測のループを回すのが基本です。

ベースライン(例)
最初の実装:~2,919,840 ns(横ブラー1パスの一回実行あたりの目安)
以降の各ステップで、どのくらい短縮できたかを確認しながら進めます。

5-1. Thread Group Shared Memory(TGSM/共有メモリ)の活用

ボトルネックになりやすいのが、グローバルメモリ(テクスチャ)へのランダムアクセス。スレッドグループ共有メモリ(groupshared)に1ライン(またはタイル)をキャッシュしてから畳み込みを行うと、帯域消費と待ち時間を大幅に削減できます。

groupshared float4 lineCache[LINE_MAX];  // 例:横ブラー用
[numthreads(128,1,1)]
void CSMain(uint3 tid : SV_DispatchThreadID, uint3 gid : SV_GroupThreadID)
{
    // 1. グローバルから共有メモリにロード
    lineCache[gid.x] = Source[int2(tid.x, tid.y)];
    GroupMemoryBarrierWithGroupSync();

    // 2. 畳み込みは lineCache から読む
    float4 sum = 0;
    for (int k = -R; k &lt;= R; k++) {
        sum += weight[abs(k)] * lineCache[clamp(gid.x + k, 0, LINE_MAX-1)];
    }
    Result[int2(tid.x, tid.y)] = sum;
}

効果(例): ~2,919,840 ns → ~2,435,072 ns

5-2. サンプリング回数の間引き

半径が大きいブラーはサンプル数が増えて計算量が膨らみます。最大サンプル数の上限や、sampleStep を導入して「1,2,3…」ではなく「1,3,5…」のように間引くと、見た目を保ちつつ高速化できます(強いブラーで格子状アーティファクトが出やすい点は留意)。

for (int s = sampleStep; s &lt;= R; s += sampleStep) {
    float w = weight[s];
    sum += w * (lineCache[x - s] + lineCache[x + s]);
    wsum += 2*w;
}

効果(例): ~2,435,072 ns → ~1,905,920 ns

5-3. 対称性を利用して計算を半分に

ガウス重みは左右対称です。そこで片側だけループして左右を同時に足し合わせ、最後に重み合計を倍にする方法で、乗算や分岐の回数を削減できます。

// s = 1..R だけ回す(0は中心)
for (int s = 1; s &lt;= R; s++) {
    float w = weight[s];
    sum  += w * (lineCache[x - s] + lineCache[x + s]);
    wsum += 2*w;
}
sum /= (wsum + weight[0]);   // 中心の重みを最後に加味

効果(例): ~1,905,920 ns → ~1,604,448 ns

5-4. numthreads と 占有率(Occupancy)の調整

スレッドグループのサイズ([numthreads(x,y,z)])は、TGSM使用量・レジスタ数とトレードオフ。大きすぎると同時実行グループ数が減り、占有率が下がります。GPU(例:RTX 2070 SUPER / TGSM 32KiB 目安)の特性を踏まえて、128~512程度を目安に試行し、同時実行数×一回あたり効率のバランスが最良となる点を探ります。

効果(例): ~1,604,448 ns → ~1,308,608 ns

5-5. 測定と検証のチェックリスト

  • 境界アクセス: 共有メモリの読書きは範囲チェック/パディングで安全に。
  • メモリアクセスの並び: 可能な限り連続アクセス(coalescing)を意識。転置パスは特に注意。
  • レジスタ圧: 一時変数やunrollのやり過ぎはレジスタ増→占有率低下に繋がります。
  • 分岐の削減: 条件分岐はマスク化や事前計算で最小化。
  • プロファイラ: GPU専用の計測(イベントタイムライン / メモリ帯域 / 共有メモリヒット)を見る。

作業を快適にするおすすめアセット


6. さらに使える最適化Tips

ここからは、Compute Shaderをチューニングする際に知っておくと便利な追加テクニックをまとめます。実際のプロジェクトで「あともう一歩速くしたい」というときの参考にしてください。

6-1. ループのunroll(展開)

HLSLには [unroll] 属性を付けて、ループをコンパイル時に展開する機能があります。条件分岐やインクリメントのオーバーヘッドを削減できる場合があります。
ただし、展開によりレジスタ消費が増え、かえってOccupancy(同時実行効率)が下がる場合もあるので注意が必要です。

6-2. バンク衝突を避ける

NVIDIA GPUなどでは、共有メモリ(TGSM)が複数のバンクに分かれています。同じバンクに同時アクセスすると衝突して待ち時間が発生します。
インデックスを工夫して「連続アクセス」や「バンクごとの均等分散」を意識することで、メモリアクセスをスムーズにできます。

6-3. Memory Coalescing(連続アクセス)

グローバルメモリにアクセスする際は、スレッドごとにランダムな位置を読むのではなく、連続領域をまとめてアクセスすることで転送が最適化されます。
たとえば行方向のアクセスは高速ですが、列方向(転置アクセス)は非効率になる傾向があるため、処理順序を工夫すると効果的です。

6-4. 条件分岐の削減

GPUは条件分岐が多いと「スレッドごとに実行を分けて待機」するため効率が落ちます。場合によっては分岐をマスク処理や数式で置き換えると高速化することがあります。

6-5. 計測ツールの活用

「この最適化は本当に効いているのか?」を判断するには、必ず計測が必要です。Unity標準のProfilerだけでなく、PIX, RenderDoc, NsightなどのGPU専用ツールを併用して、帯域使用率・レジスタ消費・占有率を観察することが大切です。




7. まとめ

本記事では、UnityでCompute Shaderを使うための基礎と実装例、さらに実践的な最適化手法について解説しました。ポイントをおさらいすると以下のとおりです。

  • Compute ShaderはGPU上で並列処理を実行でき、描画に縛られない自由度が高い
  • 基本手順は「カーネル定義 → バッファ準備 → Dispatch実行 → CPUへ結果取得」
  • サンプルとして256×256のグリッド処理を紹介し、CPU/GPUの連携方法を解説
  • 最適化は共有メモリの活用 → サンプリング削減 → 計算重複削除 → numthreads調整と段階的に進めると効果的
  • 最終的なパフォーマンス向上には、必ずプロファイラやGPU専用ツールで実測することが重要

Compute Shaderを使いこなせば、大量のパーティクルや高度なポストエフェクト、物理シミュレーションなども快適に動かせるようになります。GPUの力を味方につけて、あなたのゲームを一段上の表現に仕上げてみてくださいね!


より深くUnityのプログラミングを学びたい方には、こちらの書籍がおすすめです:
Unityゲーム プログラミング・バイブル 2nd Generation
Amazonでチェックする|✅ 楽天でチェックする


あわせて読みたい

Compute Shaderを理解したら、さらにステップアップとして以下の記事もチェックしてみてください。GPUや並列処理に関連するテーマを深掘りでき、パフォーマンスチューニングの幅が広がります。


よくある質問(FAQ)

Q
Compute Shaderと通常のShader Graphはどう違うの?
A

Shader Graphやフラグメントシェーダーは主に「描画処理」に特化しています。一方でCompute Shaderは、描画に縛られず任意のデータをGPUで並列処理できる点が大きな違いです。つまり「画面に表示する前の純粋な計算」にも使えるのが特徴です。

Q
Compute Shaderはモバイルでも使える?
A

多くの最新モバイルGPU(Metal / Vulkan / OpenGL ES 3.1以降対応)では利用可能です。ただし、機種によっては対応していない場合や、PCに比べてパフォーマンスが制限されることもあります。モバイル向けに使う際は、フォールバック処理を用意しておくのが安全です。

Q
Job SystemやECSと組み合わせられる?
A

はい、可能です。CPU側の大量データ処理をECSやJob Systemで分担し、さらにGPUへCompute Shaderでオフロードする設計はよく使われる手法です。
たとえば「CPUでAIの初期計算 → GPUで数千体の物理挙動を並列処理」というように役割を分けると、マルチコアCPU+GPUの両方を最大限活用できます。

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

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

スポンサーリンク