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

Часть 1: обработка вершин

Часть 2: растеризация и трассировка лучей

Часть 3: текстурирование и фильтрация текстур

Вспомним пройденное


Ранее мы рассматривали ключевые аспекты перемещения и обработки объектов в сценах, их преобразования из трёхмерного пространства в плоскую сетку пикселей, а также способы наложения текстур на эти объекты. В течение многих лет такие операции составляли основную часть процесса рендеринга, и мы можем увидеть это, вернувшись в 1993 год и запустив Doom компании id Software.


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

Теней не было, потому что они не входили в задачу программистов: PC того времени представлял собой процессор на 66 МГц (то есть на 0,066 ГГц!), жёсткий диск на 40 МБ и 512-килобайтную графическую карту с минимальными 3D-возможностями. Перенесёмся на 23 вперёд: в знаменитой перезагрузке серии мы видим совершенно другую историю.


Для рендеринга этого кадра использовалось множество технологий, он может похвастаться такими этапами, как screen space ambient occlusion, pre-pass depth mapping, фильтры размытия боке, операторы тональной коррекции, и так далее. Расчёт освещения и затенения каждой поверхности выполняется динамически: они постоянно изменяются в зависимости от условий окружающей среды и действий игрока.

Так как для любых операций 3D-рендеринга требуется математика (целая куча вычислений!), нам лучше начать с того, что происходит за кулисами любой современной игры.

Математика освещения


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

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


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

При 3D-рендеринге для этого требуется множество информации, что проще всего представить в виде такой схемы:


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

  • 3 вектора для позиции вершины, источника освещения и камеры, смотрящей на сцену
  • 2 вектора для направлений источника освещения и камеры с точки зрения вершины
  • 1 вектор нормали
  • 1 полувектор (он всегда посередине между векторами направлений освещения и камеры)

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


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

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

Значения цветов поверхности и источника освещения — это стандартные числа RGBA (красный, зелёный, синий и альфа-прозрачность). Они могут быть целочисленными (например, INT8 для каждого цветового канала), но почти всегда являются числами с плавающей запятой (например, FP32). Коэффициент затухания определяет, как уменьшается уровень освещения при отдалении от источника, и вычисляется другим уравнением:


Члены AC, AL и AQ — это различные коэффициенты (constant, linear, quadratic), описывающие способ воздействия расстояния на уровень освещения. Все они задаются программистами при создании движка рендеринга. В каждом графическом API это реализуется по-своему, но коэффициенты вводятся при кодировании типа источника освещения.

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


Точечные источники (Point lights) равномерно испускают свет во всех направлениях, а направленные источники испускают свет только в одном направлении (с точки зрения математики, это просто точечный источник, удалённый на бесконечное расстояние). Прожекторы (Spotlights) являются сложными направленными источниками, так как они испускают свет в форме конуса. То, как свет варьируется в теле конуса, определяет размер внутренней и внешней частей конуса.

И да, для коэффициента прожекторности есть ещё одно уравнение:


Значение коэффициента прожекторности равно или 1 (т.е. источник не является прожектором), или 0 (если вершина находится вне пределов направления конуса), или какому-то вычисленному значению между этими двумя. Углы ? (фи) и ? (тета) задают размеры внутренней/внешней части конуса прожектора.

Два вектора: Ldcs и Ldir (обратные направлению камеры и направлению прожектора) используются для определения того, касается ли конус вершины.

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

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

Для моделирования этого вычисляется ещё одно значение освещения: ambient lighting (освещение окружающей среды).


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

  • CSA — цвет подсветки поверхности
  • CGA — цвет подсветки глобальной 3D-сцены
  • CLA — цвет подсветки всех источников освещения в сцене

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

Итак, у нас есть фоновое освещение и мы учли рассеянное освещение источников освещения от различных поверхностей 3D-мира. Но модель Ламберта работает только для материалов, которые отражают освещение от своей поверхности во всех направлениях; объекты, изготовленные из стекла или металла, создают другой тип отражения, называемый зеркальным (specular); естественно, для него тоже есть уравнение!


Отдельные части этой формулы должны быть вам уже знакомы: у нас есть два значения зеркального цвета (одно для поверхности — CS, другое для света — CLS), а также привычные коэффициенты затухания и прожекторности.

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

Последний учитываемый элемент — самый простой, потому что это всего лишь число. Оно называется испускаемым (emissive) освещением, и применяется к объектам, являющимся непосредственным источником освещения, то есть к пламени, фонарику или Солнцу.

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


Визуально сочетание выглядит вот так:


