Performance of arc mesh/shader vs ellipse function

Hi all,

I read that to get better performance on drawing, you should write the drawing using mesh and shader. I need to draw some arcs, I mean lots of arcs. I used to draw an arc using simple trigonometry and use ellipses as the “pixel”. It’s fine as long as I draw less than 10 arcs. Above that, it’s getting to slow, like 3-5 fps. While I need to draw about 50-60 moving arcs simultaneously. :frowning:

Then, I took a look at the arc shader sample and use the arc function from Codea’s wiki. To my surprise, its performance is almost equal to my technique. I knew there must be something wrong, but I couldn’t find it.

So, could someone tell me how to draw arc fastly? It’d be great if it’s able to draw arc smoothly (anti-aliased) and respect the lineCapMode as well.

Thank you.

The arc shader works very well. Unfortunately it doesn’t respect lineCapMode (afaik, if not feel free to correct me).

Using the code I wrote below, I am able to stamp arcs all over the screen while maintaining 60 FPS.


--# Main
-- Arc

-- Use this function to perform your initial setup
function setup()
    parameter.watch("1/DeltaTime")
    arcs = {}
end

-- This function gets called once every frame
function draw()
    -- This sets a dark background color 
    background(255, 255, 255, 255)

    for i, arc in ipairs(arcs) do
        arc:draw()
    end
end

