3D Camera Math

Pages: 123
The glRotate*() takes an angle and a direction. Direction determines a line that goes through (0,0,0) and the coordinates of the direction. That line is an axis of rotation. If you want to rotate around an axis that does not go through the (0,0,0), then you have to translate first, then rotate, and finally translate back to original frame of reference.

1
2
3
glRotated(r.x, 1.0, 0.0, 0.0);
glRotated(r.y, 0.0, 1.0, 0.0);
glRotated(r.z, 0.0, 0.0, 1.0);

Stepwise rotations can get tricky, depending how the axes are chosen. Some use the non-rotating world axes, others use axes that have rotated after the first rotation, which introduces Gimbal lock. In other words: it can get real tricky if you have to add up transformations.

Quaternions are mathematically more elegant.
Here is how to rotate at an anlge alpha around an axis given by vector (x,y,z), passing through (0,0,0).

Let <, > stand for scalar product, that is:
<(a1, a2, a3), (b1, b2, b3)>:=a1*b1+a2*b2+a3*b3;
Let |(a1, a2, a3)| stand for vector length, that is
|(a1, a2, a3)| =sqrt (<(a1,a2,a3), (a1,a2,a3)>)


Mathematically, to rotate around an axis given by vector (x,y,z) and passing through (0,0,0):
1. Complete (x,y,z) to a vector space basis:

1.1 If x !=0
e_1:=(x,y,z); e_2:=(0,1,0); e_3:=(0,0,1);
If x<0 then swap e_2 and e_3 ("to preserve orientation")

1.2 Else If x == 0 and y!=0
e_1:=(x,y,z); e_2:=(0,0,1); e_3:=(1,0,0);
If y>0 then swap e_2 and e_3 ("preserve orientation").

1.3 Else if z!=0
e_1:=(x,y,z); e_2:=(1,0,0); e_3:=(0,1,0);
If z<0 then sawp e_2 and e_3 ("preserve orientation").

1.4 Else if x==0 && y==0 && z==0
crash with a message: "attempting to rotate around zero axis".

2. Transform e_1, e_2, e_3 to orthonormal basis using Gramm-Schidt orthogonalization:
2.1 Normalize e_1, i.e., replace e_1 by e_1/|e_1|
2.2 Replace e_2 by e_2-<e_2, e_1>e_1
2.3 Normalize e_2 (Replace e_2 by e_2/|e_2|)
2.3 Replace e_3 by e_3-<e_1, e_3> - <e_2, e_3>
2.4 Normalize e_3 (i.e., send e_3 to e_3/ |e_3|)

3. Get the 3x3 matrix E given by:
E=e_1|e_2|e_3,
where e_1, e_2, e_3 are the columns of E.

4. Given an angle alpha, let B be the matrix

B:=
(1 0 0 )
(0 cos(alpha) -sin(alpha))
(0 sin(alpha) cos(alpha) )


After all this work: the rotation matrix at an angle alpha around the axis given by (x, y,z) is given by

E B E^{-1},

where E^{-1} denotes the matrix inverse of E (there should be a function for that in openGL, if not, I can post how to implement that as well).

I don't understand why I need all this though when all I want to do is calculate how much to change p.x, p.y, p.z based on r.x, r.y, r.z
I don't think you need any of it, especially since you already have a working solution. It was more of a how-I'd-do-it-if-it-were-me post.

By the way, procedure 3 (making a matrix orthonormal) is (no surprise) implemented in opengl, there apparently is a funtion glm::orthonormalize. Of course, what I am suggesting would be implemented with very little actual coding, mostly it will be looking up the correct opengl functions.
Last edited on
L B wrote:
I don't understand why

You state in Lounge that you are new to 3D math, so it should not be a surprise if someone mentions a thing or two about 3D math that you might like to know about.
tition wrote:
I don't think you need any of it, especially since you already have a working solution.
But I don't have a working solution. My code currently doesn't take r.z into account.

@keskiverto: I would have thought that converting relative translation into absolute translation would be much simpler?
Last edited on
You should add a vector that represents where you're facing. It'd make everything much easier.
I already have that, it's {r.x, r.y, r.z}, aka just r.

Let me try a different explanation.

