Das Hobbit-Erlebnis

Mittelerde mit Mobile WebGL zum Leben erwecken

Daniel Isaksson
Daniel Isaksson

Bisher war es eine Herausforderung, interaktive, webbasierte, multimedialastige Inhalte auf Smartphones und Tablets zu präsentieren. Die Haupteinschränkungen waren die Leistung, die API-Verfügbarkeit, Einschränkungen bei HTML5-Audio auf Geräten und die fehlende nahtlose Inline-Videowiedergabe.

Anfang des Jahres haben wir mit Freunden von Google und Warner Bros. ein Projekt gestartet, um eine mobile Website für den neuen Hobbit-Film Der Hobbit: Die Zerstörung von Smaug zu entwickeln. Die Erstellung eines multimedialastigen mobilen Chrome-Experiments war eine wirklich inspirierende und herausfordernde Aufgabe.

Die Nutzung ist für Chrome für Android auf den neuen Nexus-Geräten optimiert, auf denen wir jetzt auf WebGL und Web Audio zugreifen können. Dank hardwaregestütztem Compositing und CSS-Animationen ist ein Großteil der Funktionen jedoch auch auf Geräten und in Browsern ohne WebGL verfügbar.

Die gesamte Erfahrung basiert auf einer Karte von Mittelerde und den Orten und Charakteren aus den Hobbit-Filmen. Mit WebGL konnten wir die reiche Welt der Hobbit-Trilogie dramatisieren und erkunden und den Nutzern die Kontrolle über die Inhalte geben.

Herausforderungen von WebGL auf Mobilgeräten

Erstens ist der Begriff „Mobilgeräte“ sehr weit gefasst. Die technischen Daten der Geräte variieren stark. Als Entwickler müssen Sie also entscheiden, ob Sie mehr Geräte mit einer weniger komplexen Oberfläche unterstützen möchten oder, wie in diesem Fall, die unterstützten Geräte auf diejenigen beschränken, die eine realistischere 3D-Welt darstellen können. Bei „Reise durch Mittelerde“ haben wir uns auf Nexus-Geräte und fünf beliebte Android-Smartphones konzentriert.

Für den Test haben wir three.js verwendet, wie schon bei einigen unserer früheren WebGL-Projekte. Wir begannen mit der Implementierung, indem wir eine erste Version des Trollshaw-Spiels erstellten, die auf dem Nexus 10-Tablet gut laufen sollte. Nach einigen ersten Tests auf dem Gerät hatten wir eine Liste mit Optimierungen, die denen ähnelten, die wir normalerweise für einen Laptop mit niedrigen Spezifikationen verwenden würden:

  • Low-Poly-Modelle verwenden
  • Verwenden Sie Texturen mit niedriger Auflösung.
  • Reduzieren Sie die Anzahl der Drawcalls so weit wie möglich, indem Sie Geometrie zusammenführen.
  • Materialien und Beleuchtung vereinfachen
  • Nachbearbeitungseffekte entfernen und Anti-Aliasing deaktivieren
  • JavaScript-Leistung optimieren
  • WebGL-Canvas in halber Größe rendern und mit CSS skalieren

Nachdem wir diese Optimierungen auf unsere erste grobe Version des Spiels angewendet hatten, hatten wir eine stabile Framerate von 30 fps, mit der wir zufrieden waren. Unser Ziel war es damals, die visuellen Elemente zu verbessern, ohne die Framerate negativ zu beeinflussen. Wir haben viele Tricks ausprobiert: Einige hatten tatsächlich einen großen Einfluss auf die Leistung, andere nicht so viel, wie wir uns erhofft hatten.

Low-Poly-Modelle verwenden

Beginnen wir mit den Modellen. Die Verwendung von Low-Poly-Modellen verkürzt die Downloadzeit und die Zeit, die zum Initialisieren der Szene benötigt wird. Wir haben festgestellt, dass wir die Komplexität erheblich erhöhen konnten, ohne die Leistung wesentlich zu beeinträchtigen. Die Trollmodelle, die wir in diesem Spiel verwenden, haben etwa 5.000 Gesichter und die Szene etwa 40.000 Gesichter. Das funktioniert gut.

