北美防空司令部追踪圣诞老人

今年,像 WebGLWeb WorkersTyped Arrays全屏 等开放式 Web 标准将在北美防空司令部的年度追踪圣诞老人全球之旅任务中发挥重要作用。这是因为 Analytical Graphics, Inc. 使用 Cesium 作为 3D 追踪圣诞老人 应用程序的基础。

Cesium 是一个开源库,它使用 JavaScript、WebGL 和其他 Web 技术在 Web 浏览器中呈现详细、动态和交互式的虚拟地球,无需插件。 以千兆字节或太字节计量的地形和图像数据集根据需要流式传输到浏览器,并叠加在直线、多边形、地标、标签、模型和其他要素之上。 这些要素在 3D 世界中被精确地定位,并且可以高效地移动和随时间推移而改变。 简而言之,Cesium 为开放式 Web 带来了响应式地理空间体验,这种体验在几年前的笨重桌面应用程序中都很少见。

北美防空司令部追踪圣诞老人 网页应用程序将于 12 月 24 日上线。 但是,Cesium 已根据 Apache 2.0 许可证在今天向商业和非商业用途免费 提供

在本文中,我将介绍 Cesium 如何使用最先进的 Web API 将令人兴奋的浏览器内体验带给 12 月 24 日的数百万用户。

北美防空司令部追踪圣诞老人应用程序屏幕截图中使用的位置基于测试数据。 当然,在我们开始跟踪圣诞老人之前,我们不会知道他的路线。 此外,本文中的代码示例仅用于说明目的,并不一定反映 Cesium 中使用的确切代码。 如果你想查看官方代码,请查看我们的 GitHub 存储库

WebGL

没有 WebGL,Cesium 将无法存在,这项技术将硬件加速的 3D 图形带到了 Web 上。

很难夸大这项技术将全新的科学和娱乐应用程序带到 Web 上的潜力。Cesium 只是这种潜力的一个实现。 使用 WebGL,我们可以以每秒超过 60 帧的速度呈现像上面这样的场景,该场景包含数十万个三角形。

是的,你可以说我很兴奋。

如果你熟悉 OpenGL,WebGL 对你来说会很自然。 为了简单起见,WebGL 使应用程序能够非常快地绘制阴影三角形。 例如,从 JavaScript 中,我们执行以下代码

gl.bindBuffer(gl.ARRAY_BUFFER, vertexBuffer);

gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);

gl.drawElements(gl.TRIANGLES, numberOfIndices, gl.UNSIGNED_SHORT, 0);

vertexBuffer 是一个预先配置的数据结构,它保存顶点或三角形的角点。 一个简单的顶点只是在 3D 空间中指定顶点的位置作为 X、Y、Z 坐标。 但是,顶点可以具有额外的属性,例如颜色和顶点在 2D 图像中的坐标,用于纹理映射。

indexBuffer 将顶点链接在一起形成三角形。 它是一个整数列表,其中每个整数指定 vertexBuffer 中顶点的索引。 每三个索引指定一个三角形。 例如,如果列表中的前三个索引为 [0, 2, 1],则第一个三角形由连接顶点 0、2 和 1 定义。

drawElements 调用指示 WebGL 绘制由顶点和索引缓冲区定义的三角形。 真正酷的事情是接下来发生的事情。

对于 vertexBuffer 中的每个顶点,WebGL 执行一个程序,称为顶点着色器,该程序由 JavaScript 代码提供。 然后,WebGL 确定屏幕上的哪些像素被每个三角形“点亮”——这个过程称为光栅化。 对于这些像素中的每一个,称为片段,都会调用另一个程序,片段着色器。 这些程序是用一种类似 C 的语言编写的,称为 GLSL,它在系统的图形处理单元 (GPU) 上执行。 由于这种低级访问和 GPU 令人印象深刻的并行计算能力,这些程序可以非常快地进行复杂的计算,从而产生令人印象深刻的视觉效果。 当你考虑到它们每渲染帧被执行数十万甚至数百万次时,这一壮举尤其令人印象深刻。

Cesium 的片段着色器近似地模拟大气散射,模拟海浪,模拟阳光在海面上的反射等等。

