Уроки по OpenGL с сайта OGLDev


Урок 43 - Многопроходные карты теней с точечным источником света

В уроке уроке 24 мы изучили основы карты теней - первый проход из позиции источника света, используя направление освещения как вектор обзора, а второй из камеры, используя данные, полученные во время первого прохода для вычисления теней. В тот момент большинство программистов спросили себя - Такой подход будет работать для направленного света, но что если я захочу генерировать тени для точечного источника света? В этом случае не будет какого-либо конкретного направления света. Тема нашего сегодняшнего разговора - решение этой проблемы.

Решением является признание того факта, что точечный источник света проливает свет во всех направлениях, поэтому текстура карты теней будет получать лишь часть света, куда бы мы её не поставили; вместо этого мы можем поместить источник света в центр кубической текстуры. Тогда у нас будет 6 прямоугольных карт теней, и свет никуда не ускользнет. Каждый "луч" света будет падать на одну из этих карт теней, а дальше обычные вычисления тени. Мы уже видели кубическую текстуру в деле в уроке по скайбоксу, так что будем считать, что уже знакомы с ней.

На практике, для того, что бы сымитировать направление света во всех направлениях, мы добавим 6 проходов рендера из позиции источника света, и для каждого из них будет уникальное направление. Мы собираемся использовать направления основных осей в качестве направления света - положительная / отрицательная часть оси X, Y и Z. В итоге стороны кубической текстуры будут содержать расстояние до ближайших пикселей по всей сцене. Сравнивая эти значения с расстоянием от каждого пикселя до источника света во время второго прохода, мы сможем определить, находится ли пиксель в тени.

Посмотрите на изображение ниже:

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

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

Поскольку одна и таже сцена рендерится 6 раз во время первого прохода, мы назовем это Многопроходные Карты Теней.

Прямиком к коду!

shadow_map_fbo.h

class ShadowMapFBO
{
public:
    ShadowMapFBO();

    ~ShadowMapFBO();

    bool Init(unsigned int WindowWidth, unsigned int WindowHeight);

        void BindForWriting(<b>GLenum CubeFace</b>);

    void BindForReading(GLenum TextureUnit);

private:
    GLuint m_fbo;
    GLuint m_shadowMap;
        GLuint m_depth;
};

Давайте начнем осматривать код с изменений в FBO карты теней. Он остался практически без изменений, за исключением: метод BindForWriting() теперь принимает enum для сторон куба. Поскольку мы осуществляем несколько проходов рендера в кубическую текстуру, это поможет сообщить GL в какую из граней куба будет происходить рендер. Другое отличие - добавлен отдельный буфер глубины. Ранее мы использовали свойство m_shadowMap как объект карты теней (что по своей сути - буфер глубины). Теперь m_shadowMap будет использовано как кубическая карта и нам потребуется отдельный буфер глубины. Для каждого из 6 проходов мы будем использовать этот буфер глубины (и, конечно, очищать его перед каждым проходом).

shadow_map_fbo.cpp:46

bool ShadowMapFBO::Init(unsigned int WindowWidth, unsigned int WindowHeight)
{
    // Create the FBO
    glGenFramebuffers(1, &m_fbo);

    // Create the depth buffer
    glGenTextures(1, &m_depth);
    glBindTexture(GL_TEXTURE_2D, m_depth);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT32, WindowWidth, WindowHeight, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glBindTexture(GL_TEXTURE_2D, 0);

        // Create the cube map
        glGenTextures(1, &m_shadowMap);
        glBindTexture(GL_TEXTURE_CUBE_MAP, m_shadowMap);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
        glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE);

    for (uint i = 0 ; i &lt; 6 ; i++) {
        glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, 0, GL_R32F, WindowWidth, WindowHeight, 0, GL_RED, GL_FLOAT, NULL);
    }

    glBindFramebuffer(GL_FRAMEBUFFER, m_fbo);
    glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, m_depth, 0);

    // Disable writes to the color buffer
    glDrawBuffer(GL_NONE);

    // Disable reads from the color buffer
    glReadBuffer(GL_NONE);

    GLenum Status = glCheckFramebufferStatus(GL_FRAMEBUFFER);

    if (Status != GL_FRAMEBUFFER_COMPLETE) {
        printf("FB error, status: 0x%x\n", Status);
        return false;
    }

    glBindFramebuffer(GL_FRAMEBUFFER, 0);

    return GLCheckError();
}