Einer der Trolle des Trollshaw-Waldes
Einer der Trolle des Trollshaw-Waldes

Bei einem anderen (noch nicht veröffentlichten) Standort in der Umgebung haben wir eine größere Leistungssteigerung durch die Reduzierung der Polygone festgestellt. In diesem Fall haben wir für Mobilgeräte Objekte mit weniger Polygonen geladen als für Computer. Das Erstellen verschiedener 3D‑Modelle ist mit zusätzlichem Aufwand verbunden und nicht immer erforderlich. Das hängt davon ab, wie komplex Ihre Modelle sind.

Bei der Arbeit an großen Szenen mit vielen Objekten haben wir versucht, die Geometrie strategisch zu unterteilen. So konnten wir weniger wichtige Raster schnell ein- und ausschalten, um eine Einstellung zu finden, die für alle Mobilgeräte funktionierte. Anschließend können wir die Geometrie in JavaScript zur dynamischen Optimierung zur Laufzeit oder in der Vorproduktion zusammenführen, um Anfragen zu sparen.

Verwenden Sie Texturen mit niedriger Auflösung.

Um die Ladezeit auf Mobilgeräten zu verkürzen, haben wir uns entschieden, andere Texturen zu laden, die nur halb so groß wie die Texturen auf dem Computer sind. Es stellte sich heraus, dass alle Geräte Texturgrößen bis 2.048 × 2.048 Pixel verarbeiten können und die meisten 4.096 × 4.096 Pixel. Die Textursuche für die einzelnen Texturen scheint kein Problem zu sein, sobald sie auf die GPU hochgeladen wurden. Die Gesamtgröße der Texturen muss in den GPU-Arbeitsspeicher passen, damit Texturen nicht ständig hoch- und heruntergeladen werden müssen. Dies ist für die meisten Web-Anwendungen jedoch wahrscheinlich kein großes Problem. Es ist jedoch wichtig, Texturen in so wenige Spritesheets wie möglich zu kombinieren, um die Anzahl der Drawcalls zu reduzieren. Dies hat einen großen Einfluss auf die Leistung auf Mobilgeräten.

Textur für einen der Trolle im Trollshaw-Wald
Textur für einen der Trolle des Trollshaw-Waldes
(Originalgröße 512 × 512 Pixel)

Materialien und Beleuchtung vereinfachen

Die Auswahl der Materialien kann sich ebenfalls stark auf die Leistung auswirken und muss auf Mobilgeräten mit Bedacht erfolgen. Wir haben die Leistung unter anderem durch die Verwendung von MeshLambertMaterial (Berechnung des Lichts pro Vertex) in three.js anstelle von MeshPhongMaterial (Berechnung des Lichts pro Texel) optimiert. Im Grunde haben wir versucht, so einfache Shader mit so wenigen Beleuchtungsberechnungen wie möglich zu verwenden.

Wenn Sie sehen möchten, wie sich die von Ihnen verwendeten Materialien auf die Leistung einer Szene auswirken, können Sie die Materialien der Szene mit einem MeshBasicMaterial überschreiben . So erhalten Sie einen guten Vergleich.

scene.overrideMaterial = new THREE.MeshBasicMaterial({color:0x333333, wireframe:true});

JavaScript-Leistung optimieren

Bei der Entwicklung von Spielen für Mobilgeräte ist die GPU nicht immer das größte Hindernis. Die CPU wird stark beansprucht, insbesondere bei der Physik und bei Skelettanimationen. Je nach Simulation kann es hilfreich sein, diese aufwendigen Berechnungen nur alle zwei Frames auszuführen. Sie können auch verfügbare JavaScript-Optimierungstechniken für Objekt-Pooling, Speicherbereinigung und Objekterstellung verwenden.

Das Aktualisieren bereits zugewiesener Objekte in Schleifen anstelle des Erstellens neuer Objekte ist ein wichtiger Schritt, um Ruckler bei der automatischen Speicherbereinigung während des Spiels zu vermeiden.

Angenommen, Sie haben folgenden Code:

var currentPos = new THREE.Vector3();

function gameLoop() {
  currentPos = new THREE.Vector3(0+offsetX,100,0);
}

