Analiza przypadku — skalowanie aplikacji między urządzeniami z różnymi możliwościami

W tym badaniu przypadku opisano, jak aplikacja Windows Mixed Reality może kierować różne platformy o różnych możliwościach sprzętowych. Datascape to aplikacja Windows Mixed Reality, która wyświetla dane pogodowe na podstawie danych terenowych. Aplikacja otacza użytkowników wizualizacjami danych holograficznej. Użytkownicy mogą eksplorować unikatowe szczegółowe informacje, które uzyskują, odnajdując dane w rzeczywistości mieszanej.

Aplikacja Datascape jest przeznaczona Microsoft HoloLens, Windows Mixed Reality immersywny zestaw słuchawkowy, niższe komputery i komputery o wysokiej wydajności. Głównym wyzwaniem było renderowanie wizualnie atrakcyjnej sceny, podczas gdy wykonywane z dużą szybkością klatek na urządzeniach z szalenie różnymi możliwościami wydajności.

W tym badaniu przypadku przedstawiono proces i techniki, które użyliśmy do tworzenia niektórych systemów intensywnie korzystających z wydajności, zwłaszcza renderowania pogody, takich jak chmury. Opisujemy napotkane problemy i sposób ich pokonania.

Aby uzyskać więcej informacji na temat zagadnień dotyczących wydajności dla rzeczywistości mieszanej i aplikacji aparatu Unity, zobacz:

Omówienie analizy przypadku

Oto kilka informacji na temat aplikacji Datascape i wyzwań.

Przezroczystość i przerysowanie

Nasze główne problemy z renderowaniem zajmowały się przejrzystością, ponieważ przejrzystość może być kosztowna.

Możesz renderować solidną geometrię z tyłu podczas zapisywania w buforze głębokości, co uniemożliwia renderowanie wszystkich przyszłych pikseli znajdujących się za tym pikselem. Ta operacja zapobiega wykonywaniu cieniowania pikseli przez ukryte piksele i znacznie przyspiesza renderowanie. Jeśli posortujesz geometrię optymalnie, każdy piksel na ekranie rysuje się tylko raz.

Przezroczysta geometria musi być posortowana z powrotem do przodu i opiera się na połączeniu danych wyjściowych cieniowania pikseli z bieżącym pikselem na ekranie. Ten proces może spowodować, że każdy piksel na ekranie jest rysowany wiele razy na ramkę o nazwie overdraw.

W przypadku urządzeń HoloLens i komputerów głównych można wypełnić ekran tylko kilka razy, co sprawia, że renderowanie przezroczyste jest problematyczne.

Składniki sceny danych

Scena datascape ma trzy główne składniki: interfejs użytkownika, mapę i pogodę. Wiedzieliśmy, że efekty pogodowe będą potrzebować wszystkich wydajności, które mogą uzyskać, więc zaprojektowaliśmy interfejs użytkownika i mapujemy, aby zmniejszyć narysowanie.

Przerobiliśmy interfejs użytkownika kilka razy, aby zminimalizować ilość przerysowania. W przypadku składników, takich jak świecące przyciski i przeglądy mapy, wybraliśmy bardziej złożoną geometrię, a nie nakładając przezroczystą sztukę.

Na mapie użyliśmy niestandardowego cieniowania, który pozbawił standardowe funkcje aparatu Unity, takie jak cienie i złożone oświetlenie. Niestandardowy cieniator zastąpił te funkcje prostym, pojedynczym modelem oświetlenia słonecznego i niestandardowym obliczeniem mgły. Ten prosty cień pikseli poprawił wydajność.

Mamy zarówno interfejs użytkownika, jak i mapę do renderowania w budżecie, więc nie potrzebują żadnych zmian zależnych od sprzętu. Wizualizacja pogody, zwłaszcza renderowanie chmury, była trudniejsza.

Dane w chmurze

Dane w chmurze pobrane z serwerów NOAA w trzech różnych warstwach 2D. Każda warstwa miała górną i dolną wysokość chmury oraz gęstość chmury dla każdej komórki siatki. Przetworzyliśmy dane w teksturze informacji o chmurze, która przechowywała każdy składnik w kolorze czerwonym, zielonym i niebieskim składniku tekstury.

Tworzenie chmur geometrycznych

