Question

How do you implement a camera with pan/zoom/rotation for a 2d game? Is it acceptable to multiply together a series of transformation matrixes to generate a final transformation matrix that is applied to the vectors/vertices in a scene, or is there a more efficient way?

Overview

Working on a 2d game in löve2d I recently tried implementing a camera class that allows for panning/rotation/scrolling.

I tried finding a good resource for how to implement such a 2d camera, but was unable to find one that covered all three degrees of movement, and so I came up with my own solution.

I would like some feedback on if this is a good approach, and how it could be optimized/improved upon.

My Attempt

My camera object stores the following values:

1. position: (Vector2)(World-Space position relative to the origin)
2. angle: (float)(how much the world is rotated, in radians)
3. zoom: (float)(the zoom factor, 1 = normal, 0.5 = 2x zoom, etc)
4. offset_center: (Vector2)(center the camera on the point of rotation using [screen_width/2,screen_height/2])
5. offset: (Vector2)(any additional offset away from the center of rotation, typically just [0,0]

Whenever a camera value changes (basically each frame) I run camera:computeMatrix() which calculates its transformation matrix:

Camera.computeMatrix = function (self)

local m_pos = Matrix{ -- World-space Position
{1, 0, self.position.x},
{0, 1, self.position.y},
{0, 0, 1}
}

local m_zoom = Matrix{ -- Zoom
{self.zoom, 0, 0},
{0, self.zoom, 0, 0},
{0, 0, 1}
}

local m_rot = Matrix{ -- Rotation
{math.cos(self.angle), math.sin(self.angle), 0},
{-math.sin(self.angle), math.cos(self.angle), 0},
{0, 0, 1}
}

local m_offset = Matrix{ -- Screen-space Offset
{1, 0, self.offset_center.x + self.offset.x},
{0, 1, self.offset_center.y + self.offset.y},
{0, 0, 1}
}

self.matrix = nil
-- reverse order than when applying them 1 by 1
self.matrix =  m_offset * m_rot * m_zoom * m_pos
end


So once Camera.matrix is calculated as a 3x3 matrix, I apply that to a given vertex/vector:

Vector2.TransformM = function (v, m)
-- m is a 3x3 matrix of format {{a,b,c},{d,e,f},{g,h,i}}
return Vector2(v.x*m[1][1]+v.y*m[1][2]+m[1][2], v.x*m[2][2]+v.y*m[2][2]+m[2][3])
end


An example of this implementation:

• What is the question? Commented Apr 25, 2014 at 9:57
• Sorry, I should probably clarify that... Basically I'd like to know how you would handle a 2d camera with pan/rotation/zoom in a 2d game. It seems to me that the way I'm generating the transformation matrix might not be ideal. Commented Apr 25, 2014 at 10:35
• You could do it this way. Your interface deals with vectors and scalars instead of matrices. This is probably more natural for a 2D game. You could have the interface use more matrices instead. This way you just multiply your existing transform by a new matrix, instead of changing position vectors and scale floats. This probably belongs on Code Review more, though.
– Ben
Commented Apr 26, 2014 at 14:22
• Have you looked at hump.camera? If you just want the functionality then you could use that (it's simple to use and it's working for me). If you want to write your own camera, take a look at the implementation for ideas. Commented May 18, 2014 at 1:13
• @NauticalMile Yep, I actually stumbled upon this myself looking at the love.graphics coordinate system methods, which it turns out is what hump.camera seems to be using. The result runs about 3-5 times faster than my hardcoded transformation matrixes :) copy.com/uTmUCRkIhrQeqr2g Commented May 19, 2014 at 11:51

Sorry for late answer - I originally jumped on the love IRC where I was pointed towards love.graphics's Coordinate Functions which allows you to modify love's coordinate system to do exactly what I wanted. Credit also to NauticaMile for suggesting hump.camera, which uses this as the underlying implementation.

This example replaces my original hardcoded transformation matrices with the relevant love.graphics functions. Using this my original program ended up being 3-5 times faster, but it could certainly be more optimized:

-- CANVAS METHODS --
Camera.attach = function (self)
local cx, cy = love.graphics.getWidth()/(2*self.scale), love.graphics.getHeight()/(2*self.scale)
love.graphics.push()
love.graphics.scale(self.scale)
love.graphics.translate(cx, cy)
love.graphics.rotate(self.rot)
love.graphics.translate(-self.pos.x, -self.pos.y)
return self
end
Camera.detatch = function (self)
love.graphics.pop()
return self
end
Camera.draw = function (self, func)
self:attach()
func()
self:detach()
end


To use it you can do something like Camera:draw(World:draw()) in your love.draw call, where World:draw() could be any function that draws something to the screen.