Blur Shader Needed.

Hi, I’m looking for a good blur shader, something similar to the blur you get in the iOS Notification Center or OS X Yosemite. The blur shader included doesn’t really do what I want. Also, preferably something non GPU intensive, as I have a lot of other stuff going on in the background (Even if it is GPU intensive, id still love to see it and test it) (also, preferably something with an open licence, as this will be used in a game that will most likely be on the App Store in the next couple of months). The background will constantly be changing, as this will be a background for a store which is “on top” or the menu screen which has several effects in it.

Most of the examples I’ve seen online require 2 passes. It also seems that you’d have to render the underlying scene to an image, and then pass that image as a texture to the shader. I don’t know what the performance of that would be like if you were doing it every frame. This one looks interesting:

http://xissburg.com/faster-gaussian-blur-in-glsl/

@Mr_Ninja

the fastest method may be to create an overlay image which can be sprited over the drawn screen, creating the impression of a blur. This is much faster than creating a blur in real time (and does the difference really matter to users?).

The code below demonstrates. Touch the screen to toggle the “blur” on and off. You can adjust the light drop off towards the edges with this line in the shader: f = fff; (the more you mutiply f by itself, the faster the light drops off, and vice versa)

function setup()
    --create "blur" image using shader
    blurImg=image(WIDTH,HEIGHT)
    setContext(blurImg)
    local m=mesh()
    m:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
    m.shader=shader(BlurShader.vertexShader,BlurShader.fragmentShader)
    m.shader.centre=vec2(WIDTH/2,HEIGHT/2)
    m:draw()
    setContext()
   --image completed
    showBlur=false --blur toggle
end

function draw()
    background(50) 
   --draw something on the screen
    sprite("SpaceCute:Rocketship",WIDTH-200,HEIGHT-200)
    sprite("Small World:Store Extra Large",200,200,300)
    sprite("SpaceCute:Beetle Ship",WIDTH/2,HEIGHT/2)
    if showBlur then --draw blur if required
        sprite(img,WIDTH/2,HEIGHT/2) 
        sprite("Cargo Bot:Codea Icon",WIDTH/2,HEIGHT/2)
    end
end

--touching toggles blur on and off
function touched(t)
    if t.state==ENDED then showBlur=not showBlur end
end

--blur shader
BlurShader = {
vertexShader = [[
uniform mat4 modelViewProjection;
attribute vec4 position;
attribute vec4 color;
varying highp vec4 vPosition;

void main()
{
    gl_Position = modelViewProjection * position;
    vPosition =  position;
}
]],
fragmentShader = [[
precision highp float;
uniform vec2 centre;
varying highp vec4 vPosition;

float L = sqrt(centre.x*centre.x+centre.y*centre.y)*1.4;

void main()
{
    float f=1.0 - distance(vPosition.xy,centre)/L;
    f=f*f*f;
    gl_FragColor = vec4(f,f,f,1.0-f);
}
]]}

Here’s a Gaussian blur shader. It looks great, but on an iPad Air, the FPS drops to 35 with two passes. I haven’t tried optimising it, but I’m sure the bottleneck is sending two screen-sized images as textures to the shader, rather than the calculations in the shader itself. i.e. I’m not sure that reducing the number of samples in the shader from 14 would necessarily help. Could be worth trying?


--# Main
--Gaussian blur
--adapted by Yojimbo2000 from http://xissburg.com/faster-gaussian-blur-in-glsl/ 
    
function setup()
    blur = {} --images
    blurred = {} --meshes
    for i=1,2 do --2 passes, one for horizontal, one vertical
        blur[i]=image(WIDTH,HEIGHT)
        blurred[i]=mesh()
        blurred[i].texture=blur[i]
        blurred[i]:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
        blurred[i].shader=shader(Gaussian.vs[i], Gaussian.fs)
    end
    unblurred=mesh() --mesh w/o the blur shader
    unblurred.texture=blur[1]
    unblurred:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)

    showBlur=false --blur toggle
    pos={x1=200, y2=200, x3=800}
    tween(2, pos, {x1=800, y2=800, x3=200}, {easing=tween.easing.cubicInOut, loop=tween.loop.pingpong})
    movement = true 
    profiler.init()
end

function draw()
    if movement then
        setContext(blur[1])
        background(50)
        
        sprite("SpaceCute:Rocketship",pos.x1,HEIGHT-200)
        sprite("Small World:Store Extra Large",pos.x3,200,300)
        sprite("SpaceCute:Beetle Ship",WIDTH/2,pos.y2)
    end
    
    if showBlur then --draw blur if required
        setContext(blur[2])
        blurred[1]:draw() --pass one, offscreen
        setContext()
        blurred[2]:draw() --pass two
    else
        setContext()
        unblurred:draw()
    end
    profiler.draw()
end

--touching toggles blur on and off
function touched(t)
    if t.state==ENDED then showBlur=not showBlur end
end

profiler={}

function profiler.init(quiet)    
    profiler.del=0
    profiler.c=0
    profiler.fps=0
    profiler.mem=0
    if not quiet then
        parameter.watch("profiler.fps")
        parameter.watch("profiler.mem")
    end
end

function profiler.draw()
    profiler.del = profiler.del +  DeltaTime
    profiler.c = profiler.c + 1
    if profiler.c==10 then
        profiler.fps=profiler.c/profiler.del
        profiler.del=0
        profiler.c=0
        profiler.mem=collectgarbage("count", 2)
    end
end

