3

I'm trying to replicate a web design trick known as "gooey effect" (see it live here). It's a technique applying SVG filters on moving ellipses in order to get a blob-like motion. The process is rather simple:

  • apply a gaussian blur
  • increase the contrast of the alpha channel only

The combination of the two creates a blob effect

enter image description here enter image description here

The last step (increasing the alpha channel contrast) is usually done through a "color matrix filter".

A color matrix is composed of 5 columns (RGBA + offset) and 4 rows.

The values in the first four columns are multiplied with the source red, green, blue, and alpha values respectively. The fifth column value is added (offset).

In CSS, increasing the alpha channel contrast is as simple as calling a SVG filter and specifying the contrast value (here 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" />

In Processing though, it seems to be a bit more complicated. I believe (I may be wrong) the only way to apply a color matrix filter is to create one in a shader. After a few tries I came up with these (very basic) vertex and fragment shaders for color rendering:

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

PROBLEM:

The color matrix is partially working: changing the RGB values do affect the colors but changing the alpha values (last row) don't !

When trying to combine the shader with a Gaussian filter, the drawn ellipse stays blurry even after I set the alpha channel contrast to 60 (like in the codepen example):

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

The same thing happens when I implement the color matrix within @cansik 's Gaussian blur shader (from the PostFX library). I can see the colors changing but not the alpha contrast:

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

Setting glBlendFunc and enabling glEnable(GL_BLEND) in the main .pde file didn't fix the issue either.

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

Questions:

  • Why does the alpha channel contrast not work ? How can I make it work ?
  • Is there something wrong with the way I implemented the color matrix ?
  • Do you know a better way to implement that gooey effect ?

Any help would be much appreciated !

Thank you

Glorfindel
  • 21,988
  • 13
  • 81
  • 109
solub
  • 1,291
  • 17
  • 40
  • This only addressed the last question, but another option is to use metaballs. – Luple Apr 12 '18 at 22:54
  • Thanks for the suggestion, I have already coded metaballs. I want to try a new (and faster) approach. – solub Apr 12 '18 at 22:57
  • Okay! I haven't worked with metaballs before, but they are on the todo list. This just got added, thanks! – Luple Apr 12 '18 at 23:00
  • 1
    @solub I am not familiar with processing nor the packages you are using but: Alpha channel in **OpenGL** is only used if `GL_BLEND` is enabled and only if `glBlendFunc` uses alpha channel and if source Alpha is used then you must have also Alpha buffer while creating pixelformat of your context. So my bet is your blur is not using alpha at all. Using different shader could mean different in/out layouts and uniforms so you must match your CPU side code with it otherwise shader will not work properly. Btw maybe you could use Stencil for this to treshold blurred image into solid one somehow – Spektre Apr 13 '18 at 07:50
  • @Spektre I think you're onto something here but I can't figure out how to enable that blend function in the blurFrag shader I posted (question edited). – solub Apr 13 '18 at 13:44
  • 2
    @solub Blending can't be enabled in the fragment shader. You have to set [`glBlendFunc`](https://www.khronos.org/registry/OpenGL-Refpages/es3.0/html/glBlendFunc.xhtml) and enable [`glEnable(GL_BLEND)`](https://www.khronos.org/registry/OpenGL-Refpages/es3.0/html/glEnable.xhtml) – Rabbid76 Apr 13 '18 at 14:48
  • @Rabbid76 Thanks. I just tried to enable glBlendFunc in Processing but unfortunately it didn't work (question edited) – solub Apr 13 '18 at 15:57
  • @solub All the OpenGL rendering pipeline setup is done on CPU side code so use commands in Rabbid76's comments. To test if alpha is working try to render few partially overlapping squares with different colors and alpha to actually see if it is working or not. Once got it working then you can tweak your shaders because right now you can have many consequent brick walls and repairing one will not affect result (until all the stuff is correct) take a look at [OpenGL - How to create Order Independent transparency?](https://stackoverflow.com/a/37783085/2521214) – Spektre Apr 13 '18 at 16:07
  • @Spektre the commands do increase the transparency of the circles BUT setting the alpha contrast to 60 in the fragment shader doesn't do anything. – solub Apr 13 '18 at 16:26
  • @solub you can do final pass filter where you threshold alpha channel ... so your rendered image pass as texture for QUAD covering the same size and render with shader where you discard all fragments with alpha below threshold and output only the rest of the fragment (one texture fetch and one if statement) but I still think this all could be encoded into stencil buffer without any shaders use as it has features more suited for this job than alpha blending – Spektre Apr 13 '18 at 16:51
  • @solub see [I have an OpenGL Tessellated Sphere and I want to cut a cylindrical hole in it](https://stackoverflow.com/a/39466130/2521214) it is an example of STENCIL use (I do not usually use stencil so I have not much experience with it but still was able to use it to solve that problem I was facing elegantly) – Spektre Apr 13 '18 at 16:55
  • @solub And another taught if your ellipses are deterministic (does not move on random) you could pass just the ellipse coordinates and parameters as uniform (if not too many) and render the goofy directly in fragment as sum of explicit ellipse inside tests ... without any blendig stencil or thresholding ... – Spektre Apr 13 '18 at 17:02

2 Answers2

4

@noahbuddy from the Processing Forum could find a solution to the problem so I'm posting it here.

To preserve transparency, with or without shaders, use an offscreen buffer (PGraphics). For example, saving a PNG image with transparent background.

I removed the contrast matrix from @cansik 's blur shader and instead put it into a separate filter.

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

Personally I think the sweet spot lies somewhere:

  • between 8 and 11 for the alpha contrast
  • between -7 and -9 for the alpha offset

    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 and 15 for "sigma"

  • bewteen 30 and 40 for "blurSize"

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

I've coded 2d metaballs before using signed distance functions and marching square algorithms but I find this solution to be the most efficient one. Performance wise I can display up to 4500 balls at 60 fps on a 800x600 canvas (tested on an entry-level 2012 imac desktop with Python Mode).

solub
  • 1,291
  • 17
  • 40
0

Unfortunately I'm not able to debug the exact issue, but I have a couple of ideas that hopefully might help you make some progress:

  1. For a simpler/cheaper effect you can use the dilate filter
  2. You can find other metaballs shaders on shadertoy and tweak the code a bit so you can run it in Processing

For example https://www.shadertoy.com/view/MlcGWn becomes:

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

and in Processing:

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

or 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.);
}

and in Processing:

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);
}
George Profenza
  • 50,687
  • 19
  • 144
  • 218
  • that's super nice of you to share this approach. Someone on the Processing Forum just helped me debug the issue so I'll post his solution and accept it as an answer since it specifically addresses the problem. – solub Apr 16 '18 at 16:41