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

Questo case study descrive come un'applicazione Windows Mixed Reality può essere usata per diverse piattaforme con diverse funzionalità hardware. Datascape è un'applicazione Windows Mixed Reality che visualizza i dati meteo sui dati del terreno. L'applicazione circonda gli utenti con visualizzazioni di dati olografici. Gli utenti possono esplorare informazioni dettagliate uniche acquisite individuando i dati nella realtà mista.

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

Questo case study illustra il processo e le tecniche usate per creare alcuni dei sistemi a elevato utilizzo di prestazioni, in particolare il rendering di meteo come le nuvole. Descriveremo i problemi riscontrati e come 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 generali sull'applicazione Datascape e sulle sfide.

Trasparenza e sovrapposizione

Le nostre principali difficoltà di rendering sono state affrontate con la trasparenza, perché la trasparenza può essere costosa.

È possibile eseguire il rendering della geometria continua front-to-back durante la scrittura nel buffer di profondità, che arresta eventuali pixel futuri che si trovano dietro tale pixel dal rendering. Questa operazione impedisce l'esecuzione di pixel nascosti e velocizza notevolmente il rendering. Se si ordina in modo ottimale la geometria, ogni pixel sullo schermo viene disegnato 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 ogni fotogramma, chiamato overdraw.

Per HoloLens e i PC mainstream, è possibile riempire lo schermo solo alcune volte, rendendo il rendering trasparente problematico.

Componenti della scena 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 componenti come pulsanti incandescenti e panoramiche della mappa, abbiamo scelto di usare una geometria più complessa anziché sovrapporre l'arte trasparente.

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

È stata ottenuta sia l'interfaccia utente che la mappa per il rendering in base al budget, quindi non sono necessarie modifiche dipendenti dall'hardware. La visualizzazione meteo, in particolare il rendering del cloud, era più complessa.

Dati cloud

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

Creare cloud geometry

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

Sono stati prodotti cloud generando una mesh a altezza continua per ogni livello. È stato usato il raggio della trama delle informazioni cloud per ogni vertice per generare la forma. È stato usato uno shader geometry per produrre i vertici nelle estremità superiore e inferiore delle nuvole, generando forme a nuvola continua. È stato usato il valore di densità dalla trama per colorare la nuvola con colori più scuri per 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 disturbo per ottenere maggiori dettagli sui dati reali. Per produrre bordi di nuvola arrotondati, i valori quasi zero sono stati eliminati ritagliando i pixel nel pixel shader quando il valore del raggio interpolato raggiunge una soglia.

Poiché le nuvole sono geometrie solide, è possibile eseguirne 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 continua, questa soluzione è stata eseguita correttamente in tutte le schede grafiche, dalle schede grafiche minime alle schede grafiche di fascia alta e in HoloLens.

Immagine che mostra le nuvole geometriche.

Usare nuvole di particelle solide

La soluzione ha prodotto una rappresentazione decente dei dati cloud, ma è stata un po' caresta. Il rendering del cloud non ha trasmesso la sensazionemetrica desiderata per le macchine di fascia alta. Il nostro passo successivo era quello di produrre un aspetto più organico emetrico rappresentando le nuvole con circa 100.000 particelle.

Se le particelle rimangono solide e ordinate front-to-back, si traggono comunque vantaggio dalla culling del buffer di profondità dietro le particelle sottoposte a rendering precedentemente, riducendo il sovraccarico. Inoltre, una soluzione basata su particelle può modificare il numero di particelle per indirizzare hardware diverso. Tuttavia, tutti i pixel devono comunque essere testati in profondità, causando un sovraccarico maggiore.

Prima di tutto, abbiamo creato posizioni 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 alla parte posteriore, quindi le particelle più vicine ne hanno prima eseguito il rendering.

Un compute shader ha campionato la trama delle informazioni sul cloud per posizionare ogni particella in corrispondenza di 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 sul cloud. Il raggio era basato sulla distribuzione iniziale, che calcolava e archiviava la distanza orizzontale al vicino più vicino.

Abbiamo usato DrawProcedural per eseguire il rendering di un quad per particella. I quad utilizzavano questi dati per orientarsi, inclinati in base all'altezza. Quando gli utenti esaminano una particella orizzontalmente, mostra l'altezza. Quando gli utenti esaminano la particella dall'alto verso il basso, l'area tra di essa e i suoi vicini è coperta.

Diagramma che mostra la forma e la copertura delle particelle.

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 a basso livello, evitando costose sovrapposizioni.

