Howdy, Stranger!

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

Batch drawing shapes on one mesh

in Questions Posts: 2,020

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

Comments

  • Jmv38Jmv38 Mod
    Posts: 3,295

    very impressive

  • dave1707dave1707 Mod
    Posts: 7,472

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

  • edited May 2015 Posts: 2,020

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

  • edited May 2015 Posts: 2,020

    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.

  • IgnatzIgnatz Mod
    Posts: 5,396

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

  • edited May 2015 Posts: 2,020

    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
  • edited May 2015 Posts: 2,020

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

  • edited May 2015 Posts: 2,020

    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

  • IgnatzIgnatz Mod
    Posts: 5,396

    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.

  • Posts: 2,020

    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!

  • IgnatzIgnatz Mod
    Posts: 5,396

    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.

  • Posts: 2,020

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

  • IgnatzIgnatz Mod
    Posts: 5,396

    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?

  • dave1707dave1707 Mod
    edited May 2015 Posts: 7,472

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

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

  • IgnatzIgnatz Mod
    Posts: 5,396

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

  • IgnatzIgnatz Mod
    Posts: 5,396

    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.

  • IgnatzIgnatz Mod
    edited May 2015 Posts: 5,396

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

  • dave1707dave1707 Mod
    Posts: 7,472

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

Sign In or Register to comment.