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 worden weergegeven boven op terreingegevens. De toepassing omringt gebruikers met holografische gegevensvisualisaties. Gebruikers kunnen unieke inzichten verkennen die ze verkrijgen door gegevens in mixed reality te ontdekken.

De Datascape-app is gericht op Microsoft HoloLens, Windows Mixed Reality immersive 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, terwijl het uitvoeren met een hoge framesnelheid, op apparaten met zeer verschillende prestatiemogelijkheden.

Deze casestudy doorloopt het proces en de technieken die we hebben gebruikt om enkele van de meer prestatie-intensieve systemen te maken, met name het weergeven van weer zoals wolken. We beschrijven de problemen die we hebben ondervonden en hoe we deze hebben overwonnen.

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

Overzicht van casestudy

Hier volgt wat achtergrondinformatie over de Datascape-toepassing en uitdagingen.

Transparantie en overtekening

Onze belangrijkste renderingsstrijd ging over transparantie, omdat transparantie duur kan zijn.

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

Transparante geometrie moet van achteren naar voren worden gesorteerd en is afhankelijk van het mengen van de uitvoer van de pixel-arcering met de huidige pixel op het scherm. Dit proces kan ertoe leiden dat elke pixel op het scherm meerdere keren per frame wordt getekend, ook wel overtekening genoemd.

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

Onderdelen van scènes in datascape

De scène Datascape bestaat uit drie hoofdonderdelen: de gebruikersinterface, de kaart en het weer. We wisten dat de weerseffecten alle prestaties nodig zouden hebben die ze konden krijgen, dus hebben we de gebruikersinterface en kaart ontworpen om overtekening te verminderen.

We hebben de gebruikersinterface meerdere keren aangepast om de hoeveelheid overschrijding te minimaliseren. Voor onderdelen zoals gloeiende knoppen en kaartoverzichten hebben we ervoor gekozen om complexere geometrie te gebruiken in plaats van transparante illustraties te overlays.

Voor de kaart hebben we een aangepaste arcering gebruikt die standaard Unity-functies zoals schaduwen en complexe belichting heeft verwijderd. De aangepaste arcering vervangt deze functies door een eenvoudig, enkel zonlichtmodel en een aangepaste mistberekening. Deze eenvoudige pixel-shader verbeterde prestaties.

Zowel de gebruikersinterface als de kaart moesten op budget worden weergegeven, zodat er geen hardwareafhankelijke wijzigingen nodig waren. De weervisualisatie, met name de cloudweergave, was een grotere uitdaging.

Cloudgegevens

Cloudgegevens gedownload van NOAA-servers in drie verschillende 2D-lagen. Elke laag had de bovenste en onderste hoogte van de wolk en de dichtheid van de wolk voor elke cel van het raster. We hebben de gegevens verwerkt in een cloudgegevenstextuur waarin elk onderdeel is opgeslagen in de rode, groene en blauwe component van de textuur.

Geometriewolken maken

Om ervoor te zorgen dat machines met een lager vermogen de clouds konden weergeven, gebruikte onze back-upbenadering solide geometrie om overtekening tot een minimum te beperken.

We hebben clouds geproduceerd door voor elke laag een solide heightmap-mesh te genereren. We hebben de straal van het cloudinformatiepatroon per hoekpunt gebruikt om de shape te genereren. We hebben een geometrie-arcering gebruikt om de hoekpunten aan de boven- en onderkant van de wolken te produceren, waardoor vaste wolkenvormen worden gegenereerd. We hebben de dichtheidswaarde van de textuur gebruikt om de wolk donkerder te kleuren voor dichtere wolken.

Met de volgende arceringscode 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 waarden van bijna nul verwijderd door de pixels in de pixel-arcering af te knippen wanneer de waarde van de geïnterpoleerde radius een drempelwaarde bereikt.

