Sdílet prostřednictvím


Případová studie – Škálování aplikací na různých zařízeních s různými možnostmi

Tato případová studie popisuje, jak může aplikace Windows Mixed Reality cílit na různé platformy s různými hardwarovými možnostmi. Datascape je Windows Mixed Reality aplikace, která zobrazuje data o počasí nad daty terénu. Aplikace obklopuje uživatele vizualizacemi holografických dat. Uživatelé můžou zkoumat jedinečné přehledy, které získají tím, že objeví data v hybridní realitě.

Aplikace Datascape cílí na Microsoft HoloLens, Windows Mixed Reality imerzivní náhlavní soupravy, počítače s nižším výkonem a vysoce výkonné počítače. Hlavní výzvou bylo vykreslení vizuálně atraktivní scény při provádění s vysokou snímkovou frekvencí na zařízeních s výrazně odlišnými výkonovými schopnostmi.

Tato případová studie vás provede procesem a technikami, které jsme použili k vytvoření některých systémů s vyšší náročností na výkon, zejména vykreslování počasí, jako jsou mraky. Popíšeme problémy, se kterými jsme se setkali, a způsob, jakým jsme je překonali.

Další informace o aspektech výkonu pro hybridní realitu a aplikace Unity najdete tady:

Přehled případové studie

Tady najdete základní informace o aplikaci Datascape a výzev.

Průhlednost a překreslení

Naše hlavní problémy s vykreslováním se zabývaly transparentností, protože průhlednost může být nákladná.

Při zápisu do vyrovnávací paměti hloubky můžete vykreslit plnou geometrii zepředu dozadu, což zabrání vykreslování všech budoucích pixelů umístěných za tímto pixelem. Tato operace zabraňuje spuštění shaderu pixelů skrytým pixelům a výrazně zrychluje vykreslování. Pokud geometrii řadíte optimálně, nakreslí se každý pixel na obrazovce jenom jednou.

Průhledná geometrie musí být seřazena dopředu a závisí na prolnutí výstupu shaderu pixelů s aktuálním pixelem na obrazovce. Tento proces může vést k tomu, že se každý pixel na obrazovce nakreslí několikrát za snímek, který se označuje jako překreslení.

U HoloLensu a běžných počítačů můžete obrazovku vyplnit jenom několikrát, takže transparentní vykreslování bude problematické.

Komponenty scény datového panoramatu

Scéna Datascape má tři hlavní komponenty: uživatelské rozhraní, mapu a počasí. Věděli jsme, že efekty počasí budou vyžadovat veškerý výkon, který by mohly dosáhnout, a proto jsme navrhli uživatelské rozhraní a mapu tak, abychom omezili překreslení.

Uživatelské rozhraní jsme několikrát přepracovali, abychom minimalizovali překreslení. Pro komponenty, jako jsou zářící tlačítka a přehledy map, jsme se rozhodli použít složitější geometrii místo překrytí průhledných obrázků.

Pro mapu jsme použili vlastní shader, který odstranil standardní funkce Unity, jako jsou stíny a komplexní osvětlení. Vlastní shader nahradil tyto funkce jednoduchým modelem s jedním slunečním osvětlením a vlastním výpočtem mlhy. Tento jednoduchý pixel shader zlepšil výkon.

Uživatelské rozhraní i mapu jsme získali k vykreslení podle rozpočtu, takže nepotřebovali žádné změny závislé na hardwaru. Vizualizace počasí, zejména vykreslování cloudu, byla náročnější.

Cloudová data

Cloudová data stahovaná ze serverů NOAA ve třech různých 2D vrstvách. Každá vrstva měla pro každou buňku mřížky horní a dolní výšku cloudu a hustotu mraku. Data jsme zpracovali do textury informací o cloudu, která uložila každou komponentu v červené, zelené a modré komponentě textury.

Vytváření geometrických cloudů

Abychom zajistili, že počítače s nižším napájením můžou vykreslovat cloudy, použil náš přístup k zálohování pevnou geometrii, aby se minimalizovalo překreslení.