function touched(touch)
    if touch.state == ENDED then
        table.insert(arcs, Arc(touch.x, touch.y))
        arcs[#arcs]:start()
    end
end

--# Timer
Arc = class()

function Arc:init(x, y, s, col, t)
    self.x = x or WIDTH / 2
    self.y = y or HEIGHT / 2
    self.size = s or WIDTH / 10
    self.time = t or 10
    
    self.color = col or color(245, 3, 120, 255)
    
    self.paused = false
    self.amnt = 0
    
    self.tMesh = mesh()
    self.tMesh.vertices = triangulate({vec2(-self.size / 2, -self.size / 2),
                        vec2(-self.size / 2, self.size / 2),
                        vec2(self.size / 2, self.size / 2),
                        vec2(self.size / 2, -self.size / 2)})
    self.tMesh.shader = shader("Patterns:Arc")
    self.tMesh.shader.a1 = math.pi
    self.tMesh.shader.a2 = math.pi
    self.tMesh.shader.size = .45
    self.tMesh.shader.color = self.color
    self.tMesh.texCoords = triangulate({vec2(0,0),vec2(0,1),vec2(1,1),vec2(1,0)})
end

function Arc:start()
    self.amnt = 0
    if self.timing == nil then
        self.timing = tween(self.time, self, { amnt = 1 }, tween.easing.linear)
    end
end

function Arc:pause()
    if self.timing ~= nil then
        tween.stop(self.timing)
    end
    
    self.paused = true
end

function Arc:resume()
    if self.timing ~= nil then
        tween.play(self.timing)
    end
    
    self.paused = false
end

function Arc:stop()
    if self.timing ~= nil then
        tween.stop(self.timing)
        self.timing = nil
    end
end

function Arc:restart()
    self:stop()
    self:start()
end

function Arc:draw()
    -- Update timer
    -- self.tMesh.shader.color = vec4(1 * self.amnt, 1 - (1 * self.amnt), 0, 1)
    self.tMesh.shader.a2 = -self.amnt * (math.pi * 2) + math.pi
    
    -- Draw timer
    pushMatrix()
    
    translate(self.x, self.y)
    
    rotate(270.1)
    
    self.tMesh:draw()
    
    popMatrix()
end

Thanks for the code, @jakattak. I forgot to tell in the first post that I need to draw big and thick arcs. As I modify your code to draw big and thick arcs (mesh.size = WIDTH * 0.75; shader.size = 0.25), unfortunately, your code doesn’t do any better than mine. Once there are more than 3 arcs, the drawing speed drops rapidly, like 5-7 fps or lower. Everything goes fast if arcs are small and thin, my code also got no problem with that. :slight_smile:

Oh, one more thing… I’m using iPad 1 on iOS 5. :frowning:

@bee Can you give a sample of how many, how thick, and how big your arcs are for testing? I have some code for drawing curves that I could dig out, but I don’t want to bother posting it if it isn’t any improvement.

As far as I’m aware, the inbuilt arc shader is basically a rectangle with stuff chopped out. This makes it expensive as the rectangles can be very big. My curve drawing makes the curve itself into a mesh which is actually cheaper even though it uses more vertices in the mesh.

@andrew_stacey: You could use @jakattak’s code above. Just change these lines:

in Arc:init function self.size = s or WIDTH / 10 to self.size = s or (WIDTH * 0.75) and self.tMesh.shader.size = .45 to self.tMesh.shader.size = .25

Run the program. Then tap to create about 50 arcs. The given arc dimension is about what I need. By the time you’re adding more arcs, you should see the drawing performance goes down considerably, long before you reach 50. At least that’s what I see on my iPad1.

I’m sorry I still can’t provide my own code because I’m still working on it. But basically, it’s the same code from the arc shader sample and wiki. Just a tiny bit of modification that I’m sure wouldn’t give noticeable affect to the performance.

If you need to be more precise… The diameter of arc range from 200 to 500 pixel. The thickness of arc range from 20 to 100 pixel. And the amount of arc to be drawn on the screen is about 40-60 arcs with variable sizes.

Actually, I also need to draw some sectors and segments (you know, part of circle), with stroke and fill color. If you have the code how to draw them fastly, I hope you can share it with us here, if you don’t mind. Thank you. :slight_smile:

@Bee since you want to make ‘thick’ objects, you will probably be slow. Can you accept a sharpness loss? If yes, you could sprite your circles into an image 4 times smaller than the screen. This will be 16x faster. Then you sprite this image once with scale(4). The edges will be a bit smooth, but the fps might be good enough.

The arc object isn’t static. It should change (recalculated) the angle almost 30 times per second. Do you think using sprite technique will boost the performance?

@bee if you post your code i’ll check if i can make it faster with the sprite thing.
Using the sprite will drop around 30Hz because of setContext, but maybe it will stay at 25-30 because of the x16 gain…
The advantage is that it is simple to implement.
But @Andew_stacey’s solution is probably the best solution for thin arcs, but needs some work.

My code is now so messy with some (failed) optimization attempts. I need some time to clean it up, if really needed. But basically it’s taken from the given arc shader sample in Codea’s shader lab. So, you -or anyone else interested with this problem- could start from there. Or you can also use the @jakattak’s code above. Mine is similar to that, minus the class thingy.

The old version using simple trigonometry, taken from my analog clock program (available on CC).

I haven’t tried the sprite technique, yet. Because I thought it wouldn’t be any different. I think shader and mesh would be the best technique.

Or, should I make a contest for this? :smiley:

@bee i have checkd the sprite with jakattack code (modified for big thick circles) :it does work! Results:
– normal: #arcs: 40 => fps: 19.
– sprite: #arcs: 90 => fps: 56.
Note that this is on ipad air (5x faster than ipad1).
check this:


--# Main
-- Arc
-- on ipad air:
-- normal: #arcs: 40 => fps: 19
-- sprite: #arcs: 90 => fps: 56



-- Use this function to perform your initial setup
function setup()
    fps = 60
    parameter.watch("math.floor(fps)")
    parameter.watch("#arcs")
    parameter.action("print",status)
    arcs = {}
    img = image(WIDTH/4,HEIGHT/4)
end
function status()
    print("#arcs: "..tostring(#arcs) .." => fps: "..tostring(math.floor(fps)))
end
-- This function gets called once every frame
function draw()
    k = 0.01
    fps = fps*(1-k) + k/DeltaTime
    -- This sets a dark background color 

    setContext(img)
    background(255, 255, 255, 255)
    scale(1/4)
    for i, arc in ipairs(arcs) do
        arc:draw()
    end
    setContext()
    resetMatrix()
    background(255, 255, 255, 255)
    spriteMode(CORNER)
    sprite(img,0,0,WIDTH,HEIGHT)
end

function touched(touch)
    if touch.state == ENDED then
        table.insert(arcs, Arc(touch.x, touch.y,WIDTH))
        arcs[#arcs]:start()
    end
end


--# Timer
Arc = class()

function Arc:init(x, y, s, thick, col, t)
    self.x = x or WIDTH / 2
    self.y = y or HEIGHT / 2
    self.size = s or WIDTH / 10
    self.thick = thick or WIDTH / 10
    self.time = t or 10
    local ran = function() return math.random(255) end
    self.color = col or color(ran(), ran(), ran(), 255)

    self.paused = false
    self.amnt = 0

    self.tMesh = mesh()
    self.tMesh.vertices = triangulate({vec2(-self.size / 2, -self.size / 2),
                        vec2(-self.size / 2, self.size / 2),
                        vec2(self.size / 2, self.size / 2),
                        vec2(self.size / 2, -self.size / 2)})
    self.tMesh.shader = shader("Patterns:Arc")
    self.tMesh.shader.a1 = math.pi
    self.tMesh.shader.a2 = math.pi
    self.tMesh.shader.size = .1
    self.tMesh.shader.color = self.color
    self.tMesh.texCoords = triangulate({vec2(0,0),vec2(0,1),vec2(1,1),vec2(1,0)})
end

function Arc:start()
    self.amnt = 0
    if self.timing == nil then
        self.timing = tween(self.time, self, { amnt = 1 }, tween.easing.linear)
    end
end

function Arc:pause()
    if self.timing ~= nil then
        tween.stop(self.timing)
    end

    self.paused = true
end

function Arc:resume()
    if self.timing ~= nil then
        tween.play(self.timing)
    end

    self.paused = false
end

function Arc:stop()
    if self.timing ~= nil then
        tween.stop(self.timing)
        self.timing = nil
    end
end

function Arc:restart()
    self:stop()
    self:start()
end

function Arc:draw()
    -- Update timer
    -- self.tMesh.shader.color = vec4(1 * self.amnt, 1 - (1 * self.amnt), 0, 1)
    self.tMesh.shader.a2 = -self.amnt * (math.pi * 2) + math.pi

    -- Draw timer
    pushMatrix()

    translate(self.x, self.y)

    rotate(270.1)

    self.tMesh:draw()

    popMatrix()
end


Hmm, I’m not sure if this is actually much of an improvement. But here it is.

There’s an arc function and an Arc class. Parameters are centre, xaxis (can be vec2 or number), yaxis (ditto), start angle (radians), delta angle (radians).

-- Arc path drawing

Arc = class()

local __makeArc = function(nsteps)
    -- nsteps doesn't make a huge difference in the range 50,300
    nsteps = nsteps or 50
    local m = mesh()
    m.shader = shader([[
//
// A basic vertex shader
//

//This is the current model * view * projection matrix
// Codea sets it automatically
uniform mat4 modelViewProjection;
uniform lowp vec4 scolour;
uniform lowp vec4 ecolour;
lowp vec4 mcolour = ecolour - scolour;
//This is the current mesh vertex position, color and tex coord
// Set automatically
attribute vec4 position;
attribute vec4 color;
attribute vec2 texCoord;

varying highp vec2 vTexCoord;
varying lowp vec4 vColour;
varying highp float vWidth;
varying highp float vCore;

uniform float width;
uniform float taper;
uniform float blur;
uniform float cap;
uniform float scap;
uniform float ecap;
float swidth = width + blur;
float ewidth = taper*width - width;
float ecapsw = clamp(cap,0.,1.)*ecap;
float scapsw = clamp(cap,0.,1.)*scap;
uniform vec2 centre;
uniform vec2 xaxis;
uniform vec2 yaxis;
uniform float startAngle;
uniform float deltaAngle;

void main()
{
    highp float t = clamp(position.y,0.,1.);
    vCore = t;
    highp float w = smoothstep(0.,1.,t);
    vWidth = w*ewidth + swidth;
    highp vec2 bpos = centre + cos(t*deltaAngle + startAngle) * xaxis + sin(t*deltaAngle + startAngle) * yaxis;
    highp vec2 bdir = -sin(t*deltaAngle + startAngle) * xaxis + cos(t*deltaAngle + startAngle) * yaxis;
    bdir = vec2(bdir.y,-bdir.x);
    bdir = vWidth*normalize(bdir);
    bpos = bpos + position.x*bdir;
    highp vec4 bzpos = vec4(bpos.x,bpos.y,0.,1.);
    bzpos.xy += (ecapsw*max(position.y-1.,0.)
                +scapsw*min(position.y,0.))*vec2(-bdir.y,bdir.x);
    highp float s = clamp(position.y, 
            scapsw*position.y,1.+ecapsw*(position.y-1.));
    vTexCoord = vec2(texCoord.x,s);
    vColour = t*mcolour + scolour;
    //Multiply the vertex position by our combined transform
    gl_Position = modelViewProjection * bzpos;
}
]],[[
//
// A basic fragment shader
//

uniform highp float blur;
uniform highp float cap;

varying highp vec2 vTexCoord;
varying highp float vWidth;
varying lowp vec4 vColour;
varying highp float vCore;

void main()
{
    lowp vec4 col = vColour;
    highp float edge = blur/(vWidth+blur);
    col.a = mix( 0., col.a, 
            (2.-cap)*smoothstep( 0., edge, 
                min(vTexCoord.x,1. - vTexCoord.x) )
            * smoothstep( 0., edge, 
                min(1.5-vTexCoord.y, .5+vTexCoord.y) ) 
            + (cap - 1.)*smoothstep( 0., edge,
             .5-length(vTexCoord - vec2(.5,vCore)))
                );

    gl_FragColor = col;
}
]])

    for n=1,nsteps do
        m:addRect(0,(n-.5)/nsteps,1,1/nsteps)
    end
    m:addRect(0,1.25,1,.5)
    m:addRect(0,-.25,1,.5)
    return m
end

local m = __makeArc()
m.shader.blur = 2
m.shader.cap = 2
m.shader.scap = 1
m.shader.ecap = 1

-- centre, xaxis, yaxis, startAngle, deltaAngle, taper
function arc(a,b,c,d,e,f)
    if type(a) == "table" then
        f = b
        a,b,c,d,e = unpack(a)
    end
    if type(b) ~= "userdata" then
        b = b*vec2(1,0)
    end
    if type(c) ~= "userdata" then
        c = c*vec2(0,1)
    end
    --m.shader.blur = 15
    m.shader.taper = f or 1
    m.shader.width = strokeWidth()
    m.shader.scolour = color(stroke())
    m.shader.ecolour = color(stroke())
    m.shader.cap = (lineCapMode()-1)%3
    m.shader.centre = a
    m.shader.xaxis = b
    m.shader.yaxis = c
    m.shader.startAngle = d
    m.shader.deltaAngle = e
    m:draw()
end

function Arc:init(...)
    self:setParams(...)
end

function Arc:clone()
    return Arc(self.params)
end

function Arc:makeDrawable(t)
    t = t or {}
    local nsteps = t.steps or self.steps
    local m = __makeArc(nsteps)
    m.shader.taper = t.taper or self.taper or 1
    m.shader.blur = t.blur or self.blur or 2
    m.shader.cap = t.cap or self.cap or (lineCapMode()-1)%3
    m.shader.scap = t.scap or self.scap or 1
    m.shader.ecap = t.ecap or self.ecap or 1
    m.shader.width = t.width or self.width or strokeWidth()
    m.shader.scolour = t.scolour or self.scolour or t.colour or color(stroke())
    m.shader.ecolour = t.ecolour or self.ecolour or t.colour or color(stroke())
    local a,b,c,d,e = unpack(self.params)
    m.shader.centre = a
    m.shader.xaxis = b
    m.shader.yaxis = c
    m.shader.startAngle = d
    m.shader.deltaAngle = e
    self.curve = m
    self.draw = function(self) self.curve:draw() end
end

function Arc:draw(t)
    self:makeDrawable(t)
    self.curve:draw()
end

function Arc:setParams(a,b,c,d,e)
    if type(a) == "table" then
        a,b,c,d,e = unpack(a)
    end
    if type(b) ~= "userdata" then
        b = b*vec2(1,0)
    end
    if type(c) ~= "userdata" then
        c = c*vec2(0,1)
    end
    self.params = {a,b,c,d,e}
    if self.curve then
        self.curve.shader.centre = a
        self.curve.shader.xaxis = b
        self.curve.shader.yaxis = c
        self.curve.shader.startAngle = d
        self.curve.shader.deltaAngle = e
    end
end

function Arc:setStyle(t)
    self.scolour = t.scolour or t.colour or self.scolour
    self.ecolour = t.ecolour or t.colour or self.ecolour
    self.width = t.width or self.width
    self.taper = t.taper or self.taper
    self.blur = t.blur or self.blur
    self.cap = t.cap or self.cap
    self.scap = t.scap or self.scap
    self.ecap = t.ecap or self.ecap
    if not self.curve then 
        return
    end
    t = t or {}
    if t.colour then
        self.curve.shader.scolour = t.colour
        self.curve.shader.ecolour = t.colour
    end
    if t.scolour then
        self.curve.shader.scolour = t.scolour
    end
    if t.ecolour then
        self.curve.shader.ecolour = t.ecolour
    end
    if t.width then
        self.curve.shader.width = t.width
    end
    if t.taper then
        self.curve.shader.taper = t.taper
    end
    if t.blur then
        self.curve.shader.blur = t.blur
    end
    if t.cap then
        self.curve.shader.cap = t.cap
    end
    if t.scap then
        self.curve.shader.scap = t.scap
    end
    if t.ecap then
        self.curve.shader.ecap = t.ecap
    end
end

function Arc:point(t)
    local a,b,c,d,e = unpack(self.params)
    return a + math.cos(t*e + d)*b + math.sin(t*e + d)*c
end

function Arc:tangent(t)
    local a,b,c,d,e = unpack(self.params)
    return -math.sin(t*e + d)*b + math.cos(t*e + d)*c
end

function Arc:normal(t)
    return self:tangent(t):rotate90()
end

function Arc:unitNormal(t)
    local pt = self:normal(t)
    local l = pt:len()
    if l == 0 then
        return vec2(0,0)
    else
        return pt/l
    end
end

function Arc:unitTangent(t)
    local pt = self:tangent(t)
    local l = pt:len()
    if l == 0 then
        return vec2(0,0)
    else
        return pt/l
    end
end

@bee have you tried my code? What perf do you get?

@jmv38: I’ve tried your code. Thank you. So far, yours is the fastest. It could maintain 50-55 fps even when drawing over 50 arcs. But, a very big but, and I’m sorry for saying this, it looks ugly. The pixel is very big, making the arc/circle very pixelated. I haven’t yet modifying your code though. I’ve been busy with something else. Any hint to make it looks (a lot) better?

@andrew_stacey: Thank you. Your code is a lot better than mine or @jakattak’s. While our code can’t go over 5 arcs without noticable lag, yours is able to get over 10. It’s about double of our code. The algorithm is also good, producing nice and sharp circle/arc. The tapper effect is quite nice. Unfortunately, the performance is still below my expectation. :frowning:

Mr Andrew, if you don’t mind, would you please explain your code to us here? Especially the shader and mesh part. (I’m still trying to understand the magic behind shader.) I want to enhance your code for drawing other parts of circle, like sector and segment. I need it to have fill color. Any hints? :slight_smile:

Btw, I still don’t understand the “magic” constants for shader program. I see some unreasonable value being used here and there like 0.5, 1.0, and 2.0. What are these numbers? I’ve been digging the Codea’s reference with no luck. :frowning:

@bee i agree the weak part is pixellization (i warned you about this…). It is possible to trade the ‘pixellized edge’ against ‘smooth edge’. Would it be acceptable? Otherwise no solution.
If you are ready to accept smooth edge, edit the shader to make smooth edge over 1 or 2 pixels. After x4 spriting, it will still look smooth (but 4 times bigger edge, of course).
Good luck!

Actually there might be a solution with one shader, but very complex (i mean simultate vector graphics?). Beyond me, though…

A possible trade off: instead of x4, try x3 or x2: the edges will be better, and the fps maybe still acceptable (30Hz).

It is possible to trade look with speed. As long as the look is quite acceptable. I’ll study your code and we’ll see what can I do to make it produces better arcs. Thank you. :slight_smile:

Actually, I think @andrew_stacey’s code looks promising. Though I don’t really understand the shader part. Waiting for Mr Andrew explanation. Perhaps there is still room for further improvements.