2 | Depth Of Field（被写界深度）

参考：CatlikeCoding-Depth Of Field：

https://catlikecoding.com/unity/tutorials/advanced-rendering/depth-of-field/

## 基礎知識
被写界深度は、ゲームでよく使われている画像後処理効果の1つです。これは、撮影の基本的な概念に由来し、カメラレンズまたは他の撮像装置の前が鮮明な画像を取得できる際に測定された被写体の前後の距離の範囲を指します。被写界深度内のオブジェクトは鮮明に画像化され、被写界深度外のオブジェクトはぼやけます。

![DepthOfField1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/1af90858-6ec4-2d1a-1e50-4434a5375e3a.png)
出典：https://imagingsolution.net/imaging/opt/depth-of-focus-field-aperture-relationships/



ピンホールカメラの場合、記録されたオブジェクトのポイントごとに1つの光線だけが小さな穴を通過して記録されます。これの利点は、画像が常に鮮明であるということですが、一本の光の明るさが低すぎるため、画像を鮮明にするのに十分な光を蓄積するために撮影するときは長時間露光が必要です。露出中に被写体が動くと、モーションブラーが発生します。

高速撮影を行い、露光時間を短縮するために、レンズイメージングが使用されます。これにより、複数の光線を同時に記録できますが、焦点距離にない点の画像の形状は、点ではなく、錯乱円（Circle of Confusion）と呼ばれる円になります。感光性要素と人間の目の解像力の影響を受けて、錯乱円のサイズが一定のサイズよりも小さい場合、それははっきりと見えます。逆に、錯乱円のサイズが一定のサイズよりも大きい場合、ぼやけて見えます。
![2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/f1a5e489-51c5-5082-a262-eb5a9360a594.jpeg)

![3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/bccc2eb1-7371-cd51-6e53-264788bcf8e9.jpeg)
（2枚の写真のピント位置が異なり、各オブジェクトのシャープネスが異なっています）


![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/a9e03784-d29a-b48c-fbf0-ada364e3548a.png)
（Crysisの被写界深度効果）

人間の観察には被写界深度という現象はなく、見たいものに焦点を当てていきます。しかし、被写界深度で作成された画像は、画像の鮮明な部分に注意を向けさせ、注目を集めて重要なポイントを強調する手段です。

次に、この効果をシミュレートし、焦点を当てたい部分を決定して、被写界深度内のオブジェクトがシャープになり、被写界深度外のオブジェクトがぼやけるようにします。

## 実装
### CoC（Circle of Confusion）値を計算する
物理方法で計算することができます。
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/62d54ef5-cf82-e7d2-5512-f2771ea3eb61.png)



知っておくべき変数は次のとおりです。

- Ape-開口径

- f-焦点距離

- F-焦点距離と呼ばれ、焦点面に完全に焦点を合わせることができるポイントからカメラまでの距離

- P-現在観測されているオブジェクトのポイントの距離。

焦点面から鏡面までの距離と焦点距離の関係と相まって、正確な関係の計算はより複雑になります。

実際、錯乱円の大きさと観測点からカメラまでの距離との関係を考えると、F = Pの場合、錯乱円の大きさは0であり、差が大きいと考えられます。差が大きいほど、錯乱円の直径が最大の錯乱円の直径まで大きくなり続けます。この特徴によれば、比較的単純な数学的モデルを構築することができて近似シミュレーションを行います。

例：SIGGRAPH2018カンファレンスでは、Epicチームのトピック「ALife of a Bokeh」で、被写界深度の実践について説明しました。その中で、錯乱円のサイズのシミュレーションに関しては、Unrealエンジンのソリューションを図に示します。
![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/362e34fd-2eab-05b2-2bd9-17972a54b7e4.png)
（SIGGRAPH2018「A Life of a Bokeh」）

関連する変数とパラメーターの意味は次のとおりです。

Pは、焦点を合わせたオブジェクトからカメラまでの距離を表します。

Zは、現在描画されているオブジェクトからカメラまでの距離を表します

MaxBgdCocは、最大後被写界深度の錯乱円のサイズを表します。これは、（開口径*焦点距離）/（焦点距離-焦点距離）として計算されます。

