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

OpenGL 11: тени ч.1 — тени от направленного источника

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

Введение

Теорию для данной заметки можно изучить в заметке посвященной освещению в трехмерных приложениях.

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

Основной идеей для расчета теней является выявление объектов, закрывающих остальные от источника света — стоящих на переднем плане с точки зрения источника. Для решения данной задачи используется буфер глубины (Z-буфер).

Алгоритм проецирования теней в данной заметке не рассматривается, так как он устарел и годится только для проецирования на плоскость.

Содержание заметки:

Карты теней

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

В результате рендера сцены в текстуру буфера с использованием полученной матрицы вида-проекции источника получается карта теней, которая подключается на этапе расчетов освещения. Для определения затененности точка переводится в пространство источника и производится сравнение значений глубины: если больше, чем в карте теней — точка затемнена.

Буфер, используемый для расчета теней, не требует привязок текстур, так что можно модифицировать конструктор класса FBO, задав значения по умолчанию в файле include/Buffers.h:

FBO(GLuint *attachments = 0, int count = 0); // Создает буфер кадра с нужным числом прикреплений текстур

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

class Camera
{
    public:
...
        std::pair<bool, const glm::vec4*> getProjCoords(); // Доступ к координатам с флагом изменения, описывающим пространство вида с пересчетом, если это требуется
    protected:
...
        bool requiredRecalcCoords; // Необходимость пересчета точек, описывающих пространство камеры
        glm::vec4 coords[8]; // Координаты, описывающие пространство камеры 
};

Для расчета точек, описывающих объем пространства вида, ограниченного матрицей проекции и трансформирования используется метод Camera::getProjCoords в совокупности с приватным полем Camera::requiredRecalcCoords, определяющем необходимость пересчета точек. Для определения точек в пространстве используется обратная матрица произведения проекции на вида, получаемая с помощью функции glm::inverse и метода Camera::getVP. Данная матрица умножается на «типовые точки», расположенные по краям экрана, а результатом являются точки в мировых координатах.

Дополним конструктор копирования и изменим метод пересчета матриц в классе Camera::recalcMatrices и реализуем метод вычисления и доступа к точкам в файле src/Camera.cpp:

// Конструктор копирования камеры
Camera::Camera(const Camera& copy) 
: Node(copy), projection(copy.projection), requiredRecalcVP(copy.requiredRecalcVP), sensitivity(copy.sensitivity),
requiredRecalcCoords(true)
{
    // Если у оригинала не было изменений - перепишем матрицу вида-проекции
    if (!requiredRecalcVP)
        vp = copy.vp;
}

// Метод пересчета матрицы вида и произведения Вида*Проекции по необходимости, должен сбрасывать флаг changed
void Camera::recalcMatrices()
{
    if (changed || parent_changed)
    {
        glm::vec3 _position = position;
        glm::quat _rotation = rotation;
        if (parent) // Если есть родитель
        {
            glm::mat4 normalized_transform = parent->getTransformMatrix();
            for (int i = 0; i < 3; i++) 
            {
                glm::vec3 axis = glm::vec3(normalized_transform[i]);
                normalized_transform[i] = glm::vec4(glm::normalize(axis), normalized_transform[i].w);
            }
            glm::vec4 tmp = normalized_transform * glm::vec4(_position, 1.0f);
            tmp /= tmp.w;
            _position = glm::vec3(tmp);
            _rotation = glm::quat_cast(normalized_transform) * _rotation;
        }
        glm::mat4 rotationMatrix = glm::mat4_cast(glm::conjugate(_rotation));
        glm::mat4 translationMatrix = glm::translate(glm::mat4(1.0f), -_position);
        view = rotationMatrix * translationMatrix;
        requiredRecalcVP = true;
    }
  
    Node::recalcMatrices();

    if (requiredRecalcVP)
    {
        vp = projection * view;
        requiredRecalcCoords = true; // Требуется пересчитать точки пространства камеры
        requiredRecalcVP = false; // Изменения применены
    }
}

// Доступ к координатам с флагом изменения, описывающим пространство вида с пересчетом, если это требуется
std::pair<bool, const glm::vec4*> Camera::getProjCoords() 
{
    const glm::mat4& cam_vp = getVP(); // Получение ссылки на матрицу вида-проекции с пересчетом, если требуется и активацией флага requiredRecalcCoords
    bool changes = false; // Возвращаемое значение

    if (requiredRecalcCoords)
    { 
        // Инверсия матрицы вида/проекции камеры
        glm::mat4 inv = glm::inverse(cam_vp);
        // Типовые точки, описывающие пространство 
        glm::vec4 typical_points[8] = {  { 1, 1, 1,1}
                                       , { 1, 1,-1,1}
                                       , { 1,-1, 1,1}
                                       , { 1,-1,-1,1}
                                       , {-1, 1, 1,1}
                                       , {-1, 1,-1,1}
                                       , {-1,-1, 1,1}
                                       , {-1,-1,-1,1}};
        // Цикл по типовым точкам
        for (int i = 0; i < 8; i++)
        {
            coords[i] = inv * typical_points[i];
            coords[i] /= coords[i].w; // Переход от гомогенных координат к обычным
        }

        requiredRecalcCoords = false; // Сбрасываем флаг
        changes = true;
    }

    return std::make_pair(changes, coords); 
}

Примечание: при получении ссылки на матрицу вида-проекции камеры вызывается метод Camera::recalcMatrices, который в случае пересчета матрицы активирует флаг requiredRecalcCoords, а далее исходя из состояния флага производится вычисление точек.

На основании полученных точек можно рассчитать матрицу проекции и вида для направленного источника света. Для этого добавим матрицу Sun::vp и вспомогательный метод Sun::getVP в файле include/Lights.h:

