Casestudy: apps schalen op verschillende apparaten met verschillende mogelijkheden

In deze casestudy wordt beschreven hoe een Windows Mixed Reality-toepassing zich kan richten op verschillende platforms met verschillende hardwaremogelijkheden. Datascape is een Windows Mixed Reality toepassing waarmee weergegevens op terreingegevens worden weergegeven. De toepassing omringt gebruikers met holografische gegevensvisualisaties. Gebruikers kunnen unieke inzichten verkennen die ze verkrijgen door gegevens te ontdekken in mixed reality.

De Datascape-app is gericht op Microsoft HoloLens, Windows Mixed Reality insluitende headsets, pc's met een lager vermogen en pc's met hoge prestaties. De belangrijkste uitdaging was het weergeven van een visueel aantrekkelijke scène, tijdens het uitvoeren met een hoge framesnelheid, op apparaten met wild verschillende prestatiemogelijkheden.

Deze casestudy begeleidt het proces en de technieken die we hebben gebruikt om enkele van de meer prestatieintensieve systemen te maken, met name het weergeven van weer zoals wolken. We beschrijven de problemen die we hebben aangetroffen en hoe we ze hebben overschreven.

Zie voor meer informatie over prestatieoverwegingen voor mixed reality en voor Unity-apps:

Overzicht van casestudy

Hier volgt een achtergrond over de Datascape-toepassing en uitdagingen.

Transparantie en overtekening

Onze belangrijkste rendering worstelt met transparantie, omdat transparantie duur kan zijn.

U kunt een ononderbroken geometrie aan de achterkant weergeven tijdens het schrijven naar de dieptebuffer, waardoor toekomstige pixels die zich achter die pixel bevinden, niet meer worden weergegeven. Met deze bewerking voorkomt u dat verborgen pixels de pixel-shader kunnen uitvoeren en de rendering aanzienlijk wordt versneld. Als u de geometrie optimaal sorteert, tekent elke pixel op het scherm slechts één keer.

Transparante geometrie moet terug naar voor worden gesorteerd en is afhankelijk van het combineren van de uitvoer van de pixel-shader tot de huidige pixel op het scherm. Dit proces kan ertoe leiden dat elke pixel op het scherm meerdere keren per frame wordt getekend, genaamd overdraw.

Voor HoloLens en basis-pc's kunt u het scherm slechts een paar keer vullen, waardoor transparante rendering problematisch wordt.

Onderdelen van datascape-scènes

De Datascape-scène heeft drie belangrijke onderdelen: de gebruikersinterface, de kaart en het weer. We wisten dat de weereffecten alle prestaties nodig zouden hebben die ze konden krijgen, dus hebben we de gebruikersinterface ontworpen en toegewezen om de overtekening te verminderen.

We hebben de gebruikersinterface meerdere keren opnieuw bewerkt om de hoeveelheid overtekening te minimaliseren. Voor onderdelen zoals gloeiende knoppen en kaartoverzichten hebben we ervoor gekozen om complexere geometrie te gebruiken in plaats van transparante kunst te overlayen.

Voor de kaart hebben we een aangepaste shader gebruikt waarmee standaard Unity-functies, zoals schaduwen en complexe verlichting, zijn verwijderd. De aangepaste shader vervangt deze functies door een eenvoudig, enkel zonnelichtmodel en een aangepaste mistberekening. Deze eenvoudige pixel shader verbeterde prestaties.

We hebben zowel de gebruikersinterface als de kaart om te renderen in budget, zodat ze geen hardwareafhankelijke wijzigingen nodig hadden. De weervisualisatie, met name de cloudweergave, was lastiger.

Cloudgegevens

Cloudgegevens die zijn gedownload van NOAA-servers in drie afzonderlijke 2D-lagen. Elke laag had de bovenste en laagste hoogte van de cloud en de dichtheid van de cloud, voor elke cel van het raster. We hebben de gegevens verwerkt in een cloudgegevenspatroon dat elk onderdeel heeft opgeslagen in het rode, groene en blauwe onderdeel van het patroon.

Geometriewolken maken

