Wprowadzanie Śródziemia w życie dzięki WebGL na urządzeniach mobilnych
W przeszłości przeniesienie interaktywnych, internetowych i bogatych w multimedia treści na urządzenia mobilne i tablety było trudne. Główne ograniczenia to wydajność, dostępność interfejsu API, ograniczenia dźwięku w HTML5 na urządzeniach i brak płynnego odtwarzania filmów wstawionych w treści.
W tym roku wspólnie z Google i Warner Bros. rozpoczęliśmy projekt, którego celem było stworzenie strony internetowej z nową wersją Hobbita, Hobbit: Pustkowie Smauga, zoptymalizowaną pod kątem urządzeń mobilnych. Stworzenie eksperymentu mobilnego w Chrome, zawierającego wiele multimediów, było naprawdę inspirującym i wymagającym zadaniem.
Ta funkcja jest zoptymalizowana pod kątem Chrome na Androida na nowych urządzeniach Nexus, na których mamy teraz dostęp do WebGL i Web Audio. Jednak większość funkcji jest dostępna również na urządzeniach i w przeglądarkach innych niż WebGL dzięki akceleracji sprzętowej i animacji CSS.
Cała rozgrywka opiera się na mapie Śródziemia oraz lokacjach i postaciach z filmów Hobbit. Dzięki technologii WebGL mogliśmy przedstawić bogaty świat trylogii Hobbita i zachęcić użytkowników do samodzielnego odkrywania jego tajemnic.
Problemy związane z WebGL na urządzeniach mobilnych
Po pierwsze, termin „urządzenia mobilne” jest bardzo ogólny. Dane techniczne urządzeń różnią się znacznie. Jako deweloper musisz więc zdecydować, czy chcesz obsługiwać więcej urządzeń z mniej złożonym interfejsem, czy też, tak jak w tym przypadku, ograniczyć obsługiwane urządzenia do tych, które są w stanie wyświetlać bardziej realistyczny świat 3D. W przypadku „Podróży po Śródziemiu” skupiliśmy się na urządzeniach Nexus i pięciu popularnych smartfonach z Androidem.
W eksperymencie użyliśmy biblioteki three.js, tak jak w przypadku niektórych naszych poprzednich projektów WebGL. Rozpoczęliśmy wdrażanie od stworzenia początkowej wersji gry Trollshaw, która miała działać dobrze na tablecie Nexus 10. Po wstępnych testach na urządzeniu mieliśmy listę optymalizacji, która wyglądała podobnie do tego, czego zwykle używamy w przypadku laptopów o niskich specyfikacjach:
- Używanie modeli o niskiej liczbie wielokątów
- Używanie tekstur o niskiej rozdzielczości
- Zmniejsz liczbę wywołań drawcall do minimum, łącząc geometrię.
- Uprość materiały i oświetlenie
- Usuń efekty postprodukcyjne i wyłącz wygładzanie krawędzi.
- Optymalizacja wydajności kodu JavaScript
- Renderowanie obrazu WebGL w połowie rozmiaru i powiększanie go za pomocą CSS
Po zastosowaniu tych optymalizacji w pierwszej przybliżonej wersji gry uzyskaliśmy płynną liczbę klatek na sekundę wynoszącą 30 FPS, która nas zadowoliła. Naszym celem było wtedy polepszenie jakości wizualnej bez negatywnego wpływu na liczbę klatek na sekundę. Wypróbowaliśmy wiele sztuczek: niektóre okazały się naprawdę skuteczne, inne nie przyniosły oczekiwanych rezultatów.
Używanie modeli o niskiej liczbie wielokątów
Zacznijmy od modeli. Korzystanie z modeli o niskiej liczbie wielokątów z pewnością skraca czas pobierania i czas potrzebny do zainicjowania sceny. Okazało się, że możemy znacznie zwiększyć złożoność bez znacznego wpływu na wydajność. Modele trolli, których używamy w tej grze, mają około 5000 twarz. Scena zawiera około 40 tys. twarz, co działa dobrze.

