Esettanulmány – Alkalmazások skálázása különböző képességekkel rendelkező eszközökre

Ez az esettanulmány azt ismerteti, hogy egy Windows Mixed Reality alkalmazás hogyan célozhat meg különböző, különböző hardverképességű platformokat. A Datascape egy Windows Mixed Reality alkalmazás, amely időjárási adatokat jelenít meg a terepadatokon. Az alkalmazás holografikus adatvizualizációkkal veszi körül a felhasználókat. A felhasználók a vegyes valóságban lévő adatok feltárásával fedezhetik fel az általuk szerzett egyedi megállapításokat.

A Datascape alkalmazás Microsoft HoloLens, Windows Mixed Reality modern headseteket, alacsonyabb teljesítményű számítógépeket és nagy teljesítményű számítógépeket céloz meg. A fő kihívás az volt, hogy vizuálisan vonzó jelenetet jelenítsünk meg, miközben nagy képkockasebességgel hajtunk végre vadul eltérő teljesítménnyel rendelkező eszközökön.

Ez az esettanulmány végigvezeti azokat a folyamatokat és technikákat, amelyeket a teljesítményigényesebb rendszerek létrehozásához használtunk, különösen az időjárás, például a felhők megjelenítéséhez. Ismertetjük a tapasztalt problémákat, és hogy hogyan győztük le őket.

További információ a vegyes valóság és a Unity-alkalmazások teljesítményével kapcsolatos szempontokról:

Esettanulmány áttekintése

Íme néhány háttér a Datascape alkalmazásról és a kihívásokról.

Átlátszóság és túllépés

A fő renderelési küzdelmek az átláthatósággal foglalkoznak, mivel az átláthatóság költséges lehet.

A mélységi pufferbe való írás során a tömör geometria elölről hátra is renderelhető, ami megakadályozza a képpont mögött található jövőbeli képpontok renderelését. Ez a művelet megakadályozza, hogy a rejtett képpontok végrehajtják a képpontárnyékolót, és jelentősen felgyorsítsa a megjelenítést. Ha optimálisan rendezi a geometriát, a képernyő minden képpontja csak egyszer rajzol.

Az átlátszó geometriát előre kell rendezni, és a képpontárnyékoló kimenetét a képernyő aktuális képpontjára kell keverni. Ez a folyamat azt eredményezheti, hogy a képernyő minden képpontja keretenként többször is megrajzolódik, úgynevezett overdraw néven.

A HoloLens és az általános pc-k esetében csak néhányszor töltheti ki a képernyőt, így az áttetsző megjelenítés problémássá válik.

Datascape-jelenetösszetevők

A Datascape jelenet három fő összetevőből áll: a felhasználói felületből, a térképből és az időjárásból. Tudtuk, hogy az időjárási hatásoknak minden teljesítményre szükségük lesz, ezért úgy terveztük meg a felhasználói felületet és a térképet, hogy csökkentse a túlterjedés mértékét.

Többször átdolgoztuk a felhasználói felületet, hogy minimálisra csökkentsük a túlterhelés mértékét. Az olyan összetevők esetében, mint a ragyogó gombok és a térkép áttekintése, úgy döntöttünk, hogy összetettebb geometriát használunk a transzparens művészet felülírása helyett.

A térképhez egy egyéni árnyékolót használtunk, amely levágta az olyan standard Unity-funkciókat, mint az árnyékok és az összetett világítás. Az egyéni árnyékoló ezeket a funkciókat egy egyszerű, egyetlen napvilágítási modellre és egy egyéni ködszámításra cserélte. Ez az egyszerű képpontárnyékoló javította a teljesítményt.

A felhasználói felületet és a térképet is a költségvetés alapján rendereltük, így nem volt szükségük hardverfüggő módosításokra. Az időjárási vizualizáció, különösen a felhőbeli megjelenítés, nagyobb kihívást jelentett.

Felhőbeli adatok

A NOAA-kiszolgálókról letöltött felhőadatok három különböző 2D rétegben. Minden réteg a felhő felső és alsó magasságával, valamint a felhő sűrűségével rendelkezett a rács minden cellájára vonatkozóan. Az adatokat egy felhőinformációs anyagmintává dolgoztuk fel, amely az egyes összetevőket a textúra piros, zöld és kék összetevőjében tárolta.

Geometriai felhők létrehozása