Om ervoor te zorgen dat machines met lagere voeding de clouds kunnen genereren, heeft onze back-upbenadering een solide geometrie gebruikt om de overtekening te minimaliseren.

We hebben clouds geproduceerd door voor elke laag een solide heightmap-mesh te genereren. We hebben de radius van het cloudinformatiepatroon per hoekpunt gebruikt om de shape te genereren. We hebben een geometrie-shader gebruikt om de hoekpunten aan de bovenkant en onderkant van de wolken te produceren, waardoor solide cloudshapes worden gegenereerd. We hebben de dichtheidswaarde van het patroon gebruikt om de cloud te kleuren met donkerdere kleuren voor dichte wolken.

Met de volgende shader-code worden de hoekpunten gemaakt:

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

We hebben een klein ruispatroon geïntroduceerd om meer details te krijgen over de echte gegevens. Om ronde wolkenranden te produceren, hebben we bijna nul waarden verwijderd door de pixels in de pixel-shader te knippen wanneer de geïnterpoleerde radiuswaarde een drempelwaarde bereikt.

Omdat de wolken solide geometrie zijn, kunnen ze worden gerenderd voordat het terrein wordt weergegeven. Door de dure kaart pixels onder de clouds te verbergen, wordt de framesnelheid verder verbeterd. Vanwege de robuuste geometrieweergave is deze oplossing goed uitgevoerd op alle grafische kaarten, van minimumspecificatie tot high-end grafische kaarten en op HoloLens.

Image that shows geometry clouds.

Vaste deeltjeswolken gebruiken

Onze oplossing heeft een fatsoenlijke weergave van de cloudgegevens geproduceerd, maar was een beetje gebrek aanluster. De cloudrendering bracht het volumetrische gevoel niet over dat we wilden voor onze high-end machines. Onze volgende stap was het produceren van een meer organische en volumetrische look door de wolken weer te geven met ongeveer 100.000 deeltjes.

Als deeltjes ononderbroken blijven en van voor naar achteren sorteren, profiteert u nog steeds van dieptebuffer die achter eerder gerenderde deeltjes wordt afgemaakt, waardoor de overtekening wordt verminderd. Bovendien kan een oplossing op basis van deeltjes het aantal deeltjes wijzigen om verschillende hardware te bereiken. Alle pixels moeten echter nog steeds uitgebreid worden getest, wat meer overhead veroorzaakt.

Eerst hebben we deeltjesposities gemaakt rond het middelpunt van de ervaring bij opstarten. We verdeelden de deeltjes dichter rond het midden en minder in de afstand. We hebben alle deeltjes van het midden naar de achterkant vooraf gesorteerd, dus de dichtstbijzijnde deeltjes worden eerst weergegeven.

Een compute shader steekproefte het cloudinformatiepatroon om elk deeltje op een juiste hoogte te plaatsen en deze te kleuren op basis van dichtheid. Elk deeltje bevatte zowel een hoogte als een straal. De hoogte is gebaseerd op de cloudgegevens uit het patroon van cloudgegevens. De radius is gebaseerd op de initiële verdeling, die de horizontale afstand naar de dichtstbijzijnde buur heeft berekend en opgeslagen.

We hebben DrawProcedural gebruikt om een quad per deeltje weer te geven. De quads gebruikten deze gegevens om zich te richten, gehoekt door de hoogte. Wanneer gebruikers horizontaal naar een deeltje kijken, wordt de hoogte weergegeven. Wanneer gebruikers het deeltje boven beneden bekijken, wordt het gebied tussen de deeltjes en de buren bedekt.

Diagram that shows particle shape and coverage.

De volgende shader-code toont de distributie:

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

We hebben de deeltjes vooraan gesorteerd en nog steeds een effen stijl-shader gebruikt om transparante pixels te knippen, niet om ze te mengen. Deze techniek verwerkt een groot aantal deeltjes, zelfs op machines met lagere voeding, waardoor dure overtekening wordt vermeden.

Transparante deeltjeswolken uitproberen

