Powrót do Bloga

Optymalizacja mobilna w Unreal Engine: Uruchamianie MetaHumans i PCG w 60 FPS

Opublikowano 19 czerwca 2026
Optymalizacja mobilna w Unreal Engine: Uruchamianie MetaHumans i PCG w 60 FPS

W skrócie

Artykuł omawia kluczowe techniki optymalizacji zaawansowanych funkcji Unreal Engine, takich jak MetaHumans, PCG i Substrate, pod kątem urządzeń mobilnych. Autorzy wyjaśniają sposoby na pokonanie ograniczeń sprzętowych poprzez dostosowanie limitów kości w skinningu, zamianę strandów na hair cards oraz pre-bake'owanie grafów PCG do formatu HISM. Przedstawiono również produkcyjny skrypt w C++ automatyzujący wdrażanie optymalizacji w runtime oraz korzyści z gotowej infrastruktury backendowej w grach multiplayer.

Bariera mobilna: Przenoszenie oprawy wizualnej next-gen na urządzenia mobilne

Twój piękny projekt PC działa w płynnych 120 FPS na stacji roboczej, ale w momencie, gdy testujesz pakiet mobilny, liczba klatek na sekundę spada do wartości jednocyfrowych, docelowy telefon nagrzewa się, a GPU crashuje z powodu skinning buffer overflow. Zaawansowane funkcje, takie jak MetaHumans, Procedural Content Generation (PCG) i materiały Substrate, wyglądają oszałamiająco na PC i konsolach, ale są znane z tego, że potrafią rzucić urządzenia mobilne na kolana. Mobilne układy GPU i CPU działają w mocno ograniczonych budżetach termicznych i energetycznych, gdzie przepustowość pamięci (memory bandwidth) jest towarem deficytowym. Dostosowanie tych rozwiązań next-gen do wdrożenia mobilnego to nie tylko kwestia zaznaczenia opcji w konfiguracji; wymaga to głębokiego, systematycznego zrozumienia limitów skinowania kości (bone skinning limits), struktur groomów, procesów bake'owania proceduralnego (procedural baking workflows) oraz złożoności cieniowania materiałów (material shading).

Wyzwanie związane z GPU Skinning i limitami kości

Bottleneck związany z GPU Skinning na urządzeniach mobilnych

Skeletal meshes z przypisanym skinningem są dzielone na fragmenty (chunks) wierzchołków i kości przed wysłaniem ich do GPU. Każdy chunk jest przetwarzany w ramach jednego draw call, a mobilne układy GPU mają ścisły limit sprzętowy dotyczący liczby macierzy kości, które mogą jednocześnie skinować. Ograniczenie to wynika z liczby uniform vectors dostępnych dla vertex shader. Domyślny szkielet postaci MetaHuman zawiera ponad 600 kości, co z łatwością przekracza limity urządzeń mobilnych i prowadzi do błędów renderowania, rozrywania wierzchołków (vertex tearing) lub całkowitego zawieszenia GPU.

Aby obejść to ograniczenie sprzętowe, musisz zmusić silnik do podziału skeletal mesh na chunki w taki sposób, aby żaden pojedynczy draw call nie odwoływał się do większej liczby kości niż określony limit. Można to osiągnzyć poprzez dostosowanie ustawień kompatybilności skinningu. Jeśli nie zastosujesz tej konfiguracji, modele postaci nie będą prawidłowo skinowane na urządzeniach z systemem Android oraz iOS, co przełoży się na statyczne lub mocno zniekształcone siatki (meshes).

Konfiguracja DefaultEngine.ini

Aby rozwiązać problem limitów skinowania kości, musisz zmodyfikować plik konfiguracyjny DefaultEngine.ini swojego projektu. Znajdziesz go w folderze Config w katalogu głównym projektu. W sekcji [ConsoleVariables] dodaj następującą linię:

[ConsoleVariables]
Compat.MAX_GPUSKIN_BONES=75

