С Three.js очень легко создавать 3D-объекты и визуализировать их с помощью WebGL. В нескольких предыдущих статьях я уже показал, как можно создавать 3D-карты и даже использовать данные высот для создания 3D-представлений о реальном мире . В этой статье мы продолжим немного дальше по этому пути. В этой статье я покажу вам, как вы можете визуализировать открытые данные на трехмерном глобусе. В этой первой статье я покажу вам, как вы можете создать следующую «инфографику»:
На этом рисунке, рабочий пример которого вы можете найти здесь , показана плотность населения по всему миру, нанесенная на трехмерный глобус с использованием Three.js. Чтобы создать это, нам нужно сделать следующие шаги:
- Настройте сцену Three.js
- Создайте 3D глобус и добавьте его на сцену
- Получить информацию о плотности и преобразовать ее в формат, с которым мы можем работать
- Преобразовать каждую точку данных в координату на сфере
- Добавьте всю информацию в сцену Three.js
- Поверните сцену, чтобы мы могли видеть весь мир
Много шагов, но на самом деле это не так сложно сделать. Мы начнем, как и с каждым проектом 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); }
Это добавляет основной направленный свет (в той же позиции, что и наша камера). Я использовал синий цвет здесь. Если мы визуализируем эту сцену, мы получим следующее: земля с голубым свечением.
Получить информацию о плотности и преобразовать ее в формат, с которым мы можем работать
После создания основного земного шара нам нужно получить некоторую информацию, которую мы можем использовать для построения земного шара. Для этого примера я использовал информацию о плотности населения из Центра социально-экономических данных и приложений — 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 ); }
Вот и все. Полный пример можно найти здесь .