Мы делаем карты 2ГИС реалистичными, чтобы приложение максимально детально отражало город вокруг: чтобы у зданий были не гладкие стены, а настоящий фасад, в парках росли деревья и стояли лавочки, а скульптуры выглядели точно так же, как и в реальности. Такая реалистичность помогает в навигации: гораздо проще сравнивать то, что ты видишь в реальности, с моделью этого здания, а не с гладкой коробкой. Поддержка освещения объектов, которую мы добавили на 2gis.ru — ещё один шаг к реалистичности.
Расскажу, чего мы добились на сегодня, на какие компромиссы пришлось пойти и чуть-чуть о том, что под капотом движка освещения и теней.
Освещение и тени играют ключевую роль в восприятии объектов в трёхмерном пространстве. Они помогают лучше понимать форму, глубину и расположение объектов относительно друг друга. Без правильного освещения объекты могут выглядеть плоскими и неестественными, а без теней — «парящими» в воздухе, что сильно снижает реализм.
Для трёхмерных карт это особенно важно: тени позволяют пользователям интуитивно оценивать высоту зданий и расстояния между объектами.
Освещение в карте
Трёхмерные объекты на WebGL-картах 2ГИС условно можно разделить на простые и более реалистичные. Для простых «коробок», которые схематически изображают здания, мы сделали совсем простое освещение, состоящее из двух компонент:
Diffuse — буквально сводящийся к вычислению скалярного произведения направления освещения и вектора нормали объекта.
Ambient — рассеянный свет. Про него подробнее напишу ниже.
Для полноценных моделей — с текстурами и параметрами материалов — мы реализовали более сложное трёхкомпонентное освещение.
Диффузное освещение (Diffuse) — свет, который равномерно рассеивается по поверхности объекта. Диффузное освещение помогает передать форму и объём объекта, делая поверхность видимой с разных углов. Мы использовали затенение по Фонгу. Если оставить только его, модели будут иметь такой специфичный «лунный» вид:
Окружающее освещение (Ambient) — мягкий, рассеянный свет, который заполняет всё пространство сцены, даже в тенях. В реальном мире свет отражается от множества объектов, и даже те места, которые напрямую не освещены источником света, всё равно получают часть рассеянного света. Ambient-компонент помогает избежать полностью чёрных теней, делая сцену более естественной. С этим освещением картина приобретает более естественный вид:
Зеркальное освещение (Specular) — свет, который отражается от поверхности объекта под определённым углом — как блик на стекле или металле. Зеркальное освещение создаёт яркие пятна света (блики) на глянцевых или блестящих поверхностях, добавляя реалистичности и подчёркивая материал объекта. Чем сильнее отражение, тем более гладкой и блестящей кажется поверхность. Так эта компонента повлияла на финальную картинку. Обратите внимание на купола:
Наш подход к теням
Для отрисовки теней в realtime используют различные подходы. Самый реалистичный — техники, базирующиеся на трассировке лучей (raytracing, path tracing). Но так как в браузере нет аппаратной поддержки трассировки, мы такие алгоритмы даже не рассматривали.
Ещё интересны подходы, базирующиеся на расчёте луча света в рамках экрана — screen space техники. На данный момент никто не написал их реализацию, предлагающую приемлемый баланс качества и производительности. И эти алгоритмы, хоть и многообещающие, тоже не устроили нас из-за слишком медленной скорости отрисовки.
Более традиционные для растеризуемой графики методы — это Shadow Volume и Shadow Map. С тем объёмом геометрии и разрешениями экрана, которые используются сегодня, Shadow Volume гарантировано будет значительно медленнее Shadow Map. Собственно, это основная причина, почему последние уже давно повсеместно вытеснили Shadow Volume в той же в игровой индустрии, где борьба за производительность не прекращается десятилетиями.
Таким образом, для теней у нас не было особых альтернатив самой популярной на сегодня технике — Shadow Map.
С Shadow Map сначала сцена рендерится с точки зрения источника света, сохраняя информацию о расстоянии до объектов. Затем, при обычном рендеринге, проверяется, находится ли каждая точка сцены в тени, сравнивая её расстояние до источника света с сохранённой картой глубины. Если точка дальше, чем значение на Shadow Map, значит она — в тени.
Принцип работы:
Для удобства восприятия текстура глубины ниже представлена в черно-белом виде — чем ближе объект к источнику освещения, тем пиксель темнее. Там, где он светлее — дальше.
При самом наивном сравнении глубины со значением из текстуры отчётливо видно shadow acne — артефакты, обусловленные дискретной природой текстуры теней.
Чтобы устранить этот артефакт, мы добавляем небольшой отступ при чтении из текстуры глубины. Обычно он называется bias. Главное — не переборщить с его величиной, иначе возникнут другие артефакты в виде теней, отстоящих от объектов на некоторое расстояние. Как результат — картина становится сильно лучше:
Осталось сгладить тени простым box-фильтром, и получается финальный результат:
Конечно, не всё прошло гладко — при имплементации теней вылезло несколько существенных проблем, решение которых отняло время и силы (и нервы).
Тем не менее, большинство проблем, описанных здесь, не являются уникальными и давно проработаны подходы к их решению.
Дрожание теней при движении
При движении основной камеры тени начинали «дрожать» или «прыгать». Это происходит из-за того, что Shadow Map привязана к фиксированной сетке пикселей, и при небольших изменениях положения камеры проекция теней может слегка смещаться, из-за чего границы теней кажутся нестабильными.
Основной способ решения — это стабилизация проекции Shadow Map:
Привязка Shadow Map к «мировым» координатам — вместо пересчёта Shadow Map на каждый кадр, её проекция фиксируется относительно сцены, что устраняет смещения при движении камеры.
Смещение камеры в «целые» шаги — контролировать движение камеры, чтобы оно вызывало минимальные сдвиги проекции теней
Первый вариант для нас неприемлем, так как мы обязаны рисовать тени по всей поверхности земли. Поэтому мы лимитировали перемещение камеры дискретными шагами в один пиксель.
Зубчатые тени
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.
Немного увеличилось потребление памяти за счёт текстуры теней. Но это не слишком значительное ухудшение.
После набора достаточной статистики мы будем принимать решение, на каких системах отключать тени в угоду быстродействию.
Заключение
Несмотря на все трудности, с которыми мы столкнулись при реализации освещения, считаем, что результат того стоил. Карта заиграла новыми красками, появилась глубина, и мы уже слышали ряд положительных отзывов о её обновлённом облике.
Дальше предстоит поработать над оптимизацией всей карты, так как увеличивается количество объектов и их сложность. Этим мы уже активно занимаемся и надеемся, что пользоваться картой в самом её "сочном" виде сможет как можно большее количество наших пользователей.
Комментарии (21)
little-brother
15.10.2024 10:20Имхо, из серии "когда коту делать нечего": зачем пользователю видеть лишний "шум" на карте? Он в приложение карт заходит не ради фотореализма. Посмотрите на Яндекс, они, например, развивают более точную отрисовку дорог (особенно важно для сложных развязок). Сейчас вот глянул - у вас автобусы как будто по обочине гоняют... Пока 2ГИС использую только при выборе заведений в незнакомом городе - там поменьше накруток в отзывах, чем в Яндексе.
Spunya Автор
15.10.2024 10:20Да, за конкурентами, мы, конечно, активно наблюдаем и стараемся не отставать, а где-то и опережаем их (было уже несколько фич, которые Яндекс сделал после нас). Думаю, тени вы и у них скоро увидите.
Реалистичные дороги у нас есть уже во многих городах - в той же Москве. Их, кстати, видно на некоторых скринах в статье. Пока только они включены не у всех пользователей. Возможно, вы как раз не попали в эту выборку. А вот в мобильном приложении реалистичные дороги включаются в режиме навигатора.
drVit
15.10.2024 10:20При некотором масштабе у вас тени оторваны от объектов. Выглядит так, как будто дома висят в воздухе.
Spunya Автор
15.10.2024 10:20Спасибо. Это известный артефакт, как раз обусловленный bias, описанным в статье.
Его удалось снизить вдвое в версии движка, которая скоро пойдет в релиз. Проблема станет менее заметной :)VBDUnit
15.10.2024 10:20Есть мысль рендерить дополнительно карту высот (не глубины, именно расстояние до уровня моря для каждого пикселя) в экранном пространстве, и чем ближе пиксель к земле, тем меньше bias. Выше определённого порога bias постоянный. И зависимость нелинейная. По идее так мы как‑бы «притянем» кончики теней у оснований объектов к этим самым объектам, и удалим тем самым артефакт полностью в 99% случаев.
VBDUnit
15.10.2024 10:20Почему бы эти тени просто не запечь в WebP/JPG? На карте объекты же неподвижны, зачем тут реалтаймовые тени?
Да, у вас есть там момент, что при приближении дома вырастают из 2D в 3D — так можно сделать появление этих запечённых теней через анимацию с прозрачностью. И всё — жрать ресурсы будет по минимуму, а тени можно посчитать качественно трасировкой, даже с GI, безо всяких фейковых AO.
А если надо сделать регулируемое/привязанное к реальности положение Солнца (но нужно ли это в картах?) — можно просто заранее отрендерить карты для разных минут в разные дни, хранить на сервере, и подгружать плавненько. Весить они будут копейки.
Когда/если будете добавлять едущий 3D транспорт (на основе реалтаймовых данных) — можно обойтись запечеными тенями и парой хитростей — и тени будут падать на этот транспорт.
Spunya Автор
15.10.2024 10:20Спасибо за классное предложение. Мы рассматривали и полное запечение теней (так называемые lightmap) и частичное их кэширование в риалтайме, но тут есть ряд моментов:
Во-первых, динамика, которую вы упомянули. У нас уже начали появляться анимированные модели (колеса обозрения, ветряные мельницы, всякие атртракционы). И ещё много чего шевелящегося на подходе.
Динамическое направление света уже работает в 2gis.ru. Можете сравнить направление тени с тем, что вы видите из окна. Меняется сейчас, правда только азимут.Более того, освещение в 2gis можно конфигурировать стилями и это могут делать сторонние пользователи нашего движка (он общедоступный).
Чтобы запечь LightMap, каждому объекту нужна UV-развертка. С этим будет очень много проблем, начиная от доп потребления памяти и заканчивая тем, что многие объекты генерируются в реальном времени.
Предположение о малом весе таких карт для меня выглядит не очень обоснованным :) Тем более, если запекались бы разные направления света.
Вопрос подготовки данных. Это, возможно, самое сложное во всей этой штуке. Про это можно написать отдельную большую статью. И тут трудности были бы в первую очередь даже не техническими, а интеграционными, поскольку были бы вовлечены разные команды, а риалтаймовые тени работают сами по себе.
POPSuL
15.10.2024 10:20Кстати да, на днях сам заметил что тени стали располагаться правильно, в зависимости от времени суток. Но есть нюанс -- высота теней не сильно соответствует действительности, хотя расположение -- очень даже.
Panzerschrek
15.10.2024 10:20В целом хорошее нововведение. Есть некоторые шерохаватости, вроде таких, когда тени от очень высоких домов исчезают, если от них карту далеко отодвинуть, но всё это терпимо.
Spunya Автор
15.10.2024 10:20Всё-то вы заметили :)
Да, потому что сейчас тайлы для отрисовки выбираются одинаково для основной сцене и для теней. Это сделано с целью экономии загрузки данных. Возможно, что-то с этим сделаем, но не в ближайшее время.
Panzerschrek
15.10.2024 10:20Какие в итоге размеры теневой карты используются (в том числе на разных устройствах)? Какой у них формат - 16 бит, 32 бит, float?
Spunya Автор
15.10.2024 10:20На большинстве устройств рисуем в 24 бита (+8 съедает stencil). Это стандартная для WebGL конфигурация.
Без поддержки текстур глубины на WebGL1 получается 32 бита, так как идёт запаковка глубины в RGBA-текстуру c 4-битовыми каналами.
OptimumOption
15.10.2024 10:20как только всандалили деревья - стало невозможно нормально пользоваться. скоро 2GIS даже в браузерке будет просить минимум RTX4090?
VBDUnit
15.10.2024 10:20По идее оно может наблюдать за FPS и снижать деталлизацию в случае, если она ниже 30 к/с. Плюс можно собрать статистику, на каком устройстве с какой скоростью оно работает и подбирать под это настройки графики автоматом.
В 3ds max когда вертишь слишком тяжелую модель он начинает упрощать отображение, чтобы удержать FPS на приемлемом уровне.
Spunya Автор
15.10.2024 10:20Тут не имею права не согласиться. Контента на карте стало очень много.
Во-первых, мы работаем над рядом оптимизаций по количеству вершин на экране включая более комплексное LOD-ирование и разного рода culling.
Во-вторых, как и советует @VBDUnit у нас в обозримом будущем появятся автоматически переключаемые пресеты графики, пока правда мы прицеливаемся на хитрую оценку производительности устройства при инициализации.
venanen
15.10.2024 10:202Гис пошел вслед за яндексом, сделав из легких и быстрых карт какого-то монстра, забывая, что навигаторы очень часто работают очень далеко не на топовом железе - на магнитолах, например. Или когда они работают в смартфоне во время поездки - он разряжается мгновенно. Нельзя ли сделать переключалку, чтобы все красивости убрать, и оставить только быстрый функционал?
Spunya Автор
15.10.2024 10:20Эта статья, вообще говоря, про веб-версию карты.
Но если вы подняли вопрос мобильного приложения, то там коллеги уже довольно давно добавили именно такую переключалку. Настройки -> Карта -> Эффекты на картеDaemonGloom
15.10.2024 10:20О, за информацию о переключении - спасибо. Не знаете, случаем - не планируется ли ещё начать предупреждать заранее о том, какую полосу надо занять на перекрёстке? Сейчас это можно узнать только почти заехав на сам перекрёсток и времени/возможности на перестроение часто уже нет.
venanen
15.10.2024 10:20Поддерживаю.
Еще с детальной разметкой дорог есть нюанс - одна видна только при сильном зуме, что довольно бесполезно - на ходу карта отдалена и зумить ее на ходу на магнитоле - банально опасно. Если бы перекресток вылезал где-то в уголке призумленный или что-то типа того (чтобы заранее видеть разметку перекрестка) - было прямо классно.
Spunya Автор
15.10.2024 10:20Вот тут вам не подскажу. Совсем не моя компетенция. Может быть, кто-то из мобильного отдела и забежит в наш тред, но сомневаюсь. Могу только сказать, что разные работы по улучшению навигатора тоже, конечно, ведутся.
nerudo
Скажите, кто у вас придумал, что если пользователь елозит по карте и случайно оказывается над другой страной, его сразу нужно перекинуть на другой домен, типа .by или .kz? Особенно пикантно это выглядит когда смотришь что-то рядом с границей. Одно неосторожное движение и ты
иноагентиностранец.