Ta zmienna konsolowa (console variable) nakazuje kompilatorowi shaderów ograniczenie maksymalnej liczby kości na chunk skinningu do 75. Jest to ścisły limit sprzętowy dla starszych lub średniopółkowych procesorów graficznych w urządzeniach mobilnych. Pamiętaj, że ustawienie niższej wartości zmusza kompilator skeletal mesh do podziału siatki na większą liczbę chunków. Chociaż rozwiązuje to problem kompatybilności renderowania, większa liczba chunków oznacza więcej draw calls, co może przesunąć bottleneck wydajnościowy na wątek renderowania procesora (CPU render thread).

Redukcja liczby kości i usuwanie komponentów (Component Stripping)

W przypadku postaci w tle lub postaci niezależnych (NPC), które do działania nie wymagają pełnej mimiki twarzy, należy odchudzić szkielet. Na przykład usunięcie kości palców u rąk i nóg oraz kości mimiki twarzy pozwala zmniejszyć całkowitą liczbę kości postaci z ponad 600 do poniżej 75. Pozwala to na renderowanie mesh postaci jako pojedynczego chunka, bez generowania dodatkowych draw calls.

Jeśli wdrażasz również dedicated servers dla swojej gry multiplayer, optymalizacje po stronie klienta to tylko połowa sukcesu. Musisz także zoptymalizować wydajność po stronie serwera poprzez całkowite usunięcie assetów renderowania. Zapoznaj się z naszym przewodnikiem krok po kroku na temat tego, jak opanować usuwanie assetów renderowania (asset stripping) na Unreal Engine dedicated server w celu zmniejszenia zużycia pamięci serwera i zoptymalizowania wydajności CPU.

Optymalizacja włosów MetaHuman: Groom Strands kontra Hair Cards

Koszt renderowania Strand Rendering

Technologia renderowania groom strand od Epic rysuje poszczególne pasma włosów w sposób dynamiczny. Choć daje to niezwykle szczegółowe efekty na wydajnych, stacjonarnych procesorach graficznych (PC GPU), koszt wydajnościowy jest ogromny. Renderowanie pasm (strand rendering) opiera się na przebiegach compute shader w celu sortowania głębi (depth sorting) oraz generowania map cieni (shadow map), co zużywa znaczną część pixel fill rate i przepustowości pamięci (memory bandwidth).

Na urządzeniach mobilnych strand rendering jest albo całkowicie niewspierany, albo działa przy niedopuszczalnie wysokim koszcie – często pochłaniając ponad 15 ms czasu GPU na same włosy jednej postaci. Mobilne układy GPU nie mają wystarczającej przepustowości pamięci (memory bandwidth) potrzebnej do sortowania i cieniowania setek tysięcy pojedynczych pasm włosów w każdej klatce.

Wdrażanie Hair Cards

Rozwiązaniem jest zastąpienie groomów opartych na pasmach (strand-based grooms) techniką hair cards. Hair cards reprezentują włosy za pomocą płaskich, uproszczonych siatek wielokątów (polygon meshes) z nałożonymi wyrenderowanymi wcześniej teksturami włosów. Takie podejście jest w pełni kompatybilne z mobilną ścieżką forward rendering.

Aby wdrożyć hair cards, otwórz MetaHuman Creator i upewnij się, że generujesz LODy groomów oparte na kartach (card-based groom LODs) dla swojej postaci. Zastąpienie włosów opartych na strandach groomami opartymi na kartach (card-based grooms) skraca czas renderowania włosów pojedynczej postaci z około 18,2 ms do zaledwie 0,9 ms na nowoczesnym chipie mobilnym, takim jak Apple A15 lub Snapdragon 8 Gen 1. Ta potężna oszczędność pozwala przeznaczyć budżet renderowania na elementy rozgrywki lub szczegóły otoczenia.

Wyłączanie Post-Process Anim Blueprints

Modele MetaHumans wykorzystują post-process animation blueprints do obsługi dodatkowych ruchów kości, korekcji kształtu mięśni oraz dynamiki stawów. Choć zapewnia to realistyczny ruch skóry na PC, wykonuje zarazem złożone obliczenia szkieletowe na CPU w każdej klatce. Na urządzeniach mobilnych ten narzut na procesor może łatwo doprowadzić do bottlenecku wątku gry (game thread).