--# Gaussian
Gaussian = {
vs = {[[
uniform mat4 modelViewProjection;

attribute vec4 position;
attribute vec2 texCoord;
 
varying vec2 vTexCoord;
varying vec2 v_blurTexCoords[14];
 
void main()
{
    gl_Position = modelViewProjection * position;
    vTexCoord = texCoord;
    v_blurTexCoords[ 0] = vTexCoord + vec2(-0.028, 0.0);
    v_blurTexCoords[ 1] = vTexCoord + vec2(-0.024, 0.0);
    v_blurTexCoords[ 2] = vTexCoord + vec2(-0.020, 0.0);
    v_blurTexCoords[ 3] = vTexCoord + vec2(-0.016, 0.0);
    v_blurTexCoords[ 4] = vTexCoord + vec2(-0.012, 0.0);
    v_blurTexCoords[ 5] = vTexCoord + vec2(-0.008, 0.0);
    v_blurTexCoords[ 6] = vTexCoord + vec2(-0.004, 0.0);
    v_blurTexCoords[ 7] = vTexCoord + vec2( 0.004, 0.0);
    v_blurTexCoords[ 8] = vTexCoord + vec2( 0.008, 0.0);
    v_blurTexCoords[ 9] = vTexCoord + vec2( 0.012, 0.0);
    v_blurTexCoords[10] = vTexCoord + vec2( 0.016, 0.0);
    v_blurTexCoords[11] = vTexCoord + vec2( 0.020, 0.0);
    v_blurTexCoords[12] = vTexCoord + vec2( 0.024, 0.0);
    v_blurTexCoords[13] = vTexCoord + vec2( 0.028, 0.0);
}]],
 [[
uniform mat4 modelViewProjection;

attribute vec4 position;
attribute vec2 texCoord;
 
varying vec2 vTexCoord;
varying vec2 v_blurTexCoords[14];
 
void main()
{
    gl_Position = modelViewProjection * position;
    vTexCoord = texCoord;
    v_blurTexCoords[ 0] = vTexCoord + vec2(0.0, -0.028);
    v_blurTexCoords[ 1] = vTexCoord + vec2(0.0, -0.024);
    v_blurTexCoords[ 2] = vTexCoord + vec2(0.0, -0.020);
    v_blurTexCoords[ 3] = vTexCoord + vec2(0.0, -0.016);
    v_blurTexCoords[ 4] = vTexCoord + vec2(0.0, -0.012);
    v_blurTexCoords[ 5] = vTexCoord + vec2(0.0, -0.008);
    v_blurTexCoords[ 6] = vTexCoord + vec2(0.0, -0.004);
    v_blurTexCoords[ 7] = vTexCoord + vec2(0.0,  0.004);
    v_blurTexCoords[ 8] = vTexCoord + vec2(0.0,  0.008);
    v_blurTexCoords[ 9] = vTexCoord + vec2(0.0,  0.012);
    v_blurTexCoords[10] = vTexCoord + vec2(0.0,  0.016);
    v_blurTexCoords[11] = vTexCoord + vec2(0.0,  0.020);
    v_blurTexCoords[12] = vTexCoord + vec2(0.0,  0.024);
    v_blurTexCoords[13] = vTexCoord + vec2(0.0,  0.028);
}]]},
fs = [[precision mediump float;
 
uniform lowp sampler2D texture;
 
varying vec2 vTexCoord;
varying vec2 v_blurTexCoords[14];
 
void main()
{
    gl_FragColor = vec4(0.0);
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 0])*0.0044299121055113265;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 1])*0.00895781211794;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 2])*0.0215963866053;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 3])*0.0443683338718;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 4])*0.0776744219933;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 5])*0.115876621105;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 6])*0.147308056121;
    gl_FragColor += texture2D(texture, vTexCoord         )*0.159576912161;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 7])*0.147308056121;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 8])*0.115876621105;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 9])*0.0776744219933;
    gl_FragColor += texture2D(texture, v_blurTexCoords[10])*0.0443683338718;
    gl_FragColor += texture2D(texture, v_blurTexCoords[11])*0.0215963866053;
    gl_FragColor += texture2D(texture, v_blurTexCoords[12])*0.00895781211794;
    gl_FragColor += texture2D(texture, v_blurTexCoords[13])*0.0044299121055113265;
}]]
}

Personally, I wouldn’t go to a lot of effort just to blur the background that nobody will notice anyway :slight_smile:

Well, it’s what the OP asked for :slight_smile:

I was wrong about optimisation: if you comment out half the calculations in the fragment shader, it runs at about 58 fps, and still looks acceptably blurry. I’ll have to recalculate what the brightness values should be. I can post an optimised version later

Plus, with all of these things, it’s often the things you discover along the way. I’m interested in this because I’m currently exploring the possibility of creating an underwater ripple effect for a platform game I’m working on, which would be the same principle of sending the entire screen to an effect shader.

One really fun thing I just discovered: if you comment out every other line in the fragment shader above, you get a really cool after-image trail as the sprites move. With further adaptions so that the blur only goes in one direction, it would be a great effect

Ok, here’s an optimised version. Tap to cycle through 3 states, no blur, sideways trails, 2-pass Gaussian. This runs at 59/60 fps on the Air:


--# Main
--Gaussian blur
--adapted by Yojimbo2000 from http://xissburg.com/faster-gaussian-blur-in-glsl/ 
    
