以 3D 地球儀製作世界奇觀

Ilmari Heikkinen

世界奇觀 3D 地球儀簡介

如果您曾在支援 WebGL 的瀏覽器上查看最近推出的 Google 世界奇觀網站,可能會在畫面底部看到旋轉的地球圖示。本文將說明地球儀的運作方式,以及我們用來製作地球儀的工具。

簡單來說,世界奇觀地球儀是 Google 資料藝術團隊經過大量調整的 WebGL 地球儀。我們使用原始地球儀,移除柱狀圖位元,變更著色器,新增精美的可點選 HTML 標記,以及 Mozilla 的 GlobeTweeter 示範中的 Natural Earth 大陸幾何圖形 (非常感謝 Cedric Pinson!)這些元素可讓動畫地球儀與網站的色彩配置相符,為網站增添一層精緻感。

地球儀的設計簡報要求製作精美的動畫地圖,並在世界文化遺產地點上方放置可點選的標記。基於這個想法,我開始尋找合適的內容。我首先想到的是 Google 資料藝術團隊建立的 WebGL Globe。它是地球儀,看起來很酷。還有什麼需要嗎?

設定 WebGL 地球儀

製作地球儀小工具的第一步,是下載 WebGL 地球儀並啟用。WebGL Globe 已在 Google Code 上線,您可以輕鬆下載及執行。下載並解壓縮zip 檔案,然後切換至該檔案並執行基本網路伺服器:python -m SimpleHTTPServer。(請注意,這個選項預設不會啟用 UTF-8;您可以使用這個選項)。現在,如果您前往 http://localhost:8000/globe/globe.html,應該會看到 WebGL 地球儀。

在 WebGL Globe 啟動及運作後,我們便可刪除所有不必要的部分。我編輯 HTML 來刪除 UI 位元,並從地球儀初始化函式中移除地球儀長條圖設定內容。完成該程序後,畫面上會顯示非常簡單的 WebGL 地球儀。你可以旋轉它,看起來很酷,但就這樣而已。

為了刪除不必要的內容,我從地球儀的 index.html 中刪除所有 UI 元素,並編輯初始化指令碼,讓指令碼如下所示:

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

新增大陸幾何圖形

我們希望相機能靠近地球表面,但在測試放大地球時,明顯缺少紋理解析度。放大後,WebGL 地球儀的紋理會變得粗糙模糊。我們本來可以使用較大的圖片,但這樣會讓地球儀的下載和執行速度變慢,因此我們選擇以向量圖形呈現陸地和邊界。

針對陸地幾何圖形,我改用開放原始碼 GlobeTweeter 示範,並將其中的 3D 模型載入至 Three.js。模型載入及算繪完成後,您就可以開始調整地球儀的外觀。第一個問題是地球儀陸地模型不夠圓,無法與 WebGL 地球儀保持一致,因此我最後寫了一個快速的網格分割演算法,讓陸地模型更圓。

我使用球形陸地模型,將其放置在地球表面稍微偏移的位置,建立浮動大陸,並在下方加上 2 像素黑色線條,模擬陰影。我還嘗試使用霓虹色邊框,打造出類似《電子世界爭霸戰》的效果。

在地球儀和陸地的算繪完成後,我開始嘗試為地球儀設計不同的外觀。我們希望採用低調的單色外觀,因此我決定使用灰階地球儀和陸地。除了上述的霓虹輪廓外,我還嘗試使用深色地球儀,並在淺色背景上加入深色陸地,效果相當不錯。但對比度太低,不易閱讀,而且不符合專案的風格,因此我將其刪除。

我最初的想法是讓地球儀看起來像上釉的瓷器,我沒辦法嘗試這個選項,因為我無法編寫著色器來呈現瓷器外觀 (視覺素材編輯器會很不錯)。我嘗試過最接近的東西是這個白色發光地球儀,上面有黑色陸地。這很酷,但對比度太高。而且看起來也不太好看。所以又有一個要丟進資源回收箱。

黑白地球儀中的著色器使用了一種假的漫反光燈光。地球儀的亮度取決於表面法線與螢幕平面之間的距離。因此,地球儀中間的像素會指向螢幕,因此會顯示為黑色,而地球儀邊緣的像素則會顯示為白色。搭配淺色背景,地球儀會反射出明亮的背景,營造出高級展示間的氛圍。黑色地球儀也使用 WebGL 地球儀紋理做為光澤地圖,因此大陸棚 (淺水區) 看起來比地球儀的其他部分更亮。

以下是黑色地球儀的海洋著色器外觀。非常基本的頂點著色器和粗糙的「看起來還不錯,稍微調整一下」片段著色器。

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

