Простой сверточный шейдер GLSL ужасно медленный

Я пытаюсь реализовать шейдер 2D-контура в OpenGL ES2.0 для iOS. Это безумно медленно. Как и в 5fps медленно. Я отследил это до вызовов texture2D(). Однако без них любой сверточный шейдер невозможен. Пробовал использовать lowp вместо mediump, но с этим все просто черное, хоть и дает еще 5fps, но все равно бесполезно.

Вот мой фрагментный шейдер.

    varying mediump vec4 colorVarying;
    varying mediump vec2 texCoord;

    uniform bool enableTexture;
    uniform sampler2D texture;

    uniform mediump float k;

    void main() {

        const mediump float step_w = 3.0/128.0;
        const mediump float step_h = 3.0/128.0;
        const mediump vec4 b = vec4(0.0, 0.0, 0.0, 1.0);
        const mediump vec4 one = vec4(1.0, 1.0, 1.0, 1.0);

        mediump vec2 offset[9];
        mediump float kernel[9];
        offset[0] = vec2(-step_w, step_h);
        offset[1] = vec2(-step_w, 0.0);
        offset[2] = vec2(-step_w, -step_h);
        offset[3] = vec2(0.0, step_h);
        offset[4] = vec2(0.0, 0.0);
        offset[5] = vec2(0.0, -step_h);
        offset[6] = vec2(step_w, step_h);
        offset[7] = vec2(step_w, 0.0);
        offset[8] = vec2(step_w, -step_h);

        kernel[0] = kernel[2] = kernel[6] = kernel[8] = 1.0/k;
        kernel[1] = kernel[3] = kernel[5] = kernel[7] = 2.0/k;
        kernel[4] = -16.0/k;  

        if (enableTexture) {
              mediump vec4 sum = vec4(0.0);
            for (int i=0;i<9;i++) {
                mediump vec4 tmp = texture2D(texture, texCoord + offset[i]);
                sum += tmp * kernel[i];
            }

            gl_FragColor = (sum * b) + ((one-sum) * texture2D(texture, texCoord));
        } else {
            gl_FragColor = colorVarying;
        }
    }

Это неоптимизировано и не доработано, но мне нужно повысить производительность, прежде чем продолжить. Я попытался заменить вызов texture2D() в цикле только сплошным vec4, и он работает без проблем, несмотря на все остальное.

Как я могу оптимизировать это? Я знаю, что это возможно, потому что я видел гораздо более сложные эффекты в 3D, работающие без проблем. Я вообще не понимаю, почему это вызывает какие-то проблемы.


person user1137704    schedule 18.09.2012    source источник
comment
Я пробовал заменить вызов texture2D() в цикле только сплошным vec4, и он работает без проблем Что это значит? Стало ли это быстрее? Производительность не изменилась? Что случилось?   -  person Nicol Bolas    schedule 18.09.2012
comment
Я не понимаю, почему это вообще вызывает какие-либо проблемы. Вы выполняете десять обращений к текстуре за один вызов шейдера и не видите, что может вызвать проблема? Кроме того, вы дважды обращаетесь к центральному текселю.   -  person Nicol Bolas    schedule 18.09.2012
comment
Я получаю твердые 60 кадров в секунду без поиска текстур (за исключением финального). Как я уже сказал, он не оптимизирован, но нет никакого способа избежать этих вызовов текстур. В противном случае фильтр не мог бы работать. Но я видел множество игр, мобильных и других, в которых используются эффекты, основанные на сверточных фильтрах, и у них, похоже, нет никаких проблем. Если нет какой-то хитрости, чтобы их избежать?   -  person user1137704    schedule 18.09.2012


Ответы (2)


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

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

Во-вторых, у вас есть девять зависимых считываний текстур. Это чтение текстуры, при котором координаты текстуры вычисляются в фрагментном шейдере. Зависимое чтение текстур очень затратно для графических процессоров PowerVR на устройствах iOS, потому что они не позволяют оборудованию оптимизировать чтение текстур с помощью кэширования и т. д. Поскольку вы производите выборку с фиксированным смещением для 8 окружающих пикселей и одного центрального, эти вычисления должны быть переместился в вершинный шейдер. Это также означает, что эти вычисления не нужно будет выполнять для каждого пикселя, а только один раз для каждой вершины, а затем аппаратная интерполяция сделает все остальное.