Omdat de wolken een ononderbroken geometrie zijn, kunnen ze renderen voordat het terrein wordt weergegeven. Het verbergen van de dure kaart pixels onder de wolken verbetert de framesnelheid verder. Vanwege de benadering van solid geometry rendering is deze oplossing goed uitgevoerd op alle grafische kaarten, van minimale specificaties tot geavanceerde grafische kaarten, en op HoloLens.

Afbeelding van geometriewolken.

Vaste deeltjeswolken gebruiken

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

Als deeltjes vast blijven en van achteren sorteren, profiteert u nog steeds van dieptebuffers die achter eerder gerenderde deeltjes blijven, waardoor overtekening wordt verminderd. Een oplossing op basis van deeltjes kan ook het aantal deeltjes wijzigen waarop verschillende hardware moet worden gericht. Alle pixels moeten echter nog steeds worden getest op de diepte, wat meer overhead veroorzaakt.

Eerst maakten we deeltjesposities rond het middelpunt van de ervaring bij het opstarten. We verdeelden de deeltjes dichter rond het centrum en minder in de verte. We hebben alle deeltjes van het midden naar de achterkant vooraf gesorteerd, zodat de dichtstbijzijnde deeltjes als eerste worden weergegeven.

Een reken-shader heeft een steekproef genomen van de cloudgegevensstructuur om elk deeltje op een juiste hoogte te plaatsen en het in te kleuren op basis van de dichtheid. Elk deeltje bevatte zowel een hoogte als een straal. De hoogte is gebaseerd op de cloudgegevens uit de cloudgegevensstructuur. De straal is gebaseerd op de oorspronkelijke verdeling, die de horizontale afstand tot 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 oriënteren, gebogen door de hoogte. Wanneer gebruikers een deeltje horizontaal bekijken, wordt de hoogte weergegeven. Wanneer gebruikers het deeltje van boven naar beneden bekijken, wordt het gebied tussen het deeltje en de buren bedekt.

Diagram met de vorm en dekking van deeltjes.

De volgende arceringscode 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 van voor naar achteren 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 een lager vermogen, waardoor kostbare overtekening wordt vermeden.

Transparante deeltjeswolken uitproberen

De vaste deeltjes gaven een organisch gevoel aan de wolkenvormen, maar er was toch iets nodig om de luchtigheid van wolken vast te leggen. We hebben besloten om een aangepaste oplossing te proberen voor high-end grafische kaarten die transparantie introduceert. We hebben gewoon de initiële sorteervolgorde van de deeltjes gewijzigd en de arcering gewijzigd om de patronen alfa te gebruiken.

Afbeelding van donzige wolken.

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.

Buiten het scherm weergeven met een lagere resolutie

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

De volgende code toont de weergave buiten het scherm:

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 versnelde de verwerking viervoudig, maar had een aantal beperkingen. Ten eerste hebben we bij het weergeven in een buffer buiten het scherm alle dieptegegevens van onze hoofdscène verloren. Deeltjes achter bergen weergegeven op de top van de berg.

Ten tweede werden door het uitrekken van de buffer artefacten op de randen van wolken geïntroduceerd, waar de wijziging in de resolutie merkbaar was. In de volgende twee secties wordt beschreven hoe we deze problemen hebben opgelost.

Een deeltjesdieptebuffer gebruiken

We moesten de deeltjes naast elkaar laten bestaan met de wereldgeometrie, waar een berg of object de onderliggende deeltjes bedekte. Daarom hebben we de buffer buiten het scherm gevuld met een dieptebuffer die de geometrie van de hoofdscène bevat. 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 op te nemen. We hebben dezelfde textuur gebruikt om de afstand tot de geometrie achter een wolk pixel te berekenen. Door deze afstand te gebruiken en toe te passen op de alfa van de pixel, hebben we het effect bereikt dat wolken verdwijnen wanneer ze dicht bij het terrein komen. Dit effect verwijdert eventuele harde snijwonden waar deeltjes en terrein elkaar ontmoeten.

Afbeelding van wolken die in het terrein zijn opgegaan.

De randen verscherpen

