Визуализация траектории путешествия Three.js из Пекина в Шанхай

внешний интерфейс JavaScript three.js
Визуализация траектории путешествия Three.js из Пекина в Шанхай

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

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

Итак, приступим.

Анализ мыслей

Three.js рисует кубы, цилиндры и неправильные формы Мы все рисовали, но как нарисовать карту?

На самом деле карты тоже состоят из линий и полигонов.Имея данные мы можем их рисовать.Не хватает только данных.

Описание картографической информации является общим требованием, поэтому существует соответствующий международный стандарт, которыйGeoJson, который описывает географическую информацию с помощью точек, линий и полигонов.

Указав тип точки, линии и многоугольника, а затем указав несколько позиций координат, можно описать соответствующую форму.

Доступ к данным Geojson можно получить черезgeojson.ioПредварительный просмотр этого сайта.

Например, геоджсон карты Китая:

С этим json просто используйте Three.js, чтобы нарисовать его через линии и полигоны.

Но есть еще один вопрос.Информацию о широте и долготе, записанную в geojson, как преобразовать ее в двухмерные координаты, чтобы отрисовать?

Это включает в себя墨卡托转换, который должен преобразовать широту и долготу в двумерные координаты.

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

Таким образом, мы используем Three.js для рисования карты на основе geojson.

Мы также хотим нарисовать кривую из Пекина в Шанхай.Это делается с помощью кривой Безье.Мы знаем координаты двух конечных точек, а контрольная точка находится посередине.

Так как же узнать координаты двух конечных точек, то есть Шанхая и Пекина?

Вы можете использовать инструмент «Система захвата координат Baidu», чтобы щелкнуть определенное место на карте, и вы можете напрямую получить широту и долготу этого положения. Затем мы делаем преобразование Меркатора, чтобы получить координаты.

Нарисована карта и построена кривая движения, затем отрегулируйте положение камеры и медленно двигайтесь из Пекина в Шанхай.

Идея ясна, давайте напишем код.

Код

Мы собираемся импортировать d3, а затем использовать функцию преобразования Меркатора d3,

const projection = d3.geoMercator()
    .center([116.412318,39.909843])
    .translate([0, 0]);

Координаты средней точки — это широта и долгота Пекина, которые мы получили с помощью «Инструмента выбора координат Baidu».

Координаты Пекина и Шанхая также можно получить, переведя широту и долготу в меркаторские:

let beijingPosition= projection([116.412318,39.909843]);
let shanghaiPosition = projection([121.495721,31.236797]);

Не спешите рисовать кривую путешествия, давайте сначала нарисуем карту.

Сначала загрузите geojson:

const loader = new THREE.FileLoader();
loader.load('./data/china.json', (data) => {
    const jsondata = JSON.parse(data);
    generateGeometry(jsondata);
})

Затем нарисуйте карту на основе информации JSON.

Пройдите данные geojson, преобразуйте каждую широту и долготу в координаты через Меркатор, а затем нарисуйте их линиями и полигонами соответственно.

При рисовании полигонов я использую желтый цвет для Пекина и Шанхая и синий цвет для остальных городов.

function generateGeometry(jsondata) {
  const map = new THREE.Group();
    
  jsondata.features.forEach((elem) => {
    const province = new THREE.Group();

    // 经纬度信息
    const coordinates = elem.geometry.coordinates;
    coordinates.forEach((multiPolygon) => {
      multiPolygon.forEach((polygon) => {
        // 画轮廓线
        const line = drawBoundary(polygon);

        // 画多边形
        const provinceColor = ['北京市', '上海市'].includes(elem.properties.name) ? 'yellow' : 'blue';
        const mesh = drawExtrudeMesh(polygon, provinceColor);

        province.add(line);
        province.add(mesh);
      });
    });

    map.add(province);
  })

  scene.add(map);
}

Затем нарисуйте контурные линии и нарисуйте многоугольники соответственно:

Контур (Line) заключается в том, чтобы указать ряд вершин для формирования геометрии (Geometry), а затем указать цвет материала (Material) как желтый:

function drawBoundary(polygon) {
    const lineGeometry = new THREE.Geometry();

    for (let i = 0; i < polygon.length; i++) {
      const [x, y] = projection(polygon[i]);
      lineGeometry.vertices.push(new THREE.Vector3(x, -y, 0));
    }

    const lineMaterial = new THREE.LineBasicMaterial({ 
      color: 'yellow'
    });

    return new THREE.Line(lineGeometry, lineMaterial);
}

