Batch drawing shapes on one mesh

As part of my exploration of batch drawing methods (ie putting many objects on one mesh, to cut down on the number of draw calls) I’ve written addShape/setShape and addTri/setTri commands, based on the syntax of addRect and setRect. Unfortunately, as you can see in the profiler below, my setTri command is slower than setRect (even though it has half the number of vertices to shift); the bottleneck is rotating the vectors in setTri. Is there a faster way of rotating vectors than the builtin vec2:rotate()?


--# Main
-- 2D Mesh Profiling

function setup()
  shapePrototypes()
    methods = {Box, Tri, Pent, Box2, Box3}
    parameter.integer("number",200,5000,1000)
    parameter.integer("method", 1,4,2)
    parameter.action("INITIALISE", initialise)
    profiler.init(true)
    for i=1,#methods do
        print("Method "..i..": "..methods[i].doc)
    end
    initialise()
end

function draw()
    background(40, 40, 50)
    for i=1, #object do
        object[i]:draw()
    end
    Box.mesh:draw() --just one draw operation with rectangle method
    profiler.draw()
end

function initialise()
    object={}
    Box.mesh:clear()
    collectgarbage()
    for i=1,number do
        object[i]=methods[method]()
    end
end

profiler={}

function profiler.init(monitor)    
    profiler.del=0
    profiler.c=0
    profiler.fps=0
    profiler.mem=0
    if monitor 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

function math.round(number, places) --use -ve places to round to tens, hundreds etc
    local mult = 10^(places or 0)
    return math.floor(number * mult + 0.5) / mult
end
--# Box
Box = class()
Box.doc = "The objects are drawn as rectangles on a single mesh using setRect. No translation used"

local size=20
Box.mesh=mesh()
Box.mesh.texture=readImage("Platformer Art:Block Brick")

function Box:init()
    self.pos = vec2(math.random(WIDTH),math.random(HEIGHT))
    self.angle = math.random()*2*math.pi
    self.vel = vec2(math.random(11)-6,math.random(11)-6)
    self.angleVel=(math.random()-0.5)*.1
    self.col=color(math.random(255), math.random(255), math.random(255))
    self:add(self.pos, self.angle)   
end

function Box:draw() --nb no need for translate or rotate
    self:move()
    self.mesh:setRect(self.rect, self.pos.x, self.pos.y, size, size, self.angle)
end

function Box:move()
    self.pos = self.pos + self.vel
    if self.pos.x > WIDTH + size then self.pos.x = - size
    elseif self.pos.x < - size then self.pos.x = WIDTH + size
    end
    if self.pos.y > HEIGHT + size then self.pos.y = - size
    elseif self.pos.y < - size then self.pos.y = HEIGHT + size
    end
    self.angle = self.angle + self.angleVel
end

function Box:add(pos,ang)
    self.rect=self.mesh:addRect(pos.x,pos.y,size,size,ang)
    self.mesh:setRectTex(self.rect,0,0,1,1)
    self.mesh:setRectColor(self.rect, self.col)
end

--# Tri
Tri = class(Box) --inherits methods from Box
Tri.doc = "The objects are drawn as triangles on a single mesh using setShape. No translation used"

local size=14

function Tri:draw() 
    self:move()
    setTri(self.rect, Box.mesh, self.pos.x, self.pos.y, size, size, self.angle)
end

function Tri:add(pos,ang)
    self.rect=addTri(Box.mesh, pos.x,pos.y,size,size,ang, self.col)
end

--# Pent
Pent = class(Box) --inherits methods from Box
Pent.doc = "The objects are drawn as pentagons on a single mesh using setShape. No translation used"

local size=14

function Pent:draw() 
    self:move()
  setShape(self.rect, Box.mesh, pentagon, self.pos.x, self.pos.y, size, size, self.angle)
end

function Pent:add(pos,ang)
   self.rect=addShape(Box.mesh, pentagon, pos.x,pos.y,size,size,ang, self.col)
end
--# Box2
Box2 = class(Box) --a subclass, just changes add and draw
Box2.doc = "A single mesh with a single rectangle is drawn repeatedly over the screen using translate/ rotate/ setColors to set positions/ angle/ colour"

