Статьи

Three.js: визуализация ландшафта реального мира из карты высот с использованием открытых данных

Three.js — отличная библиотека для создания 3D-объектов и анимации. В нескольких предыдущих статьях я немного исследовал эту библиотеку, и в одном из этих примеров я показал вам, как вы можете взять формат информации ГИС (в geoJSON) и использовать D3.js и three.js для преобразования ее в трехмерную сетку, которую вы можете сделать в браузере, используя JavaScript. Это отлично подходит для инфографики, но на самом деле не показывает реальную карту, реальную местность. Three.js, к счастью, также имеет вспомогательные классы для рендеринга ландшафта, как вы можете увидеть в этой демонстрации: http://mrdoob.github.com/three.js/examples/webgl_terrain_dynamic.html

Эта демонстрация использует генератор шума для генерации случайной местности и добавляет множество дополнительных функций, но мы можем использовать эту концепцию и для рендеринга карт реальной местности. В этой статье я покажу вам, как вы можете использовать свободно доступные открытые геоданные, содержащие информацию о высоте, для рендеринга простой трехмерной местности с использованием three.js. В этом примере мы будем использовать данные высот, которые визуализируют данные для острова Корсика.

corsica.jpg

Или посмотрите живой сайт здесь: http://www.smartjava.org/examples/heightmap.

Где мы получаем данные?

Открытые данные, которые мы будем использовать, поступают из ASTER / GDEM. Который:

«Глобальная цифровая модель рельефа АСТЕР (ASTER GDEM) — это совместный продукт, разработанный и представленный общественности Министерством экономики, торговли и промышленности Японии (METI) и Национальным управлением по аэронавтике и исследованию космического пространства США (НАСА). Он создан на основе данных, собранных с помощью усовершенствованного космического радиометра для испускания и отражения тепла (ASTER), космического оптического прибора для наблюдения Земли ».

Этот проект предлагает нам данные о высоте (с геотегами) с разрешением 30 метров. На этом сайте вы можете скачать плитки для определенных областей (после бесплатной регистрации):

АСТЕР GDEM.jpg

Плитки, которые вы загружаете оттуда, имеют формат GeoTIFF (о котором я расскажу в следующей статье). Этот формат содержит значения для определенных географических координат. Three.js, однако, не может напрямую работать с геотифовыми изображениями (которые в любом случае являются очень большими), поэтому нам нужно преобразовать их в JPEG или PNG. Для этого мы будем использовать GDAL . GDAL — это библиотека и набор утилит, которые можно использовать для выполнения широкого спектра функций, связанных с ГИС. Если вы используете Mac, вы можете установить GDAL через macports:

sudo port install gdal
 
--->  Installing gdal @1.9.0_0+expat
--->  Activating gdal @1.9.0_0+expat
--->  Cleaning gdal

Поэтому загрузите несколько плиток и убедитесь, что форма, которую вы загружаете, представляет собой прямоугольник (см. Предыдущий скриншот). Разархивируйте плитки и скопируйте все файлы «* _dem.tif» в один каталог. Это на самом деле не нужно, но облегчит обработку. С GDAL теперь вы можете конвертировать эти загруженные плитки в PNG. Для этого я использую следующий очень простой скрипт bash.

#!/bin/bash
for file in `ls -d *dem.tif` ; do
   gdal_translate -b 1 -outsize 400 400 -scale -20 2200 -of PNG "$file" ~/output/$file.png
done

Это преобразует все плитки «dem.tif» в набор PNG. Каждая плитка имеет размер 400×400 пикселей, и мы также определяем диапазон входного сигнала от -20 до 2200. Другими словами, высота от входного сигнала составляет от -20 до 2200 м. Поскольку серая шкала в PNG может быть только в диапазоне 256 , это будет сокращено. (Мы можем использовать другие входные форматы или использовать шкалу RGB, но для этого потребуется некоторый пользовательский код, который немного выходит за рамки данной статьи). Однако для большинства карт этого должно быть достаточно для общего впечатления от ландшафта.

Одна из плиток для Корсики выглядит так:

ASTGTM2_N39E009_dem.tif.png

Так что теперь мы застряли с набором отдельных PNG. Нам нужно объединить их в один PNG, который мы можем использовать в качестве карты высот для Three.js. Для этого мы будем использовать другую библиотеку с открытым исходным кодом: imagemagick (также доступна через macports). В примере для Corsica у нас есть 8 файлов, которые объединены в один png, используя следующий пакетный файл:

process="ASTGTM2_N41E008_dem.tif.png ASTGTM2_N41E009_dem.tif.png  ASTGTM2_N40E008_dem.tif.png ASTGTM2_N40E009_dem.tif.png  ASTGTM2_N39E008_dem.tif.png ASTGTM2_N39E009_dem.tif.png ASTGTM2_N38E008_dem.tif.png ASTGTM2_N38E009_dem.tif.png"
montage -tile 2x4 -border 0 -frame 0 -geometry '1x1+0+0<' $process combined.png

В результате получается один PNG-файл с именем «комбинированный.png», который выглядит следующим образом (уменьшенный):

combined.png

Вот и все для подготовительной части. Мы знаем, что получили карту высот в оттенках серого, которую мы можем использовать в Three.js (и во многих других программах в этом отношении).

Создайте карту с помощью Three.js