function setup()
    --2 pass Gaussian blur:
    blur = {} --images
    blurred = {} --meshes
    for i=1,2 do --2 passes, one for horizontal, one vertical
        blur[i]=image(WIDTH,HEIGHT)
        blurred[i]=mesh()
        blurred[i].texture=blur[i]
        blurred[i]:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
        blurred[i].shader=shader(Gaussian.vs[i], Gaussian.fsO)
    end
    --mesh w/o the blur shader:
    unblurred=mesh() 
    unblurred.texture=blur[1]
    unblurred:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
    --mesh with sideways after images:
    sideTrails=mesh() 
    sideTrails.texture=blur[1]
    sideTrails.shader=shader(Gaussian.vs[1], Gaussian.fsTrails) --try Gaussian.fsTesselated for a different effect
    sideTrails:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
    
    showBlur=1 --blur state
    --some animations:
    pos={x1=200, y2=200, x3=800}
    tween(2, pos, {x1=800, y2=800, x3=200}, {easing=tween.easing.cubicInOut, loop=tween.loop.pingpong})
    movement = true 
    profiler.init()
    print ("tap screen to cycle through blur states")
end

function draw()
    if movement then
        setContext(blur[1])
        background(50)
        
        sprite("SpaceCute:Rocketship",pos.x1,HEIGHT-200)
        sprite("Small World:Store Extra Large",pos.x3,200,300)
        sprite("SpaceCute:Beetle Ship",WIDTH/2,pos.y2)
    end
    
    if showBlur==1 then --draw blur if required
        setContext()
        background(50)
        unblurred:draw()
    elseif showBlur==2 then
        setContext()
      --  background(50)
        sideTrails:draw()
    elseif showBlur==3 then
        setContext(blur[2])
       -- background(50) --hard to tell whether these background calls make a difference
        blurred[1]:draw() --pass one, offscreen
        setContext()
       -- background(50)
        blurred[2]:draw() --pass two
    end
    profiler.draw()
end

--touching toggles blur on and off
local blurStates = {"no blur", "1 pass, horizontal trails", "2 pass Gaussian blur, optimized"}
function touched(t)
    if t.state==ENDED then 
        showBlur = showBlur + 1
        if showBlur==4 then showBlur=1 end
        output.clear()
        print(blurStates[showBlur])
    end
end

profiler={}

function profiler.init(quiet)    
    profiler.del=0
    profiler.c=0
    profiler.fps=0
    profiler.mem=0
    if not quiet then
        parameter.watch("profiler.fps")
        parameter.watch("profiler.mem")
    end
end

function profiler.draw()
    profiler.del = profiler.del +  DeltaTime
    profiler.c = profiler.c + 1
    if profiler.c==10 then
        profiler.fps=profiler.c/profiler.del
        profiler.del=0
        profiler.c=0
        profiler.mem=collectgarbage("count", 2)
    end
end
--# Gaussian
Gaussian = {
vs = { -- horizontal pass vertex shader
[[
uniform mat4 modelViewProjection;

attribute vec4 position;
attribute vec2 texCoord;
 
varying vec2 vTexCoord;
varying vec2 v_blurTexCoords[14];
 
void main()
{
    gl_Position = modelViewProjection * position;
    vTexCoord = texCoord;
    v_blurTexCoords[ 0] = vTexCoord + vec2(-0.028, 0.0);
    v_blurTexCoords[ 1] = vTexCoord + vec2(-0.024, 0.0);
    v_blurTexCoords[ 2] = vTexCoord + vec2(-0.020, 0.0);
    v_blurTexCoords[ 3] = vTexCoord + vec2(-0.016, 0.0);
    v_blurTexCoords[ 4] = vTexCoord + vec2(-0.012, 0.0);
    v_blurTexCoords[ 5] = vTexCoord + vec2(-0.008, 0.0);
    v_blurTexCoords[ 6] = vTexCoord + vec2(-0.004, 0.0);
    v_blurTexCoords[ 7] = vTexCoord + vec2( 0.004, 0.0);
    v_blurTexCoords[ 8] = vTexCoord + vec2( 0.008, 0.0);
    v_blurTexCoords[ 9] = vTexCoord + vec2( 0.012, 0.0);
    v_blurTexCoords[10] = vTexCoord + vec2( 0.016, 0.0);
    v_blurTexCoords[11] = vTexCoord + vec2( 0.020, 0.0);
    v_blurTexCoords[12] = vTexCoord + vec2( 0.024, 0.0);
    v_blurTexCoords[13] = vTexCoord + vec2( 0.028, 0.0);
}]],
-- vertical pass vertex shader
 [[
uniform mat4 modelViewProjection;

attribute vec4 position;
attribute vec2 texCoord;
 
varying vec2 vTexCoord;
varying vec2 v_blurTexCoords[14];
 
void main()
{
    gl_Position = modelViewProjection * position;
    vTexCoord = texCoord;
    v_blurTexCoords[ 0] = vTexCoord + vec2(0.0, -0.028);
    v_blurTexCoords[ 1] = vTexCoord + vec2(0.0, -0.024);
    v_blurTexCoords[ 2] = vTexCoord + vec2(0.0, -0.020);
    v_blurTexCoords[ 3] = vTexCoord + vec2(0.0, -0.016);
    v_blurTexCoords[ 4] = vTexCoord + vec2(0.0, -0.012);
    v_blurTexCoords[ 5] = vTexCoord + vec2(0.0, -0.008);
    v_blurTexCoords[ 6] = vTexCoord + vec2(0.0, -0.004);
    v_blurTexCoords[ 7] = vTexCoord + vec2(0.0,  0.004);
    v_blurTexCoords[ 8] = vTexCoord + vec2(0.0,  0.008);
    v_blurTexCoords[ 9] = vTexCoord + vec2(0.0,  0.012);
    v_blurTexCoords[10] = vTexCoord + vec2(0.0,  0.016);
    v_blurTexCoords[11] = vTexCoord + vec2(0.0,  0.020);
    v_blurTexCoords[12] = vTexCoord + vec2(0.0,  0.024);
    v_blurTexCoords[13] = vTexCoord + vec2(0.0,  0.028);
}]]},
--optimised fragment shader
fsO = [[precision mediump float;
 
uniform lowp sampler2D texture;
 
varying vec2 vTexCoord;
varying vec2 v_blurTexCoords[14];
 
void main()
{
    gl_FragColor = vec4(0.0);

    gl_FragColor += texture2D(texture, v_blurTexCoords[ 5])* 0.08; //0.169179866813; //0.115876621105;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 6])* 0.26; //0.215069761937; //0.147308056121;
    gl_FragColor += texture2D(texture, vTexCoord         )*  0.32; //0.232982291755; //0.159576912161;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 7])* 0.26; //0.215069761937; //0.147308056121;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 8])* 0.08; //0.169179866813; //0.115876621105;

}]],

--fragment shader --awesome after-image trails!
fsTrails = [[precision mediump float;
 
uniform lowp sampler2D texture;
 
varying vec2 vTexCoord;
varying vec2 v_blurTexCoords[14];
 
void main()
{
    gl_FragColor = vec4(0.0);
   gl_FragColor += texture2D(texture, v_blurTexCoords[ 0])*0.0044299121055113265;
//  gl_FragColor += texture2D(texture, v_blurTexCoords[ 1])*0.00895781211794;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 2])*0.0215963866053;
//    gl_FragColor += texture2D(texture, v_blurTexCoords[ 3])*0.0443683338718;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 4])*0.0776744219933;
  // gl_FragColor += texture2D(texture, v_blurTexCoords[ 5])*0.115876621105;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 6])*0.147308056121;
    gl_FragColor += texture2D(texture, vTexCoord         )*0.159576912161;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 7])*0.147308056121;
//   gl_FragColor += texture2D(texture, v_blurTexCoords[ 8])*0.115876621105;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 9])*0.0776744219933;
//    gl_FragColor += texture2D(texture, v_blurTexCoords[10])*0.0443683338718;
   gl_FragColor += texture2D(texture, v_blurTexCoords[11])*0.0215963866053;
//   gl_FragColor += texture2D(texture, v_blurTexCoords[12])*0.00895781211794;
   gl_FragColor += texture2D(texture, v_blurTexCoords[13])*0.0044299121055113265;
}]],

