Einführung in den 3D-Globus „Weltwunder“
Wenn Sie die vor Kurzem veröffentlichte Website Google World Wonders in einem WebGL-fähigen Browser aufgerufen haben, haben Sie möglicherweise unten auf dem Bildschirm einen schick rotierenden Globus gesehen. In diesem Artikel erfahren Sie, wie der Globus funktioniert und mit welchen Tools er erstellt wurde.
Der Globus „Weltwunder“ ist eine stark modifizierte Version des WebGL-Globus, die vom Google Data Arts-Team erstellt wurde. Wir haben den ursprünglichen Globus genommen, die Balkendiagramme entfernt, die Shader geändert, ausgefallene anklickbare HTML-Markierungen und die Kontinentgeometrie von Natural Earth aus der GlobeTweeter-Demo von Mozilla hinzugefügt (vielen Dank an Cedric Pinson!). So entsteht ein schöner animierter Globus, der zum Farbschema der Website passt und ihr eine zusätzliche Raffinesse verleiht.
Die Designvorgabe für den Globus bestand darin, eine ansprechende animierte Karte mit anklickbaren Markierungen zu erstellen, die auf Welterbestätten platziert werden. Mit diesem Ziel vor Augen begann ich, nach etwas Passendem zu suchen. Als Erstes kam mir der WebGL-Globus in den Sinn, der vom Google Data Arts Team erstellt wurde. Es ist ein Globus und sieht cool aus. Was benötigen Sie sonst noch?
WebGL-Globus einrichten
Der erste Schritt beim Erstellen des Globus-Widgets bestand darin, den WebGL-Globus herunterzuladen und einzurichten. Der WebGL-Globus ist online bei Google Code verfügbar und kann einfach heruntergeladen und ausgeführt werden. Laden Sie die ZIP-Datei herunter und entpacken Sie sie. Wechseln Sie dann in das Verzeichnis und führen Sie einen einfachen Webserver aus: python -m SimpleHTTPServer
. Hinweis: UTF-8 ist hier standardmäßig nicht aktiviert. Sie können es aber verwenden. Wenn Sie jetzt zu http://localhost:8000/globe/globe.html
wechseln, sollte der WebGL-Globus angezeigt werden.
Nachdem der WebGL-Globus einsatzbereit war, war es an der Zeit, alle nicht benötigten Teile zu entfernen. Ich habe den HTML-Code bearbeitet, um die UI-Bits zu entfernen, und die Einrichtung des Globus-Balkendiagramms aus der Globus-Initialisierungsfunktion entfernt. Am Ende dieses Prozesses hatte ich einen sehr einfachen WebGL-Globus auf dem Bildschirm. Sie können es drehen und es sieht cool aus, aber das war's auch schon.
Um unnötige Elemente zu entfernen, habe ich alle UI-Elemente aus der index.html des Globus gelöscht und das Initialisierungsskript so bearbeitet, dass es so aussieht:
if(!Detector.webgl){
Detector.addGetWebGLMessage();
} else {
var container = document.getElementById('container');
var globe = new DAT.Globe(container);
globe.animate();
}
Geometrie des Kontinents hinzufügen
Wir wollten die Kamera nah an der Oberfläche des Globus positionieren. Beim Testen des Globus im Zoommodus wurde jedoch deutlich, dass die Texturauflösung nicht ausreichte. Bei einem Zoom wird die Textur des WebGL-Globus blockig und verschwommen. Wir hätten ein größeres Bild verwenden können, aber das würde das Herunterladen und Ausführen des Globus verlangsamen. Deshalb haben wir uns für eine Vektordarstellung der Landmassen und Grenzen entschieden.
Für die Geometrie der Landmasse habe ich die Open-Source-Demo GlobeTweeter verwendet und das 3D‑Modell in Three.js geladen. Nachdem das Modell geladen und gerendert wurde, war es an der Zeit, den Look des Globus zu optimieren. Das erste Problem war, dass das Modell der Landmasse des Globus nicht kugelförmig genug war, um bündig mit dem WebGL-Globus abzuschließen. Deshalb habe ich einen schnellen Algorithmus zum Aufteilen von 3D-Meshes geschrieben, der das Modell der Landmasse kugelförmiger machte.
Mit einem kugelförmigen Modell der Landmasse konnte ich es nur leicht von der Oberfläche des Globus abheben und so schwebende Kontinente mit einem schwarzen 2-Pixel-Strich darunter als Schatten erzeugen. Ich habe auch mit neonfarbenen Konturen experimentiert, um einen Art Tron-Look zu erzielen.
Beim Rendern des Globus und der Landmassen habe ich mit verschiedenen Looks für den Globus experimentiert. Da wir einen dezenten, einfarbigen Look wollten, habe ich mich für einen Graustufenglobus und -landmassen entschieden. Zusätzlich zu den oben genannten Neonkonturen habe ich einen dunklen Globus mit dunklen Landmassen auf einem hellen Hintergrund ausprobiert, was ziemlich cool aussieht. Der Kontrast war jedoch zu gering, um gut lesbar zu sein, und er passte nicht zum Stil des Projekts. Deshalb habe ich ihn verworfen.
Ein weiterer früher Gedanke für den Globus war, ihn wie glasiertes Porzellan aussehen zu lassen. Diese konnte ich nicht ausprobieren, da ich keinen Shader für den Porzellanlook schreiben konnte (ein visueller Materialeditor wäre schön). Das Beste, was ich gefunden habe, war ein weißer glühender Globus mit schwarzen Landmassen. Es ist ziemlich cool, aber der Kontrast ist zu hoch. Außerdem sieht es nicht besonders schön aus. Also noch ein Projekt auf den Schrottplatz.
Die Shader in den schwarzen und weißen Globen verwenden eine Art irreführende diffuse Hintergrundbeleuchtung. Die Helligkeit des Globus hängt von der Entfernung der Oberfläche senkrecht zur Bildschirmebene ab. Die Pixel in der Mitte des Globus, die auf das Display gerichtet sind, sind also dunkel und die Pixel an den Rändern des Globus sind hell. In Kombination mit einem hellen Hintergrund wird der diffuse, helle Hintergrund im Globus reflektiert, was einen eleganten Showroom-Look erzeugt. Für den schwarzen Globus wird auch die WebGL-Globustextur als Glanzkarte verwendet, sodass die Kontinentalschelfe (seichte Wassergebiete) im Vergleich zu den anderen Teilen des Globus glänzend erscheinen.
So sieht der Ozean-Shader für den schwarzen Globus aus. Ein sehr einfacher Vertex-Shader und ein stümperhafter Fragment-Shader, der so aussieht: „Oh, das sieht ganz nett aus tweak tweak“.
'ocean' : {
uniforms: {
'texture': { type: 't', value: 0, texture: null }
},
vertexShader: [
'varying vec3 vNormal;',
'varying vec2 vUv;',
'void main() {',
'gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );',
'vNormal = normalize( normalMatrix * normal );',
'vUv = uv;',
'}'
].join('\n'),
fragmentShader: [
'uniform sampler2D texture;',
'varying vec3 vNormal;',
'varying vec2 vUv;',
'void main() {',
'vec3 diffuse = texture2D( texture, vUv ).xyz;',
'float intensity = pow(1.05 - dot( vNormal, vec3( 0.0, 0.0, 1.0 ) ), 4.0);',
'float i = 0.8-pow(clamp(dot( vNormal, vec3( 0, 0, 1.0 )), 0.0, 1.0), 1.5);',
'vec3 atmosphere = vec3( 1.0, 1.0, 1.0 ) * intensity;',
'float d = clamp(pow(max(0.0,(diffuse.r-0.062)*10.0), 2.0)*5.0, 0.0, 1.0);',
'gl_FragColor = vec4( (d*vec3(i)) + ((1.0-d)*diffuse) + atmosphere, 1.0 );',
'}'
].join('\n')
}
Letztendlich haben wir uns für einen dunklen Globus mit hellgrauen Landmassen entschieden, die von oben beleuchtet werden. Es entsprach am ehesten dem Design-Briefing und sah gut und lesbar aus. Außerdem stechen die Markierungen und der Rest des Inhalts im Vergleich zum Globus mit etwas geringerem Kontrast besser hervor. In der Version unten sind die Ozeane komplett schwarz, während die Produktionsversion dunkelgraue Ozeane und leicht andere Markierungen hat.
Markierungen mit CSS erstellen
Apropos Markierungen: Nachdem der Globus und die Landmassen funktionierten, habe ich mit den Markierungen begonnen. Ich entschied mich für HTML-Elemente im CSS-Stil für die Markierungen, um die Erstellung und Formatierung der Markierungen zu vereinfachen und sie gegebenenfalls in der 2D-Karte wiederzuverwenden, an der das Team gerade arbeitete. Damals kannte ich auch keine einfache Möglichkeit, die WebGL-Markierungen anklickbar zu machen, und wollte keinen zusätzlichen Code zum Laden / Erstellen der Markierungsmodelle schreiben. Im Nachhinein betrachtet haben die CSS-Markierungen gut funktioniert, aber es kam gelegentlich zu Leistungsproblemen, wenn sich die Browser-Kompositoren und ‑Renderer in einer Phase des Wandels befanden. Aus Leistungsgründen wäre es besser gewesen, die Markierungen in WebGL zu erstellen. Andererseits haben die CSS-Markierungen viel Entwicklungszeit gespart.
Die CSS-Markierungen bestehen aus mehreren Divs, die mit der CSS-Eigenschaft „transform“ absolut positioniert sind. Der Hintergrund der Markierungen ist ein CSS-Gradient und der Dreiecksteil der Markierung ist ein gedrehtes div-Element. Die Markierungen haben einen kleinen Schatten, damit sie sich vom Hintergrund abheben. Das größte Problem bei den Markierungen bestand darin, eine ausreichende Leistung zu erzielen. So traurig es klingt: Wenn Sie einige Dutzend divs zeichnen, die sich bewegen und in jedem Frame ihren Z-Index ändern, können Sie damit alle möglichen Probleme beim Browser-Rendering auslösen.
Die Synchronisierung der Markierungen mit der 3D-Szene ist nicht allzu kompliziert. Jede Markierung hat ein entsprechendes Object3D in der Three.js-Szene, mit dem die Markierungen erfasst werden. Um die Bildschirmraumkoordinaten zu erhalten, nehme ich die Three.js-Matrizen für den Globus und die Markierung und multipliziere sie mit einem Nullvektor. Daraus erhalte ich die Szenenposition der Markierung. Um die Bildschirmposition der Markierung zu erhalten, projiziere ich die Szenenposition durch die Kamera. Der resultierende projizierte Vektor enthält die Bildschirmraumkoordinaten für die Markierung, die in CSS verwendet werden können.
var mat = new THREE.Matrix4();
var v = new THREE.Vector3();
for (var i=0; i<locations.length; i++) {
mat.copy(scene.matrix);
mat.multiplySelf(locations[i].point.matrix);
v.set(0,0,0);
mat.multiplyVector3(v);
projector.projectVector(v, camera);
var x = w * (v.x + 1) / 2; // Screen coords are between -1 .. 1, so we transform them to pixels.
var y = h - h * (v.y + 1) / 2; // The y coordinate is flipped in WebGL.
var z = v.z;
}
Am schnellsten ging es letztendlich mit CSS-Transformationen, um die Markierungen zu verschieben. Das Ausblenden der Deckkraft war zu langsam und führte zu Problemen in Firefox. Außerdem wurden alle Markierungen im DOM beibehalten und nicht entfernt, wenn sie hinter dem Globus verschwanden. Wir haben auch mit 3D-Transformationen anstelle von Z-Indexen experimentiert, aber aus irgendeinem Grund funktionierte das in der App nicht richtig (in einem reduzierten Testfall funktionierte es aber, wer weiß warum). Da wir zu diesem Zeitpunkt nur noch wenige Tage vor der Veröffentlichung waren, mussten wir diesen Teil der Wartung nach der Veröffentlichung überlassen.
Wenn Sie auf eine Markierung klicken, wird eine Liste mit anklickbaren Ortsnamen angezeigt. Das ist alles ganz normaler HTML-DOM-Code, daher war es super einfach, ihn zu schreiben. Alle Links und das Text-Rendering funktionieren ohne zusätzliche Arbeit von uns.
Dateigröße verkleinern
Die Demo funktionierte und war mit dem Rest der Website „Weltwunder“ verbunden, aber es gab noch ein großes Problem zu lösen. Das JSON-Format-Mesh für die Landmassen der Erde war etwa 3 MB groß. Nicht geeignet für die Startseite einer Portfolio-Website. Zum Glück konnte ich das Mesh mit gzip komprimieren und so auf 350 KB verkleinern. Aber 350 KB ist immer noch etwas groß. Ein paar E-Mails später konnten wir Won Chun gewinnen, der an der Komprimierung der riesigen Google Body-Meshes gearbeitet hatte, uns bei der Komprimierung des Mesh zu helfen. Er verkleinerte das Mesh von einer großen flachen Liste von Dreiecken, die als JSON-Koordinaten angegeben waren, auf komprimierte 11-Bit-Koordinaten mit indexierten Dreiecken und reduzierte die Dateigröße auf 95 KB (GZIP).
Durch die Verwendung komprimierter Netze wird nicht nur die Bandbreite gespart, sondern die Netze können auch schneller geparst werden. Das Umwandeln von 3 MiB Stringzahlen in native Zahlen ist wesentlich aufwendiger als das Parsen von 100 kB Binärdaten. Die daraus resultierende Größenreduzierung der Seite um 250 kB ist sehr praktisch, da die anfängliche Ladezeit bei einer Verbindung mit 2 Mbit/s unter einer Sekunde liegt. Schneller und kleiner, einfach super!
Gleichzeitig habe ich herumgespielt, um die ursprünglichen Natural Earth-Shapefiles zu laden, aus denen das GlobeTweeter-Mesh abgeleitet wird. Ich habe es geschafft, die Shapefiles zu laden, aber um sie als flache Landmassen zu rendern, müssen sie trianguliert werden (natürlich mit Löchern für Seen). Ich habe die Formen mithilfe von THREE.js-Utils trianguliert, aber nicht die Löcher. Die resultierenden Meshes hatten sehr lange Kanten, was das Mesh in kleinere Dreiecke aufteilen erforderte. Kurz gesagt: Ich habe es nicht rechtzeitig geschafft, es zum Laufen zu bringen. Das Tolle daran war jedoch, dass das weiter komprimierte Shapefile-Format ein Landmassenmodell von 8 kB ergeben hätte. Schade, vielleicht beim nächsten Mal.
Zukünftige Arbeit
Die Markierungsanimationen könnten noch etwas schöner sein. Wenn sie jetzt über den Horizont gehen, wirkt das etwas kitschig. Außerdem wäre eine coole Animation für das Öffnen der Markierung schön.
Leistungstechnisch fehlen noch zwei Dinge: die Optimierung des Algorithmus zum Aufteilen von 3D‑Meshes und die Beschleunigung der Markierungen. Abgesehen davon geht es mir gut. Hurra!
Zusammenfassung
In diesem Artikel habe ich beschrieben, wie wir den 3D-Globus für das Google World Wonders-Projekt erstellt haben. Ich hoffe, dass Ihnen die Beispiele gefallen haben und Sie Ihr eigenes benutzerdefiniertes Globus-Widget erstellen werden.