Статьи

HTML5: визуализация открытых данных на 3D-глобусе с Three.js

С Three.js очень легко создавать 3D-объекты и визуализировать их с помощью WebGL. В нескольких предыдущих статьях я уже показал, как можно создавать 3D-карты и даже использовать данные высот для создания 3D-представлений о реальном мире . В этой статье мы продолжим немного дальше по этому пути. В этой статье я покажу вам, как вы можете визуализировать открытые данные на трехмерном глобусе. В этой первой статье я покажу вам, как вы можете создать следующую «инфографику»:

Мировая Плотность Населения_ 2010.png

На этом рисунке, рабочий пример которого вы можете найти здесь , показана плотность населения по всему миру, нанесенная на трехмерный глобус с использованием Three.js. Чтобы создать это, нам нужно сделать следующие шаги:

  1. Настройте сцену Three.js
  2. Создайте 3D глобус и добавьте его на сцену
  3. Получить информацию о плотности и преобразовать ее в формат, с которым мы можем работать
  4. Преобразовать каждую точку данных в координату на сфере
  5. Добавьте всю информацию в сцену Three.js
  6. Поверните сцену, чтобы мы могли видеть весь мир

Много шагов, но на самом деле это не так сложно сделать. Мы начнем, как и с каждым проектом Three.js, с основ и добавим код инициализации Three.js.

Настройте сцену Three.js

Следующий код — это основной код, необходимый для начала работы.

   // couple of constants
    var POS_X = 1800;
    var POS_Y = 500;
    var POS_Z = 1800;
    var WIDTH = 1000;
    var HEIGHT = 600;
 
    var FOV = 45;
    var NEAR = 1;
    var FAR = 4000;
 
    // some global variables and initialization code
    // simple basic renderer
    var renderer = new THREE.WebGLRenderer();
    renderer.setSize(WIDTH,HEIGHT);
    renderer.setClearColorHex(0x111111);
 
    // add it to the target element
    var mapDiv = document.getElementById("globe");
    mapDiv.appendChild(renderer.domElement);
 
    // setup a camera that points to the center
    var camera = new THREE.PerspectiveCamera(FOV,WIDTH/HEIGHT,NEAR,FAR);
    camera.position.set(POS_X,POS_Y, POS_Z);
    camera.lookAt(new THREE.Vector3(0,0,0));
 
    // create a basic scene and add the camera
    var scene = new THREE.Scene();
    scene.add(camera);
 
    // we wait until the document is loaded before loading the
    // density data.
    $(document).ready(function()  {
        jQuery.get('data/density.csv', function(data) {
            addDensity(CSVToArray(data));
            addLights();
            addEarth();
            addClouds();
            render();
        });
    });

В этом небольшом фрагменте кода мы создаем сцену Three.js, камеру и добавляем ее к определенному элементу на html-странице. Я использую JQuery, чтобы определить, когда документ готов. Когда HTML-страница полностью загружена, я читаю данные, чтобы построить и добавить различные элементы этой графики. Мы начинаем с простого, создавая трехмерный глобус Земли (функции addEarth и addCloud).

    // add the earth
    function addEarth() {
        var spGeo = new THREE.SphereGeometry(600,50,50);
        var planetTexture = THREE.ImageUtils.loadTexture( "assets/world-big-2-grey.jpg" );
        var mat2 =  new THREE.MeshPhongMaterial( {
            map: planetTexture,
            shininess: 0.2 } );
        sp = new THREE.Mesh(spGeo,mat2);
        scene.add(sp);
    }

Мы начинаем с очень простой земли. Эта земля отображается как идеальная сфера (которой в действительности земля не является), где мы добавляем текстуру, которая является спутниковой картой Земли. Я преобразовал карту в оттенки серого, чтобы сделать ее менее заметной в финальной сцене. Хороший исходный материал для карт Земли можно найти в НАСА здесь: http://visibleearth.nasa.gov/view_cat.php?categoryID=1484 . Базовая карта Земли не содержит облаков, мы можем легко добавить их, создав немного большая сфера с текстурой облаков.

 // add clouds
    function addClouds() {
        var spGeo = new THREE.SphereGeometry(600,50,50);
        var cloudsTexture = THREE.ImageUtils.loadTexture( "assets/earth_clouds_1024.png" );
        var materialClouds = new THREE.MeshPhongMaterial( { color: 0xffffff, map: cloudsTexture, transparent:true, opacity:0.3 } );
 
        meshClouds = new THREE.Mesh( spGeo, materialClouds );
        meshClouds.scale.set( 1.015, 1.015, 1.015 );
        scene.add( meshClouds );
    }

