Фото Алексея Румянцева
Фото Алексея Румянцева

Мы делаем карты 2ГИС реалистичными, чтобы приложение максимально детально отражало город вокруг: чтобы у зданий были не гладкие стены, а настоящий фасад, в парках росли деревья и стояли лавочки, а скульптуры выглядели точно так же, как и в реальности. Такая реалистичность помогает в навигации: гораздо проще сравнивать то, что ты видишь в реальности, с моделью этого здания, а не с гладкой коробкой. Поддержка освещения объектов, которую мы добавили на 2gis.ru — ещё один шаг к реалистичности.

Расскажу, чего мы добились на сегодня, на какие компромиссы пришлось пойти и чуть-чуть о том, что под капотом движка освещения и теней.

Освещение и тени играют ключевую роль в восприятии объектов в трёхмерном пространстве. Они помогают лучше понимать форму, глубину и расположение объектов относительно друг друга. Без правильного освещения объекты могут выглядеть плоскими и неестественными, а без теней — «парящими» в воздухе, что сильно снижает реализм.

классический пример для понимания работы теней и нашего мозга
классический пример для понимания работы теней и нашего мозга

Для трёхмерных карт это особенно важно: тени позволяют пользователям интуитивно оценивать высоту зданий и расстояния между объектами.

Освещение в карте

Трёхмерные объекты на WebGL-картах 2ГИС условно можно разделить на простые и более реалистичные. Для простых «коробок», которые схематически изображают здания, мы сделали совсем простое освещение, состоящее из двух компонент:

  • Diffuse — буквально сводящийся к вычислению скалярного произведения направления освещения и вектора нормали объекта.

  • Ambient — рассеянный свет. Про него подробнее напишу ниже.

Для полноценных моделей — с текстурами и параметрами материалов — мы реализовали более сложное трёхкомпонентное освещение.

  • Диффузное освещение (Diffuse) — свет, который равномерно рассеивается по поверхности объекта. Диффузное освещение помогает передать форму и объём объекта, делая поверхность видимой с разных углов. Мы использовали затенение по Фонгу. Если оставить только его, модели будут иметь такой специфичный «лунный» вид:

  • Окружающее освещение (Ambient) — мягкий, рассеянный свет, который заполняет всё пространство сцены, даже в тенях. В реальном мире свет отражается от множества объектов, и даже те места, которые напрямую не освещены источником света, всё равно получают часть рассеянного света. Ambient-компонент помогает избежать полностью чёрных теней, делая сцену более естественной. С этим освещением картина приобретает более естественный вид:

  • Зеркальное освещение (Specular) — свет, который отражается от поверхности объекта под определённым углом — как блик на стекле или металле. Зеркальное освещение создаёт яркие пятна света (блики) на глянцевых или блестящих поверхностях, добавляя реалистичности и подчёркивая материал объекта. Чем сильнее отражение, тем более гладкой и блестящей кажется поверхность. Так эта компонента повлияла на финальную картинку. Обратите внимание на купола:

Наш подход к теням

Для отрисовки теней в realtime используют различные подходы. Самый реалистичный — техники, базирующиеся на трассировке лучей (raytracingpath tracing). Но так как в браузере нет аппаратной поддержки трассировки, мы такие алгоритмы даже не рассматривали. 

Ещё интересны подходы, базирующиеся на расчёте луча света в рамках экрана — screen space техники. На данный момент никто не написал их реализацию, предлагающую приемлемый баланс качества и производительности. И эти алгоритмы, хоть и многообещающие, тоже не устроили нас из-за слишком медленной скорости отрисовки.

Более традиционные для растеризуемой графики методы — это Shadow Volume и Shadow Map. С тем объёмом геометрии и разрешениями экрана, которые используются сегодня, Shadow Volume гарантировано будет значительно медленнее Shadow Map. Собственно, это основная причина, почему последние уже давно повсеместно вытеснили Shadow Volume в той же в игровой индустрии, где борьба за производительность не прекращается десятилетиями. 

Таким образом, для теней у нас не было особых альтернатив самой популярной на сегодня технике — Shadow Map. 

С Shadow Map сначала сцена рендерится с точки зрения источника света, сохраняя информацию о расстоянии до объектов. Затем, при обычном рендеринге, проверяется, находится ли каждая точка сцены в тени, сравнивая её расстояние до источника света с сохранённой картой глубины. Если точка дальше, чем значение на Shadow Map, значит она — в тени.

Принцип работы:

  • Для удобства восприятия текстура глубины ниже представлена в черно-белом виде — чем ближе объект к источнику освещения, тем пиксель темнее. Там, где он светлее — дальше.

  • Чтобы устранить этот артефакт, мы добавляем небольшой отступ при чтении из текстуры глубины. Обычно он называется bias. Главное — не переборщить с его величиной, иначе возникнут другие артефакты в виде теней, отстоящих от объектов на некоторое расстояние. Как результат — картина становится сильно лучше:

  • Осталось сгладить тени простым box-фильтром, и получается финальный результат:

Конечно, не всё прошло гладко — при имплементации теней вылезло несколько существенных проблем, решение которых отняло время и силы (и нервы).

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

Дрожание теней при движении

При движении основной камеры тени начинали «дрожать» или «прыгать». Это происходит из-за того, что Shadow Map привязана к фиксированной сетке пикселей, и при небольших изменениях положения камеры проекция теней может слегка смещаться, из-за чего границы теней кажутся нестабильными.

Основной способ решения — это стабилизация проекции Shadow Map:

  1. Привязка Shadow Map к «мировым» координатам — вместо пересчёта Shadow Map на каждый кадр, её проекция фиксируется относительно сцены, что устраняет смещения при движении камеры.

  2. Смещение камеры в «целые» шаги — контролировать движение камеры, чтобы оно вызывало минимальные сдвиги проекции теней

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

Зубчатые тени

Shadow Map представляет собой текстуру (обычно квадратную), которая хранит информацию о глубине сцены с точки зрения источника света. Когда наклон света большой (например, при низком солнце), тени от объектов становятся длиннее и покрывают большую площадь сцены. В таких случаях разрешение Shadow Map может оказаться недостаточным для точного отображения всех деталей теней. Это приводит к тому, что края теней выглядят рваными и «зубчатыми», как ступеньки на границе света и тени.

Обычно инструментами борьбы с этим артефактом считают:
— увеличение разрешения Shadow Map
— использование каскадов теней (CSM)
— дополнительная фильтрация теней (например PCF).

Никакие из этих подходов нам не хотелось применять из-за соображений производительности. Все они приводят к ощутимой нагрузке на GPU.

В итоге мы поступили иначе. Поскольку карта плоская и на ней почти нет объектов, высота которых сравнима с горизонтальными размерами отображаемой части карты, мы решили «сжимать» камеру теней по вертикали при увеличении угла наклона источника света.

Математически такой коэффициент сжатия вычисляется следующим образом:

const yFactor = Math.abs(vec3.dot(lightDir, Z));

Затем этот коэффициент умножается на вертикальный размер вьюпорта при построении матрицы камеры, и получается нужный эффект.

Как итог, плотность данных в Shadow Map выросла и лесенка стала значительно менее заметной. И это сделано вообще без затрат ресурсов!

Особенности и доработки для WebGL

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

Раньше карта была двумерной и работала с заранее отрисованными данными. В те времена все пользователи были примерно равны в плане возможностей карты. 

После перехода на WebGL возникла довольно существенная разница в работе на устаревших и новых устройствах. В первых используется WebGL 1, а в тех, которые помоложе  — WebGL 2.

Если карта работает на WebGL 1, то:

  • Вместо текстуры глубины можно записать 4-байтовое значение в RGBA-текстуру. Такая текстура выглядит довольно сюрреалистично, если попытаться её отдебажить:

Такая картинка получается за счёт того, что во все четыре канала текстуры происходит запаковка всего диапазона глубин. Дальнейшее чтение из такой текстуры тоже требует распаковки, что, конечно, сказывается на производительности.

  • Вместо shadow sampler используется обычный sampler. Как результат — текстура на WebGL 1 теряет линейную интерполяцию. Впрочем при дальнейшей фильтрации (box-фильтром) это не столь заметно.

Но несмотря на это, тени на WebGL 1 работают в целом не сильно уступая WebGL 2.

Производительность

Мы понимали, что не на всех устройствах эта красота будет работать достаточно быстро. В итоге в среднем FPS просел на 20–25%. У такого существенного влияния на производительность три основные причины:

  • Увеличилось количество drawcall-ов и сопутствующих им операций при отрисовке Shadow Map. Это замедлило как работу на стороне CPU, так и GPU, так как в вершинном шейдере количество работы выросло практически вдвое. Это самый основной вклад, так как в последнее время, количество объектов и их детальность в карте сильно выросли.

  • Усложнились шейдеры объектов, принимающих тени. Следовательно увеличилась нагрузка на ядра GPU.

  • Немного увеличилось потребление памяти за счёт текстуры теней. Но это не слишком значительное ухудшение.

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

Заключение

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

Дальше предстоит поработать над оптимизацией всей карты, так как увеличивается количество объектов и их сложность. Этим мы уже активно занимаемся и надеемся, что пользоваться картой в самом её "сочном" виде сможет как можно большее количество наших пользователей.

Комментарии (1)


  1. nerudo
    15.10.2024 10:20

    Скажите, кто у вас придумал, что если пользователь елозит по карте и случайно оказывается над другой страной, его сразу нужно перекинуть на другой домен, типа .by или .kz? Особенно пикантно это выглядит когда смотришь что-то рядом с границей. Одно неосторожное движение и ты иноагент иностранец.