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

L_o(\omega_o) = \int_{\Omega}^{} L_i (\omega_i) ~ \cos \theta_i ~f_r(\omega_i,\omega_o) ~ d\omega_i

Уравнение рендеринга: простое и интуитивно понятное. Но я пройдусь по каждой части, чтобы не было двусмысленностей.

\omega_o- направление от камеры к точке рендеринга.

L_o(w_o)- исходящий свет в направление \omega_o.

\Omega - полусфера с "радиусом" 1 (что служит для упрощения формул, а не радиусом в мировых координатах), с центром вокруг нормали N, содержащая все направления, для которых \omega_i \cdot N > 0.

\int_{\Omega}^{} - интегрирование всей видимой полусферы над точкой. Если объект может пропускать через себя свет, то нужно интегрировать всю сферу.

L_i (\omega_i) - количество света поступающего из направления \omega_i.

\cos\theta_i - косинус угла между \omega_i и N - нормалью поверхности. Аналогично \omega_i \cdot N, что более предпочтительно с точки зрения производительности. Чем больше угол, тем на большую площадь приходится одинаковое количество энергии.

f_r(\omega_i,\omega_o) - функция, определяющее количество света, которое может быть отражено из направления \omega_i в \omega_o. Также называется функцией материала. Обычно здесь находится Bidirectional reflectance distribution function, где Bidirectional означает, что f_r(\omega_i, \omega_o) == f_r(\omega_o, \omega_i) . Физически корректные формулы также должны отражать <= количество энергии, получаемое из всей видимой полусферы. То есть, чем более гладкий материал, тем больше света из направления \omega_i отражаются в направление идеального (зеркального) отражения, и тем более яркое и маленькое будет пятно отражённого света, и тем темнее будет выглядеть остальная поверхность. Полностью не гладкий (диффузный, Ламбертовый) материал рассеивает свет одинаково во всех направлениях. В интернете существуют дата сеты с измеренными функциями BRDF реальных материалов.

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

Свет Солнца и неба

При оценке освещения от, например, Солнца мы можем допустить, что оно бесконечно далеко, и что для всех интересующих нас точек направление к Солнцу и его интенсивность является одинаковым. Тогда, оценивая освещение поверхности в вакууме, нас волнует только одно направление (Directinal Light) к свету. Если Солнц несколько, нужно просто просуммировать их влияние.

L_o(\omega_o) = \sum^{}_{i} ~ L_i(\omega_i) ~ \cos \theta_i ~ f_r(\omega_i, \omega_o)

Говоря про вакуум, это довольно точная формула. Однако она полностью игнорирует тот факт, что на Земле атмосфера рассеивает свет по всему небосводу. В этом случае мы не можем просто отбросить интеграл:

L_o(\omega_o) =  \int_{\Omega} L_{sky}(\omega_{i}) ~ \cos \theta_{i} ~ f_r(\omega_i, \omega_o) ~ d\omega_i + L_{sun}(\omega_{sun}) ~ \cos \theta_{sun} ~ f_r(\omega_{sun}, \omega_o)

Однако, если мы имеем дело с облачной погодой, то можно сделать допущение, что свет рассеивается примерно одинаково, и что количество света примерно одинаковое со всех направлений. Тогда весь интеграл заменяется на L_{ambient}. Однако, простая константа не учитывает перекрытие этого света другими объектами. Для того, чтобы сгладить неточность первого допущения, мы вводим второе в виде Ambient Occlusion, которое обычно вычитается из общего света.

Однако, что если наш искомый объект очень гладкий, и он находится на Земле среди других объектов? Показать зеркальный объект без отражений нельзя. У нас не остаётся другого выбора, кроме как просуммировать влияние отражённого света от всех окружающих вещей: L_o(\omega_o) = L_{lights}(\omega_i) + L_{objects}(\omega_i)