Рассмотренные нами уравнения применяются графическими API (например, Direct3D и OpenGL) с помощью их стандартных функций, но для каждого типа освещения существуют альтернативные алгоритмы. Например, рассеянное освещение можно реализовать при помощи модели Орена-Найяра, которая лучше подходит для очень шершавых поверхностей, чем модель Ламберта.

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

Какая бы модель ни использовалась, все они значительно улучшаются благодаря повышению частоты их применения к 3D-сцене.

Повершинные или попиксельные вычисления


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


Остальным пикселям нужно передать информацию о цвете вершин таким образом, чтобы цвета правильно смешивались на поверхности. В 1971 году Анри Гуро, в то время бывший аспирантом Университета Юты, предложил способ, который теперь называется затенением по Гуро.

Его метод был вычислительно быстрым и на многие годы стал де-факто стандартом, но он имел и проблемы. Он не мог правильно интерполировать зеркальное освещение, а если объект был составлен из малого количества примитивов, то смешивание между примитивами выглядело ошибочным.

Решение этой проблемы было предложено в 1973 году Буй Тыонг Фонгом, тоже работавшим в Университете Юты. В своей исследовательской статье Фонг продемонстрировал методику интерполирования нормалей вершин на растеризированных поверхностях. Это означало, что модели рассеянного и зеркального отражения будут правильно работать для каждого пикселя, и мы чётко можем это видеть в онлайн-учебнике Дэвида Эка по компьютерной графике и WebGL.

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


Неподвижные изображения не передают всех улучшений, которые были принесены затенением по Фонгу, но вы можете самостоятельно запустить онлайн-демо Эка и посмотреть на анимацию.

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


Тут нам придётся серьёзно разбираться! Значения, обозначенные буквой k — это константы отражений для окружающего, рассеянного и зеркального отражения. Каждое из них — это доля соответствующего типа отражённого света от величины падающего света; значения C мы видели в уравнениях выше (значения цветов материала поверхности для каждого типа освещения).

Вектор R — это вектор «идеального отражения» — направление, в котором двигался бы отражённый свет, если бы поверхность была идеально гладкой; он вычисляется при помощи нормали поверхности и вектора падающего света. Вектор C — это вектор направления камеры; и R, и C нормализованы.

Наконец, в уравнении есть последняя константа: значение ? определяет степень блеска поверхности. Чем материал более гладкий (т.е. чем больше он напоминает стекло или металл), тем выше число.

Это уравнение обычно называют моделью отражения по Фонгу. На момент проведения его исследований такое предложение было радикальным, потому что требовало серьёзных вычислительных ресурсов. Упрощённая версия модели была создана Джимом Блинном, заменившим часть формулы с R и C на H и N (вектор полурасстояния и нормаль поверхности). Значение R необходимо вычислять для каждого источника освещения и для каждого пикселя в кадре, а H достаточно вычислять один раз для каждого источника и для целой сцены.

Модель отражения по Блинну-Фонгу является сегодня стандартной системой освещения и по умолчанию используется в Direct3D, OpenGL, Vulkan и т.п.

Существует множество других математических моделей, особенно теперь, когда GPU могут обрабатывать пиксели в длинных и сложных шейдерах; вместе такие формулы называются bidirectional reflectance/transmission distribution functions (BRDF/BTFD); они являются фундаментом для окрашивания каждого пикселя на мониторе, когда мы играем в современные 3D-игры.

Однако мы пока рассматривали только поверхности, отражающие свет: просвечивающие материалы пропускают свет, при этом лучи света преломляются. А некоторые поверхности. например, вода, в разной степени отражают и пропускают свет.

Поднимаем освещение на новый уровень


Давайте рассмотрим игру 2018 года Assassin's Creed: Odyssey компании Ubisoft, в ней игрок часто ходит под парусом на воде, как в мелких реках, так и в глубоком море.


Окрашенное дерево, металл, верёвки, ткань и вода — всё это отражает и преломляет свет при помощи кучи вычислений

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

Первая из них часто используется для генерации отражающих свойств воды — это отражения в экранном пространстве (screen space reflections) (SSR). Эта техника рендерит сцену, но цвета пикселей зависят от глубины каждого пикселя, т.е. от его расстояния до камеры. Глубина хранится в так называемом буфере глубин. Затем кадр рендерится заново со всем обычным освещением и текстурированием, но сцена сохраняется как render texture, а не как готовый буфер, который передаётся на монитор.