--fragment shader. tesselated frosted glass effect (also try commenting out 3-10)
fsTesselated = [[precision mediump float;
 
uniform lowp sampler2D texture;
 
varying vec2 vTexCoord;
varying vec2 v_blurTexCoords[14];
 
void main()
{
    gl_FragColor = vec4(0.0);
//   gl_FragColor += texture2D(texture, v_blurTexCoords[ 0])*0.0044299121055113265;
  gl_FragColor += texture2D(texture, v_blurTexCoords[ 1])*0.00895781211794;
 //   gl_FragColor += texture2D(texture, v_blurTexCoords[ 2])*0.0215963866053;
   gl_FragColor += texture2D(texture, v_blurTexCoords[ 3])*0.0443683338718;
 //   gl_FragColor += texture2D(texture, v_blurTexCoords[ 4])*0.0776744219933;
 gl_FragColor += texture2D(texture, v_blurTexCoords[ 5])*0.115876621105;
 //   gl_FragColor += texture2D(texture, v_blurTexCoords[ 6])*0.147308056121;
    gl_FragColor += texture2D(texture, vTexCoord         )*0.159576912161;
 //   gl_FragColor += texture2D(texture, v_blurTexCoords[ 7])*0.147308056121;
   gl_FragColor += texture2D(texture, v_blurTexCoords[ 8])*0.115876621105;
//    gl_FragColor += texture2D(texture, v_blurTexCoords[ 9])*0.0776744219933;
    gl_FragColor += texture2D(texture, v_blurTexCoords[10])*0.0443683338718;
//   gl_FragColor += texture2D(texture, v_blurTexCoords[11])*0.0215963866053;
   gl_FragColor += texture2D(texture, v_blurTexCoords[12])*0.00895781211794;
 //  gl_FragColor += texture2D(texture, v_blurTexCoords[13])*0.0044299121055113265;
}]]
}

On the above code,the weightings aren’t right (I made them up). This blog uses the same technique as mine (ie 5 readings, fragment interpolation). I might see if I can copy their weightings. You could then emulate the iOS 7 panel effect, fullscreen, 60 fps.

http://www.sunsetlakesoftware.com/2013/10/21/optimizing-gaussian-blurs-mobile-gpu

You must have a fast iPad!

My iPad 3 runs your code with one pass at 40 FPS, two passes at 30 FPS.

@Ignatz (or anyone else with an iPad 3), try this one. It downsamples by half, meaning there are a quarter as many calculations in the fragment shader. Because OpenGL up scaling adds a blurring effect anyway (as long as you don’t invoke noSmooth()), the visual difference between this and full-pixel calculation is unnoticeable, but it should be a lot faster:


--# Main
--Gaussian blur
--adapted by Yojimbo2000 from http://xissburg.com/faster-gaussian-blur-in-glsl/ 


