Esettanulmány – Adatok méretezése különböző teljesítményű eszközök között

A Datascape egy Windows Mixed Reality, amely a Microsoftnál lett kifejlesztve, és az időjárási adatoknak a talajra vonatkozó adatokra való megjelenítésére összpontosítottunk. Az alkalmazás a holografikus adatvizualizációval körülveszi a felhasználót, és megvizsgálja, hogy a felhasználók hogyan fedezhetik fel az adatokat vegyes valóságban.

A Datascape esetében számos különböző hardveres képességgel rendelkező platformot szerettünk volna célozni, az Microsoft HoloLens-től Windows Mixed Reality modern headsetekig, valamint az alsóbb szintű pc-ktől a legújabb, csúcsteljesítményű GPU-val rendelkező számítógépekig. A fő kihívás az volt, hogy a jelenet vizuálisan tetszethetően néz ki olyan eszközökön, amelyeken nagyon eltérő grafikus képességek biztosítanak nagy képkocka-sebességű végrehajtást.

Ez az esettanulmány a nagyobb GPU-igényű rendszerek létrehozásához használt folyamatot és technikákat ismerteti, és leírja az észlelt problémákat és azok túlmerülési folyamatát.

Átláthatóság és átfedés

A fő renderelés nehezen átlátható, mivel az átlátszóság költséges lehet egy GPU esetében.

A folytonos geometria a mélységi pufferbe írás közben elölről-hátra renderelve megakadályozza a képpont mögötti további képpontok elvetését. Ez megakadályozza, hogy a rejtett képpontok végrehajtsák a képpont árnyékolóját, ami jelentősen felgyorsítja a folyamatot. Ha a geometria optimálisan van rendezve, a képernyőn minden képpont csak egyszer lesz rajzolva.

A transzparens geometriát vissza kell rendezni az elejére, és a képpont árnyékoló kimenetét a képernyőn lévő aktuális képpontba kell keverni. Ez azt eredményezheti, hogy a képernyő minden képpontja képkockánként többször is kirajzolható. Ezt átfedésnek nevezzük.

A HoloLens és alapvető pc-k esetén a képernyő csak néhány alkalommal tölt fel, ami problémássá teszi a transzparens megjelenítést.

A Data scene-összetevők bemutatása

A jelenetnek három fő összetevője volt; a felhasználói felület, a térképés az időjárás. Korábban már tudtuk, hogy az időjárási hatásokhoz minden GPU-idő szükséges, ezért szándékosan úgy alakítottuk ki a felhasználói felületet és a hűtést, hogy csökkentsük az áteredéseket.

Többször is átdolgozottuk a felhasználói felületet, hogy minimalizáljuk a túltelkedő adatmennyiséget. Az összetettebb geometria oldalán hibáztunk ahelyett, hogy a transzparens képet egymásra fedjük az olyan összetevők számára, mint a kicsatolási gombok és a térképáttekintés.

A térképhez egy egyéni árnyékolót használtunk, amely az olyan szabványos Unity-funkciókat, mint az árnyékok és az összetett világítás, lecserélve őket egy egyszerű napvilágítási modellre és egy egyéni fogasmérték-számításra. Ez egy egyszerű képpontos árnyékolót készített, és GPU-ciklusokat szabadít fel.

A felhasználói felületet és a térképet is sikerült a költségvetésben renderelnünk, ahol a hardvertől függően nem kellett módosítanunk őket; Azonban az időjárási vizualizáció, különösen a felhőre való renderelés, nagyobb kihívást jelent!

Felhőbeli adatok háttere

A felhőadatokat a NOAA-kiszolgálókról ( ) letöltöttük, és három különálló 2D rétegben ügyfeleinkhez értünk, amelyek a felhő felső és alsó magasságával, valamint a rács egyes celláihoz a felhő sűrűségét https://nomads.ncep.noaa.gov/ tartalmazták. Az adatokat felhőalapú információs textúra-textúraként feldolgozták, ahol az egyes összetevők a textúra piros, zöld és kék összetevőjében tárolódtak a GPU egyszerű elérése érdekében.

Geometriai felhők

Úgy döntöttünk, hogy az alsóbb szintű gépek renderelnék a felhőket, és úgy döntöttünk, hogy egy olyan megközelítéssel kezdünk, amely tartós geometriát használ a túlhasználat minimalizálása érdekében.

