Case study - Ridimensionare le app tra dispositivi con funzionalità diverse

Questo case study descrive come un'applicazione Windows Mixed Reality può essere destinazione di varie piattaforme con diverse funzionalità hardware. Datascape è un'applicazione Windows Mixed Reality che visualizza i dati meteo in cima ai dati del terreno. L'applicazione circonda gli utenti con visualizzazioni di dati olografici. Gli utenti possono esplorare informazioni univoce che ottengono individuando i dati in realtà mista.

L'app Datascape è destinata Microsoft HoloLens, Windows Mixed Reality visori vr vr immersivi, PC con potenza inferiore e PC ad alte prestazioni. La sfida principale è stata il rendering di una scena visivamente accattivante, durante l'esecuzione a una frequenza elevata di fotogrammi, su dispositivi con capacità di prestazioni molto diverse.

Questo case study illustra il processo e le tecniche usate per creare alcuni dei sistemi più a elevato utilizzo delle prestazioni, in particolare il rendering delle condizioni meteorologiche come le nuvole. Vengono descritti i problemi riscontrati e il modo in cui li abbiamo superati.

Per altre informazioni sulle considerazioni sulle prestazioni per la realtà mista e per le app Unity, vedere:

Panoramica del case study

Ecco alcune informazioni sull'applicazione e sulle sfide di Datascape.

Trasparenza e overdraw

I nostri principali problemi di rendering hanno affrontato la trasparenza, poiché la trasparenza può essere costosa.

È possibile eseguire il rendering di un front-end di geometria unita a indietro durante la scrittura nel buffer di profondità, che arresta qualsiasi pixel futuro che si trova dietro tale pixel dal rendering. Questa operazione impedisce ai pixel nascosti di eseguire lo shader pixel e accelera notevolmente il rendering. Se si ordina geometria in modo ottimale, ogni pixel sullo schermo disegna una sola volta.

La geometria trasparente deve essere ordinata in primo piano e si basa sulla fusione dell'output del pixel shader al pixel corrente sullo schermo. Questo processo può comportare il disegno di ogni pixel sullo schermo più volte per fotogramma, chiamato overdraw.

Per HoloLens e PC mainstream, è possibile riempire la schermata solo alcune volte, rendendo il rendering trasparente problematico.

Componenti della scena di datascape

La scena Datascape include tre componenti principali: l'interfaccia utente, la mappa e il meteo. Sapevamo che gli effetti meteo avrebbero bisogno di tutte le prestazioni che potrebbero ottenere, quindi abbiamo progettato l'interfaccia utente e mappato per ridurre l'overdraw.

L'interfaccia utente è stata rielaborata più volte per ridurre al minimo la quantità di overdraw. Per i componenti come pulsanti luminosi e panoramica della mappa, è stato scelto di usare geometria più complessa anziché sovrapporre l'arte trasparente.

Per la mappa è stato usato un shader personalizzato che ha rimosso le funzionalità di Unity standard, ad esempio ombreggiature e illuminazione complessa. Lo shader personalizzato ha sostituito queste funzionalità con un semplice modello di illuminazione solare singolo e un calcolo personalizzato della nebbia. Questo semplice pixel shader migliora le prestazioni.

Abbiamo sia l'interfaccia utente che la mappa da eseguire per eseguire il rendering in base al budget, quindi non hanno bisogno di modifiche dipendenti dall'hardware. La visualizzazione meteo, in particolare il rendering del cloud, è stata più complessa.

Dati cloud

I dati cloud scaricati dai server NOAA in tre livelli 2D distinti. Ogni livello aveva l'altezza superiore e inferiore del cloud e la densità del cloud, per ogni cella della griglia. I dati sono stati elaborati in una trama di info cloud che ha archiviato ogni componente nel componente rosso, verde e blu della trama.

Creare cloud geometry

Per assicurarsi che i computer con potenza inferiore possano eseguire il rendering dei cloud, l'approccio di backup usa la geometria solida per ridurre al minimo l'overdraw.