После этого выполняется ray marching. Для этого из камеры испускаются лучи и вдоль хода луча задаются расстояния. Код проверяет глубину луча относительно пикселей в буфере глубин. Если они имеют одинаковое значение, код проверяет нормаль пикселя, чтобы убедиться, направлена ли она в камеру, и если это так, то движок ищет соответствующий пиксель из render texture. Затем дальнейший набор инструкций инвертирует позицию пикселя, чтобы он корректно отражался в сцене.


Порядок SSR, используемый в движке Frostbite компании EA.

Кроме того, свет рассеивается в процессе перемещения внутри материалов, и для материалов наподобие воды или кожи используется ещё один трюк под названием подповерхностное рассеяние (sub-surface scattering) (SSS). Мы не будем объяснять её подробно, но вы можете прочитать, как она используется для создания таких потрясающих результатов, в презентации Nvidia 2014 года.


Демо FaceWorks 2013 года, созданное Nvidia (ссылка)

Вернёмся к воде Assassin's Creed: реализация SSS здесь малозаметна, и из-за соображений скорости используется не так активно. В предыдущих играх серии AC компания Ubisoft использовала фальшивое SSS, но в последней игре его использование более сложно, но всё равно не так масштабно, как мы видели в демо Nvidia.

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

Результаты впечатляют:


Assassin's Creed: Odyssey — рендеринг воды во всей красе.

Мы рассмотрели воду, но как насчёт движения света в воздухе? Частицы пыли, влага и другие элементы тоже приводят к рассеянию света. В результате этого лучи света получают объём, а не остаются просто набором прямых лучей.

Тему объёмного освещения можно растянуть ещё на десяток статей, поэтому мы расскажем о том, как с ним справляется игра Rise of the Tomb Raider. В показанном ниже видео есть только один основной источник освещения — солнце, светящее сквозь проём в здании.


Для создания объёма света игровой движок берёт пирамиду видимости камеры (см. ниже) и экспоненциально разбивает её по глубине на 64 части. Затем каждый срез растеризуется в сетки размером 160 x 94 элемента, и все эти данные сохраняются в трёхмерной render texture формата FP32. Так как текстуры обычно двухмерные, «пиксели» объёма пирамиды видимости называются вокселями.


Для блока размером 4 x 4 x 4 вокселя вычислительные шейдеры определяют, какие активные источники освещения влияют на этот объём, а затем записывают эту информацию в ещё одну трёхмерную render texture. Затем для оценки общей «плотности» света внутри блока вокселей используется сложная формула, называемая функцией рассеяния Хеньи-Гринштейна.

Затем движок выполняет ещё несколько шейдеров для уточнения данных, после чего выполняется ray marching по срезам пирамиды с накоплением значений плотности освещения. Eidos-Montreal утверждает, что на Xbox One все эти операции выполняются примерно за 0,8 миллисекунды!

Хотя эта методика используется не во всех играх, игроки ожидают увидеть объёмное освещение практически во всех выпускаемых сегодня популярных 3D-играх, особенно в шутерах от первого лица и в экшн-адвенчурах.


Объёмное освещение, использованное в сиквеле Rise of the Tomb Raider 2018 года

Изначально эта техника освещения называлась «божественными лучами», или, как они называются по-научному, «сумеречными лучами». Одной из первых игр, в которых она использовалась, был первый Crysis компании Crytek, выпущенный в 2007 году.

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

Этот буфер маски несколько раз сэмплировался, а шейдер брал сэмплы и смешивал их размытием вместе. Результат этой операции смешивался с готовой сценой:


Прогресс графических карт за последние 12 лет был колоссальным. Самыми мощными GPU на момент выпуска Crysis были Nvidia GeForce 8800 Ultra. Самый быстрый современный GPU — GeForce RTX 2080 Ti имеет в 30 с лишним раз большую вычислительную мощь, в 14 раз больше памяти и в 6 раз большую пропускную способность.

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


«Божественные лучи» в The Division 2 компании Ubisoft

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

Суть тени


Давайте начнём новый раздел статьи с игры Shadow of the Tomb Raider. На показанном ниже изображении отключены все параметры графики, относящиеся к теням; справа они включены. Разница огромная, правда?


Так как в реальном мире тени образуются естественным образом, игры, в которых они реализованы неверно, никогда не будут выглядеть правильно. Наш мозг привык использовать тени как визуальную опору для создания ощущения относительной глубины, расположения и движения. Но сделать это в 3D-игре на удивление сложно, или, по крайней мере, сложно это сделать правильно.

Давайте начнём с уточки. Вот она вперевалочку идёт по полю, а лучи солнца достигают её и правильно блокируются.


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