Możesz wyłączyć post-process animation blueprint na urządzeniach mobilnych, aby odzyskać cykle procesora. Robi się to poprzez ustawienie bDisablePostProcessAnims = true na komponentach skeletal mesh. Wyłączenie tych procesów post-processingu pozwala zaoszczędzić do 4,5 ms czasu klatki procesora (CPU frame time) na standardowym sprzęcie mobilnym.

Optymalizacja PCG dla środowisk mobilnych

Narzut związany z wykonywaniem PCG w runtime

Framework Procedural Content Generation (PCG) pozwala na dynamiczne zapełnianie środowisk poprzez rozmieszczanie siatek statycznych (static meshes), roślinności (foliage) oraz aktorów (actors) w oparciu o reguły i objętości (volumes). Jednak wykonywanie grafów PCG w runtime na mobilnych procesorach powoduje poważne przycięcia wydajności (performance hitches). Typowe generowanie grafu w runtime może zamrozić wątek gry (game thread) na 1,5 do 3 sekund podczas ładowania poziomu (level loading) lub spawnowania gracza.

W grze multiplayer takie przycięcie jest niebezpieczne; może prowadzić do utraty pakietów (packet loss) i wywołać desynchronizację stanu klient-serwer. Aby utrzymać wysoką wydajność, musisz pre-bake'ować swoje grafy PCG w edytorze. Spowoduje to konwersję instancji proceduralnych na geometrię statyczną przed spakowaniem gry (packaging).

Krok po kroku: Workflow bake'owania PCG

  1. Wybierz PCG Volume: W oknie widokowym (viewport) Unreal Editor zaznacz PCG volume zawierający elementy Twojego środowiska.
  2. Generuj i sprawdź: W panelu szczegółów (details panel) danego volume kliknij Generate, aby podejrzeć proceduralne rozmieszczenie swoich assetów.
  3. Eksportuj do aktora: Znajdź opcję Export to Actor w menu narzędziowym PCG.
  4. Wybierz Instanced Meshes: Jako format docelowy wybierz Hierarchical Instanced Static Mesh (HISM). Ta grupa instancji jest wysoce zoptymalizowana pod kątem mobilnych układów GPU, ponieważ rysuje wszystkie identyczne meshe w ramach pojedynczego GPU draw call.
  5. Wyczyść graf: Po wyeksportowaniu ustaw trigger generowania PCG volume na Editor Only lub wyczyść dany volume. Zapobiegnie to próbom ponownego wygenerowania grafu przez silnik w runtime.

Dynamiczne usuwanie niewidocznych obiektów (Culling) i streaming HISM

Po zbake'owaniu instancji PCG do HISM, musisz skonfigurować odległości ich cullingu. HISM obsługują culling na poziomie pojedynczych instancji (per-instance culling), co oznacza, że instancje znajdujące się poza określoną odległością od kamery nie będą rysowane. Ustaw Start Cull Distance oraz End Cull Distance w panelu details komponentów HISM. Na urządzeniach mobilnych zalecana odległość cullingu to 5000 do 8000 jednostek, co pozwala utrzymać łączną liczbę widocznych wielokątów (polygon count) w granicach budżetu mobilnego GPU.

Produkcyjny skrypt optymalizacyjny w C++

Aby zautomatyzować te optymalizacje w runtime, możesz napisać własną klasę pomocniczą. Poniższy kod C++ pokazuje, jak sprawdzić platformę docelową, programowo wymusić niskie poziomy szczegółowości (LODs), wyłączyć post-process animation blueprints oraz wymusić użycie renderowania opartego na kartach (cards rendering) w komponentach groom podczas spawnowania MetaHuman na urządzeniu mobilnym. Możesz to zaimplementować w klasie takiej jak UMetaHumanMobileOptimizer:

#pragma once

#include "CoreMinimal.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Components/SkeletalMeshComponent.h"
#include "GroomComponent.h"
#include "MetaHumanMobileOptimizer.generated.h"

UCLASS()
class MYPROJECT_API UMetaHumanMobileOptimizer : public UBlueprintFunctionLibrary

