Aus Linux-Magazin 09/2012

3-D-Szenen im Browser mit Three.js

© view7, photocase.com

Mit HTML 5 kommt 3-D-Unterstützung in den Browser. Dank Web GL stellen Firefox, Chrome & Co. dreidimensionale Welten ohne spezielle Plugins oder Viewer dar. Zusätzlich macht die Javascript-Bibliothek Three.js dem Programmierer die Arbeit leicht.

Unter dem Schlagwort HTML 5 zieht ein ganzes Bündel neuer Technologien in die Browser auf Desktoprechnern und Mobilgeräten ein [1]. Darunter befindet sich eine, die die Tür zur 3-D-Welt aufstößt: Web GL. Die Web Graphics Library [2] ist eine Javascript-Schnittstelle für die 3-D-Bibliothek Open GL. Damit können normale HTML-Seiten dreidimensionale Anteile enthalten, die Hardware-beschleunigt auf der Grafikkarte laufen.

Web GL ist in den Desktop-Browsern Mozilla Firefox (ab Version 4), Opera (ab Version 12) und Google Chrome (ab Version 9) umgesetzt. Da die Khronos Group als Basis Open GL für Embedded-Systeme (Open GL ES) nutzt, lässt es sich auch unter Android mit Firefox for Mobile und Opera Mobile nutzen.

Die Idee, 3-D-Anwendungen ins Web zu bringen, ist zwar nicht neu, doch alle bisherigen Anläufe wie VRML, Java-Applets mit Java-3-D/Jogl oder 3-D-Flash erreichten wegen der erforderlichen Installation von speziellen Viewern, Java-Bibliotheken oder der geringen Performance nur ein begrenztes Publikum. Dank der breiten Web-GL-Unterstützung in den Browsern darf der Entwickler nun erstmals eine große Nutzerschar erwarten.

Hardware-Unterstützung

Der Stand auf dem eigenen Rechner lässt sich am einfachsten mit der Testseite [3] des Web-GL-Konsortiums ermitteln. Zeigt sie bei Firefox keine Unterstützung an, empfiehlt es sich, die Einstellungen unter »about:config« mit Abbildung 1 zu vergleichen. Chrome ist bei den Grafikkarten etwas wählerisch. Falls die Web-GL-Unterstützung dort nicht sofort klappt, lässt sie sich in der Konfigurationsdatei »/usr/share/applications/google-chrome.desktop« mit der folgen Zeile erzwingen:

Abbildung 1: Die Web-GL-Einstellungen finden sich beim Browser Firefox unter »about:config«.

Abbildung 1: Die Web-GL-Einstellungen finden sich beim Browser Firefox unter »about:config«.

Exec=/opt/google/chrome/google-chrome--enable-webgl -ignore-gpu-blacklist %U

Details zur Grafikkarte zeigt Chrome unter der URL »chrome://gpu« .

Programmieren mit Open GL war nie ein Spaziergang, auch Web GL zeigt keinen großen Unterschied. Mit Javascript fällt anders als bei C oder Java zwar das Kompilieren weg, dennoch ist mit dieser Lowlevel-Bibliothek eine ganze Menge Code für das erste Erfolgserlebnis zu schreiben. Abhilfe schafft eine darüberliegende Bibliothek, die Standardaufgaben wie das Erstellen von Basisgeometrien oder das Laden von Modellen oder Texturen kapselt. Dieses Extra an Benutzerfreundlichkeit bietet die Javascript-Bibliothek Three.js [4].

Sie stammt von einem spanischen Programmierer, der sich “Mr. Doob” nennt, und steht unter MIT-Lizenz. Einen Überblick über die Möglichkeiten von Three.js bietet die umfangreiche Demoseite auf der Projekthomepage: vom einfachen Würfel über komplexe 3-D-Szenen bis hin zu Vertex- und Fragment-Shadern. Weitere Beispiele finden sich im Blog von Jerome Etienne [5] und auf der Seite “Learning Web GL” [6]. Diese Beispiele lassen die oft gehörten Bedenken über die Leistungsfähigkeit vergessen. Javascript kann es als Sprache zwar nicht mit der Performance von C++- oder Java aufnehmen, doch sobald die Daten in den Open-GL-Stack geladen sind, spielt das kaum mehr eine Rolle.