Первые 3D-игры, например, первая Tomb Raider 1996 года, использовала этот метод, потому что железо того времени, например, Sega Saturn и Sony PlayStation, не могло обеспечить ничего лучшего. Этот способ отрисовывал простой набор примитивов чуть выше поверхности, по которой движется модель, а затем затеняла их; также использовалась отрисовка внизу простой текстуры.


Ещё один из первых методов заключался в проецировании теней. При этом испускающий тень примитив проецировался на плоскость, содержащую пол. Часть необходимых для этого математических вычислений была создана Джимом Блинном в конце 80-х. По современным стандартам это простой процесс, и лучше всего он работает для простых статичных объектов.


Но благодаря оптимизации проецирование теней обеспечило создание первых достойных примеров динамических теней, например, в игре 1999 года Kingpin: Life of Crime компании Interplay. Как мы видим на изображении ниже, только анимированные персонажи (даже крысы!) имеют тени, но это лучше, чем простые пятна.


Самые серьёзные проблемы такого подхода: (a) совершенная непрозрачность тени и (b) метод проецирования выполняет испускание тени на одну плоскую поверхность (например, на землю).

Эти проблемы можно решить, применив долю прозрачности при раскрашивании проецируемого примитива и выполнив для каждого персонажа несколько проекций, но аппаратные возможности PC конца 90-х не могли справиться с дополнительным рендерингом.

Современные технологии создания теней


Более точный способ реализации теней был предложен гораздо ранее, аж в 1977 году. Во время работы в Университете Остина (Техас) Франклин Кроу написал исследовательскую статью, в которой предложил несколько техник с использованием объёмов теней.

В общем виде их можно описать так: процесс определяет, какие примитивы направлены в сторону источника освещения; их рёбра растягиваются в плоскость. Пока это очень похоже на проецирование теней, но важное отличие заключается в том, что созданный объём теней используется затем для проверки того, находится ли пиксель внутри/снаружи объёма. Благодаря этой информации тени можно испускать на все поверхности, а не только на землю.

Эту методику в 1991 году усовершенствовал Тим Хейдманн, работавший на Silicon Graphics. Её дальнейшим развитием занимался в 1999 году Марк Килгард, а способ, который мы будем рассматривать, создал в 2000 году Джон Кармак из id Software (хотя метод Кармака независимо от него был открыт двумя годами ранее Билодо и Сонги из Creative Labs; привело к тому, что Кармак был вынужден изменить свой код, чтобы избежать юридических проблем).

Такой подход требует многократного рендеринга кадра (называемого многопроходным (multipass) рендерингом — очень затратного в начале 90-х процесса, который сегодня используется везде) и концепции, называемой стенсил-буфером.

В отличие буферов кадра и глубин, он создаётся не самой 3D-сценой — этот буфер является массивом значений, равным по всем измерениям (т.е. разрешению по x и y ) в виде растра. Хранимые в нём значения используются, чтобы сообщать движку рендеринга, что делать с каждым пикселем в буфере кадра.

Простейшим примером использования этого буфера является применение в качестве маски:


Метод с объёмом теней выполняется примерно так:

  • Рендерим сцену в буфер кадра, но используем только окружающее освещение (также включаем в него все значения испускания, если пиксель содержит источник освещения)
  • Рендерим сцену заново, но только для поверхностей, направленных на камеру (это называется усечением обратных граней (back-face culling)). Для каждого источника освещения вычисляем объёмы теней (например, методом проецирования) и сравниваем глубину пикселя каждого кадра с размерами объёма. Для пикселей внутри объёма теней (т.е. когда проверка глубины завершилась «неудачей») увеличиваем значение в стенсил-буфере в соответствии с этим пикселем.
  • Повторяем предыдующую операцию, но со включенным усечением передних граней (front-face culling) и уменьшением значений стенсил-буфера, если пиксели находятся в объёме.
  • Рендерим всю сцену заново, но на этот раз со всем включенным освещением, а затем смешиваем готовый кадр и стенсил-буферы.

Такие стенсил-буферы и объёмы теней (обычно называемые стенсил-тенями) применялись в игре id Software 2004 года Doom 3:


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

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

К тому же существует ограничение на количество объёмов теней, которое можно проверить при помощи стенсил-буфера, потому что все графические API выделяют на него довольно малое количество бит (обычно всего 8). Впрочем, из-за вычислительной затратности стенсил-теней эта проблема обычно не возникает.

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


