用Three.js可视化数字高程模型【DEM】

在这个教程中,我们将学习如何使用three.js渲染土耳其最高的Ağrı山脉的数字高程模型(DEM)数据,使用的工具包括Three.js、geotiff、webpack和QGIS。

我们将要使用的数据是由USGS (美国地质调查局)免费提供的。使用USGS Earth Explorer我下载了Ağrı山脉的DEM(数字高程模型)和卫星图像,这是一个休眠的火山,也是土耳其境内最高的山。

我使用USGS应用程序下载了一些卫星影像,然后尝试着找出云层覆盖率小于10%的图像:

Landsat🛰️ - 用卫星影像做纹理

如果你不熟悉遥感和图像处理,你可能没有听说过Landsat。Landsat是由美国地质调查局控制的卫星,为研究人员提供约30平方米的分辨率的科学卫星图像已经很多年了。图像的一个像素覆盖30平方米的地面区域,卫星的摄像头与地球垂直。现在,有些卫星的分辨率可以做到小于一米,但一般来说,它们的数据不能免费获得。所以,Landsat对我们来说已经足够了,我们将使用Landsat卫星影像作为我们的3D模型的纹理。

下载Landsat卫星图像可以点这里。图像的云层覆盖范围应小于 10%,并且应将其添加到标准中。很难找到一个好的,因为山是如此之高,大部分情况下图像中都有很多云。在找到合适的图像后,我意识到Landsat覆盖了一个巨大的区域,需要裁剪感兴趣的区域作为3D模型的纹理。但更重要的是,我们需要一个数字高程模型来将山脉可视化。

SRTM🗻 - 栅格化DEM数据

SRTM是Shuttle Radar Topography Mission的缩写,中文含义是航天飞机雷达地形任务。SRTM由美国宇航局运营,提供栅格化的数字高程模型。SRTM 的 30平米 分辨率的DEM数据,意味着一个像素覆盖约30平米的面积,并且将该地区的平均高程作为像素值。这些数据对于使用three.js生成我们的山地模型非常有价值。

数据预处理

我们使用 QGIS栅格工具剪切、制作DEM和相关卫星图像的掩膜,并将它们复制到工作目录:

看起来像Mouth Doom,这是在QGIS中使用默认调色板显示高程模型的效果。考虑到性能问题,我裁剪了两个不同尺寸的图像,你可以在代码仓库中找到。在下面的示例中我们将使用其中较小的那个以便快速查看运行结果。

Three.js

Three.js是一个优秀的JS库,使WebGL更易于使用WebGL。在three.js世界中,我们需要一些基本的设置,其中的4个基本组件是:

  1. 场景
  2. 渲染器
  3. 相机
  4. 对象(包含材质)

添加场景灯光

我们将从添加场景开始,然后设置渲染器、摄像头、控件和光线。添加光线至关重要,否则你在场景中看不到任何东西。

 setupLight() {
    this.light = new THREE.DirectionalLight(0xffffff);
    this.light.position.set(500, 1000, 250);
    this.scene.add(this.light);
  }

用DEM数据生成山的模型

我们要渲染的几何形状不是使用Blender、Maya等软件建模的,相反,我们将使用DEM数据直接用js生成一个3D模型,借助于"geotiff"库:

import * as GeoTIFF from "geotiff";

  setupTerrainModel() {
    const readGeoTif = async () => {

     ... 
     
    };

    readGeoTif();
  }

首先读取图像文件:

const rawTiff = await GeoTIFF.fromUrl(terrain);
const tifImage = await rawTiff.getImage();
const image = {
  width: tifImage.getWidth(),
  height: tifImage.getHeight()
};

// Our initial plane geometry
const geometry = new THREE.PlaneGeometry(
  image.width,
  image.height,
  image.width - 1,
  image.height - 1
);

在setupTerrainModel函数实现中,将剪裁的图像添加到项目后,我们使用geotiff库来读取DEM文件,并添加一个新的与DEM图像相同大小的PlaneGeometry对象。好了,让我们来生成这个大家伙:

// Read image pixel values that each pixel corresponding a height
const data = await tifImage.readRasters({ interleave: true });

// Fill z values of the geometry
console.time("parseGeom");
geometry.vertices.forEach((geom, index) => {
  geom.z = (data[index] / 20) * -1;
});
console.timeEnd("parseGeom");


接下来我们读取每个像素的值 - 对应于高程值。我只是试探地将这个值除以20,使其看起来不错,并乘以-1,否则模型将颠倒过来 —这是因为three.js的z坐标方向 —我稍后会解释。使用console.time来跟踪代码性能。

现在我们的模型就可以显示出来了,但没有卫星图像,它只是一个3D白模:

纹理拟合

生成模型后,我们将使用 RGB 卫星图像,该图像也是之前用 QGIS 剪接的:

import * as mountainImage from "../textures/agri-small-autumn.jpg";

// ... other stuff

const texture = new THREE.TextureLoader().load(mountainImage);
const material = new THREE.MeshLambertMaterial({
  wireframe: false,
  side: THREE.DoubleSide,
  map: texture
});

我们正在加载卫星图像,并保存在material变量中,以便后续在Three.js的 MESH对象上使用。不要忘记旋转对象,因为three.js采用右手坐标系,这意味着,默认情况下,Z轴不是朝上而是指向你。关于这一点的详细解释可以查看这里

const mountain = new THREE.Mesh(geometry, material);
mountain.position.y = -100;
mountain.rotation.x = Math.PI / 2;

this.scene.add(mountain);

你可以在开始时使用camera.up.set(0,0,1)旋转 x 轴或旋转相机,然后在上面放置山的模型,沿y 轴向下移动一点点,稍微调整下,就可以了。

源代码下载:


原文链接:Visualizing a mountain using Three.js, Landsat and SRTM

BimAnt翻译整理,转载请标明出处