Sono stati generati cloud generando una mesh di altezza solida per ogni livello. È stato usato il raggio della trama delle informazioni cloud per vertice per generare la forma. È stato usato un shader geometry per produrre i vertici in alto e in basso delle nuvole, generando forme cloud solide. È stato usato il valore di densità dalla trama per colorare il cloud con colori più scuri per le nuvole più dense.

Il codice shader seguente crea i vertici:

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

È stato introdotto un piccolo modello di rumore per ottenere maggiori dettagli sui dati reali. Per produrre bordi cloud arrotondati, sono stati eliminati valori quasi zero ritagliando i pixel nel pixel shader quando il valore del raggio interpolato ha raggiunto una soglia.

Poiché le nuvole sono geometrie solide, possono eseguire il rendering prima del rendering del terreno. Nascondere i pixel di mappa costosi sotto le nuvole migliora ulteriormente la frequenza dei fotogrammi. A causa dell'approccio di rendering della geometria solida, questa soluzione è stata eseguita bene su tutte le schede grafiche, da schede grafiche minime a schede grafiche di alto livello e su HoloLens.

Image that shows geometry clouds.

Usare cloud di particelle solide

La soluzione ha prodotto una rappresentazione decente dei dati del cloud, ma è stata un po'di meno. Il rendering del cloud non ha trasmesso la sensazione volumetrica che volevamo per le nostre macchine di fascia alta. Il nostro passaggio successivo era quello di produrre un aspetto più organico e volumetrico rappresentando le nuvole con circa 100.000 particelle.

Se le particelle rimangono solide e ordinano front-to-back, si può comunque trarre vantaggio dal buffer di profondità dietro particelle di cui è stato eseguito il rendering precedente, riducendo l'overdraw. Inoltre, una soluzione basata su particelle può modificare il numero di particelle in modo da puntare a hardware diverso. Tuttavia, tutti i pixel devono comunque essere testati in profondità, che causano un sovraccarico maggiore.

Prima di tutto, abbiamo creato posizioni di particella intorno al punto centrale dell'esperienza all'avvio. Abbiamo distribuito le particelle più densamente intorno al centro e meno così in distanza. Abbiamo preordinato tutte le particelle dal centro al retro, quindi le particelle più vicine sono prima di tutto sottoposte a rendering.

Un shader di calcolo ha campione la trama delle informazioni cloud per posizionare ogni particella a un'altezza corretta e colorarla in base alla densità. Ogni particella contiene sia un'altezza che un raggio. L'altezza è basata sui dati cloud campionati dalla trama delle informazioni cloud. Il raggio è basato sulla distribuzione iniziale, che ha calcolato e archiviato la distanza orizzontale al suo vicino più vicino.

Abbiamo usato DrawProcedural per eseguire il rendering di un quad per particella. I quad utilizzano questi dati per orientarsi, angolati dall'altezza. Quando gli utenti esaminano una particella orizzontalmente, mostra l'altezza. Quando gli utenti esaminano la particella in alto, l'area tra di esso e i suoi vicini è coperta.

Diagram that shows particle shape and coverage.

Il codice shader seguente mostra la distribuzione:

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

Abbiamo ordinato le particelle front-to-back e abbiamo ancora usato uno shader di stile solido per ritagliare pixel trasparenti, non fonderli. Questa tecnica gestisce un numero elevato di particelle anche su macchine con alimentazione inferiore, evitando il sovraccarico costoso.

Provare cloud di particelle trasparenti

Le particelle solide hanno fornito un aspetto organico alle forme del cloud, ma hanno ancora bisogno di qualcosa per catturare la fluffibilità delle nuvole. Abbiamo deciso di provare una soluzione personalizzata per schede grafiche di alta qualità che introduce la trasparenza. È sufficiente cambiare l'ordine iniziale di ordinamento delle particelle e modificare lo shader per usare le trame alfa.

Image that shows fluffy clouds.

