ПрограммированиеСтатьиГрафика

На пути к эффективному алгоритму Global Illumination, часть 1

Автор:

В этой статье мы рассмотрим небольшой исторический контекст разработки алгоритмов global illumination, а также поразмышляем, какими свойствами должен обладать оптимальный алгоритм расчёта global illumination.

Исторический экскурс
На каком этапе здесь находится индустрия?
Почему именно screenspace?
  We need to go deeper
  Even deeper
Алгоритмическая сложность трассировки одного луча
Заключение

Исторический экскурс

В компьютерной графике сложилась интересная ситуация — в то время как для растеризации геометрии на экране уже последние несколько десятков(!) лет де-факто стандартным представлением является полигональное представление геометрии, для представления global illumination используется огромное количество самых разных, часто не имеющих ничего общего друг с другом, структур данных: light propagation volumes, sparse voxel trees, imperfect shadow maps, итп. Получается так в первую очередь потому, что полигональное представление очень эффективно справляется с задачей растеризации, в которой каждому фрагменту трёхмерной геометрии ставится в соответствие единственый фрагмент на экране, в который он проецируется. Это достигается через проецирование вершин треугольника на экранную плоскость и дальнейшей растеризацией каждого треугольника уже в экранных координатах. Этот способ исторически себя настолько хорошо себя зарекомендовал, что после того, как первые аппаратные ускорители графики реализовали эти преобразования в "железе" в начале 90-х, до момента написания этой статьи, в них по большому счёту ничего принципиально не менялось.

Взглянем на основное уравнение рендеринга, определяющее освещённость фрагмента:
\(L_0(\vec x, \vec \omega_0) = L_e(\vec x) + \int_{\Omega} f(\vec x, \vec \omega_0, \vec \omega_i) L_i(\vec x, \vec \omega_i,) (\vec w_i \cdot \vec n) d\vec \omega_i\) (1)
Для простоты я выбросил из уравнения зависимость от длины волны \(\lambda\) и от времени \(t\). К сожалению, это уравнение не только выглядит страшновато, но история также показала, что его исключительно трудно решать даже численно. Более того, все существующие подходы в компьютерной графике являются в той или иной степени его различными аппроксимациями. Если вы не совсем понимаете смысл этого уравнения, то ничего страшного, далее мы будем рассматривать его упрощённые формы с подробными пояснениями, пока не придём к нему в изначальном виде для моделирования полного GI. Например, растеризацию объектов без учёта освещения можно описать как уравнение рендеринга, в котором полностью пренебрегли существованием интеграла и свели его к следующему виду:
\(L_0(\vec x, \vec \omega_0) = L_e(\vec x)\) (2)
Интуитивно это выражение можно понять следующим образом — светимость фрагмента на экране определяется собственной светимостью этого самого фрагмента и больше ни от чего не зависит.

Расчёт прямого освещения объектов точечных (или направленных) источников также можно свести к растеризации, добавив технологию расчёта видимости фрагментов для каждого источника (для учёта теней), что делается через алгоритмы shadow mapping'а, которые хоть и улучшаются со временем, но идейно остаются теми же самыми уже много лет. Это позволяет получить более точную аппроксимацию решения уравнения рендеринга, используя одну лишь растеризацию:
\(L_0(\vec x, \vec \omega_0) = L_e(\vec x) + \sum_{j=1}^{N}f(\vec x, \vec \omega_0, \vec \omega_j) L_j V_j(x) (\vec w_j \cdot \vec n)\) (3)
Можно заметить, что классический расчёт множества источников света аппроксимирует интеграл освещения как сумму освещения от N источников. В этой формуле под знаком суммы стоит BRDF поверхности \(f(\vec x, \vec \omega_0, \omega_j)\), который определяет, какая доля энергии, падающей на поверхность в точке \(x\) под направлением \(\vec \omega_j\) отражается в рассеивается в направлении \(\vec \omega_0\). Типично для аппроксимации этого параметра используются разные модели микрорельефа поверхности вроде lambert или GGX, однако, их рассмотрение выходит за рамки этой статьи. Под \(\vec \omega_j\) понимается направление на источник света под номером \(j\), \(L_j\) — его яркость, а \(V_j(\vec x)\) — функция видимости точки \(\vec x\) для источника j, что типично реализуется через shadow mapping.

