Případová studie – škálování Datascape napříč zařízeními s jiným výkonem

Datascape je aplikace vyvinutá interně v microsoftu, kde Windows Mixed Reality se zaměřujeme na zobrazování dat o počasí nad daty o terénu. Aplikace bude prozkoumat jedinečné uživatele Insights, kteří získají data ve smíšené realitě, a to tím, že kolem uživatele dokončí vizualizaci holografických dat.

pro Datascape jsme chtěli cílit na nejrůznější platformy s různými hardwarovými možnostmi, které jsou od Microsoft HoloLens Windows Mixed Reality ponořené sluchátka, a z počítačů s nižší spotřebou až po nejnovější počítače s vysokým procesorem GPU. Hlavní otázkou bylo vykreslovat naši scénu na zařízeních s volně rostoucími možnostmi grafiky při provádění s vysokým snímkem.

Tato případová studie vás provede procesem a technikami, které vám pomůžou vytvořit některé z našich dalších systémů s vysokým PROCESORem, které popisují problémy, se kterými jsme narazili a jak je overcame.

Průhlednost a překreslování

Naše hlavní vykreslování potýká s transparentností, protože transparentnost může být nákladná na GPU.

Při zápisu do vyrovnávací paměti hloubky je možné vykreslit plnou geometrii a zastavovat všechny budoucí pixely umístěné za tímto pixelem v případě, že se zahodí. To brání tomu, aby skryté pixely neprováděly pixel shader, což výrazně zrychluje proces. Pokud je geometrie seřazena optimálně, každý pixel na obrazovce se vykreslí pouze jednou.

Transparentní geometrii je potřeba seřadit zpátky na začátek a spoléhá na to, že se výstup shaderu v obrazci vrátí na aktuální pixel na obrazovce. To může vést k tomu, že každý pixel na obrazovce se vykreslí na více než jednou za rámec, což se označuje jako překreslování.

v případě HoloLens a běžných počítačů se může obrazovka vyplňovat jenom několik, takže transparentní vykreslování bude problematické.

Úvod k komponentám scény Datascape

Naši scénu máme tři hlavní komponenty; uživatelské rozhraní, mapaa počasí. Věděli jsme se, že naše klimatické účinky by vyžadovaly veškerou dobu GPU, kterou by mohla získat, takže jsme záměrně navrhli uživatelské rozhraní a terén způsobem, který by omezil překreslování.

Několikrát jsme napracovali uživatelské rozhraní, aby se minimalizovalo množství překreslování. Erred jsme na straně složitější geometrie místo toho, aby byly na sebe navzájem překryty průhledné obrázky pro komponenty, jako jsou tlačítka záře a přehledy map.

Pro mapu jsme použili vlastní shader, který by provedl standardní funkce Unity, jako jsou stínové a komplexní osvětlení, a nahrazuje je jednoduchým modelem osvětlení Sun a vlastním běžným způsobem. Tím se vytvořila jednoduchá funkce pixel shaderu a uvolníte cykly GPU.

Spravovali jsme přístup k uživatelskému rozhraní i k mapě, abyste mohli vykreslit v rozpočtu, kdy v závislosti na hardwaru Nepotřebujeme žádné změny. vizualizace počasí, zejména vykreslování v cloudu, ukázala, že se jedná o větší část výzvy!

Pozadí dat v cloudu

