Aus ihrem schicken 3-D-Intro für die Frankfurter MTV European Music Awards 2012 entwickelte die Berliner Motion-Design-Agentur SEHSUCHT ein Browser-Musik-Game, in dem der User im Takt zur Musik die Puppen tanzen lässt. Hier erzählen sie, wie sie das interaktive Spiel im WebGL-Framework ThreeJS umsetzten
Drehen die Spieler das Karussell im Takt zur Musik, tanzt der animierte Protagonist Seth auf dem Synthesizer. Wer den Takt hält, kommt in die zweite Runde. So beginnt unser Spiel »The Zoetrope«. Die Spielelemente stammen von unserem Show-Package der MTV European Music Awards 2012. Anlässlich dieses Events produzierte SEHSUCHT Berlin gemeinsam mit dem VIMN MTV World Design Studio Mailand eine Reihe von Trailern, die weltweit auf MTV und während der Award-Show liefen.
Im Mittelpunkt der Animation steht ein schnell drehendes Karussell, das mithilfe eines Lichtblitzgeräts eine Reihe statischer Elemente zum Leben erweckt und so die Illusion von Bewegung erzeugt. Auf fünf Stockwerken lassen wir unsere Charaktere Seth und Roxy in der Animation tanzen, crowdsurfen und Gitarre spielen. Damit nicht genug: Wir sahen die Chance, in neue Technologien einzutauchen und unser Zoetrop sprichwörtlich auf die nächste Ebene zu bringen. Stichwort: Usability! Ziel war es, ein interaktives, musikalisches Zoetrop zu entwickeln, das es dem User ermöglicht, das Ganze per Mouse oder Keyboard im Takt zu drehen und damit Spielpunkte zu sammeln.
Das Game setzten wir mit ThreeJS, einem mittlerweile gut ausgebauten WebGL-Framework um. Mit WebGL kann man 3-D-Daten direkt im Browser darstellen. Außer einem modernen Browser benötigt der User kein weiteres Plug-in. Die Open-SourceThreeJS-JavaScript-Library können Sie kostenlos unter www.threejs.org herunterladen. Da wir unmöglich das gesamte Spiel in diesem Tutorial abarbeiten können, haben wir die Elemente stark reduziert, um einzelne Schritte besser erklären zu können. Und los!
1 3-D-Szene öffnen Die 3-D-Modelle können Sie für die Nutzung in ThreeJS prinzipiell mit jeder Software erstellen, solange diese den Export in ein unterstütztes Austauschformat (in diesem Fall Wavefront OBJ) unterstützt. Behalten Sie beim Modellieren aber die Unterteilung Ihrer Objekte im Auge, denn WebGL ist nicht so performant wie sein großer Bruder OpenGL.
Mit ThreeJS sind Shading und Ausleuchtung direkt im JavaScript-Code möglich. Wer aber wie wir physikalisch korrekte Schatten und indirektes Licht setzen möchte, sollte die Objekte in einer 3-D-Software inszenieren und daraus Texturen generieren.
Für dieses Tutorial liegt uns eine Szene in Cinema 4D bereits vor. Öffnen Sie die Datei »01 ausleuchten.c4d« mit Cinema 4D, sie enthält eine Ballsequenz. Dreht sich diese im Spiel schnell genug, sieht man einen hüpfenden Ball. Sie können die Datei kurz im Editor rendern, um zu sehen, wie die Webseite später aussehen wird.
2 Texturen backen und bearbeiten Nun backen wir aus dieser Ausleuchtung Texturen. Damit Sie diese auf Ihrem Modell richtig backen können, benötigen Sie eine gute UV-Map, also eine Repräsentation Ihres 3-D-Models im 2-D-Raum. Stellen Sie sich vor, man würde dafür dem 3-D-Model die Schale abziehen und diese flach auf ein Blatt Papier legen. In unserem Beispiel haben die Modelle bereits gute UV-Maps. Sie können also gleich mit dem Backen der Texturen beginnen.
Wählen Sie in der Objekthierarchie nun den Boden (»floor«) und die Bälle (»bouncing ball«) aus. Dann klicken Sie mit der rechten Maustaste darauf und wählen »Cinema 4D tags«/»Textur backen«. Damit haben wir ein neues Tag auf den Objekten erstellt .
Klicken Sie darauf, und wechseln Sie im Attribute Manager auf den Tab »Tag«. Dort bestimmen Sie den Namen der zu erstellenden Texturen. Stellen Sie das Format auf JPEG sowie die Breite und Höhe auf 2000 Pixel. Im Tag für den springenden Ball aktivieren Sie unter »Optionen« »Farbe« und unter dem nun entstandenen Klappmenü »Licht«, »Schatten«, »Leuchten« und »Diffusion«.
Für den Boden aktivieren Sie im Objektmanager lediglich »Schatten« und »Ambient Occlusion« .
Zuletzt klicken Sie auf »Bearbeiten Projektvoreinstellungen« und stellen das Eingangsfarbprofil auf »linear«.
Würden Sie nun rendern, sähen die Farben blasser aus – in den gebackenen Texturen sind diese aber final richtig. Markieren Sie nun beide »Texturen backen«-Tags und klicken Sie auf »backen«. Nach dem Backen haben Sie drei JPEG-Bilder, die Sie in Photoshop öffnen können. Dort multiplizieren Sie die Texturen des Bodens auf eine Farbfläche mit dem Farbwert #3f95a8. Reduzieren Sie die Deckkraft der Ambient-Occlusion-Ebene auf 60 Prozent, die der Schattenebene auf 30 Prozent.
Als Nächstes müssen wir die Textur der Bälle an den Rändern erweitern, um keine weißen Nähte zu erhalten. Dafür müssen Sie sie freistellen . Kopieren Sie dann die Ebene und zeichnen Sie die untere der beiden mit dem Gaußschen Weichzeichner mit einem Zwei-Pixel-Radius weich.
Wenn Sie diese Ebene nun mehrfach duplizieren, erscheinen die weichen Ränder immer dichter . Skalieren Sie die Größe der Dateien auf 1000 x 1000 Pixel und speichern Sie sie als JPEG .
Diese Texturen können Sie nun in Cinema 4D testen. Öffnen Sie dazu im mitgelieferten Material die Datei »texturen prüfen.c4d«, und laden Sie Ihre Texturen in den Leuchtenkanal der Materialien. Wenn alles richtig ist, sollte Ihre Szene so aussehen wie vor dem Backen, allerdings ist die Beleuchtung jetzt in den Texturen gespeichert.
3 OBJ-Daten exportieren und konvertieren Löschen Sie nun alle 3-D-Objekt-Tags und kopieren Sie diese einzeln in die neuen Cinema-4D-Dateien. Klicken Sie auf »Datei › Exportieren › Wavefront obj«, und exportieren Sie beide Dateien bei einem Skalierungsfaktor von 1. Falls Sie eine komplexere Szene exportieren wollen, empfiehlt es sich, ein Plug-in wie Riptide Pro zu verwenden. Vorteil: Riptide Pro benötigt nur einen Exportvorgang, um für alle Objekte einer Szene einzelne OBJ-Datei zu erzeugen. Darüber hinaus empfiehlt sich das Plug-in, wenn Sie die 3-D-Objekte direkt in ThreeJS ausleuchten wollen, denn es exportiert im Gegensatz zu Cinema 4D auch Phong-Normalen, die für eine glatte Oberflächendarstellung niedrig aufgelöster Modelle sorgen. Da wir die Beleuchtung gebacken haben, brauchen wir keine Phong-Normalen. Diese Oberflächenglättung ist dann bereits in den Texturen vorhanden.
Die OBJ-Dateien könnten wir nun direkt in ThreeJS einbinden. Um aber die 3-D-Daten so klein wie möglich zu halten, sollten Sie diese vorher in das binäre Format von ThreeJS umwandeln, denn es halbiert die Dateigröße. Dazu benötigen wir ein Python-Script, das im ThreeJS-Paket enthalten ist. Auf Mac OS X ist Python vorinstalliert. Sollten Sie ein anderes Betriebssystem nutzen, müssen Sie vorher Python herunterladen und installieren.
Verschieben Sie jetzt die »convert_obj_three.py«-Datei in denselben Ordner, in dem auch ihre OBJ-Daten liegen, und öffnen Sie diese mit dem Python Launcher. Geben Sie nacheinander folgende Befehle ein und bestätigen Sie sie per Eingabetaste:
Mit dem ersten Befehl wandeln Sie eine »floor.obj«-Datei in eine »floor.js«-Datei im binären Format um. Der zweite Befehl macht das Gleiche für den springenden Ball. Wenn Ihre Dateien anders heißen, müssen Sie die Befehle natürlich entsprechend variieren. Python sollte jetzt für jede OBJ-Datei eine JS-Datei und eine BIN-Datei erstellt haben.
4 HTML5-Grundgerüst vorbereiten Da nun alle 3-D-Daten und -Texturen aufbereitet sind, wenden wir uns der Webseite zu und fügen die Elemente in ThreeJS zusammen. Eine HTML5-Datei ist bereits vorbereitet. Neben dem HTML5-Grundgerüst enthält sie die grafische Oberfläche unseres Test-Zoetrops und lädt ThreeJS sowie einige ThreeJS-Hilfsklassen. Die Oberfläche besteht aus einfachen HTML5- und CSS3-Befehlen, Sie können also auch Ihre eigene Oberfläche gestalten. Wichtig ist lediglich, dass jedes Oberflächenelement eine eigene CSS-ID bekommt, mit deren Hilfe wir sie später finden und ein- beziehungsweise ausblenden können. Als Schaltflächen nutzen wir HTML-Hyperlinks, die wir ebenfalls mit einer CSS-ID ausstatten (zum Beispiel <a id=”meineID”> Schalter </a>).
5 WebGL-Kompatibilität prüfen und initialisieren Nun können wir die ThreeJS-Detector-Klasse benutzen, um zu prüfen, ob Browser und Grafikkarte des Nutzers WebGL unterstützen. Ist kein WebGL verfügbar, blenden wir das Oberflächenelement mit der entsprechenden Nachricht ein, ansonsten blenden wir die erste Spielanweisung ein und bereiten unsere ThreeJS-Szene vor. Dazu registrieren wir zunächst die Variablen für die Szene und das Spiel und erstellen einen neuen DIV-Container für unseren WebGL-Inhalt.
Außerdem erstellen wir eine neue Szene sowie eine Kamera mit einer Brennweite von 50 und dem Aspekt-Ratio des Browsers. Die Kamera positionieren wir im Raum und erstellen ein neues Kontrollelement, mit dem wir sie durch den 3-D-Raum bewegen können. Dabei geben wir mittels controls.target die Blickrichtung an und sorgen mit einem Listener dafür, dass neu gerendert wird, wenn die Kamera bewegt wird.
if ( !Detector.webgl ) { document.getElementById( “noWebGl” ).style.display = ‘inline’; } else { document.getElementById( “playInstruction” ).style.display = ‘inline’; var container, stats, camera, scene, renderer; var controls, group, lastFrameTime = Date.now(); var mouseDown = false, lastMousePosition, mouseSpeed = 0, rotationIncrease = 0; container = document.createElement( ‘div’ ); container.id = “webGlContainer”; document.body.appendChild( container ); scene = new THREE.Scene(); camera = new THREE.PerspectiveCamera( 50, window.innerWidth / window.innerHeight, 1, 20000 ); camera.position = new THREE.Vector3(700,600,-2000) ; controls = new THREE.TrackballControls2( camera ); controls.addEventListener( ‘change’, render ); controls.target = new THREE.Vector3(0,0,0);
6 Modelle und Texturen laden Um später nicht einzelne 3-D-Objekte drehen zu müssen, erstellen Sie eine Gruppe, die Sie der Szene hinzufügen und die als Überobjekt für alle 3-D-Modelle fungiert. Dann laden wir unsere 3-D-Modelle mittels getModelJS und unsere Texturen mittels getTexture. Diese beiden Routinen werden wir unter Punkt 9 definieren.
group = new THREE.Object3D(); // alle 3 D Objekte werden Unterobjekte dieser Gruppe scene.add( group ); getModelJS( ‘objekte/floor.js’ , getTexture( ‘texturen/floor.jpg’ ) ); getModelJS( ‘objekte/bouncing_ball.js’ , getTexture( texturen/bouncing_ball.jpg’ ) );
7 Renderer starten Uns fehlt nun nur noch ein Renderer, der unsere Szene darstellt. Neben den Dimensionen der Darstellungsfläche aktivieren wir hier das Antialiasing. Dabei sollten Sie wissen, dass viele Grafikkarten diese Kantenglättung nicht bei WebGL-Inhalten unterstützen. Wichtig ist auch, die richtigen Gamma-Einstellungen zu wählen, ansonsten werden unsere Texturen falsch dargestellt. Mit der Funktion animate, die wir ebenfalls später definieren werden, starten wir unseren Renderer.
8 Listener erstellen Der letzte Initialisierungsschritt ist das Registrieren von Listenern. Das machen wir zunächst für unsere Schaltflächen. Bei Klick auf die Schaltfläche der ersten Anweisung soll nur die erste Anweisung aus- und die nächste Anweisung eingeblendet werden. Beim Klick auf die Schaltfläche der zweiten Anweisung registrieren wir die Listener für die Maus, mit denen wir das Zoetrop steuern.
9 Laderoutinen definieren Die Funktionen getTexture und getModelJS haben wir bereits benutzt, aber noch nicht definiert. GetTexture lädt eine Textur mithilfe eines neuen Image Loaders und gibt diese, sobald sie fertig geladen ist, zurück.
function getTexture( url ) { var texture = new THREE.Texture(); var loader = new THREE.ImageLoader(); loader.addEventListener( ‘load’, function ( event ) { texture.image = event.content; texture.needsUpdate = true; } ); loader.load( url ); return texture; }
GetModelJS lädt eine 3-D-Datei mittels eines neuen Binary Loaders. Ist die Datei geladen, kann der Befehl new THREE.MeshFaceMaterial aus den geladenen Daten ein Mesh erstellen, dem wir anschließend ein Material mit unserer Textur zuweisen. Der Parameter color steuert hier lediglich die Helligkeit der Textur. Zuletzt wird das Modell zu unserer Gruppe hinzugefügt.
function getModelJS( url, texture ){ var loader = new THREE.BinaryLoader(), callback = function( geometry ) { createMesh( geometry ) }; loader.load( url , callback); function createMesh( geometry ) { zmesh = new THREE.Mesh( geometry, new THREE.MeshFace Material() ); zmesh.material = new THREE.MeshBasicMaterial( { color: 0xffffff, map: texture } ); group.add( zmesh ); } }
10 Render-Rate einstellen Die ebenfalls bereits zum Einsatz gekommene animate-Routine nutzt die Funktion requestAnimationFrame, um sich selbst in einer kontrollierten Endlosschleife aufzurufen und dann das Bild neu zu zeichnen. RequestAnimationFrame wartet dabei so lange mit dem Aufruf, bis das System für einen neuen Zeichenvorgang bereit ist. Damit der Renderer unser Zoetrop korrekt darstellt, drosseln wir die Darstellungsrate auf zwölf Bilder pro Sekunde, denn bei einer anderen Frame-Rate würde die Animation zu schnell wirken. Dazu merken wir uns den Zeitpunkt des letzten Render-Vorgangs in einer Variablen und rendern erst wieder, wenn genügend Zeit verstrichen ist. Dann aktualisieren wir auch die Rotation des Zoetrops und updaten die Kameraposition.
function animate() { requestAnimationFrame( animate ); if (Date.now() – lastFrameTime >= 1000/12) { lastFrameTime = Date.now(); rotateZoetrope(); // Zeotrope Rotieren render(); // Rendern controls.update(); } } function render(){ renderer.render( scene, camera ); }
11 Mausevents bearbeiten Um das Zoetrop zu steuern, greifen wir als Erstes Daten der Maus ab. Listener für Mouseklicks und die Mausbewegung haben wir bereits erstellt. Mit den Funktionen MouseDownHandler und MouseUpHandler speichern wir, ob die Maustaste gedrückt ist.
function MouseDownHandler(e){ mouseDown = true; mouseSpeed = 0; } function MouseUpHandler(e){ mouseDown = false; }
Falls die Maustaste gerade gedrückt ist, kann MouseMoveHandler die Geschwindigkeit der Maus anhand der aktuellen und letzten Position errechnen. Die Funktion dividiert jetzt die Geschwindigkeit noch durch einen empirisch ermittelten Faktor von 50, damit sie eine angemessene Größe für die Rotation des Zoetrops erhält.
12 Zoetrop drehen und Spiel testen Nun übertragen wir das Maustempo auf die Zoetrop-Rotation. Dafür limitieren wir den Wert von mouseSpeed auf ein Sechstel von Pi, also ein Zwölftel Umdrehung pro Frame. So dreht sich das Zoetrop in einer Sekunde maximal um 360 Grad, und zwar in einem realistisch anmutenden Tempo. Dann nutzen wir die Variable rotationIncrease, die sich dem Wert von mouseSpeed exponentiell annähert, um eine gewisse Trägheit zu erzeugen. Wir addieren den Wert von rotationIncrease auf die Zoetrop-Rotation und verringern mouseSpeed linear mit einem empirisch ermittelten Wert, sodass das Zoetrop ausrollen würde, wenn es nicht angeschubst wird.
function rotateZoetrope() { if (mouseSpeed > Math.PI/6) mouseSpeed = Math.PI/6; else if (mouseSpeed < -Math.PI/6) mouseSpeed = -Math.PI/6; rotationIncrease += (mouseSpeed – rotationIncrease)*.2; group.rotation.y += rotationIncrease; if (mouseSpeed > 0) mouseSpeed -= 1/300; if (mouseSpeed < 0) mouseSpeed += 1/300; }
Das Zoetrop ist fertig , viel Spaß beim Testen in Ihrem Browser!
Tools Cinema 4D (notfalls andere 3-D-Software), Photoshop, ThreeJS, JavaScript-Editor
Vorkenntnisse HTML, CSS, Cinema-4-D- und JavaScript-Basics
Virtual Reality und die Neurowissenschaften: Das müssen VR-, UX- und Interaction Designer wissen. Internationale Studien und Experimente, Ethikstandards