class Sun
{
    public:
        static Sun& get(); // Доступ к синглтону
        static void upload(UBO& sun_data); // Загрузка данных об источнике в буфер

    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; // Цвет
        alignas(16) glm::mat4 vp; // Матрица вида-проекции источника

        void recalcVP(); // Пересчитывает по необходимости матрицу вида-проекции
        
        static Sun instance; // Экземпляр синглтона
        static bool uploadReq; // Необходимость загрузки в следствии изменений
};

Реализация метода Sun::vp в файле src/Lights.cpp:

// Пересчитывает по необходимости матрицу вида-проекции
void Sun::recalcVP()
{
    std::pair <bool, const glm::vec4*> camProjCoords = Camera::current().getProjCoords();

    // Есть изменения по источнику или камере
    if (uploadReq || camProjCoords.first)
    {
        uploadReq = true; // Требуется загрузка в следствии пересчета матрицы

        glm::vec3 mean = glm::vec3(0); // Среднее арифметическое
        glm::vec4 max, min; // макс и мин координаты
        glm::vec4 point; // Точка приведенная в пространство источника света

        // Найдем среднее арифметическое от точек для нахождения центра прямоугольника
        for (int i = 0; i < 8; i++)
            mean += glm::vec3(camProjCoords.second[i]);
        mean /= 8;
        // Используем среднее арифметическое для получения матрицы вида параллельного источника
        glm::mat4 lightView = glm::lookAt(mean + glm::normalize(direction), mean, CAMERA_UP_VECTOR);
        
        // Примем первую точку как минимальную и максимальную (приведя в пространство вида источника)
        min = max = lightView * camProjCoords.second[0];
        // Для оставшихся точек
        for (int i = 1; i < 8; i++)
        {
            // Приведем в пространство вида источника
            point = lightView * camProjCoords.second[i];
            max = glm::max(max, point);
            min = glm::min(min, point);
        }

        // Максимальное значение глубины
        max.z = std::max(fabs(max.z), fabs(min.z));
        // На основании максимальных и минимальных координат создадим матрицу проекции источника
        vp = glm::ortho(min.x, max.x, min.y, max.y, min.z, max.z) * lightView;
    }
}

Данный метод следует вызвать внутри метода Sun::upload:

// Загрузка данных об источнике на шейдер
void Sun::upload(UBO& sun_data)
{
    instance.recalcVP(); // Пересчет матрицы вида-проекции источника по необходимости (влияет на флаг uploadReq)

    if (uploadReq)
    {
        sun_data.loadSub(&instance, sizeof(instance));

        uploadReq = false;
    }
}

Добавим вершинный шейдер shaders/sun_shadow.vert для вычисления буфера глубины относительно источника освещения:

#version 420 core

layout (location = 0) in vec3 pos;

uniform mat4 model;

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

void main()
{
    gl_Position = sun.vp * model * vec4(pos, 1.0);
}

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

Создадим буфер, текстуру с разрешением 1024px (sunShadow_resolution) и загрузим шейдер для расчета теней в файле src/main.cpp:

    // Размер текстуры тени от солнца
    const GLuint sunShadow_resolution = 1024;
    // Создадим буфер кадра для рендера теней
    FBO sunShadowBuffer;
    // Создадим текстуры для буфера кадра
    Texture sunShadowDepth(sunShadow_resolution, sunShadow_resolution, GL_DEPTH_ATTACHMENT, 4, GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT);
    // Отключим работу с цветом
    glDrawBuffer(GL_NONE);
    glReadBuffer(GL_NONE);
    // Активируем базовый буфер кадра
    FBO::useDefault();

    // Шейдер для расчета теней
    ShaderProgram sunShadowShader;
    // Загрузим шейдер
    sunShadowShader.load(GL_VERTEX_SHADER, "shaders/sun_shadow.vert");
    sunShadowShader.link();

Важное замечание: текстура теней должна иметь internalFormat и format в значениях GL_DEPTH_COMPONENT.

Примечание: размеры текстуры теней не изменяются при изменении размеров окна.

Добавим работу с новым буфером, включая отправку матрицы проекции и трансформации в пространство источника, в цикл рисования после формирования g-буфера (перед работой с основным буфером кадра):

...
        // Активируем буфер кадра для теней от солнца
        sunShadowBuffer.use();
        // Подключим шейдер для расчета теней
        sunShadowShader.use();
        // Очистка буфера глубины
        glClear(GL_DEPTH_BUFFER_BIT);
        // Рендерим геометрию в буфер глубины
        scene.render(sunShadowShader, material_data);  
...

Так как рендер происходит в текстуру с разрешением окна, которое может отличаться, необходимо изменить параметры окна просмотра с помощью функции glViewport перед рендером теневой карты и после:

...
        // Изменим размер вывода для тени
        glViewport(0, 0, sunShadow_resolution, sunShadow_resolution);
        // Активируем буфер кадра для теней от солнца
        sunShadowBuffer.use();
        // Подключим шейдер для расчета теней
        sunShadowShader.use();
...
        // Изменим размер вывода для окна
        glViewport(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT);
        // Активируем базовый буфер кадра
        FBO::useDefault();
        // Подключаем шейдер для прямоугольника
        lightShader.use();
...

Теперь можно привязать новую текстуру к шейдеру расчета освещения. Добавим uniform-переменную для текстуры и исправим uniform-буфер в файле shaders/lights.frag:

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

uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D gDiffuseP;
uniform sampler2D gAmbientSpecular;
uniform sampler2D sunShadowDepth;
...

В файле src/main.cpp при инициализации uniform-переменных для текстур добавим карту теней:

     const char* gtextures_shader_names[]  = {"gPosition", "gNormal", "gDiffuseP", "gAmbientSpecular", "sunShadowDepth"};
     lightShader.bindTextures(gtextures_shader_names, sizeof(gtextures_shader_names)/sizeof(const char*));

