Реализация липкого эффекта с помощью шейдера (Обработка 3)

Я пытаюсь воспроизвести уловку веб-дизайна, известную как "липкий эффект" (см. live здесь). Это метод применения SVG-фильтров к движущимся эллипсам для получения движения, похожего на капли. Процесс довольно простой:

  • применить размытие по Гауссу
  • увеличить контраст только альфа-канала

Комбинация этих двух элементов создает эффект капли.

Последний шаг (увеличение контрастности альфа-канала) обычно выполняется с помощью «цветового матричного фильтра».

Цветовая матрица состоит из 5 столбцов (RGBA + смещение) и 4 строк.

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

В CSS увеличить контраст альфа-канала так же просто, как вызвать фильтр SVG и указать значение контраста (здесь 18):

<feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 18 -7" result="goo" />

Однако в обработке все немного сложнее. Я считаю (могу ошибаться) единственный способ применить фильтр цветовой матрицы - это создать его в шейдере. После нескольких попыток я придумал эти (очень простые) вершинные и фрагментные шейдеры для цветопередачи:

colorvert.glsl

uniform mat4 transform;
attribute vec4 position;
attribute vec4 color;
varying vec4 vertColor;

uniform vec4 o=vec4(0, 0, 0, -9); 
uniform lowp mat4 colorMatrix = mat4(1.0, 0.0, 0.0, 0.0, 
                                     0.0, 1.0, 0.0, 0.0, 
                                     0.0, 0.0, 1.0, 0.0, 
                                     0.0, 0.0, 0.0, 60.0);


void main() {
  gl_Position = transform * position; 
  vertColor = (color * colorMatrix) + o  ;
}

colorfrag.glsl

#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif

varying vec4 vertColor;

void main() {
  gl_FragColor = vertColor;
}

ПРОБЛЕМА:

Цветовая матрица работает частично: изменение значений RGB влияет на цвета, а изменение альфа-значений (последняя строка) - нет!

При попытке объединить шейдер с фильтром Гаусса нарисованный эллипс остается размытым даже после того, как я установил контраст альфа-канала на 60 (как в примере кода):

PShader colmat;

void setup() {
  size(200, 200, P2D);
  colmat = loadShader("colorfrag.glsl", "colorvert.glsl");
}

void draw() {
  background(100);
  shader(colmat);

  noStroke();
  fill(255, 30, 30);
  ellipse(width/2, height/2, 40, 40);
  filter(BLUR,6);
}

То же самое происходит, когда я реализую цветовую матрицу в @cansik's Gaussian blur шейдер (из библиотеки PostFX). Я вижу, как меняются цвета, но не альфа-контраст:

blurFrag.glsl

/ Adapted from:
// <a href="http://callumhay.blogspot.com/2010/09/gaussian-blur-shader-glsl.html" target="_blank" rel="nofollow">http://callumhay.blogspot.com/2010/09/gaussian-blur-shader-glsl.html</a>

#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif

#define PROCESSING_TEXTURE_SHADER


uniform sampler2D texture;

uniform vec4 o=vec4(0, 0, 0, 0); 
uniform lowp mat4 colorMatrix = mat4(1, 0.0, 0.0, 0.0, 
                                     0.0, 1, 0.0, 0.0, 
                                     0.0, 0.0, 1, 0.0, 
                                     0, 0.0, 0.0, 60.0); //Alpha contrast set to 60


varying vec2 center;

// The inverse of the texture dimensions along X and Y
uniform vec2 texOffset;

varying vec4 vertColor;
varying vec4 vertTexCoord;

uniform int blurSize;       
uniform int horizontalPass; // 0 or 1 to indicate vertical or horizontal pass
uniform float sigma;        // The sigma value for the gaussian function: higher value means more blur
                            // A good value for 9x9 is around 3 to 5
                            // A good value for 7x7 is around 2.5 to 4
                            // A good value for 5x5 is around 2 to 3.5
                            // ... play around with this based on what you need <span class="Emoticon Emoticon1"><span>:)</span></span>

const float pi = 3.14159265;

