使用 WebGL 可视化地球的 10 万年历史

May 19, 2025 7-minute read programminghistory

当人类迁徙到美洲时,地球是什么样子的?在上一个冰河时代,海平面变化和巨大的冰盖在人类迁徙中起着关键作用,并解释了许多现在埋在海底的考古遗址。

我想要一种更好的方式来可视化地理如何塑造我们的历史。所以我建立了一个交互式的地球历史模型,涵盖了过去 10 万年。它包括海拔、海平面上升、气候变化和冰盖移动。它可以在你的浏览器中运行。在这篇文章中,我将解释我是如何使用多个科学数据集、数据处理、THREE.js 和 shaders 构建它的。想跳过这些直接体验吗?试试这个 Demo

Demo

海拔地图

我需要的最重要的数据是地球的全球海拔地图。此数据包含特定分辨率的地形高度。每个像素代表 -8714 米到 5724 米之间的高度,像珠穆朗玛峰这样的山峰不包括在内,因为实际的最高点太小。

我使用了 NOAA (National Centers for Environmental Information) 的 ETOPO Global Relief Model。但是这个数据集比我实际需要的要详细得多。这里的分辨率以弧秒为单位给出。一弧秒是 1/3600 度,在赤道大约是 30 米。就我的目的而言,我只需要大约 180 弧秒的分辨率(约 6 公里),所以我下载了 60 弧秒的文件并将其降采样到这个大小。这将其缩小到 7200x3600 像素。

然后我使用 geotiff 将其加载到 Node.js 中,并编写了一个脚本来进一步压缩它。我只需要海平面附近的高分辨率来确定确切的海岸线,所以我为此制作了一个自定义值范围。我通过为水下分配 25 个值,为陆地分配 25 个值,以及为海平面分配剩余的 206 个值,将每个高度值压缩为一个字节(最多 256 个值)。由于海平面仅波动约 160 米,因此该范围足以满足我的目的。结果保存到 1 通道(无颜色)PNG 文件。

Height Map

我使用自定义 shader 和 THREE.js 中的一个球体来渲染它。我加载高度纹理并使用纹理坐标对颜色进行采样。然后我将灰度值转换回高度,并根据其低于还是高于海平面来选择水或草的颜色。我还根据高度使输出颜色变暗,以使表面具有一定的纹理。

Show shader code

vec4 heightColor = texture2D(heightTexture, vUv);
float heightIntensity = heightColor.r * 255.0;
float height = 0.0;
if (heightIntensity < 25.0) {
 height = -150.0 + (25.0 - heightIntensity) / 25.0 * -8714.0;
} else if (heightIntensity <= 230.0) {
 height = heightIntensity - 175.0;
} else {
 height = (heightIntensity - 230.0) / 25.0 * 5724.0;
}
if (height < sealevel) {
 float darkness = 1.0 - (height / -8714.0 * 0.25);
 outColor = waterColor * darkness;
} else {
 float darkness = 1.0 - (height / 5724.0 * 0.25);
 outColor = grassColor * darkness;
}

Terrain Render

历史海平面

为了使其具有交互性,我需要有关历史海平面的数据。我从 NOAA Paleoclimatology Program 下载了 Global Sea Level Reconstruction dataset。它包含高达 80 万年前的全球海平面。我只提取了海平面值并将它们存储在一个二进制文件中。用户现在可以选择一个年份,shader 使用相应的海平面来绘制海岸线。

如果我现在在 15,000 年前和现在之间切换,你可以看到大不列颠和欧洲大陆之间的区域以前是连接的(见下图)。该地区被称为 Doggerland,渔民仍然从那里挖掘出骨制工具和猛犸象牙。另一个有趣的区域是白令海峡。俄罗斯和阿拉斯加之间的这个缺口曾经是陆地,它促进了早期人类从亚洲到美洲的迁徙。

Dogger Land

气候数据

现在,我有一个交互式地球仪,但一切仍然是绿色或蓝色。为了添加更多颜色,我需要有关气候的信息,例如降雨量和温度。幸运的是,有一个数据集 包含长达 300 万年前的模拟气候数据。你可以在下面看到每日平均降雨量的一个例子:

Precipitation Graph

我收集了过去 10 万年的所有降雨量和温度数据,并将其压缩到一个 1.2 MB 的文件中。我使用 sampler2DArray 将数据加载到 GPU 中。这基本上是一堆纹理,堆栈中的每一层都是特定的年份。纹理中的每个像素都包含红色、绿色和蓝色值,我用它们来编码最低和最高温度以及降雨量。通过对此纹理进行采样,你可以同时在时间和位置上进行采样。因此,如果当前查看年份介于两层之间,它将平均这些值。

然后我编写了一些代码来根据这些值选择渲染颜色,以及一个平滑过渡不同颜色的函数,这样就不会有硬边。例如,当温度高而降雨量低时,它是沙漠。但是如果降雨量很高,它会被渲染为森林。经过一些调整,这对于我的目的来说效果非常好。

Show shader code

vec3 slideColor(vec3 from, vec3 to, float value, float low, float high) {
 float ratio = (value - low) / (high - low);
 return mix(from, to, ratio);
}
vec3 color = slideColor(
 slideColor(
  snowColor, 
  slideColor(desertColor, grassColor, precipitation, 250.0, 500.0), 
  maxTemp, 5.0, 10.0
 ),
 slideColor(
  slideColor(desertColor, grassColor, precipitation, 400.0, 1000.0),
  slideColor(grassColor, forestColor, precipitation, 1000.0, 2000.0),
  precipitation, 200.0, 1000.0
 ),
 maxTemp, 0.0, 10.0
);

Climate Render

冰盖

最后一个冰河时代大约在 12,000 年前结束,对早期人类的定居产生了很大的影响。我需要关于历史冰盖位置的准确数据,以便在地球仪上可视化它。我最终使用了过去 80000 年的全球冰盖重建数据集。这是我能找到的最准确和最高分辨率的数据集,具有 0.25 度的网格和 2500 年的时间步长。

该数据集采用 NetCDF 格式,这是一种科学数据格式,我使用 netcdf4 对其进行了解析。它包含冰厚度数据,但就我的目的而言,知道是否存在冰盖就足够了。

Ice Raw

为了使用 GPU 有效地渲染它,我需要以某种方式将其转换为三角形。我不能仅仅使用纹理,因为数据限制,即使是非常大的纹理在放大时也会看起来很糟糕。我分多个步骤完成了三角剖分。首先,我使用 flood fill 算法找到所有单独的冰“岛”。然后我找到每个岛的边缘并将它们变成坐标列表。接下来,我运行一个平滑步骤来平滑边缘并优化所需的点数。

Ice Debug

然后我在每个“岛”形状的表面上以规则的间隔添加点(下图中的红点)。之后,我使用 Delaunay triangulation 通过有效地在点之间创建边缘,将其转换为三角形。你可以在下面看到结果:

Ice Triangles

然后我将这些平面点投影到地球仪上,并使用 THREE.js 渲染这些形状。在形状表面上添加的点对于确保平滑的曲率至关重要。shader 还使较高的海拔变暗,以使冰盖具有一定的纹理。

Ice Render

国界线

最后,我想添加现代国家边界,以便更容易理解某些特征的位置。我使用了 World Administrative Boundaries - Countries and Territories。此数据集存储不同国家之间所有边界的坐标。

我使用了与之前相同的平滑算法来平滑锯齿状边缘并减少点数。然后使用 THREE.js 线将其渲染在地球仪的顶部。

Borders Render

Demo

这个项目教会了我很多关于地理、数据处理以及在浏览器中使用 graphics shaders 的知识。你可以在这里查看实时 demo。也可以在下面的视频中观看:

我的下一步是寻找更准确的数据,或者可以追溯到更久远时代的数据集。并优化渲染。我还想在地球仪上显示历史事件,例如人类迁徙、早期文明和战争。我已经开始了,但还需要更多的工作。如果这种项目让你兴奋,请在 GitHub 上关注或通过 LinkedIn 联系我。

Events Preview