В-третьих, циклы for() до сих пор не очень хорошо обрабатывались компилятором шейдеров iOS, поэтому я стараюсь избегать тех, где могу.

Как я уже упоминал, я делал такие шейдеры свертки в своей среде iOS с открытым исходным кодом GPUImage. Для универсального фильтра свертки я использую следующий вершинный шейдер:

 attribute vec4 position;
 attribute vec4 inputTextureCoordinate;

 uniform highp float texelWidth; 
 uniform highp float texelHeight; 

 varying vec2 textureCoordinate;
 varying vec2 leftTextureCoordinate;
 varying vec2 rightTextureCoordinate;

 varying vec2 topTextureCoordinate;
 varying vec2 topLeftTextureCoordinate;
 varying vec2 topRightTextureCoordinate;

 varying vec2 bottomTextureCoordinate;
 varying vec2 bottomLeftTextureCoordinate;
 varying vec2 bottomRightTextureCoordinate;

 void main()
 {
     gl_Position = position;

     vec2 widthStep = vec2(texelWidth, 0.0);
     vec2 heightStep = vec2(0.0, texelHeight);
     vec2 widthHeightStep = vec2(texelWidth, texelHeight);
     vec2 widthNegativeHeightStep = vec2(texelWidth, -texelHeight);

     textureCoordinate = inputTextureCoordinate.xy;
     leftTextureCoordinate = inputTextureCoordinate.xy - widthStep;
     rightTextureCoordinate = inputTextureCoordinate.xy + widthStep;

     topTextureCoordinate = inputTextureCoordinate.xy - heightStep;
     topLeftTextureCoordinate = inputTextureCoordinate.xy - widthHeightStep;
     topRightTextureCoordinate = inputTextureCoordinate.xy + widthNegativeHeightStep;

     bottomTextureCoordinate = inputTextureCoordinate.xy + heightStep;
     bottomLeftTextureCoordinate = inputTextureCoordinate.xy - widthNegativeHeightStep;
     bottomRightTextureCoordinate = inputTextureCoordinate.xy + widthHeightStep;
 }

и следующий фрагментный шейдер:

 precision highp float;

 uniform sampler2D inputImageTexture;

 uniform mediump mat3 convolutionMatrix;

 varying vec2 textureCoordinate;
 varying vec2 leftTextureCoordinate;
 varying vec2 rightTextureCoordinate;

 varying vec2 topTextureCoordinate;
 varying vec2 topLeftTextureCoordinate;
 varying vec2 topRightTextureCoordinate;

 varying vec2 bottomTextureCoordinate;
 varying vec2 bottomLeftTextureCoordinate;
 varying vec2 bottomRightTextureCoordinate;

 void main()
 {
     mediump vec4 bottomColor = texture2D(inputImageTexture, bottomTextureCoordinate);
     mediump vec4 bottomLeftColor = texture2D(inputImageTexture, bottomLeftTextureCoordinate);
     mediump vec4 bottomRightColor = texture2D(inputImageTexture, bottomRightTextureCoordinate);
     mediump vec4 centerColor = texture2D(inputImageTexture, textureCoordinate);
     mediump vec4 leftColor = texture2D(inputImageTexture, leftTextureCoordinate);
     mediump vec4 rightColor = texture2D(inputImageTexture, rightTextureCoordinate);
     mediump vec4 topColor = texture2D(inputImageTexture, topTextureCoordinate);
     mediump vec4 topRightColor = texture2D(inputImageTexture, topRightTextureCoordinate);
     mediump vec4 topLeftColor = texture2D(inputImageTexture, topLeftTextureCoordinate);

     mediump vec4 resultColor = topLeftColor * convolutionMatrix[0][0] + topColor * convolutionMatrix[0][1] + topRightColor * convolutionMatrix[0][2];
     resultColor += leftColor * convolutionMatrix[1][0] + centerColor * convolutionMatrix[1][1] + rightColor * convolutionMatrix[1][2];
     resultColor += bottomLeftColor * convolutionMatrix[2][0] + bottomColor * convolutionMatrix[2][1] + bottomRightColor * convolutionMatrix[2][2];

     gl_FragColor = resultColor;
 }

Униформы texelWidth и texelHeight обратны ширине и высоте входного изображения, а юниформа convolutionMatrix задает веса для различных выборок в вашей свертке.