Annak biztosítása érdekében, hogy az alacsonyabb teljesítményű gépek renderelhetik a felhőket, a biztonsági mentési megközelítés szilárd geometriát használt a túlterhelés minimalizálása érdekében.

A felhőket úgy hoztuk létre, hogy minden réteghez létrehoztunk egy folytonos magasságtérkép-hálót. Az alakzat létrehozásához a felhőinformációk csúcsonkénti mintázatának sugarát használtuk. Egy geometriai árnyékolót használtunk a felhők tetején és alján lévő csúcsok létrehozásához, így szilárd felhőalakzatokat hoztunk létre. A textúra sűrűségértékét használtuk, hogy a sűrűbb felhők esetében sötétebb színekkel színeződjön a felhő.

A következő shader-kód hozza létre a csúcspontokat:

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

Bevezettünk egy kis zajmintát, hogy részletesebben lekérjük a valós adatokat. Kerek felhőszélek létrehozásához a közel nulla értéket elvetettük a képpontárnyékolóban lévő képpontok kivágásával, amikor az interpolált sugárérték elérte a küszöbértéket.

Mivel a felhők szilárd geometriák, a terep renderelése előtt renderelhetők. A költséges térkép képpontjainak a felhők alatti elrejtése tovább javítja a képkockasebességet. A szilárd geometria renderelési megközelítése miatt ez a megoldás jól futott az összes grafikus kártyán, a minimális specifikációtól a csúcskategóriás grafikus kártyákig és a HoloLens-en.

Geometriai felhőket ábrázoló kép.

Szilárd részecskefelhők használata

A mi megoldásunk a felhőbeli adatok megfelelő ábrázolását eredményezte, de kissé hiányos volt. A felhőbeli renderelés nem azt a mennyiségi érzést közvetíti, amit a csúcskategóriás gépekhez szeretnénk. A következő lépés az volt, hogy egy szervesebb és mennyiségi megjelenést hozzunk létre a felhők körülbelül 100 000 részecskékkel való ábrázolásával.

Ha a részecskék szilárdak maradnak, és előre-hátra rendeznek, akkor is kihasználhatja a korábban renderelt részecskék mögötti mélységi puffert, csökkentve a túldúltságokat. Emellett a részecskealapú megoldás megváltoztathatja a részecskék számát a különböző hardverek megcélzásához. Az összes képpontot azonban mélységi vizsgálatnak kell alávetni, ami nagyobb többletterhelést okoz.

Először is, az indításkor a tapasztalat középpontjában részecskepozíciót hoztunk létre. A részecskéket sűrűbben elosztottuk a középpont körül, és kevésbé a távolságban. Előre rendeztük az összes részecskéket a középponttól a hátig, így a legközelebbi részecskék jelennek meg először.

Egy számítási árnyékoló mintát vett a felhőinformációs anyagmintából, hogy az egyes részecskéket a megfelelő magasságba helyezze, és a sűrűség alapján színezhesse. Minden részecske magasságot és sugarat tartalmazott. A magasság a felhőadatok anyagmintájából vett felhőbeli adatokon alapult. A sugár a kezdeti eloszláson alapult, amely kiszámította és tárolta a legközelebbi szomszédja vízszintes távolságát.

A DrawProcedural használatával rendereltünk egy quadot részecskeenként. A quadok ezeket az adatokat használták fel, hogy tájékozódjanak a magasság alapján. Amikor a felhasználók vízszintesen tekintenek meg egy részecskerészecskét, az megjeleníti a magasságot. Amikor a felhasználók felülről lefelé nézik a részecskerészecskét, a környezet és a szomszédai közötti terület lefedve lesz.

A részecske alakját és lefedettségét bemutató ábra.

A következő shader-kód az eloszlást mutatja:

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

Előre-hátra rendeztük a részecskéket, és továbbra is egyszínű stílusárnyékolót használtunk áttetsző képpontok kivágásához, nem kevertük őket. Ez a technika nagy mennyiségű részecskéket kezel még az alacsonyabb teljesítményű gépeken is, elkerülve a költséges túldúltságokat.

Próbálja ki a transzparens részecskefelhőket

A szilárd részecskék szerves érzetet adtak a felhőalakzatoknak, de mégis kellett valami a felhők bolyhosságának rögzítéséhez. Úgy döntöttünk, hogy kipróbálunk egy egyéni megoldást a csúcskategóriás grafikus kártyákhoz, amelyek átlátszóságot vezetnek be. Egyszerűen átállítottuk a részecskék kezdeti rendezési sorrendjét, és megváltoztattuk az árnyékolót, hogy az alfa anyagmintát használjuk.

