Skip to main content
GameDev.net gamedev.net
🔒 Locked

Camera Class

Started by Chozo Oct 17, 2004 at 2:45 PM 52 replies 8.3k views
Original Post
Chozo
Chozo
I've been trying to come up with a decent camera class for myself (I know there's a new article up on this, but I'm trying to make sure I really understand what's going on). This is what I have so far: Camera.h

/*
====================
File: Camer.h
Author: Shane Lillie
Description: Camera module header.
====================
*/

#if !defined __CAMERA_H__
#define __CAMERA_H__

#if _MSC_VER >= 1000
#pragma once
#endif

#include "EnVector.h"


/*
class Camera
*/
class Camera
{
public:
    Camera();
    ~Camera();

public:
    // calls gluLookAt()
    // does contraints on the camera
    void look();

    // moves the camera
    // in the looking direction
    void move(const EnVector3<GLdouble>& vector);

    // rotates the camera
    // (these are angles in degrees)
    void rotate(GLfloat yaw, GLfloat pitch);

private:
    EnVector3<GLdouble> m_position;
    EnVector3<GLdouble> m_lookat;
    EnVector3<GLdouble> m_up;
    GLfloat m_yaw, m_pitch;

private:
    Camera(const Camera& camera);
    Camera& operator=(const Camera& rhs);
};

#endif

Camera.cpp

/*
====================
File: Camer.cc
Author: Shane Lillie
Description: Camera module source.
====================
*/

#include <cassert>
#include <cmath>
#include <iostream>

#include <GL/glut.h>

#include "Camera.h"


#define DEG_RAD(d) ((d) * 0.0174532925) // M_PI / 180.0 is about 0.0174532925


/*
 *  Camera methods
 *
 */


Camera::Camera()
    : m_position(0.0, 0.0, 10.0),
        m_lookat(0.0, 0.0, -90.0),
        m_up(0.0, 1.0, 0.0),
        m_yaw(90.0), m_pitch(0.0f)
{
}


Camera::~Camera()
{
}


void Camera::look()
{
    gluLookAt(m_position.x(), m_position.y(), m_position.z(),
              m_position.x() + m_lookat.x(), m_position.y() + m_lookat.y(), m_position.z() + m_lookat.z(),
              m_up.x(), m_up.y(), m_up.z());

#if 1
    glBegin(GL_LINES);
        glVertex3f(m_position.x(), m_position.y(), m_position.z());
        glVertex3f(m_position.x() + m_lookat.x(), m_position.y() + m_lookat.y(), m_position.z() + m_lookat.z());
    glEnd();
#endif
}


void Camera::move(const EnVector3<GLdouble>& vector)
{
    // rotate the vector into
    // the direction we're looking
    // only on the xz-plane
    EnVector3<GLdouble> nv(vector);
    nv[0] += std::cos(DEG_RAD(m_yaw));

    // move the camera in the
    // direction we're looking
    m_position += nv;
}


void Camera::rotate(GLfloat yaw, GLfloat pitch)
{
    m_yaw += yaw;
    m_pitch -= pitch;

    // constrain the yaw
    if(m_yaw <= -360.0 || m_yaw >= 360.0)
        m_yaw = 0.0;

    // constrain the pitch
    if(m_pitch > 60.0)
        m_pitch = 60.0;
    else if(m_pitch < -60.0)
        m_pitch = -60.0;

std::cout << "yaw: " << m_yaw << ", pitch: " << m_pitch << std::endl;

    // set the new lookat vector
    // radius = 100
    m_lookat = EnVector3<GLdouble>(std::cos(DEG_RAD(m_yaw)),
                                   std::sin(DEG_RAD(m_pitch)),
                                   -std::sin(DEG_RAD(m_yaw)));

std::cout << m_lookat.to_string() << std::endl;

/*    // set the new up vector
    // (position - lookat) x vector
    EnVector3<GLdouble> vector = m_position - m_lookat;
    if(!m_yaw) m_up = vector.cross(EnVector3<GLdouble>(-1.0, 0.0, 0.0));            // looking down the z-axis
    else if(m_yaw > 0.0f) m_up = vector.cross(EnVector3<GLdouble>(0.0, 0.0, -1.0)); // positive rotation
    else m_up = vector.cross(EnVector3<GLdouble>(0.0, 0.0, 1.0));                   // negative rotation

std::cout << m_up.to_string() << std::endl;*/
}

Assuming all of my vector operations are done correctly, I'm wondering why my rotations don't work quite right. For example, if I rotate towads the +x-axis and then move down the rotated z-axis, then back on the z-axis, then forward and so on, it moves me down the x-axis. I'm sure it's something being inconsistant between my rotation and movement calculations, it's just not clear to me what it is. I appreciate any advice or suggestions. I'm not so good with the math here so it's been a very slow process getting what I have. I feel close to the correct solution, it's just enough out of my reach that I don't see what I'm doing wrong. EDIT - Also, the line I'm trying to draw from my position to where I'm looking (so I can see where I'm looking) doesn't show up at all. I have a polygon I'm rendering in front of me that I'm using a reference, but I'd really like to see that line if I can get it. [Edited by - Chozo on October 18, 2004 4:58:13 PM]
Zakwayda
Zakwayda
First of all, the line drawn in front of you may be only a dot, if that, because it's heading straight into the screen. That may be why you're not seeing it.

I'm not sure I understand your move function. It looks like you're only making an assignment to the x component of the delta vector. I'd think you'd want to assign the sin of the yaw angle to the z component as well. Then you might need to mess with the signs to get the motion going in the right direction.

I may be misunderstanding though.
Chozo
Chozo
That's what I was doing at first, but I ran into a problem with it. I start out with a yaw of 90 degrees (looking down the -z axis), which makes the sin 1. If I try to move forward (down the -z axis) I end up canceling out the movement in that direction (it's basically limiting me to one direction). I'm guessing I'll have the same problem if I face down the x-axis and try to move there (since the cos becomes 1). Is there a fix for that problem that I haven't come across?

Thanks again for the help. I appreciate whatever suggestions I can get on this.

EDIT - It's probably not clear, but I'm calling move like this:

g_camera.move(EnVector3(0.0, 0.0, -0.25));

And if I add this:

nv[2] += std::sin(DEG_RAD(m_yaw));

to the move() method, I can only move backwards, regardless of whether I use my forward key or my backward key.
Zakwayda
Zakwayda
I don't have much time, but a couple quick thoughts.

Why are you passing an initial z value in to your move routine? It seems that would throw things off and bias your movement in the world z direction, which I can't imagine is what you want.

Anyway, take a look at the following generic function to move an object given a position, an angle, and a speed:

Vector3 Move(const Vector3& pos, float angle, float speed){    float s = sinf(DegToRad(angle));    float c = cosf(DegToRad(angle));    Vector3 delta(s * speed, 0.0f, c * speed);    return pos + delta;}


Although you would probably have to mess with the signs of the delta x and z components, that should work for the general case.

If that doesn't work, you may have some other problem. Perhaps you're not doing the camera transform correctly, and although you're actually moving correctly it's coming out wrong visually. Remember that you need to translate by the negative of your camera position.

Anyway, let us know if any of that helps.
Chozo
Chozo
The initial z value of the position was just so that I didn't have to move around to see the polygon sitting at the origin. I'll move things around so the camera starts at (0,0,0) and gets re-oriented to view the polygon. See if that helps things.

I think I see what I'm doing differently now. I'm thinking of movement as a set of units to travel (ie, 0.25 units in the direction I'm looking) rather than as a speed in a given direction. So I'd end up adding the sin of the angle (1 at 90 degrees) to the number of units I wanted to move. In the approach you present, the speed is multiplied by the sin, giving the number of units to move. I must have just had my thinking backwards. Does that sound like I'm on the right track now?

I'll give that fix a try when I get home tonight and see how it goes. Thanks again for the help.
Chozo
Chozo
Okay, I got the changes implemented, but now the problem is places where the cos is 0 (ie, at an angle of 90 degrees). Doing the multiplication forces the value to 0, regardless of the speed. This is my new move() method:

void Camera::move(const EnVector3<GLdouble>& velocity){std::cout << "velocity: " << velocity.to_string() << std::endl;    // rotate the vector into    // the direction we're looking    // only on the xz-plane    EnVector3<GLdouble> delta(velocity);    delta[0] *= std::cos(DEG_RAD(m_yaw));    delta[2] *= std::sin(DEG_RAD(m_yaw));std::cout << "delta: " << delta.to_string() << std::endl;    // move the camera in the    // direction we're looking    m_position += delta;std::cout << "position: " << m_position.to_string() << std::endl;}


Right now I've moved the camera to face down the -z-axis (m_yaw = 90). Since the cos here is 0, I can't "strafe" along the x-axis. Am I still missing the core concept somewhere?

[Edited by - Chozo on October 18, 2004 4:12:43 PM]
JimPrice
JimPrice
The 90 degree issue should indicate a need to google for 'Gimbal Lock'.

Once you understand that, then it will help you to finish off your camera class.

Unless I've missed you accounting for that in the code above! I admit to only skimming it...

Jim.
Zakwayda
Zakwayda
Quote:
Am I still missing the core concept somewhere?


Perhaps. If you are so inclined, humor me and just try the function I gave you as is. Don't maintain a velocity vector - the Move() function will construct it from scratch internally based on your speed and rotation. Just pass the function your position, your angle in degrees, and your speed, and use the position it gives back.

If you move but not in the right direction, try changing the signs of the cos and sin components (4 permutations) until it's right.

The function assumes you have a DegToRad() function of some sort. Also, for now movement is not time based, but you can make it so by making speed units per second and including a dt (frametime) argument in the move function to multiply speed by.

Anyway, I recommend trying it that way and seeing if it works. If it does, then we can probably figure out where your misunderstanding is and why your version doesn't seem to work.
Zakwayda
Zakwayda
Oh, and Re: Jim's post, once you start trying to consider more than one angle (i.e. yaw) in your movement, you'll run into the problem he mentioned. At that point you'll want to take a different approach using matrices and/or quaternions.

But let's try to get your current version working first.
Chozo
Chozo
Alright, I'll hold off on the gimbal lock research for now. I had a feeling it might be a problem later, so it's good to know it's worth looking into.

Here's my implementation:

void Camera::move(float angle, float speed){    Move(m_position, angle, speed);}void Camera::Move(EnVector3<GLdouble>& pos, float angle, float speed){    float s = -sinf(DEG_RAD(angle));    float c = cosf(DEG_RAD(angle));    EnVector3<GLdouble> delta(s * speed, 0.0, c * speed);    pos += delta;}


And I call it using:

    switch(key)    {    case 'q': case 'Q':        //g_camera.move(EnVector3<GLdouble>(0.0, -0.25, 0.0));        break;    case 'e': case 'E':        //g_camera.move(EnVector3<GLdouble>(0.0, 0.25, 0.0));        break;    case 'w': case 'W':        //g_camera.move(EnVector3<GLdouble>(0.0, 0.0, -0.25));        g_camera.move(0.0f, -0.25f);        break;    case 's': case 'S':        //g_camera.move(EnVector3<GLdouble>(0.0, 0.0, 0.25));        g_camera.move(0.0f, 0.25f);        break;    case 'a': case 'A':        //g_camera.move(EnVector3<GLdouble>(-0.25, 0.0, 0.0));        g_camera.move(90.0f, 0.25f);        break;    case 'd': case 'D':        //g_camera.move(EnVector3<GLdouble>(0.25, 0.0, 0.0));        g_camera.move(-90.0f, 0.25f);        break;    }


This works perfectly (given that I'm not doing any actual rotation of the lookat vector yet). It looks like what we're doing differently here is specifying the angle relative to where we're looking and the speed in that direction. Before I was taking a 'velocity' vector and trying to rotate it into the direction I'm looking before applying it. I kind of see why this approach is better, but I'm not sure I fully understand why.
Zakwayda
Zakwayda
I see what you're doing - I guess you're just moving around but not rotating. This is more like what I had in mind:

void Camera::Move(){    if (/*rotate left key*/)    {        m_angle -= rotspeed; // Note: you may have to switch left and right        if (m_angle < 0.0f)        	m_angle += 360.0f;    }    if (/*rotate right key*/)    {        m_angle += rotspeed;        if (m_angle > 360.0f)            m_angle -= 360.0f;    }    if (/*forward key*/)        Move(m_pos, m_angle, m_speed);    if (/*backward key*/)        Move(m_pos, m_angle, -m_speed);            // You could probably also do something like this:    if (/*move left key*/)        Move(m_pos, m_angle - 90.0f, m_speed);    // And likewise for move right}


With something like that, you should be able to rotate and have your movements adjust accordingly.

Having said that, it might be about time to start looking into a more 'correct' and robust implementation. As I often do, I recommend the book '3D Math Primer', which outlines matrix and vector math, and how to use matrices and quaternions to represent orientations.

Anyway, let us know if you have any more questions.
Chozo
Chozo
Cool, I'll take a look more into quaternions tomorrow. I've read elsewhere that quaternions take care of issues like gimbal lock, is that correct? One of the first results on google for quaternion camera is an article on gamedev, so I'll probably start there and see where it takes me.

Thanks again for the help. I think I've got a better understanding of how to approach the problem so hopefully I'll get my head around a quaternion implementation after a bit of messing with it.
Zakwayda
Zakwayda
Quote:
I've read elsewhere that quaternions take care of issues like gimbal lock, is that correct?


Actually you can avoid gimbal lock with matrices as well, but quaternions are just a little nicer to work with. So I recommend them.

Still, for 2.5 D (like Quake) you might find them to be overkill. But if you get into 6DOF movement (like Descent) they are a must have.

Let me know if that new code works.
Chozo
Chozo
Good deal. I wrote up a Quaternion class using the operations described in the gamedev article. Everything looks good with rotations so far. This is how my move/rotation looks:

void Camera::move(CameraDirection direction, float speed){    float angle = 0.0f;    switch(direction)    {    case CameraDirectionUp:        m_position[1] += speed;        return;    case CameraDirectionDown:        m_position[1] -= speed;        return;    case CameraDirectionLeft:        angle = 90.0f;        break;    case CameraDirectionRight:        angle = -90.0f;        break;    case CameraDirectionForward:        angle = 180.0f;        break;    case CameraDirectionBackward:        angle = 0.0f;        break;    }    const float sa = -std::sin(DEG_RAD(angle));    const float ca = std::cos(DEG_RAD(angle));    m_position += EnVector3<GLdouble>(sa * speed, 0.0, ca * speed);}void Camera::rotate(GLfloat yaw, GLfloat pitch){std::cout << "Before: " << m_lookat.to_string() << std::endl;    m_lookat = m_lookat.rotate(yaw, m_up);    m_lookat = m_lookat.rotate(pitch, EnVector3<GLdouble>(1.0, 0.0, 0.0));std::cout << "After: " << m_lookat.to_string() << std::endl;}


So the problem right now is that the movement isn't relative to where I'm looking. It's still stuck moving down specific axes. I see that it's just a matter of figuring out what the angles should be relative to where I'm looking, but I don't know how to extract the look angle from the Quaternion. Is there a way of doing that? It's not in any of the articles I"ve seen so far.

I'm also a little unsure of how to do looking up/down. I'm doing an FPS style camera so I don't want to move relative to the yaw, I just want to rotate the camera. I figure this means rotating the quarternion around the relative x-axis (once I can extract the angles from the quaternion) and then rotating the up vector to match where I'm looking (which means I"d have to change the horizontal rotation to not use the up vector, but rather the y-axis vector).

It's a lot of new stuff for me here, so please forgive me if I'm missing some detail somewhere. I'm trying to take it all in while applying it to what I'm doing. Thanks again for all of the help with this.

EDIT - m_lookat is a quaternion, m_up is a vector, and m_position is a vector. Just in case that's not clear.

EDIT - Added the pitch rotation (which obviously doesn'twwork exactly because it's not rotating the vector to rotate around).

[Edited by - Chozo on October 19, 2004 11:58:32 PM]
Zakwayda
Zakwayda
Ok, a few thoughts...

It's good that you're getting started with quaternions, but as I may have mentioned earlier they may actually be overkill for a 2 1/2 D camera like you're trying to do. So first let's think about an easier implementation for a second.

All you really need is:

float yaw;
float pitch;
float speed;

Unless I screwed up somewhere, the code I posted earlier should give you everything you need to move and strafe using the above variables (you'd use the yaw angle for the move function).

You want the pitch to change view direction only, not movement. So you want to create a lookat vector from an angle (yaw) and an azimuth (pitch). To be honest I've never had to do this and don't know the math off the top of my head, but it should be easy to find - try googling 'vector angle azimuth'.

In your current code you're not really doing anything with the quaternion - you're just using it to store an angle, which you then want to extract right back out.

I would recommend trying to get the basics working without the quats - just get yourself moving and looking in the right directions, using an angle/azimuth lookat vector and the code I posted earlier for movement (assuming I got it right).

Once you start working on 6dof movement, it'll be time for quaternions and matrices.

Anyway, I hope I'm helping and not making things worse. Let us know if you have more questions.
Chozo
Chozo
Is it still 6dof if you ignore roll? In other words, if I wanted movement in the exact direction I'm looking, including accounting for the pitch. Right now I'm using the camera as a way to look at what shapes I'm building from different angles so I'm not sure if I want the strict FPS move inside the xz-plane idea. Does that make sense? I guess I'd just rather have the most general approach so that I can make into whatever I need depending on what I end up doing with the camera class.
Zakwayda
Zakwayda
In the long run you'll certainly be better off with the general approach. And if you just exclude changes in roll from your input routine, it will behave as you are describing.

So you may have a little work ahead of you. I'd recommend getting a book like '3D Math Primer' and really going through all the matrix and quaternion stuff.

It can be kind of tricky, because different books and math libraries use different conventions - right or left handed, row or column vectors, etc. Getting any one of these things wrong can throw everything off.

I've got my math library set up for OpenGL (right-handed). I'd be glad to send it to you if it would help you get started.

Let me know if you need any other help.
slyterence
slyterence
I think you can extract the look vector by converting your quaternion to a matrix. It should be one of the row/column vectors of the matrix. I don't know offhand which one.

I believe the idea is supposed to be that you work with quaternions all the way through your engine, and then just convert to matrix form at the end, when you want to render your objects.
We scratch our eternal itchA twentieth century bitchWe are grateful forOur Iron Lung
Chozo
Chozo
I can't afford any more books right now, however it turns out I picked up Physics for Game Developers a while back and it has an appendix about quaternions and a chapter on using them for rotation. I'll take a look at that and see what I can come up with from it.

Thanks again for all of the help. I really appreciate it. I'll probably post again when I work out some of this to make sure I'm doing it correctly.
zedzeek
zedzeek
heres how i get the direction vector from a quaternion

dir.x = 2.0 * ( x * z - y * w );
dir.y = 2.0 * ( y * z + x * w );
dir.z = 1.0 - 2.0 * ( x * x + y * y );

Topic Locked

This topic has been locked by a moderator. New replies are not allowed.

Sign in to reply to this topic.