Meta balls shader

Interesting. It’s definitely supported hardware wise on iPad2 cos that’s what I have, I guess it’s iOS 5, I am running 6.

Yes @spacemonkey ,that extension was added with iOS6 beta :slight_smile:

After about two hours of coding, my first Metaball shader:

-- Metaballs-4

--blendMode(ADDITIVE)

-- Use this function to perform your initial setup
function setup()
    print("Hello World!")
    
    m = {}
    
    parameter.boolean("Use_Shader", true, function()
        if Use_Shader then
            m.shader = shader(vS, fS)
        else
            m.shader = nil
        end
    end)
    
    parameter.boolean("Animated", false)
    
    parameter.boolean("Show_Preview", true)
    
    parameter.integer("Number_of_Balls", 1, 1000, 25, function()
        balls = {}
        ballData = {}
        for i=1,Number_of_Balls do
            table.insert(balls, vec2(math.random(0, WIDTH), math.random(0, HEIGHT)))
            table.insert(ballData, vec2(math.random(-1, 1), math.random(-1, 1)))
        end
    end)
    
    parameter.integer("Metaball_Size", 1, 200, 100)
    
    parameter.integer("Metaball_Resolution", 1, 255, 255)
    
    parameter.action("Register Resolution (May lag!)", function()
        GENERATE_METABALL()
    end)
    
    parameter.integer("FPS_Smoothing", 1, 100, 50)
    
    parameter.action("Restart", function()
        balls = {}
        setContext(img)
        background(0)
        setContext()
        restart()
    end)
    
    function GENERATE_METABALL()
        
        local mr = Metaball_Resolution
        local ms = Metaball_Size
    
    blendMode(NORMAL)
    
    ballTex = image(200, 200)
    setContext(ballTex)
    pushStyle()
    noStroke()
    for i = 255, 0, -(256 - mr) do
    --[[
    fill(128)
    ellipse(100, 100, 100, 100)
    fill(191)
    ellipse(100, 100, 75, 75)
    fill(255)
    ellipse(100, 100, 25, 25)
    popStyle()
    --]]
    
    fill(255 - i, 255 - i)
    local K = 1000
    ellipse(100, 100, (i / 255) * ms, (i / 255) * ms)
    
    --print(i)
    --print(255 - i, 255 - i)
    --print((i / 255) * 100)
    
    --setContext()
    end
    
    setContext()
    
    end
    
    GENERATE_METABALL()
    
    --blendMode(ADDITIVE)
    
    balls = {}
    ballData = {}
    for i=1,Number_of_Balls do
        table.insert(balls, vec2(math.random(0, WIDTH), math.random(0, HEIGHT)))
        table.insert(ballData, vec2(math.random(-1, 1), math.random(-1, 1)))
    end
    
    img = image(WIDTH, HEIGHT)
    
    m = mesh()
    r = m:addRect(WIDTH / 2, HEIGHT / 2, WIDTH, HEIGHT)
    m:setRectTex(r, 0, 0, 1, 1)
    m.texture = img
    if Use_Shader then
        m.shader = shader(vS, fS)
    end
    
    FPSHistory = {}
end

