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

OpenGL 10: освещение ч.3 — различные виды источников освещения

Данная заметка описывает реализацию направленного и параллельного источников освещения

Введение

Согласно заметке об освещении в трехмерных приложениях существуют четыре вида источников света:

  • фоновое (ambient) — нулевая отметка при расчете освещения, обеспечивающая равномерное освещение при отсутствии других источников;
  • точечное (omni) — точка в пространстве, которая светит одинаково во все стороны;
  • прожектор (spot) — точечный источник, ограниченный конусом, задающим направление;
  • направленное (direct) — бесконечно далеко удаленная точка, лучи которой идут параллельно (упрощает расчет освещения) благодаря расстоянию до неё.

Фоновое и точечное реализованы в рамках первой части заметок об освещении в OpenGL.

Прожектор

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

Рисунок 1 — Прожектор

На рисунке 1 изображен угол angle, ограничивающий максимальное значение угла между векторами направления (direction) и обратным расположения источника относительно поверхности (L_vertex). Для всенаправленного источника (обычный точечный) значения угла будет равно 180°.

Для удобства восприятия пользователем в классе Light будет храниться двойноу угол angle, а при загрузке на шейдер будет отправляться половина.

Для задания конуса, ограничивающего область действия источника, необходимо добавить приватное поле и публичные методы доступа к нему для класса источника света: Light::angle (угол) в файле include/Lights.h:

// Источник света
class Light : public Node
{
    public:
...
        const float& c_angle() const; // Константный доступ к углу освещенности
        float& e_angle(); // Неконстантная ссылка для изменений угла освещенности
...
    private:
...
        float angle; // Угол полный освещенности
...
};

А так же добавить поле direction_angle к структуре LightData:

// Точечный источник света
struct LightData 
{
    alignas(16) glm::vec3 position; // Позиция
    alignas(16) glm::vec3 color; // Цвет 
    alignas(16) glm::vec3 attenuation; // Радиус действия источника, линейный и квадратичный коэф. угасания   
    alignas(16) glm::vec4 direction_angle; // Направление и половинный угол освещенности
};

Направление источника задается на основании кватерниона поворота родительского класса Node::rotation. Для перевода кватерниона в направление необходимо задать стандартное направление:

// Стандартное направление источника без поворота
#define DEFAULT_LIGHT_DIRECTION glm::vec4(0.0f, 0.0f, 1.0f, 0.0f)

Реализация методов доступа, а так же модификация конструктора и оператора присваиваниия:

// Конструктор без параметров
Light::Light() : Node(), index(-1), uploadReq(false), color(1.0f), radius(10.0f), angle(360.0f)
{
    
}

// Оператор  присваивания
Light& Light::operator=(const Light& other)
{
    // Проверка на самоприсваивание
    if (this != &other) 
    {
        index = other.index; // Переносим индекс
        uploadReq = other.uploadReq; // Необходимость загрузки 
        color = other.color;
        radius = other.radius;
        angle = other.angle;

        Node::operator=(other);
    }
    return *this;
}

// Константный доступ к углу освещенности
const float& Light::c_angle() const
{
    return angle;
}

// Неконстантная ссылка для изменений угла освещенности
float& Light::e_angle()
{
    uploadReq = true;

    return angle;
}

Тогда метод загрузки в буфер будет иметь следующий вид:

// Преобразует информацию об источнике в структуру LightData
void Light::toData()
{
    check_id(); // Проверка на работу с корректным индексом

    data[index].position = glm::vec3(result_transform[3]); // Позиция из матрицы трансформации
    data[index].color = color; // Цвет
    // Если радиус изменился
    if (data[index].attenuation.r != radius)
    {
        data[index].attenuation.r  = radius; // Радиус действия источника
        data[index].attenuation[1] = 4.5/radius;      // Линейный коэф. угасания
        data[index].attenuation[2] = 4 * data[index].attenuation[1] * data[index].attenuation[1]; // Квадратичный коэф. угасания
    }
    // Направление и угол источника
    data[index].direction_angle = glm::vec4( glm::normalize(glm::vec3(result_transform * DEFAULT_LIGHT_DIRECTION))
                                           , angle / 2 // Половинный угол для вычислений на шейдере
                                           );
}

В фрагментном шейдере, рассчитывающем освещенность фрагментов (shaders/lighting.frag), необходимо дополнить uniform-блок освещения и добавить проверку угла:

...
struct LightData
{
    vec3 position;
    vec3 color;
    vec3 attenuation;
    vec4 direction_angle;
};

