Странное поведение производительности с алгоритмом SSAO с использованием OpenGL и GLSL
Я работаю над алгоритмом SSAO (Окружающее пространство в пространстве), используя технику рендеринга Ориентированное полушарие.
Я) алгоритм
Этот алгоритм требует в качестве входных данных:
- 1 массив, содержащий предварительно вычисленные сэмплы (загружается до основного цикла -> В моем примере я использую 64 сэмпла, ориентированных по оси z).
- 1 текстура шума, содержащая нормализованные векторы вращения, также ориентированные по оси z (эта текстура генерируется один раз).
- 2 текстуры из GBuffer: PositionSampler и NormalSampler, содержащие позиции и векторы нормалей в пространстве вида.
Вот исходный код фрагмента шейдера, который я использую:
#version 400
/*
** Output color value.
*/
layout (location = 0) out vec4 FragColor;
/*
** Vertex inputs.
*/
in VertexData_VS
{
vec2 TexCoords;
} VertexData_IN;
/*
** Inverse Projection Matrix.
*/
uniform mat4 ProjMatrix;
/*
** GBuffer samplers.
*/
uniform sampler2D PositionSampler;
uniform sampler2D NormalSampler;
/*
** Noise sampler.
*/
uniform sampler2D NoiseSampler;
/*
** Noise texture viewport.
*/
uniform vec2 NoiseTexOffset;
/*
** Ambient light intensity.
*/
uniform vec4 AmbientIntensity;
/*
** SSAO kernel + size.
*/
uniform vec3 SSAOKernel[64];
uniform uint SSAOKernelSize;
uniform float SSAORadius;
/*
** Computes Orientation matrix.
*/
mat3 GetOrientationMatrix(vec3 normal, vec3 rotation)
{
vec3 tangent = normalize(rotation - normal * dot(rotation, normal)); //Graham Schmidt process
vec3 bitangent = cross(normal, tangent);
return (mat3(tangent, bitangent, normal)); //Orientation according to the normal
}
/*
** Fragment shader entry point.
*/
void main(void)
{
float OcclusionFactor = 0.0f;
vec3 gNormal_CS = normalize(texture(
NormalSampler, VertexData_IN.TexCoords).xyz * 2.0f - 1.0f); //Normal vector in view space from GBuffer
vec3 rotationVec = normalize(texture(NoiseSampler,
VertexData_IN.TexCoords * NoiseTexOffset).xyz * 2.0f - 1.0f); //Rotation vector required for Graham Schmidt process
vec3 Origin_VS = texture(PositionSampler, VertexData_IN.TexCoords).xyz; //Origin vertex in view space from GBuffer
mat3 OrientMatrix = GetOrientationMatrix(gNormal_CS, rotationVec);
for (int idx = 0; idx < SSAOKernelSize; idx++) //For each sample (64 iterations)
{
vec4 Sample_VS = vec4(Origin_VS + OrientMatrix * SSAOKernel[idx], 1.0f); //Sample translated in view space
vec4 Sample_HS = ProjMatrix * Sample_VS; //Sample in homogeneus space
vec3 Sample_CS = Sample_HS.xyz /= Sample_HS.w; //Perspective dividing (clip space)
vec2 texOffset = Sample_CS.xy * 0.5f + 0.5f; //Recover sample texture coordinates
vec3 SampleDepth_VS = texture(PositionSampler, texOffset).xyz; //Sample depth in view space
if (Sample_VS.z < SampleDepth_VS.z)
if (length(Sample_VS.xyz - SampleDepth_VS) <= SSAORadius)
OcclusionFactor += 1.0f; //Occlusion accumulation
}
OcclusionFactor = 1.0f - (OcclusionFactor / float(SSAOKernelSize));
FragColor = vec4(OcclusionFactor);
FragColor *= AmbientIntensity;
}
И вот результат (без размытия прохода рендера):
Пока здесь все не кажется правильным.
II) производительность
Я заметил, что у NSight Debugger очень странное поведение в отношении производительности:
Если я подвину свою камеру ближе и ближе к дракону, это сильно повлияет на производительность.
Но, на мой взгляд, это не должно быть так, потому что алгоритм SSAO применяется в Screen-Space и не зависит, например, от количества примитивов дракона.
Вот 3 скриншота с 3 различными положениями камеры (в этих 3 случаях все 1024*768 пиксельных шейдеров выполняются с использованием одного и того же алгоритма):
а) GPU бездействует: 40% (пиксельное влияние: 100%)
б) GPU простаивает: 25% (пиксель подвергается воздействию: 100%)
в) GPU простаивает: 2%! (влияние пикселя: 100%)
Мой движок рендеринга использует в моем примере точно 2 прохода рендеринга:
- Material Pass (заполнение позиции и нормальные пробоотборники)
- Ambient pass (заполнение текстуры SSAO)
Я думал, что проблема заключается в добавлении выполнения этих двух проходов, но это не так, потому что я добавил в свой клиентский код условие, чтобы не вычислять даром проход материала, если камера неподвижна. Поэтому, когда я сделал эти 3 фотографии выше, был выполнен только Ambient Pass. Так что это отсутствие производительности не связано с материалом прохода. Другой аргумент, который я мог бы вам дать, - это если я уберу сетку дракона (сцену только с плоскостью), результат будет таким же: чем больше моя камера находится близко к плоскости, тем больше нехватка производительности огромна!
Для меня это поведение не логично! Как я уже говорил выше, в этих 3 случаях все пиксельные шейдеры выполняются с применением точно такого же кода пиксельных шейдеров!
Теперь я заметил еще одно странное поведение, если я изменил небольшой кусочек кода прямо в фрагментном шейдере:
Если я заменю строку:
FragColor = vec4(OcclusionFactor);
По линии:
FragColor = vec4(1.0f, 1.0f, 1.0f, 1.0f);
Отсутствие производительности исчезает!
Это означает, что если код SSAO выполнен правильно (я попытался установить некоторые точки останова во время выполнения, чтобы проверить его), и я не использую этот OcclusionFactor в конце, чтобы заполнить окончательный выходной цвет, поэтому нет недостатка производительности!
Я думаю, что мы можем заключить, что проблема не исходит из кода шейдера перед строкой "FragColor = vec4 (OcclusionFactor);"... Я думаю.
Как вы можете объяснить такое поведение?
Я перепробовал множество комбинаций кода как в коде клиента, так и в коде фрагментного шейдера, но не могу найти решение этой проблемы! Я действительно потерян.
Заранее большое спасибо за вашу помощь!
2 ответа
Короткий ответ - эффективность кеша.
Чтобы понять это, давайте посмотрим на следующие строки из внутреннего цикла:
vec4 Sample_VS = vec4(Origin_VS + OrientMatrix * SSAOKernel[idx], 1.0f); //Sample translated in view space
vec4 Sample_HS = ProjMatrix * Sample_VS; //Sample in homogeneus space
vec3 Sample_CS = Sample_HS.xyz /= Sample_HS.w; //Perspective dividing (clip space)
vec2 texOffset = Sample_CS.xy * 0.5f + 0.5f; //Recover sample texture coordinates
vec3 SampleDepth_VS = texture(PositionSampler, texOffset).xyz; //Sample depth in view space
Что вы делаете здесь:
- Перевести оригинальную точку в поле зрения
- Преобразуйте это, чтобы вырезать пространство
- Пример текстуры
Так как же это соответствует эффективности кеша?
Кэши хорошо работают при доступе к соседним пикселям. Например, если вы используете размытие по Гауссу, вы получаете доступ только к соседям, которые с высокой вероятностью будут уже загружены в кэш.
Итак, скажем, ваш объект сейчас очень далеко. Тогда пиксели, выбранные в пространстве клипа, также находятся очень близко к точке оригинала -> высокая локальность -> хорошая производительность кэша.
Если камера находится очень близко к вашему объекту, сгенерированные точки выборки находятся дальше (в пространстве клипа), и вы получаете случайный образец доступа к памяти. Это резко снизит вашу производительность, хотя на самом деле вы не выполняли больше операций.
Редактировать:
Для повышения производительности вы можете восстановить позицию пространства просмотра из буфера глубины предыдущего прохода.
Если вы используете 32-битный буфер глубины, который уменьшает объем данных, требуемых для одной выборки, с 12 до 4 байтов.
Реконструкция позиции выглядит следующим образом:
vec4 reconstruct_vs_pos(vec2 tc){
float depth = texture(depthTexture,tc).x;
vec4 p = vec4(tc.x,tc.y,depth,1) * 2.0f + 1.0f; //tranformed to unit cube [-1,1]^3
vec4 p_cs = invProj * p; //invProj: inverse projection matrix (pass this by uniform)
return p_cs / p_cs.w;
}
Пока вы работаете над этим, вы можете сделать еще одну оптимизацию - визуализировать текстуру SSAO в уменьшенном размере, предпочтительно в два раза меньше основного окна просмотра. Если вы сделаете это, не забудьте скопировать текстуру глубины в другую текстуру половинного размера (glBlitFramebuffer) и сэмплировать свои позиции из этого. Я ожидаю, что это увеличит производительность на порядок, особенно в сценарии наихудшего случая, который вы привели.