カスタムOpenGLES2.0シェーダーを使用して分子構造の3D表現を表示するオープンソースのiOSアプリケーションがあります。これは、多くの頂点を使用して構築された同じ形状の代わりに、長方形上に描画された手続き的に生成された球と円柱の詐欺師を使用して行われます。このアプローチの欠点は、これらの詐欺師オブジェクトの各フラグメントの深度値をフラグメントシェーダーで計算して、オブジェクトがオーバーラップするときに使用する必要があることです。
残念ながら、OpenGL ES 2.0 gl_FragDepthへの書き込みは許可されていません なので、これらの値をカスタムの深度テクスチャに出力する必要がありました。フレームバッファオブジェクト(FBO)を使用してシーンをパスオーバーし、深度値に対応する色のみをレンダリングして、結果をテクスチャに保存します。次に、このテクスチャはレンダリングプロセスの後半に読み込まれ、実際の画面イメージが生成されます。その段階のフラグメントが、画面上のそのポイントの深度テクスチャに格納されている深度レベルにある場合、それが表示されます。そうでなければ、それは投げられます。ダイアグラムを含むプロセスの詳細については、私の投稿 ここ を参照してください。
この深さのテクスチャの生成は、私のレンダリングプロセスのボトルネックであり、それを高速化する方法を探しています。本来より遅いようですが、理由がわかりません。この深度テクスチャの適切な生成を実現するために、GL_DEPTH_TEST
は無効にされ、GL_BLEND
はglBlendFunc(GL_ONE, GL_ONE)
で有効にされ、glBlendEquation()
はGL_MIN_EXT
に設定されます。この方法でのシーン出力は、iOSデバイスのPowerVRシリーズのようなタイルベースの遅延レンダラーでは最速ではないことを私は知っていますが、これを行うためのより良い方法を考えることはできません。
球体用の深度フラグメントシェーダー(最も一般的な表示要素)がこのボトルネックの中心にあるように見えます(Instrumentsでのレンダラー使用率は99%に固定されており、フラグメント処理によって制限されていることを示しています)。現在、次のようになっています。
precision mediump float;
varying mediump vec2 impostorSpaceCoordinate;
varying mediump float normalizedDepth;
varying mediump float adjustedSphereRadius;
const vec3 stepValues = vec3(2.0, 1.0, 0.0);
const float scaleDownFactor = 1.0 / 255.0;
void main()
{
float distanceFromCenter = length(impostorSpaceCoordinate);
if (distanceFromCenter > 1.0)
{
gl_FragColor = vec4(1.0);
}
else
{
float calculatedDepth = sqrt(1.0 - distanceFromCenter * distanceFromCenter);
mediump float currentDepthValue = normalizedDepth - adjustedSphereRadius * calculatedDepth;
// Inlined color encoding for the depth values
float ceiledValue = ceil(currentDepthValue * 765.0);
vec3 intDepthValue = (vec3(ceiledValue) * scaleDownFactor) - stepValues;
gl_FragColor = vec4(intDepthValue, 1.0);
}
}
IPad 1では、パススルーシェーダーを使用してDNA空間充填モデルのフレームをレンダリングするのに35〜68ミリ秒かかります(iPhone 4では18〜35ミリ秒)。 PowerVR PVRUniSCoコンパイラ( SDK の一部)によると、このシェーダーは最大で11 GPUサイクル、最悪で16サイクルを使用します。シェーダーで分岐を使用しないことをお勧めしますが、この場合、それ以外の場合よりもパフォーマンスが向上します。
私がそれを単純化すると
precision mediump float;
varying mediump vec2 impostorSpaceCoordinate;
varying mediump float normalizedDepth;
varying mediump float adjustedSphereRadius;
void main()
{
gl_FragColor = vec4(adjustedSphereRadius * normalizedDepth * (impostorSpaceCoordinate + 1.0) / 2.0, normalizedDepth, 1.0);
}
iPad 1では18〜35ミリ秒かかりますが、iPhone 4では1.7〜2.4ミリ秒しかかかりません。このシェーダーの推定GPUサイクル数は8サイクルです。サイクルカウントに基づくレンダリング時間の変化は直線的ではないようです。
最後に、一定の色を出力するだけの場合:
precision mediump float;
void main()
{
gl_FragColor = vec4(0.5, 0.5, 0.5, 1.0);
}
レンダリング時間は、iPad 1では1.1〜2.3ミリ秒(iPhone 4では1.3ミリ秒)に低下します。
レンダリング時間の非線形スケーリングと、2番目のシェーダーのiPadとiPhone 4の間の突然の変化により、ここで欠けているものがあると思います。これらの3つのシェーダーバリアント(SphereDepth.fshファイルを調べて適切なセクションをコメントアウトする)とテストモデルを含む完全なソースプロジェクトは、これを自分で試してみたい場合は、 ここ からダウンロードできます。
これまで読んだことがあるなら、私の質問は次のとおりです。このプロファイリング情報に基づいて、iOSデバイスでカスタムデプスシェーダーのレンダリングパフォーマンスを向上させるにはどうすればよいですか?
Tommy、Pivot、およびrotoglupの推奨に基づいて、いくつかの最適化を実装しました。これにより、アプリケーションの深度テクスチャ生成と全体的なレンダリングパイプラインの両方のレンダリング速度が2倍になりました。
最初に、以前はほとんど効果がなかった、事前に計算された球の深さとライティングテクスチャを再度有効にしましたが、そのテクスチャから色やその他の値を処理するときに、適切なlowp
精度値を使用するようになりました。この組み合わせと、テクスチャの適切なミップマッピングにより、パフォーマンスが最大10%向上するようです。
さらに重要なことに、深度テクスチャと最終的なレイトレーシングされた詐欺師の両方をレンダリングする前にパスを実行し、不透明なジオメトリを配置して、レンダリングされないピクセルをブロックします。これを行うには、深度テストを有効にしてから、シーン内のオブジェクトを構成する正方形を、単純な不透明シェーダーを使用してsqrt(2)/ 2で縮小して描画します。これにより、表現された球体で不透明であることがわかっている領域をカバーするはめ込み正方形が作成されます。
次に、glDepthMask(GL_FALSE)
を使用して深度書き込みを無効にし、ユーザーに1半径近い場所に正方形の球の詐欺師をレンダリングします。これにより、iOSデバイスのタイルベースの遅延レンダリングハードウェアは、どのような条件下でも画面に表示されないフラグメントを効率的に取り除き、ピクセルごとの深度値に基づいて、可視球の詐欺師間のスムーズな交差を実現します。これは、以下の私の大まかな図に示されています。
この例では、上位2つの詐欺師の不透明なブロック正方形は、これらの可視オブジェクトからのフラグメントのレンダリングを妨げませんが、最下位の詐欺師からのフラグメントのチャンクをブロックします。最前部の詐欺師は、ピクセルごとのテストを使用してスムーズな交差を生成できますが、後部の詐欺師からのピクセルの多くは、レンダリングによってGPUサイクルを無駄にしません。
深度書き込みを無効にすることは考えていませんでしたが、最後のレンダリング段階を実行するときは深度テストをそのままにしておきます。これは、詐欺師が単純に互いに積み重なるのを防ぎながら、PowerVRGPU内のハードウェア最適化の一部を使用するための鍵です。
私のベンチマークでは、上記で使用したテストモデルをレンダリングすると、以前に取得した35〜68ミリ秒と比較して、フレームあたり18〜35ミリ秒の時間が得られ、レンダリング速度がほぼ2倍になります。この同じ不透明なジオメトリの事前レンダリングをレイトレーシングパスに適用すると、全体的なレンダリングパフォーマンスが2倍になります。
奇妙なことに、はめ込みと外接八角形を使用してこれをさらに洗練しようとすると、描画時にカバーするピクセルが約17%少なくなり、フラグメントのブロックがより効率的になります。実際には、単純な正方形を使用する場合よりもパフォーマンスが低下しました。最悪の場合、タイラーの使用率は依然として60%未満であったため、ジオメトリが大きいほどキャッシュミスが多くなる可能性があります。
編集(2011年5月31日):
Pivotの提案に基づいて、長方形の代わりに使用する内接八角形と外接八角形を作成しました。ラスタライズ用に三角形を最適化するための推奨事項 ここ に従っただけです。以前のテストでは、多くの不要なフラグメントを削除し、カバーされたフラグメントをより効率的にブロックできるにもかかわらず、八角形は正方形よりもパフォーマンスが低下していました。次のように三角形の描画を調整します。
正方形から八角形に切り替えることで、上記の最適化に加えて、全体のレンダリング時間を平均14%短縮することができました。深度テクスチャは19ミリ秒で生成され、時折2ミリ秒にディップし、35ミリ秒にスパイクします。
編集2(2011年5月31日):
八角形のために破棄するフラグメントが少なくなったので、step関数を使用するというTommyのアイデアを再検討しました。これを球の深度ルックアップテクスチャと組み合わせると、テストモデルの深度テクスチャ生成のためにiPad1で平均2ミリ秒のレンダリング時間が発生します。私は、このレンダリングの場合に期待できるほど良いものであり、私が始めたところから大幅に改善されたと考えています。後世のために、これが私が現在使用しているデプスシェーダーです:
precision mediump float;
varying mediump vec2 impostorSpaceCoordinate;
varying mediump float normalizedDepth;
varying mediump float adjustedSphereRadius;
varying mediump vec2 depthLookupCoordinate;
uniform lowp sampler2D sphereDepthMap;
const lowp vec3 stepValues = vec3(2.0, 1.0, 0.0);
void main()
{
lowp vec2 precalculatedDepthAndAlpha = texture2D(sphereDepthMap, depthLookupCoordinate).ra;
float inCircleMultiplier = step(0.5, precalculatedDepthAndAlpha.g);
float currentDepthValue = normalizedDepth + adjustedSphereRadius - adjustedSphereRadius * precalculatedDepthAndAlpha.r;
// Inlined color encoding for the depth values
currentDepthValue = currentDepthValue * 3.0;
lowp vec3 intDepthValue = vec3(currentDepthValue) - stepValues;
gl_FragColor = vec4(1.0 - inCircleMultiplier) + vec4(intDepthValue, inCircleMultiplier);
}
テストサンプルを更新しました ここ 、この新しいアプローチが最初に行っていたものと比較して実際に動作することを確認したい場合。
私はまだ他の提案を受け入れていますが、これはこのアプリケーションにとって大きな前進です。
デスクトップでは、多くの初期のプログラマブルデバイスで、8または16などのフラグメントを同時に処理できたものの、それらの多くに対して実質的に1つのプログラムカウンタしか持っていなかったことがありました(これは、フェッチ/デコードユニットが1つだけであり、 8または16ピクセルの単位で動作する限り、他のすべての1つ)。したがって、条件付きの最初の禁止と、その後しばらくの間、一緒に処理されるピクセルの条件付き評価が異なる値を返した場合、それらのピクセルはいくつかの配置でより小さなグループで処理されるという状況。
PowerVRは明示的ではありませんが、 アプリケーション開発の推奨事項 フロー制御に関するセクションがあり、動的ブランチに関する多くの推奨事項を作成します。通常、結果が合理的に予測できる場合にのみ、動的ブランチを推奨します。彼らは同じようなことをしている。したがって、速度の不一致は、条件付きを含めたことが原因である可能性があることをお勧めします。
最初のテストとして、次のことを試してみるとどうなりますか?
void main()
{
float distanceFromCenter = length(impostorSpaceCoordinate);
// the step function doesn't count as a conditional
float inCircleMultiplier = step(distanceFromCenter, 1.0);
float calculatedDepth = sqrt(1.0 - distanceFromCenter * distanceFromCenter * inCircleMultiplier);
mediump float currentDepthValue = normalizedDepth - adjustedSphereRadius * calculatedDepth;
// Inlined color encoding for the depth values
float ceiledValue = ceil(currentDepthValue * 765.0) * inCircleMultiplier;
vec3 intDepthValue = (vec3(ceiledValue) * scaleDownFactor) - (stepValues * inCircleMultiplier);
// use the result of the step to combine results
gl_FragColor = vec4(1.0 - inCircleMultiplier) + vec4(intDepthValue, inCircleMultiplier);
}
これらのポイントの多くは、回答を投稿した他の人によってカバーされていますが、ここでの包括的なテーマは、レンダリングが多くの作業を実行し、それが破棄されることです。
シェーダー自体が冗長な作業を行う可能性があります。ベクトルの長さはsqrt(dot(vector, vector))
として計算される可能性があります。円の外側のフラグメントを拒否するためにsqrtは必要ありません。とにかく、深さを計算するために長さを2乗します。さらに、深度値の明示的な量子化が実際に必要かどうかを確認しましたか、またはフレームバッファーに浮動小数点から整数へのハードウェアの変換を使用するだけで解決できますか(準を確実にするために追加のバイアスが必要になる可能性があります) -深度テストはすぐに出てきます)?
多くのフラグメントは自明に円の外側にあります。描画している四角形の領域のπ/ 4のみが有用な深度値を生成します。この時点で、アプリはフラグメント処理に大きく偏っていると思います。そのため、シェーディングする必要のある領域を減らす代わりに、描画する頂点の数を増やすことを検討することをお勧めします。正射影で球を描画しているので、外接する正多角形でもかまいませんが、十分なピクセルをラスタライズするために、ズームレベルによっては少し余分なサイズが必要になる場合があります。
多くのフラグメントは他のフラグメントによって簡単に遮られます。他の人が指摘しているように、ハードウェア深度テストを使用していないため、シェーディング作業を早期に強制終了するTBDRの機能を十分に活用していません。 2)の何かをすでに実装している場合は、生成できる最大の深さ(球の中央を通る平面)に内接する正多角形を描画し、最小の深さで実際の多角形を描画するだけです。 (球の正面)。 Tommyとrotoglupの両方の投稿には、すでに状態ベクトルの詳細が含まれています。
2)と3)は、レイトレーシングシェーダーにも適用されることに注意してください。
私はモバイルプラットフォームの専門家ではありませんが、あなたを悩ませているのは次のことだと思います。
GL_DEPTH
テストを無効にすると、深度パスで大規模なオーバードローが発生します深度テストの前に描かれた追加のパスは役に立ちませんか?
このパスでは、たとえば、クワッドフェーシングカメラ(またはセットアップが簡単な立方体)として表され、関連付けられた球に含まれる各球を描画することにより、GL_DEPTH
プレフィルを実行できます。このパスは、GL_DEPTH_TEST
とglDepthMask
を有効にしただけで、カラーマスクやフラグメントシェーダーなしで描画できます。デスクトッププラットフォームでは、これらの種類のパスは、カラー+深度パスよりも速く描画されます。
次に、深度計算パスで、GL_DEPTH_TEST
を有効にしてglDepthMask
を無効にすることができます。これにより、より近いジオメトリによって隠されているピクセルに対してシェーダーが実行されなくなります。
このソリューションでは、別の一連の描画呼び出しを発行する必要があるため、これは有益ではない可能性があります。