Super Ripple. Ripple shader + tiler + (Update) set multiple ripple points for multitouch

Here’s a modified version of the ripple shader. It adds support for texture tiling, and you can set where the centre of the ripples is, so you can make the shader more interactive. Tap and drag your finger across the screen to make ripples in the image. As you can see, the ripples flow across the boundaries between the tile iterations.

ripples

If you use this with a repeating water texture it looks great. Edit: the ripples now decrease in size the further they are away from the centre


--# Main
-- SuperRipple
-- Ripple shader + texture tiling + set the centre of the ripples
displayMode(STANDARD)

function setup()
    --create image
    local img = readImage("Cargo Bot:Codea Icon")
    setContext(img)
    textWrapWidth(img.width)
    textMode(CORNER)
    textAlign(CENTER)
    font("IowanOldStyle-Roman")
    fill(0, 114, 255, 255)
    fontSize(40)
    text("Tap or drag your finger on the screen to make ripples\
")
    setContext()
    
    --pos and dimesnions of rectangle
    local pos = vec2(WIDTH, HEIGHT)/2
    local w,h = WIDTH*0.8, HEIGHT*0.8
    local radius = vec2(w,h)/2
    local aa = pos - radius --bottom left corner of rect
    local bb = pos + radius 
    texScale = img.width * 0.75 --what scale the texture will be displayed at
    
    --create mesh
    m = mesh()
    m.texture = img
    --set up rect
    local rIdx = m:addRect(pos.x, pos.y, w,h)
    m:setRectTex(rIdx, aa.x/texScale, aa.y/texScale, bb.x/texScale, bb.y/texScale) --texCoords according to tiler formula. nb textures will be rendered square in order to keep ripples circular.
       
    --set up shader 
    m.shader = shader(SuperRipple.vs, SuperRipple.fs)
    m.shader.centre = vec2(1.5,1.5) --eg centre of second tile from the bottom-left
    parameter.watch("m.shader.centre")
    anim={Freq = 8} --value to animate with tween
    rippleTween = tween(anim.Freq*0.3, anim, {Freq=0.3}, tween.easing.backOut)
end

function draw()
    background(40, 40, 50)
    m.shader.time = ElapsedTime
    m.shader.freq = anim.Freq
    m:draw()
end

function touched(t)
    m.shader.centre = vec2(t.x, t.y)/texScale  --convert touch into tiled-texture space
    if t.state==BEGAN then
        if rippleTween then 
            tween.stop(rippleTween) 
            rippleTween = nil
        end
        anim.Freq = 2 
    elseif t.state==ENDED then
        anim.Freq = math.max(5, anim.Freq)
        rippleTween = tween(anim.Freq*0.3, anim, {Freq=0.3}, tween.easing.backOut)
    else         
        anim.Freq = 1 + 5 * smoothstep(vec2(t.deltaX + t.deltaY):len()*0.5,0,6)
    end
end

function smoothstep(t,a,b)
    local a,b = a or 0,b or 1
    local t = math.min(1,math.max(0,(t-a)/(b-a)))
    return t * t * (3 - 2 * t)
end
--# SuperRipple
SuperRipple = { --set the centre point of the ripple
vs=[[ // Super Ripple vertex shader 

uniform mat4 modelViewProjection;

attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

varying lowp vec4 vColor;
varying highp vec2 vTexCoord;

void main()
{
    gl_Position = modelViewProjection * position;
    vColor = color;
    vTexCoord = texCoord;
}]],

fs = [[// Ripple fragment shader

uniform lowp sampler2D texture;
uniform highp float time;
uniform highp float freq;
uniform lowp vec2 centre;

varying lowp vec4 vColor;
varying highp vec2 vTexCoord;

void main()
{
    highp vec2 tc = vTexCoord.xy;
    highp vec2 p = 1.5 * (tc-centre);
    highp float len = length(p);
    highp vec2 uv = fract(tc + (p/len)*freq*max(0.3, 2.-len)*cos(len*24.0-time*4.0)*0.03); 

    gl_FragColor = texture2D(texture,uv) * vColor;
}
]]
}

Ok, here’s a version that lets you set up to 5 ripple points, allowing multitouch. This creates a much more realistic watery effect.

water

Edit : fixed a bug in the shader for loop, it should be for (int i=0; i<5; ++i) . So much simpler to write for i=0, 4 do!

