24. OpenGL Camera Part 3 (Version 2.0)
Introduction
In this tutorial, we’ll explore how to create a third-person camera system in OpenGL. Unlike the first-person camera, which places the viewer inside the scene, a third-person camera follows a target object from behind. This type of camera is common in adventure and role-playing games, offering a clear view of the character while maintaining player control over movement and rotation.
We’ll build on the concepts introduced in the previous camera tutorials, adding new techniques to handle third-person perspective.
Understanding the Third-Person Camera
A third-person camera system requires two key components:
- The target object, which the camera follows (e.g., a player or character).
- The camera offset, which determines the distance and angle of the camera relative to the target.
By combining translations and rotations, we can position the camera behind the target, ensuring the character stays in view.
Defining the Camera Offset
To begin, we define a variable for the camera’s radius, which controls the distance between the camera and the target object:
float cRadius = 10.0f; // Distance from the character to the camera
This value can be adjusted to change the camera’s zoom level or perspective.
Setting Up the Camera Transformations
The camera’s transformations involve three steps:
- Translate: Move the camera backward by the radius value to position it behind the character.
- Rotate: Apply pitch and yaw rotations to allow the camera to look around the target.
- Adjust Scene: Move the entire scene to match the camera’s perspective.
Here’s how we translate the camera:
glTranslatef(0.0f, 0.0f, -cRadius); // Move the camera backward
Next, we rotate the view to simulate camera movement around the target:
glRotatef(xrot, 1.0, 0.0, 0.0); // Rotate up and down glRotatef(yrot, 0.0, 1.0, 0.0); // Rotate left and right
Drawing the Target Object
The target object (e.g., a character) is drawn at the origin of the camera’s perspective. For this example, we’ll use a red cube to represent the character:
glColor3f(1.0f, 0.0f, 0.0f); // Set color to red glutSolidCube(2); // Draw the character
Rendering the Scene
Once the camera transformations are applied, we adjust the scene based on the character’s position:
glTranslated(-xpos, 0.0f, -zpos); // Translate the scene to match the camera's position
Finally, we draw additional objects in the scene, such as cubes, to create a 3D environment.
Maintaining Movement and Controls
The third-person camera retains the movement and strafing controls from the previous tutorials. The character always stays in view, and the camera follows smoothly as the player navigates the environment.
Tutorial Code
Here’s the complete implementation of the third-person camera system:
#include <GL/gl.h> #include <GL/glut.h> #include <cstdlib> #include <cmath> // Camera and character variables float xpos = 0, ypos = 0, zpos = 0, xrot = 0, yrot = 0, cRadius = 10.0f; float lastx, lasty; // Cube positions float positionz[10]; float positionx[10]; void cubepositions(void) { for (int i = 0; i < 10; i++) { positionz[i] = rand() % 5 + 1; positionx[i] = rand() % 5 + 1; } } // Draw cubes void cube(void) { for (int i = 0; i < 10; i++) { glPushMatrix(); glTranslated(-positionx[i + 1] * 10, 0, -positionz[i + 1] * 10); glutSolidCube(2); glPopMatrix(); } } // Initialize cube positions void init(void) { cubepositions(); } // Enable OpenGL features void enable(void) { glEnable(GL_DEPTH_TEST); glEnable(GL_LIGHTING); glEnable(GL_LIGHT0); glShadeModel(GL_SMOOTH); } // Render scene void display(void) { glClearColor(0.0, 0.0, 0.0, 1.0); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); enable(); glLoadIdentity(); // Camera transformations glTranslatef(0.0f, 0.0f, -cRadius); // Move the camera backward glRotatef(xrot, 1.0, 0.0, 0.0); // Rotate pitch glColor3f(1.0f, 0.0f, 0.0f); // Draw character glutSolidCube(2); glRotatef(yrot, 0.0, 1.0, 0.0); // Rotate yaw glTranslated(-xpos, 0.0f, -zpos); // Adjust scene glColor3f(1.0f, 1.0f, 1.0f); cube(); // Draw cubes glutSwapBuffers(); } // Adjust viewport and perspective void reshape(int w, int h) { glViewport(0, 0, (GLsizei)w, (GLsizei)h); glMatrixMode(GL_PROJECTION); glLoadIdentity(); gluPerspective(60, (GLfloat)w / (GLfloat)h, 1.0, 100.0); glMatrixMode(GL_MODELVIEW); } // Handle keyboard input void keyboard(unsigned char key, int x, int y) { if (key == 'w') { // Move forward float yrotrad = (yrot / 180 * 3.141592654f); xpos += float(sin(yrotrad)); zpos -= float(cos(yrotrad)); } if (key == 's') { // Move backward float yrotrad = (yrot / 180 * 3.141592654f); xpos -= float(sin(yrotrad)); zpos += float(cos(yrotrad)); } if (key == 27) { // Exit exit(0); } } int main(int argc, char **argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH); glutInitWindowSize(500, 500); glutCreateWindow("Third-Person Camera"); init(); glutDisplayFunc(display); glutIdleFunc(display); glutReshapeFunc(reshape); glutKeyboardFunc(keyboard); glutMainLoop(); return 0; }
If you have any questions, feel free to email me at swiftless@gmail.com. Happy coding!
with mouse movement in this tutorial, the object also moves? as opposed to the object turning on the spot
Another big thanks for the tutorial. I had a bit of an idea how to do this, but it was nice to not have to derive the equations :).
Ecellent stuff, Got mine working also. I have removed the Yposition updates when walking forward and backward, so when you mouselook up and down, you don’t actually move up or down while walking / strafing.
I’ve also changed the Key Behavior in this one: Key Repeat is off, and moved the code that takes care of walking / strafing in it’s own function, wich i then call in the camera function after checking the bool\s associated with the key pressed ( also means that you need to have the release function to set everything to false again, otherwise it keeps moving )
The mouse motion function does a little trick with warping the mouse back to the center of the screen with glutWarpPointer(CenterX, CenterY) ;. The boolean WarpingMouse is set to false initially, when you move the mouse it is set to true, then glutWarpPointer(); does it’s centering, and when the mouse stops, WarpingMouse will be set to false again. This way the mouse never ever leaves the screen with just moving it. Also removed the jerky pointer with glutSetCursor(GLUT_CURSOR_NONE);
Something nice to start with for an FPS.
This should compile on windows, will not work on mac, and i don’t know about linux.
( is in there for future debug purposes )
#include
#include
#include
#include
float Xposition;
float Yposition;
float Zposition;
float Xrotation;
float Yrotation;
float Zraotaion;
float XRotationRadius;
float YRotationRadius;
float MoveVelocity = 0.25f ;
float MouseVelocity = 0.35f ;
float CubePositionsX[10] ;
float CubePositionsZ[10] ;
bool bForward = false;
bool bReverse = false;
bool bStrafeLeft = false;
bool bStrafeRight = false;
bool WarpingMouse = false;
//yeah wat is dit heh ?
float Pi = 3.141592654f;
void CubePositions(void){
for(int i = 0; i <10; i++){
CubePositionsX[i] = i + 5 ;
CubePositionsZ[i] = i + 5 ;
}
}
void init(void){
CubePositions();
glutSetCursor(GLUT_CURSOR_NONE);
}
void DrawCubes(void) {
for(int i = 0; i < 10; i++){
glPushMatrix();
glTranslated(-CubePositionsX[i + 1] * 10, 0, -CubePositionsZ[i + 1] * 10 );
glutSolidCube(2);
glPopMatrix();
}
}
void Enables(void){
glEnable(GL_DEPTH_TEST);
glEnable(GL_LIGHTING);
glEnable(GL_LIGHT0);
glShadeModel(GL_SMOOTH);
}
void Forward(void) {
YRotationRadius = (Yrotation / 180 * Pi ) ;
XRotationRadius = (Xrotation / 180 * Pi ) ;
Xposition += float(sin(YRotationRadius)) * MoveVelocity ;
Zposition -= float(cos(YRotationRadius)) * MoveVelocity ;
}
void Reverse(void) {
YRotationRadius = (Yrotation / 180 * Pi) ;
XRotationRadius = (Xrotation / 180 * Pi) ;
Xposition -= float(sin(YRotationRadius)) * MoveVelocity ;
Zposition += float(cos(YRotationRadius)) * MoveVelocity;
}
void StrafeRight(void) {
YRotationRadius = (Yrotation / 180 * Pi );
Xposition += float(cos(YRotationRadius)) * MoveVelocity ;
Zposition += float(sin(YRotationRadius)) * MoveVelocity;
}
void StrafeLeft(void) {
YRotationRadius = (Yrotation / 180 * Pi) ;
Xposition -= float(cos(YRotationRadius)) * MoveVelocity;
Zposition -= float(sin(YRotationRadius)) * MoveVelocity;
}
void Camera(void) {
if(bForward)
Forward();
if(bReverse)
Reverse() ;
if(bStrafeLeft)
StrafeLeft();
if(bStrafeRight)
StrafeRight();
glRotatef(Xrotation, 1.0, 0.0, 0.0) ;
glRotatef(Yrotation, 0.0, 1.0, 0.0) ;
glTranslated(-Xposition, -Yposition, -Zposition);
}
void RenderScene(void){
glClearColor(0.0, 0.0, 0.0, 1.0);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
Camera();
Enables();
DrawCubes();
glutSwapBuffers();
}
void Reshape(int w, int h){
glViewport(0, 0, (GLsizei)w, (GLsizei)h) ;
glMatrixMode(GL_PROJECTION) ;
glLoadIdentity();
gluPerspective(60, (GLfloat)w / (GLfloat)h, 1.0, 10000.0) ;
glMatrixMode(GL_MODELVIEW);
}
void NormalKeyPress(unsigned char key, int x, int y){
switch(key) {
case 'w':
bForward = true;
break;
case 's':
bReverse = true;
break;
case 'a':
bStrafeLeft = true;
break;
case 'd':
bStrafeRight = true;
break;
case 27:
exit(0);
break;
}
}
void NormalKeyRelease(unsigned char key, int x, int y) {
switch(key) {
case 'w':
bForward = false ;
break;
case 's':
bReverse = false;
break;
case 'a':
bStrafeLeft = false ;
break;
case 'd':
bStrafeRight = false;
break;
}
}
void PassiveMouseMotion(int x, int y){
int CenterX = glutGet(GLUT_WINDOW_WIDTH) / 2 ;
int CenterY = glutGet(GLUT_WINDOW_HEIGHT) / 2 ;
int DiffX = x – CenterX;
int DiffY = y – CenterY;
if(!WarpingMouse) {
WarpingMouse = true;
Xrotation += (float) DiffY * MouseVelocity ;
Yrotation += (float) DiffX * MouseVelocity ;
glutWarpPointer(CenterX, CenterY) ;
} else {
WarpingMouse = false ;
}
}
int main(int argc, char **argv) {
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH);
glutInitWindowSize(800, 600) ;
glutInitWindowPosition(100, 100) ;
glutCreateWindow("Blocks!") ;
init();
glutDisplayFunc(RenderScene) ;
glutIdleFunc(RenderScene) ;
glutReshapeFunc(Reshape) ;
glutKeyboardFunc(NormalKeyPress) ;
glutKeyboardUpFunc(NormalKeyRelease) ;
glutPassiveMotionFunc(PassiveMouseMotion);
glutMainLoop();
return 0;
}
Hi. I’m new to this site. I browsed thru your tutorials and it look excited. I’m busy with XCode4 and would like to try all of them out. Don’t know if it is gonna work but if so I will tell you about it. But this really looks like a great site.
i’ve just begun going through them with Xcode 4.0.2 – they work wonderfully.
Have fun!!
-kropcke
There is also another method that i found best for me. I used the gluLookAt() to get a good 3rd person view of an object. for instance (Just a snippet from a bigger game)
void UpdateCamera()
{
float yRadians = Player.yrot / 180 * 3.14159;
gluLookAt( Player.xpos – (sin(yRadians)), -0.4, Player.zpos + (cos(yRadians)),
Player.xpos, Player.ypos, Player.zpos, 0.0, 1.0, 0.0);
}
with the Player struct being global.
Hi Shadeirou,
Thats not a bad suggestion you have made there. When I get around to it, I am hoping to write a 4th camera tutorial on using quaternions instead of Eular angles.
You have probably heard of Gimbal lock, this is when your rotations along the different axis have gone in such a way that you lose one degree of freedom.
Therefore, looking straight up and down using this camera system, while it will mostly work, can fail miserably if you want to create something like a flight simulator.
Just a heads up if you plan to keep using this.
Cheers,
Swiftless
Forgot to put this in my last post: the variable “dist” is the distance you want that camera to move forward.
Hey, nice camera tutorials. Just wanted to make a quick suggestion. I was messing around with making a camera class myself and used the your forward movement formulas, but came across a problem: When you look straight up or down an move forward, you don’t move only up or down, but also forward a bit. I decided to do the math myself and came up with a fix. Here is the code I use:
float d = dist * cos( degreeToRadian( xrot ) );
origin_.x += d * cos( degreeToRadian( yrot + 90 ) );
origin_.y += dist * sin( degreeToRadian( xrot ) );
origin_.z += d * sin( degreeToRadian( yrot + 90 ) );
This causes the x and z distances to be affected by the x-axis rotation, or the roll. Take into account that I am using the following coordinate system:
glOrtho( 0, scene_w, scene_h, 0, 0, scene_l );
Anyway, just thought I’d see if this helped anyone.
Excellent website.
One thing, could you post a source file as copy and pasting of the code doesn’t compile straight away on Linux.
Whilst I know that it is necessary to write it yourself to learn how it all works and know how it works but using it as a temporary snippet so is a little bit of a pain.
REALLY good site though 🙂
Rich
Hi Richard,
Thanks for your feedback.
I used to have source files (and they still exist on the server), but I removed the links when I redid the site recently. I had someone else experience a copy and paste issue, because their IDE support rich text and it copied over the formatting, causing errors.
I am currently in the process of rewriting the tutorials, and it uses a code box, with a copy and paste safe ability, so I don’t see a point in putting the links back up and maintaining those files.
Cheers,
Swiftless
Hi Zero,
That is almost the exact way I check for multiple key presses at a time. I plan to write this into the Keyboard tutorial as I have had it asked before.
Cheers,
Swiftless
I just do this:
static bool keys[256];
void keyup(unsigned char key,int x,int y)
{
keys[key]=false;
}
void keydn(unsigned char key,int x,int y)
{
keys[key]=true;
}
and in main:
glutKeyboardUpFunc(keyup);
glutKeyboardFunc(keydn);
Doh’, its only use glutKeyboardUpFunc();
He he he..
Thank you..
Hi, do you know how to use glutKeyboardFunc and glutSpecialFunc to get two or more keys at same time?
Like press ‘W’ to go front and ‘D’ to go right?
Thank you..
Zero, from Brazil.