Обратим внимание, что формула (3) удобно ложится на механизм растеризации, так как функцию \(V_j(\vec x)\) для каждого источника можно рассчитать, используя растеризацию shadow map'ы, а потом, используя её, растеризовать сам объект на экран, рассчитав формулу (3) в явном виде в шейдере. Из-за того, что в формуле присутствует цикл по всем источникам, она эффективнее всего работает, если этих самых источников света достаточно мало и производительность будет линейно убывать с количеством источников.

Принципиальная же особенность расчёта непрямого освещения (формула (1)) от прямого (формула (3)) заключается в том, что непрямое освещение эквивалентно расчёту прямого освещения от бесконечного количества источников прямого освещения. То есть рассчитать его трудно не просто потому, что источников бесконечное количество, но ещё и потому, что производительность алгоритмов прямого освещения линейно падает с количеством источников. Один из первых подходов к аппроксимации GI — reflective shadow maps. В этом подходе непрямое освещение моделируется как прямое освещение от большого количества вторичных источников света с яркостью, определяющейся их первичным освещением. То есть после расчёта первичного освещения на все фрагменты, на которое попал первичный свет, ставятся вторичные источники света и по формуле (3) рассчитывается их влияние на сцену. Уравнение рендеринга для этого подхода можно описать так:
\(L_0(\vec x, \vec \omega_0) = L_e(\vec x) + \sum_{j=1}^{N}f(\vec x, \vec \omega_0, \vec \omega_j) L_j V_j(x) (\vec w_j \cdot \vec n) +\)
\(+ \sum_{k=1}^{N_d}f(\vec x, \vec \omega_0, \vec \omega_k) L_k \cdot (\vec w_k \cdot \vec n)\) (4)
здесь \(N_d\) — количество вторичных источников, которые расставляются в область прямого освещения, а \(L_k\) определяет их яркость.

Это был одним из первых алгоритмов, которые я реализовал, это выглядело примерно так:

Запустить видео по клику - Как делать игрыЗапустить видео по клику - Как делать игры

Мало того, что этот подход является очень неэффективным(итерирование через сотню источников в шейдере очень долго считать), в нём также полностью игнорируется существование множителя \(V_j(\vec x)\), отвечающего за тени от вторичного освещения, поэтому они полностью отсутствуют.

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

Конечно же, к такому выводу пришёл не я один. Огромное количество исследователей пробовали побороть уравнение рендеринга. И самое, на мой взгляд, интересное — то, насколько разными путями они шли. Кто-то представлял  сцену как дискретную трёхмерную сетку и выпускал по ней лучи (vxgi), кто-то представлял сцену как множество элементарных элементов поверхности и рассчитывал их видимость для источников вторичного освщещения (imperfect shadow mapping), кто-то рассчитывал распространение светового фронта как функцию направления для сцены (light propagation volumes), кто-то предрассчитывал излучение каждого фрагмента сцены на основе его окружения (precomputed radiance transfer). Единственное, что объединяет все эти подходы — что в них нет растеризации треугольников. То есть исследователи уже смирились, что для реализации GI нужно альтернативное представление сцены, которое позволит рассчитать влияние вторичного освещения более эффективно, чем через растеризацию. Вопрос лишь в том, какое это должно быть представление. На момент написания этой статьи это — по-прежнему открытый и, на мой взгляд, очень острый вопрос. Сразу скажу, что я не смогу дать на него однозначного ответа. Но я потратил на его обдумывание тысячи часов и хотел бы поделиться некоторыми выводами.

На каком этапе здесь находится индустрия?