Először úgy próbáltunk felhőket létrehozni, hogy minden réteghez egy folytonos magasságtérkép-hálót hoztunk létre a felhőinformációs textúra csúcsonkénti sugárával az alakzat létrehozásához. Egy geometriai árnyékolóval hoztunk létre csúcsokat a felhő tetején és alján, szilárd felhőalakzatokat generálva. A textúra sűrűségének értékét arra használtjuk, hogy a felhőt sötét színekkel színezzék a sűrűbb felhőkhöz.

Shader a csúcsok létrehozásához:

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, amely részletesebben ismerteti a valós adatokat. A kerek felhőbeli élek előállításához képpont árnyékolást készítünk a képpont árnyékolóban, amikor az interpolált sugár értéke eléri a küszöbértéket a közel nulla érték elvetése érdekében.

Geometriai felhők

Mivel a felhők szilárd geometriák, renderelheti őket a váz előtt, hogy elrejtse az alatta lévő drága térkép képpontokat a képkocka-teljesítmény további javítása érdekében. Ez a megoldás jól futott az összes grafikus kártyán a min-spec és a high-end grafikus kártyák között, valamint a HoloLens-on is a folytonos geometriai renderelési megközelítés miatt.

Szilárd részecskéket tartalmazó felhők

Most már volt egy olyan biztonsági mentési megoldásunk, amely a felhőbeli adatok megfelelő megfelelő ábrázolása volt, de a "wow" tényező nem volt elég egyértelmű, és nem közvetíti azt a mennyiségi érzést, amelyet a felső szintű gépeink számára szeretnénk.

A következő lépés a felhők létrehozása volt azáltal, hogy körülbelül 100 000 részecskékkel ábrázoljuk őket, hogy természetesebb és mennyiségibb megjelenést hozzunk létre.

Ha a részecskék szilárdak maradnak, és elölről-hátra rendezik őket, akkor is hasznos lehet a korábban renderelt részecskék mögötti képpontok mélységi pufferelése, így csökkentve a túlcsordulást. Emellett egy részecskéken alapuló megoldással megváltoztathatjuk a különböző hardverek megcélzott részecskék mennyiségét. Azonban minden képpontot meg kell vizsgálni a mélységi teszt során, ami további többletterhelést okoz.

Először is részecskék pozícióit hoztuk létre az indításkor tapasztalt élmény középpontja körül. A részecskéket a középpont körül sűrűbben, a távolságban kevésbé osztotta el. Minden részecskéket előre rendeztünk középről hátra, hogy a legközelebbi részecskéket rendereljük először.

Egy számítási árnyékoló mintát vett a felhőinformációs textúra mintázatából, hogy az egyes részecskéket a sűrűség alapján a megfelelő magasságba helyezze és színezzék.

A DrawProcedural segítségével egy quadot rendereltünk részecskékenként, így a részecskék adatai mindig a GPU-on maradnak.

Mindegyik részecskék egy magasságot és egy sugárt is tartalmaznak. A magasság a felhőinformációs textúra mintából vett felhőadatokon alapult, a sugár pedig azon a kezdeti eloszláson alapult, ahol a legközelebbi szomszédtól való vízszintes távolság tárolására lett kiszámítva. A quadok ezeket az adatokat arra használják, hogy a magasság alapján szögezték le magát, így amikor a felhasználók vízszintesen néznek rá, a magasság látható lesz, és amikor a felhasználók felülről lefelé néznek, a szomszédai közötti területet fedik le.

Részecskék alakja

Az eloszlást megjelenítő árnyékolókód:

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

Mivel a részecskéket elölről-hátra rendezjük, és továbbra is szilárd stílusú árnyékolót használtunk a átlátszó képpontok kivágásához (nem kevert), ez a technika meglepő mennyiségű részecskéket kezel, így elkerülve a költséges túlrajzolásokat még az alsóbb szintű gépeken is.

Átlátszó részecskéket tartalmazó felhők

A szilárd részecskék jó szerves formában biztosítanak a felhők alakját, de mégis szükség volt valamire a felhők ingadozása érdekében. Úgy döntöttünk, hogy egy egyéni megoldást próbálunk ki a csúcs szintű grafikus kártyákhoz, ahol bevezetjük az átláthatóságot.

