Математика наклона в картах, или как мы сделали небо
Недавно в карте 2ГИС появились небо и туман, которые можно увидеть, увеличив масштаб и наклон. В статье рассказываю, для чего нам понадобились эти фичи, с какими сложностями столкнулись в процессе исследований и как в итоге реализовали нужную функциональность.
Изначально карта не была готова к тому, чтобы её сильно наклоняли: при угле наклона, превышающему 45°, можно было наблюдать картинку, как на скриншоте ниже ↓
Дальний край карты располагался слишком близко к камере, карта заканчивалась и начиналась пустота, окрашенная в бежевый цвет. Крупные объекты, такие как парки, торговые центры, высотки, обрезались. Это убивало реалистичность карты.
К тому же в 2023-м в 2ГИС появились первые иммерсивные модели зданий. И для сценариев, где пользователи захотят детальнее рассмотреть новые 3D-модели, также стала необходима проработка механики работы с большим наклоном.
Что такое наклон карты
Сначала разберёмся, что такое наклон и к чему приводит его изменение. Нам понадобится немного математики.
В двумерном пространстве рассмотрим прямую A и точку C, которую будем называть камерой. Направление камеры — это ненулевой вектор, начинающийся в камере. Hc — это перпендикуляр опущенный из камеры на прямую A. Тогда наклон (α) — это угол между Hc, и направлением камеры.
Для простоты считаем, что
Введём ещё одно определение: видимая область (X) — это длина отрезка, отсекаемого от прямой А направлением камеры и перпендикуляром. Тогда
При увеличении угла наклона будет увеличиваться и размер видимой области, а при α = 90° размер области X равен бесконечности. Также обратим внимание на производную по α:
Можно сделать вывод, что даже малое приращение угла α сильно увеличивает видимую камерой область X.
Теперь рассмотрим то, как работает видимая область нашей карты. Усложним случай, рассмотренный выше: вместо прямой A камера будет смотреть на плоскость. Видимая область двумерная, а именно, трапеция, высота которой прямо пропорциональна tg (α).
Пример видимой области карты
Именно эту видимую область пользователь видит при просмотре карты, а мы должны заполнить её данными. Главный вывод на текущий момент: увеличение наклона ведёт к быстрому увеличению видимой области — следовательно, нам нужно значительно больше данных.
Сколько нужно данных
Данные до пользователя мы доставляем, используя векторные тайлы, которые располагаются в квадратной сетке.
В зависимости от величины масштаба карты размер и детальность тайлов меняются. Для меньшего масштаба применяются более крупные тайлы с меньшей детальностью, а для большего масштаба — наоборот.
Чтобы заполнить всю видимую область данными, нужно найти все тайлы, которые видны из неё, то есть где хотя бы одна точка тайла находится внутри видимой области.
Зелёная трапеция — видимая область. Синие квадраты — тайлы, которые нужно показать. Здесь размер тайла — 1,2 км²
Как мы уже выяснили, увеличение наклона приводит к быстрому увеличению размера видимой области, соответственно, быстро возрастает число тайлов, которые нужно загрузить по сети, обработать и нарисовать. Замостить тайлами всю видимую область для произвольного положения камеры мы не можем. Что же тогда делать?
Попытка замостить видимую область
По мере увеличения расстояния до тайла его полезность для пользователя снижается: он занимает всё меньший размер видимой области, детали сливаются и становятся нечитаемыми.
Это натолкнуло нас на идею для исследования: можно попробовать использовать подход аналогичный LOD«ам в играх. А именно, использовать тайлы меньшей детальности по мере увеличения расстояния до камеры. В таких тайлах меньше данных, и они покрывают большую площадь, что может обеспечить покрытие большей части видимой области без создания каши из объектов вдали.
Пример видимой области, заполненной тайлами разного размера. Красной стрелкой обозначена граница детальности
Просто так использовать более крупные тайлы не получилось, мы столкнулись с двумя проблемами.
1. Для объектов, которые попадают в несколько тайлов, может возникнуть ситуация, когда они разрезаются границей, где начинаются тайлы меньшей детальности. Вот так, например, выглядит Москва Сити, если посмотреть на неё с определенного ракурса:
Москва Сити пропал потому что в тайле меньшей детальности нет данных о высотах зданий
2. При изменении видимой области (например, вращении и перемещении камеры) на границе смены детальности можно увидеть мелькание объектов, так как они отсутствуют в менее детальных тайлах:
Первую проблему решить в общем виде можно только на этапе подготовки векторных тайлов. На уровне движка мы могли только изменять удаление границы детальности от камеры. К сожалению, удаление границы детальности от камеры значительно увеличивает количество загружаемых тайлов и объектов, которые нужно нарисовать.
Проблему номер два мы пытались решить, изменив геометрию области видимости. Теперь это была не трапеция, а сектор окружности с центром в центре карты определенного радиуса. Использование такой геометрии для видимой области усложнило бы определение видимых тайлов.
Пока что мы отказались от замощения видимой области тайлами меньшей детальности. Это требует больше ресурсов от пользовательских устройств и усложняет работу движка. И хоть замощение видимой области тайлами улучшает иммерсивность, в большинстве случаев это не так важно для пользователя, так как тайлы находятся вдали от камеры.
После всех проведенных исследований мы решили не изменять логику работы видимой области. Так как на текущий момент очень сложно добиться приемлемого результата за предсказуемые ресурсы.
Единственное незначительное изменение, которое было сделано — максимальный размер видимой области на крупных масштабах был увеличен до размера тайла 15 зума. Это не оказывает влияния на работу карты при небольших наклонах и зумах, но позволяет хоть немного замостить видимую область вдали.
Таким образом карта стала выглядеть как на картинке выше. Единственное, что выглядело не очень — участок, выделенный красным прямоугольником. На краю карты простирается полотно светло-бежевого цвета, которое никак не напоминало небосвод. Поэтому для большей реалистичности мы решили отрисовать небо.
Небо
У нас было несколько экспериментов с градиентами. Обсуждали даже реалистичные модели, которые используют в играх. Они учитывают разные типы рассеивания на частицах в атмосфере, но пока от них отказались в угоду производительности. Даже при максимальном наклоне небо занимает всего лишь небольшой участок экрана.
Самое примитивное решение — залить всё однотонным цветом, что мы в итоге и сделали. Дёшево, сердито, но эффективно.
Реализуется это так: перед отрисовкой объектов карты на canvas рисуем прямоугольник нужного цвета на весь экран. В результате мы почти достигли нужного результата, но выглядит слишком примитивно. Начали работать с туманом.
Туман
У тумана на карте две цели. Первая — он должен скрыть некрасивую границу тайлов. Вторая — потенциально оптимизировать то, что он скрывает: либо совсем скрывать все здания за ним, либо рисовать их сильно менее детальными.
Мы не можем использовать отдельный проход для создания тумана, потому что наша карта богата прозрачными 3D-объектами. Поэтому мы попробовали смешать цвета объектов с цветом тумана в зависимости от расстояния от этих объектов до камеры. Как функцию распределения интенсивности тумана мы выбрали обычную линию — это самый быстрый вариант. В играх часто используют нелинейные зависимости, потому что они дают более реалистичное распределение, но в картах нам пока это не нужно.
Получается с какого-то расстояния туман становится видимым и при удалении от камеры он постепенно заслоняет собой объекты. Немного поигравшись с границами на близком зуме, мы получили примерно такую картинку.
Слева — маска, по которой применяем туман, а справа — как это выглядит в итоге. Мы здесь немного увеличили настройки тумана, чтобы его было лучше видно
Для этого в вершинном шейдере конкретного объекта нам нужно рассчитать расстояние от камеры до объекта в мировой системе координат и степень скрытия туманом:
uniform mat4 u_mat4_model;
uniform vec3 u_camera_position;
uniform float u_fog_start_distance;
varying vec3 v_relative_to_fog_position;
void calculate_fog_position(vec3 object_position) {
// мировые координаты объекта
vec3 world_position = u_mat4_model * object_position;
// относительная величина, которая определяет степень скрытия объекта туманом
v_relative_to_fog_position = (world_position - u_camera_posision) / u_fog_start_distance;
}
Во фрагментном шейдере мы смешиваем цвет объекта и цвет тумана в зависимости от v_relative_to_fog_position:
uniform float u_float_fog_start; // граница начала тумана
uniform float u_float_fog_end; // граница конца тумана (объекты дальше нее полностью скрыты туманом)
uniform vec3 u_vec3_fog_color; // цвет тумана
varying vec3 v_relative_to_fog_position;
vec4 apply_fog(vec4 color) {
float fog_factor = (length(v_relative_to_fog_position) - u_float_fog_start) / (u_float_fog_end - u_float_fog_start);
color.rgb = mix(color.rgb, u_vec3_fog_color, fog_factor);
}
Для некоторых объектов мы не смешиваем их цвета с цветом тумана, а увеличиваем их прозрачность (например POI).
В этом подходе есть две очевидные и одна неочевидная проблемы.
Туман не влияет на небо, потому что оно не учитывается в глубине при рендеринге. Иными словами, мы видим слишком жёсткий переход на горизонте.
Иконки и некоторые другие 2D-объекты явно выбиваются из общей картины, но к их цвету нельзя просто применить цвет тумана, так же как к остальным объектам, поскольку они рисуются поверх всех остальных слоёв.
Неочевидно то, что если мы уменьшим масштаб карты, то монотонный туман окутает вообще всё.
Первую проблему мы решили, добавив в шейдер неба информацию о тумане.
uniform float u_float_fog_horizon_blend; // коэффициент блендинга тумана с небом
varying vec3 v_relative_to_fog_position;
float calculate_blending_factor(vec3 dir, float blend_factor) {
// рассчитывает вертикальный градиент, который обеспечивает переход туман-небо
}
vec4 apply_fog_to_sky(vec4 color) {
float depth = length(v_relative_to_fog_position);
vec3 dir = v_relative_to_fog_position / depth;
float blending_factor = calculate_blending_factor(dir, u_float_fog_horizon_blend);
color.rgb = mix(color.rgb, u_fog_color.rgb, blending_factor);
}
A ещё учли цвет неба при применении тумана ко всем объектам на карте.
Со второй проблемой разобрались, снизив прозрачность иконок по тому же закону, по какому мы затуманиваем другие объекты. На наш взгляд, получилось довольно гармонично. Лишние иконки не мешают при навигации по карте, а читаемость того, что находится вблизи, не пострадала.
А вот неочевидная проблема заслуживает отдельного параграфа.
Туман на разных масштабах
Предположим, мы настроили расстояние, которое нас устраивает и на котором растёт интенсивность тумана. И сделали это на близком масштабе. Картинка выглядит хорошо. При вращении камеры тоже не возникает проблем. Казалось бы, всё готово. Но это не так. Стоит отдалить камеру, уменьшив масштаб, как туман окутывает весь экран. Мы этого совсем не хотим — у нас всё-таки карта, а не Сайлент Хилл. Нам в первую очередь важна удобная навигация.
Вот как эта проблема выглядит сбоку.
Всё, что находится за пределами сферы, полностью скрыто в тумане. Слева — вид, который мы получаем на предыдущих скринах. А справа расстояние до всех объектов настолько велико, что туман даёт 100-процентную заливку
Изначально все наши идеи крутились вокруг более сложной функции тумана. Наиболее многообещающим вариантом казалось снижение интенсивности в конусе под камерой. Но мы не будем приводить все неудачные попытки решить эту проблему.
В итоге пришли к очень простому варианту, который полностью покрыл наши требования: мы привязали распределение тумана к расстоянию до точки вращения камеры. Это выглядит примерно так.
Что получилось
В итоге получилось реализовать продуктовые требования: теперь карты на 2gis.ru можно рассматривать под большим наклоном и заодно лицезреть голубое небо. Особенно приятно, что мы смогли добиться желаемого эффекта и не потерять производительность карты.
Сейчас большой наклон и туман работает на 2gis.ru, а так же доступен в нашем MapGL JS API. Следующим этапом опубликуем их в наших мобильных приложениях.
Если у вас остались вопросы, буду рад ответить в комментариях. А если захотите развивать трёхмерную карту в браузере с нами — в команде Web-карты сейчас как раз открыта вакансия.