Web GL kommt auch noch nicht an die Leistung von jahrzehntelang gepflegten C++- [7] oder Java-Anwendungen [8] heran, doch Welten mit mehreren Hunderttausend Polygonen lassen sich sinnvoll nutzen und laufen dafür ohne jede Installation plattformübergreifend ab. Egal wie aufwändig die Szenen sind, alle Three.js-Programme durchlaufen dabei die gleichen Schritte:

  • Laden der Javascript-Bibliotheken
  • Erzeugen der Zeichenfläche
  • Erzeugen der Three.js-Szene
  • Starten der Eventloops

Ein einfaches Beispiel für diesen Ablauf ist in Listing 1 zu sehen, das die Webseite in Abbildung 2 erzeugt. Das Beispiel verwendet die Three.js-Hauptbibliothek sowie die Hilfsklasse »Detector.js« . Die HTML-Seite bindet die Bibliotheken mit dem »script« -Tag im Header ein. Die Zeilen 16 und 17 verwenden dazu eine absolute URL, auf dem eigenen Server verfügbare Bibliotheken lassen sich auch über einen relativen Pfad wie »js/Three.js« laden. Die von Three.js gezeichnete Fläche ist anders als bei Plugins und Applets kein Fremdkörper in der Webseite, der Browser behandelt sie als Teil des normalen HTML.

Listing 1

Ein einfaches Beispiel

001 <!doctype html>
002 <html>
003     <head>
004         <title>Three.js Beispiel</title>
005         <style type="text/css">
006             body {
007                 background-color: #ffffff;
008             }
009
010             #ThreeJSCanvas {
011                 background: #ababab;
012                 width: 800px;
013                 height: 600px;
014             }
015         </style>
016         <script src="https://github.com/mrdoob/three.js/raw/master/build/Three.js"></script>
017         <script src="https://github.com/mrdoob/three.js/raw/master/examples/js/Detector.js"></script>
018     </head>
019     <body>
020         <h1>WebGL Beispiel</h1>
021         Lorem ipsum dolor sit amet, consetetur ...
022
023         <div id="ThreeJSCanvas"></div>
024
025         <script>
026             var renderer;
027             var scene;
028             var camera;
029             var cameraControl;
030             var cubusMesh;
031
032             // Initialisiere die Szene
033             init();
034
035             // Animiere die Szene
036             animate();
037
038             function init(){
039                 console.log("init");
040
041                 // Wenn WebGL vom Browser unterstützt wird, nutze den
042                 // hardwarebeschleunigten WebGL-Renderer, ansonsten den
043                 // normalen CanvasRenderer
044                 if(Detector.webgl){
045                     renderer = new THREE.WebGLRenderer({antialias:true});
046                 } else {
047                     renderer = new THREE.CanvasRenderer();
048                 }
049
050                 // Hole das definierte DIV-Element anhand seiner ID
051                 var divElement = document.getElementById('ThreeJSCanvas');
052
053                 // Renderer auf Groesse des DIV-Elementes setzen
054                 var  canvasWidth = divElement.offsetWidth;
055                 var  canvasHeight = divElement.offsetHeight;
056
057                 renderer.setSize(canvasWidth, canvasHeight);
058
059                 // und an das DIV-Element anhängen
060                 divElement.appendChild(renderer.domElement);
061
062                 // Erzeuge die Szene in die alle Objekte eingefügt werden
063                 scene = new THREE.Scene();
064
065                 // Erzeuge die Kamera, positioniere sie
066                 // und richte auf Ursprung ein
067                 camera = new THREE.PerspectiveCamera(50, canvasWidth / canvasHeight, 1, 10000);
068                 camera.position.set(3, 4, 5);
069                 camera.lookAt( new THREE.Vector3( 0, 0,0));
070                 scene.add(camera);
071
072                 // Steuerung der Kamera mit der Maus
073                 cameraControl = new THREE.TrackballControls(camera, renderer.domElement);
074
075                 // Eine Lichtquelle definieren und einfügen
076                 var directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
077                 directionalLight.position = new THREE.Vector3( 20,20,20);
078                 scene.add(directionalLight);
079
080                 // Eine 5X5 große Ebene
081                 var planeGeo = new THREE.PlaneGeometry(5, 5, 20, 20);
082                 var planeMat = new THREE.MeshLambertMaterial({color: 0xef0000});
083                 var mesh = new THREE.Mesh(planeGeo, planeMat);
084                 mesh.doubleSided = true
085                 scene.add(mesh);
086
087                 // Ein Kubus
088                 var cubusGeo = new THREE.CubeGeometry( 1, 2, 4, 1,2,4); //new Three.new THREE.TorusGeometry( 1, 0.42 );
089                 var cubusMat = new THREE.MeshLambertMaterial( { color: 0x00ef00 } );
090                 cubusMesh = new THREE.Mesh( cubusGeo, cubusMat );
091                 cubusMesh.position = new THREE.Vector3( 0, 1.01, 0);
092
093                 scene.add( cubusMesh );
094
095
096             }
097
098             function animate(){
099
100                 // Drehen des Kubus
101                 cubusMesh.rotation.y =  1e-4* new Date().getTime();
102                 // Drehen der Kamera entsprechend Nutzereingaben
103                 cameraControl.update();
104
105                 // Darstellen der Szene
106                 renderer.render(scene, camera);
107
108                 // Nächstes Rendern nur wenn nötig
109                 requestAnimationFrame(animate);
110             }
111
112         </script>
113     </body>
114 </html>
Abbildung 2: Die Webseite aus <link href="#article_l1" class="listing" srcset=

