🪄

【Unity】「なんとなく」を卒業する。4種類のブラーシェーダーの処理を紐解いてみた。

に公開

はじめに

Unityで映像制作やゲーム開発をしているとよくある「ぼかし(ブラー)」の表現ですが、『アセット等で使ってはいるものの、処理の中身はなんとなくしか知らない』『用途に合わせて適切なブラーを選べるようになりたい』といった方に向けて、今回はシェーダーにおけるブラーの仕組みを基礎から紐解いていこうと思います。

ちなみに自分もシェーダー表現に関して詳しくはない為、ちゃんと調べるまで「なんか色混ぜてるんでしょ?」とか「畳み込みってやつやってるんでしょ?」くらいしか思ってなかったので、読んでいる方も一緒に学んでいきましょう。

筆者のプロフィール
  • 1社目:ソーシャルゲーム開発(7年半)。PHP, C#(Unity), MySQLを中心にリードエンジニア等も経験。
  • 2社目:映像系のUnityエンジニアへ転職。表現回りの知識も勉強中

ブラーの原理

シェーダーで「ぼかす」とは、一言で言うと**「周りのピクセルの色を混ぜ合わせる」**処理のことです。

水彩絵の具で色を塗った後、指でこすって隣の色と混ぜる感覚に近いですね。
通常、シェーダーは自分自身のピクセルの色だけを見ますが、ブラーの場合は「お隣さんは何色かな?」と周囲の色を参考にします。そして、みんなの色を均等に混ぜることで、モヤッとした(ぼやけた)状態が生まれます。

ブラーの原理の図解

このように、対象のピクセルに対して周囲のピクセルの色を「サンプリング(抽出)」し、それらを足し合わせて平均をとることで、色が混ざり合った新しい色を作り出します。

この「周りのピクセルを一定の範囲で集めて、決められた割合で混ぜる」計算を、専門用語で畳み込み(Convolution) と呼びます。

ブラーの種類

一口にブラーと言っても、計算方法によっていくつかの種類があります。それぞれの特徴を比較表にまとめました。

手法名 特徴 主な用途
ボックスブラー 全ピクセルを均等に平均化。実装が最もシンプル。 負荷を抑えたい簡易的なぼかし
ガウスブラー 距離に応じた重み付け。高品質で非常に滑らか。 UI背景、高品質なポストエフェクト
Kawase Blur ダウンサンプリングと複数パスを組み合わせ。効率的。 広範囲のボケ
ラジアルブラー 中心から放射状にサンプリング。 スピード感の演出、集中線効果

それぞれの詳細解説とソースコード

今回、各ブラーの効果を比較するために、以下のオリジナル画像にそれぞれのシェーダーを適用しました。画像がどのように変化するか見比べてみてください。

Original Image

1. ボックスブラー(Box Blur)

一言で言うと**「ご近所さん全員で平等に割り勘する」方式**です。
名前の通り、自分を中心とした正方形の範囲(ボックス)にいるピクセルを「みんな平等」に足して、人数で割ります。

特徴: 実装がとても簡単で単純!…なのですが、みんなを平等に扱いすぎるため、ボケ方が少し「四角っぽく(格子状に)」不自然に見えてしまうことがあります。

ボックスブラーの図解

// 3x3のボックスブラーの例  
float4 frag (v2f i) : SV_Target  
{  
    float2 texelSize = _MainTex_TexelSize.xy;  
    float4 color = 0;

    // 縦横3ピクセルずつ、合計9ピクセルをサンプリング  
    for (int x = -1; x <= 1; x++)  
    {  
        for (int y = -1; y <= 1; y++)  
        {  
            color += tex2D(_MainTex, i.uv + float2(x, y) * texelSize);  
        }  
    }  
    return color / 9.0; // 9ピクセルの平均値を返す  
}

💡 幕間:「軽くする3種類の工夫」

ここで少し、裏側の「軽くするための工夫」の話をいくつか挟みます。

ブラーで「もっと広くぼかしたい!」と範囲を広げると、計算量が爆発してGPUがパンクしてしまいます。
例えば、15x15ピクセル分の広さをぼかそうとすると、1ピクセルにつき 15 \times 15 = 225 回 も「お隣さんは何色?」と聞くハメになります。
そして画像サイズが大きくなればなるほど、この問題は深刻になります。

