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