Silhouette Rendering

基礎知識

シルエットレンダリングは、アウトライン(Outline)とも呼ばれる一般的な視覚効果で、非写実的レンダリングでよく見られます。Borderlandsシリーズのようなコミックスタイルの強いゲームでは、多くのアウトラインレンダリングが使用されます。

Borderlandsシリーズのスクリーンショット

一般的な方法の1つは、ジオメトリ空間で、シーンが正常にレンダリングされた後、輪郭を描く必要のあるジオメトリを再レンダリングすることです。ジオメトリは、最初にその頂点位置を法線方向に沿って移動することによって拡大されます。次に、拡大されたジオメトリの背面のみを残して、正方向の面をカリングし、アウトライン効果を形成します。

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

 

ジオメトリ空間に基づいた方法は、本節で検討しません。

画面スペースに基づく別の後処理スキームがあり、キーリンクはエッジ検出(Edge Detection)です。エッジ検出の原理は、エッジ検出演算子を使用して画像に対して畳み込み演算を実行することです。一般的に使用されるエッジ検出演算子はSobel演算子で、水平方向と垂直方向の両方の畳み込みカーネルが含まれます。

色、深度、その他の情報など、エッジにある隣接するピクセル間で特定の属性に明らかな違いがあると見なすことができます。 Sobel演算子を使用して画像を畳み込むと、隣接するピクセル間のこれらの属性の差を取得できます。これは勾配(gradient)と呼ばれ、エッジ部分の勾配値は比較的大きくなります。ピクセルの場合、水平方向と垂直方向にそれぞれ畳み込み演算を実行して、2つの方向の勾配値GxとGyを取得し、それによって全体的な勾配値を取得します:

フィルタリングするしきい値を設定し、エッジに配置したピクセルを保留し、それらに色を付けてアウトライン効果を形成します。

たとえば、色の変化が少ない3次元オブジェクトの場合、アウトラインの描画には深度情報が使用され、効果は次のようになります。


Unityの実装

上記のアルゴリズムに従って、UnityでBuild-inパイプラインを使用してアウトライン効果を実装し、静止画像を選んで色のプロパティの違いに応じて処理します。

まず、Sobel演算子を実装します。

half2 SobelUV[9] = { half2(-1,1),half2(0,1),half2(1,1),
					half2(-1,0),half2(0,0),half2(1,0),
					half2(-1,-1),half2(0,-1),half2(1,-1) };
half SobelX[9] = { -1,  0,  1,
					-2,  0,  2,
					-1,  0,  1 };
half SobelY[9] = { -1, -2, -1,
					0,  0,  0,
					1,  2,  1 };

演算子に従って画像をサンプリングし、fixed4タイプのカラー値を取得します。RGBAという4つのチャネルが含まれているため、いくつかの重みを設定して明るさの値を計算することができます。たとえば、平均値の計算を選択します。

fixed Luminance(fixed4 color)
{
	return 0.33*color.r + 0.33*color.g + 0.34*color.b;
}

明るさの値と演算子に従って勾配を計算します。

half texColor;
half edgeX = 0;
half edgeY = 0;
for (int index = 0; index < 9; ++index)
{
	texColor = Luminance(tex2D(_MainTex, i.uv + _MainTex_TexelSize.xy*SobelUV[index]));
	edgeX += texColor * SobelX[index];
	edgeY += texColor * SobelY[index];
}
half edge = 1-sqrt(edgeX*edgeX + edgeY * edgeY);

変数edgeの値が0に近いほど、境界と見なされます。

次に、描画します。輪郭のみを描画できます。

fixed4 onlyEdgeColor = lerp(_EdgeColor, _BackgroundColor, edge);

効果は図のようです。

元の画像をブレンドしてアウトラインすることもできます。

fixed4 withEdgeColor = lerp(_EdgeColor, tex2D(_MainTex, i.uv), edge);

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

元の画像は次のとおりです。

アウトラインの色を調整して、効果は図のようになります。


UWA公式サイト:https://jp.uwa4d.com

UWA公式ブログ:https://blog.jp.uwa4d.com

UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com

Lens Flare —— Streak

基礎知識

撮影際、強い光源からの光が多くのレンズで生成されたレンズ群を通過する際に反射や散乱が発生し、他の入射光と同じ方向を保っていない光がフレアを生成することがあります。

(右上隅の明るい光により、画像に目立つハローが生じます)

もともと技術的な欠陥で画像が歪んでいたのですが、意外と特殊効果が出て立体感が増し、雰囲気を盛り上げることができました。写真の世界では、いくつかの効果を生み出すために特別なフィルターが作られています。同様に、これらの効果はゲームでシミュレートされ、画質を向上させ、雰囲気を高めます。次の章では、レンズフレアによって生成されるいくつかの効果を紹介し、それを実装します。

 

このセクションでは、Lens Flare効果の一つ、Streak効果を紹介します。

図の中心に、顕著な長いフレアがあります

撮影界にはStreak Filtersという特別なフィルターがあります。ストリークフィルターは、輝点を中心に一連の平行線を放射し、放射効果をもたらします。

(写真のまぶしさによるStreak効果)

ゲーム界で、Streakはよく見られた効果として、輝点のハイライトを表示して雰囲気を引き立たせます。

(Mass Effect 2のLens Flare Streak効果)

このセクションでは、Dual Blurアルゴリズムのアイデアに基づいた比較的単純な方法を使用してそれを実現します。Dual Blurアルゴリズムでは、ブラー効果は、ダウンサンプリングで画像を縮むことと、アップサンプリングで画像を拡大することを繰り返してぼかし効果を取得します。このように、周囲のピクセルも当ピクセルの色の一部を取得したようになって、色のぼかし効果も実現されています。この考え方に従って、一方向にアップ・ダウンサンプリングを繰り返すことを選択できます。

 

Unityの実装

アップおよびダウンサンプリング

まず、アップサンプリングとダウンサンプリングのプロセスを実装するには、ハイライトポイントを一方向に伸ばす必要があるため、選択したサンプリングポイントは一方向にのみ配置する必要があります。ダウンサンプリングを実行する場合、サンプリング範囲を適切に拡大すると、縮小された画像の色のピクセル数ができるだけ多くなり、重みの制御により、明るさの減衰がより自然になります。アップサンプリングを実行する場合は、数回デバッグしてサンプリングポイントを適度な範囲内で維持し、色が暗すぎないようにします。

// Downsampler
half4 frag_down(v2f_img i) : SV_Target
{
	const float dx = _MainTex_TexelSize.x;

	float u0 = i.uv.x - dx * 5;
	float u1 = i.uv.x - dx * 3;
	float u2 = i.uv.x - dx * 1;
	float u3 = i.uv.x + dx * 1;
	float u4 = i.uv.x + dx * 3;
	float u5 = i.uv.x + dx * 5;

	half3 c0 = tex2D(_MainTex, float2(u0, i.uv.y));
	half3 c1 = tex2D(_MainTex, float2(u1, i.uv.y));
	half3 c2 = tex2D(_MainTex, float2(u2, i.uv.y));
	half3 c3 = tex2D(_MainTex, float2(u3, i.uv.y));
	half3 c4 = tex2D(_MainTex, float2(u4, i.uv.y));
	half3 c5 = tex2D(_MainTex, float2(u5, i.uv.y));

	return half4((c0 + c1 * 2 + c2 * 3 + c3 * 3 + c4 * 2 + c5) / 12, 1);
}

// Upsampler
half4 frag_up(v2f_img i) : SV_Target
{
	const float dx = _MainTex_TexelSize.x * 3;

	float u0 = i.uv.x - dx;
	float u1 = i.uv.x;
	float u2 = i.uv.x + dx;

	half3 c0 = tex2D(_MainTex, float2(u0, i.uv.y)) / 4;
	half3 c1 = tex2D(_MainTex, float2(u1, i.uv.y)) / 2;
	half3 c2 = tex2D(_MainTex, float2(u2, i.uv.y)) / 4;
	half3 c3 = tex2D(_HighTex, i.uv);
	return half4(lerp(c3, c0 + c1 + c2, _Stretch), 1);
}

しきい値を適用してハイライトをフィルタリングする

円形の発光点の場合、望ましい効果は、円の中心を通過する水平ビームが最も高い輝度と最も長い長さを持ち、垂直方向に沿って減少することです。Y軸に沿って周囲のピクセルをサンプリングすることを選択します。これにより、エッジピクセルがブレンドによってこぼれる明るさが少なくなります。

half4 frag_prefilter(v2f_img i) : SV_Target
{
	const float dy = _MainTex_TexelSize.y;
	half3 c0 = tex2D(_MainTex, float2(i.uv.x, i.uv.y - dy));
	half3 c1 = tex2D(_MainTex, float2(i.uv.x, i.uv.y + dy));
	half3 c = (c0 + c1 ) / 2;
	c = max(0, c - _Threshold);
	return half4(c, 1);
}

重畳

同様のアイデアを使用して複数のピクセルをサンプリングし、それらを混合してピクセルの色を取得すると、遷移がスムーズになります。

half4 frag_composite(v2f_img i) : SV_Target
{
	float dx = _MainTex_TexelSize.x * 1.5;
	float u0 = i.uv.x - dx;
	float u1 = i.uv.x;
	float u2 = i.uv.x + dx;
	half3 c0 = tex2D(_MainTex, float2(u0, i.uv.y)) / 4;
	half3 c1 = tex2D(_MainTex, float2(u1, i.uv.y)) / 2;
	half3 c2 = tex2D(_MainTex, float2(u2, i.uv.y)) / 4;
	half3 c3 = tex2D(_HighTex, i.uv);
	half3 cf = (c0 + c1 + c2) * _Color * _Intensity * 10;
	return half4(cf + c3, 1);
}

自己照明球を使用し、鏡面反射光を適用して、効果を取得します。

平行光の強度を上げて、効果を得ます。

興味のある方は参考してください:

HDRPバージョンのStreak効果(https://github.com/Cyber​​Deck/Unity-HDRP-LensFlares

(このプロジェクトの効果)

URPバージョンのStreak効果(https://github.com/ColinLeung-NiloCat/UnityURP-BillboardLensFlareShader

(このプロジェクトの効果)

 

まとめ

Streak効果の実現から、画像ぼかしアルゴリズムのアイデアの美しさを徐々に理解することができます。色をぼかし、自然な遷移を形成します。 いくつかのトリックと相まって、多くの効果を達成することができます。


UWA公式サイト:https://jp.uwa4d.com

UWA公式ブログ:https://blog.jp.uwa4d.com

UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com

Depth Of Field(被写界深度)

参考:CatlikeCoding-Depth Of Field:

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

基礎知識

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

 

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

高速撮影を行い、露光時間を短縮するために、レンズイメージングが使用されます。これにより、複数の光線を同時に記録できますが、焦点距離にない点の画像の形状は、点ではなく、錯乱円(Circle of Confusion)と呼ばれる円になります。感光性要素と人間の目の解像力の影響を受けて、錯乱円のサイズが一定のサイズよりも小さい場合、それははっきりと見えます。逆に、錯乱円のサイズが一定のサイズよりも大きい場合、ぼやけて見えます。

(2枚の写真のピント位置が異なり、各オブジェクトのシャープネスが異なっています)

(Crysisの被写界深度効果)

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

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


実装

CoC(Circle of Confusion)値を計算する

物理方法で計算することができます。

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

Ape-開口径

f-焦点距離

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

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

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

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

例:SIGGRAPH2018カンファレンスでは、Epicチームのトピック「ALife of a Bokeh」で、被写界深度の実践について説明しました。その中で、錯乱円のサイズのシミュレーションに関しては、Unrealエンジンのソリューションを図に示します。

(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);

}

簡単なシーンを作成し、深度の関係が次の図に示されます。

CoC値の概略図は次のとおりです。

Bokeh Filter

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

(ウィキペディアから)

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

(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

円演算子を使用して畳み込みします。その効果は次の図のようになります。

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

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);

}

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

処理後、明るい部分に丸い輝点が見られ、要件を満たしていることがわかります。ただし、輝点領域は非常にシャープでトランジションがないため、トランジションをより自然にするために別のぼかし処理を追加します。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;

}

効果は図のようです。

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

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値がサンプリングポイントから中心点までの距離よりも小さい場合は、サンプリングポイントが中心点に影響を与えていないことを意味し、この時点ではポイントをサンプリングしません。

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

ボケ関連のシェーダーコードの変更について。

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をコメントアウトし、絞りの直径を調整して、効果を確認します。

緑の立方体に焦点を合わせると、より離れている2つのパーティクルシステムのボケがより顕著であることがわかります。

焦点が合っている部分のボケが殆どありません。

畳み込み

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

#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);

}

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

前景と背景を分離する

このアルゴリズムを使用すると、次の状況が発生します。

緑の正方形に焦点を合わせると、前の白い球形の粒子にボケがなくなります。これは、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);

}

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

まとめ

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

(PPT)

CatlikeCoding

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

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

(kinoBokehプロジェクトによって達成された被写界深度効果)

このような効果は、Mobile側に適用したパフォーマンスの問題、およびそれを解決する方法は、学堂のコースを読むことができます。

https://edu.uwa4d.com/course-intro/0/141?purchased=true


UWA公式サイト:https://jp.uwa4d.com

UWA公式ブログ:https://blog.jp.uwa4d.com

UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com

Real-Time Glow & Bloom

 

https://software.intel.com/content/www/us/en/develop/articles/compute-shader-hdr-and-bloom.html


前書き

先々週、画像ぼかし処理のアルゴリズムの効率を探りながら、コアアイデアを深く理解しました。これらのアイデアは多くの後処理効果に適用します。

  1. Gaussian Blur
  2. Box Blur
  3. Kawase Blur
  4. Dual Blur

それでは、いくつかの一般的な画面後処理効果を実装する方法を学びましょう。


基礎知識

Bloom(Glow)特殊効果は、ゲームでよく使われる画面の後処理特殊効果の一つで、ハイライトオブジェクトに投光照明(floodlight)効果を与え、光影の表現力を向上させる機能を持ちます。Bloomは通常、より良い結果を得るためにHDRおよびToneMappingとペアになっています。

 

まず、HDRとToneMappingとは何かを理解する必要があります。電子機器は表示範囲が限られているため、RGBの3つのチャネルにとって、各チャネルは8ビットで、合計(2^8)^3=16777216種類の色を表示することができます。この範囲は低ダイナミックレンジ(Low-Dynamic Range)と呼ばれます。この範囲は経験的な輝度の範囲であり、人間の目に害を及ぼすことはありません。それに対して、この範囲を超える色空間は、ハイダイナミックレンジ(High-Dynamic Range)と呼ばれます。実生活での輝度には範囲制限はありません。より多くの輝度を表現するには、RGBの各チャンネルを12ビット以上に拡張します。演算が完了したら、出力時にToneMapping(トーンマッピング)を介してHDR空間の色彩情報をLDR空間に変換して、モニターに表示させる必要があります。実生活で起こったBloomは人間の水晶体の散乱に由来します。例えば、明るい光を浴びるとき、見えにくくなります。従って、Bloomでハイライトブルームの効果を表すことでこの現象をシミュレートします。

 

HDR、LDR、およびToneMappingの詳細については、「HDRと色彩管理」シリーズ(以下のリンク)を参照してください。

  1. 光、色、色度図
  2. 色空間
  3. SDRとHDR
  4. HDR標準とACES
  5. ゲームにおけるHDR

一般的なBloom効果は、畳み込みによって画像のハイライトされた部分をぼかし、元の画像に重ね合わせることによって作成されます。「画面後処理効果シリーズの画像ぼけアルゴリズム」(以下のリンク)では、さまざまな画像ぼかしアルゴリズムを紹介し、最も効率的なDual Blur方法を使用してぼかし処理を行うことにしました。

  1. Gaussian Blur
  2. Box Blur
  3. Kawase Blur
  4. Dual Blur

Unityの実装:

まず、しきい値を設定し、画像をサンプリングします。しきい値を超える画像内のピクセルがハイライトされた部分として決定します。ハイライトされた部分は色を保持し、残りは0に戻し、RTに保存します。

BloomMaterial.SetFloat("_Threhold", threshold);
BloomMaterial.SetFloat("_Intensity", intensity);
RenderTexture HighLighrRT=RenderTexture.GetTemporary(src.descriptor);
Graphics.Blit(src, HighLighrRT, BloomMaterial, 0);

 

Boom.shader:
Pass
{
	CGPROGRAM

	#pragma vertex vert_img
	#pragma fragment frag

	fixed4 frag(v2f_img input) : SV_Target
	{
		float4 color = tex2D(_MainTex, input.uv);
		return max(color - _Threhold, 0) * _Intensity;
	}

	ENDCG
}

次に、ハイライトされた部分をぼかして、元の画像に重ね合わせます。

Pass
{
	CGPROGRAM

	#pragma vertex vert_img
	#pragma fragment frag
	sampler2D _CompositeTex;
	float4    _CompositeColor;
	fixed4 frag(v2f_img input) : SV_Target
	{
		float4 mainColor = tex2D(_MainTex,input.uv);
		float4 compositeColor = tex2D(_CompositeTex, input.uv);
		return saturate(mainColor + compositeColor);
	}
	ENDCG
}

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

元の画像は次のとおりです。

同様に、「ライトセーバー」効果の実現を模倣します。


まとめ

Bloom効果は、ゲームで使用される最も一般的な画面後処理効果だと言え、実装も簡単です。これからは、ぼかし処理で自然に滑らかに色をぼかし、色を重ねることで輝度を向上させることができるようになります。

 

ブルーム効果のパフォーマンスに影響を与える最大の要因は、ぼかし処理を行うコストです。Dual Blurアルゴリズムを選んで処理することをお勧めします。


UWA公式サイト:https://jp.uwa4d.com

UWA公式ブログ:https://blog.jp.uwa4d.com

UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com

Dual Blur

6. Dual Blur

6.1 基礎知識

SIGGRAPH 2015で、ARMチームは「Bandwidth-Efficient Rendering」を共有しました。この共有では、この前紹介したBlurアルゴリズムを分析し、新しいBlurアルゴリズムであるDual Blurを提案しました。

添付ファイルにPDFとNotesがあります。

添付ファイル: siggraph2015-mmg-marius-notes.pdf https://uploader.shimo.im/f/9sLFLRmewABWbeo6.pdf?accessToken= eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ. eyJhdWQiOiJhY2Nlc3NfcmVzb3VyY2UiLCJleHAiOjE2NTM2MjU2MTcsImZpbGVHVlEIjoiVXl0UVhhUEZaalk1TEk1WiIsImlhdCI6MTY1MzYyNTMxNywidXNlcklkIjo3MjI0ODE3OX0 .CGbZUilomfy2VIewrNWWHempzxcurQcjVJ9u3YO6hsw

Dual Blurは、Kawase Blurをベースに改良したアルゴリズムで、Down-Samplingで画像を縮小し、Up-Samplingで画像を拡大しながら、GPUのハードウェア機能を十分に活用してマッピングの読み込み回数をさらに削減しました。

まず、画像のダウンサンプリング(Down-Sampling)を行い、原画像の縦横を1/2に縮小してターゲット画像を取得します。図に示すように、ピンク色の部分は元のサイズに引き伸ばされた後のターゲット画像の1画素を表し、白い四角はそれぞれ元の画像の1画素を表しています。サンプリングするとき、選択されるサンプリング位置は、図の青い円で表される位置です。すなわちターゲット画像の各画素の4角と中心を選び、それぞれ1/8と1/2の重みを付け、そのUV座標を元画像に代入してサンプリングを行います。ターゲット画像の1画素を処理する場合、5回のテクスチャ読み取りを通し、原画の16画素が演算に関与することにします。その後、複数回のダウンサンプリングを行い、毎回に得られたターゲット画像を次のサンプリング用の原画とします。このようにしたら、ダウンサンプリングするたびに、演算に参加する画素数を前回の1/4になります。

 

そして、画像のアップサンプリング(Up-Sampling)を行い、原画像の縦横を2倍に拡大してターゲット画像を取得します。図に示すように、ピンク色の部分は原画のサイズに縮小されたターゲット画像の画素を表し、白い四角はそれぞれ元の画像の1画素を表しています。サンプリング位置を選択するとき、図の青い円で表されたところを選びます。つまり原画像の対応画素の4角と隣接する4つの画素の中心を選び、それぞれ1/6と1/12の重みを付けます。ターゲット画像の画素を処理する場合、8回のテクスチャ読み取りを通し、原画の16画素が演算に関与することにします。ターゲット画像の画素数を原画像の4倍に拡大します。このように、画像が元のサイズに戻るまでサンプリング操作を繰り返します。


6.2 Unityの実装

上記のアルゴリズムに基づき、UnityにDual Blurアルゴリズムを実装します。4つのダウンサンプルと4つのアップサンプルを選択して、ぼかし処理を行います。

ダウンサンプリングの実装

float4 frag_downsample(v2f_img i) :COLOR
{
	float4 offset = _MainTex_TexelSize.xyxy*float4(-1,-1,1,1);
	float4 o = tex2D(_MainTex, i.uv) * 4;
	o += tex2D(_MainTex, i.uv + offset.xy);
	o += tex2D(_MainTex, i.uv + offset.xw);
	o += tex2D(_MainTex, i.uv + offset.zy);
	o += tex2D(_MainTex, i.uv + offset.zw);
	return o/8;
}

アップサンプリング実装。

float4 frag_upsample(v2f_img i) :COLOR
{
	float4 offset = _MainTex_TexelSize.xyxy*float4(-1,-1,1,1);
	float4 o = tex2D(_MainTex, i.uv + float2(offset.x, 0));
	o += tex2D(_MainTex, i.uv + float2(offset.z, 0));
	o += tex2D(_MainTex, i.uv + float2(0, offset.y));
	o += tex2D(_MainTex, i.uv + float2(0, offset.w));
	o += tex2D(_MainTex, i.uv + offset.xy / 2.0) * 2;
	o += tex2D(_MainTex, i.uv + offset.xw / 2.0) * 2;
	o += tex2D(_MainTex, i.uv + offset.zy / 2.0) * 2;
	o += tex2D(_MainTex, i.uv + offset.zw / 2.0) * 2;
	return o / 12;
}

対応するpassを実装する。

Pass
{
	ZTest Always ZWrite Off Cull Off
	CGPROGRAM
	#pragma target 3.0
	#pragma vertex vert_img
	#pragma fragment frag_downsample
	ENDCG
}

Pass
{
	ZTest Always ZWrite Off Cull Off
	CGPROGRAM
	#pragma target 3.0
	#pragma vertex vert_img
	#pragma fragment frag_upsample
	ENDCG
}

OnRenderImageでダウンサンプリングとアップサンプリングを繰り返します。

private void OnRenderImage(RenderTexture src, RenderTexture dest)
{
    int width = src.width;
    int height = src.height;
    var prefilterRend = RenderTexture.GetTemporary(width / 2, height / 2, 0, RenderTextureFormat.Default);
    Graphics.Blit(src, prefilterRend, m_Material, 0);
    var last = prefilterRend;
    for (int level = 0; level < MaxIterations; level++)
    {
        _blurBuffer1[level] = RenderTexture.GetTemporary(
            last.width / 2, last.height / 2, 0, RenderTextureFormat.Default
        );
        Graphics.Blit(last, _blurBuffer1[level], m_Material, 0);

        last = _blurBuffer1[level];
    }
    for (int level = MaxIterations-1; level >= 0; level--)
    {
        _blurBuffer2[level] = RenderTexture.GetTemporary(
            last.width * 2, last.height * 2, 0, RenderTextureFormat.Default
        );

        Graphics.Blit(last, _blurBuffer2[level], m_Material, 1);

        last = _blurBuffer2[level];
    }
    Graphics.Blit(last, dest); ;
    for (var i = 0; i < MaxIterations; i++)
    {
        if (_blurBuffer1[i] != null)
        {
            RenderTexture.ReleaseTemporary(_blurBuffer1[i]);
            _blurBuffer1[i] = null;
        }

        if (_blurBuffer2[i] != null)
        {
            RenderTexture.ReleaseTemporary(_blurBuffer2[i]);
            _blurBuffer2[i] = null;
        }
    }
    RenderTexture.ReleaseTemporary(prefilterRend);
}

効果が出ました。

FramDebug でこのプロセスをチャックすることができます。

(2回目のダウンサンプリング)

(4回目のダウンサンプリング)

(2回目のアップサンプリング)


6.3 まとめ

Dual Blurアルゴリズムは、Kawase Blurの上にダウンサンプリングで画像を縮小し、アップサンプリングで画像を拡大します。画像処理の画素数を減らすことで、テクスチャのサンプル数を減らし、効率を高めます。

ARMチームの「Bandwidth-Efficient Rendering」シェアによるテスト結果は以下の通りです。

 

Mali-T760 MP8を搭載したモバイルデバイス(例:Samsung Galaxy S6)で実験したところ、Dual Blurアルゴリズムが最も高速で、パフォーマンスが一番優れたことが分かりました。

Blurの改良アルゴリズムの間で、読み取りと書き込みの帯域幅に大きな差はない。

まとめると、現段階では、Dual Blurはぼかし効果を実現する最も効率的なアルゴリズムだと言えます。

DEMO:https://github.com/UWA-MakeItSimple/Course-PostProcessingEffect/blob/main/Assets/Shaders/Blur/DualBlur.shader

Kawase Blur

5. Kawase Blur

5.1 基礎知識

GDC2003で川瀬さんが発表した「Frame Buffer Postprocessing Effects in DOUBLE-S.T.E.A.L (Wreckless)」では、Bloom効果に適用したアルゴリズムが紹介されました。その後ぼかしアルゴリズム「Kawase Blur」として普及されてきました。 .

このプレゼンテーションは、GDCのウェブサイトにてご覧いただけます。

https://www.gdcvault.com/play/1022665/Frame-Buffer-Postprocessing-Effects-in

https://www.gdcvault.com/play/1022664/Frame-Buffer-Postprocessing-Effects-in

添付ファイルにはこのスピーチのPPTがあります。

添付ファイル: GDC2003_DSTEAL.ppt https://uploader.shimo.im/f/E0dNiYPaUKGqORTp.ppt?accessToken= eyJhbGciOiJIUzI1NiIsImtpZCI6ImRlZmF1bHQiLCJ0eXAiOiJKV1QifQ. eyJhdWQiOiJhY2Nlc3NfcmVzb3VyY2UiLCJleHAiOjE2NTM1NzQ0OTUsImZpbGVHUlEIjoiMU03TXpuZVRSSlFRV3RJZSIsImlhdCI6MTY1MzU3NDE5NSwidXNlcklkIjo3MjI0ODE3OX0 .KXPZRBuJd9DwLMVaBeQjMnhda8xOHS-ZQvo96N0bGbA

前節で述べたBox Blurアルゴリズムの最適化のアイデアは、時間的複雑さを定数のレベルにまで下げることです。一方、Kawase Blurアルゴリズムは、ハードウェアに焦点を当て、GPUによるバイリニア補間のサンプリングのオーバーヘッドが小さいことを最大限に利用して、アルゴリズムの最適化を実行します。

GPU によるバイリニア補間のサンプリングについては 3 章で説明しているので,ここでは詳細な説明をしません。例えば、3×3の画素領域の場合。

(GDC2003で川瀬さんが共有したPPTより)

図に示すように、黒い点で表された画素を処理するとき、赤い点で示された周囲の4つのサンプリング点を選択し、データの平均値を求めます。サンプリングに選ばれた点(=赤い点)は、4つの画素の中点である。そして、GPUは周囲の4つのピクセルをサンプリングしてバイリニア補間を行い、その結果をサンプリングデータとして生成します。このように処理した結果は、図中の右側に示した重みを持つ3×3の畳み込みカーネルで処理する結果と同じです。でも、このような方法は、1画素のテクスチャ読み込み回数を9回から4回に削減しました。

ぼかし効果を最大化するために、従来のアルゴリズムの経験を考慮し、複数回のぼかしを行うことや、より多くの画素をサンプリングすることを選択することが可能です。例えば、最初のぼかしとして上記の方法を使用した場合、次のぼかしは、以下の画像のようにサンプリング点のオフセットを大きくして行うことができます。

(GDC2003で川瀬さんが共有したPPTより)

 

ぼかし処理手段として、毎回、処理対象画素からサンプリング点までのオフセットを自由に選択することができます。このようにして、より多くの画素がぼかし演算に参加できるようになりました。何度かぼかすと、ガウスぼかしに近似した効果が得られます。


5.2 Unityの実装

上記のアルゴリズムに基づき、Unityで川瀬ブラーを実装します。0、1、2、2、3のカーネルの5パスKawaseフィルターを選択して、35×35の畳み込みカーネルを用いたGaussian Blur「まとめ」の部分でさらに説明)の効果と比較します。

最初にKawase Blurサンプリングのアルゴリズムを実装します。簡単なので、オフセットに対応する位置でテクスチャのデータを読み込んで平均化演算を行うだけで済みます。

float4 KawaseBlur(pixel_info pinfo, int pixelOffset)
{
	float4 o = 0;
	o += tex2D(pinfo.tex, pinfo.uv + (float2(pixelOffset + 0.5, pixelOffset + 0.5) * pinfo.texelSize)) * 0.25;
	o += tex2D(pinfo.tex, pinfo.uv + (float2(-pixelOffset - 0.5, pixelOffset + 0.5) * pinfo.texelSize))* 0.25;
	o += tex2D(pinfo.tex, pinfo.uv + (float2(-pixelOffset - 0.5, -pixelOffset - 0.5) * pinfo.texelSize)) * 0.25;
	o += tex2D(pinfo.tex, pinfo.uv + (float2(pixelOffset + 0.5, -pixelOffset - 0.5) * pinfo.texelSize)) * 0.25;
	return o;
}

オフセットが異なるフラグメントシェーダーを実装する。

float4 frag0(v2f_img i) : COLOR
{
	pixel_info pinfo;
	pinfo.tex = _MainTex;
	pinfo.uv = i.uv;
	pinfo.texelSize = _MainTex_TexelSize;
	return KawaseBlur(pinfo, 0);
}

float4 frag1(v2f_img i) : COLOR
{
	pixel_info pinfo;
	pinfo.tex = _GrabTexture;
	pinfo.uv = i.uv;
	pinfo.texelSize = _GrabTexture_TexelSize;
	return KawaseBlur(pinfo, 1);
}

float4 frag2(v2f_img i) : COLOR
{
	pixel_info pinfo;
	pinfo.tex = _GrabTexture;
	pinfo.uv = i.uv;
	pinfo.texelSize = _GrabTexture_TexelSize;
	return KawaseBlur(pinfo, 2);
}

float4 frag3(v2f_img i) : COLOR
{
	pixel_info pinfo;
	pinfo.tex = _GrabTexture;
	pinfo.uv = i.uv;
	pinfo.texelSize = _GrabTexture_TexelSize;
	return KawaseBlur(pinfo, 3);
}

同様に、GrabPass{}関数を利用して、前のぼかし効果を得ることができます(この関数はまだ比較的に時間がかかるので、OnRenderImage()関数のblitの使用を検討してください)、5 passを実装します。

Pass
{
	CGPROGRAM
	#pragma target 3.0
	#pragma vertex vert_img
	#pragma fragment frag0
	ENDCG
}

GrabPass{}

Pass
{
	CGPROGRAM
	#pragma target 3.0
	#pragma vertex vert_img
	#pragma fragment frag1
	ENDCG
}

GrabPass{}

Pass
{
	CGPROGRAM
	#pragma target 3.0
	#pragma vertex vert_img
	#pragma fragment frag2
	ENDCG
}

GrabPass{}

Pass
{
	CGPROGRAM
	#pragma target 3.0
	#pragma vertex vert_img
	#pragma fragment frag2
	ENDCG
}

GrabPass{}

Pass
{
	CGPROGRAM
	#pragma target 3.0
	#pragma vertex vert_img
	#pragma fragment frag3
	ENDCG
}

得られた結果

(上記方法で得られた結果)

 

(35×35 Gaussian Blurの結果)

この処理で得られる結果は、35×35の畳み込みカーネルを用いたGaussian Blurで得られる結果と似ていることがわかります。


5.3 まとめ

Kawase Blurアルゴリズムの利点は、GPUのハードウェア機能を最大限に活用することで、アルゴリズムの性能が非常に良くなることです。しかし、Kawase Blurアルゴリズムのぼかし効果の質の推定は、試行錯誤が必要な経験値です。これまでのアルゴリズムでは、ぼかし効果の質を測定するための畳み込みカーネル数とぼかし回数が決まっており、畳み込みカーネルサイズが大きく、ぼかし回数が多いほど良い結果が得られるようになっています。Kawase Blurで設定したオフセットは、演算子と同様にぼかし効果に質的な影響を与えません。そこで、ガウスぼかし操作の効果に近くために、いくつかの異なるオフセット値を選択し、いくつかのぼかし操作を行う必要があります。以下の実験は、Intelの研究成果に由来します。

リンク: https://www.intel.com/content/www/us/en/developer/articles/technical/an-investigation-of-fast-real-time-gpu-based-image-blur-algorithms.html

入力値:黒い背景に12×12の白い画素のブロック(左:X軸の中央断面、右:上面図)。

35×35のガウシアンフィルターを適用した値。

0, 1, 2, 2, 3 カーネルの 5 パス川瀬フィルターを適用した値 – 35×35 ガウシアンフィルターに非常に近い(ただし完全には一致しない):.

Kawase Blurアルゴリズムの欠点は、複数のぼかし処理を行うと、それに応じた数のDrawCallが発生することです。

例えば、上記の例では、5回のKawase Blur計算で5回のDrawCallが発生しますが、1画素を処理するのに4*5=20回だけのテクスチャ読み込みが必要となります。同様の 35×35 畳み込みカーネルを持つ Gaussian Blur linear sampling アルゴリズムは、2 つの DrawCall を生成し、1 ピクセルを処理するために 17*2=34 のテクスチャ読み込みを必要とします。 処理する画像を1枚に拡張した場合、DrawCallの回数が増えるとデメリットは、テクスチャの読み込み回数の大幅な減少による速度向上で相殺されるほどです。つまり、Kawase Blurは、効果も性能も優れているのです。

DEMO:https://github.com/UWA-MakeItSimple/Course-PostProcessingEffect/blob/main/Assets/Shaders/Blur/KawaseBlur.shader


UWA公式サイト:https://jp.uwa4d.com

UWA公式ブログ:https://blog.jp.uwa4d.com

UWA公式Q&Aコミュニティ(中国語注意):https://answer.uwa4d.com

Box Blur

4. Box Blur

4.1 基礎知識

Box Blurアルゴリズムも畳み込み演算を用いるが、各位置に同じ重みを持つ畳み込みカーネルを使用し、例えば3*3の畳み込みカーネルは次のように表すことができる。

 

Box Blurの畳み込みカーネルも線形分離可能なので、分離可能なGaussian Blurと同じように、2次元のBox Blur畳み込み演算を2つの1次元Box Blur畳み込み演算に分割することができます。 例として、3×3 Box Blur の畳み込みカーネルが挙げられます。

各位置で同じ重みを持つ畳み込みカーネルは、処理によって得られる各画素点の値が、自身の画素値とフィールド内の他の画素値の平均となることを意味します。この利点は、スライディングウィンドウアルゴリズムを使用せず、より高速な累積アルゴリズムで実装できることです[1]。行列を処理する場合、畳み込みカーネルサイズの最初の要素について、累積和の平均を計算し、出力に書き込みます。その後の新しい要素については、累積和から畳み込みカーネルでカバーされなくなった要素を引き、新しい要素に加え、平均を計算し、出力に書き込みます。このことから、1次元のBox Blur演算では、1つの要素を処理する時間複雑度は、畳み込みカーネルの長さに依存せず、O(1)の定数であることがわかります。