Listing 1 erstellt eine einfache 3-D-Szene.” width=”300″ height=”286″ /> Abbildung 2: Die Webseite aus Listing 1 erstellt eine einfache 3-D-Szene.

Im Beispiel dient ein einfaches »div« -Element mit der ID »ThreeJSCanvas« als Malfläche. Der Entwickler kann dessen Position, Größe, Hintergrundfarbe und vieles mehr über Cascading Style Sheets (CSS) definieren. Im Beispiel sind unterschiedliche Hintergrundfarben für den 2-D- (Weiß) und 3-D-Teil (Grau) angegeben, ebenso wäre ein gemeinsames Hintergrundbild möglich.

Die eigentliche Implementierung des Beispiels findet im Javascript-Block ab Zeile 25 statt. Er ruft nacheinander die Funktionen zum Initialisieren der Szene und zum Starten der Eventloop auf. Die »init« -Funktion nutzt zuerst die Hilfsbibliothek »Detector.js« . Diese prüft, ob der Browser Web GL unterstützt, und zeigt dies über das Attribut »webgl« an. Falls dies zutrifft, kann das Skript den Hardware-beschleunigten Web-GL-Renderer verwenden, ansonsten erzeugt es den »CanvasRenderer« . Der funktioniert auch auf Browsern ohne Web GL, bietet aber deutlich weniger Features.

Als Nächstes ist es erforderlich, den Renderer mit dem HTML-Element zu verbinden. Zeile 51 fragt dazu das »div« -Element anhand der ID aus dem DOM (Document Object Model) ab. Die folgenden Zeilen ermitteln die Größe des Elements und übergeben sie dem Renderer. In Zeile 60 fügt das Skript das DOM-Element des Renderers ins »div« -Element ein.

Damit ist der 3-D-Teil in die HTML-Seite eingebettet. Jetzt geht es an die Definition der 3-D-Szene. Sie ist durch ein »THREE.Scene« -Objekt repräsentiert, das als Vaterknoten für die darzustellende Geometrie sowie Kamera und Lichter dient.

Kamera läuft

Der Beispielcode behandelt zuerst die Kamera (ab Zeile 67). Three.js bietet drei Kameraklassen: eine mit perspektivischer Projektion (»PerspectiveCamera« ), eine mit orthogonaler (»OrthographicCamera« ) sowie eine, die zwischen beiden Projektionen umschalten kann (»CombinedCamera« ). Beim Erzeugen der Kamera gibt der Programmierer den Blickwinkel (»fov« ), das Seitenverhältnis sowie die minimale (»near« ) und maximale Distanz (»far« ) zur Geometrie an. Damit definiert er das Frustum, also den pyramidenförmigen Sichtbereich in die Szene, dessen Inhalt der Browser rendert. Abbildung 3 zeigt das Frustum mit den jeweiligen Three.js-Parametern. Das Beispiel in [9] demonstriert einige dieser Werte.

Abbildung 3: Das Frustum bestimmt den Blick in die 3-D-Welt. Three.js bildet die Eigenschaften der Sichtpyramide in Javascript-Variablen ab.

Abbildung 3: Das Frustum bestimmt den Blick in die 3-D-Welt. Three.js bildet die Eigenschaften der Sichtpyramide in Javascript-Variablen ab.