Aby upewnić się, że maszyny o niższej mocy mogą renderować chmury, nasze podejście do tworzenia kopii zapasowej używało stałej geometrii w celu zminimalizowania przerysowania.

Wygenerowaliśmy chmury przez wygenerowanie siatki stałej mapy wysokości dla każdej warstwy. Użyliśmy promienia tekstury informacji o chmurze na wierzchołek, aby wygenerować kształt. Użyliśmy cieniowania geometrii do tworzenia wierzchołków u gór i dołu chmur, generując kształty chmury stałej. Użyliśmy wartości gęstości z tekstury do koloru chmury z ciemniejszymi kolorami dla gęstszych chmur.

Poniższy kod cieniowania tworzy wierzchołki:

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

Wprowadziliśmy mały wzorzec szumu, aby uzyskać więcej szczegółowych informacji na temat rzeczywistych danych. Aby utworzyć zaokrąglone krawędzie chmury, odrzuciliśmy wartości zbliżone do zera, przycinając piksele w cieniatorze pikseli, gdy wartość interpolowanego promienia osiągnie próg.

Ponieważ chmury są solidną geometrią, mogą być renderowane przed renderowaniem terenu. Ukrywanie drogich pikseli mapy pod chmurami dodatkowo poprawia szybkość klatek. Ze względu na podejście do renderowania geometrii stałej to rozwiązanie działa dobrze na wszystkich kartach graficznych, od minimalnej specyfikacji do wysokiej klasy kart graficznych i na urządzeniu HoloLens.

Obraz przedstawiający chmury geometryczne.

Używanie chmur cząstek stałych

Nasze rozwiązanie stworzyło przyzwoitą reprezentację danych w chmurze, ale było nieco słabe. Renderowanie w chmurze nie przekazało dźwięku woluminowego, którego chcieliśmy dla naszych maszyn wysokiej klasy. Następnym krokiem było stworzenie bardziej organicznego i objętościowego wyglądu, reprezentując chmury z około 100 000 cząstek.

Jeśli cząstki pozostają stałe i sortowane z przodu do tyłu, nadal korzystasz z buforu głębi za wcześniej renderowanymi cząstkami, zmniejszając przerysowanie. Ponadto rozwiązanie oparte na cząstkach może zmienić liczbę cząstek w celu kierowania innego sprzętu. Jednak wszystkie piksele nadal muszą być przetestowane w głębi, co powoduje większe obciążenie.

Najpierw utworzyliśmy pozycje cząstek wokół centrum doświadczenia podczas uruchamiania. Rozkładaliśmy cząstki bardziej gęsto wokół środka i mniej w odległości. Wstępnie posortowaliśmy wszystkie cząstki z środka do tyłu, więc najbliższe cząstki renderowane najpierw.

Cieniator obliczeniowy próbkował teksturę informacji o chmurze, aby umieścić każdą cząstkę w odpowiedniej wysokości i kolorować ją na podstawie gęstości. Każda cząstka zawierała zarówno wysokość, jak i promień. Wysokość została oparta na danych w chmurze próbkowanych z tekstury informacji o chmurze. Promień był oparty na początkowym rozkładzie, który obliczał i przechowywał odległość poziomą do najbliższego sąsiada.

Użyliśmy drawProcedural , aby renderować czworokąt na cząstkę. Czworokąta używały tych danych do samodzielnego zorientowania się pod kątem wysokości. Gdy użytkownicy patrzą na cząstkę poziomo, pokazuje wysokość. Gdy użytkownicy patrzą na cząstkę w górę w dół, obszar między nim a sąsiadami jest pokryty.

Diagram przedstawiający kształt i pokrycie cząstek.

Poniższy kod cieniowania przedstawia rozkład:

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

Posortowaliśmy cząstki z przodu do tyłu i nadal używaliśmy solidnego cieniowania stylu do przycinania przezroczystych pikseli, a nie mieszać ich. Ta technika obsługuje dużą liczbę cząstek nawet na maszynach o niższej mocy, unikając kosztownego narysowania.

Wypróbuj chmury przezroczystych cząstek

Stałe cząstki dostarczyły organicznego poczucia kształtów chmury, ale nadal potrzebowały czegoś, aby uchwycić puszystość chmur. Postanowiliśmy wypróbować niestandardowe rozwiązanie dla wysokiej klasy kart graficznych, które wprowadza przezroczystość. Po prostu zmieniliśmy początkową kolejność sortowania cząstek i zmieniliśmy cieniowanie, aby używać tekstur alfa.