-- This function gets called once every frame
function draw()
    blendMode(ADDITIVE)
    
    output.clear()
    table.insert(FPSHistory, 1 / DeltaTime)
    
    while #FPSHistory > FPS_Smoothing do
        table.remove(FPSHistory, 1)
    end
    
    local result = 0
    
    for k,v in ipairs(FPSHistory) do
        result = result + v
    end
    
    print(result / #FPSHistory)
    
    -- This sets a dark background color 
    background(0)

    -- This sets the line thickness
    strokeWidth(5)

    -- Do your drawing here
    if Animated then
        for i = 1, Number_of_Balls do
            local b = balls[i]
            local bd = ballData[i]
            
            if b.x > WIDTH then
                bd.x = -math.abs(bd.x)
            end
            
            if b.x < 0 then
                bd.x = math.abs(bd.x)
            end
            
            if b.y > HEIGHT then
                bd.y = -math.abs(bd.y)
            end
            
            if b.y < 0 then
                bd.y = math.abs(bd.y)
            end
            
            b.x = b.x + bd.x
            b.y = b.y + bd.y
        end
    end
    
    setContext(img)
    background(0)
    for k,v in ipairs(balls) do
        sprite(ballTex, v.x, v.y)
    end
    setContext()
    m.texture = img
    m:draw()
    
    if Show_Preview then
        blendMode(NORMAL)
        
        fill(0)
        
        stroke(127)
        
        strokeWidth(5)
        
        rect(0, 0, 200, 200)
        
        noStroke()
        
        sprite(ballTex, 100, 100)
        
        fill(255)
        font("Inconsolata")
        fontSize(12)
        textMode(CENTER)
        text("Metaball Compution Preview:", 100, 150)
    end
end

function touched(touch)
    if touch.state == BEGAN or touch.state == MOVING then
        if balls[Number_of_Balls + 1] == nil then
            table.insert(balls, vec2(touch.x, touch.y))
        else
            balls[Number_of_Balls + 1] = vec2(touch.x, touch.y)
        end
    end
end

vS = [[
//
// A basic vertex shader
//

//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;

//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

//This is an output variable that will be passed to the fragment shader
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;

void main()
{
    //Pass the mesh color to the fragment shader
    vColor = color;
    vTexCoord = texCoord;
    
    //Multiply the vertex position by our combined transform
    gl_Position = modelViewProjection * position;
}
]]

fS = [[
//
// A basic fragment shader
//

//Default precision qualifier
precision highp float;


//This represents the current texture on the mesh
uniform lowp sampler2D texture;

//The interpolated vertex color for this fragment
varying lowp vec4 vColor;

//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;

void main()
{
    //Sample the texture at the interpolated coordinate
    lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;
    
    //Set the output color to the texture color
    if (max(col.r, max(col.g, col.b)) > 0.75)
    {
        gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
    }
    else
    {
        gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
    }
}
]]

It only gets about 10 FPS… At a thousand Metaballs. :stuck_out_tongue:

Mine uses the simplest math to calculate bridges…none. It just draws a bunch of shaded balls on the screen with the ADDITIVE blend mode. Then it has a shader check if a pixel is bright enough, if it is it renders it white, if it isn’t it renders it transparent. Turn off the shader using the parameter to see what I mean. The shader just filters out the gradients.

Sorry for lots of commented-out lines and messy code. I’m too lazy to clean it up. :stuck_out_tongue:

Steady 50 FPS at a hundred Metaballs. :wink:

I really like this piece of code, since there is a generated texture you could add a noise to give more “water” appearance

Yep. I’m also gonna work on a shader for the screen, that replaces the white with a specific texture.

Here’s one where I encode color across the falloff (dark blue middle, light blue ring, then yellow falloff). Somethings not right that I haven’t debugged, you might notice some strange internal artifacts, but it pretty much works. It’s slower though… Probably would be better to colorise on a final pass perhaps.

Code tweaked because I realised colorising the source falloff texture was unecessary and caused more calculation


displayMode(STANDARD)
function setup()
    displayMode(FULLSCREEN)
    balls = {}
    touches = {}
    nextball = 1
        
    base = physics.body(EDGE, vec2(100,0), vec2(WIDTH-100,0))
    
    --create a texture for the falloff curve
    size = 500
    adjustment = 5
    img = image(size,size)
    setContext(img)
    
    for i=size/2+adjustment,adjustment+1,-1 do
        fill(255*1/((i/15)^2),255)
        ellipse(size/2,size/2,(i-adjustment)*2)
    end
    
    ballSize = 25
    
    ballMesh = mesh()
    ballMesh.shader  = shader(aballShader.vertexShader, aballShader.fragmentShader)
    --ballMesh.shader = shader("Documents:layershade")
    ballMesh:addRect(0, 0, ballSize, ballSize)
    ballMesh:setRectTex(1, 0,0,1,1)
    ballMesh.texture = img
    --ballMesh:setRect(rIdx, 0, 0, ballSize, ballSize)
    firstPass = image(WIDTH,HEIGHT)
end

function touched(touch)
    if touch.state == ENDED then
        touches[touch.id] = nil
    else
        --if touches[touch.id] == nil then
        touches[touch.id] = touch
        --end
    end    
end    

function touchActions()
    for k,v in pairs(touches) do 
        if CurrentTouch.state == ENDED then
            --if there are no current touches then we kill all current touches to avoid bugged ball producers
            touches[k] = nil
        else

            --add a new ball at the touch location
            size = math.random(1,20)
            tspot = physics.body(CIRCLE, size)
            tspot.position = vec2(v.x+math.random(-1,1), v.y+math.random(-1,1))
            tspot.restitution = 0.95

            balls[nextball] = { tspot = tspot, size = size * 2, r = math.random(30,255), g = math.random(30,255), b = math.random(30,255) }

            nextball = nextball + 1
        end    
    end
end

function draw()

    background(0)
    --sprite(img,WIDTH/2,HEIGHT/2,1000,1000)
    for k,v in pairs(balls) do
        if v.tspot.x < -20 or v.tspot.x > WIDTH + 20 or v.tspot.y < -20 then
            balls[k].tspot:destroy()
            balls[k] = nil  
        else
            resetMatrix()
            translate(v.tspot.x,v.tspot.y)
            scale(v.size)

            --[[
            ballMesh.shader.mModel = modelMatrix()
            ballMesh.shader.vEyePosition = vec4(v.tspot.x,v.tspot.y,250,0)
            ballMesh.shader.vLightPosition = vec4(v.tspot.x,v.tspot.y,250,0)
            ]]
            ballMesh:setColors(color(v.r,v.g,v.b,255))

            ballMesh:draw()
            --fill(v.r, v.g, v.b, 255)
            --ellipse(v.tspot.x, v.tspot.y, v.size)
        end
    end
    touchActions()
end

aballShader = {
vertexShader = [[
//
// A basic vertex shader
//

//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;

//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec2 texCoord;

//This is an output variable that will be passed to the fragment shader
varying highp vec2 vTexCoord;

void main()
{
    //Pass the mesh color to the fragment shader
    vTexCoord = texCoord;
    
    //Multiply the vertex position by our combined transform
    gl_Position = modelViewProjection * position;
}
]],
fragmentShader = [[
#extension GL_EXT_shader_framebuffer_fetch : require
//
// A basic fragment shader
//

//Default precision qualifier
precision highp float;

//This represents the current texture on the mesh
uniform lowp sampler2D texture;

//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;

float numberize(lowp vec3 col)
{
    float value;
    if (col.z == 0.0) {
        //this is level 1
        value = col.x;
    }
    else if (col.x > 0.5) {
        //this is level 2
        value = 1.0 + col.z;
    }
    else {
        //this is level 3
        value = 2.0 + (1.0-(col.z - 0.5)*2.0); 
    }
    return value;
}

lowp vec4 colorize(float value)
{
    lowp vec4 col;
    if (value <= 1.0) {
        //level 1
        col = vec4(value,value,0.0,1.0);
    }
    else if (value <= 2.0) {
        //level 2
        col = vec4(1.0 - (value - 1.0)/2.0, 1.0 - (value - 1.0)/2.0, value - 1.0, 1.0);
    }
    else if (value <= 3.0) {
        col = vec4(0.49, 0.49, 1.0 - (value - 2.0)/2.0, 1.0);
    }
    else {
        col = vec4(0.49,0.49,0.49, 1.0);
    }
    return col;
}

void main()
{
    //Sample the texture at the interpolated coordinate
    float curVal = numberize(gl_LastFragData[0].xyz);
    //curVal = curVal + numberize(texture2D( texture, vTexCoord ).xyz);
    curVal = curVal + texture2D( texture, vTexCoord ).x * 3.0;
    
    lowp vec4 col = colorize(curVal);
    //vec4(0.0,0.0,curVal,1.0);    
    //col.xyz += texture2D( texture, vTexCoord ).xyz;
    //col.a = 1.0;
    //Set the output color to the texture color
    gl_FragColor = col;

}
]] }

.@spacemonkey I have exported the code to a project and then run it in an iPhone4 with iOS6.1.1 and it appears the next warning:

Shader compile log:
WARNING: 0:58: Overflow in implicit constant conversion, minimum range for lowp float is (-2,2)

But it worked!, so weird when the balls are created :smiley:

I cant understand how are you coloring the balls, where are you defining the “power of yellow”? I would like to change it and try with another layer in multiply mode…

Here it is a video:
http://www.youtube.com/watch?v=9qFaER90pm4

@jauxix I think iPhone doesn’t support high precision, change the shader line: precision highp float; to precision mediump float; and it might work.

For the coloring it’s the colorize and numberize that do the work. Effectively I treat the strength in a pixel as a number from 0.0 - 3.0. It encodes 0.0 - 1.0 as black through to bright yellow, then 1.0 - 2.0 as the yellow declining to half strength while blue comes to full strength, and finally 2.0 to 3.0 as the blue declining to half strength (ie ending in mid grey).

Colorize builds the right color for this in a fairly clear way. The numberize is doing the reverse, but it looks at the various values of the color to decode it. So if the blue is 0.0 then I know it must be in the 0.0 - 1.0 range as the other 2 ranges include blue the strength within that range (decimal part) is then just the red or green value. If the blue has some value, and the red/green is still strong than 0.5 then it must be in 1.0 - 2.0 as that finishes with the red/green just below 0.5 the decimal part is then just blue. Finally if red/green is below 0.5 it must be in the 3rd range 2.0 - 3.0 and the strength will be where the blue is in it’s change from full strength to half strength.

I dont understand…so, if we want to turn it like the @Zoyt version here:
http://twolivesleft.com/Codea/Talk/discussion/3200/realistic-2d-water-effect
we need to change the values to encode blue in stead of yellow, in the numberize function or the colorize? >:D<

Hmmm… I described my algorithm, I haven’t looked at the other code (and I don’t have my ipad here today), but you need to conceptually come up with a colour pattern you like and then think how you can translate from that color pattern to a strength and back. Then those algorithms for translation get put into numberize (color in, strength out) and colorize (strength in, color out).

If you do an image of the kind of color gradient you want to use I’d happily look at it and see if I can implement it.

Ok , @spacemonkey , take a look to this:

http://appaddict.net/wordpress/wp-content/uploads/2011/10/wheres-my-water_449735650_ipad_02.jpg

Hey, @SkyTheCoder, here it is a mod of your code with some color and box2d physics.
Thanks for the code, im using it in 6Dimensions game!

http://www.youtube.com/watch?v=z6JMbeTDHqU


displayMode(FULLSCREEN)
-- Use this function to perform your initial setup
function setup()
    parameter.watch("#balls")
    parameter.watch("math.floor(1/DeltaTime)")
    
    ground = physics.body(EDGE, vec2(0,0), vec2(WIDTH,0))
    ground.type = STATIC
    ramp = physics.body(EDGE, 
        vec2(WIDTH/2-333, HEIGHT/2-100), 
        vec2(WIDTH/2-66, HEIGHT/2+100))
  
    ramp2 = physics.body(EDGE, 
        vec2(WIDTH/2+66, HEIGHT/2+100), 
        vec2(WIDTH/2+333, HEIGHT/2-100))
    m = {}
    m.shader = shader(vS, fS)
    Number_of_Balls = 333

    Metaball_Size = 79
    parameter.integer("Metaball_Resolution", 1, 255, 255)
    parameter.action("Register Resolution (May lag!)", function()
        GENERATE_METABALL()
    end)
    function GENERATE_METABALL()
        local mr = Metaball_Resolution
        local ms = Metaball_Size
        blendMode(NORMAL)
        ballTex = image(200, 200)
        setContext(ballTex)
        pushStyle()
        noStroke()
        for i = 255, 0, -(256 - mr) do
            fill(255 - i, 255 - i)
            ellipse(100, 100, (i / 255) * ms, (i / 255) * ms)
        end
        setContext()
    end

    GENERATE_METABALL()
    balls = {}
    for i=1,Number_of_Balls do
        local ball = createDrop( math.random(0, WIDTH), math.random(HEIGHT/2+130, HEIGHT))
        table.insert(balls, ball)
    end

    img = image(WIDTH, HEIGHT)
    m = mesh()
    r = m:addRect(WIDTH / 2, HEIGHT / 2, WIDTH, HEIGHT)
    m:setRectTex(r, 0, 0, 1, 1)
    m.texture = img
    m.shader = shader(vS, fS) 
end

function createDrop(x,y)
    local ball = physics.body(CIRCLE, 12)
    ball.x = x
    ball.y = y
    ball.restitution = .1
    ball.linearVelocity = vec2(0,0)
    ball.friction = 0.1
    ball.mass = 10
    ball.angularVelocity = 0.0
    ball.bullet = false
    ball.linearDamping = 0.1
    return ball
end

-- This function gets called once every frame
function draw()
    -- remove invisible balls... here to avoid flickering
    for k,b in ipairs(balls) do
        if b.x+Metaball_Size> WIDTH +Metaball_Size *2 or b.x<-Metaball_Size then
            table.remove(balls, k)
            b:destroy()
        end
    end
    blendMode(NORMAL)
    background(0)
    
    sprite("SpaceCute:Background",WIDTH/2,HEIGHT/2, WIDTH,HEIGHT)
    
    rectMode(CENTER)
    stroke(255, 0, 0,255)
    
    strokeWidth(3)
    line(WIDTH/2-333, HEIGHT/2-100, 
         WIDTH/2-66, HEIGHT/2+100)
    line(WIDTH/2+66, HEIGHT/2+100, 
         WIDTH/2+333, HEIGHT/2-100)

    setContext(img)
        background(0) -- clear buffer
        --tint(139, 145, 157, 255)
        blendMode(ADDITIVE)
        for k,b in ipairs(balls) do
            sprite(ballTex, b.x, b.y)
        end
        --noTint()
   setContext()
   m.texture = img
   blendMode(MULTIPLY)
   m:draw()

end

function touched(touch)
    if touch.state == BEGAN or touch.state == MOVING then
        if balls[Number_of_Balls + 1] == nil then
            local ball = createDrop(touch.x,touch.y)
            table.insert(balls, ball)
        else
            balls[Number_of_Balls + 1].x = touch.x
            balls[Number_of_Balls + 1].y = touch.y
        end
    end
end

vS = [[
//
// A basic vertex shader
//

//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;

//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

//This is an output variable that will be passed to the fragment shader
varying lowp vec4 vColor;
varying highp vec2 vTexCoord;

void main()
{
    //Pass the mesh color to the fragment shader
    vColor = color;
    vTexCoord = texCoord;

    //Multiply the vertex position by our combined transform
    gl_Position = modelViewProjection * position;
}
]]

fS = [[
//
// A basic fragment shader
//

//Default precision qualifier
precision highp float;


//This represents the current texture on the mesh
uniform lowp sampler2D texture;

//The interpolated vertex color for this fragment
varying lowp vec4 vColor;

//The interpolated texture coordinate for this fragment
varying highp vec2 vTexCoord;

void main()
{
    //Sample the texture at the interpolated coordinate
    lowp vec4 col = texture2D( texture, vTexCoord ) * vColor;

    //Set the output color to the texture color
    if (max(col.r, max(col.g, col.b)) > 0.75)
    {
        gl_FragColor = vec4(0.0, 0.0, 1.0, 1.0);
    }
    else
    {
        gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0);
    }
}
]]