Я использовал текстуру облаков из примеров Three.js, но вы также можете найти другие текстуры онлайн. Теперь все, что нам нужно сделать, это добавить несколько источников света, и у нас есть базовая настройка глобуса.

// add a simple light
    function addLights() {
        light = new THREE.DirectionalLight(0x3333ee, 3.5, 500 );
        scene.add( light );
        light.position.set(POS_X,POS_Y,POS_Z);
    }

Это добавляет основной направленный свет (в той же позиции, что и наша камера). Я использовал синий цвет здесь. Если мы визуализируем эту сцену, мы получим следующее: земля с голубым свечением.

Мировая плотность населения_ 2010-1.png

Получить информацию о плотности и преобразовать ее в формат, с которым мы можем работать

После создания основного земного шара нам нужно получить некоторую информацию, которую мы можем использовать для построения земного шара. Для этого примера я использовал информацию о плотности населения из Центра социально-экономических данных и приложений — SEDAC . Оттуда вы можете скачать информацию о плотности в различных форматах. Я использовал 1-градусный формат ascii, который содержит точку данных для каждой комбинации широт / долгот Земли. Этот формат выглядит примерно так:

ncols         360
nrows         143
xllcorner     -180
yllcorner     -58
cellsize      1.0000000000008
NODATA_value  -9999
value1 value2 value3 value4 value5 (repeated 360 times)
value1 value2 value3 value4 value5 (repeated 360 times)

Таким образом, мы получили 143 строки и 360 столбцов, представляющих данные для всей Земли. В моей первой попытке я преобразовал это в данные json, но полученный файл был размером 1,5 МБ и занял некоторое время для анализа. Поэтому в следующей попытке я просто удалил заголовок и сохранил его как простой файл cvs, где каждая строка представляет собой координату x, y.

102,1,0.0003149387
103,1,0.0003149386
104,1,0.0003149387
105,1,0.0003149387
106,1,0.0003149387
107,1,0.0003149386
108,1,0.0003149387
109,1,0.0003149387
110,1,0.0003149387
133,1,0.008578668
etc..

Это также позволило мне отфильтровать значения -9999 и упростить обработку в javascript. Для загрузки этих данных я использую jquery:

jQuery.get('data/density.csv', function(data) {
            addDensity(CSVToArray(data));
             ...
        });

И используйте функцию CSVToArray, чтобы преобразовать данные в массив массивов. Функция CSVToArray была скопирована из этой статьи stackoverflow: http: //stackoverflow.com/questions/1293147/javascript-code-to-parse-csv -…

На данный момент у нас есть набор координат x, y (в стиле WGS84), который мы можем использовать для отображения этой информации на 2D-карте (как это делается на сайте SEDAC). Нам нужно преобразовать это x, y в точку на нашей сфере.

Преобразовать каждую точку данных в координату на сфере

Теперь, как нам преобразовать точку в двухмерном пространстве в трехмерную сферу? К счастью, для этого есть набор стандартных методов. Эта статья в википедии объясняет, как конвертировать различные системы координат. Не вдаваясь в подробности, код JavaScript для этого выглядит следующим образом:

    // convert the positions from a lat, lon to a position on a sphere.
    function latLongToVector3(lat, lon, radius, heigth) {
        var phi = (lat)*Math.PI/180;
        var theta = (lon-180)*Math.PI/180;
 
        var x = -(radius+heigth) * Math.cos(phi) * Math.cos(theta);
        var y = (radius+heigth) * Math.sin(phi);
        var z = (radius+heigth) * Math.cos(phi) * Math.sin(theta);
 
        return new THREE.Vector3(x,y,z);
    }

Эта функция преобразует координаты ax, y в точку в трехмерном пространстве. Радиус, предоставленный здесь, является радиусом нашей земли, а высота используется как смещение того, насколько высоко над поверхностью мы хотим начать рисование.

Добавьте всю информацию в сцену Three.js

