CS409/CS809 Tutorial 6: Local Camera Coordinates

This tutorial will show you how to specify and control a local camera coordinate system using orthonormal basis vectors.  This allows the camera to be rotated freely in any direction at any time without bias from the global coordinate system.  This means that the camera will be able to fly loops and barrel rolls. Here the demonstration simply shows how to use vector functions from the Obj Library to accomplish these tasks; to see how they are done mathematically, please see the source code in the Obj Library.

Setting up an OpenGL Project with the Obj Library of Files

1.      Start Microsoft Visual Studio 2013.  Select File / New / Project.  Under “Project Type”, choose Templates / Visual C++. Then choose a Win32 Console Application.  Change the name to Tutorial6 or some suitable name and change the location to a hard drive located on the computer (typically C or D).  You will need to remember this location for steps 3 and 5.  Make sure the “Create folder for project” checkbox is not checked and that the “Add to source control” is not checked.  Click OK.

·         If the Chose Location window keeps popping up again, you aren’t allowed to save files at the location you chose.  Select a different location, such as the desktop.

·         Do not put your program on one of the external drives like H.  The connection is slow, and your program may not compile correctly.

2.      Click Next> or Application Settings (they go to the same place).  Check the “Empty Project” box.  Make sure the “Security development lifecycle check” is not checked.  Click Finish.

3.      Go to http://www2.cs.uregina.ca/~anima/409/Terms/201810/Tutorials/index.html and download the Tutorial6.zip and ObjLibrary.zip files to your project directory.

4.      Unzip the 2 zip files.  You will have a folder called ObjLibrary and 12 other files.

5.      In Visual C++, go to Project/Add Existing Item… and select main6.cpp (and holding control, continue to select), Sleep.h, Sleep.cpp, and GetGlut.h.

6.      Ensure that Solution Explorer is visible (View / Solution Explorer). In the Solution Explorer, select Tutorial6 (the project name) and right click it.  Choose Add / New Filter.  A new folder icon should appear in the Solution Explorer.  Name it ObjLibrary.  (This creates an imaginary folder in the project, but it would not create a Windows folder on the computer).

7.      In the Solution Explorer, right click on the ObjLibrary folder icon and select Add / Existing item.  Go into the ObjLibrary folder on Windows and select all files.  The files should appear in the Solution Explorer inside the ObjLibrary folder.  You should get 26 files.

8.      Compile and run the original program.  You should see 2 windows: a text console and a graphical window labeled “Local Camera Coordinates”.  There is a grid of coloured diamonds connected with lines across this second window.  These coloured diamonds are part of a hollow cube around the camera, which will help us keep track of camera orientation.  Exit the windows.  Closing the text window will close the graphical window, and usually vice versa.

9.      For the remainder of the instructions, it is assumed that you compile and run regularly so that you can see what each change does.

10.  We will be editing the file main.cpp.  You do not need to change OrientationCube.h or OrientationCube.cpp.

Representing the Camera Orientation Using Vector3s

11.  Add an #include for "ObjLibrary/Vector3.h"
#include "ObjLibrary/Vector3.h"
Add a
using directive for ObjLibrary after the #includes:
using namespace ObjLibrary;

12.  Declare 3 global variable (before the main function) of the Vector3 type:
Vector3 forward( 0,  0, -1);
Vector3 up     ( 0,  1,  0);
Vector3 right  ( 1,  0,  0);
This corresponds to the default OpenGL camera orientation.

13.  In the display function, change the call to gluLookAt to use the forward and up vectors for the look at point and up direction.  We will create a temporary variable to represent the point we want to look at.
  look_at = Vector3::ZERO + forward;
  gluLookAt(0.0,       0.0,       0.0,
            look_at.x, look_at.y, look_at.z,
            up.x,      up.y,      up.z);
Here,
Vector3::ZERO represents the origin.  There should be no visible effect.

14.  The right vector must be set to a specific value based on the forward and up vectors.  To calculate it automatically, change its declaration to:
Vector3 right = forward.crossProduct(up);

15.  Try setting the forward and up vectors to different values.  The vectors must be part of an orthonormal basis, which means they must have a norm (length) of 1.0 and be at right angles to each other.  If this condition is met, a correct value for right will be calculated automatically.  Changing the forward vector will change the direction of the camera, and you will see octahedrons of different colours.  Changing the up vector will change the rotation of the camera, and thus where the octahedrons are on the screen.  Some possibilities to try are:
// same but upside down
Vector3 forward( 0,  0, -1);