local size = 20
Box2.mesh=mesh()
Box2.mesh.texture=readImage("Platformer Art:Block Brick")
Box2.mesh:addRect(0,0,size,size) --a single rectangle this time

function Box2:draw()
    self:move()
    pushMatrix()
    translate(self.pos.x,self.pos.y)
    rotate(math.deg(self.angle))
    self.mesh:setColors(self.col)
    self.mesh:draw() --here, self.mesh is actually Box2.mesh
    popMatrix()
end

function Box2:add()
    -- empty function so that we don't inherit the add function from Box
end

--# Box3
Box3 = class(Box)
Box3.doc = "Each object has its own mesh, drawn using translate/ rotate to set positions/ angle."

function Box3:draw() --just one line different (no need to set colour) compared to Box2
    self:move()
    pushMatrix()
    translate(self.pos.x,self.pos.y)
    rotate(math.deg(self.angle))
    self.mesh:draw()
    popMatrix()
end

function Box3:add()
    self.mesh=mesh() --a separate mesh for each instance
    self.mesh.texture=readImage("Platformer Art:Block Brick")
    Box.add(self,vec2(0,0),0) --0 the coordinates for the rect as we're using translate
end

--# AddShape
--ADD SHAPE
--Pack a large number of shapes onto a single mesh. Similar syntax to addRect and setRect.
local triangle={}

--Add tri, set tri

function addTri(m,x,y,w,h,r,col) --mesh, x, y, width, height, [rotation(in radians), color]
    local id=#m.vertices
    m:resize(id+3)
    local col = col or color(255)
    local r = r or 0
    for i=1, 3 do
        local pos = vec2(triangle.vertices[i].x*w, triangle.vertices[i].y*h):rotate(r)
        m:vertex(id+i, pos.x+x, pos.y+y)
        m:texCoord(id+i, triangle.texCoords[i])
        m:color(id+i, col)
    end
    return id --returns shape id number
end

function setTri(id,m,x,y,w,h,r) --shape id number, mesh, x, y, width, height, [rotation(in radians)]
    local r = r or 0
    for i=1, 3 do
        local pos = vec2(triangle.vertices[i].x*w, triangle.vertices[i].y*h):rotate(r)
        m:vertex(id+i, pos.x+x, pos.y+y)
      --  m:texCoords(id+i, sh.texCoords[i]) --make a setTriTexCoords
       -- m:color(id+i, col) --and setTriColor function ....
    end
end

--GENERIC SHAPE FUNCTIONS. nb requires that the shape prototype tables (triangle etc) be exposed as global variables

function addShape(m,sh,x,y,w,h,r,col) --mesh, shapePrototype table, x, y, width, height, [rotation(radians), color]
    local id=#m.vertices
    local n=#sh.vertices
    m:resize(id+n)
    local col = col or color(255)
    local r = r or 0
   -- local w,h = w*0.5, h*0.5
    for i=1, n do
       -- print (sh.vertices[i].x:rotate(1))
        local pos = vec2(sh.vertices[i].x*w, sh.vertices[i].y*h):rotate(r)
        m:vertex(id+i, pos.x+x, pos.y+y)
        m:texCoord(id+i, sh.texCoords[i])
        m:color(id+i, col)
    end
    return id --returns shape id number
end

function setShape(id,m,sh,x,y,w,h,r) --shapeIdNumber, mesh, shapePrototype table, x, y, width, height, [rotation(radians)]
    local n=#sh.vertices
    local r = r or 0
    for i=1, n do
        local pos = vec2(sh.vertices[i].x*w, sh.vertices[i].y*h):rotate(r)
        m:vertex(id+i, pos.x+x, pos.y+y)
      --  m:texCoords(id+i, sh.texCoords[i])
       -- m:color(id+i, col)
    end
end

-- Generate verts, texCoords for triangle

function shapePoints(s)
    local ang = (math.pi*2)/s
    local p={} --unique points
    local t={} --unique texCoords
    for i=1,s do
        p[i]=vec2(math.sin(ang*i), math.cos(ang*i))
        t[i]=(p[i]*0.5)+vec2(0.5,0.5)
    end
    return p,t
