How to code a 3d metaball ?

edited April 9 in Python Mode

Hi everyone,

Among the various coding challenges I set for myself there's one at the top of my list that still remains obscure and rather complex to me: coding a 3d metaball.

Since I first saw Zack Lieberman's work on 3d blobs and metaballs I've been obssessed with them.

made by Zack Lieberman with OpenFrameworks

made by Satoshi Horii (Rhizomatics) probably with OpenFrameworks

made by Mike Walczyk with OpenFrameworks and rendered in TouchDesigner (? not sure)

made by Martin Meurin with Processing

I've read the wikipedia page, I know from other sources that it may involve Ray Marching shaders and Signed distance Functions but the problem is I haven't been able to find a source code, even a snippet, to work on. I'm relatively new to Processing and starting from scratch for this challenge seems really tough.

I would love to read your suggestions and advices. If you have any link to tutorials, step-by-step roadmaps or even code in Java, Python or even C++ I would really appreciate if you could share it here.

Thank you !

Answers

  • Did you read the 2D metaball? It’s essentially a heat map of summing up distances to the balls.

    if you think this in 3D you would have a 3D heat map. That means triple nested for loop over a section of space and adding up colors following distance to balls.

  • Of course very slow... But okay to make a movie

  • edited April 4

    Of course I've read it but it seems that coding a 3d metaball is slightly more complicated than just adding another dimension to the 2d version.

    The solution you suggest is computationnally very expensive and doesn't take into account the tracing of the metaball edges.

    Quoting Zack Lieberman:

    Metaballs is an old school computer graphics algorithm where you define shapes via an implicit functions and use a technique like marching cubes to trace the edges of those functions on some sort of grid.

    Quoting Mike Walczyk:

    The trick is to use signed distance functions or SDF. These are mathematical functions that take a point in space and tell you how far that point is from a surface.

    In order to render the scene, we are going to use a technique called ray marching. At a high level, we will shoot out a bunch of imaginary rays from a virtual camera that is looking at our world. For each of these rays, we are going to "march" along the direction of the ray, and at each step, evaluate our SDF. This will tell us: "from where I currently stand, how far am I from the closest point on the surface of our sphere?"

    More on his blog post

  • edited April 4

    It looks like Walczyk's blog post and sample code is about implementing this particular approach inside a GLSL shader.

    Have you looked at the PShader tutorial for Processing?

  • edited April 5

    @kfrajer @jeremydouglass sorry for not replying I'm doing some research. I'll comment back as soon as I'm finished with this metaball thing. (Thank you both for the links. It seems the ray marching technique is more widespread than I thought. Not sure yet if I'll implement it in a shader)

  • edited April 9

    Update: I've managed to make a bloby metaball (sort of fusion of several metaballs) using the toxclibs library.

    But there's still one major thing I can't figure out:

    • How to give each ball a distinct color ?

    On a similar issue with the toxiclibs library, @amnon replied:

    TriangleMesh does not support colored faces. You have to either extend the class or store this information externally and then draw each face individually with it's connected color.

    Following this answer I tried to draw each face individually:

     beginShape(TRIANGLES)
        num = mesh.getNumFaces()
        for e in range(num):
            f = mesh.faces[e]
            fill(e * .01 , 82 , e*.03) 
            vertex(f.a.x(), f.a.y(), f.a.z())
            vertex(f.b.x(), f.b.y(), f.b.z())
            vertex(f.c.x(), f.c.y(), f.c.z())
        endShape()
    

    It becomes then possible to draw different colors depending on the face's index, however it still doesn't tell me how to give each ball a different color

    Do you guys have any idea how to solve this problem ?

    Full code:

    add_library('toxiclibs_p5')
    add_library('toxiclibscore')
    add_library('volumeutils')
    add_library('peasycam')
    from toxi.processing import ToxiclibsSupport
    
    mesh = TriangleMesh("mesh")
    iso, grid, dim = .5, 50, 800
    scl = Vec3D(dim, dim, dim)
    volume = VolumetricSpaceArray(scl, grid, grid, grid)
    surface, brush = ArrayIsoSurface(volume), RoundBrush(volume, scl.x() / 2)
    
    def setup():
        global points, gfx, a
        size(720, 720, OPENGL)
    
        cam, gfx, a = PeasyCam(this, 800), ToxiclibsSupport(this), Attractor(0, 0, 0, 1, 100)
        cam.rotateY(4)
        points = []    
        [points.append(Point()) for e in range(10)]
        noStroke()
    
    def draw():
        background(0)
        lights()
    
        volume.clear()
        for i, p in enumerate(points):
            force = a.attract(p)
            p.applyForce(force)
            p.update()
            p.display()
        volume.closeSides()
    
        surface.reset()
        surface.computeSurfaceMesh(mesh,iso)
        mesh.computeVertexNormals()
        gfx.mesh(mesh, True)
    
        # beginShape(TRIANGLES)
        # num = mesh.getNumFaces()
        # for i in range(num):
        #     f = mesh.faces[i]
        #     fill(100 + i * .01 , 182 , 100 + i*.03) 
        #     vertex(f.a.x(), f.a.y(), f.a.z())
        #     vertex(f.b.x(), f.b.y(), f.b.z())
        #     vertex(f.c.x(), f.c.y(), f.c.z())
        # endShape()
    
    
    class Point(object):
        def __init__(self):
            self.r = random(15, 140)
            self.pos = PVector(random(-100,100), random(-100,100), random(-100, 100))
            self.vel = PVector(0, 0, 0)
            self.acceleration = PVector(0, 0, 0)
            self.mass = 1 + (self.r * .006)
    
        def display(self):
            v = Vec3D(self.pos.x, self.pos.y, self.pos.z)
            brush.setSize(self.r)
            brush.drawAtAbsolutePos(v, 1)
    
        def applyForce(self, f):
            f = f / self.mass
            self.acceleration += f
    
        def update(self):
            self.vel += self.acceleration
            self.pos += self.vel
            self.acceleration *= 0
            self.vel.limit(10)
    
    class Attractor():
        def __init__(self, x, y, z, m, g):
            self.location = PVector(x, y, z)
            self.mass = m
            self.G = g
    
        def attract(self, m):
            force = PVector.sub(self.location, m.pos)
            d = force.mag()
            force.normalize()
            strength = (self.G * self.mass * m.mass) / (d * d)
    
            if keyPressed:
                force.mult(-strength*2)
    
            return force
    
  • edited April 9

    What bugs me is that although I'm drawing multiple objects (from line 28) toxiclibs won't allow to assign different colors.

    For example, if I store different colors in a list:

    colors = [[64,165,231],[253,194,57], [122,208,136], [225,134,195], [230,75,49], [64,165,231],[253,194,57], [122,208,136], [225,134,195], [230,75,49]]
    

    And then specify within the for loop (from line 28) something like:

    fill(colors[i][0],colors[i][1],colors[i][2])

    ... I still end up with 10 balls filled with the same color.

  • edited April 9

    Are you referring to lines 40-48? fill() doesn't affect vertex-vertex in TRIANGLES. It only gets used once at endShape() -- so you can change it as many times as you want in the loop while specifying points; the state of the color won't matter until the final value. To have fill take effect in that way you would need to call fill(); triangle(); on each iteration of the loop, not a bunch of vertex points in a beginShape() / endShape.

  • @jeremydouglass I was referring to line 28:

    for i, p in enumerate(points): force = a.attract(p) p.applyForce(force) p.update() p.display()

    points here refer to the balls. So I was thinking that for each ball I could assign a specific color:

    for i, p in enumerate(points): force = a.attract(p) p.applyForce(force) p.update() p.display() fill(colors[i][0],colors[i][1],colors[i][2])

    You're suggesting to call triangle() but this function only takes 2d coordinates, not 3d (here f.a.x(), f.a.y() and f.a.z()).

  • edited April 9

    Re: triangle() ... "this function only takes 2d coordinates"

    Fair enough: for 3D you could also use a shape TRIANGLES that is only a single vertex triple, or use a polygon. So "draw each face individually with it's connected color" would involve a separate beingShape / endShape inside the loop for each vertex triple.

    For line 28, does p.display() -- that is, a call to ArrayIsoSurface.drawAtAbsolutePos() -- care about fill() as a parameter? Or is it just generating mesh geometry, not rendering? I'm not familiar with toxiclibs, but if after breaking out your loop into separate PShapes you are still getting one color, then color-dependent rendering might actually be happening in 35-38.

    Possibly relevant:

  • edited April 10

    "...if after breaking out your loop into separate PShapes you are still getting one color, then color-dependent rendering might actually be happening in 35-38."

    That's exactly what I thought so I included these lines in the loop, trying each and every configuration possible. The following snippet seems to draw different colors:

    for i, p in enumerate(points):
            force = a.attract(p)
            p.applyForce(force)
            p.update()
            p.display()
    
            surface.computeSurfaceMesh(mesh,iso)
            mesh.computeVertexNormals()
            gfx.mesh(mesh, True)
            fill(colors[i][0],colors[i][1],colors[i][2])
    

    But I get strange glitch-like results (not to mention the sketch running super slow). From the outside the metaballs seem to be covered with one single color (some triangle faces are visible):

    From the inside, you can clearly see that each ball has in fact its own color. However they do not "melt" organically:

    And by "organically" I mean something like this:

    I'm really lost here. How such a simple thing can be made that difficult in toxiclibs.

  • edited April 10

    @kfrajer @jeremydouglass Since my script is heavily depending on the tocixlibs library, would you guys mind if I post a question related to this metaball in the Library Questions" category ? I was hoping that maybe amnon or toxmeister would help even though they both don't seem to be very active on this forum anymore.

  • That makes sense. Perhaps link back to this thread in your new question -- or just move this thread into Library questions, having already tagged @amnon or @toxmeister, and see if continuing this thread there helps resolve the issue.

  • edited April 11

    Thanks Jeremy. I prefer to open a new thread to be as clear as possible.

Sign In or Register to comment.