世界奇观 3D 地球仪简介
如果您曾在支持 WebGL 的浏览器中访问最近发布的 Google 世界奇观网站,可能就会在屏幕底部看到一个旋转的地球仪。本文将介绍地球仪的工作原理以及我们在制作地球仪时所用的方法。
简要介绍一下,世界奇观地球仪是 Google 数据艺术团队对 WebGL 地球仪进行大量调整后的产品。我们取下原始地球仪,移除了条形图部分,更改了着色器,添加了来自 Mozilla 的 GlobeTweeter 演示的炫酷可点击 HTML 标记和 Natural Earth 大陆几何图形(非常感谢 Cedric Pinson!)所有这些都是为了制作一个与网站的配色方案相匹配且为网站增添一层复杂性的动画地球仪。
地球仪的设计提案要求制作一张美观的动画地图,并在世界遗产地点上方放置可点击的标记。有了这个想法,我就开始寻找合适的解决方案。我首先想到的是 Google 数据艺术团队构建的 WebGL 地球仪。它是一个地球仪,看起来很酷。您还需要其他信息吗?
设置 WebGL 地球仪
制作地球仪 widget 的第一步是下载 WebGL 地球仪并启动它。WebGL 地球仪在 Google Code 上线,下载和运行都很简单。下载并解压缩ZIP 文件,进入该文件夹并运行基本 Web 服务器:python -m SimpleHTTPServer
。(请注意,默认情况下,此工具不支持 UTF-8;您可以使用此工具。)现在,如果您前往 http://localhost:8000/globe/globe.html
,应该会看到 WebGL 地球仪。
WebGL 地球仪已启动并正常运行,接下来需要移除所有不需要的部分。我修改了 HTML 以移除界面部分,并从地球仪初始化函数中移除了地球仪条形图设置内容。该过程结束后,我的屏幕上显示了一个非常简单的 WebGL 地球仪。您可以旋转它,看起来很酷,但仅此而已。
为了删除不需要的内容,我从地球仪的 index.html 中删除了所有界面元素,并修改了初始化脚本,使其如下所示:
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 内容,因此非常容易编写。所有链接和文本渲染都会自动运行,无需我们额外执行任何工作。
缩减文件大小
演示版已正常运行并连接到世界奇观网站的其余部分,但仍有 1 个重大问题需要解决。地球陆地的 JSON 格式网格的大小约为 3 MB。不适合用于展示网站的首页。好在使用 gzip 压缩网格后,其大小缩减到了 350 KB。不过,350 KB 还是有点大。几封电子邮件后,我们成功说服 Won Chun(负责压缩庞大的 Google 身体网格)帮助我们压缩网格。他将网格从以 JSON 坐标给出的大量平面三角形列表压缩为带有编号三角形的压缩 11 位坐标,并将文件大小缩减到 95 KB(经过 GZIP 压缩)。
使用压缩网格不仅可以节省带宽,还可以加快网格的解析速度。将 3 MB 的字符串化数字转换为原生数字所需的工作量要比解析几百 KB 的二进制数据多得多。网页的大小因此缩减了 250 KB,这非常棒,而且在 2 Mbps 的连接速度下,网页的初始加载时间也缩短到了 1 秒以下。速度更快、体积更小,太棒了!
与此同时,我还尝试加载 GlobeTweeter 网格派生自的原始 Natural Earth Shapefile。我设法加载了 Shapefile,但若要将其渲染为平坦的陆地,则需要对其进行三角化处理(当然,湖泊需要有洞)。我使用 THREE.js 实用程序对形状进行了三角剖分,但没有对孔进行三角剖分。生成的网格边缘非常长,因此需要将网格拆分成较小的三角形。长话短说,我没能及时解决这个问题,但有一点很棒,那就是经过进一步压缩的 Shapefile 格式可以为您提供 8 KB 的陆地模型。也许下次吧。
后续工作
我们还需要做一些额外的工作,让标记动画看起来更美观。现在,当它们越过地平线时,效果有点俗气。此外,为标记开启添加酷炫的动画效果会很不错。
在性能方面,还需要优化网格拆分算法并提高标记的速度。除此之外,一切顺利。太棒了!
摘要
在本文中,我介绍了我们如何为 Google 世界奇观项目构建 3D 地球仪。希望您喜欢这些示例,并尝试构建自己的自定义地球仪微件。