end

triangle.vertices, triangle.texCoords = shapePoints(3)

function triangulatePoints(p,t) --takes a set of unique points and texCoords, returns triangulated points and texCoords
    local u={} --table of unique points, indexed by x and y
    for i,v in ipairs(p) do
        local x = math.round(v.x,6) --round the coords to ensure consistency with look-up
        local y = math.round(v.y,6)
        if not u[x] then u[x]={} end
        u[x][y]=t[i] --index texCoords by their rounded x and y
    end
    local verts = triangulate(p)
    local texCoords = {}
    for i,v in ipairs(verts) do
        local x = math.round(v.x,6)
        local y = math.round(v.y,6)
        texCoords[i]=u[x][y] --look up texCoords by their triangulated position
    end
    return verts, texCoords
end

function shapePrototypes()
    pentagon = {}
    pentagon.vertices, pentagon.texCoords = triangulatePoints(shapePoints(5))
end

very impressive

@yojimbo2000 Nice example. One little problem. Your method parameter goes from 1 to 4, but you have 5 methods.

@dave1707 oh yeah, that should have been updated, thanks.

Methods 4 and 5 (multiple draw calls) are clearly the slowest.

But I’m disappointed by the performance of setTri. I thought I’d try to make setTri faster by using a 3x2 matrix instead of calling rotate for every point. But it runs at about the same speed (slightly slower I think).

If anyone can make setTri (method 2) run as fast (or faster) than setRect (method 1), that would be awesome. Maybe I should cheat and add a sin/ cosine lookup table?

Here’s the matrix version:

function setTri(id,m,x,y,w,h,r) --shape id number, mesh, x, y, width, height, [rotation(in radians)]
    local r = r or 0
    local mat = rotMat(x,y,w,h,r)
    for i=1, 3 do     
        --local pos = vec2(triangle.vertices[i].x*w, triangle.vertices[i].y*h):rotate(r)
      --  m:vertex(id+i, pos.x+x, pos.y+y)
        m:vertex(id+i, vecMat(triangle.vertices[i], mat) )
    end
end

function vecMat(vec, mat) --rotate vector by current transform. 
    return vec2(mat[1]*vec.x + mat[4]*vec.y + mat[3], mat[2]*vec.x + mat[5]*vec.y + mat[6])
end

function rotMat(x,y,w,h,r) --returns a 3x2 matrix, rotated by r, scaled by w,h, translated by x,y
    local rx, ry =  math.cos(r), math.sin(r)
    return {rx*w,ry*w,x,
            -ry*h,rx*h,y}
end

[edit: fixed incorrect application of width and height scaling]

It’s ever so slightly faster with a sin / cosine table. I guess I should have a system to cache results (ie given that in this demo each object on the mesh is the same shape and size), rather than recalculating each time. Anyone have any other performance improving ideas?

Edit: ok, one thing that makes a surprisingly big difference is if the vecMat function just returns x and y, rather than wrapping the answers in a vec2.

function vecMat(vec, mat) --rotate vector by current transform. 
    return mat[1]*vec.x + mat[4]*vec.y + mat[3], mat[2]*vec.x + mat[5]*vec.y + mat[6]
end

Fps goes from 52-54 to 59-60 on an iPad Air with a thousand triangles. Didn’t expect that to make such a difference.

I suspect you’ll always struggle to beat the performance of built in functions because they are (I think) written in a faster language than Lua. Also, I wouldn’t go with something like this unless it was significantly faster than the normal methods, because it adds more complexity to the code.

If you are transforming every single vertex in the mesh at every frame, it may be faster to keep a copy of them outside the mesh, in a table, and transform that, and then set the vertices equal to that table, rather than setting individual vertices one at a time within the mesh.

But the killer for me is what happens when the components of your mesh have textures - and more than one in total. A mesh can only have one texture. For me, that makes this exercise impractical (though interesting).

Caching the results makes quite a big difference. SetTri now seems almost as fast as setRect, and even the pentagons are quite fast.


--# Main
-- 2D Mesh Profiling