Eine verbesserte Version dieser Schleife verhindert das Erstellen neuer Objekte, die dann vom Garbage Collector beseitigt werden müssen:

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

Ereignishandler sollten nach Möglichkeit nur Eigenschaften aktualisieren und die Aktualisierung der Bühne dem requestAnimationFrame-Render-Loop überlassen.

Ein weiterer Tipp ist, Ray-Tracing-Vorgänge zu optimieren und/oder vorab zu berechnen. Wenn Sie beispielsweise ein Objekt während einer Bewegung auf einem statischen Pfad an ein Mesh anhängen möchten, können Sie die Positionen während einer Schleife „aufzeichnen“ und dann aus diesen Daten lesen, anstatt ein Ray-Tracing auf das Mesh anzuwenden. Oder wie bei Rivendell: Sie können einen Raycast verwenden, um nach Mausinteraktionen mit einem einfacheren, unsichtbaren Low-Poly-Mesh zu suchen. Die Suche nach Kollisionen in einem High-Poly-Mesh ist sehr langsam und sollte in einem Gameloop generell vermieden werden.

WebGL-Canvas in halber Größe rendern und mit CSS skalieren

Die Größe des WebGL-Canvas ist wahrscheinlich der effektivste Parameter, den Sie zur Leistungsoptimierung anpassen können. Je größer das Canvas ist, auf dem Sie Ihre 3D-Szene zeichnen, desto mehr Pixel müssen in jedem Frame gezeichnet werden. Das wirkt sich natürlich auf die Leistung aus. Das Nexus 10 mit seinem hochauflösenden Display mit 2.560 × 1.600 Pixeln muss viermal so viele Pixel berechnen wie ein Tablet mit niedriger Auflösung. Um das für Mobilgeräte zu optimieren, verwenden wir einen Trick: Wir legen die Leinwand auf die Hälfte der Größe (50%) fest und skalieren sie dann mit hardwarebeschleunigten CSS-3D-Transformationen auf die gewünschte Größe (100%). Der Nachteil dabei ist ein pixeliges Bild, bei dem dünne Linien zu einem Problem werden können. Auf einem hochauflösenden Bildschirm ist der Effekt jedoch nicht so schlimm. Die zusätzliche Leistung ist es auf jeden Fall wert.

Dieselbe Szene ohne Canvas-Skalierung auf dem Nexus 10 (16 fps) und skaliert auf 50% (33 fps)
Diese Szene ohne Canvas-Skalierung auf dem Nexus 10 (16 fps) und skaliert auf 50% (33 fps).

Objekte als Bausteine

Um das große Labyrinth des Schlosses Dol Guldur und das endlose Tal von Rivendell zu erstellen, haben wir eine Reihe von wiederverwendbaren 3D-Bausteinmodellen erstellt. Durch die Wiederverwendung von Objekten können wir dafür sorgen, dass Objekte zu Beginn und nicht in der Mitte der Wiedergabe erstellt und hochgeladen werden.

3D-Objektbausteine, die im Labyrinth von Dol Guldur verwendet wurden.
3D-Objektbausteine, die im Labyrinth von Dol Guldur verwendet wurden.

In Rivendell gibt es eine Reihe von Bodenabschnitten, die wir im Verlauf der User Journey ständig in der Z‑Tiefe neu positionieren. Wenn der Nutzer Abschnitte passiert, werden diese in die Ferne verschoben.

Für das Schloss von Dol Guldur wollten wir, dass das Labyrinth für jedes Spiel neu generiert wird. Dazu haben wir ein Script erstellt, das das Labyrinth neu generiert.

Wenn Sie die gesamte Struktur von Anfang an in einem großen Mesh zusammenführen, führt dies zu einer sehr großen Szene und einer schlechten Leistung. Um dem zu begegnen, haben wir uns entschieden, die Bausteine je nach Sichtbarkeit auszublenden oder einzublenden. Wir hatten von Anfang an die Idee, ein 2D-Raycaster-Script zu verwenden, haben aber letztendlich das integrierte Three.js-Frustum-Culling verwendet. Wir haben das Raycaster-Script wiederverwendet, um heranzuzoomen, wenn die Gefahr, der sich der Spieler gegenübersieht, näher rückt.