Ehhez egyszerűen átváltottunk a részecskék kezdeti rendezési sorrendjére, és módosítottunk egy árnyékolót az alfa textúra használatára.

Fluffy-felhők

Nagyszerűnek tűnt, de még a legkombesebb gépekhez is túl nehéznek bizonyult, mivel több száz alkalommal renderelné az egyes képpontokat a képernyőn!

Képernyőről kiolvasva, alacsonyabb felbontással

A felhők által renderelt képpontok számának csökkentése érdekében (a képernyőhöz képest) egy negyedévi felbontáspufferben kezdtük renderelni őket, majd az eredmény visszahágása a képernyőre az összes részecskék megrajzolása után. Ez nagyjából 4-edik gyorsulást adott, de több kikötéssel is járt.

Kód a képernyőről való megjelenítéshez:

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

Először is, amikor képernyőről kiveszi a képernyőről a puffert, minden mélységi információt elveszítettünk a fő jelenetből, ami a hegy tetején lévő, hegyre renderelt részecskéket eredményezett.

Másodszor, a puffer kinyújtása olyan összetevőkhöz is vezetett a felhők szélein, ahol a megoldás módosítása észrevehető volt. A következő két szakasz azt ismerteti, hogyan oldottuk meg ezeket a problémákat.

Részecskék mélységi puffere

Annak érdekében, hogy a részecskék együtt létezhetnek a világ geometriájával, ahol egy hegy vagy objektum el tudja fedni a mögöttes részecskéket, a képernyőfedő puffert a fő jelenet geometriáját tartalmazó mélységi pufferrel tölti fel. Az ilyen mélységi puffer előállításához létrehoztunk egy második kamerát, amely csak a jelenet stabil geometriáját és mélységét renderelte.

Ezután a felhők képpontárnyaló új textúraját használtuk a képpontok kitűnése érdekében. Ugyanazt a textúra-t használtjuk a felhő képpontja mögötti geometria távolságának kiszámításához. Ennek a távolságnak a használatával és a képpont alfajával való alkalmazása után a felhők elszürkednek, ahogy közel vannak az agyhoz, eltávolítva a szemük és a fogazat közötti kemény vágásokat.

A felhők beleolvadtak a felhőkbe

Az élek éleinek élezése

A kinyújtott felhők szinte teljesen azonosak voltak a részecskék közepén, vagy ahol átfedésben voltak, de néhány összetevőt a felhő szélein mutattak. Ellenkező esetben az éles élek homályosnak jelennek meg, és aliashatások jelentek meg a kamera mozgatáskor.

Ezt úgy oldottuk meg, hogy egy egyszerű árnyékolót futtattunk a képernyőfüves pufferen annak meghatározásához, hogy hol történtek nagy változások a kontrasztban (1). A nagy módosításokat is eltévő képpontokat új sablonpufferbe (2) szeretnénk tenni. Ezt követően a sablonpuffer segítségével maszkoltuk ezeket a kontrasztos területeket, amikor a képernyőről kiveszi a képernyőről a puffert a képernyőre, így a felhők be- és körül ásnak a rések (3).

Ezután ismét teljes képernyős módban rendereltünk minden részecskéket, de ezúttal a sablonpuffer segítségével maszkoltunk mindent, kivéve az éleket, így a képpontok minimális készlete meg volt érintve (4). Mivel a parancspuffer már létre lett hozva a részecskék számára, egyszerűen újra kellett renderelni az új kamerába.

A felhőbeli élek renderelésének előrehaladása

Az eredmény éles széleket eredményez, a felhők olcsóbb középső szakaszaival.

Bár ez sokkal gyorsabb volt, mint az összes részecskék teljes képernyős megjelenítése, a képpontok a rajzsablonpufferen való tesztelése továbbra is költséggel járt, így nagy mennyiségű túlcsordulás is jár költséggel.

Részecskék kigúnyolása

A szélhatáshoz hosszú háromszögsávokat hoztunk létre egy számítási árnyékolóban, így számos szeletet hoztunk létre a világból. Bár a szélerősség nem volt túl nagy hatással a kitöltési sebességre a vékony sávok miatt, több százezer csúcsot generált, ami nagy terhelést eredményezett a csúcs árnyékolója számára.

