На этом уроке мы рассмотрим, что такое гамма и гамма-коррекция в OpenGL.
Гамма
После того, как мы закончим вычислять окончательные цвета всех пикселей сцены, нужно будет отобразить их на экране компьютера. В старые времена цифрового формирования изображений большинство мониторов производили построение картинки на своем экране при помощи электронно-лучевой трубки (сокр. «ЭЛТ»). Из-за чего они обладали одним отличительным свойством, а именно: удвоенное входное напряжение не приводило к удвоенному значению яркости. Вместо этого изменение яркости у них происходит по экспоненциальному закону с соотношением 2.2
, более известному как гамма монитора. И по случайному стечению обстоятельств данная особенность совпадает с тем, как воспринимает яркость человеческий глаз. Чтобы лучше понять смысл вышесказанных слов, взгляните на следующее изображение:
Верхняя строка выглядит как корректная, с точки зрения восприятия человеческим глазом, шкала яркости. Удвоение яркости (например, от 0.1
до 0.2
) действительно представляется так, как будто оно в два раза ярче, с хорошо видимыми последовательными различиями. Однако, когда мы говорим о физической яркости света, например, о количестве фотонов, испускаемых источником света, то физически правильную яркость отображает именно нижняя шкала. Но поскольку наши органы зрения воспринимают яркость по-разному (более восприимчивы к изменениям темных цветов), то это выглядит странно.
Так как человеческие глаза предпочитают воспринимать цвета яркости в соответствии с верхней шкалой, то мониторы (до сих пор) используют определенное соотношение мощности для отображения выходных цветов так, что яркость исходных физических цветов сопоставляется с нелинейными цветами яркости в верхней шкале.
Данное нелинейное сопоставление действительно дает более приятные результаты яркости для нас, но когда дело доходит до рендеринга графики, появляется одна проблема: все параметры цвета и яркости, которые мы настраиваем в наших приложениях, зависят от того, что мы видим на экране монитора, и поэтому все параметры яркости/цвета на самом деле являются нелинейными. Взгляните на следующий график:
Линия из серых точек описывает значения цвета/освещенности в линейном пространстве, а сплошная линия — это цветовое пространство, отображаемое экраном монитора. Если мы увеличим в 2 раза значение яркости цвета в линейном пространстве, то результатом действительно будет удвоенное значение яркости. Возьмем, к примеру, цветовой вектор света (0.5, 0.0, 0.0)
, который представляет собой полутемный красный свет. Если бы мы удвоили этот свет в линейном пространстве, то он стал бы (1.0, 0.0, 0.0)
, как вы можете видеть на описываемом графике. Однако, исходный цвет отображается на мониторе как (0.218, 0.0, 0.0)
. Вот тут-то и начинаются проблемы: как только мы удваиваем темно-красный свет в линейном пространстве, на мониторе он становится по факту более чем в 4.5 раза ярче!
До этого урока мы предполагали, что находимся в линейном пространстве, но на самом деле мы работали в отображаемом пространстве монитора, поэтому все переменные цвета и освещения, которые мы настроили, не были физически корректными, а просто выглядели таковыми именно на нашем мониторе. По этой причине мы (и художники в том числе) обычно устанавливаем значения освещения намного ярче, чем они должны быть (так как монитор затемняет их), что в результате делает большинство линейных пространственных вычислений некорректными. Обратите внимание, что монитор (ЭЛТ) и линейный график начинаются и заканчиваются в идентичных позициях; промежуточные значения между ними затемняются.
Поскольку настройка цветов происходит в зависимости от того, какая картинка отображается на экране монитора, то все промежуточные вычисления (освещения) в линейном пространстве не являются физически корректными. Это становится все более очевидным при использовании более продвинутых алгоритмов освещения — пример этого вы можете видеть на следующем рисунке:
Вы можете видеть, что с гамма-коррекцией (обновленные) значения цвета куда лучше сочетаются друг с другом, а более темные области теперь отображают гораздо больше деталей. В итоге, при помощи нескольких небольших модификаций, мы получаем улучшенное качество изображения.
Без грамотной коррекции гаммы монитора освещение будет выглядеть неправильно, и художникам будет трудно получить реалистичные и красивые результаты. Решение заключается в применении гамма-коррекции.
Гамма-коррекция
Идея гамма-коррекции состоит в том, чтобы применить инверсию гаммы монитора к конечному выходному цвету картинки перед её отображением на экране. Оглядываясь на предыдущий график гамма-кривой, мы видим еще одну пунктирную линию, обратную относительно гамма-кривой монитора. Мы умножаем каждый из линейных выходных цветов на эту обратную гамма-кривую (делая их ярче), и, как только цвета картинки будут отображены на экране компьютера, к ним применится гамма-кривая монитора, в результате чего итоговые цвета становятся линейными. Т.е. по факту мы осветляем промежуточные цвета для того, чтобы, как только монитор затемнит их, они сразу вернулись бы к нормальным значениям.
Приведем еще один пример. Допустим, мы снова имеем темно-красный цвет (0.5, 0.0, 0.0)
. Перед отображением этого цвета на мониторе мы сначала применяем кривую гамма-коррекции к значению цвета. Линейные цвета, отображаемые монитором, приблизительно масштабируются к степени с показателем 2.2
, а значит инверсия требует проведения масштабирования цветов с показателем степени 1/2.2
. Таким образом, гамма-скорректированный темно-красный цвет принимает значение (0.5, 0.0, 0.0)1/2.2 = (0.5, 0.0, 0.0)0.45 = (0.73, 0.0, 0.0)
. Затем скорректированные цвета поступают на экран монитора, и в результате цвет отображается следующим образом (0.73, 0.0, 0.0)2.2 = (0.5, 0.0, 0.0)
. Вы можете видеть, что с помощью гамма-коррекции монитор теперь отображает цвета такими, какими мы устанавливаем их в линейном пространстве приложения.
Примечание: Значение гаммы равное 2.2
является заданным по умолчанию значением, которое приблизительно оценивает среднюю гамму большинства мониторов. Цветовое пространство в результате применения данной гаммы называется цветовым sRGB-пространством (не 100% точным, но близким к нему). Каждый монитор имеет свои собственные гамма-кривые, но гамма-значение 2.2
дает хорошие результаты на большинстве используемых мониторов. По этой причине игры часто позволяют игрокам изменять настройки своей гаммы, поскольку для каждого отдельно взятого монитора она может немного варьироваться.
Есть два способа применить гамма-коррекцию к сцене:
Использовать встроенную в OpenGL поддержку sRGB-фреймбуфера.
Сделать гамма-коррекцию самостоятельно во фрагментном шейдере.
Первый вариант самый простой, но и дает нам меньше контроля. Задействовав GL_FRAMEBUFFER_SRGB
, мы сообщаем OpenGL, что каждая последующая команда отрисовки должна сначала произвести гамма-коррекцию цветов (из цветового sRGB-пространства), прежде чем сохранить их в цветовом буфере. sRGB — это цветовое пространство, которое примерно соответствует гамме 2.2
и является стандартом для большинства устройств. В результате включения опции GL_FRAMEBUFFER_SRGB
, OpenGL будет автоматически выполнять гамма-коррекцию после каждого запуска фрагментного шейдера для всех последующих фреймбуферов (включая фреймбуфер по умолчанию).
Включить опцию GL_FRAMEBUFFER_SRGB
так же просто, как и вызвать функцию glEnable():
1 |
glEnable(GL_FRAMEBUFFER_SRGB); |
С этого момента ваши визуализированные изображения будут иметь скорректированную гамму. Единственный момент, который мы должны иметь в виду, используя описываемый подход (и другие подходы тоже), заключается в том, что гамма-коррекция (также) преобразует цвета из линейного пространства в нелинейное пространство, поэтому очень важно выполнять гамма-коррекцию только на последнем (заключительном) этапе. Если мы произведем гамма-коррекцию наших цветов перед окончательным выводом картинки на экран, то все последующие операции с данными цветами будут работать с неверными значениями. Например, если мы используем несколько фреймбуферов, то есть смысл, чтобы промежуточные результаты, передаваемые между фреймбуферами, оставались в линейном пространстве, и только последний фреймбуфер применял гамма-коррекцию перед отправкой изображения на экран монитора.
Второй подход требует немного большей работы (в отличие от первого), но при этом дает нам полный контроль над операциями гамма-коррекции. Мы применяем гамма-коррекцию в конце каждого запуска соответствующего фрагментного шейдера, в результате чего итоговые цвета перед отправкой на монитор будут иметь скорректированную гамму:
1 2 3 4 5 6 7 8 9 10 |
void main() { // Делаем супер причудливое освещение в линейном пространстве [...] // Применяем гамма-коррекцию float gamma = 2.2; FragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma)); } |
Предпоследняя строка кода возводит каждый отдельный цветовой компонент переменной fragColor
в степень 1.0/gamma
, корректируя выходные данные цвета, полученные в результате запуска фрагментного шейдера.
Проблема с этим подходом заключается в том, что мы должны применить гамма-коррекцию к каждому фрагментному шейдеру, который вносит свой вклад в конечный результат. Если у нас есть дюжина фрагментных шейдеров для нескольких объектов, то мы должны добавить код гамма-коррекции к каждому из них. Более простым решением было бы ввести стадию постобработки в цикл рендеринга и применить гамма-коррекцию к обрабатываемому кадру в качестве заключительного шага, который нужно будет сделать только один раз.
Вышеприведенный фрагмент кода представляет собой техническую реализацию гамма-коррекции. Не слишком впечатляюще, но есть несколько дополнительных вещей, которые мы должны учитывать при выполнении данной операции.
sRGB-текстуры
Поскольку мониторы отображают цвета с примененной гаммой, то всякий раз, когда мы визуализируем, редактируем или рисуем картинку на своем компьютере, мы выбираем цвета на основе того, что видим на мониторе. Это фактически означает, что все изображения, которые мы создаем или редактируем, находятся не в линейном пространстве, а в sRGB-пространстве. Например, удвоение темно-красного цвета на нашем экране на основе воспринимаемой яркости не приведет к такому же удвоению красной составляющей.
В результате, когда художники текстур создают свои творения такими, какими они их видят, все значения цвета текстур находятся в sRGB-пространстве, поэтому, если мы собираемся использовать данные текстуры в нашем приложении рендеринга, мы должны это учитывать. До того, как мы узнали о гамма-коррекции, это не было для нас проблемой, потому что текстуры выглядели хорошо и в sRGB-пространстве, которое является тем же самым пространством, в котором мы работали; текстуры отображались точно так же, какими они и воспринимались, что было прекрасно. Однако теперь, когда мы отображаем все объекты в линейном пространстве, цвета текстур будут искажены, как показано на следующем рисунке:
Текстурное изображение слишком яркое, и это происходит потому, что на самом деле оно дважды подвергается гамма-коррекции! Ведь если задуматься об этом, то получается, что мы создаем изображение, зависящее от того, что видим на своем мониторе, применяя гамма-коррекцию к его цветам. Поскольку затем мы снова исправляем гамму, изображение получается слишком ярким.
Чтобы устранить эту проблему, мы должны убедиться, что создатели текстур работают в линейном пространстве. Однако, поскольку проще работать в sRGB-пространстве, и большинство инструментов даже не поддерживают должным образом линейное текстурирование, это, вероятно, не самое эффективное решение.
Другое решение состоит в том, чтобы повторно исправить или преобразовать sRGB-текстуры в линейное пространство, прежде чем делать какие-либо вычисления их цветовых значений. Мы можем выполнить это следующим образом:
1 2 |
float gamma = 2.2; vec3 diffuseColor = pow(texture(diffuse, texCoords).rgb, vec3(gamma)); |
Однако выполнение этого преобразования для каждой текстуры в sRGB-пространстве является довольно хлопотным занятием. К счастью, OpenGL предоставляет внутренние форматы текстур GL_SRGB
и GL_SRGB_ALPHA
.
Если мы создадим текстуру в OpenGL с любым из этих двух форматов sRGB-текстур, то OpenGL автоматически скорректирует цвета к линейному пространству, что позволит нам правильно работать в нем. Мы можем указать текстуру как sRGB-текстуру следующим образом:
1 |
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); |
Если вы также хотите включить альфа-компоненты в свою текстуру, то вам придется указать внутренний формат текстуры как GL_SRGB_ALPHA
.
Вы должны быть осторожны при указании ваших текстур в sRGB-пространстве, так как не все текстуры на самом деле будут находиться в нем. Текстуры, используемые для раскрашивания объектов (например, диффузные текстуры), почти всегда находятся в sRGB-пространстве. Текстуры, используемые для извлечения параметров освещения (например, карты отраженного цвета и карты нормалей), почти всегда находятся в линейном пространстве, поэтому, если вы настроите их как sRGB-текстуры, освещение будет выглядеть странно. Будьте осторожны и внимательно следите за тем, для каких текстур вы собираетесь использовать sRGB-тип.
С нашими диффузными текстурами, заданными в качестве sRGB-текстур, вы снова получите ожидаемый визуальный результат, но на этот раз гамма-коррекция проводится только один раз.
Затухание
Еще один нюанс, который возникает при использовании гамма-коррекции — это ослабление освещения. В реальном физическом мире освещение ослабевает обратно пропорционально квадрату расстояния до источника света. Если говорить простыми словами, то это означает, что сила света уменьшается с квадратом расстояния до источника света, как показано ниже:
1 |
float attenuation = 1.0 / (distance * distance); |
Однако, при использовании этого уравнения, эффект затухания обычно слишком силен, придавая световым пятнам небольшой радиус, который выглядит неправильным с точки зрения физики. По этой причине были использованы другие функции затухания (которые мы обсуждали на уроке о базовом освещении в OpenGL), которые дают гораздо больше контроля, в данном случае — линейный эквивалент:
1 |
float attenuation = 1.0 / distance; |
Линейный эквивалент без гамма-коррекции дает более правдоподобные результаты по сравнению с квадратичным вариантом, но когда мы включаем гамма-коррекцию, линейное затухание выглядит слишком слабым, и физически правильное квадратичное затухание внезапно дает лучшие результаты. На следующем рисунке показаны различия:
GitHub / Урок №31. Гамма-коррекция в OpenGL — Исходный код
Программа наглядно демонстрирует, как на самом деле применять все описываемые методы. Нажав пробел, мы переключаемся между гамма-скорректированной и нескорректированной сценами, причем обе сцены используют свои эквиваленты затухания.
Причина этого различия заключается в том, что функции затухания света изменяют яркость, и, поскольку мы не визуализировали нашу сцену в линейном пространстве, мы выбрали функции затухания, которые лучше всего выглядели на нашем мониторе, но не были физически правильными. Рассмотрим квадратичную функцию затухания: если бы мы использовали эту функцию без гамма-коррекции, то при отображении на мониторе функция затухания имела бы следующий вид: (1.0/distance 2)2.2
. Это создает гораздо большее ослабление по сравнению с тем, что мы первоначально ожидали. Это также объясняет то, почему линейный эквивалент имеет гораздо больше смысла без гамма-коррекции, поскольку он фактически принимает вид (1.0/distance) 2.2 = 1.0/distance2.2
, что намного больше напоминает его физический эквивалент.
Заключение
Подводя итог, можно сказать, что гамма-коррекция позволяет выполнять все наши расчеты шейдеров/освещения в линейном пространстве. Поскольку линейное пространство имеет смысл в физическом мире, то большинство физических уравнений теперь действительно дают хорошие результаты (например, реальное затухание света). Чем более совершенным становится ваше освещение, тем легче получить хорошие (и реалистичные) результаты с помощью гамма-коррекции. Именно поэтому рекомендуется окончательно настраивать параметры освещения только после непосредственного применения гамма-коррекции.
Дополнительные ресурсы
Что должен знать о гамме каждый программист — хорошо написанная статья Джона Новака для глубокого понимания гамма-коррекции.
www.cambridgeincolour.com — подробнее о гамме и гамма-коррекции.
blog.wolfire.com — пост Дэвида Розена о преимуществах гамма-коррекции в графическом рендеринге.
renderwonk.com — некоторые дополнительные практические соображения.
Шикарная работа, спасибо.
Спасибо большое 🙂