Реализация непрямого освещения ч.1
Поговорим о непрямом освещении или как его ещё часто называют - глобальном. Вы скорее всего могли слышать, а если нет, то теперь знаете, что освещение подобного типа является самым сложным в реализации, из-за чего первые попытки внедрить это в продакшн начались лишь в десятых годах нашего века.
То есть, прежде чем рассказывать об имплементации, надо как следует погрузиться в проблематику и рассмотреть задачу со всех сторон. Вот об этом и поговорим.
Каких-то особых преград для реализации динамического прямого света не существовало никогда. В самом деле, в сети можно ещё найти древние примеры от SGI, приуроченные к выходу OpenGL 1.1, кажется за 97-й год, с демонстрацией динамического освещения. С единственной оговоркой - у него не было тени, но, как вы знаете, эта проблема была решена в кратчайшие сроки. Сперва при помощи стенсильных объемов, а затем и при помощи теневых карт, которые де-факто стали стандартом при затенении. То есть всё упиралось лишь в недостаточную мощность видеокарт, которые тогда каждый год прибавляли в производительности чуть ли не вдвое.
С глобальным освещением всё оказалось куда сложнее. Если мы хотим получить полностью физически корректное освещение, то мы должны использовать уравнение рендеринга.
Отдельно отмечу, что к честному расчёту освещения через это уравнение до сих пор не готов ни один домашний компьютер. По крайней мере в реальном времени и с достаточной частотой кадров в условиях реальной игровой обстановки, а не просто тестовой комнатки, типа корнеллской коробки.
Да, RTX видеокарты в какой-то степени изменили эту ситуацию, но как я наглядно показывал ещё в цикле статей по реализации прямого освещения, нам требуется выпустить до полумиллиарда лучей за один кадр, чтобы получить что-то более-менее физически корректное. Ни одна из существующих видеокарт на такой подвиг пока что не способна. Поэтому в ход идут различные ухищрения, такие как расчёт освещения в более низком разрешении, использование репроекции (данных с предыдущего кадра), заполнение выпадающих фрагментов адаптивными системами шумоподавления, интеллектуальными апскейлерами на основе специально обученных нейросетей, в общем от изначально поставленной задачи индустрия свернула в какую-то совершенно побочную область, благодаря чему красивая картинка в современных играх непременно получает в нагрузку ряд довольно специфических артефактов, буквально на грани различимости, которые тем не менее оставляют смутное ощущение некоей неправильности. Впрочем, я полагаю, игроки и сами могут об этом прекрасно рассказать, собрав все характерные проявления компенсирующих технологий в безразмерный термин "мыльное кинцо". Я в свою очередь этим путём идти бы не хотел, потому что, повторюсь, к самому освещению он имеет крайне слабое отношение и это явно не та область, где малыми силами можно что-либо сделать. Тут скорее надо просто подождать, пока производители видеокарт не придумают очередной нейросетевой апскейлер, который подарит нам новые совершенно невиданные доселе артефакты. Ну а мы вернёмся к проблематике непрямого освещения.
Итак, как мы выяснили в предыдущем абзаце, любые попытки посчитать непрямое освещение физически корректно приводят к неприятным результатам и уходу от изначальной задачи. Очевидно, здесь тоже нужны некие допущения. Родоначальником появления непрямого освещения в играх принято считать Quake II. Да, освещение там было предрасчитанным и запечённым в лайтмапу, но до него не было и такого. Даже предрасчёт непрямого освещения в 97-м году был весьма затратным делом, поэтому и там были приняты некоторые допущения. Так, например, разрешение прямого света равнялось 1 люксель на 16 текстурных юнитов, а разрешение непрямого уже 1 люксель на 64 текстурных юнита.
Примечание: для движков Кармака характерно приравнивание размера текселей к размеру трехмерных единиц измерения, в качестве которых тогда выступали некие юниты (по факту это были обычные дюймы), при условии, что масштаб текстуры оставался равным единице. Таким образом, разрешение прямого света у нас было ~40х40 сантиметров на сэмпл, а непрямое - более чем полтора метра на сэмпл. Затем сэмплы непрямого освещения запекались в ту же самую лайтмапу, что и сэмплы прямого, сглаживались, триангулировались и характерный паттерн, когда в одном и том же изображении фактически запечено два узора, практически не проступал. Ну и не будем забывать, что лайтмапы ещё умножались на диффузную текстуру, что окончательно маскировало весь этот коктейль. Да и как вы сами могли убедиться, несмотря на все принятые допущения, это давало приятный и довольно мягкий свет. Пусть и не особо реалистичный, но примерно совпадающий с внутренними представлениями о том, как должны выглядеть прямой и отражённый свет, если не слишком придираться.
Сам симулятор радиосити был устроен примерно следующим образом:
1. пространство нарезалось на квадратные (по возможности) патчи, каждый из которых был минимальной единицей дискретизации пространства, для расчёта непрямого освещения. Как было сказано выше, размеры каждого патча примерно полтора на полтора метра.
2. В эти патчи загонялась световая энергия, полученная из прямого света. Поскольку патчи в основном были квадратные, то было достаточно определить люксель, находящийся в границах патчах (с помощью простой проверки пересечения объемов) и добавить его световую энергию к общей энергии патча. В дальнейшем суммарный объем световой энергии делился либо на кол-во патчей, либо на соотношение площади патча к суммарной площади люкселей, внёсших свой вклад в освещение этого патча (в компиляторах, основанных на идеях Кармака, в принципе, встречаются оба варианта, либо нечто производное. Не возьмусь сказать какой из способов более корректный).
3. Теперь, когда у нас есть массив патчей и в некоторые из них занесена начальная энергия, мы получаем симулятор непрямого освещения. Самый настоящий, физически корректный симулятор, просто с очень низким разрешением. Как и в большинстве различных симуляторов, сама эволюция довольно проста, а сложность заключается в корректном вводе данных и фазе отбора полученных результатов. Симулятор радиосити исключением не является, тут тоже всё довольно просто:
- определить видимость каждого патча из каждого
- переизлучить (re-emit) свет на все видимые патчи, решив уравнение, что будет переизлучено, а что задержится на том или ином патче в виде финального освещения
- повторять до тех пор, пока суммарная величина оставшейся энергии не снизится до нуля
- вычесть начальное освещение из патчей (чтобы не нарушать закон сохранения энергии)
4. В принципе, на этом всё. Освещение посчитано, осталось лишь выполнить пресловутый апскейлинг, подогнав сверхнизкое разрешение вторичного освещения до разрешения лайтмапы таким образом, чтобы два сложенных паттерна не проявлялись характерным образом. Т.е. замаскировать неизбежно возникающую интерференцию.
Вы, вероятно, уже устали от такого обилия текста, поэтому проиллюстрирую вышеописанное на примере:
Всё вышесказанное, как вы понимаете, справедливо для отложенных расчётов, выполняемых один раз и сохраняющих свой результат в лайтмапу. Но мы хотим иметь динамический расчёт непрямого освещения. Например, смену времени суток или просто открытые и закрытые двери, которые вносят вклад в освещение, в случае, когда в помещении нет источника прямого света, и оно освещается исключительно вторичным освещением.
И вот здесь мы уже сталкиваемся с серьезными трудностями, обусловленными историческим вектором развития видеокарт как устройств для растеризации. Расчёт прямого освещения, реализованный в лайтмаппере, в наше время можно чуть ли не механически переложить на GPU - все условия для этого созданы и каких-то трудностей не возникнет. Да и как я показывал, вместо теневых карт мы даже можем использовать настоящий рейтрейсинг, тот самый, который используют оффлайн-компиляторы освещения. И это всё будет работать, может недостаточно быстро, по крайней мере на железе прошлого поколения, но на современных видеокартах - с более чем достаточным запасом по производительности.
С непрямым освещением так не получится. Потому что в пределе оно учитывает вклад каждого фрагмента на каждый, то есть объем вычислений потенциально растёт в геометрической прогрессии и механический перенос вышеописанного алгоритма на GPU, может потенциально оказаться даже тяжелее чем практикуемый повсеместно подход рейтрейсинга (когда мы из камеры выпускаем лучи, в надежде что они попадут в какой-либо источник света).
Но у вышеописанного алгоритма при этом есть одно замечательное свойство - он имеет гарантированную сходимость за довольно небольшое число шагов, более того, его работу можно прервать на любой итерации и всё равно получить приличный результат, т.к. каждая итерация, по сути, представляет собой ровно один отскок непрямого освещения от геометрии.
Я специально заостряю ваше внимание на этом моменте, чтобы пояснить почему он мне так нравится и почему в дальнейшем я буду стараться оставаться в рамках именно этого подхода. Потому что рейтрейсинг, который применяется в играх, сейчас использует рандомизацию. Пусть даже по алгоритмам русской рулетки или Монте-Карло, но это всё равно будет рандомизация, которая в любой момент времени выполнения алгоритма не сможет вам гарантировать что освещение просчитано достаточно для того, чтобы остановить расчёты (ну скажем в рамках оптимизации для слабых компьютеров) и вывести результаты на экран. Ничего подобного! В любой момент времени расчёта мы потенциально имеем достаточно большие выпадающие фрагменты, на заполнение которых может потребоваться неопределённое кол-во времени, из-за чего их и предпочитают маскировать алгоритмами шумоподавления. То есть время сходимости тут не детерминировано явным образом. Можете считать это моим личным предпочтением, но я подобные алгоритмы не люблю и стараюсь с ними не связываться.
Если же вам всё ещё непонятна проблематика, то поясню - видеокарты очень долгое время могли оперировать исключительно с данными, которые выводят на экран в настоящее время. А для непрямого освещения мы как раз имеем дело с данными, которые не видим на экране никоим образом (поэтому оно и непрямое). Данное ограничение было успешно убрано с появлением, сначала геометрических, а затем и вычислительных шейдеров, и вот именно тогда и начали появляться первые реализации realtime radiosity, которые уже было не стыдно использовать в продакшне, пусть и с рядом оговорок к сеттингу.
Вы, конечно, можете задаться вопросом, неужели не было попыток обойти ограничение видеокарт того времени и попытаться рассчитать непрямое освещение на центральном процессоре?
Разумеется, такие попытки были. Но об этих попытках, равно как и о различных техниках расчёта непрямого освещения непосредственно на видеокарте мы с вами поговорим в следующей части нашего цикла.