Making of the World Wonders Trójwymiarowa kula ziemska

Ilmari Heikkinen

Wprowadzenie do globusa 3D z cudami świata

Jeśli niedawno przeglądałeś stronę Cuda świata Google w przeglądarce obsługującej WebGL, prawdopodobnie zauważyłeś u dołu ekranu ładną obracającą się kulę ziemską. Z tego artykułu dowiesz się, jak działa globus i co wykorzystaliśmy do jego stworzenia.

Aby przedstawić Ci ogólny zarys, zespół Google Data Arts opowie Ci o kuli Ziemi z World Wonders, która jest znacznie zmodyfikowaną wersją kuli Ziemi WebGL. Wzięliśmy oryginalną kulę ziemską, usunęliśmy elementy wykresu słupkowego, zmieniliśmy shadery, dodaliśmy ładne klikalne znaczniki HTML i geometrię kontynentów Natural Earth z demo GlobeTweeter Mozilli (wielkie podziękowania dla Cedrica Pinsona!). Wszystko to tworzy ładną, animowaną kulę ziemską, która pasuje do schematu kolorów witryny i nadaje jej dodatkowy sznyt.

W ramach briefu projektowego na temat kuli ziemskiej należało stworzyć ładną animowaną mapę ze klikalnymi znacznikami umieszczonymi na wierzchu obiektów wpisanych na listę światowego dziedzictwa. Mając to na uwadze, zaczęłam szukać czegoś odpowiedniego. Jako pierwszy przyszedł mi do głowy globus WebGL stworzony przez zespół Google Data Arts. To kula ziemska i wygląda świetnie. Co jeszcze potrzebujesz?

Konfigurowanie kuli ziemskiej WebGL

Pierwszym krokiem w tworzeniu widżetu kuli ziemskiej było pobranie i uruchomienie kuli WebGL. Globus WebGL jest dostępny online na stronie Google Code. Można go łatwo pobrać i uruchomić. Pobierz i rozpakuj plik ZIP, przejdź do niego i uruchom podstawowy serwer WWW: python -m SimpleHTTPServer. (uwaga: domyślnie nie jest włączone kodowanie UTF-8; możesz go użyć). Jeśli teraz przejdziesz do http://localhost:8000/globe/globe.html, powinieneś zobaczyć globus WebGL.

Gdy glob WebGL był już gotowy, nadszedł czas na usunięcie wszystkich zbędnych części. Zmieniłem kod HTML, aby usunąć elementy interfejsu użytkownika, i usunąłem elementy konfiguracji wykresu słupkowego na mapie globusie z funkcji inicjalizowania mapy globusa. Na koniec tego procesu na ekranie miałem bardzo podstawową mapę WebGL Globe. Możesz go obracać, wygląda fajnie, ale to wszystko.

Aby usunąć niepotrzebne elementy, usunąłem wszystkie elementy interfejsu z pliku index.html globu i zmodyfikowałem skrypt inicjalizacji, aby wyglądał tak:

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

Dodawanie geometrii kontynentu

Chcieliśmy, aby kamera była blisko powierzchni globu, ale podczas testowania powiększonego globu okazało się, że brakuje rozdzielczości tekstury. Po zbliżeniu tekstura kuli ziemskiej WebGL staje się nieostra i pikselowana. Mogliśmy użyć większego obrazu, ale spowodowałoby to dłuższe pobieranie i uruchamianie globu, dlatego zdecydowaliśmy się na wektorową reprezentację lądów i granic.

W przypadku geometrii lądów skorzystałem z demo aplikacji open source GlobeTweeter i wczytałem model 3D do Three.js. Po załadowaniu i wyrenderowaniu modelu nadszedł czas na dopracowanie wyglądu kuli. Pierwszym problemem było to, że model masy lądowej na kuli ziemskiej nie był wystarczająco sferyczny, aby pasował do kuli ziemskiej WebGL, więc napisałem szybki algorytm dzielenia siatki, który sprawił, że model masy lądowej stał się bardziej sferyczny.

Dzięki temu, że model kuli ziemskiej jest sferyczny, mogłem umieścić go tylko nieznacznie przesuniętym od powierzchni globu, tworząc unoszące się kontynenty, które są obrysowane czarną linią o szerokości 2 pikseli, tworząc w ten sposób cień. Eksperymentowałem też z neonowymi obrysami, aby uzyskać efekt podobny do tego z filmu Tron.