このように構築された数学モデルは、錯乱円の変化する傾向をシミュレートします。焦点から離れるほど、錯乱円の直径は大きくなり、徐々に最大値になります。

カスタム関連変数：

```
#region FocalLength 焦点距離

[SerializeField]

float _focalLength = 1.4f;

public float focalLength

{

get { return _focalLength; }

set { _focalLength = value; }

}

#endregion





#region Aperture Diameter 開口径

[SerializeField]

float _Aperture = 1.4f;

public float Aperture

{

get { return _Aperture; }

set { _Aperture = value; }

}

#endregion





#region FocusDistance フォーカスされたオブジェクトの深さ

[SerializeField]

Transform _pointOfFocus;

public Transform pointOfFocus

{

get { return _pointOfFocus; }

set { _pointOfFocus = value; }

}





[SerializeField]

float _focusDistance;

public float focusDistance

{

get { return _focusDistance; }

set { _focusDistance = value; }

}





float CalculateFocusDistance()

{

if (_pointOfFocus == null) return focusDistance;

var cam = TargetCamera.transform;

return Vector3.Dot(_pointOfFocus.position - cam.position, cam.forward);

}

#endregion
```
Shaderでは数学モデルに従って、対応するピクセルのCoCの値を計算します。
```
sampler2D _MainTex;

sampler2D _CameraDepthTexture;

float _FocusDistance;

float _MaxBgdCoc;





half frag_coc(v2f_img i) : SV_Target

{

half depth = LinearEyeDepth(SAMPLE_DEPTH_TEXTURE(_CameraDepthTexture,i.uv));

half CoC = (1 - _FocusDistance / depth);

CoC = clamp(CoC, -1, 1)*_MaxBgdCoc;

return CoC;

}
```
一時的なRTを使用してCOC値を保存します。
```
private void OnRenderImage(RenderTexture source, RenderTexture destination)

{

SetUpShaderParameters(source);

RenderTexture CocRT = new RenderTexture(source.width, source.height, 0, RenderTextureFormat.RHalf, RenderTextureReadWrite.Linear);

Graphics.Blit(source, CocRT, _material, 0);

Graphics.Blit(source, destination);

RenderTexture.ReleaseTemporary(CocRT);

}
```
簡単なシーンを作成し、深度の関係が次の図に示されます。
![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/0a8f154e-70ef-2b70-8e06-4c351b1db4d6.png)



CoC値の概略図は次のとおりです。
![image-11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/794c798e-36f3-5a9e-a9e4-c963607276bb.png)



### Bokeh Filter

次に、ボケフィルターを作成します。


![image-12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/46c6f797-3a79-4ebd-06e4-0b0d6f3e3bcb.png)

（ウィキペディアから）

開口部の構造が円に近いため、画像は被写界深度内になく、明るい領域の形状は円に似ています。前のぼかしコースのアイデアによると、このような効果を作成するために、畳み込み演算子が使用され、サンプリングポイントの範囲は円に近くなります。次の図は、さまざまなサンプリング周波数が異なる円形演算子の形状を示しています：


![image-13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/99f7d3d8-8fb2-9132-26f7-454b145e6086.png)

（SIGGRAPH2018「A Life of a Bokeh」）

Unityのポストエフェクトスタックv2にサンプリング範囲は記載されています。参考してください。
```
#if defined(KERNEL_SMALL)

// rings = 2

// points per ring = 5

static const int kSampleCount = 16;

static const float2 kDiskKernel[kSampleCount] = {

float2(0,0),

float2(0.54545456,0),

float2(0.16855472,0.5187581),

float2(-0.44128203,0.3206101),

float2(-0.44128197,-0.3206102),

float2(0.1685548,-0.5187581),

float2(1,0),

float2(0.809017,0.58778524),

float2(0.30901697,0.95105654),

float2(-0.30901703,0.9510565),

float2(-0.80901706,0.5877852),

float2(-1,0),

float2(-0.80901694,-0.58778536),

float2(-0.30901664,-0.9510566),

float2(0.30901712,-0.9510565),

float2(0.80901694,-0.5877853),

};

#endif
```
円演算子を使用して畳み込みします。その効果は次の図のようになります。
![image-14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/84eaaed0-0e86-be46-fb0b-db7153397c33.png)

