Howdy, Stranger!

It looks like you're new here. If you want to get involved, click one of these buttons!

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

edited August 2015 in Code Sharing Posts: 2,020

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\n") 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; } ]] }
Tagged:

Comments

  • edited August 2015 Posts: 2,020

    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\n")
        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;
    }
    ]]
    }
    
  • IgnatzIgnatz Mod
    Posts: 5,396

    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.

  • Posts: 2,020

    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...

  • IgnatzIgnatz Mod
    Posts: 5,396

    very nice!

  • Posts: 2,020

    ...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...

  • Posts: 2,020

    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.

  • Posts: 725

    Very impressive!

  • Posts: 2,020

    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.

  • Posts: 8

    @yojimbo2000 That is really impressive, great job dude.

  • IgnatzIgnatz Mod
    edited August 2015 Posts: 5,396

    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!

  • Posts: 289

    fancy ripple special effect and 3d special effect

  • Posts: 2,020

    @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.

  • edited August 2015 Posts: 2,020

    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.

  • IgnatzIgnatz Mod
    Posts: 5,396

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

Sign In or Register to comment.