A bolyhos felhőket ábrázoló kép.

Ez a megoldás nagyszerűnek tűnt, de még a legkeményebb gépek esetében is túl nehéznek bizonyult. Minden képpontot több százszor kellett megjeleníteni a képernyőn.

Képernyőkijelenítés alacsonyabb felbontással

A felhők rendereléséhez szükséges képpontok számának csökkentése érdekében egy olyan pufferben rendereltük őket, amely a képernyőfelbontás negyede volt. A végeredményt visszanyújtottuk a képernyőre, miután megrajzoltuk az összes részecskéket.

Az alábbi kód a képernyőn kívüli megjelenítést mutatja be:

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

Ez a megoldás négyszeresére felgyorsította a feldolgozást, de volt néhány kikötése. Először is, amikor egy képernyőkijelzőn kívüli pufferbe renderelünk, elveszítettük a fő jelenet összes mélységi információját. A hegyek mögötti részecskék a hegy tetején jelennek meg.

Másodszor, a puffer kinyújtása összetevőket vezetett be a felhők peremén, ahol a felbontás változása észrevehető volt. A következő két szakasz ismerteti, hogyan oldottuk meg ezeket a problémákat.

Részecskemélység-puffer használata

Ahhoz, hogy a részecskék együtt létezhessenek a világ geometriájával, ahol egy hegy vagy objektum a mögötte lévő részecskéket fedte le. Ezért feltöltöttük a képernyő-puffert egy mélységi pufferrel , amely tartalmazza a fő jelenet geometriája. A mélységi puffer előállításához létrehoztunk egy második kamerát, amely csak a jelenet szilárd geometriája és mélysége volt látható.

A felhőbeli képpontárnyékoló új anyagmintázatával elzártuk a képpontokat. Ugyanazt az anyagmintát használtuk a felhőbeli képpontok mögötti geometria távolságának kiszámításához. Ezt a távolságot használva és a képpont alfajára alkalmazva elértük a felhők elhalványulásának hatását, amint közel kerülnek a terephez. Ez a hatás eltávolítja a kemény vágásokat, ahol a részecskék és a terep találkozik.

A terepbe kevert felhőket ábrázoló kép.

Élesebb szélek

A felnyúló felhők majdnem teljesen azonosak voltak a részecskék középpontjában lévő normál méretű felhőkkel, vagy ahol átfedésben voltak, de néhány összetevőt a felhő peremén mutattak. Az éles élek elmosódottnak tűntek, és a kamera mozgása aliaseffektusokat vezetett be.

A probléma megoldásához a következőket tesszük:

  1. Futtatott egy egyszerű árnyékolót a képernyőn kívüli pufferen, hogy megállapítsa, hol történtek nagy változások a kontrasztban.
  2. Helyezze a nagy módosításokat tartalmazó képpontokat egy új rajzsablonpufferbe.
  3. A rajzsablonpuffer használatával elfedte ezeket a kontrasztos területeket, amikor a képernyőre visszahelyezte a képernyőre a képernyőt, ami lyukakat eredményezett a felhőkben és a felhők körül.
  4. Az összes részecskét teljes képernyős módban renderelték, a rajzsablonpufferrel elfedve a széleken kívül mindent, ami minimális képpontméretet eredményezett. Mivel már létrehoztuk a parancspuffert a részecskék rendereléséhez, egyszerűen újra rendereltük az új kamerára.

Kép a felhőszélek renderelésének előrehaladásáról.

A végeredmény éles szélek, a felhők olcsó középső szakaszaival. Bár ez a megoldás sokkal gyorsabb, mint az összes részecskét teljes képernyőn renderelni, a képpontok a rajzsablonpufferen való tesztelése még mindig költségekbe ütközik. Egy hatalmas mennyiségű túldrágulás még mindig drága.

Leselejtezett részecskék

A szélhatáshoz hosszú háromszögcsíkokat hoztunk létre egy számítási árnyékolóban, ami sok széllökéseket eredményezett a világon. A szélhatás nem volt nehéz a kitöltési sebesség miatt a keskeny csíkok. A több százezer csúcspont azonban nagy terhelést okozott a csúcspontárnyékolónak.

