Reputation: 770
I'm currently working on a quirky retro flight sim and I've run into a few problems with my 3d projects, in so much as I can't find any solid generic documentation on the subject.
How do I convert simple positional vectors in my game world into 2d vectors on screen given the following information:
The cameras position a cameras orientation ( see below) a field of view a height and width of the screen (and aspect ratio)
I'm also looking for a way to store orientations, I've already written a basic vector library but I'm unsure as to how to store rotations for use in both the camera (and projection code) as well as actual handling of rotations of in-game objects. I'm currently looking at using quaternions but it is it possible (and easy) to use quaternions instead of matrices for projection transformations?
Are there any good sources on implementation quaternions in code? Will I have to write a seperate library for complex numbers?
Thank you for your time and any help :)
Upvotes: 2
Views: 2277
Reputation: 129
CAUTION: LONG ANSWER!
I've done a similar project in Love2D, and it runs perfectly fast, so I don't see the problem with doing the math yourself in Lua rather than using OpenGL (which isn't exposed anyway).
Contrary to the comments, you should not be discouraged. The math behind 3D orientation and perspective is in fact quite simple, once you get the feel for it.
For orientation, quaternions are probably overkill. I've found that to do 3D projection with rotation, only Vec2
, Vec3
, and Camera
classes are needed. While mathematically there are a few subtle differences, in practicality Vectors of Vectors make perfectly suitable transformation matrices, and transformation matrices make perfectly suitable orientations. A matrix being a vector of vectors has the upside that you only need to write one class to handle both.
To project a vector v
, take into account 3 parameters of the camera:
loc
, a Vec3
for the camera's positiontrans
, a Mat3by3
(also known as a Vec3
of Vec3
's) for the inverse of the camera's orientation
zoom
, a scaling factor used to determine the perspective. A z
(relative to the camera) of zoom
is equivalent to being in 2D; that is, no scaling from perspective.projection works like this:
function Camera:project(v)
local relv -- v positioned relative to the camera, both in orientation and location
relv = self.trans * (v - self.loc) -- here '*' is vector dot product
if relv.z > 0 then
-- v is in front of the camera
local w -- perspective scaling factor
w = self.zoom / relv.z
local projv -- projected vector
projv = Vec2(relv.x * w, relv.y * w)
return projv
else
-- v is behind the camera
return nil
end
end
this assumes that Vec2(0, 0) corresponds to the center of the window, not a corner. Setting that up is a simple translation.
trans
should start out as the identity matrix: Vec3(Vec3(1, 0, 0), Vec3(0, 1, 0), Vec3(0, 0, 1))
and be calculated incrementally, making small adjustments every time an orientation change is made.
I get the feeling you already know the basics of matrices, but if you don't, the idea is this: A matrix is a Vector of Vectors, which can be thought of, at least in this case, as a coordinate system. Each vector can be thought of as one axis of the coordinate system. By changing the elements of the matrix (which are vectors and are thought of as the matrix's columns), you change the meaning of coordinates in that coordinate system. In normal usage, the first component of a vector means move right, the second component means up, and the third component means forward. However, with a matrix, you can make each component point in an arbitrary direction. The definition of dot product is
function Vec3.dot(a, b) return a.x * b.x + a.y + b.y + a.z * b.z end
for a matrix
Vec3(axis1, axis2, axis3)
given the definition of dot product, that matrix dotted with a vector v
would yield
axis1 * v.x + axis2 * v.y + axis3 * v.z
which means that the first element of v
says how many axis1
s to move by, the second element says how many axis2
's to move by, and the third element says how many axis3
's to move by, with the end result being v
, if it were expressed in standard coordinates instead of the coordinates of the matrix. When we multiply a matrix with a vector, we're changing the meaning of the components of the vector. Essentially, its the mathematical expression of a statement like "anything that was to the right is now less to the right and more forward" or anything similar. In one sentence, a matrix transforms a space.
Getting back to the task at hand, to represent a rotation in "pitch" (meaning around the x axis) by an angle theta
using a matrix, you could write:
function pitchrotation(theta)
return Vec3(
-- axis 1
-- rotated x axis
-- we're rotating *around* the x axis, so it stays the same
Vec3(
1,
0,
0
),
-- axis 2
-- rotated y axis
Vec3(
0,
math.cos(theta),
math.sin(theta)
),
-- axis 3
-- rotated z axis
Vec3(
0,
-math.sin(theta),
math.cos(theta)
)
)
end
and for "yaw" (around the y axis):
function yawrotation(theta)
return Vec3(
-- axis 1
-- rotated x axis
Vec3(
math.cos(theta),
0,
math.sin(theta)
),
-- axis 2
-- rotated y axis
-- we're rotating *around* the y axis, so it stays the same
Vec3(
0,
1,
0
),
-- axis 3
-- rotated z axis
Vec3(
-math.sin(theta),
0,
math.cos(theta)
)
)
end
and finally "roll" (around the z axis), which is especially useful in a flight sim:
function rollrotation(theta)
return Vec3(
-- axis 1
-- rotated x axis
Vec3(
math.cos(theta),
math.sin(theta),
0
),
-- axis 2
-- rotated y axis
Vec3(
-math.sin(theta),
math.cos(theta),
0
),
-- axis 3
-- rotated z axis
-- we're rotating *around* the z axis, so it stays the same
Vec3(
0,
0,
1
)
)
end
If you visualize what that does to the x, y, and z axes in your head, all those cosines and sines and sign flips might start to make sense. They're all in there for a reason.
Finally, we reach the last step of the puzzle, which is applying these rotations. A nice feature of matrices is that it's easy to compound them. You can transform a transformation very easily - you simply transform each axis! To transform an existing matrix A
by a matrix B
:
function combinematrices(a, b)
return Vec3(b * a.x, b * a.y, b * a.z) -- x y and z are the first second and third axes
end
what this means is that if you want to apply a change to your camera, you can simply use this matrix combination mechanism to rotate the orientation a little bit each frame. These functions for your camera class will provide an easy way to make changes:
function Camera:rotateyaw(theta)
self.trans = combinematrices(self.trans, yawrotation(-theta))
end
we use negative theta because we want trans to be the opposite of the orientation of the camera, for projection. You can make similar functions with pitch and roll.
With all of these building blocks in place, you should be all set to write 3D graphics code in Lua. You're going to want to experiment with zoom
- I generally use 500
, but it really depends of the application.
The one piece missing, that really can't be accomplished without OpenGL, is depth testing. If you're drawing anything but points of wireframe, there isn't really a good way to make sure everything draws in the right order. You can sort, but that's inefficient, and it doesn't handle some corner cases where you have to do it pixel by pixel, which is what OpenGL does.
Happy coding! Hope that was helpful!
Upvotes: 8