Étude de cas - Mettre à l’échelle des applications sur des appareils avec différentes fonctionnalités

Cette étude de cas décrit comment une application Windows Mixed Reality peut cibler différentes plateformes avec différentes fonctionnalités matérielles. Datascape est une application Windows Mixed Reality qui affiche les données météorologiques en plus des données de terrain. L’application entoure les utilisateurs avec des visualisations de données holographiques. Les utilisateurs peuvent explorer des insights uniques qu’ils gagnent en découvrant des données en réalité mixte.

L’application Datascape cible Microsoft HoloLens, Windows Mixed Reality casques immersifs, des PC à faible puissance et des PC hautes performances. Le principal défi était de rendre une scène visuellement attrayante, tout en s’exécutant à une fréquence d’images élevée, sur des appareils avec des capacités de performances extrêmement différentes.

Cette étude de cas décrit le processus et les techniques que nous avons utilisés pour créer certains des systèmes plus gourmands en performances, en particulier le rendu des conditions météorologiques comme les nuages. Nous décrivons les problèmes que nous avons rencontrés et comment nous les avons dépassés.

Pour plus d’informations sur les considérations relatives aux performances pour la réalité mixte et pour les applications Unity, consultez :

Vue d’ensemble de l’étude de cas

Voici quelques informations sur l’application datascape et les défis.

Transparence et sursintrait

Nos principales luttes de rendu ont trait à la transparence, car la transparence peut être coûteuse.

Vous pouvez afficher une géométrie solide avant à l’arrière tout en écrivant dans la mémoire tampon de profondeur, ce qui arrête les pixels futurs situés derrière ce pixel du rendu. Cette opération empêche les pixels masqués d’exécuter le nuanceur de pixels et accélère considérablement le rendu. Si vous triez la géométrie de manière optimale, chaque pixel sur l’écran ne dessine qu’une seule fois.

La géométrie transparente doit être triée vers l’avant et s’appuie sur la fusion de la sortie du nuanceur de pixels au pixel actuel sur l’écran. Ce processus peut entraîner chaque pixel sur l’écran en cours de dessin plusieurs fois par image, appelé overdraw.

Pour HoloLens et les PC standard, vous ne pouvez remplir l’écran que quelques fois, ce qui pose problème de rendu transparent.

Composants de scène datascape

La scène Datascape comporte trois composants majeurs : l’interface utilisateur, la carte et la météo. Nous savions que les effets météorologiques auraient besoin de toutes les performances qu’ils pouvaient obtenir. Nous avons donc conçu l’interface utilisateur et la carte pour réduire le redessinage.

Nous avons retravaillé l’interface utilisateur plusieurs fois pour réduire la quantité de surdraw. Pour les composants tels que les boutons lumineux et les vues d’ensemble de la carte, nous avons choisi d’utiliser une géométrie plus complexe plutôt que de superposition d’art transparent.

Pour la carte, nous avons utilisé un nuanceur personnalisé qui a supprimé des fonctionnalités Unity standard telles que les ombres et l’éclairage complexe. Le nuanceur personnalisé a remplacé ces caractéristiques par un modèle d’éclairage solaire simple, unique et un calcul de brouillard personnalisé. Ce nuanceur de pixels simple a amélioré les performances.

Nous avons obtenu à la fois l’interface utilisateur et la carte à afficher au budget. Ils n’ont donc pas besoin de modifications dépendantes du matériel. La visualisation météorologique, en particulier le rendu cloud, était plus difficile.

Données du cloud

Données cloud téléchargées à partir de serveurs NOAA dans trois couches 2D distinctes. Chaque couche avait la hauteur supérieure et inférieure du cloud, et la densité du cloud, pour chaque cellule de la grille. Nous avons traité les données dans une texture d’informations cloud qui stockait chaque composant dans le composant rouge, vert et bleu de la texture.

Créer des clouds géométriques