В цикле рисования дополним блок с вычислением освещенности:

...
         gNormal.use();
         gDiffuseP.use();
         gAmbientSpecular.use();
         // Подключаем текстуры теней
         sunShadowDepth.use();
         // Загружаем информацию о направленном источнике
         sun.upload(lightShader);
        // Рендерим прямоугольник с расчетом освещения
        quadModel.render();
...

На данный момент почти все готово для расчета теней за исключением сцены, на которой есть один объект, которому не на что проецировать тени. Создадим прямоугольник, на который будут проецироваться тени:

    // Модель прямоугольника
    Model rectangle; 

    // Вершины прямоугольника
    glm::vec3 rectangle_verticies[] = {  {-0.5f, -0.5f, 0.0f}
                                       , { 0.5f, -0.5f, 0.0f}
                                       , { 0.5f,  0.5f, 0.0f}
                                       , {-0.5f,  0.5f, 0.0f}
                                    };
    // Загрузка вершин модели
    rectangle.load_verteces(rectangle_verticies, sizeof(rectangle_verticies)/sizeof(glm::vec3));

    // индексы вершин
    GLuint rectangle_indices[] = {0, 1, 2, 2, 3, 0}; 
    // Загрузка индексов модели
    rectangle.load_indices(rectangle_indices, sizeof(rectangle_indices)/sizeof(GLuint));

    // Нормали
    glm::vec3 rectangle_normals[] = {  {0.0f, 0.0f, -1.0f}
                                     , {0.0f, 0.0f, -1.0f}
                                     , {0.0f, 0.0f, -1.0f}
                                     , {0.0f, 0.0f, -1.0f}
                                    };
    // Загрузка нормалей модели
    rectangle.load_normals(rectangle_normals, sizeof(rectangle_normals)/sizeof(glm::vec3));

    // Зададим горизонтальное положение перед камерой
    rectangle.e_position().y = -1;
    rectangle.e_position().z = 2;
    rectangle.e_rotation().x = 90;
    rectangle.e_scale() = glm::vec3(4);

    // Параметры материала
    rectangle.material.ka = {0.4, 0.4, 0.4};
    rectangle.material.kd = {0.4, 0.4, 0.4};

Данный прямоугольник можно добавить к существующей сцене (scene), либо использовать как отдельный объект с индивидуальными вызовами рендера. Автор использует второй вариант, добавляя вызов рендера прямоугольника в процессе расчета g-буфера и карты теней:

...
        // Тут производится рендер
        scene.render(gShader, material_data);
        rectangle.render(gShader, material_data);

        // Изменим размер вывода для тени
        glViewport(0, 0, sunShadow_resolution, sunShadow_resolution);
        // Активируем буфер кадра для теней от солнца
        sunShadowBuffer.use();
        // Подключим шейдер для расчета теней
        sunShadowShader.use();
        // Очистка буфера глубины
        glClear(GL_DEPTH_BUFFER_BIT);
        // Рендерим геометрию в буфер глубины
        scene.render(sunShadowShader, material_data);
        rectangle.render(sunShadowShader, material_data);
...

На данный момент источник расположен в зените — находится строго сверху над сценой из-за чего объекты не отбрасывают тень. Для решения данной проблемы перед циклом сдвинем источник по оси Z в координаты (0.0;0.0;-1.0), а также для удобства можно передвинуть камеру источника на 0.3 по оси X:

    // Сдвинем направленный источник света и камеру
    Sun::get().e_direction().z = -1.0;
    Camera::current().e_position().x = 0.3f;

Для вычисления теней добавим три переменные в файле фрагментного шейдера, занимающегося расчетом освещенности, shaders/lights.frag:

    vec3 fragPosLightSpace; // Фрагмент в пространстве источника
    float closestDepth; // Значение глубины из буфера тени
    float shadowValue; // Значение затененности

Перед вычислением освещенности фрагмента добавим проверку на значение тени, которое вычисляется приведением координат фрагмента в пространство источника. Координаты по осям XY нужно дополнительно привести к диапазону [0;1] для работы с текстурой. Используя данные координаты на текстуре теневой карты можно получить значение глубины, которое будет сравниваться с координатой на оси Z. Изменения имеют следующий вид:

    // Расчет солнца, если его цвет не черный
    if (length(sun.color) > 0)
    {
        // Расположение фрагмента в координатах теневой карты
        fragPosLightSpace = (sun.vp * vec4(fragPos, 1.0)).xyz;
        // Переход от [-1;1] к [0;1]
        fragPosLightSpace = (fragPosLightSpace + vec3(1.0)) / 2;
        // Получим значение ближайшей глубины к источнику
        closestDepth = texture(sunShadowDepth, fragPosLightSpace.xy).r; 
        // Проверим, что рассматриваемый фрагмент ближе чем значение глубины
        shadowValue = fragPosLightSpace.z > closestDepth ? 1.0 : 0.0;
        // Рассчитываем освещенность, если значение тени меньше 1
        if (shadowValue < 1.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);
        }
    }

Результат расчета таких теней представлен на рисунке 1.

Рисунок 1 — Тени с акне

Эффект представленный на рисунке 1 называется теневым акне (shadow acne). Такой эффект возникает из-за особенностей растеризации текстуры на ограниченном разрешении. Артефакты усиливаются с возрастанием угла между углом падения луча и нормалью. Пример данной проблемы представлен на рисунке 2.

Рисунок 2 — Причина возникновения теневого акне

Желтыми ступеньками на рисунке 2 обозначены сдвиги, получаемые при растеризации текстуры теней. Для исправления данной проблемы вводится малый сдвиг, который вычитается из координаты Z рассчитываемого фрагмента в пространстве источника.