Obraz przedstawiający puszyste chmury.

To rozwiązanie wyglądało świetnie, ale okazało się zbyt ciężkie dla nawet najtrudniejszych maszyn. Każdy piksel musi być renderowany na ekranie setki razy.

Renderuj ekran offscreen z niższą rozdzielczością

Aby zmniejszyć liczbę pikseli renderowania chmur, renderowaliśmy je w buforze, który był jedną czwartą rozdzielczości ekranu. Rozciągnięliśmy wynik końcowy z powrotem na ekran po rysunku wszystkich cząstek.

Poniższy kod przedstawia renderowanie poza ekranem:

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

To rozwiązanie przyspieszyło przetwarzanie cztery razy, ale miało kilka zastrzeżenia. Najpierw podczas renderowania w buforze offscreen utraciliśmy wszystkie informacje o głębi z naszej sceny głównej. Cząstki za górami renderowane na szczycie góry.

Po drugie, rozciągnięcie buforu wprowadził artefakty na krawędziach chmur, gdzie zmiana rozdzielczości była zauważalna. W kolejnych dwóch sekcjach opisano sposób rozwiązywania tych problemów.

Używanie buforu głębokości cząstek

Musieliśmy sprawić, aby cząstki współistnieły z geometrią świata, gdzie góra lub obiekt pokryte cząstkami za nim. Wypełniliśmy bufor offscreen buforem głębokości , który zawierał geometrię sceny głównej. Aby utworzyć bufor głębokości, utworzyliśmy drugi aparat, który renderował tylko solidną geometrię i głębokość sceny.

Użyliśmy nowej tekstury w cieniatorze pikseli chmury do oklude pikseli. Użyliśmy tej samej tekstury, aby obliczyć odległość do geometrii za pikselem chmury. Korzystając z tej odległości i stosując ją do alfa piksela, osiągnęliśmy efekt blaknięcie chmur, gdy zbliżają się do terenu. Ten efekt usuwa wszelkie twarde cięcia, w których spotykają się cząstki i teren.

Obraz przedstawiający chmury połączone z terenem.

Wyostrzenie krawędzi

Rozciągnięte chmury wyglądały niemal identycznie z chmurami o normalnym rozmiarze w centrach cząstek lub tam, gdzie nakładały się, ale pokazały niektóre artefakty na krawędziach chmury. Ostre krawędzie pojawiły się rozmyte, a ruch kamery wprowadził efekty aliasu.

Aby rozwiązać ten problem, wykonaj następujące zadania:

  1. Uruchom prostą cieniowanie w buforze offscreen, aby określić, gdzie wystąpiły duże zmiany kontrastu.
  2. Umieść piksele z dużymi zmianami w nowym buforze wzornika.
  3. Użyto buforu wzornika, aby zamaskować te obszary o wysokim kontraście podczas stosowania buforu offscreen z powrotem do ekranu, co powoduje otwory w chmurach i wokół nich.
  4. Renderowane wszystkie cząstki ponownie w trybie pełnoekranowym, używając buforu wzornika, aby zamaskować wszystko, ale krawędzie, co spowodowało minimalny zestaw pikseli dotknął. Ponieważ już utworzyliśmy bufor poleceń do renderowania cząstek, po prostu renderowaliśmy go ponownie do nowego aparatu.

Obraz przedstawiający postęp renderowania krawędzi chmury.

Wynik końcowy był ostrymi krawędziami z tanimi środkowymi częściami chmur. Chociaż to rozwiązanie jest znacznie szybsze niż renderowanie wszystkich cząstek na pełnym ekranie, nadal jest koszt testowania pikseli względem buforu wzornika. Ogromna ilość przerysowania jest nadal kosztowna.

Cząstki cull

W przypadku efektu wiatru wygenerowaliśmy długie paski trójkątów w cieniowaniu obliczeniowym, tworząc wiele wisps wiatru na świecie. Efekt wiatru nie był ciężki w szybkości wypełnienia, ze względu na wąskie paski. Jednak wiele setek tysięcy wierzchołków spowodowało duże obciążenie cieniowania wierzchołków.

