Как сделать более плавные границы с помощью шейдера фрагментов в OpenGL?

#android #opengl-es #glsl

Вопрос:

Я пытался нарисовать границу изображения с прозрачным фоном, используя OpenGL в Android. Я использую Шейдер фрагментов и Шейдер вершин. (Из библиотеки GPUImage)

Ниже я добавил рис. A и Рис B.

Рис. A. с неровной границей

Рис. А.

Рис. B. с гладкой границей

Рис.

Я достиг Рис. А. С помощью настраиваемого шейдера фрагментов. Но не удалось сделать границу более гладкой, как на рис. Б. Я прикрепляю код шейдера, который я использовал (для достижения грубой границы). Может ли кто-нибудь здесь помочь мне в том, как сделать границу более гладкой?

Вот мой вершинный шейдер :

     attribute vec4 position;
    attribute vec4 inputTextureCoordinate;        
    varying vec2 textureCoordinate;
    
    void main()
    {
        gl_Position = position;
        textureCoordinate = inputTextureCoordinate.xy;
    }
 

Вот мой шейдер фрагментов :

Я рассчитал 8 пикселей вокруг текущего пикселя. Если какой-либо один пиксель из этих 8 непрозрачен(с альфой больше 0,4), он рисуется как цвет границы.

                 precision mediump float;
                uniform sampler2D inputImageTexture;
                varying vec2 textureCoordinate;
                uniform lowp float thickness;
                uniform lowp vec4 color;

                void main() {
                    float x = textureCoordinate.x;
                    float y = textureCoordinate.y;
                    vec4 current = texture2D(inputImageTexture, vec2(x,y));

                    if ( current.a != 1.0 ) {
                        float offset = thickness * 0.5;
                        
                        vec4 top = texture2D(inputImageTexture, vec2(x, y - offset));
                        vec4 topRight = texture2D(inputImageTexture, vec2(x   offset,y - offset));
                        vec4 topLeft = texture2D(inputImageTexture, vec2(x - offset, y - offset));
                        vec4 right = texture2D(inputImageTexture, vec2(x   offset, y ));
                        vec4 bottom = texture2D(inputImageTexture, vec2(x , y   offset));
                        vec4 bottomLeft  = texture2D(inputImageTexture, vec2(x - offset, y   offset));
                        vec4 bottomRight = texture2D(inputImageTexture, vec2(x   offset, y   offset));
                        vec4 left = texture2D(inputImageTexture, vec2(x - offset, y ));
                        
                        if ( top.a > 0.4 || bottom.a > 0.4 || left.a > 0.4 || right.a > 0.4 || topLeft.a > 0.4 || topRight.a > 0.4 || bottomLeft.a > 0.4 || bottomRight.a > 0.4 ) {
                             if (current.a != 0.0) {
                                 current = mix(color , current , current.a);
                             } else {
                                 current = color;
                             }
                        }
                    }
                    
                    gl_FragColor = current;
                }
 

Ответ №1:

Ты был почти на правильном пути.

Основным алгоритмом является:

  • Размытие изображения.
  • Используйте пиксели с непрозрачностью выше определенного порога в качестве контура.

Основная проблема-это шаг размытия. Это должно быть большое и плавное размытие, чтобы получить плавный контур, который вы хотите. Для размытия мы можем использовать ядро фильтра свертки. И чтобы добиться большого размытия, мы должны использовать большое ядро. И я предлагаю использовать распределение размытия по Гауссу, так как оно очень хорошо известно и используется.


Обзор алгоритма he таков:

Для каждого фрагмента мы пробуем множество мест вокруг него. Образцы выполнены в сетке N на N. Мы усредняем их вместе, используя веса, соответствующие двумерному распределению Гаусса. Это приводит к размытому изображению.

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


На боковой заметке ваше решение выглядит почти размытым с ядром 3 x 3 (вы выбираете места вокруг фрагмента в сетке 3 на 3). Однако ядро размером 3 х 3 не даст вам необходимого размытия. Вам нужно больше образцов (например, 11 х 11). Кроме того, веса, расположенные ближе к центру, должны оказывать большее влияние на результат. Таким образом, равномерные веса будут работать не очень хорошо.


О, и еще одна важная вещь:

Один шейдер для завершения этого-НЕ самый быстрый способ реализовать это. Обычно это завершается 2 отдельными рендерами. Первый отрисовал бы изображение как обычно, а второй отрисовал бы размытие и добавил контур. Я предположил, что вы хотите сделать это с помощью 1 одного рендеринга.

Ниже приведен шейдер вершин и фрагментов, который выполняет это:

Вершинный шейдер

 varying vec2 vecUV;
varying vec3 vecPos;
varying vec3 vecNormal;

void main() {
    vecUV = uv * 3.0 - 1.0;
    vecPos = (modelViewMatrix * vec4(position, 1.0)).xyz;
    vecNormal = (modelViewMatrix * vec4(normal, 0.0)).xyz;

    gl_Position = projectionMatrix * vec4(vecPos, 1.0);
}
 

Шейдер фрагментов

 
precision highp float;

varying vec2 vecUV;
varying vec3 vecPos;
varying vec3 vecNormal;

uniform sampler2D inputImageTexture;


float normalProbabilityDensityFunction(in float x, in float sigma)
{
    return 0.39894*exp(-0.5*x*x/(sigma*sigma))/sigma;
}