Vector p is my absolute position on the real coordinate system, vector r is my rotation about the x, y, and z axis of the real coordinate system, vector d is another position-based vector on the real coordinate system, and vector m is my position-based vector relative to my camera's local coordinate system.

Given r, how can I convert m into d?
Last edited on
Okay so you still can't just add that times speed to you?
Last edited on
@Lumpkin no, because it is not a normal vector, it's just a vector with three components to explain the rotation about each of the x, y, and z axis.
@L B: No, it is not a "vector". It is an ordered set of three angles. An ordered set of three rotations, each around a different axis. However, rotations could be combined. You would have only one angle, but you would need to store the axis, which requires two floats at minimum.

An example of something almost, but not quite, entirely unrelated: http://theobald.brandeis.edu/qcp/
Here's how I've been do it.

1
2
3
4
5
6
7
8
9
10
11
12
void Orientation::calculate_direction_vectors ()
{
    forward_direction   = glm::vec3 ( std::cos( vertical ) * std::sin( horizontal ),
                                      std::sin ( vertical ),
                                      std::cos( vertical ) * std::cos( horizontal ) );

    rightward_direction = glm::vec3 ( std::sin( horizontal - PI_OVER_2 ),
                                      0.f,
                                      std::cos( horizontal - PI_OVER_2 ));

    upward_direction    = glm::cross( rightward_direction, forward_direction );
}


1
2
3
4
5
6
7
8
9
10
11
glm::mat4 Model::get_model_matrix() {

    model_matrix = glm::translate(glm::mat4(1.f), modelspace_position + worldspace_position);
                                                     //note rotating around rightward direction, not x-axis
    model_matrix = glm::rotate(model_matrix, (orientation.vertical / (2.f*PI)) * 360.f, orientation.rightward_direction);
   //here rotate around just about y-axis, rotating about the upward direction would not be the same thing
    model_matrix = glm::rotate(model_matrix, (orientation.horizontal / (2.f*PI)) * 360.f, glm::vec3(0.f, 1.f, 0.f));
   //if I wanted to be able to do cartwheels, then I would rotate about forward direction also.
  
    return model_matrix;
}


And update is like this,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//...
    orientation.horizontal += angular_speed * delta_t * ( WIDTH  / 2 - control_state.mouse_pos_x );
    orientation.vertical   += angular_speed * delta_t * ( HEIGHT / 2 - control_state.mouse_pos_y );
    orientation.calculate_direction_vectors();

    if (control_state.up_key)
        worldspace_position += orientation.forward_direction * delta_t * forward_speed;

    if (control_state.down_key)
        worldspace_position -= orientation.forward_direction * delta_t * forward_speed;

    if (control_state.right_key)
        worldspace_position += orientation.rightward_direction * delta_t * strife_speed;

    if (control_state.left_key)
        worldspace_position -= orientation.rightward_direction * delta_t * strife_speed;
//...
 

1
2
3
4
5
6
7
8
9
10
11
12
    //...
    model_matrix = model.get_model_matrix();
    view_matrix  = view;
    projection_matrix = projection;

    mvp_matrix   = projection_matrix * view_matrix * model_matrix;

    glUniformMatrix4fv(mvp_id,   1, GL_FALSE,   &mvp_matrix[0][0]);
    glUniformMatrix4fv(view_id,  1, GL_FALSE,  &view_matrix[0][0]);
    glUniformMatrix4fv(model_id, 1, GL_FALSE, &model_matrix[0][0]);
    //...
    
Last edited on
htirwin wrote:
upward_direction = glm::cross( rightward_direction, forward_direction );
How does this work to give the upward direction from the forward and rightward directions?
It just the cross product.
http://en.wikipedia.org/wiki/Cross_product
vector r is my rotation about the x, y, and z

Ouch.. that is confusing!
You got us all mixed up because you use letters x,y,z to denote angles, instead of distances!

Convention: always denote angles with Greek letters, coordinates- with small Latin letters. Reason: everyone expects that. Doing anything different is bound to confuse readers (example: I completely misunderstood your notation).

****
What I would do if I were you: drop the angles altogether. Instead store rotation (orthonormal) matrix. Those are 9 coordinates, but after you orthonormalize, you get only 3 free parameters, so it's the same amount of information.