Zaczęłam eksperymentować z wyświetlaniem kuli ziemskiej i lądów. Chcieliśmy uzyskać monochromatyczny wygląd, więc zdecydowaliśmy się na globus i lądy w szarościach. Oprócz wspomnianych neonowych konturów wypróbowałam ciemną kulę ziemską z ciemnymi konturami na jasnym tle, co wygląda naprawdę fajnie. Jednak kontrast był zbyt niski, aby tekst był czytelny, i nie pasował do projektu, więc zrezygnowałem z tego pomysłu.

Innym pomysłem na wygląd kuli było nadanie jej wyglądu glazurowanej porcelany. Nie udało mi się tego wypróbować, ponieważ nie udało mi się napisać shadera, który nadałby efekt porcelany (przydałby się edytor materiałów wizualnych). Najbliżej było do białego świecącego globu z czarnymi kontynentami. Jest ładna, ale ma zbyt duży kontrast. I nie wygląda to zbyt dobrze. Kolejny odpad.

Shadery w czarnych i białych globusach wykorzystują rodzaj fałszowanego oświetlenia rozproszonego. Jasność kuli zależy od odległości powierzchni od normalnej płaszczyzny ekranu. Dlatego piksele w środku globusa, które wskazują na ekran, są ciemne, a piksele na krawędziach globusa są jasne. W połączeniu ze światłem tła uzyskujesz efekt, w którym glob odbija jasne tło, tworząc elegancki wygląd showroomu. Czarna kula ziemska korzysta z tekstury WebGL Globe jako mapy połysku, dzięki czemu kontynentalne szelfy (obszary płytkich wód) wyglądają na błyszczące w porównaniu z pozostałymi częściami kuli ziemskiej.

Oto jak wygląda shader oceanu na czarnej kuli. Bardzo prosty shader wierzchołków i niezbyt ładny shader fragmentów, ale „no, wygląda całkiem nieźle 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')
    }

Ostatecznie zdecydowaliśmy się na ciemną kulę ziemską z jasnymi szarymi obszarami lądowymi oświetlonymi od góry. Najlepiej odpowiadał briefowi, był czytelny i ładny. Ponadto, jeśli mapa jest w nieco ciemniejsza, znaczniki i pozostałe elementy będą się na niej lepiej wyróżniać. W wersji poniżej oceany są całkowicie czarne, natomiast w wersji produkcyjnej są ciemnoszare i mają nieco inne znaczniki.

Tworzenie znaczników za pomocą CSS

A gdy już kulka i lądy działały, zaczęłam pracować nad znacznikami miejsc. Postanowiłam użyć elementów HTML w stylu CSS, aby ułatwić sobie tworzenie i stylizowanie znaczników oraz umożliwić ich ewentualne ponowne użycie na mapie 2D, nad którą pracuje zespół. W tym czasie nie znałem też łatwego sposobu na to, aby uczynić znaczniki WebGL klikalne, i nie chciałem pisać dodatkowego kodu do wczytywania lub tworzenia modeli znaczników. Wstecz patrz, znaczniki CSS działały dobrze, ale czasami występowały problemy z wydajnością, gdy kompozytory i renderowanie przeglądarki były w okresie zmian. Z punktu widzenia wydajności lepszym rozwiązaniem byłoby użycie znaczników w WebGL. Z drugiej strony znaczniki CSS zaoszczędziły sporo czasu programistom.

znaczniki CSS składają się z kilku elementów div o pozycji bezwzględnej z właściwością transform CSS. Tło przy znacznikach to gradient CSS, a trójkąt w znaczniku to obracany element div. Znaczniki mają mały cień, dzięki któremu wyróżniają się na tle. Największym problemem było zapewnienie im odpowiedniej wydajności. Jakkolwiek smutno to brzmi, rysowanie kilkudziesięciu elementów div, które się poruszają i zmieniają swój indeks Z na każdym klatku, to dobry sposób na wywołanie wszelkiego rodzaju błędów związanych z renderowaniem w przeglądarce.