Very Nice @juaxix however, individual drops have weird behavior when they merge/separate…

Yes, we can try with joints each two balls, but it could be weird too, maybe a little noise for every single drop alone :smiley:
what do you think @Jmv38 ?

Well, must admit i havent understood how your code works. I have seen that when 2 unit balls touch each other they are transformed in a single ball too much bigger. The ideal would be to keep the surface constant (ball1+ball2+junction surface = constant), or better the volume constant, but this is easier to specify it than to code it!

The code is easy to understand, look, the balls are just physic bodies generated with parameters to give them the behaviour of water drops, it is: restitution, friction, damping, linear velocity, ok, to draw these balls we are using a technique with a shader and a texture, you need a mesh to apply the shader, just like the ripple or other samples, we are setting the width and height of the area of the mesh to the whole screen:

m:addRect(WIDTH / 2, HEIGHT / 2, WIDTH, HEIGHT)

this way we can use the (x,y) position of each ball to paint in the virtual space of the texture (with setContext) of the rect attached to the mesh, so, each ball is represented in the texture painted with the image generated of the ball (200*200) -ballTex -

for k,b in ipairs(balls) do
 sprite(ballTex, b.x, b.y)
end

then , you have to use the ADDITIVE blend mode to add all the painted balls to the texture and mix with a background.