function setup()
    --2 pass Gaussian blur:
    local downSample = 0.5
    local dimensions = {
    vec2(WIDTH, HEIGHT), --full size
    vec2(WIDTH, HEIGHT) * downSample, --down sampled
    vec2(WIDTH, HEIGHT) * 0.5, --centre of fullsize (for positioning rect)
    vec2(WIDTH, HEIGHT) * downSample * 0.5, --centre of downsampled 
    }
    blur = {}  --images
    blurred = {} --meshes
    for i=1,2 do --2 passes, one for horizontal, one vertical
        blur[i]=image(dimensions[i].x, dimensions[i].y) --image 1 is full sized, image 2 is downsampled
        blurred[i]=mesh()
        blurred[i].texture=blur[i]
        local j=3-i --invert i so that...
        blurred[i]:addRect(dimensions[j+2].x, dimensions[j+2].y, dimensions[j].x, dimensions[j].y) --mesh 1 rect is down-sampled, mesh 2 rect is full-sized
        blurred[i].shader=shader(Gaussian.vs[i], Gaussian.fsO)
    end

    --mesh w/o the blur shader:
    unblurred=mesh() 
    unblurred.texture=blur[1]
    unblurred:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
    --mesh with sideways after images:
    sideTrails=mesh() 
    sideTrails.texture=blur[1]
    sideTrails.shader=shader(Gaussian.vs[1], Gaussian.fsTrails) --try Gaussian.fsTesselated for a different effect
    sideTrails:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
    
    showBlur=1 --blur state
    --some animations:
    pos={x1=200, y2=200, x3=800}
    tween(2, pos, {x1=800, y2=800, x3=200}, {easing=tween.easing.cubicInOut, loop=tween.loop.pingpong})
    movement = true 
    profiler.init()
    print ("tap screen to cycle through blur states")
end

function draw()
    if movement then
        setContext(blur[1])
        background(50)
        
        sprite("SpaceCute:Rocketship",pos.x1,HEIGHT-200)
        sprite("Small World:Store Extra Large",pos.x3,200,300)
        sprite("SpaceCute:Beetle Ship",WIDTH/2,pos.y2)
    end
    
    if showBlur==1 then --draw blur if required
        setContext()
        background(50)
        unblurred:draw()
    elseif showBlur==2 then
        setContext()
      --  background(50)
        sideTrails:draw()
    elseif showBlur==3 then
        setContext(blur[2])
       -- background(50) --hard to tell whether these background calls make a difference
        blurred[1]:draw() --pass one, offscreen
        setContext()
       -- background(50)
        blurred[2]:draw() --pass two
    end
    profiler.draw()
end

--touching toggles blur on and off
local blurStates = {"no blur", "1 pass, horizontal trails", "2 pass Gaussian blur, optimized"}
function touched(t)
    if t.state==ENDED then 
        showBlur = showBlur + 1
        if showBlur==4 then showBlur=1 end
        output.clear()
        print(blurStates[showBlur])
    end
end

profiler={}

function profiler.init(quiet)    
    profiler.del=0
    profiler.c=0
    profiler.fps=0
    profiler.mem=0
    if not quiet then
        parameter.watch("profiler.fps")
        parameter.watch("profiler.mem")
    end
end

function profiler.draw()
    profiler.del = profiler.del +  DeltaTime
    profiler.c = profiler.c + 1
    if profiler.c==10 then
        profiler.fps=profiler.c/profiler.del
        profiler.del=0
        profiler.c=0
        profiler.mem=collectgarbage("count", 2)
    end
