ケース スタディ - パフォーマンスが異なるデバイス間で Datascape をスケーリングする

Datascape は、microsoft Windows Mixed Reality開発されたデータ アプリケーションの 1 つで、地形データの上に気象データを表示する方法に重点を置いたアプリケーションです。 このアプリケーションは、ホログラフィック データの視覚化でユーザーを囲み、Mixed Reality でデータを検出することでユーザーが得る独自の分析情報を探索します。

Datascape では、Microsoft HoloLens から Windows Mixed Reality イマーシブ ヘッドセットまで、および低電力 PC から最新の GPU 搭載 PC まで、さまざまなハードウェア機能を備えたさまざまなプラットフォームをターゲットにしたいと思っています。 主な課題は、高フレームレートで実行しながら、グラフィックス機能が大きな異なるデバイスで視覚的に魅力的な問題でシーンをレンダリングする方法でした。

このケース スタディでは、より多くの GPU を集中的に使用するシステムの作成に使用されるプロセスと手法について説明し、発生した問題とそれらを克服する方法について説明します。

透明度とオーバー描画

GPU では透明性が高い可能性があるという理由で、主なレンダリングは透明性に対処する上で苦労します。

深度バッファーへの書き込み中に、ソリッド ジオメトリを前後にレンダリングして、そのピクセルの背後にある将来のピクセルが破棄されるのを停止できます。 これにより、非表示のピクセルがピクセル シェーダーを実行するのを防ぎ、プロセスを大幅に高速化できます。 geometry が最適に並べ替えらた場合、画面の各ピクセルは 1 回だけ描画されます。

透過ジオメトリは前面に並べ替える必要があります。ピクセル シェーダーの出力を画面の現在のピクセルにブレンドする必要があります。 これにより、画面の各ピクセルがフレームごとに複数回描画され、オーバー描画と呼ばれる可能性があります。

メインHoloLens PC の場合、画面を塗りつぶすのは数回だけであり、透過的なレンダリングが問題になります。

Datascape シーン コンポーネントの概要

シーンには 3 つの主要なコンポーネントがあります。 UI、マップ、 および 天気。 気象効果には、取得できるすべての GPU 時間が必要なことを早い段階で知っていたので、UI と地形を、オーバー描画を減らす方法で設計しました。

UI を何度か作り直して、それが生成するオーバー描画の量を最小限に抑えました。 光るボタンやマップの概要などのコンポーネントに対して、透明なアートを重ね合うのではなく、より複雑なジオメトリの側で誤りを見ていました。

マップでは、シャドウや複雑な照明などの標準的な Unity 機能を取り除くカスタム シェーダーを使用し、それらを単純な単一の太陽照明モデルとカスタムの太陽の計算に置き換えます。 これにより、単純なピクセル シェーダーが生成され、GPU サイクルが解放されます。

UI とマップの両方を予算内でレンダリングし、ハードウェアに応じて変更を加える必要がなかったことを確認しました。しかし、気象の視覚化 (特にクラウド レンダリング) は、より困難であることが証明されました。

クラウド データの背景