Эффект сейчас такой:

Полигоны — это ExtrudeGeometry, то есть можно сначала нарисовать фигуру, а потом сделать ее трехмерной, растянув.

function drawExtrudeMesh(polygon, color) {
    const shape = new THREE.Shape();

    for (let i = 0; i < polygon.length; i++) {
      const [x, y] = projection(polygon[i]);

      if (i === 0) {
        shape.moveTo(x, -y);
      }

      shape.lineTo(x, -y);
    }

    const geometry = new THREE.ExtrudeGeometry(shape, {
      depth: 0,
      bevelEnabled: false
    });

    const material = new THREE.MeshBasicMaterial({
      color,
      transparent: true,
      opacity: 0.2,
    })

    return new THREE.Mesh(geometry, material);
}

Используйте moveTo для первой точки и lineTo для следующих точек, чтобы сформировать многоугольник, а затем укажите толщину 0 и укажите, что нет необходимости в дополнительном скосе на стороне.

Таким образом, мы закрасили каждую провинцию цветом: Пекин и Шанхай — желтым, а остальные — синим.

Затем нарисуйте кривую Безье между Пекином и Шанхаем:

const line = drawLine(beijingPosition, shanghaiPosition);
scene.add(line);

Кривые Безье рисуются с помощью QuadraticBezierCurve3, а контрольная точка указывает точку посередине.

function drawLine(pos1, pos2) {
  const [x0, y0, z0] = [...pos1, 0];
  const [x1, y1, z1] = [...pos2, 0];

  const geomentry = new THREE.Geometry();
  geomentry.vertices = new THREE.QuadraticBezierCurve3(
      new THREE.Vector3(-x0, -y0, z0),
      new THREE.Vector3(-(x0 + x1) / 2, -(y0 + y1) / 2, -10),
      new THREE.Vector3(-x1, -y1, z1),
  ).getPoints();

  const material = new THREE.LineBasicMaterial({color: 'white'});

  const line = new THREE.Line(geomentry, material);
  line.rotation.y = Math.PI;

  return line;
}

Таким образом рисуется карта и дорожка путешествия:

Конечно, есть также коды инициализации для рендереров, камер и источников света:

Рендерер:

const renderer = new THREE.WebGLRenderer();
renderer.setClearColor(0x000000);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

Средство визуализации устанавливает черный цвет фона и размер холста в соответствии с размером окна.

свет:

let ambientLight = new THREE.AmbientLight(0xffffff);
scene.add(ambientLight);

Освещение использует окружающий свет, что означает, что свет и тень одинаковы во всех направлениях.

камера:

const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 0, 10);
camera.lookAt(scene.position);

Камера использует перспективную камеру, которая характеризуется большим ближним и дальним малым, и для нее необходимо указать четыре параметра, такие как угол обзора, соотношение сторон и диапазон дальнего и ближнего.

Позиция установлена ​​​​на 0 0 10. Обратите внимание на 0 0 0 в этой позиции, которая является видом сверху над Пекином (мы указали Пекин в качестве центра при преобразовании Меркатора).

После изменения положения камеры карта, которую вы видите, стала намного больше:

Следующим шагом будет рендеринг кадр за кадром и перемещение положения камеры при рендеринге каждого кадра Это эффект перемещения из Пекина в Шанхай:

function render() {
    if(camera.position.x < shanghaiPosition[0]) {
        camera.position.x += 0.1;
    }  
    if(camera.position.y > -shanghaiPosition[1]) {
        camera.position.y -= 0.2;
    }
    renderer.render(scene, camera);
    requestAnimationFrame(render);
}

Готово! Давайте посмотрим на окончательный эффект:

Код загружен на GitHub:GitHub.com/кварк глюон P…