layout(std140, binding = 2) uniform Light
{
    LightData data[300];
    int count;
} light_f;
...
void main() 
{
...
    // Переменные используемые в цикле:
    float acosA; // Косинус между вектором от поверхности к источнику и обратным направлением источника
...    
        // Проверка на дистанцию
        if (L_distance < light_f.data[i].radius)
        {
            // Нормирование вектора
            L_vertex = normalize(L_vertex);
            // арккосинус между вектором от поверхности к источнику и обратным направлением источника
            acosA = degrees(acos(dot(-L_vertex, normalize(light_f.data[i].direction_angle.xyz))));
            // Если угол меньше угла источника или угол источника минимален, то считаем освещенность
            if(acosA <= light_f.data[i].direction_angle.a) 
            {
                // Диффузная составляющая
                diffuse = max(dot(L_vertex, N), 0.0); // скалярное произведение с отсеканием значений < 0
...
                color += vec4(light_f.data[i].color*kd*diffuse  * attenuation, 1) 
                      +  vec4(light_f.data[i].color*ks*specular * attenuation, 1);
            }
...

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

Для отладочного вывода упростим задачу, рисуя только фрагменты, попадающие в конус. Из вершинного шейдера (shaders/bulb.vert) передадим в фрагментный информацию о локальном положении фрагмента:

...
out vec3 pos_local;

void main() 
{ 
    pos_local = pos;
    gl_Position = camera.projection * camera.view * model * vec4(pos, 1.0);
} 

В фрагментном шейдере (shaders/bulb.frag) добавим две uniform-переменные, которые заполняются в методе Bulb::render, и добавим проверку:


...
in vec3 pos_local;

out vec4 color;

uniform float angle;
uniform vec3 direction;

void main()
{  
    float cosA = dot(normalize(pos_local), normalize(direction));
    if (degrees(acos(cosA)) <= angle)
        color = vec4(ka, 1);
    else
        discard;
}

Важное замечание: вектор pos_local, выполняющий здесь роль L_vertex, берется без изменения для того, чтобы окрашивались только вершины, входящие в конус.

Ключевое слово discard отбрасывает обрабатываемый фрагмент.

Метод рисования отладочной лампочки в файле src/Lights.cpp будет иметь следующий вид:

// Рисование отладочных лампочек
void Light::render(ShaderProgram &shaderProgram, UBO &material_buffer)
{
    // Загрузка модели лампочки при первом вызове функции
    static Scene bulb = loadOBJtoScene("../resources/models/bulb.obj", "../resources/models/", "../resources/textures/");
    static Model sphere = genShpere(1, 16, &bulb.root);

    GLuint angle_uniform = shaderProgram.getUniformLoc("angle");
    GLuint direction_uniform = shaderProgram.getUniformLoc("direction");

    // Цикл по источникам света
    for (int i = 0; i < count; i++)
    {
        // Загрузим направление
        glUniform3fv(direction_uniform, 1, &data[i].direction_angle.x);
        // Угол для лампочки = 180 (рисуем целую модель)
        glUniform1f(angle_uniform, 180); // Зададим параметры материала сфере действия
        
        // Сдвиг на позицию источника
        bulb.root.e_position() = data[i].position;
        sphere.e_scale() = glm::vec3(data[i].attenuation.r); // Масштабирование сферы
        // Задание цвета
        bulb.models[0].material.ka = sphere.material.ka = data[i].color;

        // Вызов отрисовки
        bulb.render(shaderProgram, material_buffer);    

        // Угол для сферы (рисуем направленный конус)
        glUniform1f(angle_uniform, data[i].direction_angle.a); 

        // Рисование сферы покрытия источника в режиме линий
        glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
        sphere.render(shaderProgram, material_buffer);
        glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);
    }
}

Результат работы представлен на рисунке 2.

Рисунок 2 — Точечный синий источник и красный прожектор

На рисунке 2 можно заметить жесткую границу при прохождении отсекающего конуса.

Для её смягчения добавим вычисление интенсивности в фрагментном шейдере shaders/lighting.frag, если источник является прожектора:

...
    // Переменные используемые в цикле:
    float acosA; // Косинус между вектором от поверхности к источнику и обратным направлением источника
    float intensity; // Интенсивность для прожектора источника
...
                // Угасание с учетом расстояния
                attenuation = 1 / (1 + light_f.data[i].K[0] * L_distance + light_f.data[i].K[1] * L_distance * L_distance);

                // Если источник - прожектор, то добавим смягчение
                if (light_f.data[i].angle < 180)
                {
                    intensity = clamp((light_f.data[i].direction_angle.a - acosA) / 5, 0.0, 1.0);  
                    diffuse  *= intensity;
                    specular *= intensity;
                }
...

Здесь в знаменателе число 5 — значение угла между векторами направления источника и обратным расположения поверхности относительно источника, для которых будет рассчитываться угасание.

Результат смягчения границ представлен на рисунке 3.

Рисунок 3 — Прожектор с мягкими границами

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

Направленный источник освещения

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

Рисунок 4 — Направленный источник освещения

Для реализации такого источника добавим класс-синглтон Sun в файле include/Lights.h:

// Класс направленного источника освещения
class Sun
{
    public:
        static Sun& get(); // Доступ к синглтону
        static void upload(UBO& sun_data); // Загрузка данных об источнике в буфер
        
        const glm::vec3& c_direction() const; // Константный доступ к направлению лучей источника
        glm::vec3& e_direction(); // Неконстантная ссылка для изменений направления лучей источника

        const glm::vec3& c_color() const; // Константный доступ к цвету
        glm::vec3& e_color(); // Неконстантная ссылка для изменений цвета

    private:
        Sun(const glm::vec3 &direction = glm::vec3(0.0f, 1.0f, 0.0f), const glm::vec3 &color = glm::vec3(0.4f, 0.4f, 0.4f));
        
        alignas(16) glm::vec3 direction; // Направление лучей источника
        alignas(16) glm::vec3 color; // Цвет

        static Sun instance; // Экземпляр синглтона
        static bool uploadReq; // Необходимость загрузки в следствии изменений
};

Данный класс учитывает наличие изменений по параметрам источника и позволяет пропускать загрузку в uniform-буфер в случае, если изменений не было.

Реализация конструктора и методов в файле src/Lights.cpp:

// Конструктор направленного источника с параметрами направления и цвета
Sun::Sun(const glm::vec3 &dir, const glm::vec3 &c) : direction(dir), color(c)
{
    
}

// Доступ к синглтону
Sun& Sun::get()
{
    return instance;
}

// Загрузка данных об источнике на шейдер
void Sun::upload(UBO& sun_data)
{
    if (uploadReq)
    {
        sun_data.loadSub(&instance, sizeof(instance));

        uploadReq = false;
    }
}

// Константный доступ к направлению лучей источника
const glm::vec3& Sun::c_direction() const
{
    return instance.direction;    
}

// Неконстантная ссылка для изменений направления лучей источника
glm::vec3& Sun::e_direction()
{
    uploadReq = true;

    return instance.direction;
}

// Константный доступ к цвету
const glm::vec3& Sun::c_color() const
{
    return instance.color;
}

// Неконстантная ссылка для изменений цвета
glm::vec3& Sun::e_color() 
{
    uploadReq = true;

    return instance.color;
}

В фрагментном шейдере, рассчитывающем освещенность фрагмента (shaders/lighting.frag), добавим новый uniform-буфер:

layout(std140, binding = 3) uniform Sun
{
    vec3 direction;
    vec3 color;
} sun;

После расчета фоновой освещенности добавим вычисления для солнца, если его цвет не черный:

...
    // Фоновая освещенность
    color = vec4(ka, 1);

    // Расчет солнца, если его цвет не черный
    if (length(sun.color) > 0)
    {
        // Данные об источнике относительно фрагмента
        L_vertex = normalize(sun.direction);
        // Диффузная составляющая
        diffuse = max(dot(L_vertex, N), 0.0); // скалярное произведение с отсеканием значений < 0
        
        // Вектор половины пути
        H = normalize(L_vertex + Cam_vertex);
        // Зеркальная составляющая
        specular = pow(max(dot(H, N), 0.0), p*4); // скалярное произведение с отсеканием значений < 0 в степени p
        // Результирующий цвет с учетом солнца
        color += vec4(sun.color*kd*diffuse,  1) 
              +  vec4(sun.color*ks*specular, 1);
    }
...

Результат работы программы с серым (0.4, 0.4, 0.4) солнцем, направленным на координаты (0, -1, 0), представлен на рисунке 5.

Рисунок 5 — Пример работы программы с точечным синим источником, красным прожектором и бесконечно удаленным белым направленным источником

Возможна простая оптимизация: инвертировать вектор направления лучей, тогда в фрагментном шейдере не будет требоваться знак минус, а вектор падения лучей поменяет свой смысл на вектор направления на солнце. Для этого в файле src/Lights.h изменим начальное положение и в преть будем считать вектор direction направлением на источник от поверхности, а не наоборот:

        Sun(const glm::vec3 &direction = glm::vec3(0.0f, 1.0f, 0.0f), const glm::vec3 &color = glm::vec3(0.4f, 0.4f, 0.4f));

Тогда в фрагментном шейдере shaders/lighting.frag:

        L_vertex = normalize(sun.direction); // Без минуса

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

Заключение

В ходе работы над заметкой были созданы два новых типа источников освещения: прожектор и направленный, для направленного добавлено смягчение краев, прилегающих к ограничивающему конусу.

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

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

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

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