In vielen Fällen soll der Anwender die Kameraposition und -blickrichtung steuern können. Three.js bietet dafür fertige Lösungen, zum Beispiel das in Zeile 73 definierte »TrackballControls()« . Diese Steuerung zeigt das allgemein übliche Verhalten: Drehen der Kamera bei gedrückter linker Maustaste, Zoomen bei gedrückter mittlerer, Verschieben bei gedrückter rechter Maustaste. Weitere Controls wie das »FirstPersonControl()« (Durchwandern der Szene wie in einem Shooter) sind verfügbar oder lassen sich programmieren.

Eine Kamera kann im Dunkeln wenig ausrichten, deshalb definiert der Code ab Zeile 76 eine Lichtquelle und weist ihr eine Position zu. Sie strahlt weißes Licht (»0xffffff« ) mit voller Intensität (»1.0« ) aus. Im Beispiel ist das Licht fest in der Szene positioniert.

Nun fehlt noch die 3-D-Welt selbst. Listing 1 erzeugt mittels Javascript ab Zeile 81 eine Ebene und ab Zeile 88 einen Kubus. Für jeden Körper definiert der Code zuerst die Geometrie und erzeugt dann zusammen mit einem Material (»Mesh« ) die Oberfläche. Das Mesh wird ähnlich wie zuvor die Kamera positioniert und in die Szene eingehängt.

Das Material entscheidet über das Aussehen des Körpers: Neben einfachen Farben kann der Entwickler auch Texturen oder Shader für die Oberflächen nutzen. Damit ist die Definition der 3-D-Welt abgeschlossen und die Eventloop kann starten.

Innerhalb der Eventloop bearbeitet der Code in regelmäßigen Intervallen Nutzereingaben oder animiert die Szene, bevor ein neues Bild aus der 3-D-Szene gerendert wird. Dafür ist im Beispiel die Funktion »animate()« zuständig, die das erste Mal in Zeile 36 nach der Definition der Szene zum Zuge kommt. Damit aus dem einmaligen Aufruf eine echte Eventloop wird, kommt die Funktion »requestAnimationFrame();« zum Einsatz. Sie wird vom Browser verwaltet und sorgt für den regelmäßigen Aufruf der »animate()« -Funktion. Dabei unterbricht sie auch die Eventloop, etwa wenn das Fenster gerade nicht sichtbar ist, und spart dadurch Systemressourcen.

Zuvor rotiert Listing 1 jedoch den Kubus um seine y-Achse und aktualisiert die Kameraposition entsprechend den Nutzereingaben. Auch wenn sie auf den ersten Blick ähnlich erscheinen, handelt es sich dabei um zwei unterschiedliche Bewegungen: Die eine dreht einen Körper in der Szene, die andere den Blick auf die Szene. Nachdem die 3-D-Szene entsprechend aktualisiert ist, ruft Zeile 106 den Renderer auf, um sie in einer 2-D-Ansicht abzubilden.

Nur Ebenen und Kuben, das wäre ein wenig langweilig – für das Erzeugen der 3-D-Szenen stellt Three.js weitere Möglichkeiten bereit. Es kann Geometrieprimitive (Dreiecke, Kuben, Zylindern, Kugeln, Linien) erzeugen sowie Flächen zu Volumenkörpern extrudieren. Daneben lädt die Bibliothek extern definierte Geometrien und lässt Vertex-Shader auf der Grafikkarte berechnen. Gerade das Laden externer Geometrien macht Three.js für viele Anwendungsfälle attraktiv, denn es erlaubt das Einbinden leistungsfähiger Modellierwerkzeuge wie Blender oder eines CAD-Systems.

Abhängig vom Format führt der Weg dabei entweder über einen Three.js-Loader für Fremdformate wie Object Wavefront, Collada und VTK oder über einen Konverter, der Dateien in das Json-Format von Three.js übersetzt. Dabei handelt es sich um eine Ascii-Datei, in der die Punkte, Flächen, Normalen und Farben definiert sind. Selbst animierte Modelle wie beispielsweise laufende Personen lassen sich in solchen Dateien ablegen.

Geometrie-Import

Im Quelltext von Three.js finden sich unter »utils/exporters« sowohl Konverter für die Kommandozeile als auch Plugins für Blender und 3DS Max. Weitere Konverter lassen sich relativ schnell programmieren: Das Motorrad in Abbildung 4 haben die Autoren mit einem selbst geschriebenen Konverter aus dem in der CAD-Welt verbreiteten JT-Format nach Json exportiert. Das Laden der Json-Dateien ist in Listing 2 zu sehen. Die Funktion »load()« nimmt sowohl die URL der Json-Datei als auch eine Callback-Funktion für die weitere Behandlung nach dem Laden entgegen. Diese Funktion definiert ähnlich wie die manuell erstellte Geometrie aus Listing 1 das Material, erzeugt ein Mesh und fügt es der Szene hinzu.

