Введение
Человеческий глаз обладает различным восприятием цветов и яркости: разница в цветах воспринимается линейно, а в яркости — нелинейно. Глаз более чувствителен к перепадам яркости в темных областях. Если яркость расположить линейно, то для человеческого глаза данная закономерность будет наблюдаться неравномерной. Сравнение яркостей представлено на рисунке 1.
На рисунке 1 можно отметить, что более привычной и правильной кажется верхняя строка — нелинейная. Физическая яркость, связанная с количеством фотонов света, является линейной.
Гамма-коррекция — это коррекция яркости цифрового изображения, которая используется для демонстрации изображений на устройствах вывода с нелинейной яркостной характеристикой.
На ЭЛТ-мониторах (мониторы с электронно-лучевой трубкой) была нелинейная зависимость между напряжением и яркостью пикселя: удваивание напряжения не приводило к удвоенному значению яркости, вместо этого изменение яркости происходит по экспоненциальному закону с гамма-коэффициентом 2.2.
Гамма-коэффициент — характеристика нелинейности — отношение между численным значением пикселя и его действительной светимостью.
Гамма-коррекцию можно описать степенной функцией:
Bin = A * Boutγ,
где A — коэффициент наклона графика, Bin и Bout — входные и выходные значения яркости, а γ — гамма-коэффициент.
На рисунке 2 рассматривается частный случай функции с коэффициентом A = 1, где по оси X расположились значения яркости на входе, а по оси Y — яркость на выходе. Три линии на графике: ожидаемая яркость (красный), гамма монитора (синий) и гамма-коррекция (зеленый).
Допустим, что красная линия будет ожидаемым значением цвета, то при применении гаммы монитора для значения яркости 0.5 результатом будет 0.218. Для компенсации данного изменения применяется гамма-коррекция, которая смещает значение яркости до 0.73. Данное значение после гаммы монитора будет иметь значение 0.5.
Для OpenGL существуют способа применить гамма-коррекцию к сцене:
- встроенная поддержка sRGB буфера кадра;
- ручная гамма-коррекция в фрагментном шейдере;
- угасание точечного источника.
Дополнительно рассматриваются sRGB текстуры и угасание точечного источника.
Встроенная поддержка sRGB буфера кадра
OpenGL позволяет включить встроенную реализацию гамма-коррекции с помощью параметра GL_FRAMEBUFFER_SRGB, но данный способ не позволяет управлять значением гамма-коэффициента. В данной реализации sRGB — цветовое пространство, которое примерно соответствует гамме 2.2.
Для включения встроенной реализации гамма-коррекции требуется вызвать функцию glEnable, передав ей параметр sRGB буфера кадра:
glEnable(GL_FRAMEBUFFER_SRGB);
Ручная гамма-коррекция в фрагментном шейдере
Второй способ позволяет производить ручную регулировку гамма-коррекции путем задания гамма-коэффициента и вычислений во фрагментном шейдере.
Для применения значения гамма-коррекции в фрагментном шейдере требуется возвести RBG каналы итогового цвета фрагмента в степень 1/γ. Для упрощения вычислений будем передавать значение в uniform-переменную inv_gamma, которая будет содержать заранее рассчитанное значение обратного гамма-коэффициенту.
Гамма коррекция будет применяться для трех шейдеров: расчета освещения, наложения текстур скайбокса и отладочного вывода лампочек.
В файле src/main.cpp объявим переменную inv_gamma и загрузим её обратное значение в unifrom-буфер (для простоты обмена между несколькими шейдерами):
// Значение гамма-коррекции
float inv_gamma = 1/2.2;
UBO gamma(&inv_gamma, sizeof(inv_gamma), 4);
Для шейдера освещения (shaders/lighting.frag) добавим unifrom-блок:
layout(std140, binding = 2) uniform Light
{
LightData data[64];
int count;
} light_f;
В конце функции main шейдера освещения модифицируем итоговое значение переменной color:
void main()
{
...
// Применение гамма-коррекции
color.rgb = pow(color.rgb, vec3(inv_gamma));
}
Аналогично для шейдера скайбокса (shaders/skybox.frag):
...
layout(std140, binding = 4) uniform gamma
{
float inv_gamma;
};
void main()
{
FragColor.rgb = pow(texture(skybox, TexCoords).rgb, vec3(inv_gamma));
gl_FragDepth = 0.9999f;
}
И для шейдера отладочной лампочки (shaders/bulb.frag):
layout(std140, binding = 4) uniform gamma
{
float inv_gamma;
};
...
void main()
{
float cosA = dot(normalize(pos_local), normalize(direction));
if (degrees(acos(cosA)) <= angle)
color = vec4(pow(ka, vec3(inv_gamma)), 1);
else
discard;
}
Итоговый результат представлен на рисунке 3.
На данный момент картинка пересвечена, потому что яркость фонового освещения выставлялась в без использования гамма-коррекции. Изменим характеристики материалов моделей на сцене:
...
scene.root.e_position().z = 1;
scene.models.begin()->material.kd = {0.5,0.5,0.5};
scene.models.begin()->material.ka = {0.05,0.05,0.05};
...
// Параметры материала
rectangle.material.ka = {0.05, 0.05, 0.05};
rectangle.material.kd = {1, 1, 1};
Результат с использованием новых материалов изображен на рисунке 4.
Текущая версия доступна на теге v0.1 в репозитории 15.
sRGB текстуры
По текстуре скайбокса можно заметить разницу в гамме для изображений, для большей наглядности на рисунке 5 очистим сцену оставив только скайбокс.
Данная ситуация происходит потому что изображение, используемое в качестве текстуры уже имеет примененную гамма-коррекцию и при рендере в приложении применяется вторая гамма-коррекция, которая пересвечивает изображение.
Для решения данной проблемы требуется изменить внутренний формат текстуры при загрузке с диска в конструкторах классов Texture и TextureCube на GL_SRGB (для трехканальной текстуры) и GL_SRGB_ALPHA (для четырехканальной текстуры) в файле src/Texture.cpp:
Texture::Texture(GLuint t, const std::string& filename)
{
...
if (image)
{
// Загрузка данных с учетом прозрачности
if (channels == 3) // RGB
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
else if (channels == 4) // RGBA
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB_ALPHA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
...
}
TextureCube::TextureCube(GLuint t, const std::string (&filename)[6])
{
...
// Если изображение успешно считано
if (image)
{
// Загрузка данных с учетом прозрачности
if (channels == 3) // RGB
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
else if (channels == 4) // RGBA
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_SRGB_ALPHA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image);
...
}
Правильный вывод скайбокса изображен на рисунке 6.
Текущая версия доступна на теге v0.2 в репозитории 15.
Угасание точечного источника
Как говорилось ранее в разделе «угасание источника с увеличением расстояния» 9 заметки корректная работа алгоритма расчета угасания будет только с использованием гамма-коррекции. На рисунке 7 представлено сравнение коэффициента угасания источника без и с гамма-коррекцией.
Сравнение света от источника (диффузная и зеркальные составляющие) с учетом угла падения и коэффициента угасания представлено на рисунке 8.
Вклад источника на границе сферы при применении гамма-коррекции на рисунке 8 равен 8/256 яркости цвета.
Дополнение
Помимо sRGB текстур необходимо учитывать, что материалы также записаны уже с учетом гамма-коррекции. Дополним функцию загрузчик loadOBJtoScene в файле src/Scene.cpp:
Scene loadOBJtoScene(const char* filename, const char* mtl_directory, const char* texture_directory)
{
Scene result;
Model model;
// Все модели образованные на основании этой модели будут иметь общего родителя
model.setParent(&result.root);
tinyobj::attrib_t attrib;
std::vector<tinyobj::shape_t> shapes;
std::vector<tinyobj::material_t> materials;
std::string err;
// Значение гамма-коррекции
extern float inv_gamma;
...
// Материал
s->material.ka = pow(glm::vec3(materials[materials_ids[i]].ambient[0], materials[materials_ids[i]].ambient[1], materials[materials_ids[i]].ambient[2]), glm::vec3(1/inv_gamma));
s->material.kd = pow(glm::vec3(materials[materials_ids[i]].diffuse[0], materials[materials_ids[i]].diffuse[1], materials[materials_ids[i]].diffuse[2]), glm::vec3(1/inv_gamma));
s->material.ks = glm::vec3(materials[materials_ids[i]].specular[0], materials[materials_ids[i]].specular[1], materials[materials_ids[i]].specular[2]);
s->material.p = (materials[materials_ids[i]].shininess > 0.0f) ? 1000.0f / materials[materials_ids[i]].shininess : 1000.0f;
}
return result;
}
Для доступа к переменной с инвертированной гаммой необходимо вынести переменную в глобальную область в файле src/main.cpp:
...
#define WINDOW_CAPTION "OPENGL notes on rekovalev.site"
// Значение гамма-коррекции
float inv_gamma = 1/2.2;
...
int main(void)
{
...
// Значение гамма-коррекции
float inv_gamma = 1/2.2;
UBO gamma(&inv_gamma, sizeof(inv_gamma), 4);
...
}
Сравнение будет проводится по цоколю отладочной лампочки с учетом вышеуказанной правки на рисунке 9.
По рисунку 9 можно заметить, что к левой части применяется гамма коррекция на цвета материалов, которые уже находятся в цветовом пространстве sRGB, а справа — данная коррекция аннулируется применением значения гаммы при загрузке модели (возведение в степень 1/inv_gamma).
Текущая версия доступна на теге v0.3 в репозитории 15.
Заключение
В данной заметке рассмотрены теория по гамма-коррекции, два способа реализации в приложениях с трехмерной графикой OpenGL, а так же изменен формат при загрузке текстур.
Проект доступен в публичном репозитории: 15
Библиотеки: dependencies
Ресурсы: resources