そこで現場のエンジニアは、以下の**「3つの魔法(最適化テクニック)」**を駆使して、綺麗さと軽さを両立させています。この後の解説で重要になるので、サッと目を通してみてください。

① 2パス化(計算を分ける)

「縦横いっぺんにやるから大変。なら、横にぼかしてから、その結果を縦にぼかせばいいじゃない」という算数の工夫です。

2パス化の図解

まず「横方向だけ」ぼかした画像を作って保存し、次にその画像に対して「縦方向だけ」ぼかします(2段階に分ける=2パス)。
これなら計算回数は 15 + 15 = 30 回 で済みます!

② ダウンサンプリング(画素を減らす)

ぼかす前に、あえて画像を小さく(低解像度に)縮小しておくテクニックです。画面サイズが半分になれば、計算しなきゃいけないピクセルの総数は 1/4 になります。後で元のサイズに引き伸ばす(アップサンプリングする)ことで、処理を圧倒的に軽くします。

③ サンプリング距離を飛ばす(歩幅を広げる)

ソースコードに _BlurSize や _Offset といった数値を掛け合わせてサンプリング数を増やさずにボケを大きくする手法です。

「1ピクセル隣」ではなく「2ピクセル隣、4ピクセル隣…」と間のピクセルを飛ばして大股で色を拾います。計算回数を増やさずにボケを大きくできますが、やりすぎると像が分身して重なる「ゴースト現象」が起きてしまう諸刃の剣です。

上記の軽量化の実装イメージ

【C#側のコードイメージ】

// 縮小サイズを計算(ダウンサンプリング)
int width = source.width / 2;
int height = source.height / 2;
RenderTexture tempRT1 = RenderTexture.GetTemporary(width, height);
RenderTexture tempRT2 = RenderTexture.GetTemporary(width, height);

// 縮小バッファへ一度コピー
Graphics.Blit(source, tempRT1);

// サンプリング距離を飛ばす(オフセット)
blurMaterial.SetFloat("_Offset", 1.5f);

// 1パス目:横方向にぼかして、tempRT2に書き込む
blurMaterial.SetVector("_Direction", new Vector2(1, 0));  
Graphics.Blit(tempRT1, tempRT2, blurMaterial);

// 2パス目:縦方向にぼかして、最終的な出力先(destination)に書き込む  
blurMaterial.SetVector("_Direction", new Vector2(0, 1));  
Graphics.Blit(tempRT2, destination, blurMaterial);

// 使い終わった一時バッファを解放  
RenderTexture.ReleaseTemporary(tempRT1);
RenderTexture.ReleaseTemporary(tempRT2);

【Shader側のコードイメージ(抜粋)】

// C#から渡される方向ベクトルとサンプリング距離
float2 _Direction; 
float _Offset;

float4 frag (v2f i) : SV_Target  
{  
    // 方向と距離を掛けてサンプリング用の単位ベクトルを作る
    float2 unit = _MainTex_TexelSize.xy * _Offset * _Direction;
      
    float4 color = 0;
    // ...このunitを使って周辺ピクセルを1次元でサンプリングする...
    for (int j = -2; j <= 2; j++) {
        color += tex2D(_MainTex, i.uv + unit * j);
    }
    return color / 5.0;
}

このように、C#側と連携する処理を書くことで工夫ができます。

Box Blur(軽量化版)

2. ガウスブラー(Gaussian Blur)

ボックスブラーの「全員平等」による四角い跡をなくすために使われるのがガウスブラーです。

一言で言うと**「自分に近い人の意見を重宝するエコヒイキ方式」**です。
「自分に一番近い隣人は濃く(重み大)、遠く離れた隣人は薄く(重み小)」混ぜ合わせることで、カメラなどで撮ったような滑らかで自然なボケ味を作ります。

この「近ければ濃く、遠ければ薄く」の割合を決めるカーブを「ガウス分布(正規分布)」と呼びます。グラフにすると、釣鐘(ベル)のような綺麗な形になります。

ガウス分布の図解

特徴: 距離に応じて重み付けをするため、非常に滑らかで美しいボケが得られます。計算は複雑になりますが、先ほどの「2パス化」や「ダウンサンプリング」、「サンプリング距離を飛ばす」といったテクニックを組み合わせることで、綺麗さと軽さを両立できます。(※サンプリング距離を飛ばすテクニックを使うとゴースト現象は起きるので注意です)

