Making of the World Wonders 3D-Globus

Ilmari Heikkinen

Einführung in den 3D-Globus der World Wonders

Wenn Sie sich die kürzlich veröffentlichte Google World Wonders-Website in einem WebGL-fähigen Browser angesehen haben, haben Sie möglicherweise am unteren Bildschirmrand einen sich drehenden Globus entdeckt. In diesem Artikel erfahren Sie, wie der Globus funktioniert und wie er gebaut wurde.

Der Globus World Wonders ist eine stark überarbeitete Version des WebGL-Globus des Google Data Arts-Teams. Wir haben den ursprünglichen Globus genommen, die Balkendiagramme entfernt, die Shader geändert, ausgefallene anklickbare HTML-Markierungen und Natural Earth-Kontinentgeometrie aus der GlobeTweeter-Demo von Mozilla hinzugefügt (großartig dank Cedric Pinson!) All dies dient der Erstellung eines ansprechenden animierten Globus, der zum Farbschema der Website passt und die Website noch raffinierter macht.

Der Entwurf für den Globus sah vor, eine gut aussehende animierte Karte mit anklickbaren Markierungen über den UNESCO-Welterbestätten zu erstellen. Vor diesem Hintergrund fing ich an, nach etwas Passendem zu suchen. Als Erstes kam Ihnen der WebGL-Globe ein, der vom Google Data Arts-Team entwickelt wurde. Es ist ein Globus und sieht cool aus. Was brauchst du noch, oder?

WebGL-Globus einrichten

Bei der Erstellung des Globus-Widgets bestand der erste Schritt darin, den WebGL-Globus herunterzuladen und zu starten. Der WebGL-Globus ist online bei Google Code verfügbar und kann einfach heruntergeladen und ausgeführt werden. Laden Sie die ZIP-Datei herunter, entpacken Sie sie mit CD und führen Sie einen Basis-Webserver aus: python -m SimpleHTTPServer. Hinweis: UTF-8 ist nicht standardmäßig aktiviert. Sie können diese Option verwenden. Wenn Sie jetzt http://localhost:8000/globe/globe.html öffnen, sollten Sie den WebGL-Globus sehen.

Als der WebGL-Globus betriebsbereit war, war es an der Zeit, alle nicht benötigten Teile zu kürzen. Ich habe den HTML-Code bearbeitet, um die UI-Elemente zu extrahieren, und habe die Einrichtung des Globus-Balkendiagramms aus der Globus-Initialisierungsfunktion entfernt. Am Ende dieses Prozesses hatte ich einen sehr einfachen WebGL-Globus auf meinem Bildschirm. Drehen und es sieht cool aus, aber das war's auch schon.

Um die unnötigen überflüssigen Elemente zu entfernen, habe ich alle UI-Elemente aus der Datei index.html des Globus gelöscht und das Initialisierungsskript so bearbeitet:

if(!Detector.webgl){
  Detector.addGetWebGLMessage();
} else {
  var container = document.getElementById('container');
  var globe = new DAT.Globe(container);
  globe.animate();
}

Kontinentgeometrie hinzufügen

Wir wollten die Kamera nahe an der Globusoberfläche platzieren, aber als wir den Globus testeten, wurde die fehlende Texturauflösung deutlich. Beim Heranzoomen wird die Textur des WebGL-Globus stark blockt und verschwommen. Wir hätten ein größeres Bild verwenden können, aber dadurch wird der Download und die Ausführung des Globus langsamer. Daher entschieden wir uns für eine Vektordarstellung der Landmasse und Grenzen.

Für die Landmassegeometrie habe ich die Open-Source-Demo GlobeTweeter genutzt und das 3D-Modell darin in Three.js geladen. Nachdem das Modell geladen war und gerendert worden war, war es an der Zeit, das Aussehen des Globus anzupassen. Das erste Problem bestand darin, dass das Modell der Landmasse des Globus nicht sphärisch genug war, um mit dem WebGL-Globus abzugleichen. Daher schrieb ich einen schnellen Algorithmus zur Aufteilung des Mesh-Netzwerks, der das Modell der Landmasse sphärischer macht.

Mit einem sphärischen Landmassemodell konnte ich es nur leicht versetzt von der Globusoberfläche platzieren, wodurch schwebende Kontinente entstanden, die mit einer schwarzen 2-Pixel-Linie darunter umrissen wurden. Außerdem habe ich neonfarbene Konturen ausprobiert, um einen Tron-ähnlichen Look zu verleihen.