vec4 gaussianBlur()
{
    // The gaussian operator size
    // The higher this number, the better quality the outline will be
    // But this number is expensive! O(n2)
    const int matrixSize = 11;
    
    // How far apart (in UV coordinates) are each cell in the Gaussian Blur
    // Increase this for larger outlines!
    vec2 offset = vec2(0.005, 0.005);
    
    const int kernelSize = (matrixSize-1)/2;
    float kernel[matrixSize];
    
    // Create the 1-D kernel using a sigma
    float sigma = 7.0;
    for (int j = 0; j <= kernelSize;   j)
    {
        kernel[kernelSize j] = kernel[kernelSize-j] = normalProbabilityDensityFunction(float(j), sigma);
    }
    
    // Generate the normalization factor
    float normalizationFactor = 0.0;
    for (int j = 0; j < matrixSize;   j)
    {
        normalizationFactor  = kernel[j];
    }
    normalizationFactor = normalizationFactor * normalizationFactor;
    
    // Apply the kernel to the fragment
    vec4 outputColor = vec4(0.0);
    for (int i=-kernelSize; i <= kernelSize;   i)
    {
        for (int j=-kernelSize; j <= kernelSize;   j)
        {
            float kernelValue = kernel[kernelSize j]*kernel[kernelSize i];
            vec2 sampleLocation = vecUV.xy   vec2(float(i)*offset.x,float(j)*offset.y);
            vec4 sample = texture2D(inputImageTexture, sampleLocation);
            outputColor  = kernelValue * sample;
        }
    }
    
    // Divide by the normalization factor, so the weights sum to 1
    outputColor = outputColor/(normalizationFactor*normalizationFactor);
    
    return outputColor;
}


void main()
{
    // After blurring, what alpha threshold should we define as outline?
    float alphaTreshold = 0.3;
    
    // How smooth the edges of the outline it should have?
    float outlineSmoothness = 0.1;
    
    // The outline color
    vec4 outlineColor = vec4(1.0, 1.0, 1.0, 1.0);
    
    // Sample the original image and generate a blurred version using a gaussian blur
    vec4 originalImage = texture2D(inputImageTexture, vecUV);
    vec4 blurredImage = gaussianBlur();
    
    
    float alpha = smoothstep(alphaTreshold - outlineSmoothness, alphaTreshold   outlineSmoothness, blurredImage.a);
    vec4 outlineFragmentColor = mix(vec4(0.0), outlineColor, alpha);
    
    gl_FragColor = mix(outlineFragmentColor, originalImage, originalImage.a);
}
 

Вот какой результат я получил:

Результат алгоритма набросков.

И для того же образа, что и у вас, с matrixSize = 33 , alphaTreshold = 0.05

Результат алгоритма контура с изображением вопроса.

И чтобы попытаться получить более четкие результаты, мы можем изменить параметры. Вот пример с matrixSize = 111 , alphaTreshold = 0.05 , offset = vec2(0.002, 0.002) , alphaTreshold = 0.01 , четкостью контуров = 0,00. Обратите внимание, что увеличение matrixSize сильно повлияет на производительность, что является ограничением визуализации этого контура только одним проходом шейдера.

Результат алгоритма контура с изображением вопроса и более гладкими краями.

Я протестировал шейдер на этом сайте. Надеюсь, вы сможете адаптировать его к своему решению.

Что касается ссылок, я использовал довольно много примеров этого шейдера в качестве основы для кода, который я написал для этого ответа.

Комментарии:

1. Спасибо вам за ваш ответ. Это в значительной степени помогло мне. Можете ли вы также добавить некоторые изменения, чтобы сделать края границ чистыми? сравните с моими прикрепленными фотографиями. т. е. можем ли мы сделать его антиалиализованным?

2. Существует своего рода ограничение на выполнение этого с помощью 1 прохода шейдера. Мы МОЖЕМ сделать его чище, уменьшая offset и уменьшая alphaTreshold . Кроме того, outlineSmoothness это уже фактор сглаживания. НО измените только это, и вы увидите, что контур станет тоньше, и вам также нужно будет увеличить matrixSize его, чтобы получить более крупный контур. И это сделает шейдер очень тяжелым и медленным. В этом сценарии я был бы сильно склонен попытаться достичь результата с более чем 1 проходом вместо только 1 рендеринга. — Я добавил пример к ответу.

3. Хорошо. Спасибо. Любой намек, как я могу достичь этого с помощью нескольких проходов?

Ответ №2:

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

                     if ( current.a != 1.0 ) {
                    // other processing
                    
                    if (current.a > 0.4) {
                        if ( (top.a < 0.4 || bottom.a < 0.4 || left.a < 0.4 || right.a < 0.4 
                             || topLeft.a < 0.4 || topRight.a < 0.4 || bottomLeft.a < 0.4 || bottomRight.a < 0.4 ) 
                        {
                             // Implement 3x3 box blur here
                             
                        }
                    }
    }
 

Вам нужно настроить, какие краевые пиксели вы размываете. Основная проблема заключается в том, что он переходит от непрозрачного пикселя к прозрачному — вам нужен постепенный переход.

Еще один вариант-быстрое сглаживание. Из моего комментария ниже — Увеличьте масштаб на 200%, а затем уменьшите на 50% до исходного метода. Используйте масштабирование ближайших соседей. Этот метод иногда используется для сглаживания краев текста.

Комментарии:

1. Привет, мой, спасибо за ответ. Я пробовал размывать, но это только делает границу толще. Если вы заметили на приведенной выше границе рис. B., есть некоторые пиксели, которые на рис. А непрозрачны, но прозрачны на рис. B. Если вы твердо уверены, что это единственное решение, и я делаю что-то не так, не могли бы вы поделиться каким-нибудь фрагментом или чем-то еще ? что может быть полезно..

2. Размытие 3×3 не приведет к достаточно большому радиусу размытия, что не приведет к желаемому результату.

3. Вы могли бы попробовать быстрый метод сглаживания. Увеличьте масштаб на 200% , а затем уменьшите на 50% до исходного метода. Используйте быстрое масштабирование ближайших соседей.