De vaste deeltjes hebben een organisch gevoel voor de wolkenvormen, maar hebben nog steeds iets nodig om de fluffie van wolken vast te leggen. We hebben besloten om een aangepaste oplossing te proberen voor high-end grafische kaarten die transparantie introduceert. We hebben simpelweg de eerste sorteervolgorde van de deeltjes veranderd en de shader gewijzigd om de texturen alfa te gebruiken.

Image that shows fluffy clouds.

Deze oplossing zag er geweldig uit, maar bleek te zwaar voor zelfs de zwaarste machines. Elke pixel moest honderden keren op het scherm worden weergegeven.

Scherm met lagere resolutie weergeven

Om het aantal pixels voor het weergeven van de clouds te verminderen, hebben we ze weergegeven in een buffer die een kwart van de schermresolutie was. We hebben het eindresultaat weer op het scherm uitgerekt nadat we alle deeltjes hebben getekend.

De volgende code toont de weergave van het 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);

Deze oplossing heeft viervoudige verwerking versneld, maar had een paar opmerkingen. Eerst verloren we bij het weergeven in een buffer buiten het scherm alle dieptegegevens uit onze hoofdscène. Deeltjes achter bergen die boven op de berg worden weergegeven.

Ten tweede heeft het uitrekken van de buffer artefacten geïntroduceerd op de randen van clouds, waarbij de oplossingswijziging merkbaar was. In de volgende twee secties wordt beschreven hoe we deze problemen hebben opgelost.

Een deeltjesdieptebuffer gebruiken

We moesten de deeltjes samen met de wereldgeometrie maken, waar een berg of object daarachter deeltjes bedekte. We hebben de buffer buiten het scherm gevuld met een dieptebuffer die de geometrie van de hoofdscène bevatte. Om de dieptebuffer te produceren, hebben we een tweede camera gemaakt die alleen de solide geometrie en diepte van de scène weergeeft.

We hebben de nieuwe textuur in de cloud pixel shader gebruikt om pixels af te sluiten. We hebben hetzelfde patroon gebruikt om de afstand tot de geometrie achter een cloud pixel te berekenen. Door die afstand te gebruiken en toe te passen op de alfa van de pixel, hebben we het effect bereikt dat wolken vervagen wanneer ze dicht bij terrein komen. Dit effect verwijdert harde snijpunten waar deeltjes en terrein elkaar ontmoeten.

Image that shows clouds blended into terrain.

De randen verscherpen

De uitgerekte wolken keken bijna identiek aan normale wolken in de kernen van deeltjes, of waar ze elkaar overlappen, maar toonden enkele artefacten aan de cloudranden. Scherpe randen verschenen wazig en camera-beweging introduceerde aliaseffecten.

Om dit probleem op te lossen, doen we het volgende:

  1. Er is een eenvoudige shader uitgevoerd op de buffer buiten het scherm om te bepalen waar grote wijzigingen in contrast zijn opgetreden.
  2. Plaats de pixels met grote wijzigingen in een nieuwe stencilbuffer.
  3. De stencilbuffer wordt gebruikt om deze gebieden met hoog contrast te maskeren bij het terugbrengen van de buffer buiten het scherm, wat resulteert in gaten in en rond de wolken.
  4. Alle deeltjes opnieuw weergegeven in de modus Volledig scherm, waarbij de stencilbuffer wordt gebruikt om alles behalve de randen te maskeren, wat resulteert in een minimale set pixels die worden aangeraakt. Omdat we de opdrachtbuffer al hebben gemaakt om de deeltjes weer te geven, hebben we het gewoon opnieuw gerenderd naar de nieuwe camera.

Image showing the progression of rendering cloud edges.

Het eindresultaat was scherpe randen met goedkope middensecties van de wolken. Hoewel deze oplossing veel sneller is dan het weergeven van alle deeltjes op volledig scherm, zijn er nog steeds kosten verbonden om pixels te testen op basis van de stencilbuffer. Een enorme hoeveelheid overtekening is nog steeds duur.

Deeltjes ruimen

Voor het windeffect hebben we lange driehoeksstroken gegenereerd in een berekeningstint, waardoor veel windwissers ter wereld ontstaan. Het windeffect was niet zwaar op vulsnelheid, vanwege de smalle stroken. De vele honderdduizenden hoekpunten veroorzaakten echter een zware belasting voor de hoekpunt-shader.