Mit der Darstellung des Globus und der Landmasse begann ich, mit verschiedenen Designs für den Globus zu experimentieren. Als wir uns für einen dezenten monochromen Look entschieden hatten, blieb ich bei einem Graustufen-Globus und einer Landmasse. Zusätzlich zu den oben erwähnten Neonumrissen habe ich einen dunklen Globus mit dunkler Landmasse auf hellem Hintergrund ausprobiert, der eigentlich ziemlich cool aussieht. Aber der Kontrast war zu gering, um leicht lesbar zu sein, und er entsprach nicht dem Gefühl des Projekts, also habe ich das entfernt.

Früher dachte ich, der Globus-Look sollte auch wie glasiertes Porzellan aussehen. Diesen habe ich nicht ausprobiert, da ich keinen Shader für den Porzellan-Look geschrieben habe (visueller Material-Editor wäre schön). Am allerbesten versuchte ich es, diesen weißen leuchtenden Globus mit schwarzer Landmasse zu sehen. Das ist zwar ordentlich, aber kontrastreich. Und sie sieht nicht besonders gut aus. Also noch eine für den Scrapheap.

Die Shader in den schwarzen und weißen Kugeln verwenden eine Art diffuse diffuse Hintergrundbeleuchtung. Die Helligkeit des Globus hängt von der Entfernung der Oberfläche normal zur Bildschirmebene ab. Die Pixel in der Mitte des Globus, die auf den Bildschirm zeigen, sind also dunkel und Pixel an den Rändern des Globus hell. Durch die Kombination mit einem hellen Hintergrund bekommst du einen Eindruck davon, wie der Globus den diffusen, hellen Hintergrund reflektiert. So entsteht ein stilvoller Showroom-Look. Der schwarze Globus verwendet auch die WebGL Globe-Textur als Glanzkarte, damit die Kontinentalregale (oberflächliche Wasserflächen) im Vergleich zu anderen Teilen der Erde glänzend aussehen.

So sieht der Ozean-Shader für den schwarzen Globus aus. Sehr einfacher Vertex-Shader und ein hackiger Fragment-Shader „Oh, das sieht irgendwie cooler Tweak Tweak“-Shader aus.

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

Am Ende nahmen wir einen dunklen Globus mit hellgrauer Landmasse, die von oben beleuchtet wurde. Es entsprach dem Design-Briefing und sah gut und gut lesbar aus. Darüber hinaus heben sich die Markierungen und der Rest des Inhalts im Vergleich besser ab, wenn der Globus etwas kontrastarm ist. Die folgende Version verwendet komplett schwarze Ozeane, während die Produktionsversion dunkelgraue Ozeane und leicht unterschiedliche Markierungen aufweist.

Markierungen mit CSS erstellen

Apropos Markierungen, ich begann mit der Arbeit an den Ortsmarken, während der Globus und die Landmasse in Betrieb waren. Für die Markierungen entschied ich mich für HTML-Elemente im CSS-Stil, um die Erstellung und den Stil der Markierungen zu erleichtern und um die Markierungen möglicherweise in der 2D-Karte wiederzuverwenden, an der das Team gearbeitet hat. Damals wusste 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 funktionierten die CSS-Markierungen gut, hatten aber die Tendenz, gelegentlich Leistungsprobleme zu verursachen, wenn sich Browser-Compositors und Renderer in Phasen der Fluktuation befanden. Was die Leistung anbelangt, wären die Markierungen in WebGL besser geeignet gewesen. Die CSS-Markierungen haben wiederum eine Menge Entwicklungszeit gespart.

Die CSS-Markierungen bestehen aus ein paar div-Elementen, die absolute Position über die CSS-Eigenschaft "transform" haben. Der Hintergrund der Markierungen ist ein CSS-Farbverlauf und der Dreiecksteil der Markierung ist ein gedrehtes div-Element. Die Markierungen haben einen kleinen Schlagschatten, um sie vom Hintergrund abzuheben. Das größte Problem bei den Markierungen bestand darin, dass sie eine gute Leistung erzielen. Es ist zwar sehr traurig, aber das Zeichnen von ein paar Dutzenden div-Elementen, die sich bewegen und ihren Z-Index auf jedem Frame ändern, ist eine ziemlich gute Methode, um alle möglichen Fallstricke beim Browser-Rendering auszulösen.

Die Synchronisierung der Markierungen mit der 3D-Szene ist nicht allzu kompliziert. Jede Markierung verfügt über ein entsprechendes Object3D in der Three.js-Szene, das zum Verfolgen der Markierungen verwendet wird. Um die Koordinaten des Bildschirmraums zu erhalten, nehme ich die Three.js-Matrizen für den Globus und die Markierung und multipliziere einen Nullvektor mit diesen. Daraus ermittle ich die Szenenposition der Markierung. Um die Bildschirmposition der Markierung zu ermitteln, projiziere ich die Szenenposition durch die Kamera. Der resultierende projizierte Vektor verfügt über die Bildschirmabstandskoordinaten 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;
}