Самая тёмная область теней называется полной тенью (umbra); полутень (penumbra) — это всегда более светлая тень, а граница между ними двумя часто бывает размытой (ведь обычно существует множество источников освещения). Это плохо получается моделировать при помощи стенсил-буферов и объёмов, так как создаваемые тени хранятся не в том виде, чтобы их можно было обрабатывать. На помощь приходит наложение теней (shadow mapping)!

Базовая процедура была разработала в 1978 году Лэнсом Уильямсом. Она довольно проста:

  • Для каждого источника освещения рендерим сцену с точки зрения этого источника, создавая особую текстуру глубин (то есть без цвета, освещения, текстурирования и т.п.). Разрешение этого буфера не обязано быть равно размеру готового кадра, но чем выше, тем лучше.
  • Затем рендерим сцену с точки зрения камеры, но после растеризации кадра позицию каждого пикселя (выраженную в x,y и z) преобразуем, используя в качестве точки начала координат источник освещения.
  • Глубина преобразованного пикселя сравнивается с соответствующим пикселем в сохранённой текстуре глубин: если она меньше, то пиксель будет тенью и не будет обрабатываться полной процедурой освещения.

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

К сожалению, описанная выше базовая методика генерирует всевозможные визуальные артефакты (например, перспективный алиасинг, «shadow acne», «peter panning»), основная часть которых связана с разрешением и битовым размером текстуры глубин. Все GPU и графические API имеют ограничения ан подобные текстуры, поэтому для разрешения этих проблем создан целый набор дополнительных техник.

Одно из преимуществ использования текстур для информации глубин заключается в том, что GPU могут сэмплировать и фильтровать их очень быстро и множеством разных способов. В 2005 году Nvidia продемонстрировала метод сэмплирования текстуры, при котором можно решить часть визуальных проблем, вызванных стандартным наложением теней. Кроме того, он обеспечивал определённую степень плавности краёв теней; эта техника называется percentage closer filtering.


Примерно в то же самое время Futuremark продемонстрировала использование каскадных карт теней (cascaded shadow maps) (CSM) в 3DMark06. Это техника, в которой для каждого источника освещения создаётся несколько текстур глубин с разным разрешением. Текстуры высокого разрешения используются рядом с источником, а более низкого — на удалении от источника. В результате получаются более плавные переходы теней в сцене без искажений.

Эта техника была улучшена Доннелли и Лоритзеном в 2006 году в их процедуре variance shadow mapping (VSM), а также Intel в 2010 в её sample distribution algorithm (SDSM).


Применение SDSM в Shadow of the Tomb Raider

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

К счастью, существует удобная техника, работающая с любым объектом. Она создаёт впечатление снижения яркости достигающего объекта освещения (из-за того, что он сам или другие объекты немного блокируют свет). Эта функция называется ambient occlusion и у неё существует множество версий. Некоторые из них специально разработаны производителями «железа», например, AMD создала HDAO (high definition ambient occlusion), а у Nvidia есть HBAO+ (horizon based ambient occlusion).

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

Сэмплирование буфера глубин и последующее вычисление окончательного цвета пикселя играет важную роль в обеспечении качества ambient occlusion; как и в случае наложения теней, все версии ambient occlusion для своей правильной работы требуют от программиста тщательной настройки и подгонки кода в зависимости от ситуации.


Shadow of the Tomb Raider без AO (слева) и с HBAO+ (справа)

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

Если выбрать любую из рассмотренных в этой статье последних игр, то использованный в них список техник рендеринга при обработке освещения и теней будет длиной с саму статью. И хотя не каждая новая 3D-игра может похвастаться всеми этими технологиями, универсальные игровые движки наподобие Unreal позволяют опционально включать их, а тулкиты (например, компании Nvidia) предоставляют готовый для вставки в игру код. Это доказывает, что они не являются высокоспециализированными сверхсовременными методами — бывшие когда-то достоянием самых лучших программистов, теперь они доступны любому.

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

Однако эту технологию поддерживают консоли следующего поколения Microsoft и Sony, а это значит, что в течение ближайших нескольких лет её использование станет ещё одним стандартным инструментом для разработчиков всего мира, стремящихся улучшить визуальное качество игр. Просто взгляните на то, чего удалось добиться Remedy в её последней игре Control:


Мы проделали долгий путь от фальшивых теней в текстурах и простейшего окружающего освещения!

Это ещё не всё


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

Например, мы пропустили такие темы, как освещение с сохранением энергии, lens flare, bloom, высокодинамический рендеринг, radiance transfer, тональная коррекция, туман, хроматическая аберрация, photon mapping, каустика, radiosity — этот список можно продолжать. Для их краткого изучения потребовалось бы ещё 3-4 статьи.