A terhelés csökkentése érdekében hozzáfűző puffereket vezettünk be a számítási árnyékolón, hogy a rajzolandó szélcsíkok egy részhalmazát tápláljuk. A számítási árnyékolóban az egyszerű nézet frustum culling logikáját használtuk annak megállapításához, hogy egy sáv kívül volt-e a kameranézeten, és megakadályoztuk, hogy ezek a csíkok hozzá legyenek adva a leküldéses pufferhez. Ez a folyamat jelentősen csökkentette a csíkok számát, javítva a teljesítményt.

Az alábbi kód egy hozzáfűző puffert mutat be.

Számítási árnyékoló:

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

C#-kód:

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

Ezt a technikát a felhőrészecskéken kipróbáltuk, a számítási árnyékolón selejteztük őket, és csak a látható részecskéket nyomtuk le a rendereléshez. A feldolgozást azonban nem takarítottuk meg, mert a legnagyobb szűk keresztmetszet a képernyőn megjelenítendő felhőbeli képpontok száma volt, nem pedig a csúcspontok kiszámításának költsége.

Egy másik probléma az volt, hogy a hozzáfűző puffer véletlenszerű sorrendben lett feltöltve a részecskék párhuzamos számítása miatt. A rendezett részecskék rendezetlenné váltak, ami villódzó felhőrészecskéket eredményezett. Vannak technikák a leküldéses puffer rendezésére, de a selejtezett részecskékből származó korlátozott teljesítménynövekedést valószínűleg egy másik rendezés ellensúlyozná. Úgy döntöttünk, hogy nem tesszük ezt az optimalizálást a felhőrészecskék számára.

Adaptív renderelés használata

Az alkalmazás folyamatos képkockasebességének biztosítása érdekében különböző renderelési feltételekkel, például felhős és világos nézetekkel, adaptív renderelést vezettünk be.

Az adaptív renderelés első lépése a teljesítmény mérése. Egyéni kódot szúrtunk be a parancspufferbe egy renderelt keret elején és végén, a bal és a jobb szem képernyőidő rögzítéséhez.

Hasonlítsa össze a renderelési időt a kívánt frissítési sebességgel, hogy megmutassa, milyen közel kerül a keretek elvetéséhez. Amikor közel kerül a keretek eltávolításához, a renderelést a gyorsabb működéshez igazíthatja.

A renderelés átalakításának egyik egyszerű módja a képernyőnézet méretének módosítása, hogy kevesebb képpontot igényeljen a rendereléshez. A rendszer a UnityEngine.XR.XRSettings.renderViewportScale használatával zsugorítja a megcélzott nézetportot, és automatikusan a képernyőhöz igazítja az eredményt. A skálázás kis mértékű változása alig észrevehető a világgeometria esetében, és a 0,7-es skálázási tényező a képpontok számának felét igényli.

70%-os skálázást ábrázoló kép, a képpontok felével.

Amikor azt észleljük, hogy a keretek elvetésére készülünk, rögzített arányban csökkentjük a skálát, és visszaállítjuk, amikor újra elég gyorsan futunk.

Ebben az esettanulmányban eldöntöttük, hogy melyik felhőalapú technikát érdemes használni a hardver grafikus képességei alapján az indításkor. Ezt a döntést a teljesítménymérésekből származó adatokra is alapozhatja, hogy a rendszer hosszú ideig ne maradjon alacsony felbontásban.

Javaslatok

A különböző hardveres képességek megcélzása kihívást jelent, és tervezést igényel. Íme néhány javaslat:

  • Kezdjen el kisebb teljesítményű gépeket célozni, hogy megismerkedjen a problématérrel.
  • Dolgozzon ki egy biztonsági mentési megoldást, amely az összes gépen fut. Ezután összetettebbé teheti a csúcskategóriás gépeket, vagy javíthatja a biztonsági mentési megoldás felbontását.
  • A megoldást a kitöltési arány szem előtt tartásával tervezheti meg, mivel a képpontok a legértékesebb erőforrások.
  • Szilárd geometria célzása átlátszóság felett.
  • Tervezzen a legrosszabb forgatókönyvekhez, és fontolja meg az adaptív renderelés használatát a nehéz helyzetekhez.

A szerzők ismertetése

Robert Ferrese képe Robert Ferrese
Szoftvermérnök @Microsoft
Dan Andersson képe Dan Andersson
Szoftvermérnök @Microsoft

Lásd még