Questa soluzione sembrava grande, ma si è dimostrato troppo pesante per anche le macchine più difficili. Ogni pixel deve essere eseguito il rendering sullo schermo centinaia di volte.

Eseguire il rendering dello schermo con una risoluzione inferiore

Per ridurre il numero di pixel per il rendering dei cloud, è stato eseguito il rendering in un buffer che era un quarto di risoluzione dello schermo. Abbiamo esteso il risultato finale sullo schermo dopo aver disegnato tutte le particelle.

Il codice seguente mostra il rendering offscreen:

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

Questa soluzione ha accelerato l'elaborazione di quattro volte, ma aveva un paio di avvertenze. Prima di tutto, quando si esegue il rendering in un buffer offscreen, si perdono tutte le informazioni di profondità dalla scena principale. Particelle dietro le montagne rese sulla cima della montagna.

In secondo luogo, l'estensione del buffer ha introdotto artefatti sui bordi delle nuvole, in cui la modifica della risoluzione è stata evidente. Le due sezioni successive descrivono come sono stati risolti questi problemi.

Usare un buffer di profondità delle particelle

Abbiamo bisogno di rendere le particelle coesistenza con la geometria mondiale, dove una montagna o un oggetto coperto particelle dietro di esso. Quindi abbiamo popolato il buffer offscreen con un buffer di profondità che contiene la geometria della scena principale. Per produrre il buffer di profondità, abbiamo creato una seconda fotocamera che ha eseguito il rendering solo della geometria solida e della profondità della scena.

È stata usata la nuova trama nel pixel shader cloud per occludere i pixel. È stata usata la stessa trama per calcolare la distanza della geometria dietro un pixel cloud. Usando tale distanza e applicandola all'alfa del pixel, abbiamo ottenuto l'effetto delle nuvole che si sfondano man mano che si avvicinano al terreno. Questo effetto rimuove tutti i tagli rigidi in cui si incontrano particelle e terreno.

Image that shows clouds blended into terrain.

Affilare i bordi

Le nuvole estese hanno guardato quasi identiche alle nuvole di dimensioni normali nei centri delle particelle o in cui si sovrappongono, ma hanno mostrato alcuni artefatti ai bordi del cloud. I bordi nitidi sono apparsi sfocati e il movimento della fotocamera ha introdotto effetti alias.

Per risolvere questo problema, è possibile:

  1. È stato eseguito un semplice shader nel buffer offscreen per determinare dove si sono verificate modifiche di grandi dimensioni.
  2. Inserire i pixel con modifiche di grandi dimensioni in un nuovo buffer degli stencil.
  3. È stato usato il buffer degli stencil per mascherare queste aree a contrasto elevato quando si applica di nuovo il buffer offscreen allo schermo, causando fori in e intorno alle nuvole.
  4. È stato eseguito di nuovo il rendering di tutte le particelle in modalità schermo intero, usando il buffer degli stencil per mascherare tutto il resto, ma i bordi, ottenendo un set minimo di pixel toccati. Poiché abbiamo già creato il buffer di comando per eseguire il rendering delle particelle, è sufficiente eseguirne il rendering alla nuova fotocamera.

Image showing the progression of rendering cloud edges.

Il risultato finale è stato bordi taglienti con sezioni centro economiche delle nuvole. Anche se questa soluzione è molto più veloce del rendering di tutte le particelle a schermo intero, c'è comunque un costo per testare i pixel rispetto al buffer degli stencil. Una grande quantità di overdraw è ancora costosa.

Particelle Cull

Per l'effetto vento, abbiamo generato strisce di triangoli lunghi in un compute shader, creando molti wisps di vento nel mondo. L'effetto vento non era pesante sulla frequenza di riempimento, a causa delle strisce strette. Tuttavia, molte centinaia di migliaia di vertici hanno causato un carico elevato per il vertex shader.