Vector3 up     ( 0, -1,  0);
// green octahedrons
Vector3 forward( 0,  1,  0);

Vector3 up     ( 0,  0,  1);
// green octahedrons rotated 90 degrees
Vector3 forward( 0,  1,  0);

Vector3 up     ( 1,  0,  0);
Setting the forward and up vectors to the same axis will result in a plain grey window.

16.  Change the forward and up vectors back to their original values:
Vector3 forward( 0,  0, -1);
Vector3 up     ( 0,  1,  0);

Rotating the Camera Using the Arrows

17.  In the special function under the GLUT_KEY_LEFT case in the switch statement, add a command to turn the camera to the local "left" by rotating it.  In order to rotate the camera around the up vector, so we will need to update the forward and right vectors.  They should each be rotated around the up vector by an angle of 0.1 radians:
  forward.rotateArbitrary(up, 0.1);
  right  .rotateArbitrary(up, 0.1);

18.  Add similar commands under the GLUT_KEY_RIGHT case to rotate the camera the to the right.  You will need one crucial change.

19.  Add commands under the GLUT_KEY_UP case to rotate the camera vertically.  For this, we will have to rotate the forward and up vectors around the right vector:
  forward.rotateArbitrary(right, 0.1);
  up     .rotateArbitrary(right, 0.1);

20.  Add similar commands under the GLUT_KEY_DOWN case to rotate the camera the down.  See if you can rotate the camera to look at about the same place it started, but rotated approximately 90 degrees.  You will not be able to rotate it exactly because 0.1 radians does not divide evenly into pi.

Moving the Camera

21.  Declare another global variable of the Vector3 type to represent the camera's position in space:
Vector3 position(0, 0, 0);

22.  Update the look_at variable in the display function.  The camera will move, and we always want to look at a position in front of it.  Such a position can be calculated as the sum of position and forward.
Vector3 look_at = position + forward;

23.  Modify the call to gluLookAt to place the camera at position:
  gluLookAt(position.x, position.y, position.z,
            look_at.x,  look_at.y,  look_at.z,
            up.x,       up.y,       up.z);

24.  In the keyboard function, add a case in the switch statement to move the camera in the forward direction when the spacebar is pressed.
  case ' ':
       position += forward * 0.5;
       break;

Pointing the Camera at the Origin

25.  If the camera gets outside of the orientation cube, the user can get lost.  To fix this, we will add a command to turn the camera to face the origin.  In the keyboard function, add a case to the switch statement for the [H] key.  The case should start by calculating a vector from the camera's current position to the origin.
  case 'h':
      
direction_to_origin = origin - position;
       break;

Declare the
origin and direction_to_origin variable outside the switch statement:
  Vector3 origin = Vector3::ZERO;
  Vector3 direction_to_origin;
just before the the
switch(special_key) statement.  Compilers sometimes generate errors if variables are declared inside case statements.

26.  Add a helper function named rotateCameraToVector.  It should take a vector representing the target facing direction as a parameter.  The purpose of this function is to rotate the camera so that the forward vector is lined up with the target vector.
void rotateCameraToVector(const Vector3& target_facing)
{
  // code will go here
}

Include a function prototype with the other prototypes near the top of the file.
void rotateCameraToVector(const Vector3& target_facing);

27.  If the target_facing vector is the zero vector, we do not have a direction, so we don't know which way to turn.  In that case, we will just do nothing.
  if(target_facing.isZero())
       return;

28.  Calculate the axis to rotate the camera around.  The needs to be at right angles to the camera's current forward vector and to the target_facing vector.  We can normally compute it as a cross product:
  Vector3 axis = forward.crossProduct(target_facing);

29.  The special cases occur if the forward vector is pointing exactly the same way or exactly the opposite way as the target_facing vector.  If that happens, there are an infinite number of equally-good axes, all at right angles to both vectors.  The formula represent this by setting axis to the zero vector.  We don't care which axis to use in that case, so we will just use the up vector, which will always be a possible axis when forward and target_facing are parallel..
  if(axis.isZero())
       axis = up;

30.  If, on the other hand, we did find an axis, we need to convert it to a normal vector, i.e. a vector of length 1.0:
  else
       axis.normalize();

A normal vector is also called a unit vector.

31.  Calculate the angle in radians to rotate the camera by.  This can be done using the getAngleSafe Vector3 function (which uses the dot product):
  double radians = forward.getAngleSafe(target_facing);
The
Safe variant of Vector3 functions will return a default value (1.0 here) if one of the vector parameters is a zero vector.  It is also slightly slower than the non-safe version.