Vytvořili jsme mraky tak, že pro každou vrstvu vygenerujeme pevnou síť s mapou výšky. K vygenerování obrazce jsme použili poloměr textury informací o cloudu na vrchol. Pomocí shaderu geometrie jsme vytvořili vrcholy v horních a dolních vrstvách mraků, které generovaly plné tvary mraků. Použili jsme hodnotu hustoty z textury k obarvení mraku tmavšími barvami pro hustší mraky.

Následující kód shaderu vytvoří vrcholy:

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

Zavedli jsme malý šumový vzor, abychom získali více podrobností o skutečných datech. Abychom vytvořili kulaté okraje mraků, zahodili jsme téměř nulové hodnoty oříznutím pixelů v pixelovém shaderu, když interpolovaná hodnota poloměru dosáhla prahové hodnoty.

Vzhledem k tomu, že mraky mají pevnou geometrii, mohou se vykreslit před vykreslením terénu. Skrytí drahých pixelů mapy pod mraky dále zlepšuje snímkovou frekvenci. Vzhledem k přístupu vykreslování plné geometrie fungovalo toto řešení dobře na všech grafických kartách, od minimálních specifikací až po grafické karty vyšší úrovně, a na HoloLensu.

Obrázek znázorňující geometrické mraky

Použití mraků pevných částic

Naše řešení vytvořilo slušnou reprezentaci cloudových dat, ale bylo trochu nevýrazné. Vykreslování v cloudu nevystihovělo objemový dojem, který jsme chtěli pro naše počítače vyšší úrovně. Naším dalším krokem bylo vytvoření organického a objemnějšího vzhledu znázorněním mraků s přibližně 100 000 částicemi.

Pokud částice zůstávají pevné a třídí se zepředu dozadu, stále těžíte z hloubkové vyrovnávací paměti, která se vytěsní za dříve vykreslenými částicemi, což snižuje přetěžování. Řešení založené na částicech také může změnit počet částic tak, aby se zaměřovaly na jiný hardware. Všechny pixely však musí být i nadále testovány do hloubky, což způsobuje větší režii.

Nejprve jsme vytvořili pozice částic kolem středu prostředí při spuštění. Částice jsme distribuují hustěji kolem středu a méně v dálce. Předem jsme seřadili všechny částice od středu dozadu, takže nejbližší částice se vykreslily jako první.

Výpočetní shader vzorkoval texturu informací o mraku, aby umístil každou částci ve správné výšce a vybarvoval ji na základě hustoty. Každá částice obsahovala výšku i poloměr. Výška byla založena na datech cloudu vzorkovaných z textury informací o cloudu. Poloměr byl založen na počátečním rozdělení, které vypočítalo a uložilo vodorovnou vzdálenost k nejbližšímu sousedu.

Použili jsme DrawProcedural k vykreslení čtyřúhelníku na částice. Čtyřúhelníky používaly tato data k orientaci, šikmé podle výšky. Když se uživatelé dívají na částice vodorovně, ukazuje výšku. Když se uživatelé dívají na částice shora dolů, oblast mezi ní a jeho sousedy je pokryta.

Diagram znázorňující tvar částic a pokrytí

Následující kód shaderu ukazuje distribuci:

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

Seřadili jsme částice zepředu dozadu a stále jsme použili solid style shader, abychom ořízli průhledné pixely, ne je promíchali. Tato technika zpracovává velké množství částic i na počítačích s nižším výkonem a zabraňuje nákladnému překreslení.

Vyzkoušejte průhledné mraky částic

Pevné částice poskytovaly tvary mraků organické pocity, ale přesto potřebovaly něco, co by zachytilo nadýchanost mraků. Rozhodli jsme se vyzkoušet vlastní řešení pro grafické karty vyšší úrovně, které zavádí transparentnost. Jednoduše jsme přepnuli počáteční pořadí řazení částic a změnili shader tak, aby používal textury alfa.

Obrázek znázorňující nadýchané mraky

Toto řešení vypadalo skvěle, ale ukázalo se příliš těžké i pro ty nejtvrdší stroje. Každý pixel se musel na obrazovce vykreslit stokrát.