Correct me if I’m wrong @SkyTheCoder.

We can improve the separated drops with the method from @Zoyt and @spacemonkey shaders, where they numberize and colorize each fragment and use a constant to smooth the draws.

@juaxix Yep, that’s how it works.

Shortened/simplified version:

In setup you generate a texture, a circle with a gradient, starting at invisible and working its way up to white in the center.

In setup you also create a mesh, the size of the screen, and a separate texture for it. Maybe named “screen.”

In setup, one more thing, set the blend mode to ADDITIVE. You have to do this AFTER generating the gradient texture, though.

Set the context (setContext(screen)) to the image you created above. You can then use a mesh or sprite() to draw the above texture at the position of every ball.

Set the rendering back to the screen (setContext()), and set the screen-sized mesh’s texture to the one we created for it, named screen. The mesh just uses a shader to filter out the gradients. If the brightness is greater than half way, it renders it white. If it isn’t, it’s transparent.

Haha, that’s not too much shorter…

One thing I forgot to mention: The reason it acts as Metaballs is because, with the blend mode ADDITIVE, when two gradient circles merge, the darker parts blend together to qualify as light enough to render white. And the reason why it goes from transparent to white instead of black to white is because if it was black to white I think with the blend mode ADDITIVE it would actually darken the brighter parts on the other gradient circle.

Thank you for your explanations @juaxix and @skythecoder now i understand it. So here is a very little tweek that will make your balls fusion / separation more reallistic, with no extra cost:

        -- replace lines 36-39 by this:
        -- with this gaussian shape of the ball texture, the merge will look more reallistic
        -- also replace the threshold of 0.75 in the shader by 0.5: more physical look.
        local a,d2,ref2
        ref2 = ms*ms/15
        for i = 1,200 do for j =1,200 do
            d2 = (i-100)*(i-100) + (j-100)*(j-100)
            a = math.exp(-d2/ref2)*255
            ballTex:set(i,j,color(a,a,a,255))
        end end

.@Jmv38 this water Looks awesome Smooth :smiley:
thanks