void main() {  
  float numBlurPixelsPerSide = float(blurSize / 2); 

  vec2 blurMultiplyVec = 0 < horizontalPass ? vec2(1.0, 0.0) : vec2(0.0, 1.0);

  // Incremental Gaussian Coefficent Calculation (See GPU Gems 3 pp. 877 - 889)
  vec3 incrementalGaussian;
  incrementalGaussian.x = 1.0 / (sqrt(2.0 * pi) * sigma);
  incrementalGaussian.y = exp(-0.5 / (sigma * sigma));
  incrementalGaussian.z = incrementalGaussian.y * incrementalGaussian.y;

  vec4 avgValue = vec4(0.0, 0.0, 0.0, 0.0);
  float coefficientSum = 0.0;

  // Take the central sample first...
  avgValue += texture2D(texture, vertTexCoord.st) * incrementalGaussian.x;
  coefficientSum += incrementalGaussian.x;
  incrementalGaussian.xy *= incrementalGaussian.yz;

  // Go through the remaining 8 vertical samples (4 on each side of the center)
  for (float i = 1.0; i <= numBlurPixelsPerSide; i++) { 
    avgValue += texture2D(texture, vertTexCoord.st - i * texOffset * 
                          blurMultiplyVec) * incrementalGaussian.x;         
    avgValue += texture2D(texture, vertTexCoord.st + i * texOffset * 
                          blurMultiplyVec) * incrementalGaussian.x;         
    coefficientSum += 2.0 * incrementalGaussian.x;
    incrementalGaussian.xy *= incrementalGaussian.yz;
  }
  gl_FragColor = (avgValue / coefficientSum )  * colorMatrix;
}

Установка glBlendFunc и включение glEnable(GL_BLEND) в основном файле .pde также не устранила проблему.

sketch.pde

import ch.bildspur.postfx.builder.*;
import ch.bildspur.postfx.pass.*;
import ch.bildspur.postfx.*;
import processing.opengl.*;
import com.jogamp.opengl.*;

PostFX fx;

void setup() {
    size(200, 200, P2D);
    fx = new PostFX(this); 
}

void draw() {
    background(100);
    GL gl = ((PJOGL)beginPGL()).gl.getGL();
    gl.glEnable(GL.GL_BLEND);
    gl.glBlendFunc(GL.GL_SRC_ALPHA, GL.GL_ONE);
    gl.glDisable(GL.GL_DEPTH_TEST);

    noStroke();
    fill(255, 30, 30);
    ellipse(width/2, height/2, 40, 40);
    fx.render().blur(80, 14).compose();
}

Вопросы:

  • Почему не работает контраст альфа-канала? Как заставить его работать?
  • Что-то не так с тем, как я реализовал цветовую матрицу?
  • Вы знаете, как лучше реализовать этот липкий эффект?

Любая помощь приветствуется !

Спасибо


person solub    schedule 12.04.2018    source источник
comment
Это касается только последнего вопроса, но есть еще один вариант - использовать метабалы.   -  person Luple    schedule 13.04.2018
comment
Спасибо за предложение, я уже закодировал метаболы. Я хочу попробовать новый (и более быстрый) подход.   -  person solub    schedule 13.04.2018
comment
Хорошо! Раньше я не работал с метаболами, но они есть в списке дел. Это только что добавили, спасибо!   -  person Luple    schedule 13.04.2018
comment
@solub Я не знаком ни с обработкой, ни с используемыми вами пакетами, но: Альфа-канал в OpenGL используется только в том случае, если GL_BLEND включен, и только если glBlendFunc использует альфа-канал и если используется исходный альфа-канал, вы должны иметь также альфа-буфер при создании пиксельного формата вашего контекста. Так что я держу пари, что ваше размытие вообще не использует альфа. Использование другого шейдера может означать разные макеты ввода / вывода и униформы, поэтому вы должны согласовать с ним код на стороне процессора, иначе шейдер не будет работать должным образом. Кстати, возможно, вы могли бы использовать Stencil для этого, чтобы как-то превратить размытое изображение в сплошное   -  person Spektre    schedule 13.04.2018
comment
@Spektre Я думаю, вы кое-что здесь заметили, но я не могу понять, как включить эту функцию смешивания в опубликованном мною шейдере blurFrag (вопрос отредактирован).   -  person solub    schedule 13.04.2018
comment
@solub Нельзя включить смешивание во фрагментном шейдере. Вам необходимо установить glBlendFunc и включить _ 2_   -  person Rabbid76    schedule 13.04.2018
comment
@ Rabbid76 Спасибо. Я просто попытался включить glBlendFunc в обработке, но, к сожалению, это не сработало (вопрос отредактирован)   -  person solub    schedule 13.04.2018
comment
@solub Вся настройка конвейера рендеринга OpenGL выполняется на стороне процессора, поэтому используйте команды в комментариях Rabbid76. Чтобы проверить, работает ли альфа, попробуйте отрендерить несколько частично перекрывающихся квадратов с разными цветами и альфой, чтобы на самом деле увидеть, работает он или нет. Как только он заработает, вы можете настроить свои шейдеры, потому что прямо сейчас у вас может быть много последовательных кирпичных стен, и ремонт одной не повлияет на результат (пока все не будет исправлено). Взгляните на OpenGL - Как создать прозрачность, не зависящую от порядка?   -  person Spektre    schedule 13.04.2018
comment
@Spektre команды увеличивают прозрачность кругов, НО установка альфа-контраста на 60 во фрагментном шейдере ничего не делает.   -  person solub    schedule 13.04.2018
comment
@solub вы можете выполнить фильтр финального прохода, в котором вы устанавливаете пороговое значение альфа-канала ... поэтому ваше визуализированное изображение проходит как текстура для QUAD, покрывая тот же размер, и визуализируется с помощью шейдера, где вы отбрасываете все фрагменты с альфа-каналом ниже порогового значения и выводите только остальную часть фрагмента (одна выборка текстуры и один оператор if), но я все еще думаю, что все это можно закодировать в буфер трафарета без использования каких-либо шейдеров, поскольку у него есть функции, более подходящие для этой работы, чем альфа-смешение   -  person Spektre    schedule 13.04.2018
comment
@solub см. У меня есть мозаичная сфера OpenGL, и я хочу вырезать в ней цилиндрическое отверстие, это пример использования STENCIL (Обычно я не использую трафарет, поэтому у меня не так много опыта с ним, но все же смог использовать его для элегантного решения той проблемы, с которой я столкнулся)   -  person Spektre    schedule 13.04.2018
comment
@solub И еще один, которому учили, если ваши эллипсы детерминированы (не перемещаются случайным образом), вы можете передать только координаты и параметры эллипса как однородные (если не слишком много) и визуализировать тупой прямо во фрагменте как сумму явного эллипса внутри тестов .. .без какого-либо трафарета blendig или пороговых значений ...   -  person Spektre    schedule 13.04.2018


