Matrix scale, rotation and translation with repsect to 2D "camera"

I posted this question in the GAMBAS user mailing list, archived here: http://old.nabble.com/gb3%3A-OpenGL-Rotate-and-Translate-logic-td33886871.html I’ll completely rewrite it here out of respect (link provided for reference).

I have a top-down game where all objects, including the camera, are described by (x,y) coordinates. The camera is always at the center of the render window, though the camera’s coordinates are arbitrary (Camera.WorldX, Camera.WorldY). The camera has an orientation (Camera.Orientation), which rotates the game world much like this video (Contra III Stage 2, SNES):

I’d like to set up the matrix transformations in advance (zoom, translation, rotation), so that all subsequent rendering operations can use objects’ native world coordinates and appear in the correct positions (camera in center of the screen, world rotated by the camera’s orientation). I’d like the matrix transformations to do the work so I can just use the raw game object coordinates, basically.

My code works fine with no rotation:

ScreenOffsetX = sWidth / 2 / 128 / Zoom.Current
ScreenOffsetY = sHeight / 2 / 128 / Zoom.Current
Gl.LoadIdentity()
Gl.Scalef(128 * Zoom.Current, 128 * Zoom.Current, 1)
Gl.Translatef(- Camera.WorldX + ScreenOffsetX, - Camera.WorldY + ScreenOffsetY, 0)

http://eightvirtues.com/sanctimonia/misc/Everything%20But%20Rotation.ogv

I need to combine what works in the video above with rotation around the center of the screen (camera as origin), but have been unsuccessful. I’ve been working on this for days and am starting to go crazy. I’ve researched 'til my eyes have bled and used trial and error for hours on end.

As mentioned in the GAMBAS mailing list, I have functions to rotate an arbitrary point about an arbitrary origin, and I suspect that I may need to use these functions to translate the rotated matrix along its pre-rotated (x,y) path so that the “camera” is centered on the render window post-rotation. Any help would be appreciated. I’m dying here.

What it sounds like you’re trying to re-invent is the “viewing transform”. To define this transform, you need a camera position, a camera “forward vector”, a camera “up vector”. See gluLookAt(), though it’s very simple to roll your own.

Your rotation is simply rotating the “up vector” around the “forward vector”. In this simple top-down zoom, you can probably just use sin() and cos() to compute the up vector. Nothing fancy.

Interesting. I had avoided trying GluLookAt() because like a fool I assumed that it would just “point” at a coordinate and not also translate the scene. I now have this code, which does what I was doing before but using GluLookAt():

ScreenOffsetX = sWidth / 2 / 128 / Zoom.Current
ScreenOffsetY = sHeight / 2 / 128 / Zoom.Current
Gl.LoadIdentity()
Gl.Scalef(128 * Zoom.Current, 128 * Zoom.Current, 1)
Glu.LookAt(Camera.WorldX + ScreenOffsetX, Camera.WorldY + ScreenOffsetY, 1, Camera.WorldX + ScreenOffsetX, Camera.WorldY + ScreenOffsetY, 0, 0, -1, 0)

I’ve since been doing research on what exactly the “up vector” is, and haven’t found a simple explanation. I’m guessing it means what direction is up for the camera, which is kind of wonky because two of the three orientation values for the camera should be calculated from the “eye” and “center” parameters of the GluLookAt() procedure. If all that’s true then contradictory values could be specified in the procedure parameters (thus the alleged wonkiness). Any light you could shed on that [mis]analysis might point me in the right direction, but basically at this point I’m not sure how to rotate the camera.

Is the forward vector Camera.Orientation? If so I’ll research how to rotate one vector around another and try the resulting values. Math isn’t my strong suit, but I more than make up for it in other areas of game design thankfully. Great help so far. :slight_smile:

Exactly. What you need to define the VIEWING transform (or any rigid transform really) is a frame of reference – that is: 1) an origin point, and 2) an orthonormal basis (i.e. 3 mutually perpendicular unit vectors). In this case, the origin point is the camera position. For the “forward” vector, that’s the vector from the camera point to the center point, normalized. Now we need one vector perpendicular to this vector to give us a 2nd orthonormal vector for our basis and we can cross those two to get a third. So the algorithm is to cross the “forward” vector with the “up” vector passed in to get a “right” vector, and then cross the right and the forward vectors to get a “true” up vector.

