GameDev
September 24

Реализация динамического освещения. ч. 1

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

Лайтмапу реализовать сложнее - необходимо писать компилятор, который накладывает её качественно и без артефактов. По факту, на разработку лайтмаппера у меня ушло около года, но результат в итоге понравился всем. И вот дело дошло до реализации полностью динамического освещения. Лайтмапа в данном случае выступает как референс, чтобы было с чем сравнивать результаты в процессе разработки.

Также я считаю, что динамическое освещение не должно отличаться от лайтмапы. За возможность динамического перемещения света и тени мы и так расплачиваемся снижением производительности и было бы странно при этом выдавать картинку, отличную от статичного освещения. Простой пример, где статичное освещение отличается от динамического- Сталкер. Там, как вы знаете, включение полной динамики даёт совершенно иной визуальный стиль, который нравится далеко не всем.

На мой взгляд это тупиковое направление. Даже если задумка дизайнера заключается в том, чтобы на полностью динамическом освещении всё выглядело иначе, должна оставаться возможность сделать динамический свет неотличимым от лайтмапы (в плане визуальных артефактов, алиасинга, дрожания, мерцания, и т.д.).

С такими вводными данными я и приступил к внедрению динамического освещения. На данный момент реализовано три классических типа источника света:

  1. всенаправленный свет
  2. направленный свет (прожектор)
  3. параллельный свет (солнце)

Стоит упомянуть важный момент, о котором предпочитают умалчивать - фактор затухания света. Для динамических источников он всегда линейный (кроме солнца). Потому что квадратичное затухание даёт большой радиус практически для любого источника, и эта сфера в 99% превышает размером игровой уровень. Встаёт вопрос, а как же отсекать такой источник света в целях оптимизации? Очевидно, что никак, в лоб эта задача не решается. Поэтому все используют линейное затухание, а в одном блоге я и вовсе прочёл утверждение, будто бы линейное затухание света - более корректно физически. Это, разумеется, не так, но данная тема выходит за рамки моей сегодняшней заметки. Пока что мы не будем отрываться от коллектива и тоже ограничимся линейным затуханием.

Собственно такие динамические источники света присутствовали в XashNT с 2023 года, а может и раньше. С одним недостатком - у них не было теней. Здесь стоит отметить забавное когнитивное искажение игроков, да и левел-дизайнеров: если динамический свет не имеет теней - он не воспринимается как динамический.

Поскольку свет и тени в компьютерной графике это две слабосвязанные вещи (свет сделать легко, а тени намного сложнее), я на тот момент просто оставил всё как есть. Теневые карты — это довольно капризная техника, которая по-хорошему требует реализации в ядре движка соответствующего менеджера, определения видимости, многопроходного рендерера, разбиения тени на каскады для солнца и т.п. На тот момент (в 2023), моего внимания требовали совершенно другие, более приоритетные задачи, поэтому реализация теней была отложена.

Впрочем, внимательные читатели моего канала помнят мои эксперименты с тенями через рейтрейсинг на GPU. У рейтрейсинга, как вы знаете, есть свои недостатки, на которые наложились ещё и особенности моей реализации, что не дало мне объявить этот вопрос решённым и закрытым. В дальнейшем я планирую вернуться к этой технике снова, чтобы дизайнеры могли её использовать, но в этой статье речь пойдет именно о теневых картах (shadow maps).

Поскольку в статье об освещении непременно должны быть иллюстрации, для начала покажу вам эксперименты с рейтрейсингом (он используется только для окклюзии):

Запечённая лайтмапа
Тени, полученные с помощью рейтрейсинга (похожи на стенсильные)
Стартовый уровень из Quake, освещённый лайтмапой
То же самое, но освещение произведено при помощи рейтрейсинга (обратите внимание на FPS)

Сравнительные тесты проводились на видеокарте GTX650. Конечно, по современным меркам это весьма слабая карточка, однако она тянет даже Metro Exodus на минимальных настройках.

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

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

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