Per ridurre il carico, sono stati introdotti buffer di accodamento nello shader di calcolo, per alimentare un subset delle strisce eoliche da disegnare. Nel compute shader è stata usata una semplice logica di controllo del frustum di visualizzazione per determinare se una striscia è esterna alla visualizzazione della fotocamera e ha impedito l'aggiunta di tali strip al buffer di push. Questo processo ha ridotto significativamente il numero di strip, migliorando le prestazioni.

Il codice seguente illustra un buffer di accodamento.

Compute shader:

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

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

Abbiamo provato questa tecnica sulle particelle cloud, culling them on the compute shader, and pushing the visible particelle to be renderd. Ma non è stata risparmiata molta elaborazione, perché il collo di bottiglia più grande era il numero di pixel cloud per il rendering sullo schermo, non il costo del calcolo dei vertici.

Un altro problema era che il buffer di accodamento popolato in ordine casuale, a causa del calcolo parallelizzato delle particelle. Le particelle ordinate sono diventate non ordinate, causando particelle di nuvola sfarfallio. Ci sono tecniche per ordinare il buffer push, ma la quantità limitata di miglioramento delle prestazioni dalle particelle di culling potrebbe essere compensata da un altro ordinamento. Abbiamo deciso di non perseguire questa ottimizzazione per le particelle cloud.

Usare il rendering adattivo

Per garantire una frequenza dei fotogrammi costante nell'app con condizioni di rendering variabili, ad esempio una visualizzazione cloudsa e chiara, è stato introdotto il rendering adattivo.

Il primo passaggio del rendering adattivo consiste nel misurare le prestazioni. È stato inserito codice personalizzato nel buffer dei comandi all'inizio e alla fine di un frame di cui è stato eseguito il rendering, per acquisire sia l'ora dello schermo dell'occhio sinistro che quello destro.

Confrontare il tempo di rendering con la frequenza di aggiornamento desiderata per visualizzare la chiusura dei fotogrammi. Quando ci si avvicina all'eliminazione dei fotogrammi, è possibile adattare il rendering in modo che sia più veloce.

Un modo semplice per adattare il rendering consiste nel modificare le dimensioni del riquadro di visualizzazione dello schermo in modo che richieda meno pixel per il rendering. Il sistema usa UnityEngine.XR.XRSettings.renderViewportScale per compattare il riquadro di visualizzazione di destinazione e estende automaticamente il backup dei risultati in modo che si adatti allo schermo. Una piccola modifica della scala è appena evidente sulla geometria globale e un fattore di scala pari a 0,7 richiede il rendering del numero di pixel.

Image showing 70% scale, with half the pixels.

Quando rileviamo che stiamo per eliminare i fotogrammi, la scala viene ridotta in base a un rapporto fisso e la si ripristina quando si esegue di nuovo abbastanza velocemente.

In questo case study abbiamo deciso quale tecnica cloud usare in base alle funzionalità grafiche dell'hardware all'avvio. È anche possibile basare questa decisione sui dati delle misurazioni delle prestazioni, per impedire al sistema di rimanere a bassa risoluzione per molto tempo.

Consigli

La destinazione di diverse funzionalità hardware è complessa e richiede la pianificazione. Di seguito sono elencati alcuni suggerimenti:

  • Iniziare a usare i computer con tecnologia inferiore per acquisire familiarità con lo spazio del problema.
  • Sviluppare una soluzione di backup eseguita in tutti i computer. È quindi possibile eseguire il layer in modo più complesso per i computer di fascia alta o migliorare la risoluzione della soluzione di backup.
  • Progettare la soluzione tenendo conto della frequenza di riempimento, poiché i pixel sono le risorse più preziose.
  • Specificare la geometria a tinta unita rispetto alla trasparenza.
  • Progettare per scenari di casi peggiori e prendere in considerazione l'uso del rendering adattivo per situazioni complesse.

Informazioni sugli autori

Picture of Robert Ferrese Robert Ferrese
Software Engineer @Microsoft
Picture of Dan Andersson Dan Andersson
Software Engineer @Microsoft

Vedi anche