Указатели на современное OpenGL теневое кубирование?
Фон
Я работаю над 3D-игрой с использованием C++ и современного OpenGL (3.3). Сейчас я работаю над освещением и рендерингом теней, и я успешно реализовал направленное отображение теней. После прочтения требований к игре, я решил, что мне понадобится точечная карта теней. Проведя некоторые исследования, я обнаружил, что для всенаправленного отображения теней я сделаю нечто похожее на направленное отображение теней, но вместо этого с кубической картой.
У меня нет предыдущих знаний о кубических картах, но я понимаю, что кубическая карта - это шесть текстур, которые можно легко прикрепить. Я немного осмотрелся, но, к сожалению, я изо всех сил пытался найти окончательное "учебное пособие" по этому предмету для современного OpenGL. Сначала я ищу учебники, которые объясняют это от начала до конца, потому что я серьезно пытался извлечь уроки из фрагментов исходного кода или просто концепций, но я попытался.
Текущие договоренности
Вот мое общее понимание идеи, за вычетом технических деталей. Пожалуйста, поправьте меня.
- Для каждого точечного источника света устанавливается кадровый буфер, например, направленное отображение теней
- Затем генерируется одна текстура кубической карты, которая связывается с
glBindTexture(GL_TEXTURE_CUBE_MAP, shadowmap)
, Карта куба настроена со следующими атрибутами:
glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP_ARB, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
(это также похоже на направленное отображение теней)
Сейчас
glTexImage2D()
повторяется шесть раз, по одному разу для каждого лица. Я делаю это так:for (int face = 0; face < 6; face++) // Fill each face of the shadow cubemap glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X + face, 0, GL_DEPTH_COMPONENT32F , 1024, 1024, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL);
Текстура прикреплена к фреймбуферу с вызовом
glFramebufferTexture(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, shadowmap, 0);
Когда сцена должна быть визуализирована, она визуализируется в два прохода, как направленное отображение теней.
- Прежде всего, теневой кадровый буфер связан, область просмотра настраивается на размер карты теней (в данном случае 1024 на 1024).
- Отбраковка устанавливается на передние грани с
glCullFace(GL_FRONT)
- Активная шейдерная программа переключается на вершинные и фрагментные теневые шейдеры, которые я предоставлю в дальнейшем источниках
Матрицы освещения для всех шести видов рассчитываются. Я делаю это, создавая вектор glm::mat4's и
push_back()
матрицы, как это:// Create the six view matrices for all six sides for (int i = 0; i < renderedObjects.size(); i++) // Iterate through all rendered objects { renderedObjects[i]->bindBuffers(); // Bind buffers for rendering with it glm::mat4 depthModelMatrix = renderedObjects[i]->getModelMatrix(); // Set up model matrix for (int i = 0; i < 6; i++) // Draw for each side of the light { glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_CUBE_MAP_POSITIVE_X + i, shadowmap, 0); glClear(GL_DEPTH_BUFFER_BIT); // Clear depth buffer // Send MVP for shadow map glm::mat4 depthMVP = depthProjectionMatrix * depthViewMatrices[i] * depthModelMatrix; glUniformMatrix4fv(glGetUniformLocation(shadowMappingProgram, "depthMVP"), 1, GL_FALSE, glm::value_ptr(depthMVP)); glUniformMatrix4fv(glGetUniformLocation(shadowMappingProgram, "lightViewMatrix"), 1, GL_FALSE, glm::value_ptr(depthViewMatrices[i])); glUniformMatrix4fv(glGetUniformLocation(shadowMappingProgram, "lightProjectionMatrix"), 1, GL_FALSE, glm::value_ptr(depthProjectionMatrix)); glDrawElements(renderedObjects[i]->getDrawType(), renderedObjects[i]->getElementSize(), GL_UNSIGNED_INT, 0); } }
Фреймбуфер по умолчанию связан, и сцена рисуется нормально.
вопрос
Теперь к шейдерам. Это где мое понимание иссякает. Я совершенно не уверен в том, что мне следует делать, мои исследования противоречат друг другу, потому что это для разных версий. Я закончил копировать и вставлять код из случайных источников и надеялся, что он достигнет чего-то другого, кроме черного экрана. Я знаю, что это ужасно, но нет четких определений того, что делать. В каких местах я работаю? Мне даже нужен отдельный теневой шейдер, как я использовал при точечном освещении? Какого черта я использую в качестве типа для теневой кубической карты? samplerCube? samplerCubeShadow? Как правильно выбрать образец кубической карты? Я надеюсь, что кто-то может прояснить это для меня и дать хорошее объяснение. Мое текущее понимание части шейдера таково:
- Когда сцена визуализируется в кубическую карту, вершинный шейдер просто берет форму глубины MVP, которую я рассчитал в своем коде C++, и преобразует входные вершины по ним.
- Фрагментный шейдер прохода кубической карты просто назначает единственное значение для gl_FragCoord.z
, (Эта часть не изменилась по сравнению с тем моментом, когда я реализовал направленное отображение теней. Я предполагал, что это будет то же самое для отображения кубов, потому что шейдеры даже не взаимодействуют с картой куба - OpenGL просто выводит выходные данные из них в карту куба, верно? Потому что это кадровый буфер?)
- Вершинный шейдер для обычного рендеринга не изменяется.
- В фрагментном шейдере для нормального рендеринга положение вершины преобразуется в пространство источника света с помощью матрицы проекции и вида источника света.
- Это как-то используется в поиске текстуры кубической карты.???
- Как только глубина была найдена с помощью магических средств, она сравнивается с расстоянием света до вершины, очень похоже на направленное отображение теней. Если оно меньше, эта точка должна быть затенена, и наоборот.
Это не так много понимания. Я не говорю о том, как вершины преобразуются и используются для поиска кубической карты, поэтому я собираюсь вставить исходный код для своих шейдеров, в надежде, что люди смогут прояснить это. Обратите внимание, что большая часть этого кода является слепым копированием и вставкой, я ничего не изменил, чтобы не поставить под угрозу любое понимание.
Вершинный шейдер теней:
#version 150
in vec3 position;
uniform mat4 depthMVP;
void main()
{
gl_Position = depthMVP * vec4(position, 1);
}
Шейдер фрагмента тени:
#version 150
out float fragmentDepth;
void main()
{
fragmentDepth = gl_FragCoord.z;
}
Стандартный вершинный шейдер:
#version 150
in vec3 position;
in vec3 normal;
in vec2 texcoord;
uniform mat3 modelInverseTranspose;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
out vec3 fragnormal;
out vec3 fragnormaldirection;
out vec2 fragtexcoord;
out vec4 fragposition;
out vec4 fragshadowcoord;
void main()
{
fragposition = modelMatrix * vec4(position, 1.0);
fragtexcoord = texcoord;
fragnormaldirection = normalize(modelInverseTranspose * normal);
fragnormal = normalize(normal);
fragshadowcoord = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
}
Стандартный фрагментный шейдер:
#version 150
out vec4 outColour;
in vec3 fragnormaldirection;
in vec2 fragtexcoord;
in vec3 fragnormal;
in vec4 fragposition;
in vec4 fragshadowcoord;
uniform mat4 modelMatrix;
uniform mat4 viewMatrix;
uniform mat4 projectionMatrix;
uniform mat4 viewMatrixInversed;
uniform mat4 lightViewMatrix;
uniform mat4 lightProjectionMatrix;
uniform sampler2D tex;
uniform samplerCubeShadow shadowmap;
float VectorToDepthValue(vec3 Vec)
{
vec3 AbsVec = abs(Vec);
float LocalZcomp = max(AbsVec.x, max(AbsVec.y, AbsVec.z));
const float f = 2048.0;
const float n = 1.0;
float NormZComp = (f+n) / (f-n) - (2*f*n)/(f-n)/LocalZcomp;
return (NormZComp + 1.0) * 0.5;
}
float ComputeShadowFactor(samplerCubeShadow ShadowCubeMap, vec3 VertToLightWS)
{
float ShadowVec = texture(ShadowCubeMap, vec4(VertToLightWS, 1.0));
if (ShadowVec + 0.0001 > VectorToDepthValue(VertToLightWS)) // To avoid self shadowing, I guess
return 1.0;
return 0.7;
}
void main()
{
vec3 light_position = vec3(0.0, 0.0, 0.0);
vec3 VertToLightWS = light_position - fragposition.xyz;
outColour = texture(tex, fragtexcoord) * ComputeShadowFactor(shadowmap, VertToLightWS);
}
Я не могу вспомнить, откуда появился код функции ComputerShadowFactor и VectorToDepthValue, потому что я исследовал его на своем ноутбуке, к которому сейчас не могу добраться, но это результат этих шейдеров:
Это небольшой квадрат не затененного пространства, окруженный затененным пространством.
Я, очевидно, делаю здесь много неправильного, возможно, из-за недостатка знаний по этому вопросу, потому что мне трудно учиться чему-то, кроме учебников, и мне очень жаль. Я в недоумении, было бы замечательно, если бы кто-то мог пролить свет на это с ясным объяснением того, что я делаю неправильно, почему это неправильно, как я могу это исправить и, возможно, даже некоторый код. Я думаю, что проблема может быть в том, что я работаю не в том пространстве.
1 ответ
Я надеюсь дать ответ на некоторые ваши вопросы, но сначала понадобятся некоторые определения:
Что такое кубическая карта?
Это карта от вектора направления к паре [face, 2d координаты на этой грани], полученная проекцией вектора направления на гипотетический куб.
Что такое текстура кубической карты OpenGL?
Это набор из шести "изображений".
Что такое сэмплер GLSL cubemap?
Это примитив сэмплера, из которого можно сделать выборку из кубической карты. Это означает, что он выбирается с использованием вектора направления вместо обычных текстурных координат. Аппаратные средства затем проецируют вектор направления на гипотетический куб и используют полученную пару [face, 2d texture координат] для выборки правильного "изображения" в правой 2d позиции.
Что такое сэмплер GLSL?
Это примитив сэмплера, который ограничен текстурой, содержащей значения глубины пространства NDC, и при выборке с использованием функций выборки для тени возвращает "сравнение" глубины пространства NDC (в том же пространстве карты теней, очевидно) и глубина NDC-пространства хранится внутри ограниченной текстуры. Глубина для сравнения указывается в качестве дополнительного элемента в координатах текстуры при вызове функции выборки. Обратите внимание, что теневые сэмплеры предоставляются для простоты использования и скорости, но всегда можно сделать сравнение "вручную" в шейдере.
Теперь по вашим вопросам:
OpenGL просто отображает [...] кубическую карту, верно?
Нет, OpenGL рендерится с набором целей в текущем ограниченном кадровом буфере.
В случае кубических карт обычный способ рендеринга в них:
- создать их и прикрепить каждое из их шести "изображений" к одному и тому же кадровому буферу (очевидно, в разных точках присоединения)
- чтобы включить только одну цель за раз (таким образом, вы визуализируете на каждой грани кубической карты индивидуально)
- отрисовать то, что вы хотите в гранях кубической карты (возможно, с использованием специфичных для лица матриц "вид" и "проекция")
Точечные карты света
В дополнение ко всему, что сказано о кубических картах, существует ряд проблем в их использовании для реализации точечного отображения теней и, таким образом, сравнение глубины аппаратного обеспечения используется редко.
Вместо этого обычной практикой является следующее:
- вместо того, чтобы писать глубину пространства NDC, запишите радиальное расстояние от точечного источника света
- при запросе карты теней (см. пример кода внизу):
- не используйте аппаратные сравнения глубины (используйте samplerCube вместо samplerCubeShadow)
- преобразовать точку, подлежащую проверке, в "пространство куба" (которое вообще не включает проекцию)
- используйте вектор "пространство куба" в качестве направления поиска для выборки кубической карты
- сравните радиальное расстояние, выбранное из кубической карты, с радиальным расстоянием проверяемой точки
Образец кода
// sample radial distance from the cubemap
float radial_dist = texture(my_cubemap, cube_space_vector).x;
// compare against test point radial distance
bool shadowed = length(cube_space_vector) > radial_dist;