オフセットを計算する場合、演算子のサンプリング範囲は単位円で、これに最大の錯乱円の直径を掛けて、通常のサイズを取得することに注意してください。
```
half4 frag_bokeh(v2f_img i) : SV_Target

{

half3 color = half3(0,0,0);

UNITY_LOOP for (int index = 0; index < kSampleCount; index++)

{

float2 offset = kDiskKernel[index] * _MaxBgdCoc;

color += tex2D(_MainTex, i.uv + (offset*_MainTex_TexelSize.xy)).rgb;

}

color = color / kSampleCount;

return half4(color, 1);

}
```
効果は次の図のようになります。

![image-15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/7cee3be8-49ce-3ab2-0296-14c5d3f0529d.png)


処理後、明るい部分に丸い輝点が見られ、要件を満たしていることがわかります。ただし、輝点領域は非常にシャープでトランジションがないため、トランジションをより自然にするために別のぼかし処理を追加します。Dual Blurでのダウンサンプリング操作を使用できます。
```
half4 frag_blur(v2f_img i) : SV_Target

{

float4 offset = _MainTex_TexelSize.xyxy * float4(-1.0, -1.0, 1.0, 1.0);

half4 color = tex2D(_MainTex, i.uv + offset.xy) * 0.25;

color += tex2D(_MainTex, i.uv + offset.zy) * 0.25;

color += tex2D(_MainTex, i.uv + offset.xw) * 0.25;

color += tex2D(_MainTex, i.uv + offset.zw) * 0.25;

return color;

}
```
効果は図のようです。
![image-16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/4f621941-acd8-ad1d-319c-8a439a682f11.png)



トランジションがもっと自然になったことがわかります。ただし、画像がすべてぼやけている必要はありません。被写界深度外のオブジェクトのみがぼやけになり、被写界深度内のオブジェクトは鮮明になります。

### CoCを適用する
以前に計算したCoCの値をフィルターに適用する必要があります。まず、前の手順でぼかし処理を実行するとき、画像に1回のダウンサンプリングをしたが、同様に、Coc値を保存するRTにも1回ダウンサンプリングを行います。
```
SetUpShaderParameters(source);

RenderTexture CocRT = RenderTexture.GetTemporary(source.width, source.height, 0, RenderTextureFormat.RHalf, RenderTextureReadWrite.Linear);

Graphics.Blit(source, CocRT, _material, 0);

_material.SetTexture("_CocTex", CocRT);

int width = source.width / 2;

int height = source.height / 2;

RenderTextureFormat format = source.format;

RenderTexture tmpRT0 = RenderTexture.GetTemporary(width, height, 0, format);

RenderTexture tmpRT1 = RenderTexture.GetTemporary(width, height, 0, format);

Graphics.Blit(source, tmpRT0, _material, 1);

Graphics.Blit(tmpRT0, tmpRT1, _material, 2);

Graphics.Blit(tmpRT1, tmpRT0, _material, 3);

Graphics.Blit(tmpRT0, destination);

RenderTexture.ReleaseTemporary(CocRT);

RenderTexture.ReleaseTemporary(tmpRT0);

RenderTexture.ReleaseTemporary(tmpRT1);
```
ここでは、Coc値を格納するRTの4つのピクセルの平均値をダウンサンプリングされたRTに割り当てることを選択します。
```
half4 frag_filterCoc(v2f_img i): SV_Target

{

float4 offset = _MainTex_TexelSize.xyxy * float4(0.5, 0.5, -0.5,-0.5);

half coc0 = tex2D(_CocTex, i.uv + offset.xy).r;

half coc1 = tex2D(_CocTex, i.uv + offset.zy).r;

half coc2 = tex2D(_CocTex, i.uv + offset.xw).r;

half coc3 = tex2D(_CocTex, i.uv + offset.zw).r;

half coc = (coc0 + coc1 + coc2 + coc3) *0.25;

return half4(tex2D(_MainTex, i.uv).rgb, coc);

}
```
その結果、ボケぼかしが実行されたときにサンプリングされたテクスチャでは、ピクセルのCoC値がAlphaチャネルに保存されます。