32.  Now that we have our axis and our angle, we will rotate all three of our camera vectors around it:
  forward.rotateArbitrary(axis, radians);
  up     .rotateArbitrary(axis, radians);
  right  .rotateArbitrary(axis, radians);

33.  On the [H] key, call the rotateCameraToVector function with direction_to_origin as the paremter:
       rotateCameraToVector(direction_to_origin);

34.  You should be able to fly anywhere and then press [H] to be instantly facing the origin.

Smoothly Rotating the Camera

35.  Add a double parameter to the rotateCameraToVector function to represent the maximum angle in radians to rotate in one frame:
void rotateCameraToVector(const Vector3& target_facing,
                          double max_radians)

Remember to update the prototype:
void rotateCameraToVector(const Vector3& target_facing,
                          double max_radians);

Call the function with
0.1 radians as the maximum rotation angle:
       rotateCameraToVector(direction_to_origin, 0.1);

36.  Change the code that rotates the camera to rotate by an angle of max_radians instead radians.  Save the code that radians; we will need it later.
  forward.rotateArbitrary(axis, max_radians);
  up     .rotateArbitrary(axis, max_radians);
  right  .rotateArbitrary(axis, max_radians);

37.  Test the program.  When you hold [H], the camera should smoothly turn toward the origin and then flicker back and forth.  This is because the camera is already facing very close to the target facing direction.  Each frame it turns by max_radians, which is too far, so it ends up past the target facing direction.  The next frame it turns back, only to turn too far again.

38.  The solution here is to use the smaller of radians and max_radians, and the simpllest way to do this is to set one value to the smaller of the two.  Add a check just before you rotate the camera vectors to ensure that radians is not larger than max_radians:
  if(radians > max_radians)
       radians = max_radians;

39.  Change the camera rotations to rotate by the newly-modified radians value:
  forward.rotateArbitrary(axis, radians);
  up     .rotateArbitrary(axis, radians);
  right  .rotateArbitrary(axis, radians);

40.  Holding [H] should now smoothly rotate the camera to face the origin.

Exercises

41.  Add commands to the keyboard function to make the camera roll while facing the same direction, allowing the user to flip the camera onto its side or upside-down.  This requires rotating the up and right vectors around the forward vector.  Use the [,] and [.] keys.

42.  Add a command to the special function to restore the camera to its starting position and orientation.  Use the [END] key, which uses a constant named GLUT_KEY_END.

43.  Add a command to move the camera backwards on the [/] key.

44.  Add commands to strafe the camera left and right.  This means moving it sideways without turning it.  Use the [A] and [D] keys for left and right.

45.  Add commands to strafe the camera up and down on the [W] and [S] keys.

46.  Add a red sphere to the world somewhere outside the colour cube.  Make the camera rotate to face the red sphere on the [R] key.

47.  Convert your camera to a class named something like CoordinateSystem.  The global vectors will become member variables.  Add constructors and functions to use the class.  Modify your program to use this new class.  A possible list of functions would be:

·         Default constructor

·         Initializing constructor

·         Copy constructor

·         Destructor

·         Assignment operator

·         const Vector3& getPosition () const

·         const Vector3& getForward () const

·         const Vector3& getUp () const

·         Vector3 getRight () const

·         Void setupCamera () const

·         void init (const Vector3& position, const Vector3& forward, const Vector3& up)

·         void setPosition (const Vector3& position)

·         void moveForward (double distance)

·         void moveUp (double distance)

·         void moveRight (double distance)

·         void setOrientation (const Vector3& forward, const Vector3& up)

·         void rotateAroundForward (double radians)

·         void rotateAroundUp (double radians)

·         void rotateAroundRight (double radians)

·         void rotateToVector (const Vector3& target_forward, double max_radians)

48.  As noted above, the forward, up, and right vectors form an orthonormal basis for the coordinate system.  One implication of this is that they are always at right angles to each other, and any one can be calculated from the other two.  We need both the forward and up vectors to set the camera, but the right vector is only needed for a few calculations (tilting up and down and strafing right and left).  The value of the right vector can always be calculated as:
right = forward.crossProduct(up);
Remove the
right vector from your class.  When rotating around the right axis (up and down arrows), you will need to calculate a temporary right vector to rotate around.  Otherwise, the forward and up vectors will rotate around different axes.
Warning: It is very important that your
getRight function returns its result by value, not by constant reference.  If it returns the result by constant reference, the result (which is a local variable) may be overridden by a different value before it is used.  If this happens, it can cause very insidious bugs.