Часто если людям, даже плотно занимающимся разработкой графики, показать программу и сказать, что это — unbiased screenspace GI, их первый вопрос бывает вроде "а как оно рендерит источники света за экраном"? Но господа, постойте. То есть вас не удивляет сама возможность существования unbiased solver'а (напомню, unbiased solver сходится к точному решению уравнения (1)) хотя бы в screenspace'е? Здесь и далее, если применяется термин unbiased в контексте screenspace, это обозначает, что метод сходится к точному решению при увеличении параметра точности метода и условии, что вся геометрия помещается на экране без загораживания. Также я буду рассматривать только вторичное освещение, так как третичное и далее считается по аналогии и не влияет на общую алгоритмическую сложность.

Люди привыкли видеть демонстрации модных технологий вроде rtx или последних версий unreal engine которые выглядят так, будто в них уже давно считается unbiased освещение да ещё и не в screeenspace'е, то есть, можно подумать, в этом нет ничего особо удивительного. Так вот, сразу обозначу, что ключевых вопроса здесь два — масштабируемость технологии и общность.

Масштабируемость
Разумеется, можно бросить всю вычислительную мощность последней видеокарты от nvidia, чтобы рассчитать фотореалистично выглядящий cornell box даже с самым наивным monte-carlo path tracer'ом и её вычислительной мощности будет достаточно, чтобы рендерить такую сцену в реалтайме почти без шума. Это будет очень общий подход, который теоретически позволит рассчитать unbiased GI для любой сцены, но он будет масштабироваться чуть хуже, чем никак. Path tracer'ы в наивной реализации настолько неэффективно используют вычислительную мощность, что нет смысла говорить об их применении для серьёзных сцен. Причём, на мой взгляд, наивно полагать, что это только сейчас, пока у GPU не хватает мощности считать path tracing в реальном времени. Мол, завтра (через N лет) видеокарты станут мощнее и вполне потянут path tracing в реальном времени. Наивное суждение, так как если к тому времени у них хватит ресурсов на path tracing сегодняшних сцен, то представьте, какие сцены на них можно будет рендерить более продвинутыми алгоритмами. Каким бы мощным железо ни становилось со временем, его всегда рациональнее всего по возможности использовать с наиболее эффективными алгоритмами вместо брутфорсных.

Общность
Люди привыкли видеть выносящие мозг своей красотой демки rtx от nvidia, показывающие идеально выглядящие отражения:

Запустить видео по клику - Как делать игрыЗапустить видео по клику - Как делать игры

И если человеку задать вопрос: "У них зеркальный пол, зеркальные штормтруперы и зеркальные стены. Как ты думаешь, почему?". Практически всегда люди отвечают что-то вроде : "Чтобы показать, как круто у них выглядят отражения!". На вопрос же : "Как ты думаешь, они могут так же здорово рендерить что-то кроме зеркальных поверхностей?", типичный ответ: "Ээ.. ну конечно! Наверное?". Стоит отдать должное, как принципиальное ограничение технологии nvidia с успехом выдаёт за фичу. Ответ на этот вопрос — нет, они не могут считать ни диффузные, ни glossy отражения достаточно эффективно. Если выписать уравнение рендеринга, которое эффективно позволяет считать rtx и подобные технологии, оно выглядит так:
\(L_0(\vec x, \vec \omega_0) = L_e(\vec x) + f(\vec x, \vec \omega_0, \vec \omega_r) L_i(\vec x, \vec \omega_r) (\vec w_r \cdot \vec n)\) (4)
Здесь \(\omega_r\) — направление отражённого луча. Да, надо отдать должное, рейтрейсинг действительно эффективно позволяет получить значение множителя \(L_i(\vec x, \vec \omega_r)\), но истина в том, что этот множитель эффективен лишь при расчёте зеркальных отражений. Для аппроксимации диффузных поверхностей в рейтрейсинге используется несколько лучей. Алгоритмы, которые я буду рассматривать далее, используют в среднем около 128 плоскостей на фрагмент в реалтайме на существующем железе. Каждую плоскость можно более-менее аппроксимировать через как минимум 8 (на самом деле — больше) лучей. То есть чтобы получить те же параметры сходимости, rtx или любой другой технологии, основанной на world-space лучах придётся выпустить около 1024 эффективных лучей на пиксел вместо 1. 1024 луча с использованием rtx будут работать в 1024 раза медленнее, чем железо для записи этого демо которое уже стоит как квартира его среднестатистического зрителя. И если вы сейчас подумали : "ну, значит надо подождать, когда видеокарты станут ещё в 1024 раза мощнее и вот тогда заживём!", то именно от этого я хотел предостеречь — к моменту, когда видеокарты станут в 1024 раза мощнее, на них можно будет либо гонять эту сцену с диффузными материалами и rtx, либо сцену в 1024 раза сложнее, если использовать более продвинутый алгоритм, чем rtx.

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

