Making of the World Wonders Trójwymiarowa kula ziemska

Ilmari Heikkinen

Wprowadzenie do globusa 3D World Wonders

Jeśli otwierasz niedawno uruchomioną niedawno witrynę Google World Wonders w przeglądarce z obsługą WebGL, być może zauważysz na dole ekranu kręcącą się kulę ziemską. Z tego artykułu dowiesz się, jak działa kula ziemska i czego użyliśmy do jej stworzenia.

Podstawowe informacje: kula ziemska World Wonders to mocno ulepszona wersja WebGL Globe utworzona przez zespół Google Data Arts. Prezentowaliśmy oryginalną kulę ziemską, usunęliśmy fragmenty wykresu słupkowego, zmieniliśmy cieniowanie, dodaliśmy fantazyjne, klikalne znaczniki HTML oraz geometrię kontynentu Natural Earth z prezentacji GlobeTweeter przygotowanej przez Mozillę Pinsona (duże dzięki Cedricowi Pinsonowi). Wszystko po to, aby stworzyć miłą, animowaną kulę ziemską, która pasuje do kolorystyki witryny i nadal nadaje jej charakter.

Założeniem projektu było wprowadzenie ładnej, animowanej mapy z klikalnymi znacznikami, które zostały umieszczone na miejscach z listy światowego dziedzictwa. Właśnie dlatego zacząłem szukać czegoś ciekawego. Pierwszą rzeczą, która przyszła mi na myśl, była technologia WebGL Globe utworzona przez zespół Google Data Arts. To kula ziemska i wygląda świetnie. Czego jeszcze potrzebujesz?

Konfigurowanie WebGL Globe

Pierwszym krokiem przy stworzeniu widżetu kuli ziemskiej było pobranie i uruchomienie WebGL Globe. Zjazd WebGL Globe jest dostępny online na stronie Google Code, a jego pobranie i uruchomienie jest bardzo łatwe. Pobierz i rozpakuj plik zip, do pliku CD i uruchom podstawowy serwer WWW: python -m SimpleHTTPServer. Pamiętaj, że kodowanie UTF-8 nie jest domyślnie włączone. Możesz go użyć. Po podłączeniu do http://localhost:8000/globe/globe.html zobaczysz WebGL Globe.

Po uruchomieniu WebGL Globe nadszedł czas na odcięcie wszystkich zbędnych części. Zmodyfikowałem kod HTML, aby wyodrębnić elementy interfejsu. Zmodyfikowałem też konfigurację wykresu słupkowego kuli ziemskiej z funkcji inicjowania kuli ziemskiej. Pod koniec tego procesu na moim ekranie znajdował się bezwładny globus WebGL. Możesz go obrócić i wygląda fajnie, ale to wszystko.

Aby wyciąć niepotrzebne elementy, usunąłem wszystkie elementy interfejsu z indeksu index.html kuli ziemskiej i zmodyfikowałem skrypt inicjowania w taki sposób:

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

Dodawanie geometrii kontynentu

Chcieliśmy umieścić aparat blisko powierzchni kuli ziemskiej, ale gdy testowaliśmy kulę ziemską, było to spowodowane brakiem rozdzielczości tekstur. Po powiększeniu tekstura kuli ziemskiej WebGL staje się niewyraźna i rozmyta. Trzeba było użyć większego obrazu, ale pobieranie i uruchamianie kuli ziemskiej wolniej się zmieściło, dlatego zdecydowaliśmy się przedstawić wektorowe obszary i granice.

W przypadku geometrii lądowej włączyłem pokazową wersję demonstracyjną GlobeIndexer typu open source i wczytowałem(-am) w nim model 3D do Three.js. Po załadowaniu i renderowaniem modelu można było zacząć dopracować wygląd kuli ziemskiej. Pierwszym problemem było to, że model lądowy kuli ziemskiej nie był na tyle sferyczny, aby można go było równać z WebGL Globe. Stworzyłem więc algorytm szybkiego podziału siatki, który sprawił, że model lądu stał się bardziej sferyczny.

Korzystając z sferycznego modelu lądu, udało mi się go umieścić w niewielkim odsunięciu względem powierzchni kuli ziemskiej, tworząc unoszące się kontynenty kontenery za pomocą czarnej linii o szerokości 2 pikseli w cieniu pewnego rodzaju. Poeksperymentowałam też z neonowymi konturami, by uzyskać efekt przypominający tron.

