Однажды мой босс пришел ко мне и моему коллеге Рону Аврааму и сказал: «Нам нужно улучшить нашу игру». Мы работали над этим проектом ГИС, используя Esri ArcGIS API 4.0 для JavaScript, и наш клиент проснулся утром в экспериментальном настроении. Им нужна была новая функция, которая будет визуализировать большое количество объектов на карте и часто обновлять их свойство местоположения. Они говорили о 10 000 сущностей, перемещающихся по карте, и их местоположение обновляется каждые 0,5 секунды.

Мне было интересно, сможет ли ArcGIS API справиться с этой ситуацией, используя старый добрый FeatureLayer . Я тестировал его на фиктивных данных, и, к сожалению, он не справлялся с нагрузкой. На обновление 10 000 объектов потребовалось более 500 миллисекунд, и я чувствовал себя побежденным. Если он работает так медленно с фиктивными данными, как он справится с потоком WebSocket, который был запланирован для передачи информации о местоположении объектов?

Почему ArcGIS API не справился с этой задачей? Что ж, он написан с использованием WebGL, и бог знает, какую невероятную и сложную графику можно создать с помощью WebGL. Так что я был почти уверен, что проблема не в WebGL. Но как насчет того, как ArcGIS API отображается в WebGL? Может, там и прячется узкое место?

Как подключить собственный алгоритм рендеринга WebGL

В этой части я покажу вам, как я решил свою проблему, используя собственный алгоритм рендеринга WebGL и создав пользовательский слой в ArcGIS API for JavaScript. Я основал свое решение на этом образце кода, который я модифицировал для своих целей. Я настоятельно рекомендую разобраться в процессе создания настраиваемого слоя, прежде чем продолжить чтение.

Данные

Для простоты я использовал фиктивные данные 10 000 сущностей и планировал сохранить их представление простым и связным (маленькие зеленые кораблики ⛵). Имитационные данные постоянно менялись за счет обновления местоположения сущностей, чтобы наши маленькие лодки могли перемещаться. В «реальном мире» объекты могут быть обновлены с использованием какого-либо источника подачи (например, WebSocket, как я предлагал ранее), но не будем на этом останавливаться.

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

Как рисовать лодки с использованием данных вершин (плохая практика)

Для меня интуитивно понятный способ рисования лодки - это линии, поэтому первая настройка - изменить режим рисования WebGL с TRIANGLES на LINES.

При использовании режима LINES данные вершин - это наш холст. Прежде чем начать рисовать лодки с помощью кода, давайте нарисуем ее с помощью Paint:

В примере кода, который мы модифицируем, есть алгоритм преобразования из местоположения (XY) в пиксель экрана, который понимает WebGL, поэтому каждое географическое положение объекта переводится в точку на экране, где должен быть нарисован его символ. Этот код переворачивает символ, который предоставляют данные вершины. Чтобы нейтрализовать его, вы можете перевернуть свой дизайн и использовать его точки в системе координат, как вы можете на мгновение увидеть в данных вершин.

Позже в статье я буду ссылаться на линии символа по их цвету на изображении выше.

Итак, теперь мы можем преобразовать эту форму из системы координат в данные нашей вершины:

Теперь vertexData содержит форму каждого изображения / объекта, и нам нужно настроить шейдер фрагмента, чтобы он отрисовал это:

И это должно выглядеть так:

Это решение работает очень хорошо, на обновление 10 000 объектов потребовалось 60 миллисекунд, но я не рекомендую это решение по трем основным причинам:

  1. Трудно рисовать. Даже для простого символа, такого как наша маленькая лодка, создать такой гигантский массив vertexData с трудом можно.
  2. Передача данных вершины - дорогостоящее действие с точки зрения производительности. В этом сценарии vertexData не является световым массивом, что делает это решение менее эффективным, чем может быть WebGL.
  3. Использование вершин для удержания графики - плохая практика. Атрибуты вершин могут включать положение, цвет, векторы (например, для создания затенения в трехмерной графике) и т. Д. За рисование графики должны отвечать шейдеры. И это подводит меня к следующему разделу.

Как рисовать лодки с помощью шейдеров

Хотя LINES режим может быть интуитивно понятным способом, я считаю его неуклюжим, громоздким и менее эффективным, чем может быть WebGL. Теперь давайте посмотрим, как мы можем снова изменить код и рисовать символы, используя только фрагментные и вершинные шейдеры. Поскольку шейдеры будут отвечать за форму символа лодки, мы можем удалить эту логику из массива verdexData и сохранить его чистым, указав только расположение наших сущностей:

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

Добавим в шейдеры логику рисования. Во фрагментном шейдере у нас есть 7 условий, так как количество строк составляет символ лодки (каждый вызов функции step является условием). Мы проверяем каждый пиксель / точку, является ли он частью линии, и устанавливаем для ее intensity значение 1 или 0 соответственно. Кроме того, abs функции определяют толщину линий.

Так же, как мы перевернули символ в последнем решении, мы должны сделать это и здесь. Именно это и делает первая строка фрагментного шейдера. Он переворачивает размеры x и y, чтобы убедиться, что наш символ находится в правильном направлении: vec2 coord = vec2(gl_PointCoord.x — 0.5, gl_PointCoord.y * (-1.0) + 0.5);

И вот оно:

Победа в игре - повышение производительности

Благодаря этому решению каждое обновление 10 000 объектов занимало всего 47 миллисекунд. Более эффективное, чем решение, использующее данные вершин, показало производительность 60 миллисекунд и в десять раз эффективнее обычного FeatureLayer с более чем 500 миллисекундами для того же количества объектов.