【C#側のコードイメージ】
※ここで紹介するガウスブラーは、先ほど解説した 『2パス化・ダウンサンプリング・サンプリング飛ばし』をすべて盛り込んだ実用版 のコードです。

// 縮小サイズを計算(ダウンサンプリング)
int width = source.width / 2;
int height = source.height / 2;
RenderTexture tempRT1 = RenderTexture.GetTemporary(width, height);
RenderTexture tempRT2 = RenderTexture.GetTemporary(width, height);

// 縮小バッファへコピー
Graphics.Blit(source, tempRT1);

// オフセット指定
blurMaterial.SetFloat("_Offset", 1.0f);

// 1パス目 (横)
blurMaterial.SetVector("_Direction", new Vector2(1, 0));
Graphics.Blit(tempRT1, tempRT2, blurMaterial);

// 2パス目 (縦)
blurMaterial.SetVector("_Direction", new Vector2(0, 1));
Graphics.Blit(tempRT2, destination, blurMaterial);

RenderTexture.ReleaseTemporary(tempRT1);
RenderTexture.ReleaseTemporary(tempRT2);

【Shader側のコードイメージ】

// 5-tap ガウスブラーの重み(あらかじめ数式から計算しておいたもの)  
static const float weights[3] = { 0.375, 0.25, 0.0625 };

// C#から渡される方向とサンプリング距離
float2 _Direction;
float _Offset;

float4 frag (v2f i) : SV_Target  
{  
    // 方向と距離を掛け合わせた単位ベクトル
    float2 unit = _MainTex_TexelSize.xy * _Offset * _Direction;   
      
    // 中心のピクセル(一番重みが大きい)  
    float4 col = tex2D(_MainTex, i.uv) * weights[0];

    // 左右(または上下)にピクセルをずらしながら、重みを掛けて足し合わせる  
    col += tex2D(_MainTex, i.uv + unit * 1.0) * weights[1];  
    col += tex2D(_MainTex, i.uv - unit * 1.0) * weights[1];  
    col += tex2D(_MainTex, i.uv + unit * 2.0) * weights[2];  
    col += tex2D(_MainTex, i.uv - unit * 2.0) * weights[2];

    return col;  
}

Gaussian Blur(軽量化版)

※オフセットを広げすぎた場合(ゴースト現象の例)
Gaussian Blur(ゴースト現象)

3. Kawase Blur

川瀬氏というすごい方が考えた、「少しずつ大股で歩きながらぼかす」 という画期的な手法です。
ダウンサンプリングで画像を小さくしてから、斜め4方向の色を拾ってくる処理を数回繰り返します。
特徴: ガウスブラーのように複雑な「エコヒイキ計算」をすることなく、「回数を重ねるごとに、色を拾ってくる距離(オフセット)をどんどん広げていく」 だけで、メチャクチャ少ない計算回数でとても広い範囲を綺麗にぼかすことができます。ゲームが光って見える「Bloom(ブルーム)」などの表現の定番です。

  • 1ピクセルあたりの計算量: 約8〜12回(4回のサンプリング × 2〜3回の重ね掛け)
  • 見た目の特徴: 少ない計算回数で、驚くほど広い範囲がボケます。Bloom(発光表現)の定番です。

Kawase Blur

【C#側のコードイメージ(ループ処理)】

int iterations = 4; // ぼかし処理を繰り返す回数  
float offset = 1.0f; // サンプリング距離の基本値

// 最初から半分の解像度でスタートすることで負荷を大きく下げる(ダウンサンプリング)  
int width = source.width / 2;  
int height = source.height / 2;  
RenderTexture currentRT = RenderTexture.GetTemporary(width, height);  
Graphics.Blit(source, currentRT);

// 指定回数だけパスを繰り返す  
for (int i = 0; i < iterations; i++)  
{  
    RenderTexture nextRT = RenderTexture.GetTemporary(width, height);  
      
    // Kawase Blurのキモ:繰り返すたびにオフセットを広げていく  
    blurMaterial.SetFloat("_Offset", offset + i);  
    Graphics.Blit(currentRT, nextRT, blurMaterial);  
      
    RenderTexture.ReleaseTemporary(currentRT);  
    currentRT = nextRT;  
}