Patrząc na kulę ziemską i jej renderowanie, zacząłem eksperymentować z różnymi ujęciami kuli ziemskiej. Jako że chciałam przejść na skromny, monochromatyczny wygląd, przybrałem szarość kuli ziemskiej i mapy lądowe. Oprócz wspomnianych wcześniej neonów zarysowałam też ciemną kulę ziemską z ciemnymi obszarami lądowymi na jasnym tle. Wyglądało ono naprawdę świetnie. Kontrast był zbyt niski, aby można go było odczytać, i nie pasował do charakteru projektu, więc skręciłem go w elementy.

Kolejnym pomysłem jest stworzenie glazury na kulę ziemską, która wyglądała jak szkliwiona porcelana. Tego, którego nie udało mi się wypróbować, ponieważ nie udało mi się napisać cienia do porcelany (przydatny byłby edytor materiałów wizualnych). Najbliższą rzeczą, jaką wypróbowałam, była ta biała, świecąca kula ziemska z czarnymi lądami. Jest dość schludne, ale ma zbyt wysoki kontrast. I nie wygląda to zbyt ładnie. A więc kolejne na śmieci.

cieniowanie na czarno-białych kulach ziemskich stosują podświetlenie rozproszone. Jasność kuli ziemskiej zależy od odległości normalnej od powierzchni ekranu. Piksele w środku kuli ziemskiej, które wskazują na ekran, są ciemne, a na krawędziach – jasne. Połączenie z jasnym tłem sprawia, że kula ziemska odbija rozproszone jasne tło, tworząc elegancki wygląd salonu. Czarna kula ziemska wykorzystuje teksturę WebGL Globe jako mapę błyszczącą, dzięki czemu szelfy kontynentalne (płytki obszar wodny) wyglądają błyszczące w porównaniu z innymi częściami kuli ziemskiej.

Tak wygląda cieniowanie oceanu dla czarnej kuli ziemskiej. Bardzo podstawowy program do cieniowania wierzchołków oraz haczyk „To wygląda całkiem dobrze dostosujesz” do cieniowania fragmentów.

    '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 wykorzystaliśmy ciemną kulę ziemską z jasnoszarym obszarem oświetlonym z góry. Była najbliższa założenia projektu i wyglądała ładnie i czytelnie. Dodatkowo globus o niskim kontraście sprawia, że znaczniki i reszta treści bardziej się wyróżniają. Poniższa wersja wykorzystuje całkowicie czarne oceany, a wersja produkcyjna przedstawia ciemnoszare oceany i nieco inne znaczniki.

Tworzenie znaczników za pomocą CSS

Skoro już mówimy o znacznikach, gdy kula ziemska i obszary lądowe funkcjonowały, zacząłem pracę nad oznaczeniami miejsc. Zdecydowałem się na użycie elementów HTML w stylu CSS, aby ułatwić tworzenie znaczników i ich stylizowanie, a także wykorzystanie ich ponownie na mapie 2D, nad którą pracował nasz zespół. Nie znałem też prostego sposobu na wprowadzenie znaczników WebGL do kliknięcia i nie znałem dodatkowego kodu do wczytywania / tworzenia modeli znaczników. Podsumowując, znaczniki CSS działały dobrze, ale czasami występowały problemy z wydajnością, gdy kompozytory i mechanizmy renderowania przeglądarki podlegały zmianom. Z punktu widzenia wydajności użycie znaczników w WebGL byłoby lepszym rozwiązaniem. I znów, znaczniki CSS pozwoliły zaoszczędzić sporo czasu dla programistów.

Znaczniki CSS składają się z kilku elementów div o położonym bezwzględnym położeniu za pomocą właściwości „transform” CSS. Tło znaczników jest w postaci gradientu CSS, a trójkątna część znacznika jest obróconym elementem div. Znaczniki mają mały cień, który oddziela je od tła. Największym problemem ze znacznikami było zapewnienie ich odpowiedniej skuteczności. Może to brzmi smutno, ale rysowanie kilkunastu elementów div, które się poruszają i zmieniają kolejność nakładania elementów w każdej klatce, to całkiem dobry sposób na wywołanie różnych pułapek renderowania w przeglądarce.