W przypadku innej (jeszcze niewydanej) lokalizacji zauważyliśmy większy wpływ zmniejszenia liczby wielokątów na wydajność. W tym przypadku na urządzeniach mobilnych wczytaliśmy obiekty z mniejszą liczbą wielokątów niż na komputerach. Tworzenie różnych zestawów modeli 3D wymaga dodatkowej pracy i nie zawsze jest konieczne. To zależy od tego, jak złożone są Twoje modele.
Podczas pracy nad dużymi scenami z dużą liczbą obiektów staraliśmy się strategicznie dzielić geometrię. Dzięki temu mogliśmy szybko włączać i wyłączać mniej ważne siatki, aby znaleźć ustawienie, które działa na wszystkich urządzeniach mobilnych. Następnie możemy scalić geometrię w JavaScript na etapie wykonywania kodu w celu optymalizacji dynamicznej lub scalić ją w preprodukcji, aby zaoszczędzić zapytania.
Używanie tekstur o niskiej rozdzielczości
Aby skrócić czas wczytywania na urządzeniach mobilnych, zdecydowaliśmy się na wczytywanie różnych tekstur o połowie rozmiaru tekstur na komputerze. Okazuje się, że wszystkie urządzenia mogą obsługiwać rozmiary tekstur do 2048 x 2048 pikseli, a większość z nich obsługuje rozmiar 4096 x 4096 pikseli. Wyszukiwanie tekstur w poszczególnych teksturach nie wydaje się być problemem po przesłaniu ich do procesora graficznego. Łączny rozmiar tekstur musi mieścić się w pamięci GPU, aby uniknąć ciągłego pobierania i przesyłania tekstur. W większości przypadków nie stanowi to jednak problemu. Jednak łączenie tekstur w jak najmniejszą możliwą liczbę arkuszy sprite’ów jest ważne, aby zmniejszyć liczbę wywołań funkcji drawcall. Ma to duży wpływ na wydajność na urządzeniach mobilnych.