Graphics.Blit(currentRT, destination);  
RenderTexture.ReleaseTemporary(currentRT);

【Shader側のコードイメージ】

float _Offset; // C#から渡されるサンプリング距離

float4 frag (v2f i) : SV_Target  
{  
    float2 res = _MainTex_TexelSize.xy;  
      
    // 斜め4方向(右上、左上、右下、左下)へのオフセットを計算  
    // ループが進むにつれて _Offset が大きくなるため、より遠くの色を拾うようになる  
    float4 d = res.xyxy * float4(-1, -1, 1, 1) * _Offset;

    float4 col = 0;  
    col += tex2D(_MainTex, i.uv + d.xy); // 左下  
    col += tex2D(_MainTex, i.uv + d.zy); // 右下  
    col += tex2D(_MainTex, i.uv + d.xw); // 左上  
    col += tex2D(_MainTex, i.uv + d.zw); // 右上

    return col * 0.25; // 4点の平均  
}

💡 コラム:たった4点しか拾わないのに綺麗にぼける秘密

上のプログラムを見ると、tex2Dで色を拾う回数はたったの 4回 しかありません。
「4ピクセルしか混ぜていないのに、なんであんなに滑らかにぼやけるの?」と疑問に思うかもしれません。

実は、サンプリングする位置(d.xy や d.xw など)に秘密があります。
これは「次のピクセルのど真ん中」ではなく、あえて**「4つのピクセルが重なる十字の境目」**を斜めに狙ってサンプリングしています。

ピクセルの境目(中途半端な位置)をサンプリングすると、GPUの**「バイリニアフィルタリング」**という機能が自動で働き、「境目にある4つのピクセルの色を勝手に均等に混ぜ合わせた色」を1回で返してくれます。

つまり、Shader上で4回色を拾っているだけで、裏側ではGPUが 4回 × 4ピクセル = 実質16ピクセル分 の色をブレンドしてくれているのです。これがKawase Blurが「超軽量なのに綺麗にぼやける」最大の魔法です。

4. ラジアルブラー(Radial Blur)

一言で言うと**「ワープホールに吸い込まれる時みたいなヤツ」**です。
画面の真ん中から外側へ向かって、ズギャン!と一直線上に並んだ色を拾ってきて混ぜ合わせます。

ラジアルブラーの図解

特徴: 爆発の衝撃波や、車を猛スピードで運転しているときの表現など、「画面の真ん中にグッと視線を誘導したいとき」に最適です。

Radial Blur

float4 frag (v2f i) : SV_Target  
{  
    // 中心(0.5, 0.5)から現在のピクセルへの方向ベクトル  
    float2 dir = i.uv - 0.5;  
    float4 col = 0;  
    const int SAMPLES = 10;

    for (int j = 0; j < SAMPLES; j++)  
    {  
        // 徐々に中心に向かってサンプリング位置をずらす  
        float dist = 1.0 - _BlurStrength * (j / (float)SAMPLES);  
        col += tex2D(_MainTex, 0.5 + dir * dist);  
    }  
    return col / SAMPLES;  
}

※ 余談ですが、この 0.5 の中心座標をC#側からプレイヤーの座標として渡してあげると、キャラクターを中心に景色が流れるようなスピード線の表現が作れます

🔍 関連アイテム

リーダブルコード
Unityゲーム プログラミング・バイブル
Unity ゲーム プログラミング・バイブル 2nd Generation
ゲームプログラマのためのコーディング技術
ゲームアプリの数学 Unityで学ぶ基礎からシェーダーまで

まとめ

今回はUnityにおける4つの主要なブラーシェーダーについて解説しました。

  • 実装の簡単さなら ボックスブラー
  • 正確さを求めるなら ガウスブラー
  • パフォーマンスと広がりを求めるなら Kawase Blur
  • 演出のアクセントには ラジアルブラー

といったように、用途に合わせて手法を選択できるようになると、Unityでの表現の幅がグッと広がります。 最初は難しく感じるシェーダーですが、基本は「お隣さんの色を混ぜる」というシンプルな考え方から始まっています。

今後は、ブラーをさらに応用した「被写界深度(DoF)」についてや、他のシェーダー表現にも触れていきたいと思います。

最後までお読みいただきありがとうございました!もしこの記事が面白いと思ったら、いいねやバッジなどで応援していただけると励みになります!


Discussion