Analiza przypadku — skalowanie aplikacji na urządzeniach przy użyciu różnych możliwości

W tej analizie przypadku opisano, jak aplikacja Windows Mixed Reality może być skierowana na różne platformy z różnymi możliwościami sprzętowymi. Datascape to aplikacja Windows Mixed Reality, która wyświetla dane pogodowe na podstawie danych dotyczących terenu. Aplikacja otacza użytkowników wizualizacjami danych holograficznych. 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 wykonywania z dużą szybkością klatek na urządzeniach z szalenie różnymi możliwościami wydajności.

Ta analiza przypadku przeprowadzi cię przez proces i techniki, które użyliśmy do utworzenia niektórych systemów intensywniejszych pod względem wydajności, zwłaszcza renderowania pogody, takich jak chmury. Opiszemy 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 podstawowe informacje 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 przodu do tyłu podczas zapisywania w buforze głębokości, co uniemożliwia renderowanie wszelkich przyszłych pikseli znajdujących się za tym pikselem. Ta operacja zapobiega wykonywaniu cieniowania pikseli przez ukryte piksele i znacznie przyspiesza renderowanie. W przypadku optymalnego sortowania geometrii każdy piksel na ekranie rysuje 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ć wielokrotne rysowanie każdego piksela na ekranie, nazywanego przerysowywaniem.

W przypadku HoloLens i głównych komputerów można wypełnić ekran tylko kilka razy, co sprawia problemy z przezroczystym renderowaniem.

Składniki sceny krajobrazu danych

Scena datascape ma trzy główne składniki: interfejs użytkownika, mapę i pogodę. Wiedzieliśmy, że efekty pogodowe będą wymagały całej wydajności, jaką mogą uzyskać, więc zaprojektowaliśmy interfejs użytkownika i mapę w celu zmniejszenia przerysowania.

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

W przypadku mapy użyliśmy niestandardowego cieniowania, który pozbawił standardowe funkcje aparatu Unity, takie jak cienie i złożone oświetlenie. Cieniowanie niestandardowe zastąpiło te funkcje prostym, pojedynczym modelem oświetlenia słonecznego i niestandardowym obliczeniem mgły. Ten prosty moduł cieniowania pikseli poprawił wydajność.

Mamy zarówno interfejs użytkownika, jak i mapę do renderowania w budżecie, więc nie potrzebowaliśmy ż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 odrębnych 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ższych mocy mogą renderować chmury, nasze podejście do tworzenia kopii zapasowych wykorzystało solidną geometrię w celu zminimalizowania przerysowania.

Utworzyliśmy chmury, generując stałą siatkę 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, aby utworzyć wierzchołki 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 wygenerować okrągłe krawędzie chmury, odrzuciliśmy wartości niemal zerowe, przycinając piksele w cieniowania pikseli, gdy wartość promienia interpolowanego osiągnie próg.

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

Image that shows geometry clouds.

Korzystanie z 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 wrażenia wolumetrycznego, którego chcieliśmy dla naszych maszyn wysokiej klasy. Następnym krokiem było stworzenie bardziej organicznego i wolumetrycznego 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 różnych sprzętu. Jednak wszystkie piksele nadal muszą być testowane głębokości, co powoduje większe obciążenie.

Najpierw stworzyliśmy pozycje cząstek wokół punktu środkowego doświadczenia podczas startu. Rozkładaliśmy cząstki bardziej gęsto wokół środka i mniej w odległości. Wstępnie posortowaliśmy wszystkie cząstki od środka do tyłu, więc najbliższe cząstki renderowane jako pierwsze.

Cieniowanie obliczeniowe próbkował teksturę informacji o chmurze, aby umieścić każdą cząstkę na 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 rozkładie początkowym, który obliczał i przechowywał odległość poziomą do najbliższego sąsiada.

Użyliśmy DrawProcedural do renderowania czworokąta na cząstkę. Czworokąta używały tych danych do orientowania się, pod kątem wysokości. Gdy użytkownicy patrzą na cząstkę poziomo, pokazuje wysokość. Gdy użytkownicy patrzą na cząstkę od góry w dół, obszar między nim a sąsiadami jest pokryty.

Diagram that shows particle shape and coverage.

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 od przodu do tyłu i nadal użyliśmy solidnego cieniowania stylu do przycinania przezroczystych pikseli, a nie mieszamy ich. Ta technika obsługuje dużą liczbę cząstek nawet na maszynach o niższych mocy, unikając kosztownego narysowania.

Wypróbuj przezroczyste chmury cząstek

Cząstki stałe dostarczyły organicznego uczucia do 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 przejrzystość. Po prostu zmieniliśmy początkową kolejność sortowania cząstek i zmieniliśmy cieniowanie, aby używać tekstur alfa.

Image that shows fluffy clouds.

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