Naše cloudová data se stáhla ze serverů NOAA ( https://nomads.ncep.noaa.gov/ ) a dostali se do nás ve třech různých 2D vrstvách, z nichž každá má nejvyšší a nejnižší výšku cloudu, a také hustotu cloudu pro každou buňku v mřížce. Data byla zpracována do struktury informací o cloudu, kde byla každá součást uložena v komponentě červené, zelené a modré komponenty textury pro snadný přístup k GPU.

Cloudy geometrie

Aby se zajistilo, že naše cloudy s nižším výkonem můžou vystavovat naše cloudy, rozhodli jsme se začít s přístupem, který by používal základní geometrii k minimalizaci překreslování.

Nejdřív jsme vyzkoušeli vytváření cloudů tak, že pro každou vrstvu vygenerujeme pevnou heightmapou síť pomocí poloměru informací o cloudové vrstvě na vrcholu a vygenerujete tvar. Pomocí geometrického shaderu jsme vygenerovali vrcholy jak v horní části, tak i v dolní části cloudu, která generuje tuhé cloudové obrazce. Použili jsme hodnotu hustoty z textury k obarvení cloudu s tmavšími barvami pro rozsáhlejší cloudy.

Shader pro vytváření vrcholů:

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

Představili jsme malý vzorek šumu, abyste získali více podrobností nad skutečná data. Abychom mohli vytvořit kulaté cloudové hrany, oříznou se pixely v pixel shaderu, pokud hodnota interpolované poloměru dosáhla prahové hodnoty pro zahození téměř nulových hodnot.

Cloudy geometrie

Vzhledem k tomu, že cloudy představují plnou geometrii, dají se vykreslit předtím, než terén skryje všechny nákladné pixely map pod a dále vylepšuje snímkový kmitočet. toto řešení dobře fungovalo na všech grafických kartách od minimálních grafických karet až po špičkové grafické karty a také na HoloLens z důvodu přístupu k plné geometrii vykreslování.

Cloudy tuhých částic

Teď máme řešení pro zálohování, které vytvořilo dát reprezentace našich cloudových dat, ale očekávalo se, že se lackluster v faktoru "Wow" a nedostala se do něj objemový dojem, který jsme chtěli pro naše špičkové počítače.

Náš další krok vytvořil cloudy tím, že je představuje přibližně 100 000 částic, aby se vytvořilější ekologický a objemový vzhled.

V případě, že částice zůstávají plné a řazení je zpožděno, můžeme i nadále využívat hloubku odstranení objemu pixelů za dříve vykreslenými částicemi, čímž se zmenší překreslování. V případě řešení založeného na částice můžeme také změnit množství částic používaných k zacílení na jiný hardware. Nicméně všechny pixely stále musí být testovány na hloubku, což vede k nějaké další režii.

Nejdříve jsme vytvořili umístění částic kolem středu prostředí při spuštění. Rozšíříme části částic mnohem hustě kolem středu a méně tak, jak je to možné. Předem jsme seřadili všechny částice ze středu až po zpátky, aby se nejdříve vygenerovaly nejbližší částice.

Výpočetní shader by měl vzorkovat texturu informací o cloudu, aby každou částici umístil správnou výšku a vybarvit ji na základě hustoty.

Použili jsme DrawProcedural k vykreslení Quad na částice, což umožňuje, aby data částic zůstala ve všech časech.

Každá částice obsahovala výšku i poloměr. Výška vychází z ukázkových dat v cloudu z textury cloudových informací a poloměr byl založen na počáteční distribuci, kde by se vypočítala jeho vodorovná vzdálenost k nejbližšímu sousednímu sousedovi. Ve čtverčíku by tato data používala k orientaci sebe sama na výšku, takže když se uživatelé budou zobrazovat vodorovně, zobrazí se výška, a když se uživatelé vyhledají shora dolů, bude se pokrýt oblast mezi okolními sousedními oblastmi.

Tvar částic

Kód shaderu znázorňující 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;
}

Vzhledem k tomu, že jsme naseřadili částic předem a pořád jsme pro klip (ne Blend) transparentní obrazové body využívali základní styl shaderu, tato technika zpracovává překvapivé množství částic a vyhne se nákladným překreslováním i na počítačích s nižší spotřebou.

Transparentní cloudy částic

Tuhé částice poskytují dobrý ekologický dojem na tvar cloudů, ale ještě potřebují pro prodej fluffinessy cloudů. Rozhodli jsme se vyzkoušet vlastní řešení pro špičkové grafické karty, kde můžeme zavádět transparentnost.

Pokud to chcete provést, jednoduše jsme přešli počáteční pořadí řazení částic a změnili shader na použití textur alfa.

Fluffy cloudy