{
    GENERATED_BODY()

public:
    UFUNCTION(BlueprintCallable, Category = "Optimization")
    static void OptimizeMetaHumanForMobile(AActor* MetaHumanActor)
    {
        if (!MetaHumanActor)
        {
            return;
        }

        // Apply optimizations exclusively on Android and iOS platforms
        #if PLATFORM_ANDROID || PLATFORM_IOS
        TArray<USkeletalMeshComponent*> SkeletalComponents;
        MetaHumanActor->GetComponents<USkeletalMeshComponent>(SkeletalComponents);

        for (USkeletalMeshComponent* MeshComp : SkeletalComponents)
        { 
            if (MeshComp)
            {
                // Force a low LOD (LOD 3 or 4) to bypass dense meshes
                MeshComp->SetMinLOD(3);
                MeshComp->SetForcedLOD(3);

                // Disable expensive post-process animation blueprints
                MeshComp->bDisablePostProcessAnims = true;

                // Adjust animation tick rate to only calculate when visible
                MeshComp->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::OnlyTickPoseWhenRendered;

                // Strip physics asset to avoid CPU collision overhead on cosmetic joints
                MeshComp->SetPhysicsAsset(nullptr);
            }
        }

        TArray<UGroomComponent*> GroomComponents;
        MetaHumanActor->GetComponents<UGroomComponent>(GroomComponents);

        for (UGroomComponent* GroomComp : GroomComponents)
        {
            if (GroomComp)
            {
                // Force the groom to use card rendering instead of strands
                GroomComp->SetUseCards(true);
            }
        }
        #endif
    }
};

Ta funkcja pomocnicza może być wywoływana z poziomu zdarzenia BeginPlay Twojej postaci lub natychmiast po zespawnowaniu aktora MetaHuman. Dzięki zastosowaniu makr kompilacji warunkowej (PLATFORM_ANDROID || PLATFORM_IOS), kompilator usunie te nadpisania z kompilacji na PC i konsole, co pozwoli Ci automatycznie zachować wieloplatformową wierność wizualną.

Dostosowanie materiałów Substrate do mobilnych układów GPU

Czym jest Substrate?

Substrate zastępuje tradycyjny model cieniowania (shading model) Unreal Engine modułowym, wielowarstwowym frameworkiem materiałowym. Substrate pozwala deweloperom na układanie warstwowo wielu paneli cieniowania (shading slabs) – na przykład umieszczenie błyszczącej warstwy lakieru (clear coat) bezpośrednio na szorstkiej warstwie metalu. Choć Substrate doskonale sprawdza się w przypadku zaawansowanych assetów kinowych, stanowi poważną przeszkodę wydajnościową dla mobilnych rendererów.

Mobilne układy GPU w dużej mierze opierają się na architekturach tiled rendering, w których szyna pamięci między rdzeniem graficznym a wbudowanym buforem ramki (on-chip frame buffer) jest głównym bottleneckiem. Złożone materiały Substrate zwiększają liczbę bajtów na piksel (BPP - bytes per pixel) zapisywanych do bufora ramki, co wywołuje thermal throttling oraz spadki płynności (frame rate).

Zarządzanie złożonością materiałów za pomocą Quality Level Switches

Aby utrzymać wydajność materiałów Substrate na urządzeniach mobilnych, powinieneś używać węzłów Material Quality Level Switch oraz Feature Level Switch w edytorze materiałów (Material Editor). Węzły te pozwalają na uproszczenie grafu materiału w zależności od platformy docelowej.

W przypadku platform mobilnych uprość graf poprzez pominięcie wielowarstwowych mieszanek (blends) Substrate. Zamiast tego zastosuj pojedynczy panel (slab), który będzie imitował styl wizualny danego assetu. Przekierowując węzły materiału przez przełącznik jakości (quality switch), możesz zmniejszyć przepustowość zapisu z 32 bajtów na piksel do standardowych 8 bajtów na piksel, co przełoży się na niższą temperaturę pracy urządzenia i stabilniejszą liczbę klatek na sekundę.

