Camera Rotation & Translation, and Reference Ground
CS 480 Lecture,
Dr. Lawlor
Camera Rotation
It's extremely easy to use glRotatef to make the camera rotate when you
move the mouse. Sadly, it's extremely difficult to make it rotate
well--the trouble is that the second rotation is going to be in the
frame of the first rotation, which keeps changing. Hence the "two
rotations" method invariably results in the camera moving in weird
silly ways. Go ahead, try it!
A better approach is to explicitly represent the camera's orientation using three axes, like with a class like this:
/* Orthonormal coordinate frame */
class ortho_frame {
public:
/* Unit vectors pointing along axes of our frame.
X cross Y is Z. (right-handed coordinate system)
*/
vec3 x,y,z;
ortho_frame() :x(1,0,0), y(0,1,0), z(0,0,1) {}
/* Reorient this coordinate frame by this far along in the X and Y axes.
"dx" is the distance along the z axis to push the x axis;
"dy" the distance along the z axis to push the y axis.
*/
void nudge(double dx,double dy) {
x+=dx*z;
y+=dy*z;
orthonormalize();
}
/* Reconstruct an orthonormal frame from X and Y axes.
Y is primary, X is secondary, Z is tertiary.
*/
void orthonormalize(void) {
y=normalize(y);
z=normalize(cross(x,y));
x=normalize(cross(y,z));
}
};
ortho_frame orient; /* camera orientation */
This class keeps track of three unit vectors: X, Y, and Z. In
classic camera fashion, you're looking down the negative Z axis, X is
to your right, and Y is up. The key method here is "nudge", which
pushes or pulls on the X and Y axes in the Z direction. If you
repeatedly "nudge" the coordinate frame, this makes the camera X and Y
axes rotate. Typically, you rotate the coordinate frame in
response to mouse motion, like from these GLUT mouse event handling
functions:
int mouse_x=0,mouse_y=0; /* last known mouse position */
void mouse(int button,int state,int x,int y)
{ /* mouse being pressed or released--save position for motion */
mouse_x=x; mouse_y=y;
}
void motion(int x, int y) {
float scale=0.01; /* radians rotation per pixel of mouse motion */
orient.nudge(
scale*(x-mouse_x),
-scale*(y-mouse_y) /* OpenGL Y goes the opposite direction of GLUT Y */
);
mouse_x=x; mouse_y=y; /* save old mouse positions */
}
You'd of course register these methods from your main function like so:
glutMouseFunc(mouse);
glutMotionFunc(motion);
And finally, to rotate your object to match this coordinate frame, just
gluLookAt along the new axes during your display function.
#define VEC3_TO_XYZ(v) (v).x,(v).y,(v).z /* convert a vec3 to three float arguments */
gluLookAt(VEC3_TO_XYZ(vec3(0.0)),
VEC3_TO_XYZ(-orient.z), /* we're looking down the -Z axis */
VEC3_TO_XYZ(orient.y) /* camera up vector */
);
This approach means that moving the mouse to the left always moves the
object the same way, even after a rotation. This is called a
"virtual trackball", and it's common and works well.
Oh, and if you don't want people to be able to tilt their heads, you also need this:
/* Don't allow head tilting! */
orient.x.y=0.0;
orient.y=cross(orient.z,orient.x);
orient.orthonormalize();
Camera Motion
I like first-person computer games where you move using the "WASD" keys
and look around with the mouse. It's really easy to write these,
especially if you do the following:
vec3 camera; /* camera location */
void keyboard(unsigned char key,int x,int y) {
if (key=='w') camera.z++;
if (key=='s') camera.z--;
if (key=='a') camera.x--;
if (key=='d') camera.x++;
}
// in main: glutKeyboardFunc(keyboard);
However, this doesn't work well--the "keyboard" event handling function
is only called at the keyboard repeat rate, which is somewhere between
3 and 20 times per second. This is hopefully far less than our
graphics framerate! The result is hence ugly, stuttery motion.
You can get smooth motion by keeping track of which keys are down, and
then moving the camera in the display function based on which keys are
currently pressed, like so:
bool key_down[256]; /* if true, the corresponding key is down */
void keyboard(unsigned char key,int x,int y) {
key_down[key]=true;
}
void keyboardUp(unsigned char key,int x,int y) {
key_down[key]=false;
}
// in main: glutKeyboardFunc(keyboard); glutKeyboardUpFunc(keyboardUp);
// in display function:
float vel=3.0*display_dt; /* meters camera motion per frame */
if (key_down[(int)'w']) camera-=vel*orient.z;
if (key_down[(int)'s']) camera+=vel*orient.z;
if (key_down[(int)'a']) camera-=vel*orient.x;
if (key_down[(int)'d']) camera+=vel*orient.x;
This makes the camera move a little bit per frame while the
corresponding keys are held down. If you want even smoother
motion, you can slowly ramp up the velocity over time once a key goes
down, instead of just instantly changing speed.
Ground with Grid Lines
It's trivially easy to draw in a ground plane, and it sure makes it easier to tell where you are in the world:
// Draw in a ground plane
float floor=0.0; /* height of ground */
float big=30; /* size of the ground */
glBegin(GL_QUADS);
glColor4f(0.6,0.6,0.6,1.0); /* light, opaque grey */
glVertex3f(-big,floor,-big);
glVertex3f(+big,floor,-big);
glVertex3f(+big,floor,+big);
glVertex3f(-big,floor,+big);
glEnd();
It helps even more to put in a few little grid lines, like so:
glBegin(GL_LINES);
glColor4f(0.0,0.0,0.0,0.4); /* transparent black -> darker grey */
for (int v=-big;v<=big;v++) {
glVertex3f(v,floor,-big);
glVertex3f(v,floor,+big);
glVertex3f(-big,floor,v);
glVertex3f(+big,floor,v);
}
glEnd();
This all works, but...
Everybody's Z buffer fighting! (Not Kung Fu Fighting...)
Note that this looks about a zillion times worse when it's
animating. The fix is just to turn off depth writes after we draw
the scene, but before we draw the plane--this lets the plane and lines
coexist instead of the silly back-and-forth fighting over who's on top.
glDepthMask(GL_FALSE); /* don't write to depth buffer (no Z fighting!) */
Much better! We can make the lines look even better by turning on
GL_LINE_SMOOTH, which antialiases the edges of the lines by fading away
their alpha. We also need alpha blending and the right blend
function for this to work:
glEnable(GL_LINE_SMOOTH);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);
Ah, that looks much better, and it animates beautifully. Plus,
it's insanely fast--so you might as well add some lines to your
simulators, at least so you can tell how big 1 unit is!