end
--# Gaussian
Gaussian = {
vs = { -- horizontal pass vertex shader
[[
uniform mat4 modelViewProjection;

attribute vec4 position;
attribute vec2 texCoord;
 
varying vec2 vTexCoord;
varying vec2 v_blurTexCoords[14];
 
void main()
{
    gl_Position = modelViewProjection * position;
    vTexCoord = texCoord;
    v_blurTexCoords[ 0] = vTexCoord + vec2(-0.028, 0.0);
    v_blurTexCoords[ 1] = vTexCoord + vec2(-0.024, 0.0);
    v_blurTexCoords[ 2] = vTexCoord + vec2(-0.020, 0.0);
    v_blurTexCoords[ 3] = vTexCoord + vec2(-0.016, 0.0);
    v_blurTexCoords[ 4] = vTexCoord + vec2(-0.012, 0.0);
    v_blurTexCoords[ 5] = vTexCoord + vec2(-0.008, 0.0);
    v_blurTexCoords[ 6] = vTexCoord + vec2(-0.004, 0.0);
    v_blurTexCoords[ 7] = vTexCoord + vec2( 0.004, 0.0);
    v_blurTexCoords[ 8] = vTexCoord + vec2( 0.008, 0.0);
    v_blurTexCoords[ 9] = vTexCoord + vec2( 0.012, 0.0);
    v_blurTexCoords[10] = vTexCoord + vec2( 0.016, 0.0);
    v_blurTexCoords[11] = vTexCoord + vec2( 0.020, 0.0);
    v_blurTexCoords[12] = vTexCoord + vec2( 0.024, 0.0);
    v_blurTexCoords[13] = vTexCoord + vec2( 0.028, 0.0);
}]],
-- vertical pass vertex shader
 [[
uniform mat4 modelViewProjection;

attribute vec4 position;
attribute vec2 texCoord;
 
varying vec2 vTexCoord;
varying vec2 v_blurTexCoords[14];
 
void main()
{
    gl_Position = modelViewProjection * position;
    vTexCoord = texCoord;
    v_blurTexCoords[ 0] = vTexCoord + vec2(0.0, -0.028);
    v_blurTexCoords[ 1] = vTexCoord + vec2(0.0, -0.024);
    v_blurTexCoords[ 2] = vTexCoord + vec2(0.0, -0.020);
    v_blurTexCoords[ 3] = vTexCoord + vec2(0.0, -0.016);
    v_blurTexCoords[ 4] = vTexCoord + vec2(0.0, -0.012);
    v_blurTexCoords[ 5] = vTexCoord + vec2(0.0, -0.008);
    v_blurTexCoords[ 6] = vTexCoord + vec2(0.0, -0.004);
    v_blurTexCoords[ 7] = vTexCoord + vec2(0.0,  0.004);
    v_blurTexCoords[ 8] = vTexCoord + vec2(0.0,  0.008);
    v_blurTexCoords[ 9] = vTexCoord + vec2(0.0,  0.012);
    v_blurTexCoords[10] = vTexCoord + vec2(0.0,  0.016);
    v_blurTexCoords[11] = vTexCoord + vec2(0.0,  0.020);
    v_blurTexCoords[12] = vTexCoord + vec2(0.0,  0.024);
    v_blurTexCoords[13] = vTexCoord + vec2(0.0,  0.028);
}]]},
--optimised fragment shader
fsO = [[precision mediump float;
 
uniform lowp sampler2D texture;
 
varying vec2 vTexCoord;
varying vec2 v_blurTexCoords[14];
 
void main()
{
    gl_FragColor = vec4(0.0);

    gl_FragColor += texture2D(texture, v_blurTexCoords[ 5])* 0.08; //0.169179866813; //0.115876621105;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 6])* 0.26; //0.215069761937; //0.147308056121;
    gl_FragColor += texture2D(texture, vTexCoord         )*  0.32; //0.232982291755; //0.159576912161;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 7])* 0.26; //0.215069761937; //0.147308056121;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 8])* 0.08; //0.169179866813; //0.115876621105;

}]],

--fragment shader --awesome after-image trails!
fsTrails = [[precision mediump float;
 
uniform lowp sampler2D texture;
 
varying vec2 vTexCoord;
varying vec2 v_blurTexCoords[14];
 
void main()
{
    gl_FragColor = vec4(0.0);
   gl_FragColor += texture2D(texture, v_blurTexCoords[ 0])*0.0044299121055113265;
//  gl_FragColor += texture2D(texture, v_blurTexCoords[ 1])*0.00895781211794;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 2])*0.0215963866053;
//    gl_FragColor += texture2D(texture, v_blurTexCoords[ 3])*0.0443683338718;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 4])*0.0776744219933;
  // gl_FragColor += texture2D(texture, v_blurTexCoords[ 5])*0.115876621105;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 6])*0.147308056121;
    gl_FragColor += texture2D(texture, vTexCoord         )*0.159576912161;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 7])*0.147308056121;
//   gl_FragColor += texture2D(texture, v_blurTexCoords[ 8])*0.115876621105;
    gl_FragColor += texture2D(texture, v_blurTexCoords[ 9])*0.0776744219933;
//    gl_FragColor += texture2D(texture, v_blurTexCoords[10])*0.0443683338718;
   gl_FragColor += texture2D(texture, v_blurTexCoords[11])*0.0215963866053;
//   gl_FragColor += texture2D(texture, v_blurTexCoords[12])*0.00895781211794;
   gl_FragColor += texture2D(texture, v_blurTexCoords[13])*0.0044299121055113265;
}]],

--fragment shader. tesselated frosted glass effect (also try commenting out 3-10)
fsTesselated = [[precision mediump float;
 
uniform lowp sampler2D texture;
 
varying vec2 vTexCoord;
varying vec2 v_blurTexCoords[14];
 
void main()
{
    gl_FragColor = vec4(0.0);
//   gl_FragColor += texture2D(texture, v_blurTexCoords[ 0])*0.0044299121055113265;
  gl_FragColor += texture2D(texture, v_blurTexCoords[ 1])*0.00895781211794;
 //   gl_FragColor += texture2D(texture, v_blurTexCoords[ 2])*0.0215963866053;
   gl_FragColor += texture2D(texture, v_blurTexCoords[ 3])*0.0443683338718;
 //   gl_FragColor += texture2D(texture, v_blurTexCoords[ 4])*0.0776744219933;
 gl_FragColor += texture2D(texture, v_blurTexCoords[ 5])*0.115876621105;
 //   gl_FragColor += texture2D(texture, v_blurTexCoords[ 6])*0.147308056121;
    gl_FragColor += texture2D(texture, vTexCoord         )*0.159576912161;
 //   gl_FragColor += texture2D(texture, v_blurTexCoords[ 7])*0.147308056121;
   gl_FragColor += texture2D(texture, v_blurTexCoords[ 8])*0.115876621105;
//    gl_FragColor += texture2D(texture, v_blurTexCoords[ 9])*0.0776744219933;
    gl_FragColor += texture2D(texture, v_blurTexCoords[10])*0.0443683338718;
//   gl_FragColor += texture2D(texture, v_blurTexCoords[11])*0.0215963866053;
   gl_FragColor += texture2D(texture, v_blurTexCoords[12])*0.00895781211794;
 //  gl_FragColor += texture2D(texture, v_blurTexCoords[13])*0.0044299121055113265;
}]]
}

Ok, last one I promise. This one lets you set varying amounts of horizontal and vertical blur, and has better weighting of the pixels. I’m not sure, but it could be that the downsampling only quarters the number of calls to the fragment shader on the first pass, but it depends on how OpenGL handles upscaling (ie does it call the fragment shader for the number of pixels in the source, or in the destination, when upscaling?). So it’s possible that we’re getting five-eighths the number of calculations, rather than a quarter. I can’t really check this because it all runs at 60fps for me.


--# Main
--Gaussian blur
--adapted by Yojimbo2000 from http://xissburg.com/faster-gaussian-blur-in-glsl/ and http://www.sunsetlakesoftware.com/2013/10/21/optimizing-gaussian-blurs-mobile-gpu