Pour vous assurer que les machines à faible puissance peuvent rendre les clouds, notre approche de sauvegarde a utilisé une géométrie solide pour réduire le sursinage.

Nous avons produit des clouds en générant un maillage de hauteur solide pour chaque couche. Nous avons utilisé le rayon de la texture d’informations cloud par vertex pour générer la forme. Nous avons utilisé un nuanceur géométrique pour produire les sommets en haut et en bas des nuages, générant des formes de nuages solides. Nous avons utilisé la valeur de densité de la texture pour colorer le nuage avec des couleurs plus foncées pour les nuages denses.

Le code nuanceur suivant crée les sommets :

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

Nous avons introduit un petit modèle de bruit pour obtenir plus de détails sur les données réelles. Pour produire des bords nuageux arrondis, nous avons ignoré des valeurs quasi-nulles en cmettant les pixels dans le nuanceur de pixels lorsque la valeur de rayon interpolée a atteint un seuil.

Étant donné que les nuages sont une géométrie solide, ils peuvent être rendus avant le rendu du terrain. Masquer les pixels de carte coûteux sous les nuages améliore davantage le taux d’images. En raison de l’approche de rendu géométrique solide, cette solution s’est bien exécutée sur toutes les cartes graphiques, de la spécification minimale aux cartes graphiques haut de gamme et sur HoloLens.

Image that shows geometry clouds.

Utiliser des nuages de particules solides

Notre solution a produit une représentation décente des données cloud, mais a été un peu délire. Le rendu cloud n’a pas transmis l’impression volumétrique que nous voulions pour nos machines haut de gamme. Notre étape suivante était de produire un aspect plus organique et volumetrique en représentant les nuages avec environ 100 000 particules.

Si les particules restent solides et trient avant à dos, vous bénéficiez toujours d’un culage de mémoire tampon de profondeur derrière des particules précédemment rendues, réduisant ainsi le redessinage. En outre, une solution basée sur des particules peut modifier le nombre de particules pour cibler différents matériels. Toutefois, tous les pixels doivent toujours être testés en profondeur, ce qui entraîne une surcharge plus importante.

Tout d’abord, nous avons créé des positions de particules autour du point central de l’expérience au démarrage. Nous avons distribué les particules plus densement autour du centre et moins dans la distance. Nous avons trié toutes les particules du centre au dos, donc les particules les plus proches rendues en premier.

Un nuanceur de calcul a échantillonné la texture d’informations cloud pour positionner chaque particule à une hauteur correcte et la couleur en fonction de la densité. Chaque particule contenait à la fois une hauteur et un rayon. La hauteur était basée sur les données cloud échantillonées à partir de la texture des informations cloud. Le rayon était basé sur la distribution initiale, qui a calculé et stocké la distance horizontale à son voisin le plus proche.

Nous avons utilisé DrawProcedural pour rendre un quad par particule. Les quads utilisaient ces données pour s’orienter, angleés par la hauteur. Lorsque les utilisateurs examinent une particule horizontalement, elle affiche la hauteur. Quand les utilisateurs regardent la particule en haut, la zone entre elle et ses voisins est couverte.

Diagram that shows particle shape and coverage.

Le code de nuanceur suivant montre la distribution :

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

Nous avons trié les particules avant à l’arrière, et nous avons toujours utilisé un nuanceur de style solide pour clipper les pixels transparents, pas les fusionner. Cette technique gère un grand nombre de particules, même sur des machines à faible puissance, ce qui évite un sursément coûteux.

Essayer des nuages de particules transparents

Les particules solides ont fourni une sensation organique aux formes de nuage, mais ont encore besoin de quelque chose pour capturer la fluffence des nuages. Nous avons décidé d’essayer une solution personnalisée pour les cartes graphiques haut de gamme qui introduit la transparence. Nous avons simplement changé l’ordre de tri initial des particules et modifié le nuanceur pour utiliser les textures alpha.

Image that shows fluffy clouds.