Letztendlich war es am schnellsten, CSS-Transformationen zum Verschieben der Markierungen zu verwenden, nicht das Ausblenden der Deckkraft, da dies einen langsamen Pfad in Firefox auslöste, und alle Markierungen im DOM behielten, anstatt sie zu entfernen, wenn sie hinter dem Globus verlaufen. Wir haben auch mit 3D-Transformationen anstelle von Z-Indexen experimentiert, aber aus irgendeinem Grund funktionierte dies nicht richtig in der App (aber in einem reduzierten Testfall funktionierte es). Wir waren zu diesem Zeitpunkt ein paar Tage nach dem Start und mussten diesen Teil der Wartung nach der Markteinführung überlassen.

Wenn Sie auf eine Markierung klicken, wird eine Liste anklickbarer Ortsnamen geöffnet. Das ist alles normales HTML DOM-Ziffern, sodass es sehr einfach zu schreiben war. Alle Links und das Textrendering funktionieren ohne zusätzlichen Aufwand von unserer Seite.

Dateigröße komprimieren

Während die Demo funktionierte und mit dem Rest der World Wonders-Website verknüpft war, musste noch ein großes Problem gelöst werden. Das Mesh-Netzwerk im JSON-Format für die Globuslandmasse war etwa 3 MB groß. Nicht gut für die Titelseite einer Showcase-Website geeignet. Das Gute war, dass die Komprimierung des Mesh-Netzwerks mit gzip auf 350 KB reduziert wurde. Aber 350 KB sind immer noch ein bisschen groß. Ein paar E-Mails später gelang es uns, Won Chun zu rekrutieren, der an der Komprimierung der riesigen Google Body-Mesh-Netzwerke arbeitete, um uns beim Komprimieren des Mesh-Netzwerks zu helfen. Er drückte das Mesh-Netzwerk aus einer großen, flachen Liste von Dreiecken, die als JSON-Koordinaten angegeben waren, zu komprimierten 11-Bit-Koordinen mit indexierten Dreiecken und komprimiert die Dateigröße mit gzip auf 95 KB.

Mit komprimierten Mesh-Netzwerken sparen Sie nicht nur Bandbreite, sondern die Mesh-Netzwerke lassen sich auch schneller parsen. Das Umwandeln von 3 MB an Zeichenfolgen in native Zahlen ist viel aufwendiger, als beim Parsen von hundert KB an Binärdaten. Die daraus resultierende Größenreduzierung um 250 KB für die Seite ist sehr praktisch, da die anfängliche Ladezeit bei einer Verbindung von 2 Mbit/s weniger als eine Sekunde beträgt. Schneller und kleiner, supersoße!

Gleichzeitig habe ich die ursprünglichen Natural Earth-Shapefiles geladen, aus denen das GlobeTweeter-Mesh stammt. Ich habe es geschafft, die Shapefiles zu laden, aber damit sie als flache Landmasse dargestellt werden können, müssen sie trianguliert werden (mit Löchern für Seen, Natch). Ich habe die Formen mit THREE.js utils trianguliert, aber nicht mit den Löchern. Die resultierenden Mesh-Netzwerke hatten sehr lange Kanten, wodurch das Mesh-Netzwerk in kleinere Tris aufgeteilt werden musste. Kurz gesagt: Ich habe es nicht geschafft, es rechtzeitig zum Laufen zu bringen, aber das Tolle war, dass man mit dem weiter komprimierten Shapefile-Format ein 8-KB-Landmassemodell erhalten hätte. Na ja, vielleicht nächstes Mal.

Zukünftige Arbeiten

Eine Sache, die ein wenig zusätzliche Arbeit gebrauchen könnte, wäre die Verbesserung der Markierungsanimationen. Wenn sie jetzt über den Horizont gehen, ist der Effekt ein wenig klebrig. Außerdem wäre eine coole Animation zum Öffnen der Markierung schön.

Was die Leistung angeht, ist die Optimierung des Mesh-Split-Algorithmus und die Beschleunigung der Markierungen der Fall. Abgesehen davon ist es blöd. Hurra!

Zusammenfassung

In diesem Artikel habe ich beschrieben, wie wir den 3D-Globus für das Google World Wonders-Projekt erstellt haben. Wir hoffen, dass Ihnen die Beispiele gefallen haben und versuchen Sie, Ihr eigenes benutzerdefiniertes Globus-Widget zu erstellen.

Verweise