Странное поведение производительности с алгоритмом 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

Что вы делаете здесь:

  1. Перевести оригинальную точку в поле зрения
  2. Преобразуйте это, чтобы вырезать пространство
  3. Пример текстуры

Так как же это соответствует эффективности кеша?

Кэши хорошо работают при доступе к соседним пикселям. Например, если вы используете размытие по Гауссу, вы получаете доступ только к соседям, которые с высокой вероятностью будут уже загружены в кэш.

Итак, скажем, ваш объект сейчас очень далеко. Тогда пиксели, выбранные в пространстве клипа, также находятся очень близко к точке оригинала -> высокая локальность -> хорошая производительность кэша.

Если камера находится очень близко к вашему объекту, сгенерированные точки выборки находятся дальше (в пространстве клипа), и вы получаете случайный образец доступа к памяти. Это резко снизит вашу производительность, хотя на самом деле вы не выполняли больше операций.

Редактировать:

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

Если вы используете 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) и сэмплировать свои позиции из этого. Я ожидаю, что это увеличит производительность на порядок, особенно в сценарии наихудшего случая, который вы привели.

Другие вопросы по тегам