以前にBokeh Filterを実行するとき、すべてのサンプリングポイントが中心点に影響を与えました。実際、CoC値はピクセルの影響範囲を表します。 CoC値がサンプリングポイントから中心点までの距離よりも小さい場合は、サンプリングポイントが中心点に影響を与えていないことを意味し、この時点ではポイントをサンプリングしません。
![image-17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/2a9e5e48-0808-478a-b8d9-fb3b4a11ddb3.png)



たとえば、上の図の青い部分の錯乱円の半径は、サンプリングポイントから中心点までの距離よりも小さく、中心点のピクセルカラーには影響しません。

ボケ関連のシェーダーコードの変更について。
```
half4 frag_bokeh(v2f_img i) : SV_Target

{

half3 color = half3(0,0,0);

half weight = 0;

UNITY_LOOP for (int index = 0; index < kSampleCount; index++)

{

float2 offset = kDiskKernel[index] *_MaxBgdCoc;

half radius = length(offset);

half4 tmpColor = tex2D(_MainTex, i.uv + (offset*_MainTex_TexelSize.xy));

if (abs( tmpColor.a )>=radius )

{

color += tmpColor.rgb;

weight += 1;

}

}

color = color / weight;

return half4(color, 1);

}
```
まず、Blurをコメントアウトし、絞りの直径を調整して、効果を確認します。
![image-18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/aa29c6ee-768c-b732-ed84-fe62970eafe3.png)

緑の立方体に焦点を合わせると、より離れている2つのパーティクルシステムのボケがより顕著であることがわかります。
![image-19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/817648d8-5ef5-d917-a5aa-35ac69edbe92.png)
![image-20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/e232c61e-9abc-c158-831f-9ec762a3d51e.png)


焦点が合っている部分のボケが殆どありません。
![image-21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/dccfc02a-6a6f-77b8-55d1-8f0f54e7eea9.png)



## 畳み込み
実際には、錯乱円の直径はセンサー要素のピクセルサイズよりも小さく、オブジェクトは正常に焦点が合っています。センサー要素のピクセルサイズを参照する変数を設定します。
```
#region PixelSize

[SerializeField]

float _pixelSize = 0.2f;

public float PixelSize

{

get { return _pixelSize; }

set { _pixelSize = value; }

}

#endregion

......





private void OnRenderImage(RenderTexture source, RenderTexture destination)

{

SetUpShaderParameters(source);

......

_material.SetTexture("_BokehTexture", tmpRT0);

Graphics.Blit(source, destination, _material, 4);

......

}
```
CoC値がそれ以下の場合は、鮮明な元の画像が表示に使用され、CoC値がそれより大きい場合は、ボケ画像と元の画像の混合値が使用されます。遷移のスムーズさを保証するために、Lerp関数を選んで混色を実行します。
```
half4 frag_combine(v2f_img i) : SV_Target

{

half4 rawColor = tex2D(_MainTex, i.uv);

half3 bokehColor = tex2D(_BokehTexture, i.uv).rgb;

half CoC= tex2D(_CocTex, i.uv).r;

half strength = smoothstep(_PixelSize, 1, abs(CoC));

half3 color = lerp(rawColor.rgb, bokehColor, strength);

return half4(color, rawColor.a);

}
```
シーンを調整して効果を得ます。

![image-22.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/2f8b9a11-93fc-5f0d-9292-7c8620c24288.png)


### 前景と背景を分離する
このアルゴリズムを使用すると、次の状況が発生します。
![image-23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/cca31ccb-5f58-edb1-9165-a8d27e791f5b.png)



緑の正方形に焦点を合わせると、前の白い球形の粒子にボケがなくなります。これは、CoC値が各ピクセルの深度値を使用して計算されるためです。実際、正しい効果は、前面の白い球形の例がボケ状態になり、背面の緑色の正方形をブロックすることです。

