【UE】計算式から紐解く、HLSLによる自作DoF(ミニチュア風、被写界深度)ポストプロセス
はじめに
最近は実際のコードを描かずにNeovim用UEプラグインばっかり開発しているのですが、neovimプラグインが落ち着いてきたところでコードを書かずにマウス片手にUnreal Engineのマテリアルつかって、ポストプロセスで被写界深度(DoF)エフェクトを自作してみました。
当初はCine Cameraを使い、望遠レンズでF値を下げて綺麗なボケ味を試していたのですが、これだとキャラクターとカメラの距離が離れすぎてしまい、ゲームプレイには向きません。そこで、ポストプロセスとしてHLSLで実装することにしました。
この記事では、その際に用いた計算式の変更から、HLSLによる具体的な実装、そしてボケ形状のカスタマイズまでの一連の流れをまとめます。
CoC(錯乱円)の計算式を使いやすく変更する
被写界深度を実装する上で核となるのが**錯乱円(Circle of Confusion, CoC)**です。これは、ピントが合っていない点がどのくらいボケて円形に写るか、その直径を示す値です。
一般的に使われるCoCの計算式は以下の通りです。
$$
CoC = \frac{f^2}{N} \frac{|P - S|}{S(P - f)}
$$
- $f$: レンズの焦点距離 (focal length)
- $N$: F値 (F-number)
- $P$: ピントが合っている面までの距離 (focus plane)
- $S$: 対象オブジェクトまでの距離 (subject distance)
この式は物理的に正確ですが、F値($N$)が分母にあるなど、個人的に直感的な調整がしづらいと感じました。そこで、今回は絞り(Aperture)の大きさを直接使える、以下の式に変更して使用しました。
$$
CoC = \frac{A \cdot f \cdot |P - S|}{S}
$$
- $A$: 絞りの大きさ (Aperture)
- 他の変数は上記と同様
こちらの式の方が、絞り($A$)が大きくなるほどCoCも大きくなるという関係性が分かりやすく、個人的には扱いやすかったです。
フォーカス幅によるボケ範囲の調整
次に、ピントが合っていると見なす「フォーカス幅」を導入し、ボケの範囲をコントロールできるようにしました。単純にピント位置との距離だけでボケさせると、ピントが合う領域がシビアになりすぎるためです。
具体的には、以下のような簡単な範囲判定をシェーダーに組み込みました。
$$
check = |D - F| < \frac{Region}{2.0}
$$
- $D$: シーンの深度 (Depth)
- $F$: フォーカスが合っている距離 (Focus Distance)
- $Region$: ピントが合っていると見なす幅 (Focus Region)
このcheckがtrueになる範囲(フォーカスが合っている領域)ではボケを適用せず、範囲外のピクセルに対してのみ、先ほどのCoCの計算結果に応じたブラー処理を適用します。これにより、ボケさせたい領域とさせたくない領域を明確に分けることができます。
HLSLによるディスクサンプリングの実装
ボケを表現するため、周辺のピクセルを円形(ディスク状)にサンプリングして色を混ぜ合わせる「ディスクブラー」を実装します。このサンプリングを効率的かつ均一に行うために、**黄金角(Golden Angle)**を利用した手法を用いました。
黄金角を使うと、少ないサンプル数でも放射状のムラがなく、均一にサンプル点を配置できます。
このサンプリング点を生成するHLSL関数がこちらです。
#define GOLDEN_ANGLE 2.3999632297 // (2 * PI) / (Golden Ratio^2)
/**
* 黄金角を利用してディスク状のサンプリング点を生成する
* @param Index 何番目のサンプルか (0から始まる)
* @param NumSamples 総サンプル数
* @return [-1, 1]の範囲に正規化された2D座標
*/
float2 GetRandomDiskSample(float Index, float NumSamples)
{
float Radius = sqrt(Index / NumSamples);
float Theta = Index * GOLDEN_ANGLE;
float2 SamplePoint;
SamplePoint.x = Radius * cos(Theta);
SamplePoint.y = Radius * sin(Theta);
return SamplePoint;
}
sqrt(Index / NumSamples)で中心から外側に向かってサンプル点が配置されるようにし、Index * GOLDEN_ANGLEで各サンプルを黄金角ずつ回転させることで、サンプル点同士が重なりにくく、均一な分布を実現しています。
最終的なシェーダーコードとボケ形状の適用
上記をすべて統合した最終的なシェーダーコードの抜粋がこちらです。今回は、よりリアルなボケ味を目指して、ボケの形状をテクスチャで指定できるようにしてみました。
// 関数群をまとめた構造体
struct Functions
{
float2 GetRandomDiskSample(float Index, float NumSamples)
{
float Radius = sqrt(Index / NumSamples);
float Theta = Index * 2.3999632297; // GOLDEN_ANGLE
float2 SamplePoint;
SamplePoint.x = Radius * cos(Theta);
SamplePoint.y = Radius * sin(Theta);
return SamplePoint;
}
};
Functions f;
// CoCの値(0-1)と最大ブラーサイズから、このピクセルのぼかし半径を計算
float CoC_Radius = CoC_Value * MaxBlurSize;
float4 FinalColor = float4(0, 0, 0, 0);
float TotalWeight = 0;
// 指定された回数だけ周辺ピクセルをサンプリングする
for (int i = 0; i < NumSamples; i++)
{
// 1. サンプリング用のオフセット座標を生成
float2 Offset = f.GetRandomDiskSample(i, NumSamples) * CoC_Radius;
// 2. シーンテクスチャから色を取得
float4 SampledColor = SceneTextureFetch(SceneTex.ID, UV + Offset);
// 3. (オプション) ボケ形状テクスチャを使って重みを計算
// Offsetを[-1, 1]から[0, 1]のUV座標に変換
float2 BokehUV = (Offset / CoC_Radius) * 0.5 + 0.5;
// ボケ形状テクスチャをサンプリングし、重みとして使用
float BokehWeight = length(BokehShapeTex.Sample(BokehShapeTexSampler, BokehUV));
// 4. 重みを考慮して色を加算
FinalColor += SampledColor * BokehWeight;
TotalWeight += BokehWeight;
}
// 加重平均をとって最終的な色を決定
if (TotalWeight > 0)
{
FinalColor /= TotalWeight;
}
return FinalColor;
Unreal Engineの標準のDoFエフェクトのような、絞り羽根の形(六角形など)をしたボケを再現しようと、BokehShapeTexを使って重み付けを行いました。
ただ、正直なところ、ボケ形状テクスチャの有無で劇的な見た目の変化はありませんでした。サンプル数が少ない場合や、ボケ半径が小さい場合は形状が分かりにくいため、パフォーマンスを考慮するなら、この処理は省略しても良いかもしれません。
まとめ
今回は、ポストプロセスマテリアルとHLSLを使って、オリジナルのDoFエフェクトを実装してみました。
- CoCの計算式を、物理的な正しさよりもアーティストの調整しやすさを重視した形に変更。
- フォーカス範囲の判定を導入し、意図した通りのボケを実現。
- 黄金角を用いたディスクサンプリングで、効率的で綺麗なボケ味を作成。
アセットについて
使用したアセットはコツコツためてた無料(だった)のアセット使用して作成しました。
YouTube動画について
今回自作したDOF以外にも、ColorGrading、Chromatic Aberrationの調整などをした動画になります
近影のスクショ
近くても多少ミニチュアっぽくみえますね。
--
追記2025/10/16
✅ CoC 計算の変更
float Sign = sign(P - S);
float CoC = Sign * (A * f * abs(P - S)) / S;
float NormalizedCoC = saturate(abs(CoC) / MaxCoC) * Sign;
✅ ブラー処理側の変更
float2 Offset = Sample * CoC_Radius * Sign;
float2 BokehUV = Sample * 0.5 + 0.5;
if (Sign < 0.0) BokehUV = 1.0 - BokehUV;
• singによって、サンプルの方向や BokehShapeTex の分布を反転できる
-
表現の変化と効果
• 手前のボケは広がり、奥のボケは収束するような印象
• BokehShapeTex を前後で変えることで、レンズの個性や物語性が出せる
• 実際の映像(YouTubeリンク)での効果を紹介 -
今後の展望
• 色収差やグローの前後分離
• 焦点距離や絞り値による演出の変化
• タイムラプスとの組み合わせで「動く模型都市」へ -
まとめ
• 符号付き CoC によって、空間の深さと詩情が一気に広がる
• Tilt-shift の演出が、単なるボケではなく「視線と記憶の設計」になる