Ответы (2)


@noahbuddy с Форума обработки может найти решение проблемы, поэтому я публикую его здесь.

Чтобы сохранить прозрачность, с шейдерами или без них, используйте закадровый буфер (PGraphics). Например, сохранение изображения PNG с прозрачным фоном.

Я удалил матрицу контраста из шейдера размытия @cansik и вместо этого поместил ее в отдельный фильтр.

blurfrag.glsl

// Adapted from:
// <a href="http://callumhay.blogspot.com/2010/09/gaussian-blur-shader-glsl.html" target="_blank" rel="nofollow">http://callumhay.blogspot.com/2010/09/gaussian-blur-shader-glsl.html</a>

#ifdef GL_ES
precision mediump float;
precision mediump int;
#endif


#define PROCESSING_TEXTURE_SHADER

uniform sampler2D texture;

// The inverse of the texture dimensions along X and Y
uniform vec2 texOffset;

varying vec4 vertColor;
varying vec4 vertTexCoord;

uniform int blurSize;       
uniform int horizontalPass; // 0 or 1 to indicate vertical or horizontal pass
uniform float sigma;        // The sigma value for the gaussian function: higher value means more blur
                            // A good value for 9x9 is around 3 to 5
                            // A good value for 7x7 is around 2.5 to 4
                            // A good value for 5x5 is around 2 to 3.5
                            // ... play around with this based on what you need <span class="Emoticon Emoticon1"><span>:)</span></span>

const float pi = 3.14159265;

void main() {  
  float numBlurPixelsPerSide = float(blurSize / 2); 

  vec2 blurMultiplyVec = 0 < horizontalPass ? vec2(1.0, 0.0) : vec2(0.0, 1.0);

  // Incremental Gaussian Coefficent Calculation (See GPU Gems 3 pp. 877 - 889)
  vec3 incrementalGaussian;
  incrementalGaussian.x = 1.0 / (sqrt(2.0 * pi) * sigma);
  incrementalGaussian.y = exp(-0.5 / (sigma * sigma));
  incrementalGaussian.z = incrementalGaussian.y * incrementalGaussian.y;

  vec4 avgValue = vec4(0.0, 0.0, 0.0, 0.0);
  float coefficientSum = 0.0;

  // Take the central sample first...
  avgValue += texture2D(texture, vertTexCoord.st) * incrementalGaussian.x;
  coefficientSum += incrementalGaussian.x;
  incrementalGaussian.xy *= incrementalGaussian.yz;

  // Go through the remaining 8 vertical samples (4 on each side of the center)
  for (float i = 1.0; i <= numBlurPixelsPerSide; i++) { 
    avgValue += texture2D(texture, vertTexCoord.st - i * texOffset * 
                          blurMultiplyVec) * incrementalGaussian.x;         
    avgValue += texture2D(texture, vertTexCoord.st + i * texOffset * 
                          blurMultiplyVec) * incrementalGaussian.x;         
    coefficientSum += 2.0 * incrementalGaussian.x;
    incrementalGaussian.xy *= incrementalGaussian.yz;
  }

  gl_FragColor = avgValue / coefficientSum;
}