function setup()
  shapePrototypes()
    methods = {Box, Tri, Pent, Box2, Box3}
    parameter.integer("number",200,5000,1000)
    parameter.integer("method", 1,3,2)
    parameter.action("INITIALISE", initialise)
    parameter.action("Count cache size", traverseCache) --counts size of pentagon cache (check cache is wirking)
    profiler.init(true)
    for i=1,#methods do
        print("Method "..i..": "..methods[i].doc)
    end
    initialise()
end

function draw()
    background(40, 40, 50)
    for i=1, #object do
        object[i]:draw()
    end
    Box.mesh:draw() --just one draw operation with rectangle method
    profiler.draw()
    
end

function initialise()
    object={}
    Box.mesh:clear()
    collectgarbage()
    for i=1,number do
        object[i]=methods[method]()
    end
end

profiler={}

function profiler.init(monitor)    
    profiler.del=0
    profiler.c=0
    profiler.fps=0
    profiler.mem=0
    if monitor 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

function math.round(number, places) --use -ve places to round to tens, hundreds etc
    local mult = 10^(places or 0)
    return math.floor(number * mult + 0.5) / mult
end
--# Box
Box = class()
Box.doc = "The objects are drawn as rectangles on a single mesh using setRect. No translation used"

local size=20
Box.mesh=mesh()
Box.mesh.texture=readImage("Platformer Art:Block Brick")

function Box:init()
    self.pos = vec2(math.random(WIDTH),math.random(HEIGHT))
    self.angle = math.random()*360
    self.vel = vec2(math.random(11)-6,math.random(11)-6)
    self.angleVel=(math.random()-0.5)
    self.col=color(math.random(255), math.random(255), math.random(255))
    self:add(self.pos, self.angle)   
end

function Box:draw() --nb no need for translate or rotate
    self:move()
    self.mesh:setRect(self.rect, self.pos.x, self.pos.y, size, size, math.rad(self.angle))
end

function Box:move()
    self.pos = self.pos + self.vel
    if self.pos.x > WIDTH + size then self.pos.x = - size
    elseif self.pos.x < - size then self.pos.x = WIDTH + size
    end
    if self.pos.y > HEIGHT + size then self.pos.y = - size
    elseif self.pos.y < - size then self.pos.y = HEIGHT + size
    end
    self.angle = self.angle + self.angleVel
end

function Box:add(pos,ang)
    self.rect=self.mesh:addRect(pos.x,pos.y,size,size,math.rad(ang))
    self.mesh:setRectTex(self.rect,0,0,1,1)
    self.mesh:setRectColor(self.rect, self.col)
end

--# Tri
Tri = class(Box) --inherits methods from Box
Tri.doc = "The objects are drawn as triangles on a single mesh using setTri. No translation used"

local size=10
local size2=20

function Tri:draw() 
    self:move()
    setTri(self.rect, Box.mesh, self.pos.x, self.pos.y, size, size2, self.angle)
end

function Tri:add(pos,ang)
    self.rect=addTri(Box.mesh, pos.x,pos.y,size,size2,ang, self.col)
end

--# Pent
Pent = class(Box) --inherits methods from Box
Pent.doc = "The objects are drawn as pentagons on a single mesh using setShape. No translation used"

local size=14

function Pent:draw() 
    self:move()
  setShape(self.rect, Box.mesh, pentagon, self.pos.x, self.pos.y, size, size, self.angle)
end

function Pent:add(pos,ang)
   self.rect=addShape(Box.mesh, pentagon, pos.x,pos.y,size,size,ang, self.col)
end
 
--# AddShape
--ADD SHAPE
--Pack a large number of shapes onto a single mesh. Similar syntax to addRect and setRect.
local triangle={}
triangle.cache = {} --caches same shaped/sized triangles. 

--Add tri, set tri (slightly redundant, see shape commands below)