Так мы инициализируем карту теней. Сначала мы создаем и настраиваем буфер глубины. Ничего нового. Далее идет кубическая текстура, при этом используется GL_TEXTURE_CUBE_MAP. Интерес представляет то, как мы инициализируем 6 сторон куба. OpenGL предоставляет макрос для каждой стороны: GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBE_MAP_NEGATIVE_X и так далее. Они заданы последовательно (можете посмотреть в glew.h около строки 1319 в моей версии). Каждая сторона создается с размером в 32 бита с типом float для каждого текселя.

main.cpp:183

virtual void RenderSceneCB()
{
    CalcFPS();

    m_scale += 0.05f;

    m_pGameCamera-&gt;OnRender();

    ShadowMapPass();
    RenderPass();

    RenderFPS();

    glutSwapBuffers();
}

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

tutorial43.cpp:200

void ShadowMapPass()
{
    glCullFace(GL_FRONT);

    m_shadowMapEffect.Enable();

    PersProjInfo ProjInfo;
        ProjInfo.FOV = 90.0f;
    ProjInfo.Height = WINDOW_HEIGHT;
    ProjInfo.Width = WINDOW_WIDTH;
    ProjInfo.zNear = 1.0f;
    ProjInfo.zFar = 100.0f;

    Pipeline p;
    p.SetPerspectiveProj(m_persProjInfo);

        glClearColor(FLT_MAX, FLT_MAX, FLT_MAX, FLT_MAX);

    for (uint i = 0 ; i &lt; NUM_OF_LAYERS ; i++) {
            m_shadowMapFBO.BindForWriting(gCameraDirections[i].CubemapFace);
        glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);

        p.SetCamera(m_pointLight.Position, gCameraDirections[i].Target, gCameraDirections[i].Up);

        p.Orient(m_mesh1Orientation);
        m_shadowMapEffect.SetWorld(p.GetWorldTrans());
        m_shadowMapEffect.SetWVP(p.GetWVPTrans());
        m_mesh.Render();

        p.Orient(m_mesh2Orientation);
        m_shadowMapEffect.SetWorld(p.GetWorldTrans());
        m_shadowMapEffect.SetWVP(p.GetWVPTrans());
        m_mesh.Render();
    }
}

Это полный проход теней. Следуется отметить пару отличий. Во-первых, то, что поле обзора установлено в 90 градусов. Причина тому то, что мы собираемся рендерить всю сцену на кубическую карту, поэтому, что бы установить камеру точно на каждую грань, мы устанавливаем её на одну четверть от окружности (360 градусов).

Далее, цвет очистки установлен в максимальное значение для типа float (FLT_MAX). Каждый отображаемый тексель будет иметь меньшее значение. Так мы сможем отделить "настоящие" тексели от пустых.

И, наконец, пробегаемся по сторонам кубической карты используя массив gCameraDirections (об этом ниже) для того, что бы установить соответсвующую сторону в FBO и направить камеру на неё.

tutorial43.cpp:45

struct CameraDirection
{
    GLenum CubemapFace;
    Vector3f Target;
    Vector3f Up;
};

CameraDirection gCameraDirections[NUM_OF_LAYERS] =
{
    { GL_TEXTURE_CUBE_MAP_POSITIVE_X, Vector3f(1.0f, 0.0f, 0.0f),  Vector3f(0.0f, -1.0f, 0.0f) },
    { GL_TEXTURE_CUBE_MAP_NEGATIVE_X, Vector3f(-1.0f, 0.0f, 0.0f), Vector3f(0.0f, -1.0f, 0.0f) },
    { GL_TEXTURE_CUBE_MAP_POSITIVE_Y, Vector3f(0.0f, 1.0f, 0.0f),  Vector3f(0.0f, 0.0f, -1.0f) },
    { GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, Vector3f(0.0f, -1.0f, 0.0f),  Vector3f(0.0f, 0.0f, 1.0f) },
    { GL_TEXTURE_CUBE_MAP_POSITIVE_Z, Vector3f(0.0f, 0.0f, 1.0f),  Vector3f(0.0f, -1.0f, 0.0f) },
    { GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, Vector3f(0.0f, 0.0f, -1.0f),  Vector3f(0.0f, -1.0f, 0.0f) }
};

Этот массив совмещает определенный GL enum, задающий каждую сторону куба, с двумя векторами, используемыми для задания направления камеры на соответствующую сторону.

shadow_map_fbo.cpp:96

void ShadowMapFBO::BindForWriting(GLenum CubeFace)
{
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, m_fbo);
    glFramebufferTexture2D(GL_DRAW_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, CubeFace, m_shadowMap, 0);
    glDrawBuffer(GL_COLOR_ATTACHMENT0);
}

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

tutorial43.cpp:237