したがって、前景と背景を分割して別々に計算する必要があります。CoCの正と負を判断し、前景と背景は異なる変数に格納されてボケ計算を行います。
```
half4 frag_bokeh(v2f_img i) : SV_Target

{

......

half4 bgcolor = half4(0,0,0,0);

half4 fgcolor = half4(0, 0, 0, 0);

UNITY_LOOP for (int index = 0; index < kSampleCount; index++)

{

float2 offset = kDiskKernel[index] *_MaxBgdCoc;

half radius = length(offset);

half4 tmpColor = tex2D(_MainTex, i.uv + (offset*_MainTex_TexelSize.xy));

half bgWeight = saturate(max(tmpColor.a - radius, 0));

half fgWeight = saturate(-tmpColor.a - radius);

bgcolor += half4(tmpColor.rgb, 1) * bgWeight;

fgcolor += half4(tmpColor.rgb, 1) * fgWeight;

}

bgcolor.rgb /= bgcolor.a + (bgcolor.a == 0); // zero-div guard

fgcolor.rgb /= fgcolor.a + (fgcolor.a == 0);

......

}
```
達成したい効果は次のとおりです。フォーカスされたオブジェクトの前に前景がある場合、前景はボケ効果を使用して、フォーカスされたオブジェクトをブロックします。したがって、内挿する場合、前景の重みは1より大きく、前景があることを証明し、前景のボケ効果を使用します。ソース画像と混合するときに、前景がある場合は、前景のボケ効果も使用します。それで、混合モードの重みがアルファ値として保存されます。
```
half4 frag_bokeh(v2f_img i) : SV_Target

{

......

half bgfg = min(1, fgcolor.a);

half3 color = lerp(bgcolor, fgcolor, bgfg);

return half4(color, bgfg);

}
```
ソース画像と混合する場合、まず背景の深度値に従って補間を実行します。これは、オブジェクトが後方にあるほど、より明白なボケ効果があることを示しています。次に、前景の混合モードに従って補間を実行します。これは、ピクセルの周囲が前景のボケ効果の影響を受けていることを示しています。前景のボケ効果は、重みに従って表現する必要があります。
```
half4 frag_combine(v2f_img i) : SV_Target

{

......

half strength = smoothstep(_PixelSize, 1.2, CoC);

half3 color = lerp(rawColor.rgb, bokehColor.rgb, strength);

color = lerp(color, bokehColor.rgb, bokehColor.a);

return half4(color, rawColor.a);

}
```
効果は図のようになります。
![image-24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/bddecaac-389a-bca0-f665-74916e7f5fbc.png)
![image-25.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/19fab464-5590-a199-bf63-06eb5360c835.png)





## まとめ
被写界深度の学習に関しては、この章ではいくつかの基本的な実現効果について説明します。被写界深度効果を非常に細かくしたい場合は、多くのスムーズな遷移とパラメータ調整が必要です。研究を継続することに関心のある方は、SIGGRAPH2018カンファレンスでEpicチームによるトピック「ALifeofaBokeh」を見ることができます。

（PPT）

CatlikeCodingによって実現される被写界深度効果

（https://catlikecoding.com/unity/tutorials/advanced-rendering/depth-of-field/）

と[keijiro](https://github.com/keijiro)によって実現される被写界深度効果：https://lab.uwa4d.com/lab/5b661495d7f10a201ff9e800


![GIF (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/56d0e3d8-73f4-95cc-e5ff-d1521a2f504a.gif)
（kinoBokehプロジェクトによって達成された被写界深度効果）

このような効果は、Mobile側に適用したパフォーマンスの問題、およびそれを解決する方法は、[学堂のコース(中国語注意)](https://edu.uwa4d.com/course-intro/0/141?purchased=true)を読むことができます。

---
UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、**パフォーマンス分析**と**最適化ソリューション**及び**コンサルティングサービス**を提供している会社でございます。

今なら、UWA GOTローカルツールが**15日間に無償試用**できます！！
よければ、ぜひ！

UWA公式サイト：https://jp.uwa4d.com
UWA GOT OnlineレポートDemo：https://jp.uwa4d.com/u/got/demo.html
UWA公式ブログ：https://blog.jp.uwa4d.com