function setup()
    --2 pass Gaussian blur:
    local downSample = 0.5 -- going down to 0.25 actually looks pretty good!
    local dimensions = {
    vec2(WIDTH, HEIGHT), --full size
    vec2(WIDTH, HEIGHT) * downSample --down sampled
    }
    local blurRadii = {
    vec2(0.002,0), --horizontal pass, increase x value for more horizontal blur
    vec2(0,0.002) --vertical pass, increase y value for more vertical blur
    }
    blur = {}  --images
    blurred = {} --meshes
    for i=1,2 do --2 passes, one for horizontal, one vertical
        blur[i]=image(dimensions[i].x, dimensions[i].y) --image 1 is full sized, image 2 is downsampled
        blurred[i]=mesh()
        blurred[i].texture=blur[i]
        local j=3-i --invert i so that...
        blurred[i]:addRect(dimensions[j].x/2, dimensions[j].y/2, dimensions[j].x, dimensions[j].y) --mesh 1 rect is down-sampled, mesh 2 rect is full-sized
        blurred[i].shader=shader(Gaussian.vs, Gaussian.fs)
        blurred[i].shader.blurRadius=blurRadii[i]
    end

    --mesh w/o the blur shader:
    unblurred=mesh() 
    unblurred.texture=blur[1]
    unblurred:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
    
    showBlur=false --blur state
    --some animations:
    pos={x1=200, y2=200, x3=800}
    tween(2, pos, {x1=800, y2=800, x3=200}, {easing=tween.easing.cubicInOut, loop=tween.loop.pingpong})
    movement = true 
    profiler.init()
    print ("tap screen to toggle blur")
end

function draw()
    if movement then
        setContext(blur[1])
        background(50)
        
        sprite("SpaceCute:Rocketship",pos.x1,HEIGHT-200)
        sprite("Small World:Store Extra Large",pos.x3,200,300)
        sprite("SpaceCute:Beetle Ship",WIDTH/2,pos.y2)
    end
    
    if showBlur then  --draw blur if required
        setContext(blur[2])
       -- background(50) --nice after-image effect if you dont clear the intermediary layer
        blurred[1]:draw() --pass one, offscreen
        setContext()
        blurred[2]:draw() --pass two, onscreen
    else
        setContext()
       -- background(50) --doesn't seem to be necesary to clear screen, for some reason
        unblurred:draw()  
    end
    profiler.draw()
end

--touching toggles blur on and off
function touched(t)
    if t.state==ENDED then showBlur = not showBlur end
end

profiler={}

function profiler.init(quiet)    
    profiler.del=0
    profiler.c=0
    profiler.fps=0
    profiler.mem=0
    if not quiet then
        parameter.watch("profiler.fps")
        parameter.watch("profiler.mem")
    end
end

function profiler.draw()
    profiler.del = profiler.del +  DeltaTime
    profiler.c = profiler.c + 1
    if profiler.c==10 then
        profiler.fps=profiler.c/profiler.del
        profiler.del=0
        profiler.c=0
        profiler.mem=collectgarbage("count", 2)
    end
end
--# Gaussian
Gaussian = {vs =  [[ 
uniform mat4 modelViewProjection;
uniform vec2 blurRadius;
attribute vec4 position;
attribute vec2 texCoord;

varying vec2 vBlurCoords[5];
 
void main()
{
    gl_Position = modelViewProjection * position;
	 vBlurCoords[0] = texCoord;
	 vBlurCoords[1] = texCoord + blurRadius * 1.407333;
	 vBlurCoords[2] = texCoord - blurRadius * 1.407333;
    vBlurCoords[3] = texCoord + blurRadius * 3.294215;
    vBlurCoords[4] = texCoord - blurRadius * 3.294215;
}
 
]],
--optimised fragment shader
fs = [[
precision mediump float;
 
uniform lowp sampler2D texture;

varying vec2 vBlurCoords[5];
 
void main()
{
  //  gl_FragColor = vec4(0.0);

    gl_FragColor = texture2D(texture, vBlurCoords[ 0]) * 0.304005; 
    gl_FragColor += texture2D(texture, vBlurCoords[ 1])* 0.204164; 
    gl_FragColor += texture2D(texture, vBlurCoords[ 2])* 0.204164; 
    gl_FragColor += texture2D(texture, vBlurCoords[ 3])* 0.093913; 
    gl_FragColor += texture2D(texture, vBlurCoords[ 4])* 0.093913; 

}]]}

Ok, I lied in my previous post, this really is the last one! I added a tween to the x and y radii of the blur to create a fun, “drunken insect eye” effect. Have a play, and let me know your fps (Added it to Codea Community) :


--# Main
--Gaussian blur
--adapted by Yojimbo2000 from http://xissburg.com/faster-gaussian-blur-in-glsl/ and http://www.sunsetlakesoftware.com/2013/10/21/optimizing-gaussian-blurs-mobile-gpu

