如何在 OpenGL 中使用片段着色器制作更平滑的边界?

How to make smoother borders using Fragment shader in OpenGL?

我一直在尝试在 Android 中使用 OpenGL 绘制具有透明背景的图像的边框。我正在使用片段着色器和顶点着色器。 (From the GPUImage Library)

下面我添加了图A和图B

图 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 的 alpha),它将被绘制为边框颜色。

                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;
                }

在我的过滤器中,平滑度是通过边框上的简单 boxblur 实现的。您已确定 alpha > 0.4 是边框。周围像素中 0-0.4 之间的 alpha 值给出了边缘。只需用 3x3 window 模糊此边缘即可获得平滑边缘。

                    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% 到原始方法。使用最近邻缩放。这种技术有时用于平滑文本边缘。

你几乎走在了正确的轨道上。

主要算法是:

  • 模糊图像。
  • 使用不透明度高于特定阈值的像素作为轮廓。

主要问题是模糊步骤。它需要一个大而平滑的模糊来获得你想要的平滑轮廓。对于模糊,我们可以使用卷积滤波器 Kernel. And to achieve a large blur, we should use a large kernel. And I suggest using the Gaussian Blur 分布,因为它是众所周知的和使用的。


算法概述是:

对于每个片段,我们对其周围的许多位置进行采样。这些样本是在 N 乘 N 的网格中制作的。我们使用 2D Gaussian Distribution 之后的权重将它们平均在一起。 这会导致图像模糊。

对于模糊图像,我们使用轮廓颜色绘制 alpha 大于阈值的片段。当然,原始图像中的任何不透明像素也应该出现在结果中。


在旁注中,您的解决方案几乎是 3 x 3 内核的模糊(您在 3 x 3 网格中对片段周围的位置进行采样)。但是,3 x 3 内核无法提供所需的模糊量。您需要更多样本(例如 11 x 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 = 33alphaTreshold = 0.05

为了获得更清晰的结果,我们可以调整参数。这是 matrixSize = 111alphaTreshold = 0.05offset = vec2(0.002, 0.002)alphaTreshold = 0.01、outlineSmoothness = 0.00 的示例。请注意,增加 matrixSize 将严重影响性能,这是仅使用一个着色器渲染此轮廓的限制。

我在 this site 上测试了着色器。希望您能够根据您的解决方案对其进行调整。

关于参考资料,我使用了相当多的 this shadertoy example 作为我为此答案编写的代码的基础。