Sposób synchronizacji znaczników ze sceną 3D nie jest zbyt skomplikowany. Każdy znacznik ma odpowiadający mu obiekt 3D w scenie Three.js, który służy do śledzenia znaczników. Aby uzyskać współrzędne przestrzeni ekranu, biorę macierze Three.js dla globu i oznacznika, a następnie mnożę je przez wektor zerowy. Z tego otrzymuję pozycję znacznika w scenie. Aby uzyskać pozycję znacznika na ekranie, wyświetlam pozycję sceny przez aparat. Uzyskany wektor rzutowania zawiera współrzędne przestrzeni ekranu znacznika, gotowe do użycia w CSS.

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

Ostatecznie najszybszym sposobem było użycie transformacji CSS do przemieszczania znaczników, a nie stosowanie zanikania przezroczystości, ponieważ powodowało ono spowolnienie w Firefoksie i utrzymywanie wszystkich znaczników w DOM, a nie ich usuwanie, gdy znajdowały się za kulą ziemską. Eksperymentowaliśmy też z zastosowaniem transformacji 3D zamiast indeksów Z, ale z jakichś powodów nie działało to prawidłowo w aplikacji (chociaż w ograniczonym teście działało), a do premiery zostało nam kilka dni, więc musieliśmy zostawić tę część do konserwacji po premierze.

Gdy klikniesz znacznik, rozwinie się on w listę nazw miejsc, które można kliknąć. To wszystko jest zwykłym HTML DOM, więc było bardzo łatwe do napisania. Wszystkie linki i teksty są renderowane bez dodatkowej pracy z naszej strony.

zmniejszenie rozmiaru pliku,

Wersja demonstracyjna działała i była połączona z resztą witryny World Wonders, ale wciąż trzeba było rozwiązać jeden duży problem. Siatka w formacie JSON dla lądów na kuli ziemskiej miała rozmiar około 3 M. Nie nadaje się na stronę główną witryny pokazowej. Na szczęście kompresja siatki za pomocą gzip zmniejszyła jej rozmiar do 350 kB. Ale 350 kB to nadal za dużo. Po kilku e-mailach udało nam się namówić Won Chuna, który pracował nad kompresowaniem ogromnych siatek Google Body, aby pomógł nam w skompresowaniu siatki. Zredukował siatkę z dużej płaskiej listy trójkątów podanych jako współrzędne w formacie JSON do skompresowanych 11-bitowych współrzędnych z indeksowanymi trójkątami, a rozmiar pliku skompresowany do 95 kB.

Korzystanie z skompresowanych siatek nie tylko oszczędza przepustowość, ale też przyspiesza analizowanie siatek. Przekształcanie 3 M bajtów liczb w postaci ciągu znaków w liczby natywne wymaga znacznie więcej pracy niż przetwarzanie 100 kB danych binarnych. Otrzymany rozmiar strony zmniejszył się o 250 kB, a czas początkowego wczytywania skrócił się do mniej niż sekundy przy połączeniu o szybkości 2 Mbps. Szybsze i mniejsze, świetne!

W tym samym czasie próbowałem wczytywać oryginalne pliki Natural Earth Shapefiles, z których pochodzi siateczkowa mapa GlobeTweeter. Udało mi się załadować pliki Shape, ale ich renderowanie jako płaskich obszarów lądowych wymaga ich triangulacji (z otworami na jeziora). Mam trójkąty z kształtów, ale nie mam otworów. Wygenerowane siatki miały bardzo długie krawędzie, co wymagało podzielenia siatki na mniejsze trójkąty. Krótko mówiąc, nie udało mi się tego zrobić na czas, ale fajne jest to, że skompresowany format pliku Shapefile pozwoliłby Ci uzyskać model masy lądowej o rozmiary 8 kB. No cóż, może innym razem.

Przyszłe zadania

Jednym z elementów, które wymagają nieco dodatkowej pracy, jest uatrakcyjniejsze animowanie znaczników. Teraz, gdy znikają za horyzontem, efekt jest trochę tandetny. Fajnie byłoby też mieć fajną animację otwierania markera.

W zakresie wydajności brakuje 2 elementów: optymalizacji algorytmu podziału siatki i przyspieszenia działania znaczników. Poza tym wszystko w porządku. Hurra!

Podsumowanie

W tym artykule opisuję, jak stworzyliśmy globus 3D na potrzeby projektu Cudów Świata Google. Mam nadzieję, że przykłady Ci się spodobały i spróbujesz stworzyć własny widżet kuli ziemskiej.

Odniesienia