如何在 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 = 33
、alphaTreshold = 0.05
为了获得更清晰的结果,我们可以调整参数。这是 matrixSize = 111
、alphaTreshold = 0.05
、offset = vec2(0.002, 0.002)
、alphaTreshold = 0.01
、outlineSmoothness = 0.00 的示例。请注意,增加 matrixSize
将严重影响性能,这是仅使用一个着色器渲染此轮廓的限制。
我在 this site 上测试了着色器。希望您能够根据您的解决方案对其进行调整。
关于参考资料,我使用了相当多的 this shadertoy example 作为我为此答案编写的代码的基础。
我一直在尝试在 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 = 33
、alphaTreshold = 0.05
为了获得更清晰的结果,我们可以调整参数。这是 matrixSize = 111
、alphaTreshold = 0.05
、offset = vec2(0.002, 0.002)
、alphaTreshold = 0.01
、outlineSmoothness = 0.00 的示例。请注意,增加 matrixSize
将严重影响性能,这是仅使用一个着色器渲染此轮廓的限制。
我在 this site 上测试了着色器。希望您能够根据您的解决方案对其进行调整。
关于参考资料,我使用了相当多的 this shadertoy example 作为我为此答案编写的代码的基础。