Help creating organic looking "blobs"

Hello there!

I recently started getting to grips with Processing - and I love it so far!

I want to be able to randomly create organic looking “blob” shapes - like these (which were drawn in Illustrator): Blobs

What I have now is based on this example: Noisy Circle

The example uses perlin noise on vertices forming a circle. I’ve changed it to make curveVertex instead to make the shape softer. It works sort of okay, but it still looks a bit “edgy” and not as smooth.

I tried experimenting with bezierVertex, but it takes 6 parameters - and I’m afraid I don’t have the brain power to figure out if I can use bezierVertex to generate a random, organic shape…

This is what I have now (including my own Danish comments):

float resolution = 13;  // Antal vertices i blob (virker bedst fra 13+)
float rad = 300;  // Radius på blob
float x;  // X-koordinat på vertex
float y;  // Y-koordinat på vertex
float round = random(0, 100); // Rundhed - jo større tal, jo mere aflang

float nVal; // Noise: Værdi
float nInt = 10; // Noise: Intensitet
float nAmp = 0.4; // Noise: Udsving
float nSeed = random(0, 1000); // Noise: Unik værdi

float t = 0; // Tid passeret
float tChange = 0.01; // Hvor hurtigt tiden går

void setup() {
  size(1000, 1000);
}

void draw() {
  background(255);
  pushMatrix();  // Isolér placering til hver enkelt blob (push)
  translate(width/2, height/2);  // Placering af blob (samt bevægelse på Y-aksen)
  noiseDetail(3, 0);  // Noiseværdier
  noStroke();  // Ingen streg
  fill(100);  // Fyldfarve

  /* Oprettelse af blobform */

  beginShape();  // Begynd form
  for (float a=-1; a <= TWO_PI; a += TWO_PI/resolution) {  // Skab punkter rundt i hel cirkel (TWO_PI) og fordel dem ud fra resolution

    nVal = map(noise(cos(a)*nInt+nSeed, sin(a)*nInt+nSeed, t), 0.0, 1.0, nAmp, 1.0);  // Map noiseværdi for at matche udsvinget

    x = cos(a)*(rad+round) *nVal;  // Punktets X-koordinat (radius+rundhed)
    y = sin(a)*rad *nVal;  // Punktets Y-koordinat

    curveVertex(x, y);  // Opret curveVertex punkt ud fra koordinater
  }
  endShape(CLOSE);  // Afslut form

  popMatrix();  // Isolér placering til hver enkelt blob (pop)

  t += tChange;
}

How do I make the shapes more fluid, smooth and rounded?

All comments are highly appreciated. :slight_smile:

1 Like

Hi @Hetoft,

Do you want your final output to be a static image or an animation ?

Hi solub,

Thanks for replying. Animated is ultimately the end goal, but static would be fine as a start.

There are different ways to achieve that organic look you’re looking for but I’m afraid that noising a circle isn’t the best option.

The edges on your example sketch do look “smooth” (no need to use bezierVertex()) but the reason you’re not satisfied with it is because the height around the center is lacking contrast / variation. Based on the picture you provided I’d say that “splatter effect” is what you’re actually after.

Still, we can try to change your sketch a bit. Modifying the noise function and do some scaling will somewhat improve both the motion and the shape of the circle:

Noicy%20circle

see motion here

(please note that I’m using Python mode but it should be really easy to port it to Java)

n_points, radius = 200, 300
angle = radians(360) / n_points
factor = .2

def setup():
    size(800, 600, P2D)
    noStroke()
    smooth(8)

    beginShape() #Avoid calling beginShape() at each iteration 
        
def draw():
    background(255)

    translate(width>>1, height>>1)
    
    for e in range(n_points+1):
        x = cos(angle * e) * radius
        y = sin(angle * e) * radius 
        p = PVector(x, y).normalize()
        n = map(noise(p.x * factor + frameCount*.01, p.y * factor + frameCount*.01), 0, 1, 10, 200)
        p.mult(n)
        fill(100 - e / 10 - abs(p.y), (90 + e) - (p.y/4), 220)
        vertex(p.x, p.y)
    endShape(CLOSE)

In order to get a more blob looking shape I would suggest different approaches:

1/ Iso-surface (Metaballs)

Daniel Shiffman did a coding challenge on Metaballs a while ago. You can implement your own distance field based on his code or you can chose to use a library instead. I’ve found Computational Geometry to be one of the easiest to use for this purpose.

(see the bunddled examples for Java sketches)

add_library('ComputationalGeometry')