最後,我們採用了深色地球儀,並在上面點亮淺灰色的陸地。這張圖片最符合設計簡報,而且看起來既美觀又易讀。此外,地球儀的對比度較低,因此標記和其他內容會更醒目。下方的版本使用完全黑色的海洋,而實際發布的版本則是深灰色的海洋,且標記略有不同。

使用 CSS 建立標記

說到標記,在地球儀和陸地都運作正常後,我開始著手製作地標。我決定使用 CSS 樣式的 HTML 元素做為標記,以便製作及設定標記樣式,並在團隊正在處理的 2D 地圖中重複使用標記。當時我也不清楚有哪些簡單的方法,可讓 WebGL 標記可供點選,而且我不想編寫額外的程式碼來載入 / 建立標記模型。事後看來,CSS 標記運作良好,但在瀏覽器合成器和轉譯器處於變動期間時,有時會出現效能問題。從效能角度來看,在 WebGL 中執行標記會是更好的選擇。再者,CSS 標記可節省大量開發時間。

CSS 標記包含幾個使用 CSS 轉換屬性以絕對位置放置的 div。標記的背景是 CSS 漸層,標記的三角形部分則是旋轉的 div。標記會加上小型陰影,讓標記從背景中浮現。標記最大的問題,就是要讓它們有足夠的效能。雖然聽起來很可惜,但在每個影格中繪製數十個會移動並變更 z-index 的 div,是觸發各種瀏覽器轉譯陷阱的好方法。

標記與 3D 場景同步的方式並不複雜。每個標記在 Three.js 場景中都有對應的 Object3D,用於追蹤標記。為了取得螢幕空間座標,我會取得地球儀和標記的 Three.js 矩陣,並將零向量乘上這些矩陣。我會從中取得標記的場景位置。為了取得標記的螢幕位置,我透過相機投影場景位置。產生的投影向量含有標記的螢幕空間座標,可在 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;
}

最後,最快的方法是使用 CSS 轉換來移動標記,不要使用透明度淡出效果,因為這會在 Firefox 上觸發緩慢路徑,並在 DOM 中保留所有標記,而不是在標記移至地球儀後移除。我們也嘗試使用 3D 轉換功能取代 z 索引,但出於某些原因,這項功能在應用程式中無法正常運作 (但在簡化測試案例中運作正常,請自行推測),而且當時距離推出應用程式只有幾天,因此我們必須將這項功能留待推出後維護。

點選標記後,系統會展開可點選的地點名稱清單。這些都是一般 HTML DOM 內容,因此非常容易編寫。所有連結和文字算繪都會正常運作,我們不必額外處理。

壓縮檔案大小

雖然示範功能已完成並連結至 World Wonders 網站的其他部分,但仍有一個重大問題需要解決。地球儀陸地的 JSON 格式網格大小約為 3 MB。不適合用於展示網站的首頁。好消息是,使用 gzip 壓縮網格後,大小降至 350 KB。不過,350 KB 還是有點大。幾封電子郵件後,我們成功招募到 Won Chun,他曾負責壓縮巨大的 Google Body 網格,因此可以協助我們壓縮網格。他將網格從以 JSON 座標提供的三角形大平面清單,壓縮為含有索引三角形的 11 位元壓縮座標,並將檔案大小壓縮至 95 KB。

使用壓縮網格不僅可節省頻寬,還能加快網格剖析速度。將 3 MB 的字串化數字轉換為原生數字,所需的工作量遠大於剖析 100 KB 的二進位資料。這麼一來,網頁的大小就會縮減 250 KB,不僅非常實用,而且在 2 Mbps 的連線速度下,初始載入時間也會縮短至 1 秒以下。速度更快、體積更小,真是太棒了!

與此同時,我嘗試載入 GlobeTweeter 網格所衍生的原始 Natural Earth 形狀檔案。我已成功載入 Shapefile,但要將這些檔案算繪為平坦陸地,就必須進行三角剖分 (湖泊會出現洞)。我使用 THREE.js utils 將形狀三角剖分,但洞並未三角剖分。產生的網格邊緣非常長,因此需要將網格分割成較小的三角形。簡單來說,我無法在時間內完成這項工作,但好消息是,經過進一步壓縮的 Shapefile 格式可讓你取得 8 kB 的陸地模型。好吧,下次再說。

日後的作業

您可以稍微加強標記動畫效果,但現在當它們越過地平線時,效果有點俗氣。此外,為標記開啟畫面加入酷炫的動畫效果會很棒。

就效能而言,缺少的是最佳化網格分割演算法,以及加快標記速度。除此之外,一切都很好。太棒了!

摘要

本文將說明我們如何為 Google 世界奇觀專案建構 3D 地球儀。希望您喜歡這些範例,並嘗試自行建構自訂地球儀小工具。

參考資料