function setup()
    --2 pass Gaussian blur:
    local downSample = 0.5 -- going down to 0.25 actually looks pretty good!
    local dimensions = {
    vec2(WIDTH, HEIGHT), --full size
    vec2(WIDTH, HEIGHT) * downSample --down sampled
    }
    blurRadii = {
    vec2(0.002,0), --horizontal pass, increase x value for more horizontal blur
    vec2(0,0.002) --vertical pass, increase y value for more vertical blur
    }
    blur = {}  --images
    blurred = {} --meshes
    for i=1,2 do --2 passes, one for horizontal, one vertical
        blur[i]=image(dimensions[i].x, dimensions[i].y) --image 1 is full sized, image 2 is downsampled
        blurred[i]=mesh()
        blurred[i].texture=blur[i]
        local j=3-i --invert i so that...
        blurred[i]:addRect(dimensions[j].x/2, dimensions[j].y/2, dimensions[j].x, dimensions[j].y) --mesh 1 rect is down-sampled, mesh 2 rect is full-sized (ie, opposite of their images)
        blurred[i].shader=shader(Gaussian.vs, Gaussian.fs)
        blurred[i].shader.blurRadius=blurRadii[i]
    end

    --mesh w/o the blur shader:
    unblurred=mesh() 
    unblurred.texture=blur[1]
    unblurred:addRect(WIDTH/2,HEIGHT/2,WIDTH,HEIGHT)
    
    showBlur=1 --blur state
    
    --some animations:
    pos={x1=200, y2=200, x3=800}
    radii={r1 = vec2(0.03,0.01), r2=vec2(-0.01,-0.03)}
    tween(2, pos, {x1=800, y2=800, x3=200}, {easing=tween.easing.cubicInOut, loop=tween.loop.pingpong})
    tween(3, radii, {r1 = vec2(-0.03,-0.01), r2=vec2(0.01,0.03)}, {easing=tween.easing.sineInOut, loop=tween.loop.pingpong})
    movement = true 
    profiler.init()
    print ("tap screen to cycle blur effects")
end

function draw()
    if movement then
        setContext(blur[1])
        background(50)
        
        sprite("SpaceCute:Rocketship",pos.x1,HEIGHT-200)
        sprite("Small World:Store Extra Large",pos.x3,200,300)
        sprite("SpaceCute:Beetle Ship",WIDTH/2,pos.y2)
    end
    
    if showBlur<3 then  --draw blur if required
        if showBlur==2 then
            blurred[1].shader.blurRadius = radii.r1
            blurred[2].shader.blurRadius = radii.r2
        else
            blurred[1].shader.blurRadius = blurRadii[1]
            blurred[2].shader.blurRadius = blurRadii[2]
        end
        setContext(blur[2])
       -- background(50) --nice after-image effect if you dont clear the intermediary layer
        blurred[1]:draw() --pass one, offscreen
        setContext()
        blurred[2]:draw() --pass two, onscreen
    elseif showBlur==3 then
        setContext()
       -- background(50) --doesn't seem to be necesary to clear screen, for some reason
        unblurred:draw()  
    end
    profiler.draw()
end

--touching toggles blur on and off
function touched(t)
    if t.state==ENDED then
        showBlur = showBlur + 1
        if showBlur==4 then showBlur=1 end
    end
end

profiler={}

function profiler.init(quiet)    
    profiler.del=0
    profiler.c=0
    profiler.fps=0
    profiler.mem=0
    if not quiet then
        parameter.watch("profiler.fps")
        parameter.watch("profiler.mem")
    end
end

function profiler.draw()
    profiler.del = profiler.del +  DeltaTime
    profiler.c = profiler.c + 1
    if profiler.c==10 then
        profiler.fps=profiler.c/profiler.del
        profiler.del=0
        profiler.c=0
        profiler.mem=collectgarbage("count", 2)
    end
end
--# Gaussian
Gaussian = {vs =  [[ 
uniform mat4 modelViewProjection;
uniform vec2 blurRadius;
attribute vec4 position;
attribute vec2 texCoord;

varying vec2 vBlurCoords[5];
 
void main()
{
    gl_Position = modelViewProjection * position;
	 vBlurCoords[0] = texCoord;
	 vBlurCoords[1] = texCoord + blurRadius * 1.407333;
	 vBlurCoords[2] = texCoord - blurRadius * 1.407333;
    vBlurCoords[3] = texCoord + blurRadius * 3.294215;
    vBlurCoords[4] = texCoord - blurRadius * 3.294215;
}
 
]],

--optimised fragment shader
fs = [[
precision mediump float;
 
uniform lowp sampler2D texture;

varying vec2 vBlurCoords[5];
 
void main()
{
  //  gl_FragColor = vec4(0.0);

    gl_FragColor = texture2D(texture, vBlurCoords[ 0]) * 0.304005; 
    gl_FragColor += texture2D(texture, vBlurCoords[ 1])* 0.204164; 
    gl_FragColor += texture2D(texture, vBlurCoords[ 2])* 0.204164; 
    gl_FragColor += texture2D(texture, vBlurCoords[ 3])* 0.093913; 
    gl_FragColor += texture2D(texture, vBlurCoords[ 4])* 0.093913; 

}]]}

I think that what Notification Center does (it wasn’t made in Codea, but if it was) is draw the screen into a small image, then scale that up and display the blurred result. I know it might create some odd effects when things are moving on the blurred screen, but the same happens in the real Notification Center. Try scrolling up this post, and while it’s moving, drag down Notification Center.

Yes, I think so. That’s what my code does. Change the downsamples variable to affect how small the intermediary image is.

Wow, thanks for all this! I just came to check, and saw all the code! Not sure which method will work best for me, but I’ll definantly look into all of them. Again, thanks a ton!

With my ones, I think you can just look at the last one.

Ok, will do

@yojimbo2000 - thats better, about 55 now

@Ignatz cool, glad to hear that. I put the downsampling code (from my last entry) into my first entry at the top of this thread, the one that samples the texture 15 times per fragment, to see if I could get that up to speed on the iPad Air. At full-pixel it runs at around 35 fps, downsampling by 0.5, at 50 fps. Interestingly though, if you go down to 0.25, it goes at about 40 fps or so. So presumably there’s a point at which too big a downscale/ upscale operation cancels out the benefit of quartering the number of calls to the fragment shader. In this particular case, downsampling by 0.5 seems to be optimal. You could also investigate scaling all of the draw operations too (of the 3 sprites I mean), see if that’s quicker than drawing a 0.5 image of the whole scene.