Aby zmniejszyć obciążenie, wprowadziliśmy bufory dołączania do cieniowania obliczeniowego, aby nadawać podzestaw pasów wiatrowych do narysowania. Użyliśmy prostej logiki buforowania frustum widoku w cieniowaniu obliczeniowym, aby ustalić, czy pasek był poza widokiem kamery i uniemożliwił dodanie tych pasów do buforu wypychania. Ten proces znacznie zmniejszył liczbę pasków, zwiększając wydajność.

Poniższy kod przedstawia bufor dołączania.

Cieniator obliczeniowy:

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

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

Próbowaliśmy tej techniki na cząstkach chmury, ułuszając je w cieniowaniu obliczeniowym i wypychając tylko widoczne cząstki do renderowania. Ale nie zaoszczędziliśmy dużo przetwarzania, ponieważ największym wąskim gardłem było liczba pikseli chmury do renderowania na ekranie, a nie koszt obliczania wierzchołków.

Innym problemem było to, że bufor dołączania wypełniany w kolejności losowej, ze względu na równoległe przetwarzanie cząstek. Posortowane cząstki stały się niesortowane, co spowodowało migotanie cząstek chmury. Istnieją techniki sortowania buforu wypychania, ale ograniczona ilość zysków wydajności z cząstek culling prawdopodobnie zostanie przesunięta przez inny rodzaj. Postanowiliśmy nie prowadzić tej optymalizacji dla cząstek chmury.

Korzystanie z renderowania adaptacyjnego

Aby zapewnić stałą szybkość klatek w aplikacji ze zróżnicowanymi warunkami renderowania, takimi jak chmurowy i jasny widok, wprowadziliśmy renderowanie adaptacyjne.

Pierwszym krokiem renderowania adaptacyjnego jest mierzenie wydajności. Wstawiliśmy kod niestandardowy do buforu poleceń na początku i na końcu renderowanej ramki, aby przechwycić czas ekranu po lewej i prawej stronie.

Porównaj czas renderowania z żądaną szybkością odświeżania, aby pokazać, jak blisko zbliżasz się do upuszczania ramek. Gdy zbliżasz się do upuszczania ramek, możesz dostosować renderowanie tak, aby było szybsze.

Jednym z prostych sposobów dostosowania renderowania jest zmiana rozmiaru widoku ekranu, dzięki czemu do renderowania jest mniejsza liczba pikseli. System używa aparatu UnityEngine.XR.XRSettings.renderViewportScale , aby zmniejszyć docelowy port widoku i automatycznie rozciągnąć wynik w celu dopasowania do ekranu. Niewielka zmiana skali jest ledwo zauważalna w geometrii świata, a współczynnik skali 0,7 wymaga połowy liczby pikseli do renderowania.

Obraz przedstawiający 70% skalowania z połowę pikseli.

Gdy wykryjemy, że ramki są porzucane, obniżamy skalę o stały współczynnik i przywracamy ją, gdy działamy wystarczająco szybko.

W tym badaniu przypadku postanowiliśmy, która technika chmury ma być używana na podstawie możliwości graficznych sprzętu podczas uruchamiania. Można również opierać tę decyzję na temat danych z pomiarów wydajności, aby zapobiec pozostaniu systemu w niskiej rozdzielczości przez długi czas.

Zalecenia

Określanie różnych możliwości sprzętowych jest trudne i wymaga planowania. Poniżej przedstawiono kilka rekomendacji:

  • Zacznij kierowania do maszyn o niższej mocy, aby zapoznać się z obszarem problemu.
  • Opracowywanie rozwiązania do tworzenia kopii zapasowych działającego na wszystkich maszynach. Następnie można warstwować w większej złożoności dla maszyn wysokiej klasy lub zwiększyć rozdzielczość rozwiązania do tworzenia kopii zapasowych.
  • Zaprojektuj rozwiązanie, mając na uwadze szybkość wypełniania, ponieważ piksele są twoim najcenniejszym zasobem.
  • Ukierunkowanie stałej geometrii na przezroczystość.
  • Projektowanie scenariuszy o najgorszych przypadkach i rozważ użycie renderowania adaptacyjnego w ciężkich sytuacjach.

Informacje o autorach

Zdjęcie Roberta Ferrese Robert Ferrese
@Microsoft inżyniera oprogramowania
Zdjęcie Dan Andersson Dan Andersson
@Microsoft inżyniera oprogramowania

Zobacz też