Howdy, Stranger!

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

Performance of arc mesh/shader vs ellipse function

beebee
edited May 2014 in Questions Posts: 381

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

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.

Tagged:
«1

Comments

  • Posts: 2,043

    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
  • beebee
    Posts: 381

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

    Oh, one more thing... I'm using iPad 1 on iOS 5. :(

  • Posts: 2,161

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

  • beebee
    edited May 2014 Posts: 381

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

  • beebee
    edited May 2014 Posts: 381

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

  • Jmv38Jmv38 Mod
    Posts: 3,297

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

  • beebee
    Posts: 381

    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?

  • Jmv38Jmv38 Mod
    edited May 2014 Posts: 3,297

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

  • beebee
    edited May 2014 Posts: 381

    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? :D

  • Jmv38Jmv38 Mod
    Posts: 3,297

    @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
  • Posts: 2,161

    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
    
  • Jmv38Jmv38 Mod
    Posts: 3,297

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

  • beebee
    edited May 2014 Posts: 381

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

    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? :)

  • beebee
    Posts: 381

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

  • Jmv38Jmv38 Mod
    Posts: 3,297

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

  • Jmv38Jmv38 Mod
    Posts: 3,297

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

  • Jmv38Jmv38 Mod
    Posts: 3,297

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

  • beebee
    Posts: 381

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

  • beebee
    Posts: 381

    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.

  • Posts: 2,161

    This creates the arc "object": a mesh with a shader attached. The arc function calls this with different parameters passed to the shader. The Arc class uses this function to create a new mesh for every instance.

    So using the arc function means one mesh, but every invocation requires fresh data passed to the GPU. Using the Arc class means many meshes, but each is hard-coded with its data (though it can be changed).

    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([[
    

    Now we examine the shader code, starting with the vertex shader.

    //
    // A basic vertex shader
    //
    
    //This is the current model * view * projection matrix
    // Codea sets it automatically
    uniform mat4 modelViewProjection;
    

    The uniforms scolour and ecolour hold the start and end colours of the arc. From these, we compute the difference "colour" (it may not be a valid colour, but that's not important).

    uniform lowp vec4 scolour;
    uniform lowp vec4 ecolour;
    lowp vec4 mcolour = ecolour - scolour;
    

    Standard mesh attributes and varyings.

    //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;
    

    The varying vCore holds the parameter along the curve clamped to [0,1] which will be used in the fragment shader to deal with the ends. The body of the curve corresponds to y positions in [0,1] and the caps extend beyond, so if y == vCore then we're in the body of the curve and if y ~= vCore then we're in one of the caps.

    varying highp float vCore;
    

    These constants control the appearance of the curve. width is hopefully obvious, taper is how much the width varies, blur is how much the edges are blurred. So the actual starting width, swidth, is width + blur and the actual ending width, ewidth, is taper * width - width (hmm, now that I look at it maybe there should be a + blur here!). We can set a global cap type for both ends and scap and ecap are used to turn it on or off for start and finish separately. The variables ecapsw and scapsw contain how far the caps extend from the curve.

    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;
    

    This set of parameters control the actual arc parameters. The angles are in radians. The axes are vectors as they needn't be the x-axis and y-axis.

    uniform vec2 centre;
    uniform vec2 xaxis;
    uniform vec2 yaxis;
    uniform float startAngle;
    uniform float deltaAngle;
    

    Now we enter the main function. The idea of the shader is that the mesh is a series of rectangles running from (-.5,0) to (.5,1) (well, it extends slightly above and below for the line caps). The y-coordinate is used to parametrise the arc and the x-coordinate goes across the curve. So the vertex shader needs to move these vertices into the right places and scale them accordingly.

    void main()
    {
    

    As said above, the y value is in the interval [0,1] except for the caps which extend slightly out, so t is the actual parameter along the curve.

        highp float t = clamp(position.y,0.,1.);
        vCore = t;
    

    To compute the width, we need to interpolate between the starting and ending widths. Rather than a linear interpolation, we use smoothstep so that the width doesn't change much at the start and end of the arc. This means that if we put two arcs next to each other with matching widths then there won't be a noticeable change in how much the widths change.

        highp float w = smoothstep(0.,1.,t);
        vWidth = w*ewidth + swidth;
    

    This contains the position along the curve.

        highp vec2 bpos = centre + cos(t*deltaAngle + startAngle) * xaxis + sin(t*deltaAngle + startAngle) * yaxis;
    

    To work out the thickness, we need to know the normal vector at this point on the curve. We start with the tangent vector.

        highp vec2 bdir = -sin(t*deltaAngle + startAngle) * xaxis + cos(t*deltaAngle + startAngle) * yaxis;
    

    Rotate it to get the normal vector.

        bdir = vec2(bdir.y,-bdir.x);
    

    Normalise, and multiply by the width and the x-coordinate of the vertex.

        bdir = vWidth*normalize(bdir);
        bpos = bpos + position.x*bdir;
    

    Convert this to a vec4 since that's what the shader needs to produce at the end.

        highp vec4 bzpos = vec4(bpos.x,bpos.y,0.,1.);
    

    The caps need special handling. This ensures that they poke out a bit from the ends of the arcs in the right directions.

        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);
    

    This blends the colours.

        vColour = t*mcolour + scolour;
        //Multiply the vertex position by our combined transform
        gl_Position = modelViewProjection * bzpos;
    }
    ]],[[
    //
    // A basic fragment shader
    //
    

    Now the fragment shader, we need to know the blur and cap type.

    uniform highp float blur;
    uniform highp float cap;
    

    All the varyings we want from the vertex fragment.

    varying highp vec2 vTexCoord;
    varying highp float vWidth;
    varying lowp vec4 vColour;
    varying highp float vCore;
    
    void main()
    {
    

    Start with the main colour (the blending from start to finish is already taken care of).

        lowp vec4 col = vColour;
    

    This tells us where the edge should be.

        highp float edge = blur/(vWidth+blur);
    

    The alpha is modified to take into account the edge and the cap type.

        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;
    }
    ]])
    

    Back to the lua code, as said in the shader the arc is a mesh filling in the rectangle from (-.5,0) to (.5,1). We use lots of rectangles to make the arc smooth.

        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
    

    There's a lot of flexibility in there which could be removed to speed it up a bit. One thing it doesn't do is fill in the arc: it is definitely an arc and not a segment of a circle.

  • beebee
    Posts: 381

    @andrew_stacey: Thank you so much for the explanation. I'll play around with your code. Yes, I knew it draws an arc. That's why I asked for a guide how to make it into drawing a sector and segment. I still don't have a clear understanding about how to fill an area using shader. :)

  • Jmv38Jmv38 Mod
    Posts: 3,297

    @bee the problem is that when you'll want to fill the area, you'll be back to your fps problem...

  • Posts: 425

    If you don't need to change the angle of your arc you could just draw it once with setContext()

  • beebee
    edited May 2014 Posts: 381

    @jvm38: Of course. But I think it should be possible using OpenGL (as Codea use it as backend). Some games are able to draw thousands of meshes with acceptable speed, even on iPad1.

    @coder: If I want to draw static arc, it wouldn't be this difficult. :)

  • Jmv38Jmv38 Mod
    Posts: 3,297

    @bee the problem is not the number of sprites but the total number of screen pixels that have to be redrawn over and over again when you superpose many very large circles, as you want to do (if i've understood you correctly). Unless you use some additionnal tricks to skip drawing the hidden circle parts, you've already exausted the ipad gpu and you wont succed to be faster with 'brute force'. As you've said, codea mesh uses OpenGL, very directly i assume, so there not much to hope from a direct drawing approch. I say all this just because this is my current understanding of OpenGL, but i am not an expert, so i may be wrong.

  • beebee
    edited May 2014 Posts: 381

    Then how those OpenGL-based games are able to draw hundreds even thousands of meshes so fast? Maybe we should summon the experts, @simeon and @john, to join our discussion and give us some enlightments. :)

  • Jmv38Jmv38 Mod
    Posts: 3,297

    Can you tell which games draws hundred of full-screen-size meshes simultaneously on the screen? I am really puzzled. (i have drawn myself meshes with 60 000 vertex on the ipad1 with very good speed, but in practice each pixel of the screen would correspond to only 2 triangles max. Your case is very different, since the cicles do overlap and each covers a large fraction of the screen..).

  • Posts: 2,161

    @bee What I meant was that my mesh/shader approach draws the arc and only the arc. It doesn't cover the segment of the circle. The arc shader that comes with Codea, on the other hand, does draw the segment of the circle (though possibly with transparent pixels, which is one reason that it is so slow). On the other hand, it shouldn't be difficult to adapt my shader to draw the whole segment. Basically, the left-hand side of the curve should go to the centre point. That ought to do it.

  • Posts: 1,976

    @bee Most OpenGL-based games are made in Objective-C and OpenGL. Codea is much slower, probably mainly because Lua is a very slow language in general. It's not the standard, built-in programming language, it has a bunch of other complications. Objective-C is native, so it is much faster.

    (Correct me if I'm wrong.)

  • beebee
    Posts: 381

    @jmv38: Infinity Blade? EA Games' games? CMIIW.

    @andrew_stacey: I can understand how to make a segment from an arc. What I don't quite understand is how to fill it. Especially to have an acceptable speed.

    @skythecoder: Yes, I thought about it as well. I also read that trigonometry functions is very expensive. It's suggested to use lookup table instead of calculate it during runtime.

  • Posts: 2,161

    Hmm, it would be interesting to compare the arc code above (with sines and cosines) with a bezier approximation (since that only involves polynomials). I have code for both so I'll give it a go and report back.

    With regard to filling the segment, that's what I was talking about. Simply send one edge of the rectangle to the centre and you're done.

  • Jmv38Jmv38 Mod
    Posts: 3,297

    @bee i have added a taper to smooth the circle edges for you. The visual result is no longer pixellized (but a bit fuzzy, of course!). Let me know how you like that one. Here is the code.


    --# 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 --# shader arcShader = { vprog = [[ // // Vertex shader: Arc // uniform mat4 modelViewProjection; attribute vec4 position; attribute vec2 texCoord; varying highp vec2 vTexCoord; void main() { vTexCoord = texCoord; gl_Position = modelViewProjection * position; } ]], fprog = [[ // // Fragment shader: Arc // precision highp float; uniform float size; uniform float a1; uniform float a2; uniform float pixelSize; uniform vec4 color; const float da = 0.01; varying vec2 vTexCoord; void main() { vec4 col = vec4(0.0); vec2 r = vTexCoord - vec2(0.5); float d = length(r); float taper = 1.0; if (d > size && d < 0.5) { taper = 1.0 - smoothstep(0.5-pixelSize*4.0, 0.5, d); taper = taper * smoothstep(size, size +pixelSize*4.0,d); float a = atan(r.y, r.x); if (a2 > a1) { if (a > a1 && a < a2) { col = color; } } else { if (a > a1 || a < a2) { col = color; if (a > a1) { taper = taper * smoothstep(a1, a1+0.01, a); } else { taper = taper * (1.0 - smoothstep(a2-0.01, a2, a)); } } } } gl_FragColor = col * vec4(1.0,1.0,1.0,taper); } ]] } --# 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 = shader(arcShader.vprog, arcShader.fprog) self.tMesh.shader.a1 = math.pi self.tMesh.shader.a2 = math.pi self.tMesh.shader.pixelSize = 1/s 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
  • Jmv38Jmv38 Mod
    Posts: 3,297

  • Jmv38Jmv38 Mod
    edited May 2014 Posts: 3,297

    There are still a couple defects in the 'almost horizontals' or verticals, to be removed with a larger angle deviation than 0.01 (or a better formula, because tha angle deviation does not correspond to a fixed distance).
    [edit].
    the program below shows more clearly the weak region: actually, it is only for a1=pi that there is a problem. Just avoid this value.

  • Jmv38Jmv38 Mod
    edited May 2014 Posts: 3,297

    --# 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 --# shader arcShader = { vprog = [[ // // Vertex shader: Arc // uniform mat4 modelViewProjection; attribute vec4 position; attribute vec2 texCoord; varying highp vec2 vTexCoord; void main() { vTexCoord = texCoord; gl_Position = modelViewProjection * position; } ]], fprog = [[ // // Fragment shader: Arc // precision highp float; uniform float size; uniform float a1; uniform float a2; uniform float pixelSize; uniform vec4 color; const float da = 0.02; varying vec2 vTexCoord; void main() { vec4 col = vec4(0.0); vec2 r = vTexCoord - vec2(0.5); float d = length(r); float taper = 1.0; if (d > size && d < 0.5) { taper = 1.0 - smoothstep(0.5-pixelSize*4.0, 0.5, d); taper = taper * smoothstep(size, size +pixelSize*4.0,d); float a = atan(r.y, r.x); if (a2 > a1) { if (a > a1 && a < a2) { col = color; taper = taper * smoothstep(a1, a1+da, a); taper = taper * (1.0 - smoothstep(a2-da, a2, a)); } } else { if (a > a1 || a < a2) { col = color; if (a > a1) { taper = taper * smoothstep(a1, a1+da, a); } else { taper = taper * (1.0 - smoothstep(a2-da, a2, a)); // taper = 0.0; } } } } gl_FragColor = col * vec4(1.0,1.0,1.0,taper); } ]] } --# 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 = shader(arcShader.vprog, arcShader.fprog) self.tMesh.shader.a1 = math.pi self.tMesh.shader.a2 = math.pi self.tMesh.shader.pixelSize = 1/s 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
  • Posts: 2,043

    @Jmv38, that picture looks very nice, but on my iPad Mini (no retina display) it is still pixelated

  • Jmv38Jmv38 Mod
    edited May 2014 Posts: 3,297

    this is weird... Can you change the line

     self.tMesh.shader.pixelSize = 1/s
    

    by

     self.tMesh.shader.pixelSize = 1/s * 2
    

    to increase border by x2? Or try x3,x4 etc... it should look smooth with higher values.
    If nothing changes, then that means the function smoothstep is not supported on ipad mini?

  • beebee
    edited May 2014 Posts: 381

    @jmv38: I can confirm that your code is still produces pixelated arc on my iPad 1, as it's not retina as well. Increasing pixelSize makes the circle more blur, but sacrificing its edge sharpness. And the radius line is still pixelated, pixelSize doesn't help it.

    I think this blurness and pixelation effect is because the image scaling. That's why I hesitated to use this technique since the beginning. Unless someone could show me a technique to minimize it.

  • beebee
    edited May 2014 Posts: 381

    Btw, I'm still waiting for polinomial approach from @andrew_stacey. As it doesn't use expensive trigonometry functions, I hope it would be fast and still maintain beautiful look. *finger crossed* :D

  • Jmv38Jmv38 Mod
    Posts: 3,297

    @bee some new info:
    1/ i have checked on my ipad1: i cobfirm the image is pixellized.
    2/ settling the pixelsize x3 gives a non-pixellized result, but smooth edge (not sharp).
    3/ the straight lines are still pixellized, but this could be solved at the same cost as for circle: fuzziness of the border. Is it acceptable? (i wont do the work if you consider that x3 is too fuzzy).
    4/ i think there is a solution for speed and sharpness: associate my method and andrew's. What i mean is:
    4.a/ draw the surface with my method (and forget the sahder, just make pixellized edges). This will fill the surfaces very quickly.
    4.b/ on top of that, draw the edges with andrew segment shader. This will be perfectly sharp and very quick because only a few pixels will be updated.
    But this is much work. Is it worth it? It depends on why you want these circles for (you've not explained it in details).

  • beebee
    edited May 2014 Posts: 381

    @andrew_stacey: I just noticed that your code also doesn't anti-aliased the radius line (the straight line). I didn't notice it before because my test program use ROUND linecap as the default. Pardon my lack of understanding of shader, but how did you draw the round cap? Which line is it in the code? Any hint how to anti-alias the line as well?

    What do you mean by…

    The idea of the shader is that the mesh is a series of rectangles running from (-.5,0) to (.5,1)

    Why .5? I still don't get it. :( It seems that each mesh has its own coordinate within it. Am I right? Do you have any reference to study about this openGL shader things? Codea's doc about shader doesn't give me enough informations. Or perhaps I didn't read enough.

    @jmv38: I don't really like the idea of image scaling. Yes, it boosts the performance, but I don't quite like the result. It's too blurry. CMIIW. If you think the work is too much, then don't bother to write code sample for me. :) I just need hints and direction how to do it, let me write the code myself, if you mind to provide code sample. Your explanation above is quite clear to me. Thank you.

    My biggest problem here –as I said since the beginning– is with the shader. I still don't really understand how does the shader work with mesh and how to draw using shader. What's uniform? What's varying? Etc. Some of its functions are a bit different from Lua's counter part. It's like a whole different alien world separated from Codea.

  • Jmv38Jmv38 Mod
    Posts: 3,297

    @bee thanks for the feedback. I'll see if i can do something to get really sharp edges, but no garantee.
    Concerning shaders: before i made my shader bench (on CC) i was completely lost with shaders. I had a week of vacations so i read Ignatz tuto on shaders, and more the links at the end of his tuto: the shader spec and quicksheet. Then i programmed step by step, starting from smoothing shader and including more and more variants. After one one week it was about clear to me how shaders work (although i am not very gifted at programming them). So you can surely do the same (pbly faster) when you'll decide to. It is not really complex, ignatz tuto will answer your questions above. :-)

  • Jmv38Jmv38 Mod
    Posts: 3,297

    i have tried 4.a + 4.b but it doesnt work: the speed is ok, but the edges are not correctly masked.. :((

  • Jmv38Jmv38 Mod
    Posts: 3,297

    just because it is nice visually, here is what happens:

  • beebee
    edited May 2014 Posts: 381

    I found link to @ignatz's shader tutorial ebook by searching this forum. Unfortunately, the link that refers to the file in Ignatz's dropbox is no longer active. I can read some articles about shaders in Ignatz's blog. But I want the ebook so I can read it offline. If someone here got the ebook, would you please reshare it for us here? Thank you.

  • IgnatzIgnatz Mod
    edited May 2014 Posts: 5,396

    @bee - Oops, I moved some files and broke the links

    I have relinked them here:
    https://www.dropbox.com/sh/mr2yzp07vffskxt/AACqVnmzpAKOkNDWENPmN4psa

  • beebee
    edited May 2014 Posts: 381

    Thank you, @ignatz. Btw, can you help me with this arc problem? :)

  • IgnatzIgnatz Mod
    Posts: 5,396

    @bee - sorry, I'm not very active in Codea any more, so I can't help with the arc problem (also, I suspect it involves a lot of math!)

    Start with my ebook, which should answer many of your questions about shaders, and look around for something on arcs, eg google "openGL arc shader"

  • Posts: 2,161

    @Ignatz

    sorry, I'm not very active in Codea any more

    Only temporarily, I hope.

  • Jmv38Jmv38 Mod
    Posts: 3,297

    @ignatz have you another hobby now?

Sign In or Register to comment.