世界奇观 3D 地球的制作过程

Ilmari Heikkinen

World Wonders 3D 地球简介

如果您曾使用支持 WebGL 的浏览器浏览过最近发布的 Google 世界奇观网站,可能会发现屏幕底部有一个旋转的地球仪。您可以通过本文了解地球是如何运作的,以及我们是如何构建地球的。

为方便您快速了解,“World Wonders”地球是由 Google 数据艺术团队对 WebGL Globe 进行深度调整的版本。我们从 Mozilla 的 GlobeTweeter 演示(这非常感谢 Cedric Pinson 的大力支持)中截取了原始地球,删除了条形图的部分,更改了着色器,添加了精美的可点击 HTML 标记和自然地球大陆几何图形。这一切都是为了制作一个符合网站的配色方案的精美动画地球,并使网站更加精美。

地球的设计宗旨是打造一个精美的动画地图,并在世界遗产地顶部放置可点击的标记。考虑到这一点,我开始寻找合适的作品。首先想到的就是 Google 数据艺术团队开发的 WebGL Globe。这是个球体,看起来很酷。你还需要点什么呢,嗯?

设置 WebGL Globe

制作地球微件的第一步是下载 WebGL Globe 并使其运行。您可以通过 Google 代码在线访问 WebGL Globe,并轻松下载和运行。下载并解压缩 zip,使用 cd 命令进入该 ZIP 文件,然后运行一个基本的 Web 服务器:python -m SimpleHTTPServer。(请注意,默认情况下,此选项不启用 UTF-8;您可以使用此项。)现在,如果您导航到 http://localhost:8000/globe/globe.html,应该会看到 WebGL Globe。

随着 WebGL Globe 的启动并运行,可以切去所有不需要的部分了。我修改了 HTML,去除了界面部分,并从地球初始化函数中移除了地球条形图设置内容。完成这个流程后,我的屏幕上有一个非常准的 WebGL Globe 界面。你可以旋转起来,它看起来很酷,但这一切就这么简单。

为了删除不需要的内容,我删除了地球的 index.html 中的所有界面元素,并将初始化脚本修改为如下所示:

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

添加大陆几何图形

我们希望将相机靠近地球表面,但是当我们放大地球进行测试时,纹理分辨率显而易见。放大后,WebGL Globe 的纹理变得块状和模糊。我们本来可以使用更大的图片,但这样会导致地球的下载和运行速度变慢,因此我们选择使用大陆和边界的矢量表示。

对于陆地几何图形,我访问了开源的 GlobeTweeter 演示,并将该演示中的 3D 模型加载到了 Three.js 中。加载并渲染模型后,就可以开始优化地球的外观了。第一个问题是,地球陆地模型的球形不够,无法与 WebGL Globe 平齐。因此,我最终编写了一种快速网格拆分算法,使该大陆模型更加球形。

通过球形大陆模型,我能够将它放置到与地球表面稍微偏移的位置,制作出漂浮的大陆,在它们下方用黑色的 2px 线条勾勒出某种阴影。我还尝试了霓虹色轮廓,打造出类似 Tron 的外形。

随着地球和大陆的呈现,我开始尝试不同的地球外观。因为我们想采用低调的单色外观,所以我坚持使用灰度地球和大陆。除了前面提到的霓虹轮廓外,我还尝试在浅色背景上呈现深色球体,并呈现深色陆地,这实际上看起来非常酷。但是,这张图片的对比度太低,不易看清,而且不符合项目的风格,所以我就取消了相关内容。

我早期的另一个想法是让地球仪看起来像釉面。我没有尝试尝试的那个,因为我没有编写一个着色器来实现瓷质外观(可视化 Material 编辑器会很棒)。我最接近的就是这个发光的白色球体,外带有黑色大陆。比较简洁,但对比太高了。而且看起来不太好看。再来一个垃圾留言。

黑白球体中的着色器使用一种伪造的漫射背光照明。地球的亮度取决于表面与屏幕平面法线的距离。因此,地球中间的像素表示屏幕,而地球边缘的像素则是浅色。搭配浅色背景,您会看见地球反射出漫射的明亮背景,打造出雅致的展厅外观。黑色地球还使用 WebGL Globe 纹理作为光泽图,以便与地球的其他部分相比,大陆架(浅水区)看起来很亮。

以下是黑色地球的海洋着色器的外观。非常基本的顶点着色器和“哇,看起来有点整洁的调整”片段着色器。

    '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 transform 属性绝对定位的 div 组成。标记的背景是 CSS 渐变,标记的三角形部分是旋转的 div。标记有一个小阴影,可以将其从背景弹出。标记的最大问题是使其性能足够好。这可能听起来很悲伤,绘制几十个 div 会在每一帧中四处移动并更改 Z-index,这是触发各种浏览器渲染问题的好方法。

标记与 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-index,但由于某种原因,它无法在应用中正常运行(但在缩减的测试用例中确实可行),而且我们在发布时只有几天时间,因此不得不将这部分留给发布后的维护。

当您点击某个标记时,它会展开为可点击的地名列表。这些都是普通的 HTML DOM 内容,因此编写起来非常简单。所有链接和文本呈现都可以直接使用,无需我们执行任何额外的操作。

缩小文件大小

随着演示的正常运行,并与 World Wonders 网站的其余部分连接起来,仍然有一个大问题需要解决。地球大陆的 JSON 格式的网格大小约为 3 MB。不适合展示网站的首页。令人欣慰的是,使用 gzip 压缩网格后,其大小会降至 350 kB。不过,350 KB 仍然有点大。有几封电子邮件后来,我们成功招募到了负责压缩巨大的 Google 身体网状网的 Won Chun,让我们来帮忙压缩网状网。他将网格从大型扁平三角形列表中(以 JSON 坐标表示)向下压缩到带有索引三角形的压缩 11 位坐标,并将文件压缩至 95 kB 的 Gzip 压缩大小。

使用压缩的网格不仅可以节省带宽,还可以加快网格的解析速度。与解析一百 kB 的二进制数据相比,将 300 万个字符串化数字转换为本机数字需要执行更多的工作。网页大小缩减了 250 kB,而且在 2 Mbps 连接下,初始加载时间不到 1 秒。更快、更小,酱汁!

同时,我还在不断加载最初生成 GlobeTweeter 网格的 Natural Earth Shapefile 文件。我设法加载了 Shapefile,但要将它们渲染成平面大陆,就需要对它们进行三角测量(湖的洞、划水的洞)。我使用 THREE.js 实用程序对形状进行了三角测量,但没有使用孔。由此产生的网格有非常长的边,这就需要将网格拆分成更小的三角形。长话短说,我没有及时解决问题,但更棒的是,经过进一步压缩的 Shapefile 格式,您可以得到一个 8 kB 的陆地模型。好吧,下次再见吧。

后续工作

您可能需要做一些额外工作,就是让标记动画更美观。现在,当它们超出地平线时,效果有点棘手。此外,最好为标记开口添加酷炫的动画。

在性能方面,缺少两个因素是优化网格拆分算法和提高标记速度。除此之外,其他东西都很花哨。啊!

摘要

在本文中,我介绍了我们如何为 Google 世界奇观项目构建 3D 地球。希望您喜欢这些示例,并尝试构建您自己的自定义地球微件。

参考编号