function addTri(m,x,y,w,h,r,col) --mesh, x, y, width, height, [rotation(in radians), color]
    local id=#m.vertices
    m:resize(id+3)
    local col = col or color(255)
    local d = math.ceil((r%360)*2) --cached in half-degree increments
    if triangle.cache[d] then --if cached
        for i=1,3 do
            m:vertex(id+i, triangle.cache[d][i].x+x, triangle.cache[d][i].y+y) --use cache
            m:texCoord(id+i, triangle.texCoords[i])
            m:color(id+i, col)
        end
    else
        local mat = rotMat(w,h,r) --generate a matrix for this transform
        triangle.cache[d]={}
        for i=1, 3 do
            --  local pos = vec2(triangle.vertices[i].x*w, triangle.vertices[i].y*h) --:rotate(r)
            --  m:vertex(id+i, pos.x+x, pos.y+y)
            local rx, ry = vecMat(triangle.vertices[i], mat)
            triangle.cache[d][i]={x=rx, y=ry} --cache the rotation
            m:vertex(id+i, rx+x, ry+y)
            m:texCoord(id+i, triangle.texCoords[i])
            m:color(id+i, col)
        end
    end
    return id --returns shape id number
end

function setTri(id,m,x,y,w,h,r) --shape id number, mesh, x, y, width, height, [rotation(in radians)]
    local d = math.ceil((r%360)*2)
    if triangle.cache[d] then
        for i=1, 3 do
            m:vertex(id+i, triangle.cache[d][i].x+x, triangle.cache[d][i].y+y)
        end
    else
        local mat = rotMat(w,h,r)
        triangle.cache[d]={}
        for i=1, 3 do
            --local pos = vec2(triangle.vertices[i].x*w, triangle.vertices[i].y*h):rotate(r)
            --  m:vertex(id+i, pos.x+x, pos.y+y)
            local rx, ry = vecMat(triangle.vertices[i], mat)
            m:vertex(id+i, rx+x, ry+y)
            triangle.cache[d][i]={x=rx, y=ry}
        end
    end
end

function vecMat(vec, mat) --rotate vector by current transform. 
    return mat[1]*vec.x + mat[3]*vec.y, mat[2]*vec.x + mat[4]*vec.y
end

function rotMat(w,h,r) --returns a 3x2 matrix, rotated by r, scaled by w,h
    local d = math.rad(r)
    local rx, ry =  math.cos(d), math.sin(d) --cosLUT[r], sinLUT[r] 
    return {rx*w,ry*w,
            -ry*h,rx*h}
end

--GENERIC SHAPE FUNCTIONS. nb requires that the shape prototype tables (triangle etc) be exposed as global variables

function addShape(m,sh,x,y,w,h,r,col) --mesh, shapePrototype table, x, y, width, height, [rotation(radians), color]
    local id=#m.vertices
    local n=#sh.vertices
    m:resize(id+n)
    local col = col or color(255)
    local d = math.ceil((r%360)*2)
    if sh.cache[d] then
        for i=1, n do
            m:vertex(id+i, sh.cache[d][i].x+x, sh.cache[d][i].y+y)
            m:texCoord(id+i, sh.texCoords[i])
            m:color(id+i, col)
        end
    else
        local mat = rotMat(w,h,r)
        sh.cache[d]={}
 
        for i=1, n do       
            -- local pos = vec2(sh.vertices[i].x*w, sh.vertices[i].y*h):rotate(r)
            local rx, ry = vecMat(sh.vertices[i], mat)
            m:vertex(id+i, rx+x, ry+y)
            sh.cache[d][i]={x=rx, y=ry}
            --  m:vertex(id+i, vecMat(sh.vertices[i], mat))
            m:texCoord(id+i, sh.texCoords[i])
            m:color(id+i, col)
        end
    end
    return id --returns shape id number
end

function setShape(id,m,sh,x,y,w,h,r) --shapeIdNumber, mesh, shapePrototype table, x, y, width, height, [rotation(radians)]
    local n=#sh.vertices
    local d = math.ceil((r%360)*2)
    if sh.cache[d] then
        for i=1, n do
            m:vertex(id+i, sh.cache[d][i].x+x, sh.cache[d][i].y+y)
        end
    else
        local mat = rotMat(w,h,r)
        sh.cache[d]={}
        for i=1, n do
            -- local pos = vec2(sh.vertices[i].x*w, sh.vertices[i].y*h):rotate(r)
            --  m:vertex(id+i, pos.x+x, pos.y+y)
            local rx, ry = vecMat(sh.vertices[i], mat)
            m:vertex(id+i, rx+x, ry+y)
            sh.cache[d][i]={x=rx, y=ry}
            --  m:texCoords(id+i, sh.texCoords[i])
            -- m:color(id+i, col)
        end
    end