クラウド データは NOAA サーバー ( からダウンロードされ、3 つの異なる 2D レイヤーで提供され、それぞれクラウドの上と下の高さ、およびグリッドの各セルのクラウドの密度が表示されます。 https://nomads.ncep.noaa.gov/) データはクラウド情報テクスチャに処理され、GPU に簡単にアクセスできるテクスチャの赤、緑、青のコンポーネントに各コンポーネントが格納されました。

ジオメトリ クラウド

低電力のマシンがクラウドをレンダリングすることを確認するために、まず、ソリッド ジオメトリを使用してオーバー描画を最小限に抑えるアプローチから始めました。

最初に、頂点ごとのクラウド情報テクスチャの半径を使用して、各レイヤーに対して実線の高さマップ メッシュを生成して、形状を生成することで、クラウドの生成を試みました。 ジオメトリ シェーダーを使用して、クラウドの上部と下部の両方で頂点を生成し、ソリッド クラウドシェイプを生成しました。 テクスチャの密度値を使用して、より高密度のクラウドの色を濃い色でクラウドに色付けしました。

頂点を作成するためのシェーダー:

v2g vert (appdata v)
{
    v2g o;
    o.height = tex2Dlod(_MainTex, float4(v.uv, 0, 0)).x;
    o.vertex = v.vertex;
    return o;
}
 
g2f GetOutput(v2g input, float heightDirection)
{
    g2f ret;
    float4 newBaseVert = input.vertex;
    newBaseVert.y += input.height * heightDirection * _HeigthScale;
    ret.vertex = UnityObjectToClipPos(newBaseVert);
    ret.height = input.height;
    return ret;
}
 
[maxvertexcount(6)]
void geo(triangle v2g p[3], inout TriangleStream<g2f> triStream)
{
    float heightTotal = p[0].height + p[1].height + p[2].height;
    if (heightTotal > 0)
    {
        triStream.Append(GetOutput(p[0], 1));
        triStream.Append(GetOutput(p[1], 1));
        triStream.Append(GetOutput(p[2], 1));
 
        triStream.RestartStrip();
 
        triStream.Append(GetOutput(p[2], -1));
        triStream.Append(GetOutput(p[1], -1));
        triStream.Append(GetOutput(p[0], -1));
    }
}
fixed4 frag (g2f i) : SV_Target
{
    clip(i.height - 0.1f);
 
    float3 finalColor = lerp(_LowColor, _HighColor, i.height);
    return float4(finalColor, 1);
}

実際のデータの上に詳細を表示するために、小さなノイズ パターンを導入しました。 丸いクラウド エッジを生成するために、補間半径の値がしきい値に達するとピクセル シェーダー内のピクセルをクリップし、ゼロに近い値を破棄しました。

ジオメトリ クラウド

クラウドは単一のジオメトリであり、地形の前にレンダリングして、フレームレートをさらに向上させるために、その下に高価なマップ ピクセルを非表示にできます。 このソリューションは、単一のジオメトリ レンダリング アプローチにより、min-spec から high-end グラフィックス カードまで、および HoloLens 上のすべてのグラフィックス カードでうまく機能しました。

ソリッド パーティクル クラウド

これで、クラウド データのまともな表現を生成したバックアップ ソリューションを作成しましたが、"wow" 要素では少し不足し、私たちの高性能マシンに必要なボリューム感は伝えきれなかったのです。

次の手順では、より有機的でボリューム的な外観を生成するために、約 100,000 のパーティクルでそれらを表して、クラウドを作成しました。

パーティクルがソリッドであり、前後に並べ替える場合でも、前にレンダリングされたパーティクルの背後にあるピクセルの深度バッファー カリングを利用して、オーバー描画を減らします。 また、パーティクルベースのソリューションを使用すると、さまざまなハードウェアをターゲットとするために使用されるパーティクルの量を変更できます。 ただし、すべてのピクセルは引き続き深度テストを行う必要があります。その結果、追加のオーバーヘッドが発生します。

最初に、起動時にエクスペリエンスの中心点を中心にパーティクル位置を作成しました。 中心の周りにパーティクルを密度を高め、距離を減らします。 最も近いパーティクルが最初にレンダリングされるので、すべてのパーティクルを中心から背面に事前に並べ替えました。

コンピューティング シェーダーは、クラウド情報テクスチャをサンプリングして、各パーティクルを正しい高さに配置し、密度に基づいて色付けします。

DrawProcedural を使用して、パーティクルデータを GPU 上に引きつながって、パーティクルごとに 2 つの四角形をレンダリングしました。

各パーティクルには、高さと半径の両方が含まれる。 高さは、クラウド情報テクスチャからサンプリングされたクラウド データに基づいており、半径は、最も近い近隣への水平方向の距離を格納するために計算される最初の分布に基づいていました。 この四角形では、このデータを使用して高さによって角度を付け、ユーザーが水平方向に見て、高さが表示され、ユーザーがそれを上から下に見た場合、その近隣の間の領域がカバーされます。

パーティクルの形状

分布を示すシェーダー コード:

ComputeBuffer cloudPointBuffer = new ComputeBuffer(6, quadPointsStride);
cloudPointBuffer.SetData(new[]
{
    new Vector2(-.5f, .5f),
    new Vector2(.5f, .5f),
    new Vector2(.5f, -.5f),
    new Vector2(.5f, -.5f),
    new Vector2(-.5f, -.5f),
    new Vector2(-.5f, .5f)
});
 
StructuredBuffer<float2> quadPoints;
StructuredBuffer<float3> particlePositions;
v2f vert(uint id : SV_VertexID, uint inst : SV_InstanceID)
{
    // Find the center of the quad, from local to world space
    float4 centerPoint = mul(unity_ObjectToWorld, float4(particlePositions[inst], 1));
 
    // Calculate y offset for each quad point
    float3 cameraForward = normalize(centerPoint - _WorldSpaceCameraPos);
    float y = dot(quadPoints[id].xy, cameraForward.xz);
 
    // Read out the particle data
    float radius = ...;
    float height = ...;
 
    // Set the position of the vert
    float4 finalPos = centerPoint + float4(quadPoints[id].x, y * height, quadPoints[id].y, 0) * radius;
    o.pos = mul(UNITY_MATRIX_VP, float4(finalPos.xyz, 1));
    o.uv = quadPoints[id].xy + 0.5;
 
    return o;
}

パーティクルを前後に並べ替え、透明ピクセルをクリップ (ブレンドしない) するためにソリッド スタイルのシェーダーを使用したので、この手法では驚くべき量のパーティクルが処理され、低電力のマシンでもコストの高いオーバー描画が回避されます。

透明なパーティクル クラウド

固いパーティクルは、クラウドの形状に優れた有機的な感じを提供しましたが、それでもクラウドのフラッフィを販売するために何かが必要でした。 透明性を導入できる、ハイ エンドのグラフィックス カード用のカスタム ソリューションを試用することを決定しました。

これを行うには、パーティクルの最初の並べ替え順序を切り替え、テクスチャアルファを使用するためにシェーダーを変更しました。

白い雲

見た目は良かったが、画面にピクセルを何百回もレンダリングする結果になるので、最も困難なマシンでも負荷が高すぎることを証明しました。

低解像度で画面外にレンダリングする

クラウドによってレンダリングされるピクセル数を減らすために、4 分の 1 の解像度バッファー (画面と比較) でレンダリングし、すべてのパーティクルが描画された後に結果を画面に戻して拡大し始めました。 これにより約 4 倍の高速化が実現しましたが、いくつかの注意点があります。

画面外にレンダリングするコード:

cloudBlendingCommand = new CommandBuffer();
Camera.main.AddCommandBuffer(whenToComposite, cloudBlendingCommand);
 
cloudCamera.CopyFrom(Camera.main);
cloudCamera.rect = new Rect(0, 0, 1, 1);    //Adaptive rendering can set the main camera to a smaller rect
cloudCamera.clearFlags = CameraClearFlags.Color;
cloudCamera.backgroundColor = new Color(0, 0, 0, 1);
 
currentCloudTexture = RenderTexture.GetTemporary(Camera.main.pixelWidth / 2, Camera.main.pixelHeight / 2, 0);
cloudCamera.targetTexture = currentCloudTexture;
 
// Render clouds to the offscreen buffer
cloudCamera.Render();
cloudCamera.targetTexture = null;
 
// Blend low-res clouds to the main target
cloudBlendingCommand.Blit(currentCloudTexture, new RenderTargetIdentifier(BuiltinRenderTextureType.CurrentActive), blitMaterial);

まず、画面外のバッファーにレンダリングすると、メイン シーンからすべての深度情報が失われ、山の上にレンダリングされる山の背後にパーティクルが生成されます。

次に、バッファーを拡大すると、解像度の変化が顕著だったクラウドの端にアーティファクトも導入されました。 次の 2 つのセクションでは、これらの問題を解決する方法について説明します。

パーティクル深度バッファー

山やオブジェクトが背後にあるパーティクルを覆う可能性があるワールド ジオメトリとパーティクルを共に存在させるには、メイン シーンのジオメトリを含む深度バッファーをオフスクリーン バッファーに設定しました。 このような深度バッファーを生成するために、2 つ目のカメラを作成し、シーンのソリッド ジオメトリと深度のみをレンダリングしました。

次に、クラウドのピクセル シェーダーで新しいテクスチャを使用して、ピクセルをオクルージョンしました。 同じテクスチャを使用して、クラウド ピクセルの背後にあるジオメトリまでの距離を計算しました。 その距離を使用してピクセルのアルファに適用することで、雲が地形に近付いてフェードアウトし、パーティクルと地形が満たされるハードカットを取り除くという効果が得られます。

地形にブレンドされたクラウド

エッジのむき出し

引き伸ばされたクラウドは、パーティクルの中心にある通常のサイズのクラウドとほとんど同じか、重なっている場所に見えましたが、クラウドの端にいくつかのアーティファクトが表示されました。 そうしないと、シャープなエッジがぼやけて表示され、カメラの移動時にエイリアス効果が導入されました。

これを解決するには、オフスクリーン バッファーで単純なシェーダーを実行して、コントラストの大きな変化が発生した場所を特定します (1)。 大きな変更があるピクセルを新しいステンシル バッファー (2) に置きます。 次に、ステンシル バッファーを使用して、画面外のバッファーを画面に戻す際にこれらのハイ コントラスト領域をマスクし、その結果、クラウドとその周辺に穴が開きます (3)。

その後、すべてのパーティクルを再び全画面モードでレンダリングしましたが、今回はステンシル バッファーを使用してエッジを含むすべてをマスクし、最小限のピクセル セットがタッチされました (4)。 コマンド バッファーは既にパーティクル用に作成されたので、新しいカメラにもう一度レンダリングする必要があります。

クラウド エッジのレンダリングの進行

結果は、クラウドの中央のセクションが安価なシャープエッジでした。

これは、すべてのパーティクルを全画面にレンダリングするよりもはるかに高速ですが、ステンシル バッファーに対するピクセルのテストにはコストが引き続き発生します。そのため、大量のオーバー描画にはコストが伴います。

カリングパーティクル

風の効果では、計算シェーダーで長い三角形のストリップを生成し、世界に多数の風を作り出しました。 吹き出しストリップが生成されたので、塗りつぶし速度に対する風の影響は大きくなってはいましたが、頂点シェーダーの負荷が高く、何十万もの頂点が生成されました。

描画する風のストリップのサブセットをフィードするために、計算シェーダーに追加バッファーを導入しました。 計算シェーダーでいくつかの単純なビュー frustum カリング ロジックを使用すると、ストリップがカメラ ビューの外部にあるかどうかを判断し、プッシュ バッファーに追加されるのを防ぐ可能性があります。 これにより、ストリップの量が大幅に削減され、GPU で必要なサイクルが解放されます。

追加バッファーを示すコード:

計算シェーダー:

AppendStructuredBuffer<int> culledParticleIdx;
 
if (show)
    culledParticleIdx.Append(id.x);

C# コード:

protected void Awake() 
{
    // Create an append buffer, setting the maximum size and the contents stride length
    culledParticlesIdxBuffer = new ComputeBuffer(ParticleCount, sizeof(int), ComputeBufferType.Append);
 
    // Set up Args Buffer for Draw Procedural Indirect
    argsBuffer = new ComputeBuffer(4, sizeof(int), ComputeBufferType.IndirectArguments);
    argsBuffer.SetData(new int[] { DataVertCount, 0, 0, 0 });
}
 
protected void Update()
{
    // Reset the append buffer, and dispatch the compute shader normally
    culledParticlesIdxBuffer.SetCounterValue(0);
 
    computer.Dispatch(...)
 
    // Copy the append buffer count into the args buffer used by the Draw Procedural Indirect call
    ComputeBuffer.CopyCount(culledParticlesIdxBuffer, argsBuffer, dstOffset: 1);
    ribbonRenderCommand.DrawProceduralIndirect(Matrix4x4.identity, renderMaterial, 0, MeshTopology.Triangles, dataBuffer);
}

クラウドのパーティクルに対して同じ手法を使用して、計算シェーダーでそれらをカドル化し、表示されているパーティクルのみをレンダリングする方法を試しました。 最大のボトルネックは、頂点を計算するコストではなく、画面にレンダリングされるピクセルの量だったので、この手法では実際には GPU ではあまり節約されません。

この手法のもう 1 つの問題は、アペンド バッファーがランダムな順序で設定されたのは、パーティクルを計算する並列化された性質により、並べ替えされたパーティクルが並べ替え解除され、結果としてクラウドのパーティクルがちらつきを引き起こすという問題でした。

プッシュ バッファーを並べ替える手法がありますが、カリングパーティクルから得られるパフォーマンスの向上量が限られている場合は、追加の並べ替えと相殺される可能性が高いので、この最適化を進め "ない" と決めました。

アダプティブ レンダリング

クラウドビューやクリア ビューなど、さまざまなレンダリング条件を持つアプリで安定したフレームレートを確保するために、アプリにアダプティブ レンダリングを導入しました。

アダプティブ レンダリングの最初の手順は、GPU を測定することです。 これを行ったのは、レンダリングされたフレームの先頭と末尾に GPU コマンド バッファーにカスタム コードを挿入し、左右の目の画面時間の両方をキャプチャすることで行いました。

レンダリングに費やされた時間を測定し、それを必要な更新率と比較することで、フレームの削除にどれだけ近かったかの感覚を得たのです。

フレームの削除に近い場合は、レンダリングを調整して高速にします。 適応する簡単な方法の 1 つは、画面のビューポート サイズを変更し、レンダリングに必要なピクセル数を減らします。

UnityEngine.XR.XRSettings.renderViewportScale を使用すると、対象のビューポートが縮小され、結果が画面に合わせて自動的に拡大されます。 スケールの小さな変化は、ワールド ジオメトリではわずかに顕著であり、0.7 のスケール ファクターでは、レンダリングするピクセルの半分の量が必要です。

70% スケール、ピクセルの半分

フレームがドロップされそうになってきたと検出された場合は、スケールを固定の数値で下げ、もう一度十分に高速に実行している場合は、スケールを大きくします。

起動時にハードウェアのグラフィックス機能に基づいて使用するクラウド手法を決定しましたが、GPU 測定のデータに基づいて、システムが長時間低解像度を続けなかったのを防ぐことはできますが、これは Datascape で探索する時間がかからなかった点です。

最後に

さまざまなハードウェアをターゲットに設定するには困難であり、計画が必要です。

問題領域を理解し、すべてのマシンで実行されるバックアップ ソリューションを開発するには、低電力のマシンをターゲットにすることをお勧めします。 ピクセルが最も貴重なリソースになるので、フィル レートを念頭に置いてソリューションを設計します。 透明度を超えるソリッド ジオメトリをターゲットにします。

バックアップ ソリューションを使用すると、より複雑なハイ エンド マシンのレイヤー化を開始したり、バックアップ ソリューションの解像度を強化したりすることができます。

最悪の場合のシナリオ用に設計し、場合によっては、負荷の高い状況にアダプティブ レンダリングを使用して検討してください。

著者について

Picture of Robert Ferrese Robert Robertrese
ソフトウェア エンジニア @Microsoft
Picture of Dan Andersson Dan Andersson
ソフトウェア エンジニア @Microsoft

こちらもご覧ください