De uitgerekte wolken leken bijna identiek aan wolken van normale grootte in het midden van deeltjes, of waar ze elkaar overlappen, maar toonden enkele artefacten aan de wolkenranden. Scherpe randen verschenen wazig en camerabewegingen introduceerde aliaseffecten.

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

  1. Er is een eenvoudige arcering 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 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, met behulp van de stencilbuffer 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 deze gewoon weer naar de nieuwe camera gerenderd.

Afbeelding van de voortgang van het weergeven van cloudranden.

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

Ruimingsdeeltjes

Voor het windeffect hebben we lange driehoekstrips in een rekenschaduw gegenereerd, waardoor er veel windvlokjes in de wereld zijn ontstaan. Het windeffect was niet zwaar op vulsnelheid, vanwege de smalle stroken. De vele honderdduizenden hoekpunten veroorzaakten echter een zware belasting voor de hoekpuntschaduw.

Om de belasting te verminderen, hebben we toevoegbuffers op de rekenschaduw geïntroduceerd om een subset van de te trekken windstrips te voeden. We hebben eenvoudige frustumverlooplogica voor weergave in de rekenschaduw gebruikt om te bepalen of een strip zich buiten de cameraweergave bevond en hebben voorkomen dat deze strips aan de pushbuffer werden toegevoegd. Dit proces heeft het aantal strips aanzienlijk verminderd, waardoor de prestaties verbeterden.

De volgende code demonstreert een toevoegbuffer.

Compute-arcering:

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 geprobeerd op de wolkendeeltjes, ze op te ruimen op de reken-shader en alleen de zichtbare deeltjes te pushen om te worden weergegeven. Maar we hebben niet veel verwerking bespaard, omdat het grootste knelpunt het aantal cloud pixels was dat op het scherm moest worden weergegeven, niet de kosten voor het berekenen van hoekpunten.

Een ander probleem was dat de toevoegbuffer in willekeurige volgorde werd gevuld, vanwege de geparallelliseerde berekening van de deeltjes. De gesorteerde deeltjes raakten ongesorteerd, wat resulteerde in flikkerende wolkendeeltjes. Er zijn technieken om de pushbuffer te sorteren, maar de beperkte prestatiewinst van het ruimen van deeltjes zou waarschijnlijk worden gecompenseerd door een andere sortering. We hebben besloten om deze optimalisatie voor de wolkendeeltjes niet door te zetten.

Adaptieve rendering gebruiken

Om te zorgen voor een constante framesnelheid in de app 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 de 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 in de buurt komt van het verwijderen van frames, kunt u de rendering aanpassen om sneller te gaan.

Een eenvoudige manier om de weergave aan te passen, is door de grootte van de schermweergavepoort 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 weer op zodat het op het scherm past. Een kleine verandering in de schaal is nauwelijks merkbaar in de wereldgeometrie en voor een schaalfactor van 0,7 moet de helft van het aantal pixels worden weergegeven.

Afbeelding met een schaal van 70%, met de helft van de pixels.

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

In deze casestudy hebben we besloten welke cloudtechniek we moeten gebruiken 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 op een lage resolutie blijft.

Aanbevelingen

Het richten op verschillende hardwaremogelijkheden is een uitdaging en vereist planning. Hier volgen enkele aanbevelingen:

  • Richt u op machines met een lager vermogen om vertrouwd te raken met de probleemruimte.
  • Ontwikkel een back-upoplossing die op al uw computers wordt uitgevoerd. U kunt vervolgens complexere lagen toevoegen voor high-end machines of de resolutie van de back-upoplossing verbeteren.
  • Ontwerp uw oplossing met de opvulsnelheid in gedachten, omdat pixels uw meest kostbare bron zijn.
  • Richt u op ononderbroken geometrie boven transparantie.
  • Ontwerp voor scenario's in het slechtste geval en overweeg adaptieve rendering te gebruiken voor zware situaties.

Over de auteurs

Foto van Robert Ferrese Robert Ferrese
Software engineer @Microsoft
Afbeelding van Dan Andersson Dan Andersson
Software engineer @Microsoft

Zie ook