end

-- Generate verts, texCoords for triangle

function shapePoints(s)
    local ang = (math.pi*2)/s
    local p={} --unique points
    local t={} --unique texCoords
    for i=1,s do
        p[i]=vec2(math.sin(ang*i), math.cos(ang*i))
        t[i]=(p[i]*0.5)+vec2(0.5,0.5)
    end
    return p,t
end

triangle.vertices, triangle.texCoords = shapePoints(3)

function triangulatePoints(p,t) --takes a set of unique points and texCoords, returns triangulated points and texCoords
    local u={} --table of unique points, indexed by x and y
    for i,v in ipairs(p) do
        local x = math.round(v.x,6) --round the coords to ensure consistency with look-up
        local y = math.round(v.y,6)
        if not u[x] then u[x]={} end
        u[x][y]=t[i] --index texCoords by their rounded x and y
    end
    local verts = triangulate(p)
    local texCoords = {}
    for i,v in ipairs(verts) do
        local x = math.round(v.x,6)
        local y = math.round(v.y,6)
        texCoords[i]=u[x][y] --look up texCoords by their triangulated position
    end
    return verts, texCoords
end

function shapePrototypes()
    pentagon = {}
    pentagon.vertices, pentagon.texCoords = triangulatePoints(shapePoints(5))
    pentagon.cache = {}
end

function traverseCache()
    local count = 0
    for k,_ in pairs(pentagon.cache) do
        count = count + 1
    end
    print (count)
end

@Ignatz that’s what texture atlases (aka sprite sheets) are for. Packing as many objects as you can onto one mesh is definitely the key to throwing lots of objects around the screen.

Edit: but yes, Codea’s Mesh API is C, and it’s hard to beat (math.sin and .cos are hard to beat with a lookup table too, on an Air anyway. I’m glad in a way. LUTs should go back to the 8 and 16-bit era where they belong!)

That’s also not a bad idea about setting the entire vertex table in one go, rather than individual ones (if everything on the screen is moving).

Part of the reason why I want to explore this, is as an alternative to setRect when you’re using primitives in a 3D environment, ie billboarding or whatever. Even if you follow Apple’s advice to draw according to opacity (opaque first, then objects with discard layers, then objects with alpha transparency), OpenGL can still occasionally mess-up the transparency layer, with results like this (I put a red circle round it):

rect transparency

(detail from an early build of my 3D blaster)

What’s happening above is that the alpha transparency layer cuts all the way through to the back-most layer, so that they rectangle shape around the bullet mesh is very visible.

Now you can solve this with a discard shader. But discard shaders, in my experience, can produce strange side-effects (other meshes not appearing as they should), and are not recommended by the Apple guidelines linked to above.

So I’m interested in the possibility of using a shape other than a rect, one that adheres more closely to the outline of the texture image, as the Apple docs suggest:

apple advice

@Ignatz sorry about that, I changed the link, does the image display now?

I don’t see the image you’ve labelled rect transparency.

i have found that billboards with transparency work ok, in a number of projects. I did have a strange problem (not specifically related to transparency) recently when I used the sprite command to draw billboards in a 3D scene, and I got visual artifacts popping up in certain parts of the scene nearby. Putting the sprites into a little mesh solved the problem.

Are you certain your sprites have fully transparent pixels? Do you have a (simple) example you can share of transparency not working properly?

Discard shaders can slow drawing down quite a lot, but maybe you could use them just for billboards with transparency.

I admit it’s not a great screenshot! They’re fast moving objects, so it’s hard to capture. The plane that the rects are on goes in and out of the screen, the same plane that the ship is in, if that makes it easier to see (ie it’s not billboarding in this case, in that the rect is not perpendicular to the viewport). It happens when bullets are in front of / going through explosions (which also have transparency).

Tbh I’m probably the only one that notices it, and I’ll leave it for now rather than drawing the bullets with triangles.

It think it’s just that it reminds me of the masking squares that used to be visible around the ships in the original battlestar galactica TV show. I just remember as a kid thinking those squares ruined the effect and made the whole thing look really amateurish!

