コンソールおよびデスクトップ用のグラフィックに関するほとんどの出版物は、新しいことについて語っています。モバイルプラットフォームでは、既存のものの最適化が常に最前線にあります。
後処理に関しては、写真に対する魔法の効果は最初のコンピューターが登場するずっと前に発見され、デジタル画像処理用に作成された数学的およびアルゴリズム的基盤は、プログラム可能なGPUパイプラインにうまく適合しました。
ポストエフェクト(または、あまり有効でない使用)はプレイヤー間の憎しみの問題であるという事実に加えて、それらは、写真を迅速かつ安価に「復活」および「リフレッシュ」するためのほとんど唯一の方法でもあります。 この「リバイバル」がどれだけ高品質であるか、そして「新鮮さ」の結果としてそれがどうなるかは、大部分がアーティストにかかっています。
戦争ロボットのスクリーンショット。
前述のように、この記事は主に最適化に専念します。 知らない人にとっては、GPU Gemsシリーズの本は優れた入門コースであり、最初の3つはNVidiaのWebサイトで入手できます[1]。
これらの例はUnityに実装されていますが、ここで説明する最適化方法はすべての開発環境に適用できます。
最適な後処理アーキテクチャ
ポストエフェクトをレンダリングする方法は2つあります。
- 順次-レンダリングが個別のステップに分割され、各ステップでステップごとに1つのポストエフェクトのみが画像に適用される場合。
- バッチ-最初に各エフェクトの中間結果がレンダリングされ、次に最終ステップですべてのポストエフェクトが適用されます。
シーケンシャルレンダリングは実装が簡単で、設定の点で便利です。 これは、「ポストエフェクト」タイプのオブジェクトのリストとして基本的に実装され、そのレンダリング順序は理論的には任意です(実際には、いいえ)。さらに、同じタイプのエフェクトを複数回適用できます。 実際、そのような利点は、孤立した場合にのみ需要があります。
同時に、バッチレンダリングはメモリアクセスの合計数を節約するため、著しく効率的です。 後者は、計算負荷の増加に伴って熱伝達が増加するモバイルプラットフォームに最も関連性があります(だれが考えたでしょう)。 デバイスが必要なフレームレートを発行できたとしても、プレーヤーが熱い「レンガ」を手にして快適にプレイできるとは考えられません。
明確にするために、War Robotsで使用されるポストエフェクトをレンダリングするための一貫したバッチスキームを示します。
シーケンシャルレンダリング:8読み取り、6レコード。
バッチレンダリング:7つの読み取り、5つのレコード。
Unityのバッチレンダリングは、 Post Processing Stackモジュールに実装されています[2]。
コードを変更せずにポストエフェクトを適用する順序は変更できませんが(必須ではありません)、個々のポストエフェクトを無効にできます。 さらに、このモジュールは、 Underの組み込みリソースキャッシュRenderTexture [3]を集中的に使用するため、特定のポストエフェクトのコードには通常、レンダリング命令のみが含まれます。
レンダリング中に同じポストエフェクトリクエストに直接リソースを割り当て、完了時にそれらを解放します。 これにより、キャッシュは過去数フレームで要求されていないリソースのみを削除するため、後続のポストエフェクトでリソースの再利用を整理できます。
バッチレンダリングの最終段階は、前のすべての手順の結果を組み合わせて、多変量の「uberシェーダー」を使用してレンダリングする合成効果です。 Unity3Dでは、このようなシェーダーは、プリプロセッサディレクティブ#pragma multi_compileまたは#pragma shader_featureを使用して作成できます。
一般的に、後処理スタックが好きでしたが、それでもファイルなしでは行きませんでした。 ポストエフェクト(プリパスを含む)を追加または置換できるスケーラブルなモジュールが必要でした。また、レンダリングシーケンスを定義するハードコーディングされたパイプラインと複合「uberシェーダー」を変更する必要がありました。 さらに、エフェクトの品質設定と特定のシーンのパラメーターがエフェクトにポストされました。
充填率の最適化
後処理の主なレンダリング方法はブリットです。指定されたシェーダーは、レンダリングターゲットとして使用されるテクスチャのすべてのフラグメントに適用されます。 したがって、レンダリングのパフォーマンスは、テクスチャのサイズとシェーダーの計算の複雑さに依存します。 パフォーマンスを改善する最も簡単な方法(つまり、テクスチャのサイズを小さくする)は、後処理の品質に影響します。
ただし、レンダリングがテクスチャの特定の領域でのみ必要であることが事前にわかっている場合は、たとえばブリットを3Dモデルのレンダリングに置き換えることにより、プロセスを最適化できます。 もちろん、代わりにビューポート設定を使用することを禁止する人はいませんが、3Dモデルは頂点ごとのデータ量の増加とは異なり、より高度な頂点シェーダーを使用できます。
これはまさに太陽からの光散乱のポストエフェクトで行ったことです[4]。 太陽テクスチャを使用したビルボードレンダリングに置き換えることにより、元のプリパスを簡素化しました。 シーンオブジェクトの背後に隠されたビルボードフラグメントは、フルスクリーンマスクを使用して選択されました。これは、組み合わせてシャドウバッファーとして機能します(シャドウのレンダリングについては後で詳しく説明します)。
右:シャドウバッファーとマスク。ステップ関数を適用して取得します。 アルファが1未満のすべてのテクセルは、「太陽」と重なります。
struct appdata { float4 vertex : POSITION; half4 texcoord : TEXCOORD0; }; struct v2f { float4 pos : SV_POSITION; half4 screenPos : TEXCOORD0; half2 uv : TEXCOORD1; };
#include “Unity.cginc” sampler2D _SunTex; sampler2D _WWROffscreenBuffer; half4 _SunColor; v2f vertSunShaftsPrepass(appdata v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); o.screenPos = ComputeScreenPos(o.pos); o.uv = v.texcoord; return o; }
fixed4 fragSunShaftsPrepass(v2f i) : COLOR { // _WWROffscreenBuffer - == 1 // const half AlphaThreshold = 0.99607843137; // 1 - 1.0/255.0 fixed4 result = tex2D( _SunTex, i.uv ) * _SunColor; half shadowSample = tex2Dproj( _WWROffscreenBuffer, UNITY_PROJ_COORD(i.screenPos) ).a; return result * step( AlphaThreshold, shadowSample ); }
プレパステクスチャスムージングは、3Dモデルをレンダリングすることでも実行されます。
struct appdata { float4 vertex : POSITION; }; struct v2f { float4 pos : SV_POSITION; half4 screenPos : TEXCOORD0; };
#include “Unity.cginc” sampler2D _PrePassTex; half4 _PrePassTex_TexelSize; half4 _BlurDirection; v2f vertSunShaftsBlurPrepass(appdata v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); o.screenPos = ComputeScreenPos(o.pos); o.uv = v.texcoord; return o; }
fixed4 fragSunShaftsBlurPrepass(v2f i) : COLOR { half2 uv = i.screenPos.xy / i.screenPos.w; half2 blurOffset1 = _BlurDirection * _PrePassTex_TexelSize.xy * 0.53805; half2 blurOffset2 = _BlurDirection * _PrePassTex_TexelSize.xy * 2.06278; half2 uv0 = uv + blurOffset1; half2 uv1 = uv – blurOffset1; half2 uv2 = uv + blurOffset2; half2 uv3 = uv – blurOffset2; return (tex2D(_PrePassTex, uv0) + tex2D(_PrePassTex, uv1)) * 0.44908 + (tex2D(_PrePassTex, uv2) + tex2D(_PrePassTex, uv3)) * 0.05092; }
もちろん、最後までやりました。3Dモデルをレンダリングすることによって、最後の一節も行いました。 また、ビューポートのブリントで置き換えることができる以前のケースとは異なり、3Dモデルにはエフェクトシェーダーで使用される追加のデータ(頂点カラー)が含まれています。
struct appdata { float4 vertex : POSITION; float4 color : COLOR; }; struct v2f { float4 pos : POSITION; float4 color : COLOR; float4 screenPos : TEXCOORD0; };
#include “Unity.cginc” sampler2D _PrePassTex; float4 _SunScreenPos; int _NumSamples; int _NumSteps; float _Density; float _Weight; float _Decay; float _Exposure; v2f vertSunShaftsRadialBlur(appdata v) { v2f o; o.pos = mul(UNITY_MATRIX_MVP, v.vertex); o.screenPos = ComputeScreenPos(o.pos); o.color = v.color; return o; }
float4 fragSunShaftsRadialBlur(v2f i) : COLOR { float4 color = i.color; float2 uv = i.screenPos.xy / i.screenPos.w; float2 deltaTextCoords = (uv - _SunScreenPos.xy) / float(_NumSamples) * _Density; float2 illuminationDecay = 1.0; float4 result = 0; float4 sample0 = tex2D(_PrePassTex, uv); for(int i=0; i<_NumSteps; i++) { uv -= deltaTextCoords * 2; float4 sample2 = tex2D(_PrePassTex, uv); float4 sample1 = (sample0 + sample2) * 0.5; result += sample0 * illuminationDecay * _Weight; illuminationDecay *= _Decay; result += sample1 * illuminationDecay * _Weight; illuminationDecay *= _Decay; result += sample2 * illuminationDecay * _Weight; illuminationDecay *= _Decay; sample0 = sample2; } result *= _Exposure * color; return result; }
動的シャドウ最適化
ポストエフェクトの計算の複雑さにもかかわらず、多くの場合、動的シャドウはさらにリソースに依存します。 これは、各シェーダーの計算の複雑さだけでなく、滑らかなシャドウを取得するには追加のフルスクリーンレンダリングパスが必要であるという事実によるものです。
通常、PCFフィルターは、 シャドウマッピング手法[5]を使用して、画像の断片のシェーディングを計算するために使用されます。 ただし、追加のスムージングなしの結果では、非常に大きなカーネルサイズのPCFのみが得られ、モバイルプラットフォームでは受け入れられません。 より高度なVariance Shadow Mappingメソッドでは、偏微分近似命令と浮動小数点テクスチャの双線形フィルタリングのサポートが必要です[6]。
ソフトシャドウを取得するために、可視シーン全体が2回レンダリングされます-初めて、シャドウのみがオフスクリーンバッファーにレンダリングされ、その後、スムージングフィルターがオフスクリーンバッファーに適用されます。その後、オフスクリーンバッファーからのシャドウの影響を考慮して、オブジェクトの色がスクリーンにレンダリングされます。 これにより、CPU(クリッピング、ソート、ドライバーへのアクセス)とGPUの両方の二重ロードが発生します。
この問題の解決策の1つとして、遅延照明技術に切り替えることなく、シーンの二重レンダリングを取り除くことにしました。
まず、画像をRGBA形式の中間バッファーにレンダリングします(1)。 アルファ値は、フラグメントがシャドウ内にあった場合のフラグメントの色の明度とシャドウなしの明度の比率です(2)。 次に、コマンドバッファーを使用して、不透明なジオメトリのレンダリングが完了した時点で制御を行い、バッファーからアルファを取得します。 次に、スムージング(3)し、スムージングされたシャドウを中間バッファーのカラーチャンネルで変調します(4)。 その後、Unityパイプラインが再開します。透明なオブジェクトとスカイボックスがレンダリングされます(5)。
このトリックは、日陰の場所での色再現のわずかな劣化につながりますが、アルファで書かれたものを計算するトリックは、この効果を最小限に抑えました。
// shadow = 0..1 // spec - specular lighting // diff - diffuse lighting fixed4 c = tex2D( _MainTex, i.uv ); fixed3 ambDiffuse = c.xyz * UNITY_LIGHTMODEL_AMBIENT; fixed3 diffuseColor = _LightColor0.rgb * diff + UNITY_LIGHTMODEL_AMBIENT; fixed3 specularColor = _LightColor0.rgb * spec * shadow; c.rgb = saturate( c.rgb * diffuseColor + specularColor ); ca = Luminance( ambDiffuse / c.rgb );
その結果、中程度のパフォーマンスのデバイス(主にAndroid)で生産性の顕著な増加(10-15%)を受け取り、多くのデバイスで熱伝達が減少しました。 この手法は、遅延照明に切り替える前の中間ソリューションです。
プロモーションの撮影には、より良いオプションを使用します。 色の劣化は望ましくなく、PCリソースで十分です。 この場合の影の柔らかさを改善するために、以下を適用しました。影を適用するとき、LDotNを考慮した式が使用され、明るい場所でより滑らかな遷移を可能にします。
fixed shLDotN = lerp( clamp( shadow, 0, LDotN ), LDotN * shadow, 1 - LDotN);
それに対する料金は、ブルーリング中に完全に黒くならない場所でのシャドウのわずかな燃え切りですが、その結果、部分シェードの移行がよりスムーズになります。
参照資料
[1] GPU Gems developer.nvidia.com/gpugems/GPUGems/gpugems_pref01.html
[2] Unity3D後処理スタックgithub.com/Unity-Technologies/PostProcessing
[3] RenderTextureのキャッシュdocs.unity3d.com/ScriptReference/RenderTexture.html
[4]ポストプロセスとしての体積光散乱http.developer.nvidia.com/GPUGems3/gpugems3_ch13.html
[5]パーセントクローズフィルタリングhttp.developer.nvidia.com/GPUGems/gpugems_ch11.html
[6] Summed -Area Variance Shadow Maps http.developer.nvidia.com/GPUGems3/gpugems3_ch08.html
PS
Igor Polishchukには特別な感謝を申し上げます。IgorPolishchukは、実際に、ここで説明したシャドウに関連するすべてのトリックを思いつき、さらに、この記事の執筆に参加しました。