(wymiar oryginalny: 512 × 512 pikseli)
Uprość materiały i oświetlenie
Wybór materiałów może też znacząco wpływać na wydajność i musi być odpowiednio zarządzany na urządzeniach mobilnych. Aby zoptymalizować wydajność, w three.js użyliśmy funkcji MeshLambertMaterial
(obliczanie światła na wierzchołek) zamiast funkcji MeshPhongMaterial
(obliczanie światła na piksel). Staraliśmy się używać jak najprostszych shaderów z jak najmniejszą liczbą obliczeń oświetlenia.
Aby sprawdzić, jak używane materiały wpływają na działanie sceny, możesz zastąpić materiały sceny za pomocą MeshBasicMaterial
. Dzięki temu będziesz mieć pewność, że w porównaniu zostaną użyte odpowiednie dane.
scene.overrideMaterial = new THREE.MeshBasicMaterial({color:0x333333, wireframe:true});
Optymalizowanie wydajności kodu JavaScript
Podczas tworzenia gier na urządzenia mobilne GPU nie zawsze jest największym problemem. Sporo czasu zajmuje procesorowi, zwłaszcza animacja szkieletu i fizyka. W zależności od symulacji czasami przydaje się trik polegający na wykonywaniu tych kosztownych obliczeń tylko co 2 klatki. Możesz też stosować dostępne techniki optymalizacji JavaScriptu w przypadku zbioru obiektów, czyszczenia pamięci i tworzenia obiektów.
Aktualizowanie w pętlach przydzielonych wcześniej obiektów zamiast tworzenia nowych obiektów jest ważnym krokiem, który pozwala uniknąć „zatrzymań” podczas sprzątania pamięci.
Weź pod uwagę np. ten kod:
var currentPos = new THREE.Vector3();
function gameLoop() {
currentPos = new THREE.Vector3(0+offsetX,100,0);
}
Ulepszona wersja tego pętli zapobiega tworzeniu nowych obiektów, które muszą być zebrane przez mechanizm garbage collection:
var originPos = new THREE.Vector3(0,100,0);
var currentPos = new THREE.Vector3();
function gameLoop() {
currentPos.copy(originPos).x += offsetX;
//or
currentPos.set(originPos.x+offsetX,originPos.y,originPos.z);
}
W miarę możliwości metody obsługi zdarzeń powinny aktualizować tylko właściwości, a aktualizowanie sceny powinno być obsługiwane przez pętlę renderowania requestAnimationFrame
.
Inną wskazówką jest optymalizacja lub wstępny przeliczanie operacji rzutowania promieni. Jeśli na przykład chcesz dołączyć obiekt do siatki podczas ruchu po ścieżce statycznej, możesz „zapisać” pozycje w ramach jednego cyklu, a potem odczytać te dane zamiast rzutowania promieni na siatkę. Możesz też, tak jak w przypadku doświadczenia Rivendell, użyć wiązki promieni, aby sprawdzić interakcje myszy z prostszym, niewidocznym siatkowym modelem o niskiej liczbie wielokątów. Wyszukiwanie kolizji w siatce o dużej liczbie wielokątów jest bardzo powolne i należy unikać tego w pętli gry.
Renderowanie obrazu WebGL w połowie rozmiaru i powiększanie go za pomocą CSS
Rozmiar kanwy WebGL jest prawdopodobnie najbardziej skutecznym parametrem, który możesz dostosować, aby zoptymalizować wydajność. Im większy jest obszar roboczy używany do rysowania sceny 3D, tym więcej pikseli musi być narysowanych w każdej klatce. To oczywiście wpływa na wydajność.Nexus 10 z wyświetlaczem o wysokiej gęstości pikseli 2560 x 1600 musi przesłać 4 razy więcej pikseli niż tablet o niskiej gęstości. Aby zoptymalizować tę funkcję na urządzeniach mobilnych, używamy sztuczki, która polega na ustawieniu rozmiaru płótna na połowę (50%) i następnym skalowaniu go do docelowego rozmiaru (100%) za pomocą akceleracji sprzętowej transformacji 3D w CSS. Minusem jest to, że obraz jest pikselotarny, a cienkie linie mogą stanowić problem, ale na ekranie o wysokiej rozdzielczości efekt nie jest aż tak zły. Zwiększona wydajność zdecydowanie jest tego warta.

Obiekty jako elementy składowe
Aby móc stworzyć wielki labirynt w zamku Dol Guldur i niekończącą się dolinę Rivendell, stworzyliśmy zestaw modeli 3D, które można wielokrotnie wykorzystywać. Dzięki ponownemu używaniu obiektów możemy mieć pewność, że obiekty są instancjonowane i przesyłane na początku doświadczenia, a nie w jego trakcie.