Мы разделяем прямое влияние источников света (что довольно просто посчитать), и влияние непрямое. В более привычном виде: L_o(\omega_o) = L_{direct}(\omega_i) + L_{inderect}(\omega_i). Только недавно, с ростом "грубой силы" видеокарт, мы начали пытаться рассчитать непрямое влияние в реальном времени. С зеркалами всё относительно просто, по скольку нам нужно посылать луч только в одном направление. А вот честное интегрирование диффузных материалов остаётся непосильной задачей, по скольку в среднем у нас есть бюджет на от 1 до 4 лучей на пиксель (зачастую, менее одного!). Приходится идти на огромное количество ухищрений, чтобы получить правдоподобный результат: расчёт с пониженным разрешением, применение денойзеров для борьбы с неизбежным шумом при таком количестве лучей, а также временное накопление результатов.

Свет из точки

Здесь мы начнём глубже знакомиться с L_i(\omega_i) . Для Directional light всё просто, свет равен единице в одном направление, и равен нулю во всех других. Однако, для точечного источника света нам теперь важна позиция.

L_i(\omega_i) = I_i ~ f_a(x) ~ V_i(x, \omega_i)

Где:

I_i - интенсивность света.

x - точка, для которой рассчитывается освещение.

f_a(x) - функция затухания света.

V_i(x, \omega_i) - функция видимости, равна 1 - shadow.

Для точечного источника света, есть простая формула для функции затухания:

f_a(d) = \frac{1} {d^2}, где d это расстояние от источника света до x. Это очень простая и физически корректная формула. Однако, здесь есть одна практическая проблема: когда d \to 0 , f_a \to \infty. Когда мы строим сцену с реалистичным освещением, мы пытаемся с помощью точечных источников света приблизительно отразить влияние реальных источников света, у которых есть какой-то размер. И этот нюанс с бесконечностью довольно сильно мешает. Типичными костылями являются:

f_a(d) = \frac{1}{d^2 + c} и f_a(d) = max( \frac{1}{d^2}, c) , где c является арбитрарно взятым числом. Не буду на них подробно останавливаться, и так очевидна костыльность.

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

Пример такой функции: f_a(d) = \frac{2}{d^2 + \frac{r^2}{2}}, однако она затухает медленнее, чем физически корректная.

Физически корректная функция для точечного источника света с радиусом выглядит так:

f_a(d) = \frac{2}{d^2 + r^2 + d \sqrt{d^2 + r^2}} , подробнее о выведение см тут.

У этой формулы есть несколько преимуществ: 1) интенсивность света достигает константы при нулевом расстояние, 2) при большом расстояние значения f_a приближаются к стандартной формуле, 3) при нулевом радиусе работает точно также, как и стандартная формула.

Сравнение функций затухания

Под ad hoc имеется в виду f_a(d) = \frac{2}{d^2 + \frac{r^2}{2}}

Сравнение функций на сцене с источником света одинаковой яркости:

  Значения быстро улетают в бесконечность, что даёт пересвет.
\frac{1} {d^2} Значения быстро улетают в бесконечность, что даёт пересвет.
 Немного "отталкивает" дистанцию, но всё ещё плохо.
\frac{1}{d^2 + c} Немного "отталкивает" дистанцию, но всё ещё плохо.
 Немного лучше, но слишком медленно затухает. Заметно, что стены ярче, чем на фото выше и ниже.
\frac{2}{d^2 + \frac{r^2}{2}} Немного лучше, но слишком медленно затухает. Заметно, что стены ярче, чем на фото выше и ниже.
 правильное затухание и нет бесконечности.
\frac{2}{d^2 + r^2 + d \sqrt{d^2 + r^2}} правильное затухание и нет бесконечности.

Свет из области

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

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

I = \frac{S_С}{\pi}

, где \pi это площадь диска. Тогда, если фигура полностью закрывает полусферу, её спроецированная площадь S_С = \pi и I = 1.

Доказательство: освещённость полусферической областиS_\Omega обычно определяется как интеграл косинусов направлений к области, делённых на \pi