Now, if you DON'T want to drop the angles, and want to do things the hard way, there are a few things you should know.

Let rx_\alpha, ry_\beta, rz_\gamma be rotations relative at angles \alpha, \beta and \gamma relative to x, y and z axis.
1. applying first rx_\alpha and then ry_\beta IS NOT the same as applying ry_\beta first and rx_\alpha second. Same thing holds for all other pairs of rotations.
2. Hence you need to decide the order in which you shall carry the rotations. I'd presume you rotate first around x axis, then around y axis, then around z axis.


Now, the easiest way I can think of is to write the three rotations rx_\alpha, ry_\beta, rz_\gamma as matrices:
1
2
3
4
rx_\alpha=
( 1 0           0          )
( 0 cos \alpha -sin \alpha )
( 0 sin \alpha cos \alpha  )


1
2
3
4
rz_\gamma=
( cos \gamma -sin \gamma 0 )
( sin \gamma  cos \gamma 0 )
( 0           0          1 )



I omitted ry_\beta - exercise for you - write that yourself.

Now, to get the matrix of rotation: simply carry out the multiplication of matrices

matrix_rotation:= rx_\alpha * ry_\beta * rz_\gamma

(do you know how to multiply matrices?)

Finally, to get the "up" vector from the matrix_rotation:

simply multiply matrix_rotation by the vector-column
1
2
3
(0)
(0)
(1)

In other words, your "up" vector is the last, third column, of matrix_rotation.

Last edited on
my implementation seems the hard way to do it

I agree with this :P

I'll provide an outline of how I did it when I was experimenting with 3D FPS cameras. I'll try to give a rather high-level description, because once this is understood filling in the details will be trivial. In my implementation
I use modern OpenGL (i.e. with shaders) but I believe the approach I follow should also be applicable in your case without need for major changes. OK, here it goes then!

I'll start with a very simple but at the same time extremely useful pic I made
to explain this to a friend of mine -> http://i39.tinypic.com/2duvuwm.png

In the upper half of the picture, moving the global axes aligned camera forward
is trivial (we just add z to camera.pos), but we have two major problems:

(1) We don't know how to pan the camera left and right.

(2) We don't know how to move the camera forward after panning (i.e. when it is no longer
           global axes aligned). Simply adding z to camera.pos will move it towards the flowers, which
           is no longer what the camera is facing at.

In the lower half of the picture, both problems are solved by adding a local coordinate system to the camera:

(1) Panning the camera left and right by theta degrees is as simple as rotating
           axes z' and x' around axis y' (can't see it in the pic) by +/- theta degrees.

(2) Moving the camera forward is as simple as adding z' to camera.pos

At this point, if you're not familiar with the model-view-projection matrix convention, you should spend
some time understanding it. The camera's role here is to provide and maintain the view matrix:
http://stackoverflow.com/questions/5550620/the-purpose-of-model-view-projection-matrix

Here's the camera class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
class Camera {
		
    // forward axis points inside screen
    Vector3 forwardAxis = new Vector3(0, 0, -1);

    Vector3 rightAxis = new Vector3(1, 0, 0);

    // upAxis is always (0, 1, 0) in my demo (i.e. y' == y)
    Vector3 upAxis = new Vector3(0, 1, 0);

    // camera looks inside screen: is located at (0, 0, 3) and looks at origin
    Vector3 position = new Vector3(0, 0, 3), target = new Vector3(Vector3.Zero);

    Matrix4 viewMatrix = new Matrix4();
    boolean mustUpdateViewMatrix = true;

    int viewPosId;
    int viewDirId;

    // shader stuff
    void register(GL20 gl, int program) {
        viewPosId = gl.glGetUniformLocation(program, "viewPos");
        viewDirId = gl.glGetUniformLocation(program, "viewDir");
    }

    // shader stuff
    void upload(GL20 gl) {
        gl.glUniform3f(viewPosId, position.x, position.y, position.z);
        gl.glUniform3f(viewDirId, forwardAxis.x, forwardAxis.y, -forwardAxis.z);
    }

    void moveForward(float amount) { 
        // can look up but can't move up while looking up
        position.add(forwardAxis.x * amount, forwardAxis.y * 0, forwardAxis.z * amount);
        mustUpdateViewMatrix = true;
    }

    void moveUp(float amount) { 
        // only for jumping
        position.add(upAxis.x * 0, upAxis.y * amount, upAxis.z * 0);
        mustUpdateViewMatrix = true;
    }

    void moveRight(float amount) {
        // rightAxis.y is always 0 anyway
        position.add(rightAxis.x * amount, 0, rightAxis.z * amount);
        mustUpdateViewMatrix = true;
    }

    void rotateForwardAxis(Vector3 rotationAxis, float angle) {
        forwardAxis.rotate(rotationAxis, angle);
        mustUpdateViewMatrix = true;
    }

    void rotateRightAxis(Vector3 rotationAxis, float angle) {
        rightAxis.rotate(rotationAxis, angle);
        mustUpdateViewMatrix = true;
    }

    void updateViewMatrix() {
        target.set(position);
        target.add(forwardAxis);
        
        viewMatrix.setToLookAt(position, target, upAxis);
        
        mustUpdateViewMatrix = false;
    }

    Matrix4 getViewMatrix() {
        if (mustUpdateViewMatrix) {
            updateViewMatrix();
        }
        
        return viewMatrix;
    }			
}

Here's the relevant rendering part:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
for (Mesh mesh : meshes) {
    
    // setup
    
    // get model matrix from mesh
    modelMatrix.mat.set(mesh.modelMatrix);
    
    // get view matrix from camera
    viewMatrix.mat.set(camera.getViewMatrix());
    
    // other necessary matrices
    modelViewMatrix.mat.set(viewMatrix.mat).mul(modelMatrix.mat);
    normalMatrix.mat.set(modelViewMatrix.mat).inv().transpose();
    modelViewProjectionMatrix.mat.set(projectionMatrix.mat).mul(modelViewMatrix.mat);
    
    // upload matrices to GPU
    for (GLSLMatrix matrix : allMatrices) {
        matrix.upload(gl);
    }
    
    // upload camera info to GPU
    camera.upload(gl);
    
    // bind already uploaded to GPU buffers
    bindFloatBuffer(mesh.vertexBuffer.get(0), 0, 4);
    bindFloatBuffer(mesh.colorBuffer.get(0), 1, 4);
    bindFloatBuffer(mesh.normalBuffer.get(0), 2, 4);
    
    //draw
    
    gl.glDrawArrays(gl.GL_TRIANGLES, 0, 3 * mesh.faceCount);
}

And here's the end result -> http://tinypic.com/r/2m2ib06/5

Now, about the low-level details (e.g. how to rotate a vector around another vector), I believe there is enough information in this thread if you want to implement them yourself, but, if you're bored, you can always use a library (like I did here with libGDX).
Last edited on
Your implementation seems to ignore e.g. looking upward at a 45-degree angle and moving in that direction. It's not FPS camera math I need, which is extremely simple, it's 6-degrees-of-freedom camera math I need. In fact, I don't think you even mentioned rotation around the forward axis, which is the one thing I am missing in my code.
Your implementation seems to ignore e.g. looking upward at a 45-degree angle and moving in that direction.

This is extremely trivial to add. I disabled it simply because
I didn't want it. All you have to do is change 0 to amount here:

1
2
3
4
5
    void moveForward(float amount) { 
        // can look up but can't move up while looking up
        position.add(forwardAxis.x * amount, forwardAxis.y * 0, forwardAxis.z * amount);
        mustUpdateViewMatrix = true;
    }

I don't think you even mentioned rotation around the forward axis

This is also very trivial to implement. All you have to do is add a rotateUpAxis function and
then call rotateRightAxis and rotateUpAxis by passing forwardAxis as an argument to both.

EDIT: Updated video -> http://tinypic.com/r/294h3ds/5
Last edited on
Sorry for having to be spoonfed everything, but I don't understand how left/right/up/down are affected by making that change to incorporate rotation about the forward axis. In the video the little side to side motion I saw looked like the rotation wasn't being taken into account. (I'm also away from my main development environment so I can't test right now :( )
I don't understand how left/right/up/down are affected by making that change to incorporate rotation about the forward axis
I'm not sure if I understand your question. If you bend your neck 90°, your party hat will point to the wall rather than the ceiling, while your ears will point to the floor and ceiling respectively.
Pages: 123