W Rivendell mamy kilka sekcji naziemnych, które stale przemieszczamy w głębi Z w miarę postępów użytkownika. Gdy użytkownik przejdzie przez sekcje, te zostaną przesunięte w dalszą odległość.
W przypadku zamku Dol Guldur chcieliśmy, aby labirynt był generowany na nowo w przypadku każdej gry. W tym celu utworzyliśmy skrypt, który generuje labirynt.
Połączenie całej struktury w jedną dużą siatkę od samego początku powoduje powstanie bardzo dużej sceny i niską wydajność. Aby rozwiązać ten problem, postanowiliśmy ukrywać i wyświetlać elementy składowe w zależności od tego, czy są one widoczne. Od samego początku chcieliśmy użyć skryptu 2D raycaster, ale ostatecznie skorzystaliśmy z wbudowanego w three.js mechanizmu frustrum culling. Użyliśmy skryptu raycaster, aby powiększyć „niebezpieczeństwo”, z którym mierzy się gracz.
Kolejną ważną kwestią jest interakcja z użytkownikiem. Na komputerach masz do dyspozycji mysz i klawiaturę, a na urządzeniach mobilnych użytkownicy korzystają z dotyku, przesuwania, powiększania i pomniejszania, orientacji urządzenia itp.
Interakcje dotykowe w internetowych aplikacjach mobilnych
Dodanie obsługi dotykowej nie jest trudne. Dostępne są ciekawe artykuły na ten temat. Są jednak pewne drobne kwestie, które mogą to skomplikować.
Możesz jednocześnie używać ekranu dotykowego i myszy. Chromebook Pixel i inne laptopy z ekranem dotykowym obsługują zarówno mysz, jak i gesty. Częstym błędem jest sprawdzenie, czy urządzenie obsługuje dotyk, a potem dodanie tylko detektorów zdarzeń dotykowych i żadnego dla myszy.
Nie aktualizuj renderowania w detektorach zdarzeń. Zamiast tego zapisz zdarzenia dotyku w postaci zmiennych i reaguj na nie w pętli renderowania requestAnimationFrame. Zwiększa to wydajność i ujednolica sprzeczne zdarzenia. Używaj ponownie obiektów zamiast tworzyć nowe w detektorach zdarzeń.
Pamiętaj, że jest to działanie wielodotykowe: event.touches to tablica wszystkich dotknięć. W niektórych przypadkach bardziej interesujące może być sprawdzenie wartości event.targetTouches lub event.changedTouches i zareagowanie na dotknięcia, które Cię interesują. Aby odróżnić dotknięcia od przesunięć, używamy opóźnienia przed sprawdzeniem, czy dotknięcie zostało przesunięte (przesunięcie) czy nie (dotknięcie). Aby uzyskać efekt ściskania, mierzymy odległość między 2 pierwszymi punktami styczności i sposób, w jaki zmienia się ona w czasie.
W świecie 3D musisz zdecydować, jak kamera ma reagować na ruchy myszy i przesuwanie palcem. Jednym z popularnych sposobów dodania ruchu kamery jest śledzenie ruchów myszy. Można to zrobić za pomocą bezpośredniego sterowania za pomocą pozycji myszy lub ruchu delta (zmiany pozycji). Nie zawsze chcesz, aby urządzenie mobilne działało tak samo jak przeglądarka na komputerze. Przeprowadziliśmy liczne testy, aby wybrać odpowiednią wersję.
W przypadku mniejszych ekranów i ekranów dotykowych często zdarza się, że palce użytkownika i elementy interfejtu utrudniają wyświetlanie treści. Jest to coś, do czego jesteśmy przyzwyczajeni podczas projektowania aplikacji natywnych, ale wcześniej nie musieliśmy się tym przejmować w przypadku aplikacji internetowych. To prawdziwe wyzwanie dla projektantów i projektantów UX.
Podsumowanie
Z naszych ogólnych doświadczeń z tego projektu wynika, że WebGL na urządzeniach mobilnych działa bardzo dobrze, zwłaszcza na nowszych, zaawansowanych urządzeniach. Jeśli chodzi o wydajność, wydaje się, że liczba wielokątów i rozmiar tekstury mają największy wpływ na czas pobierania i inicjowania, a materiały, shadery i rozmiar kanwy WebGL to elementy, które należy optymalizować pod kątem wydajności na urządzeniach mobilnych. Jednak to suma tych części wpływa na skuteczność, więc wszystko, co możesz zrobić, aby zoptymalizować wyniki, ma znaczenie.
Kierowanie na urządzenia mobilne oznacza też, że musisz przyzwyczaić się do myślenia o interakcjach dotykowych i nie tylko o rozmiarze w pikselach, ale także o fizycznym rozmiarze ekranu. W niektórych przypadkach musieliśmy zbliżyć kamerę 3D, aby zobaczyć, co się dzieje.
Eksperyment został uruchomiony i był to fantastyczny czas. Mamy nadzieję, że Ci się spodoba.
Chcesz spróbować? Wyrusz w podróż do Śródziemia.