I = \frac{1}{\pi} \int_{S_\Omega} \cos \theta ~ dw

Косинус в выражении можно интерпретировать как якобиан проекции на единичный диск:

\frac{dp}{d\omega} = \cos \theta

d\omega это участок на полусфере, а dp тот же участок спроецированный на диск. Если этот участок на полюсе, то \cos \theta = 1, значит dp имеет ту же самую площадь. Если у горизонта, то проекция вырождается в отрезок с нулевой площалью.

что позволяет заменить интегрирование:

I = \frac{1}{\pi} \int_{S_\Omega} \cos \theta  d\omega \quad =\frac{1}{\pi} \int_{S_C} 1 dp \quad = \frac{S_C}{\pi}

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

Синяя область равна сумме положительных (зелёных) и отрицательных (красных) областей.
Синяя область равна сумме положительных (зелёных) и отрицательных (красных) областей.

Каждое ребре треугольника (v_i v_j)  определяет сектор диска ("срез пиццы"), заданный углом \theta_{ij} и нормалью n_{ij}.

\theta_{ij} = \arccos(v_i \cdot v_j), \quad n_{ij} = \frac{v_i \times v_j}{\|v_i \times v_j\|}

Площадь сектора диска пропорциональна его углу:

A_{ij} = \frac{\theta_{ij}}{2}
Секторы диска, связанные с рёбрами треугольника. Знаковая проецируемая площадь сектора диска положительна (зелёная), если его нормаль направлена в верхнюю полусферу, и отрицательна (красная) в противном случае.
Секторы диска, связанные с рёбрами треугольника. Знаковая проецируемая площадь сектора диска положительна (зелёная), если его нормаль направлена в верхнюю полусферу, и отрицательна (красная) в противном случае.

Знаковая проецируемая площадь сектора диска равна его площади A_{ij}, умноженной на знаковый косинус его нормали (n_{ij} · z). Разделив знаковую проецируемую площадь на \pi, получим знаковую освещённость сектора диска:

I_{ij} = \frac{1}{\pi} (n_{ij} \cdot z) A_{ij}= \frac{1}{\pi} \left( \frac{v_i \times v_j}{\|v_i \times v_j\|} \cdot z \right) \frac{\arccos(v_i \cdot v_j)}{2}

В итоге, мы получаем общее освещение треугольника как сумму знаковых освещённостей секторов диска: I(v_1, v_2, v_3) = I_{12} + I_{23} + I_{13}

Все эти формулы были выведены Ламбертом ещё в 1760 году. В итоге мы можем рассчитать прекрасное диффузное освещение:

Прекрасно, не правда ли?
Прекрасно, не правда ли?

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

К счастью, в 2016 появилась работа, которая описывает использование нового семейства распределений Линейно Трансформированных Косинусов (LTCs). LTC позволяют с достаточной точностью описывать влияние полигонального освещения на PBR поверхность. Всё что для этого нужно: LUT текстура с заранее проинтегрированными параметрами, а также рассчитанное нами диффузное освещение.

Устройство LUT текстуры
Устройство LUT текстуры

Это позволяет нам в реальном времени рассчитать влияние любого полигонального источника света на практически любую поверхность. Стоимость растёт линейно в зависимости от количества рёбер + стоимость чтения из LUT текстуры.

Разные уровни гладкости - от зеркала до матовой поверхности.
Разные уровни гладкости - от зеркала до матовой поверхности.

Как вишенка на торте, мы также можем использовать текстуру на источнике света, и семплировать разные уровни Lod для имитации рассеивания из-за расстояния и шероховатости!

Изменение гладкости в реальном времени.
Изменение гладкости в реальном времени.

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

Спасибо за прочтение до конца. Ссылка на имплементацию шейдера с LTC. Ссылка на пример генерации Lut текстур.

Комментарии (1)


  1. MasterMentor
    03.07.2025 07:28

    Тот случай, когда статья вызывает Wow! эффект (ИМХО).