Об успешности других технологий, используемых в индустрии, можно судить как минимум по их разнообразию. Если бы объективно успешная технология существовала, все бы пользовались только ей. Однако, каждый AAA двиг (а иногда и каждая новая версия) сейчас используют разные подходы к рендерингу GI и у каждого есть свои преимущества и недостатки. Если вам кто-то говорит, что реализовал unbiased gi в realtime без ограничений, не верьте — вам врут. Точный GI рассчитать сейчас невозможно, вопрос в том, как меньше соврать при его расчёте и как это сделать наиболее эффективно.

Почему именно screenspace?

Сегодняшние вычислительные мощности находятся на стадии, когда в общем случае рассчитать точный GI в world space в реальном времени просто невозможно. По крайней мере нет ни одного алгоритма, даже приближающегося к этому. Однако, это — очень интересный момент, так как именно сейчас у mid-end машин едва-едва хватает производительности, чтобы считать его в screenspace, если использовать ряд принципиальных свойств уравнения(1). У кого-то может сложиться впечатление, что "нуу, если screenspace, то это проосто". Так вот, дисклеймер — даже в скринспейсе это вообще не просто, иначе бы все это делали. Первая более-менее внятная реализация SSGI, которая появлась в индустрии — это, пожалуй в unigine:
https://developer.unigine.com/en/devlog/20170531-unigine-2.5

Запустить видео по клику - Как делать игрыЗапустить видео по клику - Как делать игры

Они по понятным причинам не раскрывают деталей реализации, однако, судя по их презентации с последнего GDC, волшебства они не реализовали и у их алгоритма есть серьёзные ограничения — например, у их реализации сильно падает точность с увеличением радиуса действия GI, в моей же реализации радиус — всегда бесконечность(что физически корректно). Доступные публикации вроде https://www.in.tu-clausthal.de/fileadmin/homes/CG/data_pub/paper/SSDO_i3D09.pdf находятся в совершенно зачаточном состоянии, так как напрочь не умеют far-field global illumination и, следовательно, не являются unbiased.

Это я к тому, что даже в screenspace со всеми его недостатками, расчёт GI — вовсе не тривиальная задача и для этого на момент написания статьи по-прежнему не существует надёжных общепризнанных алгоритмов.

We need to go deeper

Есть более глубинная причина, по которой я считаю screenspace методы особенно интересными. Если вдуматься, какой именно размерности проблема, которую мы решаем? Другими словами, как именно может масштабироваться теоретически существующий оптимальный алгоритм расчёта global illumination? Для ответа на этот вопрос взглянем на самую сложную для расчёта часть уравнения (1) — интеграл:
\(\int_{\Omega} f(\vec x, \vec \omega_0, \vec \omega_i) L_i(\vec x, \vec \omega_i,) (\vec w_i \cdot \vec n) d\vec \omega_i\). Этот интеграл определяет влияние окружения на фрагмент в точке \(\vec x\). Самый наивный способ для расчёта этого интеграла — это численно разбить сферу на множество независимых лучей и от интеграла перейти к сумме, как это делает уравнение (4). Именно по такой схеме работают rtx и многие воксельные алгоритмы. Однако, этот способ не учитывает некоторые принципиальные особенности задачи, которую мы решаем, а именно:
— точки пересечения со сценой лежащих в одной плоскости лучей, также всегда лежат в одной плоскости
— точки пересечения почти параллельных лучей обычно лежат почти в одной плоскости
— параллельные лучи, выпущенные почти из одной точки, обычно пересекают сцену почти в одной точке