Om de belasting te verminderen, hebben we toevoegbuffers op de berekeningsschaduw geïntroduceerd om een subset van de windstroken te voeren die moeten worden getekend. We hebben eenvoudige frustum-logica gebruikt in de berekeningsschaduw om te bepalen of een strip buiten de cameraweergave lag en verhinderde dat deze stroken aan de pushbuffer worden toegevoegd. Dit proces heeft het aantal stroken aanzienlijk verminderd, waardoor de prestaties worden verbeterd.

De volgende code demonstreert een toevoegbuffer.

Compute-shader:

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

C#-code:

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

We hebben deze techniek op de wolkdeeltjes geprobeerd, ze op de rekenkaker te ruimen en alleen de zichtbare deeltjes te pushen om te worden gerenderd. Maar we hebben niet veel verwerking bespaard, omdat het grootste knelpunt het aantal cloud pixels was om op het scherm weer te geven, niet de kosten voor het berekenen van hoekpunten.

Een ander probleem was dat de toevoegbuffer in willekeurige volgorde werd gevuld, vanwege de parallelliseerde computing van de deeltjes. De gesorteerde deeltjes werden ongesorteerd, wat resulteert in flikkerende wolkendeeltjes. Er zijn technieken om de pushbuffer te sorteren, maar de beperkte prestatiewinst van het ruimen van deeltjes zou waarschijnlijk worden verschoven door een andere sortering. We hebben besloten deze optimalisatie voor de wolkdeeltjes niet na te streven.

Adaptieve rendering gebruiken

Om een constante framesnelheid in de app te garanderen met verschillende renderingvoorwaarden, zoals een bewolkte versus duidelijke weergave, hebben we adaptieve rendering geïntroduceerd.

De eerste stap van adaptieve rendering is het meten van prestaties. We hebben aangepaste code ingevoegd in de opdrachtbuffer aan het begin en einde van een gerenderd frame om zowel de schermtijd van het linker- als het rechteroog vast te leggen.

Vergelijk de weergavetijd met de gewenste vernieuwingsfrequentie om te laten zien hoe dicht u bij het neerzetten van frames komt. Wanneer u bij het neervallen van frames komt, kunt u de rendering aanpassen om sneller te zijn.

Een eenvoudige manier om rendering aan te passen, is door de grootte van de schermweergave te wijzigen, zodat er minder pixels nodig zijn om weer te geven. Het systeem maakt gebruik van UnityEngine.XR.XRSettings.renderViewportScale om de beoogde viewport te verkleinen en rekt het resultaat automatisch uit totdat het scherm past. Een kleine verandering in schaal is nauwelijks merkbaar op wereldgeometrie en een schaalfactor van 0,7 vereist dat de helft van het aantal pixels wordt weergegeven.

Image showing 70% scale, with half the pixels.

Wanneer we detecteren dat we frames gaan verwijderen, verlagen we de schaal met een vaste verhouding en herstellen we deze wanneer we weer snel genoeg worden uitgevoerd.

In deze casestudy hebben we besloten welke cloudtechniek moet worden gebruikt op basis van de grafische mogelijkheden van de hardware bij het opstarten. U kunt deze beslissing ook baseren op gegevens uit prestatiemetingen om te voorkomen dat het systeem lange tijd met een lage resolutie blijft.

Aanbevelingen

Het is lastig om verschillende hardwaremogelijkheden aan te pakken en hiervoor is planning vereist. Hier volgen enkele aanbevelingen:

  • Startmenu gericht op machines met lagere voeding, om vertrouwd te raken met de probleemruimte.
  • Ontwikkel een back-upoplossing die op al uw computers wordt uitgevoerd. Vervolgens kunt u laag in meer complexiteit voor high-endmachines of de oplossing voor back-up verbeteren.
  • Ontwerp uw oplossing met de vulsnelheid in gedachten, omdat pixels uw meest kostbare resource zijn.
  • Doel solide geometrie over transparantie.
  • Ontwerp voor slechtste scenario's en overweeg adaptieve rendering te gebruiken voor zware situaties.

Over de auteurs

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

Zie ook