На этом уроке мы рассмотрим, что такое HDR, LDR и тональная компрессия в OpenGL.
HDR
По умолчанию, при хранении яркости и цвета во фреймбуфере, их значения сужаются до значений, лежащих в диапазоне [0.0, 1.0]
. Эта, на первый взгляд, безобидная особенность побуждала нас всегда задавать параметры света и цвета значениями из данного диапазона, тем самым как бы подгоняя их под параметры сцены. Данный прием является вполне рабочим и дает достойные результаты, но что произойдет, если мы пройдемся по действительно яркой области с несколькими яркими источниками света, которые в общей сумме превышают значение 1.0
? Ответ заключается в том, что значения яркости всех фрагментов, имеющих яркость или цветовую сумму более 1.0
, становятся равными 1.0
. Это приводит к тому, что на изображение становится не очень приятно смотреть:
Из-за того, что большое количество цветовых значений фрагментов сужается до значения 1.0
, каждый из ярких фрагментов имеет точно такое же значение белого цвета, в результате чего теряется значительное количество деталей и сцене придается фальшивый вид.
Одним из решений данной проблемы является уменьшение силы источников света, чтобы ни одна область фрагментов в вашей сцене не была ярче 1.0
; это не очень хорошее решение, так как оно вынуждает вас использовать нереалистичные параметры освещения. Лучший подход — это разрешить цветовым значениям временно превышать границу 1.0
, а затем на заключительном шаге преобразовывать их обратно в исходный диапазон [0.0, 1.0]
, но без потерь в детализации.
Мониторы (не поддерживающие HDR) ограничены отображением цветов со значением яркости в диапазоне между 0.0
и 1.0
, но в уравнениях освещения такого ограничения нет. Позволяя яркости цветов фрагментов превышать значение 1.0
, мы получаем гораздо более высокий диапазон значений цветов, доступных для работы в так называемом расширенном динамическом диапазоне (сокр. «HDR» от «High Dynamic Range»). С расширенным динамическим диапазоном яркие вещи могут быть действительно яркими, темные вещи могут быть действительно темными, и при этом детали можно увидеть в обоих случаях.
Первоначально расширенный динамический диапазон использовался только для фотографии, где фотограф делает несколько снимков одной и той же сцены с различными уровнями экспозиции, захватывая большой диапазон цветовых значений. Сочетание этих параметров формирует HDR-изображение, в котором использование комбинированных уровней экспозиции или конкретной экспозиции помогают передать намного больший диапазон деталей объекта. Например, следующее изображение демонстрирует отображение значительного количества деталей в ярко освещенных областях с низкой экспозицией (посмотрите на окно), но эти детали пропадают на изображении с высокой экспозицией:
В свою очередь высокая экспозиция раскрывает большое количество деталей в более темных областях, которые ранее не были видны.
Принцип работы человеческого глаза является основой HDR-рендеринга. Когда света мало, человеческий глаз приспосабливается так, что более темные части становятся более заметными. Аналогичное приспособление происходит и для светлых областей. Утрируя, можно сказать, что человеческий глаз имеет автоматический регулятор экспозиции, зависящий от яркости сцены.
Рендеринг с расширенным динамическим диапазоном работает примерно так же. Мы разрешаем использование расширенного диапазона цветовых значений для рендеринга, собирая большой диапазон темных и ярких деталей сцены, и, в конце концов, преобразуем все HDR-значения обратно в низкий динамический диапазон (сокр. «LDR» от «Low Dynamic Range») — [0.0, 1.0]
. Этот процесс преобразования HDR-значений в LDR-значения называется тональной компрессией, и существует большая коллекция алгоритмов тональной компрессии, направленных на сохранение в процессе преобразования значительного количества HDR-детализации. Алгоритмы тональной компрессии часто задействуют параметр экспозиции, с помощью которого можно избирательно влиять на темные или яркие области.
Когда речь заходит о рендеринге в реальном времени, расширенный динамический диапазон позволяет нам не только превысить LDR-диапазон [0.0, 1.0]
и сохранить больше деталей, но и дает нам возможность указывать интенсивность источника света через его реальную интенсивность. Например, Солнце имеет гораздо более высокую интенсивность, чем какой-нибудь фонарик, так почему бы не настроить солнце должным образом (например, задать рассеянную яркость, равную 100.0
). Это позволяет нам более правильно настроить освещение сцены с более реалистичными параметрами освещения, что было бы невозможно при LDR-рендеринге, поскольку тогда значения яркости были бы непосредственно сужены до 1.0
.
Поскольку мониторы (не поддерживающие HDR) отображают только яркость цвета в диапазоне от 0.0
до 1.0
, то нам действительно нужно преобразовать текущий расширенный динамический диапазон значений цвета обратно в диапазон монитора. Простое обратное преобразование цветов с помощью усреднения значения не принесет нам много пользы, так как более яркие области станут намного более доминирующими. Что мы можем сделать, так это использовать различные уравнения и/или кривые для преобразования HDR-значений обратно в LDR-значения, которые дадут нам полный контроль над яркостью сцены. Данный процесс, ранее обозначенный как тональная компрессия, является заключительным этапом HDR-рендеринга.
Фреймбуферы типа с плавающей точкой
Для реализации рендеринга с расширенным динамическим диапазоном нам нужен какой-то способ предотвратить сужение цветовых значений после каждого запуска фрагментного шейдера. Когда фреймбуферы используют нормализованный с фиксированной точкой формат цвета (например, GL_RGB
) в качестве внутреннего формата цветового буфера, то OpenGL, перед сохранением во фреймбуфер, автоматически сужает диапазон значений яркости до [0.0, 1.0]
. Эта операция выполняется для большинства типов форматов фреймбуфера, за исключением форматов типа с плавающей точкой.
Когда внутренний формат цветового буфера фреймбуфера задан как GL_RGB16F
, GL_RGBA16F
, GL_RGB32F
или GL_RGBA32F
, то такой фреймбуфер превращается во фреймбуфер типа с плавающей точкой, способный хранить значения типа с плавающей точкой вне заданного по умолчанию диапазона [0.0, 1.0]
. Это идеально подходит для рендеринга в расширенном динамическом диапазоне!
Чтобы создать фреймбуфер типа с плавающей точкой, единственное, что нам нужно изменить — это внутренний параметр формата цветового буфера:
1 2 |
glBindTexture(GL_TEXTURE_2D, colorBuffer); glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL); |
Стандартный OpenGL-фреймбуфер по умолчанию обеспечивает только 8 бит на каждый цветовой компонент. С помощью фреймбуфера типа с плавающей точкой, обеспечивающего 32 бита на каждый цветовой компонент (при использовании GL_RGB32F
или GL_RGBA32F
), мы используем в 4 раза больше памяти для хранения цветовых значений. Поскольку в 32 битах на практике нет нужды (если только вам не нужен высокий уровень точности), достаточно будет использовать GL_RGBA16F
.
С помощью цветового буфера типа с плавающей точкой, подключенного к фреймбуферу, мы теперь можем рендерить сцену во фреймбуфер, зная, что значения яркости цвета не будут зажаты между 0.0
и 1.0
. В демонстрационном примере данного урока мы сначала рендерим освещенную сцену во фреймбуфер типа с плавающей точкой, а затем отображаем цветовой буфер фреймбуфера на экране; это будет выглядеть примерно так:
1 2 3 4 5 6 7 8 9 10 |
glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // [...] рендеринг (освещенной) сцены glBindFramebuffer(GL_FRAMEBUFFER, 0); // Теперь рендерим цветовой HDR-буфер на заполняющий экран 2D-прямоугольник, применяя шейдер тональной компрессии hdrShader.use(); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, hdrColorBufferTexture); RenderQuad(); |
В вышеописанном фрагменте кода значения яркости цвета сцены заполняются в буфер цвета типа с плавающей точкой, который может содержать любое произвольное значение цвета, даже превышающее 1.0
. Для этого урока была создана простая демонстрационная сцена с большим растянутым кубом, представляющим собой тоннель с четырьмя точечными источниками света, один из которых чрезвычайно яркий и расположен в конце тоннеля:
1 2 3 4 5 |
std::vector<glm::vec3> lightColors; lightColors.push_back(glm::vec3(200.0f, 200.0f, 200.0f)); lightColors.push_back(glm::vec3(0.1f, 0.0f, 0.0f)); lightColors.push_back(glm::vec3(0.0f, 0.0f, 0.2f)); lightColors.push_back(glm::vec3(0.0f, 0.1f, 0.0f)); |
Рендеринг во фреймбуфер типа с плавающей точкой аналогичен обычному рендерингу во фреймбуфер. Что нового, так это фрагментный шейдер hdrShader
, который визуализирует окончательный 2D-прямоугольник с прикрепленной к нему текстурой цветового буфера типа с плавающей точкой. Давайте сначала определим простой сквозной фрагментный шейдер:
1 2 3 4 5 6 7 8 9 10 11 12 |
#version 330 core out vec4 FragColor; in vec2 TexCoords; uniform sampler2D hdrBuffer; void main() { vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb; FragColor = vec4(hdrColor, 1.0); } |
Здесь мы используем цветовое значение буфера типа с плавающей точкой в качестве выходного сигнала фрагментного шейдера. Однако, поскольку выходные данные 2D-прямоугольника непосредственно визуализируются в заданный по умолчанию фреймбуфер, все выходные значения фрагментного шейдера все равно будут зажаты между 0.0
и 1.0
, даже если у нас есть несколько значений в цветовой текстуре типа с плавающей точкой, превышающих 1.0
.
Становится ясно, что значения интенсивности света в конце тоннеля сужены до 1.0
, так как большая часть тоннеля полностью белая; фактически, в данном случае, все детали освещения потеряны. Поскольку мы непосредственно записываем HDR-значения в выходной LDR-буфер, то это выглядит так, как если бы у нас вообще не был включен HDR. Что нам нужно сделать, так это преобразовать все значения яркости цвета типа с плавающей точкой в диапазон [0.0, 1.0]
. Нам нужно применить процесс, называемый «тональной компрессией».
Тональная компрессия
Тональная компрессия — это процесс преобразования (без существенных потерь в детализации) значений яркости цвета типа с плавающей точкой в диапазон [0.0, 1.0]
(известный как низкий динамический диапазон, часто сопровождаемый определенным стилистическим цветовым балансом).
Одним из наиболее простых алгоритмов тональной компрессии является алгоритм Рейнхарда, который включает в себя деление всех HDR-значений цвета на LDR-значения цвета. Мы добавим данный алгоритм в предыдущий фрагментный шейдер, а также для верности применим фильтр гамма-коррекции (включая использование sRGB-текстур):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void main() { const float gamma = 2.2; vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb; // Алгоритм тональной компрессии Рейнхарда vec3 mapped = hdrColor / (hdrColor + vec3(1.0)); // Гамма-коррекция mapped = pow(mapped, vec3(1.0 / gamma)); FragColor = vec4(mapped, 1.0); } |
С применением алгоритма тональной компрессии Рейнхарда мы больше не теряем никаких деталей в ярких областях нашей сцены. Он имеет тенденцию отдавать предпочтение более ярким областям, делая более темные области менее детализированными и отчетливыми:
Здесь мы снова видим детали в конце тоннеля, так как рисунок текстуры дерева снова становится видимым. С помощью этого относительно простого алгоритма тональной компрессии мы можем правильно видеть весь диапазон HDR-значений, хранящихся во фреймбуфере типа с плавающей точкой, что дает нам более точный контроль над освещением сцены без потери в деталях.
Примечание: Обратите внимание, что мы также можем напрямую выполнять тональную компрессию в конце нашего шейдера освещения, не нуждаясь ни в каком фреймбуфере типа с плавающей точкой вообще! Однако, по мере усложнения сцен, часто возникает необходимость хранить промежуточные HDR-результаты в виде буферов типа с плавающей точкой, так что это хорошее упражнение.
Еще одно интересное применение тональной компрессии — это использование параметра экспозиции. Вы, вероятно, помните (см. начало урока), что HDR-изображения содержат много деталей, видимых на разных уровнях экспозиции. Если у нас есть сцена, которая показывает дневной и ночной цикл, то имеет смысл использовать более низкую экспозицию при дневном свете и более высокую экспозицию в ночное время, подобно тому, как адаптируется человеческий глаз. Введение дополнительного параметра экспозиции позволит нам настраивать параметры освещения, которые работают как днем, так и ночью при различных условиях освещения (поскольку нам лишь нужно будет изменить только этот параметр экспозиции).
Относительно простой алгоритм применения экспозиции к тональной компрессии выглядит следующим образом:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
uniform float exposure; void main() { const float gamma = 2.2; vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb; // Экспозиция тональной компрессии vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure); // Гамма-коррекция mapped = pow(mapped, vec3(1.0 / gamma)); FragColor = vec4(mapped, 1.0); } |
Здесь мы определили uniform-переменную exposure
, которая по умолчанию равна 1.0
и позволяет нам более точно определить, хотим ли мы больше фокусироваться на темных или светлых областях HDR-значений цвета. Например, при высоких значениях экспозиции более темные участки тоннеля показывают значительно больше деталей. В отличие от этого, низкая экспозиция в значительной степени убирает детали темной области, но позволяет нам видеть больше деталей в светлых областях сцены. Взгляните на следующие изображения, чтобы увидеть тоннель на нескольких уровнях экспозиции:
Эти изображения хорошо показывают преимущество рендеринга с расширенным динамическим диапазоном. Изменяя уровень экспозиции, мы видим множество деталей нашей сцены, которые в противном случае были бы потеряны при рендеринге с низким динамическим диапазоном. Возьмем, к примеру, конец тоннеля. При нормальной экспозиции структура древесины едва видна, но при низкой экспозиции отчетливо видна детализированная текстура дерева. То же самое относится и к текстуре дерева, которая более заметна при высокой экспозиции в начале тоннеля.
GitHub / Урок №36. HDR в OpenGL — Исходный код
Еще больше HDR
Два показанных алгоритма тональной компрессии — это лишь несколько примеров из большой коллекции (более продвинутых) алгоритмов тональной компрессии, каждый из которых имеет свои сильные и слабые стороны. Некоторые алгоритмы отдают предпочтение определенным цветам/интенсивности, другие алгоритмы отображают как низкие, так и высокие цвета экспозиции одновременно, чтобы создать более красочные и детализированные изображения. Существует также набор методов, известных как автоматическая регулировка экспозиции, которые определяют яркость сцены в предыдущем кадре и (медленно) адаптируют параметр экспозиции таким образом, что сцена становится ярче в темных областях или темнее в светлых областях, имитируя способности человеческих глаз.
Реальное преимущество HDR-рендеринга проявляется в больших и сложных сценах с тяжелыми алгоритмами освещения. Поскольку трудно создать такую сложную демонстрационную сцену для учебных целей, сохраняя её доступной, демонстрационная сцена данного урока мала и лишена деталей. Хотя она относительно простая, но при этом показывает некоторые преимущества HDR-рендеринга: никакие детали не теряются в светлых и темных областях, поскольку они могут быть восстановлены с помощью тональной компрессии; добавление нескольких источников света не вызывает проблем с сужением значений яркости областей, а значения освещенности могут быть заданы реальными значениями яркости, не ограниченными LDR-значениями. Кроме того, HDR-рендеринг также делает несколько других интересных эффектов более выполнимыми и реалистичными; один из этих эффектов — свечение, который мы обсудим на следующем уроке.
Дополнительные ресурсы
Does HDR rendering have any benefits if bloom won’t be applied? — вопрос на stackexchange, который содержит отличный наиболее полный ответ, описывающий некоторые преимущества HDR-рендеринга.
What is tone mapping? How does it relate to HDR? — еще один интересный ответ на stackexchange с большими эталонными изображениями для объяснения тональной компрессии.