Эти свойства наводят на мысль, что решение уравнения (4) через выпускание лучей по полусфере — это не оптимальный способ, так как он не использует многие принципиальные особенности моделируемоей системы.

Более осязаемая количественная оценка. Рассчитаем, сколько лучей необходимо для расчёта источника вторичного освещения радиусом \(r\) на расстоянии \(R\) от текущего фрагмента. Если выпускать лучи по полусфере, то понадобится в среднем \(N \approx \frac{2\pi}{\Omega}\) лучей, чтобы его разрешить, где \(\Omega \approx \frac{\pi r^2}{R^2}\) — телесный угол, под которым виден источник, то есть
\(N_1 \approx \frac{2R^2}{r^2} \sim \frac{1}{r^2}\) (5)
Если же выпускать лучи в экранных координатах, то количество лучей можно оценить как \(N \approx {2\pi}{\alpha}\), где \(\alpha=\frac{r_p}{R_p}\approx\frac{r}{R_p^2}\) — угловой размер источника в экранных координатах (\(r_p\), \(R_p\) — размер и расстояние до источника в экранных координатах). Также заметим, что при использовании ортогональной проекции, \(R=R_p\). Другими словами, в экранных координатах нам понадобится
\(N_2 \approx \frac{2\pi R^2}{r} \sim \frac{1}{r}\) (6)
лучей, чтобы разрешить тот же самый источник.

То есть если количество лучей растёт обратно пропорционально первой степени радиуса источника в экранных координатах и второй — в мировых. Это очень важное замечание, указывающее на то, что задача расчёта GI может при определённых условиях масштабироваться с первой степенью точности, а не второй. Обращаю внимание на "может", так как в image space часть информации может теряться, но иногда может и нет. Вопрос в том, как использовать это свойство, чтобы воспользоваться этим самым может.

Even deeper

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

Изображение

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

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

Изображение

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

"Какое же это отношение имеет к рендерингу GI?" — спросите вы? А совершенно прямое. Это всего лишь ещё одно доказательство того, что теоретически оптимальный алгоритм расчёта global illumination должен работать с представлением сцены в двумерном представлении того или иного вида и масштабироваться как масштабируется разрешение плоскости, а не трёхмерного пространства. Поэтому моя реализация GI в screenspace — это всего лишь один из способов использования этого важного свойства, и я твёрдо уверен, что это можно сделать и лучше.

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

Алгоритмическая сложность трассировки одного луча

В предыдущем разделе мы установили важное свойство, согласно которому размерность проблемы global illumination должна соответствовать размерности image space, что на одно измерение меньше, чем размерность world space. Однако, существует способ снизить размерность, убрав ещё одно измерение. Дело в том, что уравнение (1) считается вовсе не для одного фрагмента на экране, а для всех. В результате этого все лучи для фрагментов, лежащих на этом самом луче, будут совпадать. Это — принципиально важное наблюдение, которое используется в line sweep ambient occlusion. К сожалению, экстраполировать этот метод на расчёт global illumination не является тривиальной задачей, однако, это теоретически возможно. Перефразируя, идея метода заключается в том, что все лучи, лежащие на одной прямой в экранном пространстве, являются общими для всех фрагментов, лежащих на этой прямой. Более того, прямой в экранном пространстве соответствует целое семейство лучей, образующих плоскость в мировом пространстве, то есть теоретически влияние их всех можно рассчитать, используя только данные с фрагментов с одной прямой.

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

Заключение

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

Запустить видео по клику - Как делать игрыЗапустить видео по клику - Как делать игры

#global illumination, #графика

22 апреля 2019 (Обновление: 23 апр 2019)

Комментарии [157]