Als Nächstes geht es um die Nutzerinteraktion. Auf dem Computer stehen Maus und Tastatur zur Verfügung, auf Mobilgeräten interagieren Nutzer über Berührung, Wischen, Zusammenziehen, Geräteausrichtung usw.

Touch-Interaktionen in mobilen Web-Anwendungen

Das Hinzufügen von Touchbedienung ist nicht schwierig. Es gibt gute Artikel zu diesem Thema. Es gibt jedoch einige Kleinigkeiten, die das Ganze etwas komplizierter machen können.

Sie können sowohl Touchbedienung als auch Maus verwenden. Chromebook Pixel und andere Touch-fähige Laptops unterstützen sowohl die Maus als auch die Touchbedienung. Ein häufiger Fehler ist es, zu prüfen, ob das Gerät Touchbedienung unterstützt, und dann nur Touch-Ereignis-Listener hinzuzufügen, aber keine für die Maus.

Aktualisieren Sie das Rendering nicht in Event-Listenern. Speichern Sie die Touch-Ereignisse stattdessen in Variablen und reagieren Sie im requestAnimationFrame-Render-Loop darauf. Dies verbessert die Leistung und führt auch zu einer Zusammenführung von sich widersprechenden Ereignissen. Verwenden Sie Objekte wieder, anstatt in den Event-Listenern neue zu erstellen.

Denken Sie daran, dass es sich um Multitouch handelt: „event.touches“ ist ein Array aller Touch-Ereignisse. In einigen Fällen ist es interessanter, sich stattdessen event.targetTouches oder event.changedTouches anzusehen und nur auf die Touch-Ereignisse zu reagieren, die Sie interessieren. Um Tippen von Wischen zu unterscheiden, verwenden wir eine Verzögerung, bevor wir prüfen, ob sich der Touch bewegt hat (Wischen) oder sich nicht bewegt hat (Tippen). Um ein Zusammenziehen zu erhalten, wird der Abstand zwischen den beiden ersten Berührungen gemessen und wie sich dieser im Laufe der Zeit ändert.

In einer 3D-Welt müssen Sie entscheiden, wie Ihre Kamera auf Maus- und Wischaktionen reagiert. Eine gängige Methode, um Kamerabewegungen hinzuzufügen, ist die Mausbewegung zu verfolgen. Dies kann entweder über die direkte Steuerung mit der Mausposition oder über eine Deltabewegung (Positionsänderung) erfolgen. Auf einem Mobilgerät soll nicht immer dasselbe Verhalten wie in einem Desktopbrowser erfolgen. Wir haben ausgiebig getestet, um herauszufinden, was für jede Version am besten geeignet ist.

Bei kleineren Bildschirmen und Touchscreens werden die Finger der Nutzer und die Grafiken für die Benutzeroberfläche oft von dem verdeckt, was Sie zeigen möchten. Das ist etwas, das wir beim Entwerfen nativer Apps gewohnt sind, aber bei Web-Anwendungen bisher nicht wirklich berücksichtigen mussten. Das ist eine echte Herausforderung für Designer und UX-Designer.

Zusammenfassung

Unsere Erfahrungen aus diesem Projekt haben gezeigt, dass WebGL auf Mobilgeräten sehr gut funktioniert, insbesondere auf neueren High-End-Geräten. Was die Leistung angeht, scheinen die Polygonanzahl und die Texturgröße vor allem die Download- und Initialisierungszeiten zu beeinflussen. Materialien, Shader und die Größe des WebGL-Canvas sind die wichtigsten Elemente, die für die Leistung auf Mobilgeräten optimiert werden sollten. Die Leistung wird jedoch durch die Summe der einzelnen Faktoren beeinflusst. Daher zählt jede Optimierung.

Wenn Sie auf Mobilgeräte ausrichten, müssen Sie sich auch an Touch-Interaktionen gewöhnen. Es geht nicht nur um die Pixelgröße, sondern auch um die physische Größe des Bildschirms. In einigen Fällen mussten wir die 3D-Kamera näher heranbringen, um zu sehen, was vor sich ging.

Der Test ist gestartet und es war eine fantastische Reise. Viel Spaß damit!

Möchten Sie es einmal ausprobieren? Mach dich auf den Weg nach Mittelerde.