Имея все это на месте, мы можем визуализировать информацию о плотности на сцене. Мы сделаем это в следующей функции JavaScript.

 // simple function that converts the density data to the markers on screen
    // the height of each marker is relative to the density.
    function addDensity(data) {
 
        // the geometry that will contain all our cubes
        var geom = new THREE.Geometry();
        // material to use for each of our elements. Could use a set of materials to
        // add colors relative to the density. Not done here.
        var cubeMat = new THREE.MeshLambertMaterial({color: 0x000000,opacity:0.6, emissive:0xffffff});
        for (var i = 0 ; i < data.length-1 ; i++) {
 
            //get the data, and set the offset, we need to do this since the x,y coordinates
            //from the data aren't in the correct format
            var x = parseInt(data[i][0])+180;
            var y = parseInt((data[i][1])-84)*-1;
            var value = parseFloat(data[i][2]);
 
            // calculate the position where we need to start the cube
            var position = latLongToVector3(y, x, 600, 2);
 
            // create the cube
            var cube = new THREE.Mesh(new THREE.CubeGeometry(5,5,1+value/8,1,1,1,cubeMat));
 
            // position the cube correctly
            cube.position = position;
            cube.lookAt( new THREE.Vector3(0,0,0) );
 
            // merge with main model
            THREE.GeometryUtils.merge(geom,cube);
        }
 
        // create a new mesh, containing all the other meshes.
        var total = new THREE.Mesh(geom,new THREE.MeshFaceMaterial());
 
        // and add the total mesh to the scene
        scene.add(total);
    }

В этом коде мы делаем следующее:

Сначала мы конвертируем x, y из входного формата в диапазон -90,90 — 180, -180.

 var x = parseInt(data[i][0])+180;
            var y = parseInt((data[i][1])-84)*-1;
            var value = parseFloat(data[i][2]);
</javscript>
 
 <h4>These coordinates are converted to a point on the sphere and used to draw a cube</h4>
 
Using the function we described earlier, we convert the x,y to a position on the sphere. These values are then used to create a cube.
 
<javascript>
            // calculate the position where we need to start the cube
            var position = latLongToVector3(y, x, 600, 2);
 
            // create the cube
            var cube = new THREE.Mesh(new THREE.CubeGeometry(5,5,1+value/8,1,1,1,cubeMat));

Вы можете видеть, что мы используем значение в качестве высоты для сферы.

Вращайте куб так, чтобы он хорошо совмещался с земным шаром

Если мы визуализируем сцену, как это, мы получаем красивые кубы, но все они направлены вверх, они не выровнены по поверхности сферы. Выравнивание этих объектов по нормальному вектору поверхности обычно требует некоторой интересной математики. К счастью, у Three.js есть более простой вариант для нас. мы можем использовать это:

cube.lookAt( new THREE.Vector3(0,0,0) );

Чтобы объект «смотрел» в определенной точке пространства. Если мы заставим объект смотреть в центр сферы, он будет правильно выровнен.

Уменьшите количество объектов для добавления

Мы сделали одну оптимизацию перед добавлением кубов на сцену.

function addDensity(data) {
        var geom = new THREE.Geometry();
        var cubeMat = new THREE.MeshLambertMaterial({color: 0x000000,opacity:0.6, emissive:0xffffff});
        for (var i = 0 ; i < data.length-1 ; i++) {
            ...
            var cube = new THREE.Mesh(new THREE.CubeGeometry(5,5,1+value/8,1,1,1,cubeMat));
            ...
            THREE.GeometryUtils.merge(geom,cube);
        }
        var total = new THREE.Mesh(geom,new THREE.MeshFaceMaterial());
        scene.add(total);
    }

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

Однако это все еще довольно тяжелый трехмерный объект, поэтому рендеринг, особенно на более медленном оборудовании, может занять некоторое время. С другой стороны, я смог просмотреть это на своем 2-летнем смартфоне.

Поверните сцену, чтобы мы могли видеть весь мир

Последний шаг, который мы добавим, — это простая вращающаяся анимация. На этот раз я поворачиваю камеру и свет.

function render() {
        var timer = Date.now() * 0.0001;
        camera.position.x = (Math.cos( timer ) *  1800);
        camera.position.z = (Math.sin( timer ) *  1800) ;
        camera.lookAt( scene.position );
        light.position = camera.position;
        light.lookAt(scene.position);
        renderer.render( scene, camera );
        requestAnimationFrame( render );
    }

Вот и все. Полный пример можно найти здесь .