Realistic 2D Water Effect

Hello,
I made this as a prototype for a new type of object in my game, so here’s the code:

displayMode(FULLSCREEN)

function setup()
    parameter.watch("1/DeltaTime")
    fixtures = {}
    local gSize = vec2(WIDTH,HEIGHT/2)
    local gScale = 5
    local funnleSize = 20
    funnle1 = physics.body(CHAIN,false,vec2(-gSize.x,gSize.y),
                                vec2(-funnleSize,-gSize.y/3),
                                vec2(-funnleSize,-gSize.y)
                                )
    local funnle2 = physics.body(CHAIN,false,vec2(gSize.x,gSize.y),
                                vec2(funnleSize,-gSize.y/3),
                                vec2(funnleSize,-gSize.y)
                                )
    local split = physics.body(CHAIN,false,vec2(-WIDTH/2,-HEIGHT/8),
                                vec2(0,HEIGHT/80),
                                vec2(WIDTH/2,-HEIGHT/8)
                                )
    funnle1.x = WIDTH / 2
    funnle1.y = HEIGHT - HEIGHT / 4
    funnle1.type = STATIC
    funnle2.x = WIDTH / 2
    funnle2.y = HEIGHT - HEIGHT / 4
    funnle2.type = STATIC
    split.x = WIDTH / 2
    split.y = HEIGHT/8
    split.type = STATIC
    --split.restitution = .5
    table.insert(fixtures,funnle1)
    table.insert(fixtures,funnle2)
    table.insert(fixtures,split)

    blankImage = image(WIDTH,HEIGHT)
    blur = 30
    water = {}
    waterimage = softImage(color(0, 155, 255),5,blur)
    allWater = blankImage

    --[[waterMesh = mesh()
    waterMesh.texture = waterimage]]--
    
    mDisplay = mesh()
    mDisplay:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
    mDisplay.shader = shader(alphaThreshold())
    mDisplay.shader.smoothness = 0
    mDisplay.shader.threshold = .1
    mDisplay.shader.unpremultiply = 1
end


function draw()
    background(0, 0, 0, 255)
    physics.gravity(Gravity)
    smooth()

    setContext(allWater)
    background(0, 0, 0, 0)
    noStroke()         
    smooth()
    --for i,v in ipairs(water) do
    for i = #water,1,-1 do
        v = water[i]
        sprite(waterimage,v.body.x,v.body.y,blur,blur)
        --waterMesh:addRect(v.body.x,v.body.y,blur*extraSpace,blur*extraSpace)
        if v.body.x <= 0 or v.body.x >= WIDTH or v.body.y <= 0 or v.body.y >= HEIGHT then
            v.body:destroy()
            table.remove(water,i)
        end
    end

    --waterMesh:draw()
    --waterMesh:clear()
    setContext()
    
    mDisplay.texture = allWater
    mDisplay:draw()

    strokeWidth(12)
    for i,v in ipairs(fixtures) do
        pushMatrix()
        translate(v.x,v.y)
        local points = v.points
        for j = 1,#points-1 do
            a = points[j]
            b = points[(j % #points)+1]
            line(a.x, a.y, b.x, b.y)
        end
        popMatrix()
    end
    popMatrix()
end

function touched(t)
    addWater(10,t.x,t.y)
end

function softImage(f,r,s)
    simg = image(r,r)
    setContext(simg)
    pushStyle()
    noStroke()
    fill(f)
    ellipse(simg.width/2,simg.height/2,simg.width)
    popStyle()
    setContext()
    limg = image(s,s)
    setContext(simg)
    pushStyle()
    smooth()
    sprite(simg,0,0,s,s)
    popStyle()
    setContext()
    return simg
end

function addWater(amnt,x,y)
    local posRand = 20
    local vRand = 0
    for i = 1,amnt do
        local b = physics.body(CIRCLE,3)
        b.x = x + math.random(-posRand,posRand)
        b.y = y + math.random(-posRand,posRand)
        b.linearVelocity = vec2(math.random(-vRand,vRand),math.random(-vRand,vRand))
        b.restitution = .1
        table.insert(water,{body = b})
    end
end

function alphaThreshold()
    return [[
    //
// 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;
}
]],
[[

precision highp float;

uniform lowp sampler2D texture;

uniform float threshold;
uniform float smoothness;
uniform float unpremultiply;

varying lowp vec4 vColor;

varying vec2 vTexCoord;

void main()
{
    lowp vec4 color = texture2D( texture, vTexCoord ) ;
    if( unpremultiply > 0.0 ) { color.rgb /= color.a ; }
    
    float range = ( color.a - (1.0 - threshold) - (smoothness * 0.05) ) / (0.0001 + smoothness * 0.1) ;
    color.a = smoothstep( 0.0, 1.0, range ) ;
    color.rgb *= color.a ;
    
    gl_FragColor = color ;
}]]
end

Please include the shader in the code… It can be done by copying the shader stuff into strings (use [[ and ]]), then compiled by using shader(vertex string, fragment string).

@SkyTheCoder - Duh. Sorry. Done

@Zoyt very cool effect!

@zoyt could you use the blue format to share the code? With the format you used i cant select the code from the forum on my ipad (so cant cpoy it). Thanks.

@Jmv38 - Sorry, I’ll fix that.
@Briarfox - Thanks.

Looks wonderful, that’s why I was looking for, the problem is that with 300+ balls there is a FPS drop, to 4-1, while below this number, it is working fine (40-60FPS) :slight_smile:

Take a look to the @SkyTheCoder method using a different shader, here:
http://twolivesleft.com/Codea/Talk/discussion/2266/meta-balls-shader#Item_23
with 400+ it runs at 50-60FPS!

@Zoyt thanks for reformatting. This is just WOW!
Thanks a lot for sharing!
It is so good i thought i’d make this little video for other to see:
http://www.youtube.com/watch?v=RhlYDFmHtRA

@Jmv38 - Thanks.

Hey, check the latest post
http://twolivesleft.com/Codea/Talk/discussion/2266/meta-balls-shader#Item_32

I’m using the shader with 300+ box2d balls at 60FPS :slight_smile:

@juaxix - Nice… Thanks.