Рубрики
3D мир Программирование

OpenGL 15: гамма-коррекция

В заметке рассматриваются подходы к работе с гамма-коррекцией

Введение

Человеческий глаз обладает различным восприятием цветов и яркости: разница в цветах воспринимается линейно, а в яркости — нелинейно. Глаз более чувствителен к перепадам яркости в темных областях. Если яркость расположить линейно, то для человеческого глаза данная закономерность будет наблюдаться неравномерной. Сравнение яркостей представлено на рисунке 1.

Рисунок 1 — Сравнение яркости, воспринимаемой человеческим глазом, и физической яркости

На рисунке 1 можно отметить, что более привычной и правильной кажется верхняя строка — нелинейная. Физическая яркость, связанная с количеством фотонов света, является линейной.

Гамма-коррекция — это коррекция яркости цифрового изображения, которая используется для демонстрации изображений на устройствах вывода с нелинейной яркостной характеристикой.

На ЭЛТ-мониторах (мониторы с электронно-лучевой трубкой) была нелинейная зависимость между напряжением и яркостью пикселя: удваивание напряжения не приводило к удвоенному значению яркости, вместо этого изменение яркости происходит по экспоненциальному закону с гамма-коэффициентом 2.2.

Гамма-коэффициент — характеристика нелинейности — отношение между численным значением пикселя и его действительной светимостью.

Гамма-коррекцию можно описать степенной функцией:
Bin = A * Boutγ,
где A — коэффициент наклона графика, Bin и Bout — входные и выходные значения яркости, а γ — гамма-коэффициент.

На рисунке 2 рассматривается частный случай функции с коэффициентом A = 1, где по оси X расположились значения яркости на входе, а по оси Y — яркость на выходе. Три линии на графике: ожидаемая яркость (красный), гамма монитора (синий) и гамма-коррекция (зеленый).

Рисунок 2 — Степенные функции для стандартной гаммы 2.2

Допустим, что красная линия будет ожидаемым значением цвета, то при применении гаммы монитора для значения яркости 0.5 результатом будет 0.218. Для компенсации данного изменения применяется гамма-коррекция, которая смещает значение яркости до 0.73. Данное значение после гаммы монитора будет иметь значение 0.5.

Для OpenGL существуют способа применить гамма-коррекцию к сцене:

Дополнительно рассматриваются 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.

Рисунок 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.

Рисунок 4 — Сцена с обновленными материалами

Текущая версия доступна на теге v0.1 в репозитории 15.

sRGB текстуры

По текстуре скайбокса можно заметить разницу в гамме для изображений, для большей наглядности на рисунке 5 очистим сцену оставив только скайбокс.

Рисунок 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.

Рисунок 6 — Правильный вывод скайбокса

Текущая версия доступна на теге v0.2 в репозитории 15.

Угасание точечного источника

Как говорилось ранее в разделе «угасание источника с увеличением расстояния» 9 заметки корректная работа алгоритма расчета угасания будет только с использованием гамма-коррекции. На рисунке 7 представлено сравнение коэффициента угасания источника без и с гамма-коррекцией.

Рисунок 7 — Сравнение коэффициентов угасания (белый цвет) источника без и с гамма-коррекцией

Сравнение света от источника (диффузная и зеркальные составляющие) с учетом угла падения и коэффициента угасания представлено на рисунке 8.

Рисунок 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 — Сравнение гамма коррекции на материалах модели

По рисунку 9 можно заметить, что к левой части применяется гамма коррекция на цвета материалов, которые уже находятся в цветовом пространстве sRGB, а справа — данная коррекция аннулируется применением значения гаммы при загрузке модели (возведение в степень 1/inv_gamma).

Текущая версия доступна на теге v0.3 в репозитории 15.

Заключение

В данной заметке рассмотрены теория по гамма-коррекции, два способа реализации в приложениях с трехмерной графикой OpenGL, а так же изменен формат при загрузке текстур.

Проект доступен в публичном репозитории: 15
Библиотеки: dependencies
Ресурсы: resources

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.