A számítási árnyékolón hozzáfűző puffereket vezettünk be, amelyek a szélsávok egy részkészletét táplják meg. A számítási árnyékoló néhány egyszerű nézet frustum-számító logikájával megállapíthatnánk, hogy egy sáv a kameranézeten kívül esik-e, és megakadályozhatnánk, hogy hozzáadjuk a leküldéses pufferhez. Ez jelentősen csökkentette a sávok mennyiségét, így felszabadított néhány szükséges ciklust a GPU-val.

A hozzáfűző puffert bemutató kód:

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

Ugyanezt a technikát próbáltuk használni a felhőrészecskéken is, ahol a számítási árnyékolóra tuktuk őket, és csak a látható részecskéket tuktuk le renderelni. Ez a technika valójában nem takarított meg sokat a GPU-val, mivel a legnagyobb szűk keresztmetszet a képernyőn megjelenített képpontok mennyisége volt, és nem a csúcsok kiszámításának költsége.

A másik probléma ezzel a technikával az volt, hogy a hozzáfűző puffer véletlenszerű sorrendben lett feltöltve a részecskék számításának párhuzamos jellegéből adódóan, ami a rendezetlen részecskék rendezését okozza, ami a felhőrészecskék kioltóját eredményezi.

Vannak módszerek a leküldéses puffer rendezésére, de a részecskék kigyűjtésből kihozott teljesítmény-nyereséget valószínűleg eltenné egy további rendezés, ezért úgy döntöttünk, hogy nem alkalmazunk ilyen optimalizálást.

Adaptív renderelés

Annak érdekében, hogy állandó képkockaszintet biztosítsunk az alkalmazás változó renderelési feltételekkel, például felhős vagy tiszta nézettel, adaptív renderelést vezettünk be az alkalmazásunkhoz.

Az adaptív renderelés első lépése a GPU mérése. Ezt úgy végeztünk el, hogy egyéni kódot illeszttünk be a GPU-parancspufferbe egy renderelt képkocka elején és végén, és a képernyő bal és jobb oldali idejét is rögzítve.

A renderelési idő mérése és a kívánt frissítési sebességhez való hasonlítása azt is érzékeltetjük, hogy milyen közel vagyunk a képkockák eldobáshoz.

Ha közel van a képkockák eldobása, a renderelést a gyorsabb renderelés érdekében adaptáljuk. Az adaptálás egyik egyszerű módja a képernyő nézőpontméretének módosítása, amely kevesebb képpontot igényel a rendereléshez.

A UnityEngine.XR.XRSettings.renderViewportScale használatával a rendszer zsugorít a célként megcélzott nézetportra, és automatikusan kinyújtja az eredményt, hogy illeszkedjen a képernyőhöz. A skálán egy kis változás észrevehető a világgeometrián, és a 0,7-es mérettényező a renderelt képpontok felének felét igényli.

70%-os skálázás, a képpontok fele

Amikor azt észleljük, hogy a képkockákat el szeretnénk dobni, a skálát egy rögzített számmal csökkentjük, majd akkor növeljük, ha ismét elég gyorsan futunk.

Bár az indításkor eldöntöttük, hogy milyen felhőalapú technikát kell használni a hardver grafikus képességei alapján, a GPU-mérésből származó adatokra alapozva megakadályozható, hogy a rendszer hosszú ideig alacsony felbontásban maradjon, de erre nem volt ideje az Adatszekésésben.

Végső gondolat

A különböző hardverek megcélzása kihívást jelent, és némi tervezést igényel.

Javasoljuk, hogy kezdje meg az alacsonyabb teljesítményű gépek megcélzását, hogy megismerkedjen a problémás területekkel, és fejlessze ki az összes gépen futó biztonsági mentési megoldást. Tervezheti meg a megoldást a kitöltési sebességet szem előtt tartva, mivel a képpontok lesznek a legbecsesebb erőforrások. Folytonos geometria megcélzott átlátszóság fölött.

A biztonsági mentési megoldással összetettebb rétegezést kezdhet a csúcsgépeken, vagy akár csak javíthatja a biztonsági mentési megoldás megoldásának megoldását.

Tervezzetek a legrosszabb esetekre, és érdemes lehet adaptív renderelést használni a nehéz helyzetekben.

A szerzőkről

Robert Kellese képe Robert Fogese
Szoftvermérnöki @Microsoft
Dan Andersson képe Dan Andersson
Szoftvermérnöki @Microsoft

Lásd még