Listing 2

Json-Daten laden

01 var loader = new THREE.JSONLoader();
02 loader.load("mini.json", function(geometry){
03     var material = new THREE.MeshLambertMaterial( { color: 0x00efef } );
04
05     mesh = new THREE.Mesh(geometry, material);
06
07     mesh.doubleSided = true;
08     THREE.GeometryUtils.center(geometry);
09     scene.add(mesh);
10                          });
Abbildung 4: Komplexe Modelle inklusive Schatten stehen ohne Plugin im Browser bereit.

Abbildung 4: Komplexe Modelle inklusive Schatten stehen ohne Plugin im Browser bereit.

Über das Mesh-Attribut »doubleSided« kann man das so genannte Backface Culling deaktivieren, sodass beide Seiten der Fläche dargestellt werden. Ohne diesen Parameter würde nur jene Seite gerendert, deren Normalenvektor zur Kamera schaut. Sind die Normalenvektoren nicht einheitlich, können dabei Löcher in der Oberfläche entstehen. Die »GeometryUtils« verschieben die Geometrie in den Koordinatenursprung, ansonsten wird sie in der Originalposition positioniert und ist eventuell nach dem Laden nicht sichtbar.

Beim Laden externer Geometrie greifen die normalen Sicherheitsmechanismen der Browser. Chrome etwa lässt es nur zu, wenn die HTML-Seite und Json-Daten vom selben Server kommen. Das Laden direkt von der lokalen Festplatte ist nur möglich, wenn Chrome mit der Option »-disable-web-security« startet.

Dieser Artikel hat die Möglichkeiten des vielseitigen Three.js gerade mal angekratzt: Neben explizit erzeugter oder geladener Geometrie unterstützt die Bibliothek auch Visualisierungstechniken wie Sprites und bietet ein Partikelsystem. Darüber hinaus lassen sich Geometrie und Farben mit Vertex- und Fragment-Shadern bis ins letzte Detail gestalten [10]. Anwendungen wie Datenvisualisierungen [11] oder das Spiel “Zombie versus Cow” [12] demonstrieren, was möglich ist (Abbildungen 5 und 6).

Abbildung 5: Die Visualisierung der Daten zur Bevölkerungsverteilung kombiniert 2-D- und 3-D-Anteile.

Abbildung 5: Die Visualisierung der Daten zur Bevölkerungsverteilung kombiniert 2-D- und 3-D-Anteile.

Abbildung 6: Auch Spiele wie "Zombie versus Cow" verwenden Three.js.

Abbildung 6: Auch Spiele wie “Zombie versus Cow” verwenden Three.js.

Bei der Umsetzung von Web GL in den Browsern, der Weiterentwicklung von Three.js selbst und auch in der Konverter-Welt wird sich in nächster Zeit noch einiges tun. Bei Spielen etwa durch Physik-Engines wie Physijs [13], in der CAD-Welt durch die CSG-Bibliotheken (Constructive Solid Geometry, Verknüpfung von Grundkörper über boolesche Operationen, [14]).

Bei der Entwicklung helfen die üblichen Javascript-Werkzeuge wie »dat-gui« [15] oder die Konsolen in Firefox und Chrome. Der Web-GL-Inspector [16] kann tief in den Open-GL-Stack blicken. Alternativ lässt sich mit Threenodes.js aber auch [17] die komplette 3-D-Szene per Drag&Drop definieren.

Bereits jetzt ist der Einstieg in die 3-D-Programmierung mit Three.js so einfach wie mit kaum einer anderen Technik. Die Verbreitung von Web-GL-fähigen Browsern sowohl auf dem Desktop wie auch auf mobilen Geräten ist schon jetzt recht gut und wird in den nächsten Jahren zunehmen. Gerade aus der Kombination von 2-D und 3-D ergeben sich zunehmend Anwendungsideen, die weit jenseits der Demo-Szene oder des Spielebereichs liegen. Die Autoren dieses Artikels arbeiten zum Beispiel an einem Viewer names CADoculus, der CAD-Daten im Webbrowser anzeigt [18]. (mhu)

Der Autor

Johannes Raida und Carsten Zerbst erstellen Individual-Software für die Automobil-, Luft- und Raumfahrtindustrie. Ihr besonderer Schwerpunkt ist Software für CAD, PDM und Datenaustausch.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 6 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Nach oben