Cette solution semblait excellente, mais s’est révélée trop lourde pour même les machines les plus difficiles. Chaque pixel devait être rendu sur l’écran des centaines de fois.

Afficher hors écran avec une résolution inférieure

Pour réduire le nombre de pixels pour le rendu des clouds, nous les avons rendus dans une mémoire tampon qui était un quart de résolution d’écran. Nous avons étiré le résultat final à l’écran après avoir dessiné toutes les particules.

Le code suivant montre le rendu hors écran :

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

Cette solution a accéléré le traitement quatre fois, mais avait quelques mises en garde. Tout d’abord, lors du rendu dans une mémoire tampon hors écran, nous avons perdu toutes les informations de profondeur de notre scène principale. Particules derrière des montagnes rendues au-dessus de la montagne.

Deuxièmement, l’étirement de la mémoire tampon a introduit des artefacts sur les bords des clouds, où le changement de résolution était visible. Les deux sections suivantes décrivent comment nous avons résolu ces problèmes.

Utiliser une mémoire tampon de profondeur de particules

Nous avions besoin de faire en sorte que les particules coexistent avec la géométrie mondiale, où une montagne ou un objet couvert de particules derrière elle. Nous avons donc rempli la mémoire tampon hors écran avec une mémoire tampon de profondeur qui contenait la géométrie de la scène principale. Pour produire la mémoire tampon de profondeur, nous avons créé une deuxième caméra qui n’a rendu que la géométrie solide et la profondeur de la scène.

Nous avons utilisé la nouvelle texture dans le nuanceur de pixels cloud pour obstruer des pixels. Nous avons utilisé la même texture pour calculer la distance vers la géométrie derrière un pixel cloud. En utilisant cette distance et en l’appliquant à l’alpha du pixel, nous avons obtenu l’effet des nuages s’évanouissant au fur et à mesure qu’ils se rapprochent du terrain. Cet effet supprime les coupes dures où les particules et le terrain se rencontrent.

Image that shows clouds blended into terrain.

Aiguiser les bords

Les nuages étendus semblaient presque identiques aux nuages de taille normale aux centres de particules, ou où ils se chevauchent, mais ont montré certains artefacts aux bords du cloud. Les arêtes nettes semblaient floues, et le mouvement de caméra a introduit des effets d’alias.

Pour résoudre ce problème, nous :

  1. Exécution d’un nuanceur simple sur la mémoire tampon hors écran pour déterminer l’endroit où des modifications importantes ont eu lieu.
  2. Placez les pixels avec de grandes modifications dans une nouvelle mémoire tampon de gabarit.
  3. Utilisé la mémoire tampon de gabarit pour masquer ces zones à contraste élevé lors de l’application de la mémoire tampon hors écran à l’écran, ce qui entraîne des trous dans et autour des nuages.
  4. Rendu à nouveau toutes les particules en mode plein écran, en utilisant la mémoire tampon de gabarit pour masquer tout, mais les bords, ce qui entraîne un jeu minimal de pixels touché. Étant donné que nous avons déjà créé la mémoire tampon de commande pour restituer les particules, nous l’avons simplement rendue à nouveau sur la nouvelle caméra.

Image showing the progression of rendering cloud edges.

Le résultat final était des arêtes nettes avec des sections centrale bon marché des nuages. Bien que cette solution soit beaucoup plus rapide que le rendu de toutes les particules en plein écran, il reste un coût pour tester les pixels par rapport à la mémoire tampon du gabarit. Une quantité massive de surinsins est encore coûteuse.

Particules de créa ment

Pour l’effet du vent, nous avons généré des bandes de triangles longs dans un nuanceur de calcul, créant de nombreux wisps de vent dans le monde. L’effet du vent n’était pas lourd sur le taux de remplissage, en raison des bandes étroites. Toutefois, les centaines de milliers de sommets ont causé une charge importante pour le nuanceur de vertex.