Ukázala se, že je moc velká, ale nemusela by být příliš velká, protože by to mělo za následek vykreslování jednotlivých pixelů na obrazovce.

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

Chcete-li snížit počet pixelů vykreslených v cloudech, začali jsme je vykreslili ve vyrovnávací paměti pro rozlišení čtvrtletí (ve srovnání s obrazovkou) a po vykreslení všech částic se celý výsledek vrátí zpět na obrazovku. To nám poskytlo zhruba 4x zrychlení, ale přišel s několika upozorněními.

Kód pro vykreslování 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);

Za prvé, při vykreslování do vyrovnávací paměti mimo obrazovku jsme ztratili všechny informace o hloubkě z naší hlavní scény, což vede k tomu, že se v části Mountains vykreslování nachází nad horském pozadím.

Druhý, roztažení vyrovnávací paměti také zavedlo artefakty na hranách našich cloudů, ve kterých byla změna rozlišení poznatelný. V dalších dvou částech se dozvíte, jak tyto problémy vyřešili.

Vyrovnávací paměť hloubky částic

Pro zajištění koexistence částic společně s geometrií světa, kde může horská oblast nebo objekt krýt částice, jsme naplnili vyrovnávací paměť pro obrazovku s hloubkou vyrovnávací paměti obsahující geometrii hlavní scény. Pro vytvoření takové vyrovnávací paměti jsme vytvořili druhý fotoaparát a vykreslit pouze plnou geometrii a hloubku scény.

Pak jsme tuto novou texturu použili v obrazovém shaderu cloudu a occlude pixely. Použili jsme stejnou texturu k výpočtu vzdálenosti na geometrii za cloudovým pixelem. Když použijete tuto vzdálenost a použijete ji na alfa pixelů, máme teď vliv cloudů, protože se blíží k terénu a odstranili jsme jakékoli tvrdé kusy, kde se podčástice a terén dostanou.

Cloudy blendované do terénu

Zostření hran

Roztažené cloudy vypadají téměř stejně jako cloudy normální velikosti v centru částic nebo tam, kde se překrývají, ale ukázaly některé artefakty na okrajích cloudu. Jinak ostré hrany by se zobrazily jako fuzzy a při přesunu kamery byly zavedeny efekty aliasu.

Vyřešili jsme to spuštěním jednoduchého shaderu na vyrovnávací paměti mimo obrazovku, abyste zjistili, kde došlo k velkým změnám v kontrastu (1). Do nové vyrovnávací paměti vzorníku jsme umístili pixely s velkým objemem změn (2). Pak jsme použili vyrovnávací paměť vzorníku k maskování těchto oblastí s vysokým kontrastem při použití vyrovnávací paměti mimo obrazovku zpátky na obrazovku, což má za následek díry v a v okolí cloudů (3).

Všechny částice pak jsme znovu vykreslili v režimu celé obrazovky, ale tentokrát se použila vyrovnávací paměť vzorníku k maskování všeho, ale jeho okrajů, což vede k minimální sadě pixelů (4). Vzhledem k tomu, že pro částice byly již vytvořeny vyrovnávací paměti příkazů, museli jsme je znovu vykreslit do nové kamery.

Průběh vykreslování okrajů v cloudu

Konečný výsledek byl ostrými okraji v cloudových oddílech s levnými centry.

I když je to mnohem rychlejší než vykreslování všech částic na celé obrazovce, jsou stále náklady spojené s testováním pixelu proti vyrovnávací paměti vzorníku, takže obrovské množství překreslování se stále účtuje s náklady.

Odstranení částic

Pro náš efekt větru jsme vygenerovali dlouhé trojúhelníkové pásy ve výpočetním shaderu a vytvořili jsme spoustu Wispr větru na světě. I když efekt větru nebyl silný pro výplň, protože byly vygenerovány pruhy se změnami, vytvořila mnoho stovek tisíc vrcholů, což má za následek vysoké zatížení pro vertex shader.