Так как величина артефактов зависит от угла падения, сдвиг можно вычислить используя выражение:
bias = 1.0 - N • L_{direction},
где N — нормаль к поверхности, а L_{direction} — угол падения лучей источника.

При использовании сдвига больше 5% (0.05) может возникнуть проблема, называемая Peter Panning (персонаж сказки у которого тень была отделена) — отделение тени от объекта. Во избежание нового типа артефакта добавим ограничение в 5% для сдвига:

...
        // Получим значение ближайшей глубины к источнику
        closestDepth = texture(sunShadowDepth, fragPosLightSpace.xy).r; 
        // Сдвиг для решения проблемы акне
        fragPosLightSpace.z -= max(0.05 * (1.0 - dot(N, sun./direction)), 0.005);
        // Проверим, что рассматриваемый фрагмент ближе чем значение глубины
        shadowValue = fragPosLightSpace.z > closestDepth ? 1.0 : 0.0;
...

Результат исправления эффекта теневого акне представлен на рисунке 3.

Рисунок 3 — Тени малого разрешения

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

  • минимальные координаты (-66.667;-50;0.1);
  • максимальные координаты (66.667;50;100).

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

Пример низкой детализации на текстуре теневой карты представлен на рисунке 4.

Рисунок 4 — Теневая карта с низкой детализацией

Первым решением может быть приближение дальней плоскости отсечения перспективной проекции камеры в файле include/Camera.h:

#define CAMERA_FAR 15.0f

Важное замечание: при изменении .h файлов make не заметит изменений, так что может потребоваться полная пересборка make clean && make

Вторым решением является использование каскадных карт теней, которым посвящен следующий раздел заметки.

Содержимое текстуры теневой карты после уменьшения объема усеченной пирамиды перспективной проекции (первый способ) изображен на рисунке 5.

Рисунок 5 — Теневая карта с большей детализацией за счет уменьшения области камеры

Пример использования такой теневой карты представлен на рисунке 6.

Рисунок 6 — Результат использования теневой карты с рисунка 5

Результат на рисунке 6 все ещё является неудовлетворительным. Для сглаживания результата применятся PCF (Percentage-closer filtering) — фильтрация на процент ближе.

    vec2 texelSize = 1.0 / textureSize(sunShadowDepth, 0); // Размер текселя текстуры теней
    int x, y; // Счетчик для PCF
    float pcfDepth; // Глубина PCF

Далее запускаются два цикла с некоторым отступом (для примера 1) по осям XY, в которых берется значение текстуры карты теней с учетом отступа, который суммируется в общую тень:

        
        // Получим значение ближайшей глубины к источнику
        closestDepth = texture(sunShadowDepth, fragPosLightSpace.xy).r; 
        // Проверим, что рассматриваемый фрагмент ближе чем значение глубины
        shadowValue = fragPosLightSpace.z > closestDepth ? 1.0 : 0.0;
        // Проверка PCF
        shadowValue = 0.0;
        for(x = -1; x <= 1; ++x)
        {
            for(y = -1; y <= 1; ++y)
            {
                pcfDepth = texture(sunShadowDepth, fragPosLightSpace.xy + vec2(x, y) * texelSize).r; 
                shadowValue += fragPosLightSpace.z > pcfDepth  ? 1.0 : 0.0;        
            }
        }

Примечание: переменную closestDepth можно удалить.

Результат вычислений необходимо поделить на количество рассмотренных в цикле точек (для примера 9):

        shadowValue /= 9.0;

Данное значение будет находится в диапазоне [0;1] и определять затененность объекта, которую необходимо использовать при расчете освещенности:

        if (shadowValue < 1.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) ) * (1.0 - shadowValue);
        }

Примечание: фильтрацию можно выполнять с большим числом точек, что обеспечит большее сглаживание теней, например 5 точек (отступ 2, деление на 25).

Результат использования фильтрации теней представлен на рисунке 7.

Рисунок 7 — Тени с применением PCF

Так как в OpenGL по умолчанию для текстур стоит режим повторения (GL_REPEAT) при выходе текстурных координат за пределы [0;1] (см. рис. 2), то может возникнуть фантомная тень от объекта, попавшего в область проекции-вида источника. Для решения данной проблемы достаточно задать режим привязки к границе заданного цвета (GL_CLAMP_TO_BORDER) при создании текстуры карты в файле src/main.cpp:

    // Создадим текстуры для буфера кадра
    Texture sunShadowDepth(sunShadow_resolution, sunShadow_resolution, GL_DEPTH_ATTACHMENT, 4, GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT); 
    // Правка фантомных теней
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    float shadowBorderColor[] = { 1.0, 1.0, 1.0, 1.0 };
    glTexParameterfv(GL_TEXTURE_2D, GL_TEXTURE_BORDER_COLOR, shadowBorderColor);

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

Модификация класса текстуры

Для реализации каскадных карт теней потребуется 3D текстура, для работы с которой необходимо создать новый класс TextureArray, который в целом повторяет поведение оригинального класса Texture. Во избежание повторений в коде добавим абстрактный класс базовый класс BaseTexture в файл include/Texture.h, который требует от потомков реализации метода BaseTexture::use. Дополнительно убрано обязательное использование перечисления (enum) TexType.

Определение класса BaseTexture:

// Абстрактный класс базовой текстуры
class BaseTexture
{
    public:
        ~BaseTexture();
        virtual void use() = 0; // Привязка текстуры
        static void disable(GLuint type); // Отвязка текстуры по типу
        GLuint getType(); // Возвращает тип текстуры
        void setType(GLuint type); // Задает тип текстуры
    protected:
        GLuint handler; // Дескриптор текстуры
        GLuint type; // Тип текстуры, соответствует её слоту
        static std::map<std::string, int> filename_handler; // Получение дескриптора текстуры по её имени
        static std::map<int, int> handler_count; // Получение количества использований по дескриптору текстуры (Shared pointer)
};

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

