Использование освещения и теней практически в любом игровом проекте добавляет реализма картинке и подчеркивает взаимное расположение объектов в сцене. Без них игры были бы скучными, безжизненными, было бы сложнее ориентироваться в игровом мире.
Сегодня мы расскажем, как в геймдеве делаются тени — в реальном времени и статичные. В своих проектах War Robots и Dino Squad мы используем сразу несколько техник — им и уделим особое внимание.
Предпосылки к написанию кастомных теней
В War Robots объекты в сцене — сами роботы и наполнение локаций — имеют более-менее сопоставимые размеры. Освещение в проекте статическое и не меняется на протяжении боя, а тени включены только для главного источника света. В связи с этим было решено использовать гибридное решение из теневой маски света (light shadow mask) от статических мешей и детализированной теневой карты (shadow map) на всех объектах, включая динамические.
В то же время одна из особенностей проекта Dino Squad — разница в масштабах диносов. При игре за маленького велоцираптора камера находится близко к террейну, а в случае с большим Ти-рексом — далеко от него. Отсюда родилась необходимость использовать каскадные тени, чтобы получить хорошую детализацию как вблизи, так и на расстоянии от камеры.
В Unity используются каскадные тени, которые работают по отложенной (deferred) схеме. Прежде всего формируются теневые карты — по одной на каждый каскад. Затем выполняется проход отрисовки глубины без шейдинга. После этого вычислительный шейдер восстанавливает по значениям глубины позицию и делает выборку из набора теневых карт, формируя полноэкранную черно-белую теневую маску. В проходе освещения эта маска используется для понимания того, где расположен фрагмент — в тени или на свету.
Таким образом, в тенях от Unity есть лишний проход рисования screen-space глубины, а также два переключения контекста: растеризации в вычислительный и обратно. Это хороший подход для отложенных рендереров (где, как правило, уже есть значение глубины), но не самое быстрое решение в прямых (forward) рендерерах для мобильных платформ. Нам же нужна была forward-реализация, когда тени получаются не в маске, а сразу во фрагментном шейдере.
Второе ограничение отложенных теней — теневая маска не позволяет рисовать тени за полупрозрачными объектами. У нас же при наложении различных эффектов динос становится прозрачным, и мы должны видеть тени сквозь него.
Еще одной особенностью при отрисовке теневых карт в Dino Squad и War Robots является то, что отсечение треугольников (triangle culling) меняется на обратное, и рисуются только задние стороны объектов (inverse culling). Эта особенность позволяет на раннем этапе отсекать 70% фрагментов, снижая объем экспорта данных в видеопамять при формировании теневых карт (shadow maps). Также отпадает необходимость подбора отступов и интервалов (depth offset/depth bias) для борьбы с такими нежелательными явлениями, как эффект Питера Пэна (peter-panning) и теневая рябь (shadow acne) на этапе отрисовки геометрии.
Взвесив все преимущества и недостатки встроенных теней Unity, мы решили сделать свою реализацию.
Теперь подробнее остановимся на используемых в проектах техниках и начнем с самой простой и доступной техники построения теней, а именно — с проекционных теней.
Проекционные тени
На картинке ниже можно заметить под роботом затененную область. Этот эффект достигается за счет проективного наложения текстуры, имитирующей затенение на поверхность под роботом. Текстура содержит концентрическую «плашку» с линейным увеличением прозрачности от центра к периферии. До перехода на Scriptable Rendering Pipeline (SRP) в Unity такие эффекты можно было сделать с помощью готового компонента Projector. Компонент определял объекты в сцене, которые затрагивались проектором, и накладывал на них проекционную текстуру. Слабым местом такого подхода являлась производительность: объекты, затронутые проектором, приходилось рисовать дважды. Отсутствие поддержки проекторов в SRP потребовало от нас разработки собственного решения.
Представим, что у нас есть некоторая текстура с рисунком (круглая тень), который необходимо наложить на произвольную рельефную трехмерную поверхность (террейн). В реальном мире для решения этой задачи мы могли бы использовать проектор (как в кинотеатре), отобразив с его помощью кадр с текстурой и направив изображение на неровный экран. В основе метода наложения проекционных текстур лежит именно этот принцип. Для более детальной информации можно обратиться к презентации NVIDIA.
В случае проекционных теней текстура с тенью и поверхность террейна представляют собой две несвязанных между собой системы координат. В каждой из этих систем элементы текстуры (тексели) и элементы поверхности (пиксели) имеют свои позиции в разных системах координат (texture space и screen space). Операция проецирования определяет взаимосвязь между текселем и пикселем и тем самым позволяет перенести цвет текселя из текстуры с тенью на соответствующий пиксель поверхности террейна.
В проекте War Robots наложение проекционных теней производится сразу при отрисовке объектов. Этот подход не требует предварительного отбора геометрии объектов, которые затрагиваются проектором, а также отдельного прохода для повторной отрисовки этой геометрии. В итоге такая реализация проекционных теней показывает хорошую производительность и довольно проста в реализации.
Теневые карты
Когда еще в прошлом веке видеопроцессоры стали программируемыми, и появилась возможность чтения из отрисованных текстур, это открыло возможности для добавления новых динамических эффектов. Например, можно отрисовать модель черным цветом в белую текстуру, а затем наложить отрисованную текстуру таким образом, чтобы получилась проекционная тень. При этом сама тень за счет обновления на каждом кадре будет изменяться вместе с моделью на террейне. Однако такой подход сталкивается с проблемой просвета тени в другую сторону.
Чтобы этого избежать, в текстуру можно записывать значение глубины — расстояние от источника света до поверхности. Эта записанная глубина впоследствии сравнивается с расчетной в каждом фрагменте, и если она оказывается меньше — значит, соответствующая ей точка находится в тени.
Такая текстура называется картой глубины (depth map). При отрисовке сцены в карту глубины камера помещается в позицию источника света и ориентируется согласно его направлению. Для каждого текселя карты производится операция сравнения и записывается наименьшее значение глубины. Таким образом, темные пиксели в карте глубины условно находятся ближе к камере, а светлые — дальше. Белый цвет означает бесконечность, где ничего нет.
Работа рендера происходит в следующем порядке. Первым проходом формируется карта глубины как отдельная текстура. Во втором проходе при отрисовке объектов на экран проверяется расстояние каждой точки объекта до источника света. Если рассчитанное и выбранное из карты глубины расстояния совпадают — значит, пиксель не затенен. Если расстояние в карте глубины окажется меньше — значит, на пути до источника света есть препятствие, и пиксель находится в тени.
Так выглядит отдельно тень:
А так — вместе со светом:
Запекание теней у статических объектов
Для формирования теневых карт требуется отдельный проход с отрисовкой почти всей сцены, что может привести к просадке производительности при выполнении вершинного шейдера (vertex shader) из-за большого количества геометрии. Именно с этим мы столкнулись на проекте War Robots. На скриншоте ниже представлена статистика кадра при рендеринге одной из сцен:
В этом кадре происходит обработка 437 тысяч треугольников. Здесь использованы два каскада теней, которые рисуются очень далеко с плавным переходом между каскадами на ближней и дальней дистанциях. Если для эксперимента полностью убрать тени, то в кадре будет обработана всего 221 тысяча треугольников — то есть, число в два раза меньшее:
Изначально тени в War Robots обновлялись в реальном времени и включали два каскада теневых карт. Такая схема позволяла отображать тени на всей геометрии сцены, попавшей в кадр, и в то же время обеспечивала отчетливую границу между освещенной и затененной областями вблизи камеры. Этот подход требует обработки очень большого количества геометрии на каждом кадре, ведь сцены в War Robots Remastered довольно увесистые в плане геометрии. Учитывая, что на игровом уровне War Robots имеется только один основной глобальный источник света, который фиксирован и не меняется на протяжении боя (например, в проекте нет динамической смены времени суток), считать на каждом кадре два довольно дорогостоящих каскада от статического источника света не очень разумно.
В War Robots используются карты освещенности (light maps) — текстуры, содержащие рассчитанное заранее непрямое освещение (baked indirect lighting) на статических объектах. По аналогии с запеканием непрямого освещения можно запечь и теневую карту для неподвижной геометрии от того же источника света.
Пример карты освещенности, содержащей непрямое освещение (light map):
Также отдельной текстурой лежат заранее просчитанные тени (shadow mask):
Таким образом, в проекте War Robots для статических объектов в сцене на дальних дистанциях от камеры используются запеченные тени (shadow masks), а для теней от динамических и статических объектов вблизи камеры — по-прежнему теневые карты. В результате комбинации заранее рассчитанных теней (shadow masks) и обновляемых в реальном времени теней (shadow maps) получается достаточно хороший компромиссный вариант с визуальной точки зрения и просто отличный с точки зрения производительности.
Каскадные тени
Так выглядят запеченные тени:
Поскольку все сцены запекаются в статическую теневую маску размером 1k, разрешение получается очень низким. Зато даже на самых дальних объектах все равно есть какая-то тень.
Динамические тени выглядят более четко вблизи, но их не хватает на дальние объекты:
Увеличивая расстояние (зеленая область) мы потеряем в разрешении: сразу проявит себя алиасинг и другие проблемы.
Для борьбы с этим существует техника каскадных теней. Выглядит она следующим образом:
В зеленую область попадают динамические тени максимального разрешения. В красной области — динамические тени более низкого разрешения. Синяя область — заранее рассчитанная и запеченная в текстуру теневая маска от статических объектов.
Теневая карта каскадных теней реализована атласом, который выглядит следующим образом:
Левая часть соответствует зеленой области высокого разрешения. Правая — красной области более низкого разрешения. При вычислении тени для фрагмента в зависимости от расстояния до камеры выбирается значение из соответствующей части атласа. Затем оно смешивается со статической маской таким образом, чтобы переход был плавным и незаметным:
В конце аддитивно накладывается рассеянное освещение и получается уже финальная картинка:
В этой статье мы рассказали о наиболее распространенных техниках затенения, которые используются в 3D-играх и в частности — в наших собственных проектах. Разумеется, это далеко не все возможные способы, но самые базовые для понимания того, как производится рендеринг теней и освещения и как различные техники могут взаимодействовать между собой.
Авторы статьи: Павел Кирсанов, Роман Вишняков, Станислав Жучков