#android #opengl-es #glsl
Вопрос:
Я пытался нарисовать границу изображения с прозрачным фоном, используя OpenGL в Android. Я использую Шейдер фрагментов и Шейдер вершин. (Из библиотеки GPUImage)
Ниже я добавил рис. 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% до исходного метода. Используйте быстрое масштабирование ближайших соседей.