WebGL 在 Windows、Linux 和 Mac OS X 上的现代浏览器中得到良好支持。 甚至 Android 版 Firefox 也支持 WebGL!

虽然我在上面的代码中展示了直接的 WebGL 调用,但 Cesium 实际上构建在超越 WebGL 本身的抽象级别的渲染器之上。 我们从不直接发出 drawElements 调用,而是创建命令对象,这些对象表示用于绘制的顶点缓冲区、索引缓冲区和其他数据。 这使得渲染器能够自动且优雅地解决深奥的渲染问题,例如对于像地球一样大的世界来说深度缓冲区精度不足的问题。 如果你感兴趣,可以阅读更多关于 Cesium 的 数据驱动渲染器 的信息。

有关在北美防空司令部追踪圣诞老人应用程序中使用的一些巧妙渲染效果的更多信息,请查看我们关于该主题的 博客文章

类型化数组和跨域资源共享

像 Cesium 这样的虚拟地球通过渲染虚拟地球以及地理参考数据(如道路、兴趣点、天气、卫星轨道甚至圣诞老人的当前位置)来提供对现实世界情况引人入胜的交互式 3D 视图。 虚拟地球的核心是地球本身的渲染,包括逼真的地形和卫星图像。

地形描述了地表的形状:山峰、隐藏的山谷、广阔的平原等等。 然后将卫星或航拍图像叠加在这个原本无色的表面上,使其栩栩如生。

北美防空司令部追踪圣诞老人应用程序中使用的全球地形数据来自 航天飞机雷达地形测绘任务 (SRTM),其在 -60 度到 60 度纬度之间每隔 90 米采样一次,以及 全球 30 秒弧度高程数据集 (GTOPO30),其在全球范围内每隔 1 公里采样一次。 数据集的总大小超过 10 千兆字节。

对于图像,我们使用 必应地图,他们也是北美防空司令部追踪圣诞老人团队的一部分。 此数据集的总大小更大,轻松达到太字节级别。

对于如此庞大的数据集,在渲染场景之前将所有地形和图像传输到浏览器显然是不切实际的。 因此,这两个数据集都被分解成数百万个称为切片的单个文件。 当圣诞老人飞遍全球时,Cesium 会根据需要下载新的地形和图像切片。

描述地球表面形状的地形切片是使用 简单格式 编码的二进制数据。 当 Cesium 确定它需要地形切片时,我们使用 XMLHttpRequest 下载它,并使用 类型化数组 访问二进制数据。

var tile = ...

var xhr = new XMLHttpRequest();

xhr.open('GET', terrainTileUrl, true);

xhr.responseType = 'arraybuffer';



xhr.onload = function(e) {

    if (xhr.status === 200) {

        var tileData = xhr.response;

        tile.heights = new Uint16Array(tileData, 0, heightmapWidth * heightmapHeight);

        var heightsBytes = tile.heights.byteLength;

        tile.childTileBits = new Uint8Array(tileData, heightsBytes, 1)[0];

        tile.waterMask = new Uint8Array(tileData, heightsBytes + 1, tileData.byteLength - heightsBytes - 1);

        tile.state = TileState.RECEIVED;

    } else {

        // ...

    }

};



xhr.send();

在类型化数组可用之前,这个过程会困难得多。 通常的做法是将数据编码为 JSON 或 XML 格式的文本。 这种数据不仅在通过网络传输时更大,而且在接收到数据后处理起来也会慢得多。

虽然使用类型化数组处理地形数据通常非常简单,但有两个问题使它变得有点棘手。

第一个是跨域限制。 地形和图像通常托管在与托管 Web 应用程序本身的服务器不同的服务器上,在北美防空司令部追踪圣诞老人中也是如此。 但是,XMLHttpRequest 通常不允许请求非源主机。 使用脚本标签而不是 XMLHttpRequest 的常见解决方法在这里行不通,因为我们正在下载二进制数据——我们不能将类型化数组与 JSONP 一起使用。