Но перед демонстрацией, давайте сперва освежим в памяти все недостатки теневых карт и методы борьбы с ними:

  1. Разрешение карты почти никогда не совпадает с разрешением экрана

    На самом деле это общая проблема всех техник затенения. И метод борьбы тут только один - сглаживание. Какой это будет тип фильтрации, в сущности, неважно, но лайтмапы без сглаживания выглядят так же отвратительно, как и теневые карты. Билинейной фильтрации текстур абсолютно недостаточно.
  2. Ошибки точности

    Для лайтмап и рейтрейсинга этой проблемы нет, поскольку трассировка с целью получения окклюзии для нашего конкретного случая не имеет ограничений по разрешению. Теневая же карта имеет определённое разрешение и если брать информацию о затенённости пикселя с несоответствующего места в теневой карте - получим ошибку затенения или освещения.
    Усугубляет ситуацию и тот факт, что производится двойное преобразование координат - сперва из перспективной проекции камеры игрока, а затем в локальное пространство теневой карты. Эти пространства могут быть ориентированы относительно друг друга как угодно, а значит ошибки попросту неизбежны. В этом легко убедиться отрисовав теневую карту без каких-либо смещений и без полигон-оффсета. Тени полученные таким образом будут равномерно усыпаны артефактами, усугублёнными аппаратной билинейной фильтрацией PCF.
    Это основное больное место теневых карт, поэтому в дальнейшем мы остановимся на этой проблеме подробнее.
  3. Тени от солнца

    Проблема частично вытекает из первого пункта: нельзя увеличивать разрешение теневой карты солнца до бесконечности, а вот размеры игрового уровня могут расти.
    Проблема решается через технику теневых каскадов, которые являются переосмыслением идеи mip-маппинга или LOD-ов, если хотите, но со своей спецификой.
  4. Общий менеджмент теневых карт

    Если тень от каждого источника будет сохранена в отдельную текстуру глубины, мы не сможем передать все текстуры в шейдер для произвольного кол-ва источников. К тому же эти текстуры имеют ещё и разные типы. Тень от проекционного источника умещается в обычной двухмерной теневой карте, тень от всенаправленного требует кубическую теневую карту и, наконец, тень от каскадов желательно разместить в многослойной двухмерной текстуре. Всё это очень неудобно и усложняет шейдер.
    Выходом из положения становится единый двухмерный атлас, в котором хранятся тени ото всех источников, но нам нужно продумать корректное выделение пространства в этом атласе для разных типов, разложить кубическую карту для всенаправленных источников в двухмерное пространство, и т.д.
    Тем не менее, несмотря на кажущуюся сложность, задача вполне решаемая.
  5. Оптимизация скорости работы

    Поскольку техника теневых карт предполагает создание тени через растеризацию буфера глубины, нам требуется отрисовать её с позиции источника света. Самый простой случай - направленный свет, рендеринг потребуется всего один раз. Для всенаправленного источника уже шесть раз, для каскадов солнца - зависит от кол-ва каскадов.
    Как минимум для каждого такого прохода нам предстоит рассчитать видимость геометрии с точки зрения источника света, собрать список всей видимой геометрии и, наконец, отрендерить её в теневую карту. Чем больше источников света - тем больше таких проходов.
    Встаёт вопрос о том, как делать некоторые проходы не каждый кадр, эффективно определять видимость источников света с точки зрения игрока.
    Тут нет какого-то единого универсального подхода, уже хотя бы потому, что методы оптимизации опираются на средства получения информации о текущей видимости, а они в каждом игровом движке устроены по-своему.
  6. Алиасинг

    Для оптимизации использования свободного пространства теневой карты или даже для сокращения числа проходов различные техники могут применять сложные двойные или даже тройные преобразования матрицы проекции источника света, чтобы заполнить теневую карту более эффективно. Это, как правило, влечёт за собой и мерцание теней при повороте камеры игрока.
    Ну а сами игроки хорошо знают, что подобное мерцание (алиасинг) является характерным артефактом именно техники теневых карт и с лайтмапой его никогда не перепутают. Поэтому одним из важнейших требований к имплементации теней я поставил нулевой алиасинг. Хорошо сглаженные тени без мерцаний действительно очень похожи на лайтмапу.

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

Продолжение следует ...