Renderowanie ekranu poza ekranem 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 narysowaniu 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żeń. Po pierwsze, podczas renderowania w buforze offscreen straciliśmy wszystkie szczegółowe informacje z naszej sceny głównej. Cząstki za górami renderowane na szczycie góry.

Po drugie, rozciąganie buforu powodowało pojawienie się artefaktów 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óry lub obiekty pokryte cząstkami. Wypełniliśmy bufor offscreen buforem głębokości , który zawierał geometrię sceny głównej. Aby utworzyć bufor głębokości, utworzyliśmy drugą kamerę, która renderowała 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. Używając tej odległości i stosując ją do alfa piksela, osiągnęliśmy efekt blaknięcia chmur, gdy zbliżają się do terenu. Efekt ten usuwa wszelkie twarde kawałki, w których spotykają się cząstki i teren.

Image that shows clouds blended into terrain.

Wyostrzenie krawędzi

Rozciągnięte chmury wyglądały niemal identycznie z chmurami o normalnym rozmiarze w centrach cząstek lub tam, gdzie się nakładały, 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. Uruchomiono prostą cieniowanie w buforze offscreen, aby określić, gdzie wystąpiły duże zmiany.
  2. Umieść piksele z dużymi zmianami w nowym buforze wzornika.
  3. Użyto buforu wzornika, aby zamaskować te obszary o dużym 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 ponownie renderowaliśmy go do nowego aparatu.

Image showing the progression of rendering cloud edges.

Wynik końcowy był ostrymi krawędziami z tanimi sekcjami środkowymi chmur. Chociaż to rozwiązanie jest znacznie szybsze niż renderowanie wszystkich cząstek na pełnym ekranie, nadal jest koszt testowania pikseli przed buforem wzornika. Ogromna ilość nadrysowania 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 tempie 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 nakarmić podzbiór pasów wiatrowych do ściągnienia. Użyliśmy prostej logiki odstrzału widoku w cieniowaniu obliczeniowym, aby określić, czy pasek był poza widokiem aparatu, i uniemożliwiliśmy dodawanie tych pasów do buforu wypychania. Ten proces znacznie zmniejszył liczbę pasków, zwiększając wydajność.

Poniższy kod przedstawia bufor dołączania.

Cieniowania zasobów obliczeniowych:

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, wyłuskając je w cieniowaniu obliczeniowym i wypychając tylko widoczne cząstki do renderowania. Nie zaoszczędziliśmy jednak 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łniony w kolejności losowej, ze względu na przetwarzanie równoległe cząstek. Posortowane cząstki stały się niesortowane, co spowodowało migotanie cząstek chmury. Istnieją techniki sortowania buforu wypychania, ale ograniczona ilość zysku wydajności z cząstek culling prawdopodobnie zostanie zrównoważyna przez inny rodzaj. Postanowiliśmy nie realizować tej optymalizacji dla cząstek chmury.

Korzystanie z renderowania adaptacyjnego

Aby zapewnić stałą szybkość klatek w aplikacji ze zmiennymi 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ć zarówno czas lewego, jak i prawego ekranu oka.

Porównaj czas renderowania z żądaną częstotliwoś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, aby było szybsze.

Jednym z prostych sposobów dostosowania renderowania jest zmiana rozmiaru ekranu ekranu w celu renderowania mniejszej liczby pikseli. System używa aparatu UnityEngine.XR.XRSettings.renderViewportScale , aby zmniejszyć docelowy port widoków i automatycznie rozciągnie wynik w celu dopasowania go 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.

Image showing 70% scale, with half the pixels.

Gdy wykryjemy, że będziemy upuszczać ramki, zmniejszamy skalę o stały współczynnik i przywracamy ją, gdy będziemy działać wystarczająco szybko.

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

Zalecenia

Ukierunkowanie na różne możliwości sprzętowe jest trudne i wymaga planowania. Oto kilka zaleceń:

  • Zacznij kierować maszyny o niższej mocy, aby zapoznać się z przestrzenią problemu.
  • Opracowywanie rozwiązania do tworzenia kopii zapasowych działającego na wszystkich maszynach. Następnie można zwiększyć złożoność maszyn wysokiej klasy lub zwiększyć rozdzielczość rozwiązania do tworzenia kopii zapasowych.
  • Zaprojektuj rozwiązanie z uwzględnieniem współczynnika wypełnienia, ponieważ piksele są najcenniejszym zasobem.
  • Docelowa ciągła geometria nad przezroczystością.
  • Projektuj scenariusze najgorsze przypadki i rozważ użycie renderowania adaptacyjnego w ciężkich sytuacjach.

Informacje o autorach

Picture of Robert Ferrese Robert Ferrese
@Microsoft inżyniera oprogramowania
Picture of Dan Andersson Dan Andersson
@Microsoft inżyniera oprogramowania

Zobacz też