So the “up” vector that you pass in is just a vector that’s “generally” in the up direction, but not necessarily the “true” up vector (i.e. one perpendicular to the “forward” vector. The gluLookAt internal math takes care of ensuring that.

It’s funny; when I first read that it was straight Chinese, but after working on my game for a while and re-reading it I actually mostly understand you. That will come in handy when I eventually “tilt” the camera a bit so it’s not strictly “top-down”, as if I understand you correctly it will require that I properly calculate the up vector at something other than right angles. Still weirds me out that the Glu.LookAt() procedure asks for values which can prove contradictory to what it’s helping calculate, but I’ll chalk that up to divide by zero syndrome.

I was unable to use the screen center as the origin using orthogonal projection with Glu.LookAt(), but I got it working perfectly in perspective mode and am ecstatic. I even created two procedures for switching between orthogonal and perspective modes so I can render pixel-precise OSD’s as necessary.

My issue now is that I need to render landscape objects using the base matrix provided by Glu.LookAt(), but oriented at right angles to the screen. Sorta like billboards in that the quad position follows the matrix but the quad orientation and “offset” do not. Here’s a visualization I put together to demonstrate what I’m trying to do:

http://eightvirtues.com/sanctimonia/misc/Problem.jpg

I experimented quite a bit but for every problem I solved nothing else worked. I’ll spare you the frustrating details unless you need them.

Got it to work by manually offsetting and rotating the vertices rather than fudging around with matrices:

Public Sub Texture3D(Group As String, Optional ID As Short, Optional SubID As Byte, WorldX As Single, WorldY As Single, WorldZ As Single, Width As Single, Height As Single, VerticalOffset As Single, Rotate As Boolean)

  ' Render textured quad.

  ' General declarations.
  Dim TopLeftX As Single
  Dim TopLeftY As Single
  Dim TopRightX As Single
  Dim TopRightY As Single
  Dim BottomRightX As Single
  Dim BottomRightY As Single
  Dim BottomLeftX As Single
  Dim BottomLeftY As Single
  Dim OffsetX As Single
  Dim OffsetY As Single

  ' Convert dimensions from pixels to feet and halve for pre-centering efficiency.
  Width = Width / 128 / 2
  Height = Height / 128 / 2

  ' Convert elevation from inches to feet.
  WorldZ = WorldZ / 12 * ElevationScale

  ' Select texture using its group, ID and SubID.
  Select Case Group
    Case "PWOs"
      Gl.BindTexture(Gl.TEXTURE_2D, tiPWO[ID].ID[SubID])
    Case "shadows"
      Gl.BindTexture(Gl.TEXTURE_2D, tiShadow[ID].ID[0])
    Case "wisps"
      Gl.BindTexture(Gl.TEXTURE_2D, tiWisp[ID].ID[0])
    Case "players"
      Gl.BindTexture(Gl.TEXTURE_2D, tiPlayer[ID].ID[SubID])
    Case "portrait"
      Gl.BindTexture(Gl.TEXTURE_2D, tiPortrait[ID].ID[SubID])
    Case "water"
      Gl.BindTexture(Gl.TEXTURE_2D, tiWater[0].ID[0])
    Case "gear"
      Gl.BindTexture(Gl.TEXTURE_2D, tiGear[0].ID[ID])
    Case "cursor"
      Gl.BindTexture(Gl.TEXTURE_2D, tiCursor[0].ID[ID])
  End Select

  ' Calculate vertex coordinates.
  If Rotate = False Then
    ' Calculate offsets for vertical offset.
    VerticalOffset = VerticalOffset / 128
    OffsetX = Convert.RotateX(0, VerticalOffset - Height, 0, 0, Camera.Orientation)
    OffsetY = Convert.RotateY(0, VerticalOffset - Height, 0, 0, Camera.Orientation)
    TopLeftX = Convert.RotateX(WorldX - Width, WorldY - Height, WorldX, WorldY, Camera.Orientation) + OffsetX
    TopLeftY = Convert.RotateY(WorldX - Width, WorldY - Height, WorldX, WorldY, Camera.Orientation) + OffsetY
    TopRightX = Convert.RotateX(WorldX + Width, WorldY - Height, WorldX, WorldY, Camera.Orientation) + OffsetX
    TopRightY = Convert.RotateY(WorldX + Width, WorldY - Height, WorldX, WorldY, Camera.Orientation) + OffsetY
    BottomRightX = Convert.RotateX(WorldX + Width, WorldY + Height, WorldX, WorldY, Camera.Orientation) + OffsetX
    BottomRightY = Convert.RotateY(WorldX + Width, WorldY + Height, WorldX, WorldY, Camera.Orientation) + OffsetY
    BottomLeftX = Convert.RotateX(WorldX - Width, WorldY + Height, WorldX, WorldY, Camera.Orientation) + OffsetX
    BottomLeftY = Convert.RotateY(WorldX - Width, WorldY + Height, WorldX, WorldY, Camera.Orientation) + OffsetY
  Else
    TopLeftX = WorldX - Width
    TopLeftY = WorldY - Height
    TopRightX = WorldX + Width
    TopRightY = WorldY - Height
    BottomRightX = WorldX + Width
    BottomRightY = WorldY + Height
    BottomLeftX = WorldX - Width
    BottomLeftY = WorldY + Height
  Endif

  ' Create the quad the texture is drawn on.
  Gl.Begin(Gl.QUADS)
    ' Top-left vertex.
    Gl.TexCoord2f(0, 0)
    Gl.Vertex3f(TopLeftX, TopLeftY, WorldZ)
    ' Top-right vertex.
    Gl.TexCoord2f(1, 0)
    Gl.Vertex3f(TopRightX, TopRightY, WorldZ)
    ' Bottom-right vertex.
    Gl.TexCoord2f(1, 1)
    Gl.Vertex3f(BottomRightX, BottomRightY, WorldZ)
    ' Bottom-left vertex.
    Gl.TexCoord2f(0, 1)
    Gl.Vertex3f(BottomLeftX, BottomLeftY, WorldZ)
  Gl.End()

End

Here are the Convert.RotateX and Convert.RotateY functions so the main procedure is more complete:

Public Function RotateX(PWOX As Single, PWOY As Single, OriginX As Single, OriginY As Single, Orientation As Single) As Single

  ' Rotate specified point about specified point and return new X coordinate.

  Return (OriginX + (Cos(Rad(Orientation)) * (PWOX - OriginX) - Sin(Rad(Orientation)) * (PWOY - OriginY)))

End

Public Function RotateY(PWOX As Single, PWOY As Single, OriginX As Single, OriginY As Single, Orientation As Single) As Single

  ' Rotate specified point about specified point and return new Y coordinate.

  Return (OriginY + (Sin(Rad(Orientation)) * (PWOX - OriginX) + Cos(Rad(Orientation)) * (PWOY - OriginY)))

End

The only part of the code you need to modify is the texture binding’s Texture parameter.