2
\$\begingroup\$

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:

Screenshot

Download (pan with WASD, rotate with Q/E, zoom with 1/2)

\$\endgroup\$
  • \$\begingroup\$ What is the question? \$\endgroup\$ – wolfdawn Apr 25 '14 at 9:57
  • \$\begingroup\$ 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. \$\endgroup\$ – Johannes Apr 25 '14 at 10:35
  • \$\begingroup\$ 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. \$\endgroup\$ – Ben Apr 26 '14 at 14:22
  • 2
    \$\begingroup\$ 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. \$\endgroup\$ – NauticalMile May 18 '14 at 1:13
  • \$\begingroup\$ @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 \$\endgroup\$ – Johannes May 19 '14 at 11:51
1
\$\begingroup\$

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.

You can download an updated version of my original project here.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.