Vykreslení mimo obrazovku s nižším rozlišením

Abychom snížili počet pixelů pro vykreslování mraků, vykreslili jsme je ve vyrovnávací paměti, která byla čtvrtina rozlišení obrazovky. Po nakreslení všech částic jsme konečný výsledek roztáhli zpět na obrazovku.

Následující kód ukazuje vykreslení mimo obrazovku:

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

Toto řešení urychlilo zpracování čtyřnásobně, ale mělo několik upozornění. Za prvé, při vykreslování do vyrovnávací paměti mimo obrazovku jsme ztratili všechny podrobné informace z hlavní scény. Částice za horami vykreslené na vrcholu hory.

Za druhé, roztažení vyrovnávací paměti zavedlo artefakty na okrajích mraků, kde byla znatelná změna rozlišení. Následující dvě části popisují, jak jsme tyto problémy vyřešili.

Použití vyrovnávací paměti pro hloubku částic

Potřebovali jsme, aby částice existovaly společně se světovou geometrií, kde částečky za sebou pokrývala hora nebo objekt. Proto jsme vyrovnávací paměť mimo obrazovku naplnili hloubkovou vyrovnávací pamětí , která obsahovala geometrii hlavní scény. Abychom vytvořili hloubkovou vyrovnávací paměť, vytvořili jsme druhou kameru, která vykreslovala pouze plnou geometrii a hloubku scény.

Použili jsme novou texturu v pixelovém shaderu cloudu k okluzu pixelů. Stejnou texturu jsme použili k výpočtu vzdálenosti ke geometrii za pixelem mraku. Použitím této vzdálenosti a jejím použitím na alfa pixel jsme dosáhli efektu, že se mraky přiblíží k terénu. Tento efekt odstraní všechny tvrdé řezy, kde se setkávají částice a terén.

Obrázek znázorňující mračna v kombinaci s terénem

Zaostření okrajů

Roztažené mraky vypadaly téměř stejně jako mraky normální velikosti ve středech částic nebo tam, kde se překrývaly, ale na okrajích mraků se zobrazily některé artefakty. Ostré hrany vypadaly rozmazaně a pohyb kamery zavedl efekty aliasů.

Abychom tento problém vyřešili, provedeme následující:

  1. Spuštění jednoduchého shaderu na vyrovnávací paměti mimo obrazovku, abyste zjistili, kde došlo k velkým změnám kontrastu.
  2. Vložte pixely s velkými změnami do nové vyrovnávací paměti vzorníku.
  3. Pomocí vyrovnávací paměti vzorníku se tyto oblasti s vysokým kontrastem maskují při použití vyrovnávací paměti mimo obrazovku zpět na obrazovku, což vede k otvorům v oblačcích a kolem nich.
  4. Znovu vykreslí všechny částice v režimu zobrazení na celou obrazovku pomocí vyrovnávací paměti vzorníku k maskování všeho kromě okrajů, což vede k minimální sadě pixelů, kterých se dotknete. Vzhledem k tomu, že jsme již vytvořili vyrovnávací paměť příkazu pro vykreslení částic, jednoduše jsme ji znovu vykreslili do nové kamery.

Obrázek znázorňující průběh vykreslování okrajů cloudu

Výsledkem byly ostré hrany s levnými středovými částmi mraků. I když je toto řešení mnohem rychlejší než vykreslení všech částic na celou obrazovku, stále existují náklady na testování pixelů proti vyrovnávací paměti vzorníku. Obrovské množství překreslení je stále drahé.

Částice cull

Pro efekt větru jsme vygenerovali dlouhé trojúhelníkové pruhy ve výpočetním shaderu, čímž jsme vytvořili mnoho wisps větru na světě. Vliv větru nebyl kvůli úzkým pruhům silný. Mnoho stovek tisíc vrcholů však způsobilo velkou zátěž pro shader vrcholů.