I can’t see the screenshot at all, just a little box labelled “rect transparency”. Can you check your link works?

If your bullets are overlaid directly on the same positions as explosions, the transparency could fail, I’m sure.

I can see it now. It looks as though the bullet has been drawn in the wrong order, and if it passes “through” enemies (or whatever the black cloud is), the drawing sequence needs to change depending on whether the bullet is in front of or behind the other objects. But I guess you’re sorting by distance every frame to figure out drawing order?

@yojimbo2000 I thought I’d try drawing on a single mesh and see what speed I could get. Here’s my code that I tried to make as similar to your triangle example as I could. I have similar triangles sizes, texture, color, rotation, and x,y movement like yours. I compared your FPS at 5000 triangles to mine. On my iPad Air, when I run your triangles at 5000, the highest FPS I see is 24. When I run mine at 5000, the highest FPS I see is 31. One thing I don’t understand is when I select 5000 on yours and press INITIALIZE, it takes 34 seconds before it shows the 5000 triangles. On mine, it’s almost instant. Is there something that I’m missing in my triangle example that you’re doing in yours.

EDIT: I wonder if some of the speed up routines you added to yours could speed mine up some. I’m not exactly sure what you added.

EDIT: Updated FPS to 24 and 31.

EDIT: I increased my triangle count to 10,000 just for kicks and had a FPS of 15 and 7 FPS at 20,000 triangles.


function setup()
    parameter.integer("number",200,5000,1000)
    parameter.action("init",calc)
    m=mesh()
    m.texture=readImage("Platformer Art:Block Brick")
    pos()
    calc()
end

function pos()
    local sin=math.sin
    local cos=math.cos
    local rad=math.rad
    newPos={}
    for a=0,359 do
        local x1=cos(math.rad(a))*14
        local y1=sin(math.rad(a))*14        
        local x2=cos(math.rad(a+120))*14
        local y2=sin(math.rad(a+120))*14
        local x3=cos(math.rad(a+240))*28
        local y3=sin(math.rad(a+240))*28
        newPos[a]={x1=x1,y1=y1,x2=x2,y2=y2,x3=x3,y3=y3}
    end
end

function calc()
    tab={}
    tc={}
    fps,dc,dt=0,0,0
    size=number
    for z=1,size do
        x=math.random(WIDTH)
        y=math.random(HEIGHT)
        table.insert(tab,{x=x,y=y,ang=0,angVel=math.random(-4,4),
                xVel=math.random(-5,5),yVel=math.random(-5,5),
                col=color(math.random(255),math.random(255),math.random(255))})
        table.insert(tc,vec2(0,0))
        table.insert(tc,vec2(1,0))
        table.insert(tc,vec2(0,1))
    end
    m.texCoords=tc
    create()
    m:setColors(255,255,255)
    for z=1,size*3 do
        m:color(z,tab[math.ceil(z/3)].col)
    end
end

function draw()
    background(0, 251, 255, 255)
    parameter.watch("fps")
    parameter.watch("memory")
    create()
    m:draw() 
    dt=dt+DeltaTime
    dc=dc+1
    if dc==10 then
        fps=dc/dt
        dc,dt=0,0
        memory=collectgarbage("count")
    end
end

function create()
    local tab1={}
    for z=1,size do
        local t2=tab[z]
        t2.x=(t2.x+t2.xVel)%WIDTH
        t2.y=(t2.y+t2.yVel)%HEIGHT
        t2.ang=(t2.ang+t2.angVel)%360
        local p=#tab1+1
        tab1[p]=vec2(newPos[t2.ang].x1+t2.x,newPos[t2.ang].y1+t2.y)    
        tab1[p+1]=vec2(newPos[t2.ang].x2+t2.x,newPos[t2.ang].y2+t2.y)
        tab1[p+2]=vec2(newPos[t2.ang].x3+t2.x,newPos[t2.ang].y3+t2.y)
    end
    m.vertices=tab1
end