Zavedli jsme připojení vyrovnávací paměti pro výpočetní shader, aby bylo možné vytvořit podmnožinu větrných proužků, které se mají vykreslit. Díky některému jednoduchému zobrazení frustum odstranení logiky ve výpočetním shaderu je možné určit, zda byl pruh mimo zobrazení kamery a zabránit jeho přidání do nabízené vyrovnávací paměti. Tím se výrazně snížilo množství pruhů, které uvolní potřebné cykly na GPU.

Kód, který demonstruje připojené vyrovnávací paměť:

Shader Compute:

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

Kód jazyka 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);
}

Snažili jsme se použít stejnou techniku v části cloudu, kde bychom je nakreslili na výpočetním shaderu a najdou jenom viditelné částice, které se mají vykreslovat. Tato technika skutečně neušetřila hodně na GPU, protože největší kritické body představovaly množství pixelů na obrazovce a ne náklady na výpočet vrcholů.

Dalším problémem s touto technikou bylo, že vyrovnávací paměť pro připojení naplněná z náhodného pořadí způsobila jejich paralelismuější charakter, což způsobí, že seřazené částice budou odtříděny, což vede ke blikání částic v cloudu.

K dispozici jsou postupy pro řazení nabízené vyrovnávací paměti, ale omezené množství zvýšení výkonu, které jsme získali z odvrácenosti částic, by pravděpodobně bylo posunuto s dodatečným řazením, takže jsme se rozhodli tuto optimalizaci Nesledovat.

Adaptivní vykreslování

Pro zajištění stabilní snímkové rychlosti aplikace s různými podmínkami vykreslování, jako je cloudové a jasné zobrazení, jsme do naší aplikace zavedli adaptivní vykreslování.

Prvním krokem adaptivního vykreslování je měření GPU. To jsme udělali vložením vlastního kódu do vyrovnávací paměti příkazů GPU na začátku a na konci vykreslených snímků a zachytáváním času obrazovky levého i pravého oka.

Měřením času stráveného vykreslováním a porovnáním s požadovanou obnovovací rychlostí jsme získali pocit, jak blízko jsme zahazování snímků.

Když se blížíme vyhazování snímků, přizpůsobili jsme vykreslování tak, aby bylo rychlejší. Jedním jednoduchým způsobem přizpůsobení je změna velikosti zobrazení obrazovky, která k vykreslení vyžaduje méně pixelů.

Pomocí UnityEngine.XR.XRSettings.renderViewportScale systém zmenší cílový port zobrazení a automaticky roztáhne výsledek zpět, aby se vešel na obrazovku. Malá změna měřítka je na světovou geometrii patrná a faktor škálování 0,7 vyžaduje vykreslení polovičního množství pixelů.

70% měřítko, polovina pixelů

Když zjistíme, že se chystáme vypustit snímky, snížíme měřítko o pevné číslo a zvýšíme ho zpět, až budeme znovu dostatečně rychle běžet.

I když jsme se rozhodli, jakou cloudovou techniku použít na základě grafických funkcí hardwaru při spuštění, je možné ji založit na datech z měření GPU, aby se zabránilo tomu, aby systém zůstal po dlouhou dobu v nízkém rozlišení, ale to je něco, co jsme v Datascape nezískali.

Závěrečné myšlenky

Cílení na nejrůznější hardware je náročné a vyžaduje určité plánování.

Doporučujeme začít se zaměřovat na počítače s nižším napájením, abyste se seznámili s problémem a vyvinuli řešení zálohování, které bude běžet na všech vašich počítačích. Navrhovat řešení s myslenou rychlostí vyplňování, protože pixely budou vaším nejcennějším zdrojem informací. Cílení plné geometrie nad průhledností

S řešením zálohování pak můžete začít vrstvit vrstvení u počítačů vyššího rozsahu nebo můžete vylepšit řešení zálohování.

Navrhujte v nejhorších scénářích a možná zvažte použití adaptivního vykreslování pro náročné situace.

O autorech

Obrázek RobertaFerreseho Robert Ferrese
Software engineer @Microsoft
Obrázek Dana Anderssona Dan Andersson
Software engineer @Microsoft

Viz také