Как только мы получим карту высот, мы можем использовать ее в Three.js. Следующий код показывает, как это сделать:

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
        "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <title></title>
    <script type="text/javascript" src="js/Three.js"></script>
    <script type="text/javascript" src="js/ShaderTerrain.js"></script>
    <script type="text/javascript" src="js/jquery-1.7.2.js"></script>
</head>
<body>
 
<div id="main_map">
 
</div>
 
<script type="text/javascript">
 
    var CONS = {
        // THREE.JS CONSTANTS
        // set the scene size
        WIDTH:904,
        HEIGHT:604,
 
        // set some camera attributes
        VIEW_ANGLE:45,
        NEAR:0.1,
        FAR:10000,
 
        CAMERA_X:1000,
        CAMERA_Y:600,
        CAMERA_Z:1300
    }
 
    var scene = {};
    var renderer = {};
    var camera = {};
    var controls;
 
 
    var n = 0;
    initMap();
 
    // Wait until everything is loaded before continuing
    function loaded() {
        n++;
        console.log("loaded: " + n);
 
        if (n == 3) {
            terrain.visible = true;
            console.log('ff');
            render();
        }
    }
 
    function initMap() {
 
        // setup default three.js stuff
        renderer = new THREE.WebGLRenderer();
        renderer.setSize(CONS.WIDTH, CONS.HEIGHT);
        renderer.setClearColor(0x0000cc);
        $("#main_map").append(renderer.domElement);
 
        camera = new THREE.PerspectiveCamera(CONS.VIEW_ANGLE, CONS.WIDTH / CONS.HEIGHT, CONS.NEAR, CONS.FAR);
        scene = new THREE.Scene();
        scene.add(camera);
 
        camera.position.z = CONS.CAMERA_Z;
        camera.position.x = CONS.CAMERA_X;
        camera.position.y = CONS.CAMERA_Y;
        camera.lookAt(scene.position);
 
        // add a light
        pointLight = new THREE.PointLight(0xFFFFFF);
        scene.add(pointLight);
        pointLight.position.x = 1000;
        pointLight.position.y = 3000;
        pointLight.position.z = -1000;
        pointLight.intensity = 8.6;
 
 
        // load the heightmap we created as a texture
        var texture = THREE.ImageUtils.loadTexture('assets/combined.png', null, loaded);
 
        // load two other textures we'll use to make the map look more real
        var detailTexture = THREE.ImageUtils.loadTexture("assets/bg.jpg", null, loaded);
 
        // the following configuration defines how the terrain is rendered
        var terrainShader = THREE.ShaderTerrain[ "terrain" ];
        var uniformsTerrain = THREE.UniformsUtils.clone(terrainShader.uniforms);
 
        // how to treat abd scale the normal texture
        uniformsTerrain[ "tNormal" ].texture = detailTexture;
        uniformsTerrain[ "uNormalScale" ].value = 1;
 
        // the displacement determines the height of a vector, mapped to
        // the heightmap
        uniformsTerrain[ "tDisplacement" ].texture = texture;
        uniformsTerrain[ "uDisplacementScale" ].value = 100;
 
        // the following textures can be use to finetune how
        // the map is shown. These are good defaults for simple
        // rendering
        uniformsTerrain[ "tDiffuse1" ].texture = detailTexture;
        uniformsTerrain[ "tDetail" ].texture = detailTexture;
        uniformsTerrain[ "enableDiffuse1" ].value = true;
        uniformsTerrain[ "enableDiffuse2" ].value = true;
        uniformsTerrain[ "enableSpecular" ].value = true;
 
        // diffuse is based on the light reflection
        uniformsTerrain[ "uDiffuseColor" ].value.setHex(0xcccccc);
        uniformsTerrain[ "uSpecularColor" ].value.setHex(0xff0000);
        // is the base color of the terrain
        uniformsTerrain[ "uAmbientColor" ].value.setHex(0x0000cc);
 
        // how shiny is the terrain
        uniformsTerrain[ "uShininess" ].value = 3;
 
        // handles light reflection
        uniformsTerrain[ "uRepeatOverlay" ].value.set(3, 3);
 
        // configure the material that reflects our terrain
        var material = new THREE.ShaderMaterial({
            uniforms:uniformsTerrain,
            vertexShader:terrainShader.vertexShader,
            fragmentShader:terrainShader.fragmentShader,
            lights:true,
            fog:false
        });
 
        // we use a plain to render as terrain
        var geometryTerrain = new THREE.PlaneGeometry(2000, 4000, 256, 256);
        geometryTerrain.applyMatrix(new THREE.Matrix4().makeRotationX(Math.PI / 2));
        geometryTerrain.computeFaceNormals();
        geometryTerrain.computeVertexNormals();
        geometryTerrain.computeTangents();
 
        // create a 3D object to add
        terrain = new THREE.Mesh(geometryTerrain, material);
        terrain.position.set(0, -125, 0);
        terrain.rotation.x = -Math.PI / 2;
 
        // add the terrain
        scene.add(terrain);
 
        // tell everything is ready
        loaded();
    }
 
    // render the scene
    function render() {
        renderer.render(scene, camera);
    }
</script>
</body>
</html>

И это все, что вам нужно сделать. После того, как вы установили эту базовую настройку, вы можете легко добавлять пользовательские текстуры, туман или другие дополнительные объекты. Если вы хотите увидеть этот базовый пример в действии, посмотрите здесь: http://www.smartjava.org/examples/heightmap