На iPhone 4 это выполняется за 4-8 мс для кадра видео с камеры 640x480, что достаточно для рендеринга 60 кадров в секунду при таком размере изображения. Если вам просто нужно сделать что-то вроде обнаружения краев, вы можете упростить описанное выше, преобразовать изображение в яркость на предварительном проходе, а затем сэмплировать только из одного цветового канала. Это еще быстрее, около 2 мс на кадр на том же устройстве.

person Brad Larson    schedule 18.09.2012
comment
Отличный пример. tl;dr: избегайте чтения зависимых текстур. Постарайтесь также протестировать разделимые свертки путем рендеринга в два прохода, чтобы уменьшить количество выборок (хотя для такого примера, как 9, это не уменьшит меньше чем вдвое, поэтому в этом случае двухпроходный подход может быть плохой идеей) - person Steven Lu; 14.09.2013
comment
@StevenLu - На многих из этих графических процессоров наблюдается удивительно резкое падение производительности, когда вы превышаете 9 чтений текстур или около того за один проход. Разделение этого на два прохода может оказать нелинейное влияние на производительность по сравнению с количеством выборок в одном проходе. Я тестировал, и выполнение этого за один проход намного, намного медленнее, чем разделение ядра, даже для такого небольшого количества образцов. - person Brad Larson; 15.09.2013
comment
Круто, спасибо, что взвесили. Итак, скорость заполнения пикселей для программ с легкими фрагментами может справиться с дополнительной нагрузкой из-за дополнительных проходов? Я где-то читал, что iPhone4 может заполнить экран 7 раз, чтобы поддерживать 60 кадров в секунду. Это составляет чуть менее 2 мс на полноэкранный проход. Это звучит правильно? - person Steven Lu; 16.09.2013
comment
Есть ли способ одновременно получить область текстуры вместо одного пикселя? - person Alex Gonçalves; 28.04.2015
comment
@AlexGonçalves - Во фрагментном шейдере? Нет, texture2D() производит выборку только одного пикселя за раз. - person Brad Larson; 28.04.2015
comment
Я надеялся применить фильтр нерезкой маски к моему изображению, используя ядро ​​​​5x5. Будет ли чтение 25 текстур слишком дорогим? (помимо резкого увеличения количества строк). - person Crearo Rotar; 24.05.2018
comment
@CrearoRotar - На современных устройствах это, вероятно, не будет иметь большого значения с точки зрения производительности между свертками 3x3 и 5x5. Вы не сможете использовать варианты, как я сделал выше, потому что вы превысите максимальное количество вариантов, поддерживаемых большинством устройств iOS. Для нерезкой маски я мог бы порекомендовать использовать разделяемое размытие по Гауссу, а затем пользовательский шейдер для смешивания пикселей, как я делаю здесь. Это может помочь уменьшить количество образцов на больших площадях и ускорить процесс. - person Brad Larson; 24.05.2018
comment
Подождите, разве это не неправильно, если это использовалось для обработки изображений на квадроцикле? Вместо выборки соседей каждого пикселя путем перемещения смещений для соседей в вершинный шейдер вы больше не производите выборку соседей. То есть вместо sample_pos_x = (texture_width * normalized_pos_between_0to1) + Neighbor_Offset. Приведенный выше код вычисляет sample_pos_x = (texture_width + nieghbor_offset) * normalized_pos_between_0to1. - person Sushanth Rajasankar; 10.07.2021

Единственный известный мне способ сократить время, затрачиваемое на этот шейдер, — это уменьшить количество выборок текстур. Поскольку ваш шейдер сэмплирует текстуры из равноотстоящих точек вокруг центральных пикселей и линейно объединяет их, вы можете уменьшить количество выборок, используя доступный режим GL_LINEAR для сэмплирования текстуры.

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

Назовем выборку со смещением (-stepw,-steph) и (-stepw,0) как x0 и x1 соответственно. Тогда ваша сумма

sum = x0*k0 + x1*k1

Теперь вместо этого, если вы сэмплируете между этими двумя текселями, на расстоянии k0/(k0+k1) от x0 и, следовательно, k1/(k0+k1) от x1, тогда GPU выполнит линейное взвешивание во время выборки и даст вам,

y = x1*k1/(k0+k1) + x0*k0/(k1+k0)

Таким образом, сумма может быть рассчитана как

sum = y*(k0 + k1) всего за один раз!

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

ссылка объясняет это гораздо лучше.

person Slartibartfast    schedule 18.09.2012