Abychom snížili zatížení, zavedli jsme na výpočetní shader vyrovnávací paměti pro připojení, které nasypají podmnožinu větrných pásů, které se mají nakreslit. Použili jsme jednoduchou logiku vyřazování zobrazení ve výpočetním shaderu, abychom zjistili, jestli je pruh mimo pohled kamery, a zabránili jsme přidání těchto pruhů do vyrovnávací paměti. Tento proces výrazně snížil počet pásů a zlepšil výkon.

Následující kód ukazuje vyrovnávací paměť pro připojení.

Shader výpočetních prostředků:

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

Kód C#:

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

Tuto techniku jsme vyzkoušeli na částice mraku, vymačkovali je na výpočetním shaderu a tlačili pouze viditelné částice, které se mají vykreslit. Zpracování jsme ale moc neušetřili, protože největším kritickým bodem byl počet pixelů cloudu, které se mají vykreslit na obrazovce, a ne náklady na výpočet vrcholů.

Dalším problémem bylo, že vyrovnávací paměť připojení byla naplněna v náhodném pořadí kvůli paralelizovanému výpočtu částic. Seřazené částice se staly neseřazenými, což mělo za následek blikající oblačné částice. Existují techniky řazení vyrovnávací paměti, ale omezené množství výkonu při vyřazování částic by pravděpodobně bylo vyváženo jiným řazením. Rozhodli jsme se nepokračovat v této optimalizaci u částic cloudu.

Použití adaptivního vykreslování

Abychom zajistili stabilní snímkovou frekvenci v aplikaci s různými podmínkami vykreslování, jako je zamračení vs. jasné zobrazení, zavedli jsme adaptivní vykreslování.

Prvním krokem adaptivního vykreslování je měření výkonu. Do vyrovnávací paměti příkazů na začátku a konci vykresleného snímku jsme vložili vlastní kód, abychom zachytili čas u obrazovky levého i pravého oka.

Porovnejte dobu vykreslování s požadovanou obnovovací frekvencí, abyste ukázali, jak blízko jste k vyřazení snímků. Když se přiblížíte snímkům, můžete vykreslování přizpůsobit tak, aby bylo rychlejší.

Jedním z jednoduchých způsobů, jak přizpůsobit vykreslování, je změnit velikost zobrazení obrazovky tak, aby k vykreslení vyžadovalo méně pixelů. Systém používá UnityEngine.XR.XRSettings.renderViewportScale ke zmenšení cílového pohledu a automaticky roztáhne výsledek zpět nahoru tak, aby se vešel na obrazovku. Malá změna měřítka je na světové geometrii sotva patrná a faktor měřítka 0,7 vyžaduje polovinu počtu pixelů, které se mají vykreslit.

Obrázek znázorňující 70% měřítko s poloviční velikostí pixelů

Když zjistíme, že se chystáme snímky vyřadit, snížíme měřítko o pevný poměr a obnovíme ho, až budeme znovu běžet dostatečně rychle.

V této případové studii jsme se rozhodli, kterou cloudovou techniku použít na základě grafických možností hardwaru při spuštění. Toto rozhodnutí můžete také založit na datech z měření výkonu, abyste zabránili systému v nízkém rozlišení po dlouhou dobu.

Doporučení

Cílení na různé hardwarové možnosti je náročné a vyžaduje plánování. Tady je několik doporučení:

  • Začněte cílit na počítače s nižším výkonem, abyste se seznámili s problémovým prostorem.
  • Vytvořte řešení zálohování, které běží na všech vašich počítačích. Pak můžete vrstvit větší složitost pro počítače vyšší kategorie nebo vylepšit řešení zálohování.
  • Navrhněte řešení s ohledem na míru naplnění, protože pixely jsou vaším nejcennějším prostředkem.
  • Zaměřte se na pevnou geometrii nad průhledností.
  • Návrh pro nejhorší scénáře a zvažte použití adaptivního vykreslování v náročných situacích.

O autorech

Obrázek Robert Ferrese Robert Ferrese
@Microsoft softwarového inženýra
Obrázek Dana Anderssona Dan Andersson
@Microsoft softwarového inženýra

Viz také