colfrag.glsl

#define PROCESSING_TEXTURE_SHADER

uniform sampler2D texture;
varying vec4 vertTexCoord;

uniform vec4 o = vec4(0, 0, 0, -7.0); 
uniform lowp mat4 colorMatrix = mat4(1.0, 0.0, 0.0, 0.0, 
                                     0.0, 1.0, 0.0, 0.0, 
                                     0.0, 0.0, 1.0, 0.0, 
                                     0.0, 0.0, 0.0, 18.0);

void main() {
  vec4 pix = texture2D(texture, vertTexCoord.st);

  vec4 color = (pix * colorMatrix) + o;
  gl_FragColor = color;
}

sketch.pde

PShader contrast, blurry;
PGraphics buf;

void setup() {
  size(200, 200, P2D);
  buf = createGraphics(width, height, P2D);

  contrast = loadShader("colfrag.glsl");
  blurry = loadShader("blurFrag.glsl");

  // Don't forget to set these
  blurry.set("sigma", 4.5);
  blurry.set("blurSize", 9);
}

void draw() {
  background(100);

  buf.beginDraw();
    // Reset transparency
    // Note, the color used here will affect your edges
    // even with zero for alpha
    buf.background(100, 0); // set to match main background

    buf.noStroke();
    buf.fill(255, 30, 30);
    buf.ellipse(width/2, height/2, 40, 40);
    buf.ellipse(mouseX, mouseY, 40, 40);

    blurry.set("horizontalPass", 1);
    buf.filter(blurry);
    blurry.set("horizontalPass", 0);
    buf.filter(blurry);
  buf.endDraw();

  shader(contrast);
  image(buf, 0,0, width,height);
}

Лично я считаю, что золотая середина где-то находится:

  • от 8 до 11 для альфа-контраста
  • от -7 до -9 для альфа-смещения

    uniform vec4 o = vec4(0, 0, 0, -9.0); 
    uniform lowp mat4 colorMatrix = mat4(1.0, 0.0, 0.0, 0.0, 
                                         0.0, 1.0, 0.0, 0.0, 
                                         0.0, 0.0, 1.0, 0.0, 
                                         1.0, 1.0, 1.0, 11.0);
    
  • bewteen 10 и 15 для "сигмы"

  • bewteen 30 и 40 для "blurSize"

    blurry.set("sigma", 14.5)
    blurry.set("blurSize", 35)
    

Я закодировал двумерные метабалы до того, как использовать функции расстояния со знаком и алгоритмы маршевого квадрата, но я считаю это решение наиболее эффективным. С точки зрения производительности я могу отображать до 4500 мячей со скоростью 60 кадров в секунду на холсте 800x600 (проверено на рабочем столе imac начального уровня 2012 года с режимом Python).

person solub    schedule 16.04.2018

К сожалению, я не могу отладить точную проблему, но у меня есть несколько идей, которые, надеюсь, помогут вам добиться определенного прогресса:

  1. Для более простого / дешевого эффекта вы можете использовать фильтр расширения
  2. Вы можете найти другие шейдеры метаболизма на shadertoy и настроить немного кода, чтобы вы могли запустить его в Обработке

Например, https://www.shadertoy.com/view/MlcGWn станет следующим:

// https://www.shadertoy.com/view/MlcGWn

uniform float iTime;
uniform vec2 iResolution;

vec3 Sphere(vec2 uv, vec2 position, float radius)
{
    float dist = radius / distance(uv, position);
    return vec3(dist * dist);
}

void main()
{
    vec2 uv = 2.0 * vec2(gl_FragCoord.xy - 0.5 * iResolution.xy) / iResolution.y;

    vec3 pixel = vec3(0.0, 0.0, 0.0);

    vec2 positions[4];
    positions[0] = vec2(sin(iTime * 1.4) * 1.3, cos(iTime * 2.3) * 0.4);
    positions[1] = vec2(sin(iTime * 3.0) * 0.5, cos(iTime * 1.3) * 0.6);
    positions[2] = vec2(sin(iTime * 2.1) * 0.1, cos(iTime * 1.9) * 0.8);
    positions[3] = vec2(sin(iTime * 1.1) * 1.1, cos(iTime * 2.6) * 0.7);

    for (int i = 0; i < 4; i++)
        pixel += Sphere(uv, positions[i], 0.22);

    pixel = step(1.0, pixel) * pixel;

    gl_FragColor = vec4(pixel, 1.0);
}