-- SuperRipple
-- Ripple shader + texture tiling + set 5 origin points for ripples (multi-touch!)
displayMode(STANDARD)

function setup()
    --create image
    local img = readImage("Cargo Bot:Codea Icon")
    setContext(img)
    textWrapWidth(img.width)
    textMode(CORNER)
    textAlign(CENTER)
    font("IowanOldStyle-Roman")
    fill(0, 114, 255, 255)
    fontSize(40)
    text("Tap or drag your finger on the screen to make ripples\
")
    setContext()
    
    --pos and dimesnions of rectangle
    local pos = vec2(WIDTH, HEIGHT)/2
    local dim = vec2(WIDTH, HEIGHT)*0.8
    --calculate texCoords
    texScale = img.width * 0.75 --what scale the texture will be displayed at
    local texPos = (pos - dim/2)/texScale --bottom left corner of rect, in tiler texCoord space
    local texDim = dim/texScale --dimensions in texCoord space. nb textures will be rendered square in order to keep ripples circular.
    
    --create mesh
    m = mesh()
    m.texture = img
    --set up rect
    local rIdx = m:addRect(pos.x, pos.y, dim.x, dim.y)
    m:setRectTex(rIdx, texPos.x, texPos.y, texDim.x, texDim.y) --texCoords according to tiler formula. 
       
    --set up shader 
    m.shader = shader(SuperRipple.vs, SuperRipple.fs)
    
    --table for ripples
    ripples = {}
    for i=1,5 do --some ripples to start with (shader requires that table must always have 5 values)
        ripples[i]={pos = vec2(math.random()*5, math.random()*5), freq=8+math.random(8)}
        animateEnd(ripples[i])
    end
    recycle = {1,2,3,4,5} --table of keys of ripples ready to be reused
end

function animateEnd(this)  
    this.tween = tween(this.freq*0.2, this, {freq=0.3}, tween.easing.backOut) --ease back to freq 0.3, so that pool never comes to a stand still
end

function draw()
    background(40, 40, 50)
    
    --convert ripple table, indexed by touch id, into array for loading into shader
    local freq = {}
    local cent = {}
    local i = 0
    for _,v in pairs(ripples) do
        i = i + 1
        freq[i]=v.freq
        cent[i]=v.pos
    end
    m.shader.freq = freq
    m.shader.centre = cent
    m.shader.time = ElapsedTime
    m:draw()
end