// Класс 2D текстуры
class Texture : public BaseTexture
{
    public:
        Texture(GLuint type = TEX_AVAILABLE_COUNT, const std::string& filename = ""); // Загрузка текстуры с диска или использование "пустой"
        Texture(GLuint width, GLuint height, GLuint attachment, GLuint texType = TEX_DIFFUSE, GLint internalformat = GL_RGBA, GLint format = GL_RGBA, GLenum dataType = GL_FLOAT); // Конструктор текстуры заданного размера для использования в буфере
        Texture(const Texture& other); // Конструктор копирования

        Texture& operator=(const Texture& other); // Оператор присваивания

        void reallocate(GLuint width, GLuint height, GLuint texType = TEX_DIFFUSE, GLint internalformat = GL_RGBA, GLint format = GL_RGBA, GLenum dataType = GL_FLOAT); // Пересоздает текстуру для имеющегося дескриптора

        virtual void use(); // Привязка текстуры       
};

Теперь необходимо переместить статические поля и реализации методов к базовому классу в файле src/Texture.cpp:

...
std::map<std::string, int> BaseTexture::filename_handler; // Получение дескриптора текстуры по её имени
std::map<int, int> BaseTexture::handler_count; // Получение количества использований по дескриптору текстуры (Shared pointer)
...
// Загрузка текстуры с диска или использование "пустой"
Texture::Texture(GLuint t, const std::string& filename) : type(t)
{
    type = t;
...
// Конструктор текстуры заданного размера для использования в буфере
Texture::Texture(GLuint width, GLuint height, GLuint attachment, GLuint texType, GLint internalformat, GLint format, GLenum dataType) : type(texType)
{
    type = texType;
...
// Конструктор копирования
Texture::Texture(const Texture& other) : handler(other.handler), type(other.type)
{
    handler = other.handler;
    type = other.type;
...
BaseTexture::~BaseTexture()
...
void BaseTexture::disable(TexType type)

...
TexType BaseTexture::getType()

...
void BaseTexture::setType(GLuint type)
...

Важное замечание: из-за изменения принадлежности метода классу может потребоваться полная пересборка make clean && make

Теперь можно добавить новый класс TextureArray в файле include/Texture.h:

// Класс 3D текстуры
class TextureArray : public BaseTexture
{
    public:
        TextureArray(GLuint levels, GLuint width, GLuint height, GLuint attachment, GLuint texType = TEX_DIFFUSE, GLint internalformat = GL_RGBA, GLint format = GL_RGBA, GLenum dataType = GL_FLOAT); // Конструктор текстуры заданного размера для использования в буфере
        TextureArray(const TextureArray& other); // Конструктор копирования

        TextureArray& operator=(const TextureArray& other); // Оператор присваивания

        void reallocate(GLuint levels, GLuint width, GLuint height, GLuint texType = TEX_DIFFUSE, GLint internalformat = GL_RGBA, GLint format = GL_RGBA, GLenum dataType = GL_FLOAT); // Пересоздает текстуру для имеющегося дескриптора

        virtual void use(); // Привязка текстуры       
};

В трехмерных текстурах используется функция glTexImage3D, которая ожидает следующие параметры:

  • target — предназначение текстуры (в заметках будет использоваться GL_TEXTURE_2D_ARRAY);
  • level — уровень детализации, который позволяет загрузить разные уровни mipmap текстуры при значениях больше 0;
  • internalformat — определяет количество цветовых каналов текстуры:
    • GL_RED,
    • GL_RG,
    • GL_RGB,
    • GL_RGBA,
    • GL_DEPTH_COMPONENT,
    • GL_DEPTH_STENCIL;
  • width — ширина изображения;
  • height — высота изображения;
  • depth — количество слоев (глубина) изображения;
  • border — «значение должно равняться нулю» (OpenGL-Refpages);
  • format — формат пикселя загружаемого изображения:
    • GL_RED,
    • GL_RG,
    • GL_RGB,
    • GL_BGR,
    • GL_RGBA,
    • GL_BGRA,
    • GL_RED_INTEGER,
    • GL_RG_INTEGER,
    • GL_RGB_INTEGER,
    • GL_BGR_INTEGER,
    • GL_RGBA_INTEGER,
    • GL_BGRA_INTEGER,
    • GL_STENCIL_INDEX,
    • GL_DEPTH_COMPONENT,
    • GL_DEPTH_STENCIL;
  • type — тип данных, которым представлен конкретный пиксель изображения:
    • тип byte:
      • GL_BYTE,
      • GL_UNSIGNED_BYTE,
      • GL_UNSIGNED_BYTE_3_3_2,
      • GL_UNSIGNED_BYTE_2_3_3_REV,
    • short:
      • GL_SHORT,
      • GL_UNSIGNED_SHORT,
      • GL_UNSIGNED_SHORT_5_6_5,
      • GL_UNSIGNED_SHORT_5_6_5_REV,
      • GL_UNSIGNED_SHORT_4_4_4_4,
      • GL_UNSIGNED_SHORT_4_4_4_4_REV,
      • GL_UNSIGNED_SHORT_5_5_5_1,
      • GL_UNSIGNED_SHORT_1_5_5_5_REV,
    • int:
      • GL_INT,
      • GL_UNSIGNED_INT,
      • GL_UNSIGNED_INT_8_8_8_8,
      • GL_UNSIGNED_INT_8_8_8_8_REV,
      • GL_UNSIGNED_INT_10_10_10_2,
      • GL_UNSIGNED_INT_2_10_10_10_REV
    • float:
      • GL_FLOAT,
      • GL_HALF_FLOAT;
  • data — адрес массива с пикселями загружаемого изображения.

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

// Конструктор текстуры заданного размера для использования в буфере
TextureArray::TextureArray(GLuint levels, GLuint width, GLuint height, GLuint attachment, GLuint texType, GLint internalformat, GLint format, GLenum dataType)
{
    type = texType;
    // Генерация текстуры заданного размера
    glGenTextures(1, &handler);
    glBindTexture(GL_TEXTURE_2D_ARRAY, handler);
    glTexImage3D(
        GL_TEXTURE_2D_ARRAY, 0, internalformat, width, height, levels, 0, format, dataType, 0);

    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    // Привязка к буферу кадра
    glFramebufferTexture(GL_FRAMEBUFFER, attachment, handler, 0);

    // Создаем счетчик использований дескриптора
    handler_count[handler] = 1;
}

// Конструктор копирования
TextureArray::TextureArray(const TextureArray& other)
{
    handler = other.handler; 
    type = other.type;
    // Делаем копию и увеличиваем счетчик
    handler_count[handler]++;
}

// Оператор присваивания
TextureArray& TextureArray::operator=(const TextureArray& other)
{
    // Если это разные текстуры
    if (handler != other.handler)
    {
        this->~TextureArray(); // Уничтожаем имеющуюся
        // Заменяем новой
        handler = other.handler;
        handler_count[handler]++;
    }
    type = other.type;

    return *this;
}

// Пересоздает текстуру для имеющегося дескриптора
void TextureArray::reallocate(GLuint levels, GLuint width, GLuint height, GLuint texType, GLint internalformat, GLint format, GLenum dataType)
{
    use();
    glTexImage2D(GL_TEXTURE_2D, 0, internalformat, width, height, 0, format, dataType, NULL);
}

// Привязка текстуры
void TextureArray::use()
{
    glActiveTexture(type + GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D_ARRAY, handler); // Привязка текстуры как активной
}

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

Каскадные карты теней

Детализация теней зависит от двух параметров: размер текстуры (пиксели) и размер области пространства (матрица проекции) источника. Увеличение размера текстуры требует дополнительных затрат памяти, а так же влияет на производительность при расчете теней. Если уменьшать область пространства источника меньше, чем видит камера, то это может привести к отсутствию теней в области видимости камеры. Метод каскадных карт теней подразумевает использование нескольких карт теней с разным уровнем детализации в зависимости от дистанции до камеры. Для примера разделения сцены на каскады по глубине разделим продлим плоскость на всю область видимости камеры и раскрасим сцену в соответствии с глубиной кадра (рисунок 8).

Рисунок 8 — Пример разделения сцены на каскады теней с окрашиванием в цвета

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

Зададим количество каскадов в файле include/Camera.h:

// Количество каскадов для карт теней
#define CAMERA_CASCADE_COUNT 4

Можно вернуть старую дистанцию отрисовки камеры:

// Дальняя граница области отсечения
#define CAMERA_FAR 100.0f

А так же границы каскадов в файле src/Camera.cpp:

// Границы каскадов
const float camera_cascade_distances[] = {CAMERA_NEAR, CAMERA_FAR / 50.0f, CAMERA_FAR / 10.0f,  CAMERA_FAR / 3.0f, CAMERA_FAR};

Важное примечание: в данной реализации дистанции каскадов заданы константно, если требуется их изменение в процессе работы, то следует убрать спецификатор const.

Объявим массив в файле include/Camera.h для использования в других файлах:

// Данные о дистанциях каскадов 
extern const float camera_cascade_distances[]; // src/Camera.cpp

Данные границы образуют следующие отрезки при камере с глубиной в диапазоне [0.1; 100]:

  • самый крупный масштаб для ближнего [0.1; 2];
  • поменьше для средней дистанции [2; 10];
  • поменьше для удаленной дистанции [10; 33.3];
  • ещё меньше для дальнего диапазона[33.3; 100].

Для хранения точек каскадов изменим массив, метод доступа, а так же добавим массив матриц проекций каскадов:

class Camera
{
    public:
...
        std::pair<bool, const glm::vec4(*)[8]> getProjCoords(); // Доступ к координатам с флагом изменения, описывающим пространство вида с пересчетом, если это требуется
    protected:
...
        glm::vec4 coords[CAMERA_CASCADE_COUNT][8]; // Координаты в проекции 
        glm::mat4 cascade_proj[CAMERA_CASCADE_COUNT]; // Матрицы проекций каскадов
};

Для вычисления матриц проекций каскадов дополним методы в файле src/Camera.cpp:

// Устанавливает заданную матрицу перспективы
void Camera::setPerspective(float fovy, float aspect)
{
    projection = glm::perspective(glm::radians(fovy), aspect, CAMERA_NEAR, CAMERA_FAR);
    requiredRecalcVP = true;
    for (int cascade = 0; cascade < CAMERA_CASCADE_COUNT; cascade++)
        cascade_proj[cascade] = glm::perspective(glm::radians(fovy), aspect, camera_cascade_distances[cascade], camera_cascade_distances[cascade+1]);
}

// Устанавливает заданную ортографическую матрицу
void Camera::setOrtho(float width, float height)
{
    const float aspect = width / height;
    projection = glm::ortho(-1.0f, 1.0f, -1.0f/aspect, 1.0f/aspect, CAMERA_NEAR, CAMERA_FAR);
    requiredRecalcVP = true;
    for (int cascade = 0; cascade < CAMERA_CASCADE_COUNT; cascade++)
        cascade_proj[cascade] = glm::ortho(-1.0f, 1.0f, -1.0f/aspect, 1.0f/aspect, camera_cascade_distances[cascade], camera_cascade_distances[cascade+1]);

}

Метод генерации точек каскадов будут иметь следующий вид:

// Доступ к координатам с флагом изменения, описывающим пространство вида с пересчетом, если это требуется
std::pair<bool, const glm::vec4(*)[8]> Camera::getProjCoords() 
{
    const glm::mat4& cam_vp = getVP(); // Получение ссылки на матрицу вида-проекции с пересчетом, если требуется и активацией флага requiredRecalcCoords
    bool changes = false; // Возвращаемое значение

    if (requiredRecalcCoords)
    { 
        // Инверсия матрицы вида/проекции камеры
        glm::mat4 inv = glm::inverse(cam_vp);
        // Типовые точки, описывающие пространство 
        glm::vec4 typical_points[8] = {  { 1, 1, 1,1}
                                       , { 1, 1,-1,1}
                                       , { 1,-1, 1,1}
                                       , { 1,-1,-1,1}
                                       , {-1, 1, 1,1}
                                       , {-1, 1,-1,1}
                                       , {-1,-1, 1,1}
                                       , {-1,-1,-1,1}};
        
        for (int cascade = 0; cascade < CAMERA_CASCADE_COUNT; cascade++)
        {
            glm::mat4 inv = glm::inverse(cascade_proj[cascade] * getView());
            // Цикл по типовым точкам
            for (int i = 0; i < 8; i++)
            {
                coords[cascade][i] = inv * typical_points[i];
                coords[cascade][i] /= coords[cascade][i].w;
            } 
        }

        requiredRecalcCoords = false; // Сбрасываем флаг
        changes = true;
    }

    return std::make_pair(changes, coords); 
}

В файле include/Lights.h изменим количество матриц вида-проекции в соответствии с количеством каскадов:

// Класс направленного источника освещения
class Sun
{
...
        alignas(16) glm::mat4 vp[CAMERA_CASCADE_COUNT]; // Матрица вида-проекции источника
...
};

Метод Sun::recalcVP в файле src/Lights.cpp необходимо изменить для работы с новым форматом массива точек:

// Пересчитывает по необходимости матрицу вида-проекции
void Sun::recalcVP()
{
    // Точки по краям проекции камеры
   std::pair <bool, const glm::vec4(*)[8]> camProjCoords = Camera::current().getProjCoords();

    // Есть изменения по источнику или камере
    if (uploadReq || camProjCoords.first)
    {
        uploadReq = true; // Требуется загрузка в следствии пересчета матрицы

        glm::vec3 mean; // Среднее арифметическое
        glm::vec4 max, min; // макс и мин координаты
        glm::vec4 point; // Точка приведенная в пространство источника света

    
        for (int cascade = 0; cascade < CAMERA_CASCADE_COUNT; cascade++)
        {
            mean = glm::vec3(0);
            // Найдем среднее арифметическое от точек для нахождения центра прямоугольника
            for (int i = 0; i < 8; i++)
                mean += glm::vec3(camProjCoords.second[cascade][i]);
            mean /= 8;
            // Используем среднее арифметическое для получения матрицы вида параллельного источника
            glm::mat4 lightView = glm::lookAt(mean + glm::normalize(direction), mean, CAMERA_UP_VECTOR);
            
            // Примем первую точку как минимальную и максимальную (приведя в пространство вида источника)
            min = max = lightView * camProjCoords.second[cascade][0];
            // Для оставшихся точек
            for (int i = 1; i < 8; i++)
            {
                // Приведем в пространство вида источника
                point = lightView * camProjCoords.second[cascade][i];
                max = glm::max(max, point);
                min = glm::min(min, point);
            }

            // Максимальное значение глубины
            max.z = std::max(fabs(max.z), fabs(min.z));
            // На основании максимальных и минимальных координат создадим матрицу проекции источника
            vp[cascade] = glm::ortho(min.x, max.x, min.y, max.y, min.z, max.z) * lightView;
        }
    }
}

Теперь можно изменить тип данных текстуры теней из файла src/main.cpp:

    // Создадим текстуры для буфера кадра
    TextureArray sunShadowDepth(CAMERA_CASCADE_COUNT, sunShadow_resolution, sunShadow_resolution, GL_DEPTH_ATTACHMENT, 4, GL_DEPTH_COMPONENT, GL_DEPTH_COMPONENT);
    // Правка фантомных теней
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER);
    glTexParameteri(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER);
    float shadowBorderColor[] = { 1.0, 1.0, 1.0, 1.0 };
    glTexParameterfv(GL_TEXTURE_2D_ARRAY, GL_TEXTURE_BORDER_COLOR, shadowBorderColor);

Важное замечание: далее работа ведется с шейдерами, важно не забыть поменять число каскадов в карте теней, но можно задать его некоторым большим числом (например 10) и использовать только часть, передавая их число в качестве uniform-переменной (автор использует первый-трудоемкий способ)

В случае использования геометрического шейдера для расчета позиций вершины в каждом каскаде, вершинный шейдер (shaders/sun_shadow.vert) освобождается от этой задачи и считает только положение вершины в мировых координатах:

#version 420 core

layout (location = 0) in vec3 pos;

uniform mat4 model;

void main()
{
    gl_Position = model * vec4(pos, 1.0);
}

Добавим геометрический шейдер shaders/sun_shadow.geom:

#version 420 core

layout(triangles, invocations = 4) in; // здесь invocations должно соответствовать количеству каскадов
layout(triangle_strip, max_vertices = 3) out;

layout(std140, binding = 3) uniform Sun
{
    vec3 direction;
    vec3 color;
    mat4 vp[4];
} sun;

void main()
{ 
	for (int i = 0; i < 3; ++i)
	{
		gl_Position = Sun_VP[gl_InvocationID] * gl_in[i].gl_Position;
		gl_Layer = gl_InvocationID;
		EmitVertex();
	}
	EndPrimitive();
}  

Данный геометрический шейдер принимает входную вершину (in) для треугольника (triangles) с определенным числом вызовов (invocations). Для каждого вызова соответствует переменная gl_InvocationID, содержащая в себе индекс текущего вызова в диапазоне [0; invocations). Выходные вершины (out) генерируются в формате троек для построения треугольника (triangle_strip) в количестве не больше 3 (max_vertices).

Для каждой вершины (цикл for по i) вычисляется её позиция в пространстве источника (произведение матрицы Sun_VP и вершины gl_in[i].gl_Position) и слой текстуры (gl_Layer), соответствующий номеру текущего вызова геометрического шейдера и номеру каскада. После вычисления вершины и задания слоя вызывается функция EmitVertex, которая создает новую вершину с заданными параметрами.

В конце геометрического шейдера вызывается функция EndPrimitive, которая создает примитив из ранее созданных вершин. Получается что данная функция вызывается в конце каждого вызова (invocation) и генерирует по примитиву для каждого слоя каскада.

Для корректной работы шейдера необходимо добавить пустой фрагментный шейдер (shaders/empty.frag):

#version 330 core

void main()
{
    
}

Примечание: несмотря на то, что фрагментный шейдер пустой, вычисления буфера глубины будут производится.

Если не добавить пустой фрагментный шейдер то будет выводится ошибка: Out of resource error.

Теперь можно добавить новые шейдеры к шейдерной программе расчета теней для направленного источника:

    // Шейдер для расчета теней
    ShaderProgram sunShadowShader;
    // Загрузим шейдер
    sunShadowShader.load(GL_VERTEX_SHADER, "shaders/sun_shadow.vert");
    sunShadowShader.load(GL_GEOMETRY_SHADER, "shaders/sun_shadow.geom");
    sunShadowShader.load(GL_FRAGMENT_SHADER, "shaders/empty.frag");
    sunShadowShader.link();

Следует определиться с тем, как будут задаваться каскады в пространстве камеры. Если их можно менять в процессе работы, то следует загружать их в случае смены или каждый цикл. В текущей реализации используется константные значения, что позволяет загрузить их один раз после загрузки шейдеров расчета освещения в файле src/main.cpp:

    ShaderProgram lightShader;
    // Загрузка и компиляция шейдеров
    lightShader.load(GL_VERTEX_SHADER, "shaders/quad.vert");
    lightShader.load(GL_FRAGMENT_SHADER, "shaders/lighting.frag");
    lightShader.link();
    const char* gtextures_shader_names[]  = {"gPosition", "gNormal", "gDiffuseP", "gAmbientSpecular", "sunShadowDepth"};
    lightShader.bindTextures(gtextures_shader_names, sizeof(gtextures_shader_names)/sizeof(const char*));
    // Загрузка данных о границах каскадов
    glUniform1fv(lightShader.getUniformLoc("camera_cascade_distances"), CAMERA_CASCADE_COUNT, &camera_cascade_distances[1]);

Примечание: значения массива передаются за исключением CAMERA_NEAR, так как он не нужен при определении границ каскадов.

В шейдере расчета освещения (shaders/lighting.frag) добавим uniform-переменную в виде массива из трех элементов (должно соответствовать размеру — CAMERA_CASCADE_COUNT):

uniform float camera_cascade_distances[4]; // Размер массива должен соответствовать количеству каскадов

Для преобразований так же необходимо изменить матрицу трансформации в пространство источника:

layout(std140, binding = 3) uniform Sun
{
    vec3 direction;
    vec3 color;
    mat4 vp[4];
} sun;

Для текстуры каскадной карты теней необходимо изменить тип:

uniform sampler2DArray sunShadowDepth;

Необходимо определить в какой каскад попадает фрагмент. Для этого сравним значение глубины фрагмента в пространстве камеры со значениями в ранее переданном массиве:

    vec4 fragPosCamSpace = camera.view * vec4(fragPos, 1); // Фрагмент в пространстве камеры
    int cascade_index; // Индекс текущего каскада для вычисления теней

    // Определение индекса каскада в который попадает фрагмент (цикл на 1 меньше чем кол-во каскадов)
    for (cascade_index = 0; cascade_index < 3; cascade_index++)
        if (abs(fragPosCamSpace.z) < camera_cascade_distances[cascade_index])
            break;

Важное замечание: цикл запускается строго меньше CAMERA_CASCADE_COUNT-1, так как если ни одно значение не подошло до последнего каскада, то будет использоваться последний (CAMERA_CASCADE_COUNT-1).

        // Расположение фрагмента в координатах теневой карты
        fragPosLightSpace = (sun.vp[cascade_index] * vec4(fragPos, 1.0)).xyz;

Для корректной работы функции textureSize с текселем 3D текстуры достаточно взять только значения в плоскости XY:

        vec2 texelSize = 1.0 / textureSize(sunShadowDepth, 0).xy; // Размер текселя текстуры теней

В GLSL для получения цвета с определенного слоя 3D текстуры используется дополненный значением слоя вектор в функции texture, пример в общем случае:

texture(ТЕКСТУРА, vec3(ПОЗИЦИЯ.xy, ИНДЕКС_СЛОЯ)

Для текстуры каскадной карты теней результат будет иметь следующий вид:

                pcfDepth = texture(sunShadowDepth, vec3(fragPosLightSpace.xy + vec2(x, y) * texelSize, cascade_index)).r; 

В результате, если все верно, приложение должно рисовать каскадную карту теней. При текущем расположении источника, камеры и сцены тень от капли должна попадать на шов между нулевым и первым каскадами. Пример работы каскадных карт теней представлен на рисунке 9.

Рисунок 9 — Результат использования каскадных карт теней

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

Вывод

В данной заметке были реализованы обычные и каскадные карты теней для направленного источника с учетом ограничения области видимости камеры. Модифицирован класс 2D текстуры, добавлен абстрактный класс и класс 3D текстуры. Добавлен геометрический шейдер, генерирующий фрагменты на разных каскадах.

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

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

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

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