Sposób synchronizacji znaczników ze sceną 3D nie jest zbyt skomplikowany. Każdy znacznik ma w scenie Three.js odpowiedni obiekt Object3D, który służy do ich śledzenia. Aby uzyskać współrzędne miejsca ekranu, pobieram macierze Three.js dla kuli ziemskiej i znacznika i mnożym przez nie wektor zerowy. Uzyskam pozycję sceny dla znacznika. Aby ustalić położenie znacznika na ekranie, rzutuję obraz w kamerę. Powstały wektor ma współrzędne miejsca na ekranie dla znacznika, które są gotowe do użycia w kodzie 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 podejściem było użycie przekształceń CSS do przesuwania znaczników, rezygnacja z zanikania przezroczystości, ponieważ powodowało to powolne śledzenie ścieżki w przeglądarce Firefox i usuwało wszystkie znaczniki w modelu DOM, a nie usuwanie ich, gdy były poza kulą ziemską. Poeksperymentowaliśmy też z przekształceniami 3D zamiast kolejności nakładania elementów, ale z jakiegoś powodu w aplikacji nie działało to dobrze (ale w przypadku mniejszego przypadku testowego – ilustracja). Mieliśmy wtedy kilka dni od wprowadzenia na rynek, więc musieliśmy to pozostawić na później.

Po kliknięciu znacznika rozwija się lista możliwych do kliknięcia nazw miejsc. To zupełnie normalne elementy HTML DOM, więc pisanie bardzo łatwe. Wszystkie linki i renderowanie tekstu działają bez dodatkowej pracy z naszej strony.

Ściskanie rozmiaru pliku

Gdy wersja demonstracyjna działała, a elementy zostały dodane do reszty witryny World Wonders, pojawił się jeszcze jeden spory problem do rozwiązania. W przypadku powierzchni lądowych siatka w formacie JSON miała rozmiar około 3 megabajtów. Nie sprawdza się w przypadku strony głównej witryny z prezentacją. Na szczęście skompresowanie siatki za pomocą narzędzia gzip spowodowało zmniejszenie jej rozmiaru do 350 kB. Ale 350 kB to nadal trochę sporo. Kilka e-maili później udało nam się zrekrutować Won Chuna, który zajmował się skompresowaniem ogromnych siatek ciał Google, i pomógł nam w ich skompresowaniu. Wycisnąć siatkę z dużej listy trójkątów podanych jako współrzędne JSON do skompresowanych 11-bitowych współrzędnych ze zindeksowanymi trójkątami i zmniejszyć rozmiar pliku do 95 KB w formacie gzip.

Użycie skompresowanych sieci nie tylko oszczędza przepustowość, ale także pozwala szybciej je analizować. Przekształcenie 3 megabajtów w liczby w postaci ciągów znaków w liczbach natywnych wymaga dużo więcej pracy niż analiza 10 KB danych binarnych. Zmniejszenie rozmiaru strony o 250 kB to bardzo dobry wynik, a przy połączeniu 2 Mb/s czas wczytywania jest znacznie krótszy niż sekunda. Jeszcze szybszy i mniejszy!

Jednocześnie rysowałem i wczytywałem oryginalne pliki kształtu z naturalnej Ziemi, z których pochodzi siatka reflektorów. Udało mi się wczytać pliki kształtu, ale renderowanie ich jako płaskich obszarów lądowych wymaga odprowadzenia ich do trójkąta (z otworami na jeziora). Udało mi się utworzyć triangulacje kształtów za pomocą funkcji THREE.js utils, ale bez otworów. Otrzymane sieci miały bardzo długie krawędzie, co wymagało podziału siatki na mniejsze części. Mówiąc w skrócie, nie udało mi się go uruchomić na czas, ale mimo to bardziej skompresowany format pliku kształtującego pozwoliłby uzyskać model lądowy o rozmiarze 8 kB. No cóż, może następnym razem.

Praca w przyszłości

Jedną z rzeczy, które wymagają dodatkowej pracy, jest ulepszenie animacji znaczników. Teraz efekt jest dość chwytliwy. Przydałaby się też fajna animacja z otwieraniem znacznika.

Pod względem wydajności 2 rzeczy, których brakuje, to optymalizacja algorytmu podziału sieci typu mesh i szybsze działanie znaczników. Poza tym wszystko jest fajne. Hurra!

Podsumowanie

W tym artykule opisaliśmy, jak zbudowaliśmy globus 3D na potrzeby projektu Google World Wonders. Mam nadzieję, że te przykłady Ci się spodobają i spróbujesz utworzyć własny niestandardowy widżet kuli ziemskiej.

Źródła