def setup():
    size(800, 500, P3D)
    rectMode(CENTER)
    background(255)
    noStroke()
    smooth(8)
    
    iso1 = IsoContour(this, PVector(), PVector(width, height), 200, 125) #last 2 int are the number of columns and rows. The higher, the better the resolution
    iso2 = IsoContour(this, PVector(), PVector(width, height), 200, 125)
    thresh = .0004 #the lower, the bigger the blobs
    
    for i in range(20):
        p1 = PVector(random(50, width - 50), random(height), 0)
        p2 = PVector(random(50, width - 50), random(height), 0)
        iso1.addPoint(p1)
        if i> 5: iso2.addPoint(p2)
        
    
    fill('#002d6b')
    rect(width>>1, height>>1, width - 120, height)
    
    fill('#e01a2c')
    iso1.plot(thresh)
    fill('#ffc20e')
    iso2.plot(thresh)

2 majors drawbacks:

  • because of the need to compute distances from each point on the grid to the ball (and this, for each ball), the algorithm is computationally expensive. Hence the difficulty to make an animation, unless you decide to implement it in a shader.
  • because it is making use of the whole array of pixels (loadPixels / updatePixels in its naive implementation) it is not possible to select the color of a specific ball.

2/ "Gooey Effect"

Visually similar to metaballs, it is a technique coming from the Web design community that consists in applying SVG filters (gaussian blur + high contrast in the alpha channel) on moving ellipses in order to get blob-like motions.

Example of a gooey effect implemented in three.js by Misaki Nakano. See it live here

Although the melting effect doesn’t look as good as in the original Javascript version, noahbuddy from the forum did a fantastic job trying to port that technique in Processing with a shader.

(See original Java version here)

def setup():
    global buf, balls, contrast, blurry
    size(1240, 720, OPENGL)
    rectMode(CENTER)
    noStroke()
    smooth(8)

    buf = createGraphics(width, height, P2D)
    contrast = loadShader("colFrag.glsl")
    blurry = loadShader("blurFrag.glsl")
    
    blurry.set("sigma", 10.5)
    blurry.set("blurSize", 30)
    
    balls = [Ball() for e in range(40)]

def draw():
    background(255)
    
    fill('#002d6b')
    rect(width>>1, height>>1, width - 120, height)
    
    buf.beginDraw()
    buf.background(190, 0)
    buf.noStroke()
    for b in balls:
        b.update()
        b.render()
        
    blurry.set("horizontalPass", 1)
    buf.filter(blurry)
    blurry.set("horizontalPass", 0)
    buf.filter(blurry)
    buf.endDraw()
    
    shader(contrast)
    image(buf, 0, 0, width, height)
    
             
class Ball(object):
    def __init__(self):
        self.loc = PVector(random(width), random(height), 0)
        self.vel = PVector.random2D()
        self.radius = random(60, 140)
        self.c = [[224,26,44], [255,194,14]]
        self.r = int(random(2))
        
    def update(self):
        self.loc.add(self.vel)
        
        if self.loc.x > width or self.loc.x < 0: self.vel.x *= -1
        if self.loc.y > height or self.loc.y < 0: self.vel.y *= -1
        
    def render(self):
        buf.fill(self.c[self.r][0], self.c[self.r][1], self.c[self.r][2])
        buf.ellipse(self.loc.x, self.loc.y, self.radius, self.radius)
colFrag.glsl
#define PROCESSING_TEXTURE_SHADER
 
uniform sampler2D texture;
varying vec4 vertTexCoord;
 
uniform vec4 o = vec4(0, 0, 0, -8.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, 8.0);
 
void main() {
  vec4 pix = texture2D(texture, vertTexCoord.st);
 
  vec4 color = (pix * colorMatrix) * 1.15 + o ;
  gl_FragColor = color;
}
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;
}

3/ Physics (springs + repulsion behaviors)

Another possibilty would be to connect particles (displayed around a circle) with springs and give them a repulsion force from each other that is proportional to distance.


(blob by Zach Lieberman, taken from this post)

see motion here

I remember trying to implement it with the toxiclibs library but failed eventually because it wa impossible to give particles an individual repulsion force that would target another specific particle. So, if you decide go down that road I would suggest to code your own physics engine from scratch (check these videos on attractors and springs from Daniel Shiffman).

4/ Differential Growth

Probably the more elaborate solution but mentionning it here just for brainstroming. It starts the same way (particles with repulsion force connected by springs around a circle) except new cells (particles) are randomly added between pre-existing cells as the sketch is iterating. Very quickly (during the early stages of the growth) the circle turns into a blob like shape with that splatter effect you’re looking for:

Check the fantastic tutorials from Satoru Sugihara for more info.

Drawbacks:

  • can be tricky to implement (managing / updating the connections)
  • the order of the cells needs to be re-computed every time a cell division occurs (only if you want to fill the shape)
  • becomes slow at some point
9 Likes

Woooooow, thank you so, so much for your detailed explanations and tryouts. A bit overwhelming, haha! Interesting to see what possibilites you have with Processing/P5 and the seemingly endless range of options…

The Iso-surface comes closest to the shapes I want, but since I want it animated (and only have so much time to work on this), I think I’ll stick to the first solution. I’ll try to port what you wrote into Java. Really appreciate your work on this!