и в обработке:

PShader shader;

void setup(){
  size(900,900,P2D);

  shader = loadShader("metaballs.glsl");
  shader.set("iResolution",(float)width/2,(float)height/2);
}
void draw(){
  shader.set("iTime", millis() * 0.001);
  shader(shader);
  rect(0,0,width,height);
}

или https://www.shadertoy.com/view/ldtSRX

// https://www.shadertoy.com/view/ldtSRX

uniform vec2 iResolution;
uniform vec2 iMouse;
uniform float iTime;

struct Metaball{
    vec2 pos;
    float r;
    vec3 col;
};

vec4 calcball( Metaball ball, vec2 uv)
{
    float dst = ball.r / (pow(abs(uv.x - ball.pos.x), 2.) + pow(abs(uv.y - ball.pos.y), 2.));
    return vec4(ball.col * dst, dst);
}

vec3 doballs( vec2 uv )
{
    Metaball mouse;
    mouse.pos = iMouse.xy / iResolution.yy;
    mouse.r = .015;
    mouse.col = vec3(.5);

    Metaball mb1, mb2, mb3, mb4;
    mb1.pos = vec2(1.3, .55+.2*sin(iTime*.5)); mb1.r = .05; mb1.col = vec3(0., 1., 0.);
    mb2.pos = vec2(.6, .45); mb2.r = .02; mb2.col = vec3(0., .5, 1.);
    mb3.pos = vec2(.85, .65); mb3.r = .035; mb3.col = vec3(1., .2, 0.);
    mb4.pos = vec2(1.+.5*sin(iTime), .2); mb4.r = .02; mb4.col = vec3(1., 1., 0.);

    vec4 ball1 = calcball(mb1, uv);
    vec4 ball2 = calcball(mb2, uv);
    vec4 ball3 = calcball(mb3, uv);
    vec4 ball4 = calcball(mb4, uv);

    vec4 subball1 = calcball(mouse, uv);

    float res = ball1.a + ball2.a + ball3.a + ball4.a;
    res -= subball1.a;
    float threshold = res >= 1.5 ? 1. : 0.;

    vec3 color = (ball1.rgb + ball2.rgb + ball3.rgb + ball4.rgb - subball1.rgb) / res;
    color *= threshold;
    color = clamp(color, 0., 1.);
    return color;
}

#define ANTIALIAS 1
void main()
{
    vec2 uv = gl_FragCoord.xy / iResolution.yy;

    vec3 color = doballs(uv);

    #ifdef ANTIALIAS
    float uvs = .75 / iResolution.y;
    color *= .5;
    color += doballs(vec2(uv.x + uvs, uv.y))*.125;
    color += doballs(vec2(uv.x - uvs, uv.y))*.125;
    color += doballs(vec2(uv.x, uv.y + uvs))*.125;
    color += doballs(vec2(uv.x, uv.y - uvs))*.125;

    #if ANTIALIAS == 2
    color *= .5;
    color += doballs(vec2(uv.x + uvs*.85, uv.y + uvs*.85))*.125;
    color += doballs(vec2(uv.x - uvs*.85, uv.y + uvs*.85))*.125;
    color += doballs(vec2(uv.x - uvs*.85, uv.y - uvs*.85))*.125;
    color += doballs(vec2(uv.x + uvs*.85, uv.y - uvs*.85))*.125;
    #endif
    #endif

    gl_FragColor = vec4(color, 1.);
}

и в обработке:

PShader shader;
PVector mouse = new PVector();
void setup(){
  size(900,900,P2D);

  shader = loadShader("metaballs.glsl");
  shader.set("iResolution",(float)width/2,(float)height/2);
}
void draw(){
  mouse.set(mouseX,mouseY);
  shader.set("iMouse", mouse);
  shader.set("iTime", millis() * 0.001);
  shader(shader);
  rect(0,0,width,height);
}
person George Profenza    schedule 16.04.2018
comment
очень мило с вашей стороны поделиться этим подходом. Кто-то на форуме обработки только что помог мне отладить проблему, поэтому я опубликую его решение и приму его как ответ, поскольку оно конкретно решает проблему. - person solub; 16.04.2018