@Dave1707 yeah, my add functions (addTri) add the shapes one at a time by resizing the mesh buffer, i.e. they’re designed for adding shapes on the fly, rather than all in one go. Your approach, of adding them all in one go by setting the entire vertices table is going to be much faster. In fact, even if you do want to have a variable number of shapes, it’s probably best to precreate them all at once, and then just set the extra triangles to width, height 0,0 until they’re needed (assuming you’re using this for some intensive task like a particle system).

The question is whether your method of setting the entire vertex table every frame is faster than what I’ve been doing in setTri of just setting individual vertices. Your testing suggests it is (if everything is moving every frame). I’m away from my iPad just now, but I’ll try your code when I get back.

@Ignatz regarding sorting by depth, beyond simple things like drawing the skydome first, I don’t find it to be practical or useful to sort hundreds of objects by depth, as OpenGL does such a good job of this anyway (and in this case, the bullets are all on one mesh, as that is by far the fastest way of having hundreds of objects flying around). I do sort by opacity though (as Apple recommends), so it’s a clash between two part-transparent objects. Here, I draw bullets first, then explosions, so that’s why the rectangle is visible. But if I draw explosions first, although the bullets on the nearside of the explosion draw correctly, the bullets on the far side of the explosion get clipped. I decided it was more important from a gameplay point of view to be able to see bullets coming towards you through the explosions, than to have the bullets rendering 100% correctly. But I might try putting the bullets on to triangles at some stage…

That’s your answer, then. There is no glitch or error in OpenGL, just incorrect ordering. If you want transparent pixels to behave, then you need to sort by depth - or use a discard shader with the bullets. (Or discard the bullets once they get to the target, so they never get behind the explosion - after all, in real life, bullets generally disintegrate on hitting a target).

I think you will find, as I suggested near the top, and as Dave has done, that when you are resetting all vertex positions every frame, it is fastest to do this outside of the mesh, using a table, and then set the mesh vertices equal to that table.

@dave1707, @yojimbo2000 -

I worked on dave’s code, which started out running at 38-40 for me, in my iPad3.

I got it up to 48, about 20% faster, by

  • using a permanent table tab1 rather than a temporary one

  • prefilling it with 0’s and then changing them each draw (rather than creating new vec2’s)

  • not looking up any table more than once, which means using lots of local variables

Here are my changes

--in setup (before calling calc)
tab1={} for i=1,number*3 do tab1[i]=vec2(0,0) end

function create()
    for z=1,size do
        local t2=tab[z]
        t2.x=(t2.x+t2.xVel)%WIDTH
        t2.y=(t2.y+t2.yVel)%HEIGHT
        t2.ang=(t2.ang+t2.angVel)%360  
        local p=z*3-2
        local n=newPos[t2.ang] 
        local t=tab1[p] t.x,t.y=n.x1+t2.x,n.y1+t2.y
        p=p+1 t=tab1[p] t.x,t.y=n.x2+t2.x,n.y3+t2.y
        p=p+1 t=tab1[p] t.x,t.y=n.x3+t2.x,n.y3+t2.y
    end
    m.vertices=tab1
end

I also tried storing vec2’s in newPos, and then adding a vec2 with the new triangle position. This was slow. It was also slow if you took the adjusted tab1 vector and added a vec2 with the new triangle position. Basically, working with vectors is slow.

@Ignatz I tried your code changes and it did increase my FPS. At 5000 triangles I reached a high FPS of 44. I put the tab1 pre-fill code in the calc() function just before create(). With it in setup(), the code was giving an error when I changed the number of triangles because it wasn’t recalculating a new tab1 size. I also changed the first y3 in the function create() to y2. That was a typo because the triangles we’re going flat as they rotated. It’s fun trying to change code to make it run faster. That gives you an idea of how to write code in other programs to make it run as fast as possible. One thing I noticed when running the code and switching to 5000 triangles. It would run at about 51-53 FPS for a while and then settle down to 41-44 FPS after about 20 seconds. I noticed that even before your changes but at a different FPS, so I’m not sure what Codea is doing. To get my running FPS, I would close all the apps, even Codea, and then open Codea and start the code. I would let it run for about 10 seconds and do a restart. I would let it run for about 30 seconds and then watch for the highest FPS. That way I knew everything should be similar as I was trying different code changes.