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.
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 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.
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.
É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:
- 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.
- Helyezze a nagy módosításokat tartalmazó képpontokat egy új rajzsablonpufferbe.
- 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.
- 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.
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.
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 Szoftvermérnök @Microsoft |
|
Dan Andersson Szoftvermérnök @Microsoft |