幸运的是,现代浏览器通过遵守跨域资源共享 (CORS) 标头提供了解决此问题的方案,该标头包含在服务器的响应中,指示响应可安全地在主机之间使用。如果您控制着 Web 服务器,则启用 CORS 非常容易,并且 Bing 地图在其瓦片文件中已经包含了必要的标头。但是,我们想在 Cesium 中使用的其他地形和影像源并不总是如此前瞻性,因此我们有时被迫通过同源代理路由跨域请求。

另一个棘手的方面是,现代浏览器只允许最多六个并发连接到给定主机。如果我们只是为 Cesium 请求的每个瓦片创建了一个新的 XMLHttpRequest,则排队的请求数量会很快变得很大。到最终下载瓦片时,查看器在 3D 世界中的位置可能已经发生变化,因此该瓦片甚至不再需要了。

相反,我们手动将自己限制为每个主机最多六个未完成的请求。如果所有六个插槽都被占用,我们不会启动新的请求。相反,我们将等待下一个渲染帧,然后再试一次。到那时,最高优先级的瓦片可能与上一帧不同,我们将很高兴当时没有排队请求。Bing 地图的一项不错的功能是它从多个主机名提供相同的瓦片,这使我们能够同时拥有更多未完成的请求,并更快地将图像加载到应用程序中。

Web 工作线程

提供给浏览器的地形数据主要是地形高度的数组。为了渲染它,我们需要将地形瓦片转换为具有顶点和索引缓冲区的三角形网格。此过程涉及将经度、纬度和高度转换为映射到WGS84 椭球体表面的 X、Y 和 Z 坐标。这样做一次非常快,但对于每个高度样本(每个瓦片有数千个)来说,这样做都会花费一些可衡量的时间。如果我们在单个渲染帧中对多个瓦片进行此转换,我们肯定会开始看到渲染中的卡顿。

一种解决方案是限制瓦片转换,每个渲染帧最多进行 N 次。虽然这有助于减少卡顿,但它无法避免瓦片转换与渲染竞争 CPU 时间,而其他 CPU 内核处于闲置状态。

幸运的是,另一个很棒的新的 Web API 出现了:Web 工作线程.

我们将通过 XMLHttpRequest 从远程服务器下载的地形 ArrayBuffer 作为可传输对象传递给 Web 工作线程。当工作线程收到消息时,它将使用顶点数据以准备直接传递给 WebGL 的形式构建一个新的类型化数组。不幸的是,Web 工作线程目前不允许调用 WebGL,因此我们无法在 Web 工作线程中创建顶点和索引缓冲区;相反,我们将类型化数组作为可传输对象发布回主线程。

这种方法的妙处在于,地形数据转换是与渲染异步进行的,并且如果可用,它可以利用客户端系统的多个内核。这将带来更流畅、更具交互性的圣诞老人追踪体验。

Web 工作线程简单优雅,但这种简单性对 Cesium 这样的引擎提出了挑战,该引擎旨在在各种不同类型的应用程序中使用。

在开发过程中,我们喜欢将每个类保存在单独的 .js 文件中,以便于导航,并避免在每次更改后进行耗时的合并步骤。每个类实际上都是一个独立的模块,我们使用异步模块定义 (AMD) API 和RequireJS 在运行时管理模块之间的依赖关系。

为了在生产环境中使用,将构成 Cesium 应用程序的数百个单独文件合并成一个文件,这将是一个很大的性能提升。这可能是一个包含所有 Cesium 的单个文件,也可能是一个用户选择的子集。将 Cesium 的部分合并到包含特定于应用程序的代码的更大文件中也可能是有益的,就像我们在 NORAD 跟踪圣诞老人应用程序中所做的那样。Cesium 支持所有这些用例,但与 Web 工作线程的交互变得棘手。

当应用程序创建 Web 工作线程时,它会向 Web 工作线程 API 提供要调用的 .js 文件的 URL。问题是,在 Cesium 的情况下,URL 会根据当前正在使用的上述用例而有所不同。更糟糕的是,工作线程代码本身需要根据 Cesium 的使用方式略有不同。这是一个大问题,因为工作线程无法访问主线程中的任何信息,除非该信息被显式地发布到它。