5 praktycznych najlepszych praktyk optymalizacji mobilnej

  1. Pre-bake'uj wszystkie grafy PCG do HISM: Nie wykonuj grafów PCG na kliencie w runtime. Pre-bake'uj grafy do Hierarchical Instanced Static Meshes (HISM) na etapie pracy w edytorze i skonfiguruj odpowiednie odległości cullingu (start/end cull distances).
  2. Ogranicz liczbę kości globalnie: Dodaj Compat.MAX_GPUSKIN_BONES=75 do pliku DefaultEngine.ini swojego projektu, aby zapewnić poprawne renderowanie skeletal meshes na mobilnych układach GPU bez wywoływania przepełnienia bufora (buffer overflow).
  3. Używaj wyłącznie grooms opartych na kartach: Wyłącz groomy oparte na strandach (strand-based grooms) dla profili mobilnych. Renderowanie włosów za pomocą kart (card-based hair rendering) skraca czas klatki GPU z 18 ms do poniżej 1 ms na postać.
  4. Wykorzystaj Material Quality Switches: Zaimplementuj węzeł Material Quality Level Switch w swoich materiałach Substrate, aby uprościć złożone warstwy cieniowania do pojedynczego panelu (slab) na platformach mobilnych, zmniejszając zużycie przepustowości GPU.
  5. Wyłącz post-process animation blueprints: Ustaw bDisablePostProcessAnims = true na komponentach szkieletowych postaci na urządzeniach mobilnych, aby odzyskać cenne cykle CPU w wątku gry (game thread).

Kwestia rozgrywki wieloosobowej i backendu

Więcej niż renderowanie: Optymalizacja sieciowa na urządzeniach mobilnych

Podczas tworzenia wieloplatformowych gier multiplayer, optymalizacje po stronie klienta to tylko część układanki. Urządzenia mobilne często doświadczają wahań jakości połączenia sieciowego, przełączając się między danymi komórkowymi (5G/4G) a Wi-Fi. Wahania te powodują utratę pakietów (packet loss), jitter oraz wysokie skoki opóźnień (latency spikes).

Jeśli Twój kod synchronizacji sieciowej nie jest niezawodny, skoki opóźnień mogą wywołać błąd synchronizacji multiplayer w Unreal Engine (the Unreal Engine multiplayer sync bug), w którym replikacja aktorów (actor replication) traci synchronizację, niszcząc stan świata. Zarządzanie stanami multiplayer w warunkach ograniczeń sieci mobilnej to złożony problem, który wymaga odpornych sterowników sieciowych, kompresji delta (delta compression) oraz uzgadniania stanu z autorytatywnym serwerem (server-authoritative reconciliation).

Odciążanie infrastruktury dzięki horizOn

Samodzielne budowanie i utrzymywanie stabilnego backendu multiplayer wiąże się z gigantycznym nakładem pracy inżynieryjnej. Musiałbyś wdrażać load balancers, zarządzać globalną replikacją baz danych, zaimplementować logikę matchmaking oraz obsługiwać płatności mobilne. Prace nad infrastrukturą tego typu mogą zająć miesiące pracy deweloperskiej i wymagać ciągłego utrzymania.

Dzięki horizOn te usługi backendowe są dostarczane w gotowej konfiguracji. horizOn oferuje twórcom gier obsługę sesji matchmaking, synchronizację stanu o niskich opóźnieniach (low-latency state synchronization), trwałość bazy danych (database persistence) oraz wieloplatformowe uwierzytelnianie (cross-platform authentication) bezpośrednio po uruchomieniu (out of the box).

Podsumowanie i kolejne kroki

Optymalizacja funkcji nowej generacji, takich jak MetaHumans i PCG, na urządzeniach mobilnych wymaga ścisłej kontroli nad budżetami renderowania, kompilacją skeletal mesh oraz złożonością cieniowania materiałów. Poprzez pre-bake'owanie assetów proceduralnych, restrykcyjne ograniczenie liczby kości oraz stosowanie renderowania włosów opartego na kartach (hair cards), możesz dostarczyć wysokiej jakości wieloplatformowe doświadczenia na urządzeniach przenośnych.

Gotowy na skalowanie swojego backendu multiplayer? Wypróbuj horizOn za darmo lub zapoznaj się z dokumentacją API, aby zobaczyć, jak możesz uprościć swoją infrastrukturę i zapewnić synchronizację graczy między PC, konsolami i urządzeniami mobilnymi.


Źródło: Tutorial: Optimizing Next Gen Features for Mobile Game Development