(https://www.intel.com/content/www/us/en/developer/articles/technical/an-investigation-of-fast-real-time-gpu-based-image-blur-algorithms.htmlより)

9×9のBox Blur 畳み込みカーネルを例にとると、横方向の処理は次のように表されます。

(https://www.intel.com/content/www/us/en/developer/articles/technical/an-investigation-of-fast-real-time-gpu-based-image-blur-algorithms.htmlより)

しかし、現代のGPUの実行は高度に並列化され、無秩序であり、アキュムレータを用いて時間複雑性を低減する最適化は、CPUの計算には有効だが、GPUには有効ではありません。そのためには、Computer Shaderを使ってそのような操作を行うことを検討してください。これにより、時間の複雑さが一定になる反面、アルゴリズムの固定費が増加します。

その後の実装によると、以下のような結果が得られました。

Box Blurを一度行った結果の品質は、Gaussian Blur処理を行った結果の品質とはかけ離れていることがよくわかります。これに対して、中心極限定理は、適切な条件下で、多数の互いに独立な確率変数の平均が、適切な正規化後に正規分布に収束することを述べています[2]。つまり、ボックスぼかしを数回かけると、最終的にGaussian Blurに近い品質になるのです。このようにして、アルゴリズムの固定費がさらに増加します。


4.2 Unityの実装

上記のアルゴリズムに基づき、Unityでは、ComputerShaderを利用し、アキュムレータを用いたBox Blurアルゴリズムを実装します。

ComputerShaderを新規に作成し、1024×768の画面に後処理を行います。

まず、水平方向の処理を行い、画像の境界から外れたサンプルは、境界の画素値をサンプリングして補完します。各行のデータを確実に共有するために、各行を1スレッドとし、合計768スレッドとします。

[numthreads(1,768,1)]
void BoxBlurX(uint3 id : SV_DispatchThreadID)
{
	float4 colourSum = InputRT[int2(0, id.y)] * BlurDiameter / 2;
	for (int index = 0; index <= BlurDiameter / 2; index++)
		colourSum += InputRT[int2(index, id.y)];
	for (int index = 0; index < InputRTWidth; index++)
	{
		Result[int2(index,id.y)] = colourSum / BlurDiameter;

		// move window to the next 
		float4 leftBorder = InputRT[int2(max(index - BlurDiameter / 2, 0), id.y)];
		float4 rightBorder = InputRT[int2(min(index + BlurDiameter / 2 + 1, InputRTWidth - 1), id.y)];

		colourSum -= leftBorder;
		colourSum += rightBorder;
	}
}

垂直方向の処理も同様で、合計1024スレッドになります。

[numthreads(1024, 1, 1)]
void BoxBlurY(uint3 id : SV_DispatchThreadID)
{
	float4 colourSum = InputRT[int2(id.x, 0)] * BlurDiameter / 2;
	for (int index = 0; index <= BlurDiameter / 2; index++)
		colourSum += InputRT[int2(id.x, index)];
	for (int index = 0; index < InputRTHeight; index++)
	{
		Result[int2(id.x,index)] = colourSum / BlurDiameter;

		// move window to the next 
		float4 leftBorder = InputRT[int2(id.x, max(index - BlurDiameter / 2, 0))];
		float4 rightBorder = InputRT[int2(id.x, min(index + BlurDiameter / 2 + 1, InputRTHeight - 1))];

		colourSum -= leftBorder;
		colourSum += rightBorder;
	}
}

Csharp側でComputerShaderを呼び出し、BoxBlurの処理を1行で行う。

private void OnRenderImage(RenderTexture input, RenderTexture output)
{
    BoxBlurShader.SetInt("BlurDiameter", BlurDiameter);
    BoxBlurShader.SetInt("InputRTWidth", input.width);
    BoxBlurShader.SetInt("InputRTHeight", input.height);

    BoxBlurShader.SetTexture(kernelHandleX, "InputRT", input);
    BoxBlurShader.SetTexture(kernelHandleX, "Result", tmp0);
    BoxBlurShader.Dispatch(kernelHandleX, 1, 1, 1);

    BoxBlurShader.SetTexture(kernelHandleY, "InputRT", tmp0);
    BoxBlurShader.SetTexture(kernelHandleY, "Result", tmp1);
    BoxBlurShader.Dispatch(kernelHandleY, 1, 1, 1);
    Graphics.Blit(tmp1, output);
}

サイズ36×36の畳み込みカーネルを選び、以下の結果を得ます。

ご覧の通り、非常にはっきりとしたモザイクがかかっているので、さらにBox Blur処理を実行します。

非常に良い結果が出てきました。


4.3まとめ

Box Blurアルゴリズムの大きな利点は、要素の処理にかかる時間的複雑さを定数に減らし、畳み込みカーネルのサイズに関係しなくすることです。しかし、固定費が高いのが難点です。また、Computer Shaderを使用しているため、このアルゴリズムにサッポートしないデバイスもあります。 このアルゴリズムの性能上の利点は、使用する畳み込みコアのサイズがある閾値を超えた場合にのみ発揮してくれます。そのため、一部の特殊なケースにしか適さないと言えます。


参考資料

[1] https://en.wikipedia.org/wiki/Box_blur

[2]https://zh.wikipedia.org/wiki/%E4%B8%AD%E5%BF%83%E6%9E%81%E9%99%90%E5%AE%9A%E7%90%86

 

https://web.archive.org/web/20060718054020/http://www.acm.uiuc.edu/siggraph/workshops/wjarosz_convolution_2001.pdf

https://ams.xyz/dev/FinalDegreeProjectReport-Andr%C3%A9sValencia.pdf

https://cloud.tencent.com/developer/article/1035559

https://en.wikipedia.org/wiki/Box_blur

https://web.archive.org/web/20060718054020/http://www.acm.uiuc.edu/siggraph/workshops/wjarosz_convolution_2001.pdf

http://blog.ivank.net/fastest-gaussian-blur.html

https://medium.com/mobile-app-development-publication/blurring-image-algorithm-example-in-android-cec81911cd5e

http://dev.theomader.com/gaussian-kernel-calculator/

http://genderi.org/frame-buffer-postprocessing-effects-in-double-s-t-e-a-l-wreckl.html