Provare le nuvole di particelle trasparenti

Le particelle solide fornivano una sensazione organica alle forme della nuvola, ma era comunque necessario qualcosa per catturare la nuvolosità delle nuvole. Abbiamo deciso di provare una soluzione personalizzata per le schede grafiche di fascia alta che introduce la trasparenza. Abbiamo semplicemente cambiato l'ordinamento iniziale delle particelle e abbiamo modificato lo shader per usare le trame alfa.

Immagine che mostra nuvole soffici.

Questa soluzione sembrava grande, ma si è dimostrato troppo pesante anche per le macchine più difficili. È stato necessario eseguire il rendering di ogni pixel sullo schermo centinaia di volte.

Eseguire il rendering fuori schermo con una risoluzione inferiore

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

Il codice seguente mostra il rendering fuori schermo:

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 quattro volte, ma aveva un paio di avvertenze. Prima di tutto, quando si esegue il rendering in un buffer fuori schermo, 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 conteneva la geometria principale della scena. Per produrre il buffer di profondità, è stata creata una seconda fotocamera che ha eseguito il rendering solo della geometria continua e della profondità della scena.

È stata usata la nuova trama nel pixel shader cloud per occudere 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 della dissolvenza delle nuvole mentre si avvicinano al terreno. Questo effetto rimuove tutti i tagli duri in cui si incontrano particelle e terreni.

Immagine che mostra le nuvole mescolate nel terreno.

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 stencil.
  3. È stato usato il buffer stencil per mascherare queste aree a contrasto elevato quando si applica il buffer offscreen indietro allo schermo, causando fori in e intorno alle nuvole.
  4. Rendering di tutte le particelle in modalità schermo intero, usando il buffer stencil per mascherare tutto, ma i bordi, causando un set minimo di pixel toccati. Poiché è già stato creato il buffer dei comandi per eseguire il rendering delle particelle, è stato semplicemente eseguito il rendering alla nuova fotocamera.

Immagine che mostra la progressione dei bordi del cloud di rendering.

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

Particelle di Cull

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

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

Il codice seguente illustra un buffer di accodamento.

Shader di calcolo:

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, cullingli sul compute shader e solo il push delle particelle visibili da eseguire. Ma non abbiamo risparmiato molta elaborazione, perché il più grande collo di bottiglia era il numero di pixel cloud da eseguire per il rendering sullo schermo, non il costo del calcolo dei vertici.

Un altro problema è che il buffer di accodamento popolato in ordine casuale, a causa del calcolo parallelizzato delle particelle. Le particelle ordinate diventano non ordinate, causando particelle di cloud flickering. Esistono tecniche per ordinare il buffer push, ma la quantità limitata di prestazioni ottenuta dalle particelle di compensazione 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 di frame 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 sottoposto a rendering, per acquisire sia l'ora dello schermo sinistro che destro.

Confrontare il tempo di rendering con la frequenza di aggiornamento desiderata per mostrare come chiudere i fotogrammi. Quando si avvicinano ai fotogrammi di eliminazione, è 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 da richiedere 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 del risultato per adattarsi allo schermo. Una piccola modifica della scala è notevolmente evidente sulla geometria mondiale e un fattore di scala pari a 0,7 richiede il rendering del numero di pixel.

Immagine che mostra la scala del 70% con metà dei pixel.

Quando si rileva che si sta per eliminare i fotogrammi, ridurre la scala in base a un rapporto fisso e ripristinarlo quando si esegue di nuovo abbastanza velocemente.

In questo caso, abbiamo deciso quale tecnica cloud usare in base alle funzionalità grafiche dell'hardware all'avvio. È anche possibile basare questa decisione sui dati dalle 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:

  • Avviare la destinazione di macchine con alimentazione inferiore, per acquisire familiarità con lo spazio dei problemi.
  • Sviluppare una soluzione di backup in esecuzione in tutti i computer. È quindi possibile eseguire un livello più complesso per i computer di fascia alta o migliorare la risoluzione della soluzione di backup.
  • Progettare la soluzione con frequenza di riempimento, poiché i pixel sono la risorsa più preziosa.
  • Destinazione della geometria solida sulla trasparenza.
  • Progettare scenari in caso peggiore e valutare l'uso del rendering adattivo per situazioni pesanti.

Informazioni sugli autori

Immagine di Robert Ferrese Robert Ferrese
@Microsoft tecnico software
Immagine di Dan Andersson Dan Andersson
@Microsoft tecnico software

Vedi anche