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

Mih*_*hah 5 android opengl-es glsl

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

下面我添加了图A和图B。

图 A. 带有粗糙边框

图A。

图 B. 具有光滑边框

图B。

我已经实现了图A。使用定制的片段着色器。但无法使边框变得更平滑,如图B所示。我附上我使用过的着色器代码(以实现粗糙边框)。这里有人可以帮助我如何使边界更平滑吗?

这是我的顶点着色器:

    attribute vec4 position;
    attribute vec4 inputTextureCoordinate;        
    varying vec2 textureCoordinate;
    
    void main()
    {
        gl_Position = position;
        textureCoordinate = inputTextureCoordinate.xy;
    }
Run Code Online (Sandbox Code Playgroud)

这是我的片段着色器:

我已经计算出当前像素周围的 8 个像素。如果这 8 个像素中的任何一个像素是不透明的(alpha 大于 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;
                }
Run Code Online (Sandbox Code Playgroud)

Alb*_*que 2

你几乎走上了正轨。

主要算法是:

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

主要问题是模糊步骤。它需要是大而平滑的模糊才能获得您想要的平滑轮廓。对于模糊,我们可以使用卷积滤波器Kernel。为了实现大模糊,我们应该使用大内核。我建议使用高斯模糊分布,因为它是众所周知和使用的。


该算法的概述是:

对于每个片段,我们对其周围的许多位置进行采样。样本是在 N × N 网格中制作的。我们使用遵循2D 高斯分布的权重对它们进行平均。这会导致图像模糊。

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


顺便说一句,您的解决方案几乎是一个模糊的 3 x 3 内核(您在 3 x 3 网格中对片段周围的位置进行采样)。但是,3 x 3 内核无法提供所需的模糊量。您需要更多样本(例如 11 x 11)。此外,靠近中心的权重对结果的影响应该更大。因此,统一的权重效果不会很好。


哦,还有一件重要的事情:

一个着色器来完成这一点并不是实现这一点的最快方法。通常,这将通过两个单独的渲染来完成。第一个渲染将照常渲染图像,第二个渲染将模糊并添加轮廓。我假设您想通过 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);
}
Run Code Online (Sandbox Code Playgroud)

片段着色器


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);
}
Run Code Online (Sandbox Code Playgroud)

这是我得到的结果:

概要算法的结果。

对于与您的相同的图像matrixSize = 33alphaTreshold = 0.05

轮廓算法与问题图像的结果。

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

轮廓算法的结果,具有问题图像和更平滑的边缘。

我在这个网站上测试了着色器。希望您能够使其适应您的解决方案。

关于参考,我使用了很多这个shadertoy示例作为我为此答案编写的代码的基础。