void RenderPass()
{
    glCullFace(GL_BACK);

    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    m_lightingEffect.Enable();
    m_shadowMapFBO.BindForReading(SHADOW_TEXTURE_UNIT);
    m_lightingEffect.SetEyeWorldPos(m_pGameCamera-&gt;GetPos());

    Pipeline p;
    p.SetPerspectiveProj(m_persProjInfo);
    p.SetCamera(*m_pGameCamera);

    // Render the quads
    m_pGroundTex-&gt;Bind(COLOR_TEXTURE_UNIT);
    p.Orient(m_quad1Orientation);
    m_lightingEffect.SetWorldMatrix(p.GetWorldTrans());
    m_lightingEffect.SetWVP(p.GetWVPTrans());
    m_quad.Render();

    p.Orient(m_quad2Orientation);
    m_lightingEffect.SetWorldMatrix(p.GetWorldTrans());
    m_lightingEffect.SetWVP(p.GetWVPTrans());
    m_quad.Render();

    // Render the meshes
    p.Orient(m_mesh1Orientation);
    m_lightingEffect.SetWorldMatrix(p.GetWorldTrans());
    m_lightingEffect.SetWVP(p.GetWVPTrans());
    m_mesh.Render();

    p.Orient(m_mesh2Orientation);
    m_lightingEffect.SetWorldMatrix(p.GetWorldTrans());
    m_lightingEffect.SetWVP(p.GetWVPTrans());
    m_mesh.Render();
}

Это полный проход света. Все как обычно - мы рендерим в буфер кадра по-умолчанию, разрешаем считывать из кубической карты, и возвращаем камеру в исходное положение. На этом заканчивается обзор C++ кода. Далее посмотрим на шейдеры.

shadow_map.vs

#version 330

layout (location = 0) in vec3 Position;
layout (location = 1) in vec2 TexCoord;
layout (location = 2) in vec3 Normal;

uniform mat4 gWVP;
uniform mat4 gWorld;

out vec3 WorldPos;

void main()
{
    vec4 Pos4 = vec4(Position, 1.0);
    gl_Position = gWVP * Pos4;
    WorldPos = (gWorld * Pos4).xyz;
}

Мы собираемся рендерить из позиции источника света; камера направлена вдоль одной из осей. Значение, которое будет записано в кубическую карту, является расстоянием от объекта до источника света. Поэтому в FS нам требуется позиция объекта в мировых координатах, там расстояние и будет найдено.

shadow_map.fs

#version 330

in vec3 WorldPos;

uniform vec3 gLightWorldPos;

out float FragColor;

void main()
{
    vec3 LightToVertex = WorldPos - gLightWorldPos;

    float LightToPixelDistance = length(LightToVertex);

    FragColor = LightToPixelDistance;
}

К этому моменту мы имеем координаты пикселя в мировом пространстве, позиция источника света передается через uniform-переменную. Определяем вектор от источника света до пикселя, находим его длину и записываем её.

lighting.vs

#version 330

layout (location = 0) in vec3 Position;
layout (location = 1) in vec2 TexCoord;
layout (location = 2) in vec3 Normal;

out vec2 TexCoord0;
out vec3 Normal0;
out vec3 WorldPos0;

uniform mat4 gWVP;
uniform mat4 gWorld;

void main()
{
    gl_Position = gWVP * vec4(Position, 1.0);
    TexCoord0   = TexCoord;
    Normal0     = (gWorld * vec4(Normal, 0.0)).xyz;
    WorldPos0   = (gWorld * vec4(Position, 1.0)).xyz;
}

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

lighting.fs …

uniform samplerCube gShadowMap;

...

float CalcShadowFactor(vec3 LightDirection)
{
    float SampledDistance = texture(gShadowMap, LightDirection).r;

    float Distance = length(LightDirection);

    if (Distance &lt;= SampledDistance + EPSILON)
        return 1.0; // Inside the light
    else
        return 0.5; // Inside the shadow
}

Отрывок из кода выше содержит ключевое изменение в FS света. Uniform-переменная, которая ранее имела тип sampler2D (в уроке 24) или sampler2DShadow (в уроке 42), теперь samplerCube. Для того, что бы извлечь значение из неё мы используем вектор LightDirection, который был вычислен как вектор из позиции источника света до пикселя. Заметим, что все 3 координаты (X, Y и Z) вектора направления света используются для извлечения. Поскольку куб находится в трёхмерном пространстве, нам нужен трёхмерный вектор что бы выбрать соответствующую сторону и указанный пиксель из этой стороны. Сравнивая полученное значение с расстоянием от источника света до пикселя, мы определяем, освещен ли пиксель.

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

powered byDisqus