function touched(t)
    local r = ripples[t.id]
    if t.state==BEGAN then
        -- need to get rid of a ripple before new touch can be accepted
        if r then --touch ids get recycled frequently by ios. Therefore dont get rid of a table item if the touch id is already in the table
            tween.stop(r.tween) --stop the tween that is about to be refired
            for i,v in ipairs( recycle) do --and remove it from recycle table
                if v==t.id then table.remove(recycle, i) end
            end
        elseif #recycle>0 then --Check whether there is a finished touch that can be removed
            ripples[recycle[#recycle]] = nil
            table.remove(recycle)
        else 
            return  --dont accept the touch if there are no spare slots in table. With apologies to six-fingered people.
        end
        ripples[t.id] = {freq = 0.3, pos = vec2(t.x, t.y)/texScale} --new ripple
        ripples[t.id].tween = tween(0.2, ripples[t.id], {freq=2+math.random()}) 
    elseif r then --check whether touch was accepted
        if t.state==MOVING then
            tween.stop(r.tween)
            r.pos = vec2(t.x, t.y)/texScale  --convert touch into tiled-texture space
            r.freq = 1 + 6 * smoothstep(vec2(t.deltaX, t.deltaY):len()*0.5,0,6) --freq of ripples depends on touch speed
        else --ENDED
            tween.stop(r.tween)
            r.freq = math.max(6, r.freq) --pulling finger out of pool creates ripple
            table.insert(recycle, 1, t.id )--flag this slot as ended and available for recycling
            animateEnd(r)
        end
    end
end

function smoothstep(t,a,b)
    local a,b = a or 0,b or 1
    local t = math.min(1,math.max(0,(t-a)/(b-a)))
    return t * t * (3 - 2 * t)
end

SuperRipple = { --allows you to set the centre points and frequencies of 5 ripples
vs=[[ //standard vertex shader. 
uniform mat4 modelViewProjection;

attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

varying lowp vec4 vColor;
varying highp vec2 vTexCoord;

void main()
{
    gl_Position = modelViewProjection * position;
    vColor = color;
    vTexCoord = texCoord;
}]],

fs = [[// Super Ripple fragment shader
const int no = 5; //set how many splash-points you want

uniform lowp sampler2D texture;
uniform highp float time;
uniform highp float freq[no];
uniform lowp vec2 centre[no];

varying lowp vec4 vColor;
varying highp vec2 vTexCoord;

void main()
{
    highp vec2 uv[no+1]; //array is 1 slot longer because...
    uv[no] = vec2(0.,0.); //last slot will hold the total (nb tables indexed from 0)
    for (int i=0; i<no; ++i) {
        highp vec2 p = 1.5 * (vTexCoord-centre[i]);
        highp float len = length(p);
        uv[i] = (p/len)*freq[i]*max(0.2, 2.-len)*cos(len*24.0-time*5.0)*0.03; 
        uv[no] += uv[i]; //tally total
    }
    gl_FragColor = texture2D(texture,fract(vTexCoord + uv[no])) * vColor;
}
]]
}

You are having fun, aren’t you? Nice work. B-)

Is this something that could be used in a game - like the naval combat game I’m building at the moment? I’m not just thinking of translating it to a 3D plane representing sea, but also about performance. Special effects are nice as long as they don’t eat precious FPS.

It’s a hypothetical question in my case, because I don’t really need ripples, but I have seen a number of wonderful visual effects, such as grass rippling, that never got used for anything. It would be a shame if your effects suffered the same fate.

This isn’t really ready for prime time, but in fact, the reason why I needed to write this is for a 3D project I’m working on. Not only does it look good for water surfaces, it looks amazing for 3D bodies of water too. I can’t share code yet, but it’s derived from the above. Check it out:

water body

The single-splash version is fine performance wise. I’ve tested it with meshes covering the whole screen.

What I’ve done above is use the same ripple data to modify the surface normal, creating a very 3D looking-effect.

If it’s for a massive sea plain then I would implement it on a separate “splash patch” rather than on the main mesh.

I’m probably not going to bother implementing the multi-splash version, as that is too much gratuitous eye-candy! But one of the first big hit iOS apps, back in the early days, was simply a coin-toss wishing fountain, based on the exact same GLES ripple shader code. You touched the screen to toss coins into the water, and could choose different backgrounds (including from the camera). I think he open sourced it. I probably should have looked at that before putting this together…

very nice!

…although, playing with it some more, maybe in a busy game with lots going on 5 splash-points is overkill, but 2 looks way better than 1. I reckon you only need two ripple sources for the patterns to disturb one another, creating that much more organic-looking effect. And one extra ripple shouldn’t be too much of a performance hit. I’ll experiment…

I updated the above code to make multitouch-tracking a bit simpler, and sped the animation up a bit to try to make the liquid seem more watery and less gloopy.

In the 3D version, I’m currently testing 3 splash-points. It seems to be optimal in terms of creating a realistic effect without too much overhead.

Very impressive!

Thanks, it was fun to make. I updated the above code again, to make it easier to change the number of splash-points in the shader. The most complex thing about the code is that it doesn’t seem to be possible to have tables of variable length (dynamically resized) in GLES, so fitting and tracking a varying amount of touches into a fixed 5-space array takes a bit of work. The game version of the code is simpler in that I just use the last 3 collision events to position the splashes in the water, and I don’t have to worry about multi-touch.

@yojimbo2000 That is really impressive, great job dude.

Very nicer work.

I note the splash seems to happen a little below and to the left of my touch, but that’s a very minor thing!

fancy ripple special effect and 3d special effect

@Ignatz yeah, I noticed that. The offset gets bigger as you get further from the origin. Weird thing is, it doesn’t seem to happen in the 3d version. I’ll look into it.

Ok, I’ve fixed it. The issue was that I had forgotten that the last two coordinates of the texCoords command are width and height, not absolute coordinates, so it should be bb-aa not just bb. So as you moved away from the origin, the touch gradually began to be offset by whatever aa was. Updated the code above to fix this. @Ignatz thanks for testing and for the bug report.

Any time, making bugs is my specialty, fixing them not so easy :stuck_out_tongue: