案例研究 - 跨具有不同功能的裝置調整應用程式
此案例研究描述Windows Mixed Reality應用程式如何以不同硬體功能的各種平臺為目標。 Datascape 是一種Windows Mixed Reality應用程式,可顯示地形資料上的天氣資料。 應用程式會將全像攝影資料視覺效果括住使用者。 使用者可以藉由探索混合實境中的資料來探索他們所取得的獨特見解。
Datascape 應用程式以Microsoft HoloLens、Windows Mixed Reality沉浸式頭戴式裝置、低電源的電腦和高效能電腦為目標。 主要挑戰是在具有非常不同 效能功能的裝置上以高畫面播放速率執行時,呈現視覺吸引人的場景。
此案例研究會逐步解說我們用來建立一些更密集效能系統的程式和技術,特別是轉譯天氣,例如雲端。 我們會描述我們遇到的問題,以及如何克服這些問題。
如需混合實境和 Unity 應用程式效能考慮的詳細資訊,請參閱:
案例研究概觀
以下是 Datascape 應用程式和挑戰的一些背景。
透明度和過度繪製
我們的主要轉譯困難是處理透明度,因為透明度可能很昂貴。
您可以在寫入深度緩衝區時,將 實心幾何 呈現回頭,這會阻止位於該圖元後方的任何未來圖元無法轉譯。 此作業可防止隱藏的圖元執行圖元 著色器,並大幅加速轉譯。 如果您以最佳方式排序幾何,螢幕上的每個圖元只會繪製一次。
透明幾何 必須排序回前方,並依賴將圖元著色器的輸出混合到螢幕上目前的圖元。 此程式可能會導致每個畫面上繪製多次圖元,稱為 過度繪製。
對於HoloLens和主要電腦,您只能填滿螢幕數次,使透明轉譯有問題。
Datascape 場景元件
Datascape 場景有三個主要元件: UI、 地圖和 天氣。 我們知道天氣效果需要它們所能取得的所有效能,因此我們設計 UI 和地圖來減少過度繪製。
我們已重新處理 UI 數次,以將過度繪製的數量降到最低。 對於光暈按鈕和地圖概觀等元件,我們選擇使用更複雜的幾何,而不是重迭透明藝術。
針對地圖,我們使用自訂著色器來 去除標準 Unity 功能,例如陰影和複雜的光源。 自訂著色器會以簡單、單一的太陽光源模型和自訂的模糊計算來取代這些功能。 這個簡單的圖元著色器已改善效能。
我們同時取得 UI 和地圖以在預算中呈現,因此不需要任何硬體相依變更。 天氣視覺效果,特別是雲端轉譯,更具挑戰性。
雲端資料
從三個不同的 2D 層中 從 NOAA 伺服器 下載的雲端資料。 每個圖層都有雲端的頂端和底部高度,以及雲端的密度,適用于格線的每個資料格。 我們已將資料處理成雲端資訊紋理,以將每個元件儲存在紋理的紅色、綠色和藍色元件中。
建立幾何雲端
為了確保較低電源的機器可以轉譯雲端,我們的備份方法使用實心幾何將過度繪製降到最低。
我們藉由為每個圖層產生實心高度地圖網格來產生雲端。 我們使用每個頂點的雲端資訊紋理半徑來產生圖形。 我們使用幾何著色器來產生雲端頂端和底部的頂點,產生穩固的雲端圖形。 我們使用紋理中的密度值,以較深的色彩來為雲端加上更密集的雲端色彩。
下列著色器程式碼會建立頂點:
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);
}
我們引進了小型雜訊模式,以取得實際資料的詳細資料。 為了產生四捨五入的雲端邊緣,我們會在插入半徑值達到臨界值時裁剪圖元著色器中的圖元,以捨棄接近零的值。
由於雲端是實心幾何,因此可以在地形轉譯之前呈現。 隱藏雲端底下的昂貴地圖圖元可進一步改善畫面播放速率。 由於實心幾何轉譯方法,此解決方案在所有圖形卡上都正常運作,從最小規格到高階圖形卡,以及HoloLens。
使用實心物件雲端
我們的解決方案會產生一個良好的雲端資料標記法,但有點不足。 雲端轉譯並未傳達我們想要用於高階機器的音量風格。 下一個步驟是藉由代表大約 100,000 個粒子的雲端來產生更有機和音量的外觀。
如果物件保持實心並往後排序,您仍可從深度緩衝區擷取先前轉譯的物件後方獲益,以減少過度繪製。 此外,以物件為基礎的解決方案可以改變以不同硬體為目標的物件數目。 不過,所有圖元仍然需要經過深度測試,這會造成更多額外負荷。
首先,我們已在啟動時,在體驗的中心點周圍建立物件位置。 我們會將物件更密集地分散在中心附近,因此在距離中較少。 我們已預先排序從中央到背面的所有物件,因此會先轉譯最接近的物件。
計算著色器會取樣雲端資訊紋理,以將每個物件置於正確的高度,並根據密度進行色彩。 每個物件都包含高度和半徑。 高度是以從雲端資訊紋理取樣的雲端資料為基礎。 半徑是以初始分佈為基礎,它會計算並儲存其最接近鄰近的水準距離。
我們使用 DrawProcedural 轉譯每個物件一個四邊形。 四邊形會使用此資料來自行設定方向,以高度的角度。 當使用者水準查看物件時,會顯示高度。 當使用者從上而下查看物件時,會涵蓋其與其鄰近之間的區域。
下列著色器程式碼會顯示分佈:
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;
}
我們已將物件排序為前向後,但仍使用純色樣式著色器來裁剪透明圖元,而不會混合它們。 這項技術即使在較低電源的機器上處理大量的物件,也避免過度繪製成本。
嘗試透明物件雲端
固態物件為雲端圖形提供了一種有機風格,但仍需要一些專案來擷取雲端的模糊。 我們決定針對引進透明度的高階圖形卡嘗試自訂解決方案。 我們只是切換了物件的初始排序次序,並將著色器變更為使用紋理 Alpha。
此解決方案看起來很棒,但甚至最困難的電腦也證明太重。 每個圖元都必須在畫面上轉譯數百次。
以較低解析度轉譯螢幕
為了減少轉譯雲端的圖元數目,我們會在螢幕解析度一季的緩衝區中轉譯它們。 我們在繪製所有物件之後,將結束結果延展回螢幕。
下列程式碼顯示幕幕外轉譯:
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);
此解決方案已加速處理四個複本,但有一些注意事項。 首先,在轉譯為離螢幕緩衝區時,我們會從主要場景遺失所有深度資訊。 在山地頂端轉譯的岩道後方物件。
其次,延展緩衝區在雲端邊緣引進的成品,其中解析變更明顯。 接下來的兩節說明如何解決這些問題。
使用物件深度緩衝區
我們需要讓物件與世界幾何並存,其中一個或物件涵蓋在它的後面。 因此,我們已將包含主要場景幾何的 深度緩衝區 填入離螢幕緩衝區。 為了產生深度緩衝區,我們建立了第二部相機,只轉譯場景的實心幾何和深度。
我們使用雲端圖元著色器中的新紋理來遮蔽圖元。 我們使用相同的紋理來計算雲端圖元後方的幾何距離。 藉由使用該距離並將它套用至圖元的 Alpha,我們達成了在接近地形時,雲端淡出的效果。 此效果會移除物件和地形符合的任何硬式切割。
讓邊緣更清晰
延展的雲端看起來幾乎與一般大小的雲端位於物件中心,或重迭的位置,但在雲端邊緣顯示某些成品。 尖角看起來模糊,而相機動作引進了別名效果。
若要解決此問題,我們會:
- 在離螢幕緩衝區上執行簡單的著色器,以判斷對比發生大型變更的位置。
- 將具有大型變更的圖元放入新的樣板緩衝區。
- 使用樣板緩衝區在將外螢幕緩衝區套用回螢幕時遮罩掉這些高對比區域,導致雲端內和周圍有漏洞。
- 在全螢幕模式中再次轉譯所有物件,使用樣板緩衝區來遮罩除了邊緣外的所有專案,也會產生一組最少的觸控圖元。 由於我們已建立命令緩衝區來轉譯物件,因此我們只會再次將它轉譯為新的相機。
最終結果是尖角邊緣,且雲端的中心區段很便宜。 雖然此解決方案比在全螢幕中轉譯所有物件更快,但仍需要針對樣板緩衝區測試圖元的成本。 大量過度繪製仍然昂貴。
Cull 物件
針對風力效果,我們在計算著色器中產生長三角形帶,在世界中建立許多風力。 由於窄條形,風力效果對填滿速率不重。 不過,數百萬個頂點會導致頂點著色器負載過重。
為了減少負載,我們在計算著色器上引進了附加緩衝區,以饋送要繪製的風帶子集。 我們使用計算著色器中的簡單 檢視擷取 邏輯來判斷帶是否不在相機檢視之外,並防止這些等量新增至推送緩衝區。 此程式大幅減少等量,以改善效能。
下列程式碼示範附加緩衝區。
計算著色器:
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);
}
我們在雲端物件上嘗試這項技術,並在計算著色器上加以消除,並只推送要轉譯的可見物件。 但我們無法節省大量處理,因為最大的瓶頸是在螢幕上呈現的雲端圖元數目,而不是計算頂點的成本。
另一個問題是附加緩衝區會以隨機順序填入,因為物件平行運算。 排序的物件變成未排序,導致雲端物件閃爍。 有一些技術可以排序推播緩衝區,但從擷取物件取得的效能有限量可能會因另一個排序而位移。 我們決定不要針對雲端物件進行此優化。
使用自適性轉譯
為了確保應用程式中具有不同轉譯條件的穩定畫面播放速率,例如雲端與清楚檢視,我們引進了調適型轉譯。
調適型轉譯的第一個步驟是測量效能。 我們已在轉譯畫面的開頭和結尾插入自訂程式碼,以擷取左右眼睛螢幕時間。
比較轉譯時間與所需的重新整理速率,以顯示卸載畫面格的距離。 當您接近卸載畫面時,可以更快速地調整轉譯。
調整轉譯的其中一個簡單方式是變更螢幕檢視區大小,因此需要較少的圖元才能轉譯。 系統會使用 UnityEngine.XR.XRSettings.renderViewportScale 來壓縮目標檢視區,並自動向上延展結果以符合螢幕。 小數位數的變更在世界幾何上幾乎不明顯,縮放比例為 0.7 需要轉譯圖元數的一半。
當我們偵測到即將卸載畫面時,我們會以固定比例降低縮放比例,並在我們再次執行速度夠快時加以還原。
在此案例研究中,我們決定要根據啟動時硬體的圖形功能使用哪一種雲端技術。 您也可以根據效能測量的資料做出此決策,以協助防止系統長時間保持低解析度。
建議
以不同的硬體功能為目標是一項挑戰,而且需要規劃。 以下是一些建議:
- 開始以較低電源的電腦為目標,以熟悉問題空間。
- 開發在所有電腦上執行的備份解決方案。 然後,您可以針對高階機器分層,或增強備份解決方案的解析度。
- 將解決方案設計為填滿率,因為圖元是您最寶貴的資源。
- 以純色幾何為目標,而非透明度。
- 針對最差案例進行設計,並考慮針對大量情況使用調適型轉譯。
關於作者
![]() |
RobertFerese 軟體工程師@Microsoft |
![]() |
Dan Andersson 軟體工程師@Microsoft |