Также размещаю копию здесь:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>map-travel</title>
    <style>
      html body {
        height: 100%;
        width: 100%;
        margin: 0;
        padding: 0;
        overflow: hidden;
      }
    </style>
  </head>
  <body>
    <script src="./js/three.js"></script>
    <script src="./js/d3.js"></script>
    <script>
      const scene = new THREE.Scene();

      const renderer = new THREE.WebGLRenderer();
      renderer.setClearColor(0x000000);
      renderer.setSize(window.innerWidth, window.innerHeight);
      document.body.appendChild(renderer.domElement);

      const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
      camera.position.set(0, 0, 10);
      camera.lookAt(scene.position);

      let ambientLight = new THREE.AmbientLight(0xffffff);
      scene.add(ambientLight);

      function create() {
          const loader = new THREE.FileLoader();
          loader.load('./data/china.json', (data) => {
            const jsondata = JSON.parse(data);
            generateGeometry(jsondata);
          })
      }

      const projection = d3.geoMercator()
            .center([116.412318,39.909843])
            .translate([0, 0]);

      let beijingPosition= projection([116.412318,39.909843]);
      let shanghaiPosition = projection([121.495721,31.236797]);

      function drawBoundary(polygon) {
        const lineGeometry = new THREE.Geometry();

        for (let i = 0; i < polygon.length; i++) {
          const [x, y] = projection(polygon[i]);
          lineGeometry.vertices.push(new THREE.Vector3(x, -y, 0));
        }

        const lineMaterial = new THREE.LineBasicMaterial({ 
          color: 'yellow'
        });

        return new THREE.Line(lineGeometry, lineMaterial);
      }

      function drawExtrudeMesh(polygon, color) {
        const shape = new THREE.Shape();

        for (let i = 0; i < polygon.length; i++) {
          const [x, y] = projection(polygon[i]);

          if (i === 0) {
            shape.moveTo(x, -y);
          }

          shape.lineTo(x, -y);
        }

        const geometry = new THREE.ExtrudeGeometry(shape, {
          depth: 0,
          bevelEnabled: false
        });

        const material = new THREE.MeshBasicMaterial({
          color,
          transparent: true,
          opacity: 0.2,
        })
        
        return new THREE.Mesh(geometry, material);
      }

      function generateGeometry(jsondata) {
          const map = new THREE.Group();

          jsondata.features.forEach((elem) => {
            const province = new THREE.Group();

            const coordinates = elem.geometry.coordinates;
            coordinates.forEach((multiPolygon) => {
              multiPolygon.forEach((polygon) => {
                const line = drawBoundary(polygon);

                const provinceColor = ['北京市', '上海市'].includes(elem.properties.name) ? 'yellow' : 'blue';
                const mesh = drawExtrudeMesh(polygon, provinceColor);
                
                province.add(line);
                province.add(mesh);
              });
            });

            map.add(province);
          })

          scene.add(map);
          const line = drawLine(beijingPosition, shanghaiPosition);
          scene.add(line);

      }

      function render() {
        if(camera.position.x < shanghaiPosition[0]) {
            camera.position.x += 0.1;
        }  
        if(camera.position.y > -shanghaiPosition[1]) {
            camera.position.y -= 0.2;
        }
        renderer.render(scene, camera);
        requestAnimationFrame(render);
      }

      function drawLine(pos1, pos2) {
          const [x0, y0, z0] = [...pos1, 0];
          const [x1, y1, z1] = [...pos2, 0];

          const geomentry = new THREE.Geometry();
          geomentry.vertices = new THREE.QuadraticBezierCurve3(
              new THREE.Vector3(-x0, -y0, z0),
              new THREE.Vector3(-(x0 + x1) / 2, -(y0 + y1) / 2, -10),
              new THREE.Vector3(-x1, -y1, z1),
          ).getPoints();

          const material = new THREE.LineBasicMaterial({color: 'white'});

          const line = new THREE.Line(geomentry, material);
          line.rotation.y = Math.PI;

          return line;
      }

      create();
      render();
    </script>
  </body>
</html>

Суммировать

Представление формы карты основано на спецификации geojson, состоящей из точек, линий, полигонов и другой информации.

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

Перед рисованием необходимо преобразовать широту и долготу в координаты, что требует преобразования Меркатора.

Мы рисуем линии с помощью Three.js, указывая ряд вершин для формирования геометрии, а рисование полигонов — это рисование формы, а затем ее экструдирование в 3D с помощью ExtrudeGeometry. Преобразования Меркатора напрямую используют встроенные функции d3. Эффект путешествия достигается за счет покадрового перемещения положения камеры.

Знаком с преобразованиями geojson и mercator, даже если это визуализация начального уровня, связанная с географией.

Вы также хотите сделать некоторые визуализации или взаимодействия, связанные с географией? Приходите и попробуйте.