我们的解决方案是 cesiumWorkerBootstrapper。无论 WebWorker 最终将执行什么操作,它始终使用 cesiumWorkerBootstrapper.js 作为其入口点进行构建。引导程序的 URL 由主线程在可能的情况下推断,并且可以在必要时由用户代码覆盖。然后,我们将一条消息发布到工作线程,其中包含有关如何实际调度工作的详细信息。

var worker = new Worker(getBootstrapperUrl());



//bootstrap

var bootstrapMessage = {

    loaderConfig : {},

    workerModule : 'Workers/' + processor._workerName

};



if (typeof require.toUrl !== 'undefined') {

    bootstrapMessage.loaderConfig.baseUrl = '..';

} else {

    bootstrapMessage.loaderConfig.paths = {

        'Workers' : '.'

    };

}

worker.postMessage(bootstrapMessage);

工作线程引导程序包含一个简单的 onmessage 处理程序

self.onmessage = function(event) {

    var data = event.data;

    require(data.loaderConfig, [data.workerModule], function(workerModule) {

        //replace onmessage with the required-in workerModule

        self.onmessage = workerModule;

    });

};

当引导程序收到 bootstrapMessage 时,它使用 RequireJS 的 require 实现(也包含在 cesiumWorkerBootstrapper.js 中)来加载消息中指定的工作线程模块。然后,它通过将其 onmessage 处理程序替换为所需的处理程序来“成为”新的工作线程。

在 Cesium 本身被合并到单个 .js 文件中的用例中,我们还将每个工作线程合并到它自己的 .js 文件中,其中包含所有其依赖项。这确保每个工作线程只需要加载两个 .js 文件:引导程序加上组合的模块。

移动设备

使用 Web 技术构建像 NORAD 跟踪圣诞老人这样的应用程序最令人兴奋的方面之一是,有可能使用单个代码库在跨操作系统和设备之间实现可移植性。Cesium 使用的所有技术都已在桌面和笔记本电脑上的 Windows、Linux 和 Mac OS X 上得到良好支持。但是,这些技术越来越多地出现在移动设备上。

目前,手机和平板电脑上 WebGL 最稳定的实现是在 Firefox for Android 中。我们尝试在多台设备上运行 Cesium,包括运行 Android 4.2.1 和 Firefox 17.0 的 Nexus 4 手机和 Nexus 7 平板电脑。经过一些调整,我们能够运行 Cesium,性能令人惊讶地好。

但是,我们确实遇到了一些问题,可能是由于驱动程序错误造成的。一个问题是,在片段着色器中规范化向量有时根本不起作用。例如,像这样的 GLSL 代码

vec3 normalized = normalize(someVector);

有时会产生一个 normalized 向量,其长度仍然大于 1。幸运的是,可以通过添加另一个对 normalize 的调用来轻松解决此问题

vec3 normalized = normalize(normalize(someVector));

我们希望随着 WebGL 在移动设备上得到更广泛的采用,类似这样的错误将在设备和驱动程序发布之前被WebGL 一致性测试检测到。

完成的应用程序

作为长期 C++ 开发人员,我们最初对在开放 Web 上构建虚拟地球应用程序持怀疑态度。我们能够完成此类应用程序预期完成的所有事情吗?性能会好吗?

我很高兴地说我们已经转变了想法。现代 Web API(如 WebGL、Web 工作线程和类型化数组),以及 JavaScript 性能的持续和令人印象深刻的提升,使 Web 成为构建复杂 3D 应用程序的便捷、高性能平台。我们期待继续使用 Cesium 推动浏览器功能的极限,并利用随着时间的推移而出现的新的 API 和功能。

我们也期待使用这项技术在今年圣诞节作为 NORAD 跟踪圣诞老人团队的一部分,为全球数百万儿童带来有趣的 3D 圣诞老人追踪体验。12 月 24 日在 www.noradsanta.org 上查看。

关于 Kevin Ring

我是Cesium的创始开发者,Cesium 是一款开源的基于 Web 的虚拟地球和地图,也是书籍虚拟地球的 3D 引擎设计的合著者。我很幸运能够在Analytical Graphics, Inc.工作,这是一家很棒的公司,它允许我灵活地开发开源项目和写书。

更多 Kevin Ring 的文章……