Pour réduire la charge, nous avons introduit des mémoires tampons d’ajout sur le nuanceur de calcul pour alimenter un sous-ensemble des bandes éoliennes à dessiner. Nous avons utilisé une logique frustum de vue simple dans le nuanceur de calcul pour déterminer si une bande était en dehors de la vue de la caméra et empêchait ces bandes d’être ajoutées à la mémoire tampon push. Ce processus a considérablement réduit le nombre de bandes, améliorant ainsi les performances.

Le code suivant illustre une mémoire tampon d’ajout.

Nuanceur de calcul :

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

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

Nous avons essayé cette technique sur les particules cloud, les mettant à l’arrêt sur le nuanceur de calcul, et nous n’avons poussé que les particules visibles à rendre. Mais nous n’avons pas enregistré beaucoup de traitement, car le plus gros goulot d’étranglement était le nombre de pixels cloud à afficher à l’écran, et non le coût du calcul des sommets.

Un autre problème était que la mémoire tampon d’ajout remplie dans un ordre aléatoire, en raison de l’informatique parallélisée des particules. Les particules triées deviennent non triées, ce qui entraîne le scintillement des particules nuageux. Il existe des techniques pour trier la mémoire tampon push, mais la quantité limitée de performances des particules d’élimination serait probablement compensée par un autre tri. Nous avons décidé de ne pas poursuivre cette optimisation pour les particules cloud.

Utiliser le rendu adaptatif

Pour garantir une fréquence d’images stable sur l’application avec des conditions de rendu variables, comme une vue nuageux et claire, nous avons introduit le rendu adaptatif.

La première étape du rendu adaptatif consiste à mesurer les performances. Nous avons inséré du code personnalisé dans la mémoire tampon de commande au début et à la fin d’un cadre rendu, pour capturer à la fois le temps d’écran gauche et droit de l’écran.

Comparez le temps de rendu au taux d’actualisation souhaité pour afficher la proximité de la suppression d’images. Lorsque vous approchez de la suppression d’images, vous pouvez adapter le rendu pour être plus rapide.

Une façon simple d’adapter le rendu consiste à modifier la taille de la fenêtre d’affichage de l’écran afin qu’elle nécessite moins de pixels à afficher. Le système utilise UnityEngine.XR.XRSettings.renderViewportScale pour réduire la fenêtre d’affichage ciblée et étire automatiquement le résultat pour s’adapter à l’écran. Une petite modification de l’échelle est à peine visible sur la géométrie mondiale, et un facteur d’échelle de 0,7 nécessite que la moitié du nombre de pixels soit rendu.

Image showing 70% scale, with half the pixels.

Lorsque nous détectons que nous allons supprimer des images, nous allons réduire l’échelle par un ratio fixe et le restaurer quand nous sommes suffisamment rapides.

Dans cette étude de cas, nous avons décidé quelle technique cloud utiliser en fonction des fonctionnalités graphiques du matériel au démarrage. Vous pouvez également baser cette décision sur les données des mesures de performances, afin d’empêcher le système de rester à faible résolution pendant longtemps.

Recommandations

Le ciblage de différentes fonctionnalités matérielles est difficile et nécessite une planification. Voici quelques recommandations :

  • Commencez à cibler des machines moins puissantes pour vous familiariser avec l’espace problématique.
  • Développez une solution de sauvegarde qui s’exécute sur toutes vos machines. Vous pouvez ensuite renforcer la complexité des machines haut de gamme ou améliorer la résolution de la solution de sauvegarde.
  • Concevez votre solution avec un taux de remplissage à l’esprit, car les pixels sont votre ressource la plus précieuse.
  • Ciblez une géométrie solide sur la transparence.
  • Concevez des scénarios pires et envisagez d’utiliser le rendu adaptatif pour les situations lourdes.

À propos des auteurs

Picture of Robert Ferrese Robert Ferrese
Ingénieur logiciel @Microsoft
Picture of Dan Andersson Dan Andersson
Ingénieur logiciel @Microsoft

Voir aussi