

Let's start with the basics: points, vectors, rays, and planes. These are basic classes that have nothing to do with OpenGL.
// Classes and utility functions for three-dimensional Points, Vectors,
// Planes, and Rays.
#ifndef GEOMETRY_H_
#define GEOMETRY_H_
#include <cmath>
class Point;
class Vector;
class Plane;
class Ray;
// Comparing doubles for equality is useless; allow clients to supply a
// tolerance.
inline bool equal(double x, double y, double epsilon = 0.000001) {
return fabs(x - y) <= epsilon;
}
// A class for 3-D Vectors.
//
// v.i, v.j, v.k Components of vector v
// Vector(i, j, k) Construct from components
// Vector(p) Construct from a point
// u + v, u += v Vector addition
// u - v, u -= v Vector subtraction
// -v <0, 0, 0> - v
// u.dot(v) Dot product of u and v
// u.cross(v) Cross product of u and v
// v * c, c * v, v *= c Multiplication of a vector and a scalar
// v / c, v /= c Division of a vector by a scalar
// v.magnitude() The length of v
// unit(v) The vector of length 1 in the direction of v
// normalize(v) Changes v to unit(v)
// cosine(u, v) The cosine of the angle from u to v
// u.isPerpendicularTo(v) Whether u is almost perpendicular to v
// u.isParallelTo(v) Whether u is almost parallel to v
// u.projectionOnto(v) The projection of u onto v
// u.reflectionAbout(v) The mirror image of u over v
class Vector {
public:
double i, j, k;
Vector(double i = 0, double j = 0, double k = 0): i(i), j(j), k(k) {}
Vector(Point p);
Vector operator +(Vector v) {return Vector(i + v.i, j + v.j, k + v.k);}
Vector& operator +=(Vector v) {i += v.i; j += v.j; k += v.k; return *this;}
Vector operator -(Vector v) {return Vector(i - v.i, j - v.j, k - v.k);}
Vector& operator -=(Vector v) {i -= v.i; j -= v.j; k -= v.k; return *this;}
Vector operator -() {return Vector(-i, -j, -k);}
double dot(Vector v) {return i * v.i + j * v.j + k * v.k;}
Vector cross(Vector);
Vector operator *(double c) {return Vector(i * c, j * c, k * c);}
friend Vector operator *(double c, Vector v) {return v * c;}
Vector& operator *=(Vector v) {i *= v.i; j *= v.j; k *= v.k; return *this;}
Vector operator /(double c) {return Vector(i / c, j / c, k / c);}
Vector& operator /=(double c) {i /= c; j /= c; k /= c; return *this;}
double magnitude() {return sqrt(this->dot(*this));}
friend Vector unit(Vector v) {return v / v.magnitude();}
friend void normalize(Vector& v) {v /= v.magnitude();}
friend double cosine(Vector u, Vector v) {return unit(u).dot(unit(v));}
bool isPerpendicularTo(Vector v) {return equal(this->dot(v), 0);}
bool isParallelTo(Vector v) {return equal(cosine(*this, v), 1.0);}
Vector projectionOnto(Vector v) {return this->dot(unit(v)) * unit(v);}
Vector reflectionAbout(Vector v) {return 2 * projectionOnto(v) - *this;}
};
// A class for 3-D Points.
//
// p.x, p.y, p.z Components (coordinates) of point p
// p + v, p += v Add a point to a vector
// p - q The vector from q to p
// p.distanceTo(q) The distance between p and q
// p.distanceTo(P) The distance between p and the plane P
class Point {
public:
double x, y, z;
Point(double x = 0, double y = 0, double z = 0): x(x), y(y), z(z) {}
Point operator +(Vector v) {return Point(x + v.i, y + v.j, z + v.k);}
Point& operator +=(Vector v) {x += v.i; y += v.j; z += v.k; return *this;}
Vector operator -(Point p) {return Vector(x - p.x, y - p.y, z - p.z);}
double distanceTo(Point p) {return (p - *this).magnitude();}
double distanceTo(Plane);
};
// A class for 3-D planes.
//
// P.a, P.b, P.c, P.d The components of plane P (P is the set of
// all points (x, y, z) for which P.a * x +
// P.b * y + P.c * z + P.d = 0)
// Plane(a, b, c d) Construct from components
// Plane(p1, p2, p3) Construct by giving three points on the plane
// (may fail if the points are collinear): the
// plane's normal is obtained by a right hand
// rule: curl your right hand ccw around p1 to
// p2 to p3 then your thumb orients the normal
// P.normal() The vector <P.a, P.b, P.c>
class Plane {
public:
double a, b, c, d;
Plane(double a = 0, double b = 0, double c = 1, double d = 0);
Plane(Point p1, Point p2, Point p3);
Vector normal() {return Vector(a, b, c);}
};
// A class for 3-D rays.
//
// r.origin, r.direction The components of the ray r
// Ray(origin, direction) Construct from components
// r(u) The point on r at distance u * |r.direction|
// from r.origin.
class Ray {
public:
Point origin;
Vector direction;
Ray(Point origin, Vector direction): origin(origin), direction(direction) {}
Point operator()(double u) {return origin + u * direction;}
};
// Bodies of inlined operations.
inline Vector::Vector(Point p): i(p.x), j(p.y), k(p.z) {
}
inline Vector Vector::cross(Vector v) {
return Vector(j * v.k - k * v.j, k * v.i - i * v.k, i * v.j - j * v.i);
}
inline Plane::Plane(double a, double b, double c, double d):
a(a), b(b), c(c), d(d)
{
}
inline double Point::distanceTo(Plane P) {
return fabs(P.a * x + P.b * y + P.c * z + P.d) / P.normal().magnitude();
}
inline Plane::Plane(Point p1, Point p2, Point p3) {
Vector n = (p2 - p1).cross(p3 - p1);
a = n.i;
b = n.j;
c = n.k;
d = -(Vector(p1).dot(n));
}
#endif
Here's a C++ spaceship class. The ship is represented with a position, orientation, and current speed. It is also completely independent of OpenGL.
// A simplified spaceship that you can use in flybys, flight simulators and
// other 3D programs. A ship has a position, an orientation and a speed.
// The position is simply a point. The orientation is given by three
// UNIT vectors: (1) forward, the vector along which the ship is currently
// moving, (2) up, a vector perpendicular to forward that describes which
// direction is "up" to someone sitting in the ship, and (3) right, which
// is really just the cross product of forward and up that we store to
// simplify most of the calculations. The speed of the ship is simply the
// distance that the ship moves every time fly() is called.
//
// The public members of the Ship class are:
//
// Ship() initialize the ship so that it is located at the
// origin, is travelling in the direction <0, 0, -1>
// that is, along the -z axis, has up vector <0, 1, 0>
// (and therefore right = <1, 0, 0>) and has initial
// speed 0.01.
// getPosition() return the current position of the ship.
// teleport(p) move the ship to absolute position p, but preserve
// the orientation and speed.
// getDirection() return the current direction of the ship.
// getVertical() return the "up" vector of the ship.
// pitch(theta) reorient the ship so that it is rising theta radians;
// technically, rotate forward and up theta radians
// around right.
// roll(theta) rotate up and right theta radians around forward.
// yaw(theta) rotate forward and right theta radians around up.
// getSpeed() return the current speed.
// setSpeed(s) set the current speed to s.
#ifndef SHIP_H_
#define SHIP_H_
#include "geometry.h"
class Ship {
Point position;
Vector forward, up, right;
double speed;
public:
Ship(Point initialPosition = Point(0, 0, 0));
Point getPosition() {return position;}
void fly() {position += speed * forward;}
void teleport(Point newPosition) {position = newPosition;}
Vector getDirection() {return forward;}
Vector getVertical() {return up;}
void pitch(double angle);
void roll(double angle);
void yaw(double angle);
double getSpeed() {return speed;}
void setSpeed(double newSpeed) {speed = newSpeed;}
};
inline Ship::Ship(Point initialPosition):
position(initialPosition),
forward(0, 0, -1),
up(0, 1, 0),
right(1, 0, 0),
speed(0.01)
{
}
inline void Ship::pitch(double angle) {
forward = unit(forward * cos(angle) + up * sin(angle));
up = right.cross(forward);
}
inline void Ship::roll(double angle) {
right = unit(right * cos(angle) + up * sin(angle));
up = right.cross(forward);
}
inline void Ship::yaw(double angle) {
right = unit(right * cos(angle) + forward * sin(angle));
forward = up.cross(right);
}
#endif
Here's a fractal landscape class. It is OpenGL-dependent, though it should probably be refactored into a non-OpenGL class and an OpenGL-dependent wrapper.
// A simple landscape class. A landscape is essentially an elevation grid
// which can be drawn in wireframe or as a solid. Landscape elevations are
// set using the Random Midpoint Displacement technique.
//
// Landscape(m, n) (constructor) sets the x and z bounds of the landscape
// so that the constructed object will be a set of points
// (x, y, z) where x and z are integers and 0 <= x < m
// and 0 <= z < n, and y is undefined.
// create(rug) assigns random elevations based on the ruggedness
// coefficient rug (rug == 0 defines the completely
// flat grid). This routine also generates OpenGL
// display lists for efficient drawing.
// draw() draws the landscape using a fixed coloring scheme.
// drawWireFrame() draws a wireframe representation of the landscape.
#ifndef LANDSCAPE_H_
#define LANDSCAPE_H_
#ifdef __APPLE_CC__
#include <GLUT/glut.h>
#else
#include <GL/glut.h>
#endif
#include <vector>
#include <cmath>
#include <cstdlib>
class Landscape {
int rows;
int columns;
double highest; // the highest point in the mesh
std::vector< std::vector<double> > d; // the grid of elevations
int solidId; // display list id for solid mesh
int wireFrameId; // display list id for wire mesh
static double unused;
void generate(int x1, int y1, int x2, int y2, double rug);
double scale(double x) {return x * (((double)rand()/RAND_MAX) - 0.5);}
void drawTriangle(int x1, int z1, int x2, int z2, int x3, int z3);
void vertex(double x, double z);
void createSolidDisplayList();
void createWireFrameDisplayList();
public:
Landscape(int rows, int columns);
void create(double rug);
void draw() {glCallList(solidId);}
void drawWireFrame() {glCallList(wireFrameId);}
};
#endif
// Implementation of the Landscape class.
#include "landscape.h"
#include "geometry.h"
GLfloat blue[] = {0.0, 0.0, 1.0, 1.0};
GLfloat green[] = {0.0, 1.0, 0.0, 1.0};
GLfloat darkGreen[] = {0.0, 0.5, 0.0, 1.0};
GLfloat brown[] = {0.5, 0.5, 0.0, 1.0};
GLfloat lightBrown[] = {0.7, 0.7, 0.3, 1.0};
GLfloat gray[] = {0.6, 0.6, 0.6, 1.0};
GLfloat forest[] = {0.4, 0.8, 0.5, 1.0};
GLfloat white[] = {1.0, 1.0, 1.0, 1.0};
// Hack.
double Landscape::unused = -10032.4775;
// Constructor. Ensure the matrix is loaded with the unused constant in
// every cell.
Landscape::Landscape(int r, int c): rows(r), columns(c) {
std::vector<double> nullRow(columns, unused);
std::vector< std::vector<double> > nullMatrix(rows, nullRow);
d = nullMatrix;
}
// create() sets the elevations of the four grid corners to 0, generates
// internal elevations remembering the highest point, then generates display
// lists.
void Landscape::create(double rug) {
int r, c;
// First put zeros for the elevations around the whole boundary:
for (r = 0; r < rows; r++) d[r][0] = d[r][columns-1] = 0;
for (c = 0; c < columns; c++) d[0][c] = d[rows-1][c] = 0;
// Then put zeros in the corners inset two units and generate
// a fractal landscape in that rectangle.
d[2][2] = d[2][columns-3] = d[rows-3][2] = d[rows-3][columns-3] = 0;
generate(2, 2, rows - 3, columns - 3, rug);
// Then smooth out the inner fractal so it meets the zeroed out
// edges. Make the part just outside the fractal one-third higher
// so it simulates flatter beaches.
for (r = 2; r < rows - 2; r++) d[r][1] = d[r][2] / 3.0;
for (r = 2; r < rows - 2; r++) d[r][c-2] = d[r][c-3] / 3.0;
for (c = 1; c < columns-1; c++) d[1][c] = d[2][c] / 3.0;
for (c = 1; c < columns-1; c++) d[r-2][c] = d[r-3][c] / 3.0;
// Finally it part of the land is underwater make that elevation 0.
highest = 0.0;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < columns; j++) {
if (d[i][j] < 0) d[i][j] = 0;
if (d[i][j] > highest) highest = d[i][j];
}
}
// Generate the display lists.
solidId = glGenLists(2);
wireFrameId = solidId + 1;
createSolidDisplayList();
createWireFrameDisplayList();
}
// Simple math for random midpoint displacement.
void Landscape::generate(int x1, int y1, int x2, int y2, double rug) {
int x3 = (x1 + x2) / 2;
int y3 = (y1 + y2) / 2;
if (y3 < y2) {
if (d[x1][y3] == unused) {
d[x1][y3] = (d[x1][y1] + d[x1][y2])/2 + scale(rug*(y2-y1));
}
d[x2][y3] = (d[x2][y1] + d[x2][y2])/2 + scale(rug*(y2-y1));
}
if (x3 < x2) {
if (d[x3][y1] == unused) {
d[x3][y1] = (d[x1][y1] + d[x2][y1])/2 + scale(rug*(x2-x1));
}
d[x3][y2] = (d[x1][y2] + d[x2][y2])/2 + scale(rug*(x2-x1));
}
if (x3 < x2 && y3 < y2) {
d[x3][y3] = (d[x1][y1] + d[x2][y1] + d[x1][y2] + d[x2][y2])/4
+ scale(rug * (fabs((double)(x2 - x1)) + fabs((double)(y2 - y1))));
}
if (x3 < x2 - 1 || y3 < y2 - 1) {
generate(x1, y1, x3, y3, rug);
generate(x1, y3, x3, y2, rug);
generate(x3, y1, x2, y3, rug);
generate(x3, y3, x2, y2, rug);
}
}
// We assign colors to points so that blue is the water, then going up
// you get green (grassland), brown (mountain) then white (snowcaps).
void Landscape::vertex(double x, double z) {
double y = d[(int)x][(int)z];
double h = y / highest;
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE,
(h < 0.03) ? forest :
(h < 0.2) ? green :
(h < 0.32) ? darkGreen :
(h < 0.45) ? brown :
(h < 0.7) ? lightBrown :
(h < 0.8) ? gray : white);
glVertex3f(x, y, z);
}
void Landscape::drawTriangle(int x1, int z1, int x2, int z2, int x3, int z3) {
Point p[] = {Point(x1, d[x1][z1], z1),
Point(x2, d[x2][z2], z2),
Point(x3, d[x3][z3], z3)};
Vector normal = unit(Plane(p[0], p[1], p[2]).normal());
//if (normal.j < 0.0) normal = -normal;
double h = 0.005;
if (d[x1][z1] < h && d[x2][z2] < h && d[x3][z3] < h) {
glMaterialfv(GL_FRONT, GL_AMBIENT_AND_DIFFUSE, blue);
glNormal3d(0, 1.0, 0);
glVertex3dv((GLdouble*)&p[0]);
glVertex3dv((GLdouble*)&p[1]);
glVertex3dv((GLdouble*)&p[2]);
}
else {
glNormal3dv((GLdouble*)&normal);
vertex(x1, z1);
vertex(x2, z2);
vertex(x3, z3);
}
}
// Drawing the landscape as a solid is fairly simple using GL_TRIANGLE_STRIP.
// We generate a display list with a triangle strip on each row.
void Landscape::createSolidDisplayList() {
glNewList(solidId, GL_COMPILE);
glEnable(GL_LIGHTING);
glBegin(GL_QUADS);
glVertex3f(-200, 0, columns+200);
glVertex3f(-200, 0, -200);
glVertex3f(rows+200, 0, -200);
glVertex3f(rows+200, 0, columns+200);
glEnd();
glBegin(GL_TRIANGLES);
for (int x = 0; x < rows - 1; x++) {
for (int z = 0; z < columns - 1; z++) {
drawTriangle(x, z, x+1, z, x, z+1);
drawTriangle(x+1, z, x+1, z+1, x, z+1);
}
}
glEnd();
glEndList();
}
// Generating a display list for a wire frame representation of the
// landscape is straightforward though a little tedious.
void Landscape::createWireFrameDisplayList() {
glNewList(wireFrameId, GL_COMPILE);
glDisable(GL_LIGHTING);
glColor3f(1.0, 1.0, 1.0);
int x, z;
for (x = 0; x < rows; x++) {
glBegin(GL_LINE_STRIP);
glVertex3f(x, d[x][0], 0);
for (z = 1; z < columns; z++) {
glVertex3f(x, d[x][z], z);
}
glEnd();
}
for (z = 0; z < columns; z++) {
glBegin(GL_LINE_STRIP);
glVertex3f(0, d[0][z], z);
for (x = 1; x < rows; x++) {
glVertex3f(x, d[x][z], z);
}
glEnd();
}
glEndList();
}
Here's a GLUT-based app. Fly with the keyboard. Good homework assignments include creating a joystick or spaceball UI, and creating an instrument panel.
// This program is a trivial little flight simulator. You control a ship
// with the keyboard. Use
//
// J and L keys to roll,
// I and K keys to pitch,
// H and ; keys to yaw,
// 8 key to increase speed and the M key to decrease speed.
// W key toggles wireframe mode
// R key generates a new landscape
//
// In this little program you fly around a single fractal landscape. It would
// be best to extend the program so that one could plug in any arbitrary
// scene.
#ifdef __APPLE_CC__
#include <GLUT/glut.h>
#else
#include <GL/glut.h>
#endif
#include "cockpit.h"
#include "landscape.h"
#include <iostream>
// A landscape to fly around, with some parameters that are manipuated by the
// program.
Landscape landscape(200, 143);
// Wireframe view or solid view?
static bool wireframe = false;
void newLandscape() {
static double rug = ((double)rand()) / RAND_MAX;
landscape.create(rug);
}
// A ship and some functions to control it: Later, we need to add a ship
// controller class so even the navigation controls are pluggable.
static Ship theShip(Point(60, 40, 220));
static Cockpit cockpit(theShip);
void keyboard(unsigned char key, int, int) {
const double deltaSpeed = 0.01;
const double angle = 0.02;
switch(key) {
case '8': theShip.setSpeed(theShip.getSpeed() + deltaSpeed); break;
case 'm': theShip.setSpeed(theShip.getSpeed() - deltaSpeed); break;
case 'w': wireframe = !wireframe; break;
case 'r': newLandscape();
case 'j': theShip.roll(angle); break;
case 'l': theShip.roll(-angle); break;
case 'h': theShip.yaw(angle); break;
case ';': theShip.yaw(-angle); break;
case 'i': theShip.pitch(-angle); break;
case 'k': theShip.pitch(angle); break;
}
}
// Display and Animation: To draw we just clear the window and draw the scene.
// Because our main window is double buffered we have to swap the buffers to
// make the drawing visible. Animation is achieved by successively moving
// the ship and drawing.
void display() {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
wireframe ? landscape.drawWireFrame() : landscape.draw();
cockpit.draw();
glFlush();
glutSwapBuffers();
}
// Move the ship one step, recompute the view, and ask to redisplay.
void timer(int v) {
theShip.fly();
Point eye(theShip.getPosition());
Point at(theShip.getPosition() + theShip.getDirection());
Vector up(theShip.getVertical());
glLoadIdentity();
gluLookAt(eye.x, eye.y, eye.z, at.x, at.y, at.z, up.i, up.j, up.k);
glutPostRedisplay();
glutTimerFunc(1000/60, timer, v);
}
// Reshape callback: Make the viewport take up the whole window, recompute the
// camera settings to match the new window shape, and go back to modelview
// matrix mode.
void reshape(int w, int h) {
glViewport(0, 0, w, h);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60.0, (GLfloat)w/(GLfloat)h, 0.05, 300.0);
glMatrixMode(GL_MODELVIEW);
}
// init(): Initialize GLUT and enter the GLUT event loop.
void init() {
srand(9903);
glEnable(GL_DEPTH_TEST);
newLandscape();
cockpit.create();
GLfloat black[] = { 0.0, 0.0, 0.0, 1.0 };
GLfloat dark[] = { 0.2, 0.15, 0.2, 1.0 };
GLfloat white[] = { 1.0, 1.0, 1.0, 1.0 };
GLfloat direction[] = { 0.2, 1.0, 0.5, 0.0 };
glMaterialfv(GL_FRONT, GL_SPECULAR, white);
glMaterialf(GL_FRONT, GL_SHININESS, 30);
glLightfv(GL_LIGHT0, GL_AMBIENT, dark);
glLightfv(GL_LIGHT0, GL_DIFFUSE, white);
glLightfv(GL_LIGHT0, GL_SPECULAR, white);
glLightfv(GL_LIGHT0, GL_POSITION, direction);
glEnable(GL_LIGHTING); // so the renderer considers light
glEnable(GL_LIGHT0); // turn LIGHT0 on
}
// Writes some trivial help text to the console.
void writeHelpToConsole() {
std::cout << "j/l = roll left / right\n";
std::cout << "i/k - pitch down / up\n";
std::cout << "h/; - yaw left / right\n";
std::cout << "8/m - speed up / slow down\n";
std::cout << "w - toggle wireframe mode\n";
std::cout << "r - generate a new landscape\n";
}
// main(): Initialize GLUT and enter the GLUT event loop.
int main(int argc, char** argv) {
writeHelpToConsole();
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGB | GLUT_DEPTH);
glutInitWindowPosition(80, 80);
glutInitWindowSize(780, 500);
glutCreateWindow("Simple Flight");
glutReshapeFunc(reshape);
glutTimerFunc(100, timer, 0);
glutDisplayFunc(display);
glutKeyboardFunc(keyboard);
init();
glutMainLoop();
}
Note that this simulator comes with a lame cockpit module. It's just blank for now, but you will have a homework assignment to put instruments on it.
// An OpenGL display of a ship's cockpit.
#ifndef COCKPIT_H_
#define COCKPIT_H_
#ifdef __APPLE_CC__
#include <GLUT/glut.h>
#else
#include <GL/glut.h>
#endif
#include "ship.h"
class Cockpit {
Ship ship;
int cockpitId;
public:
Cockpit(Ship ship): ship(ship) {}
void create();
void draw();
};
void Cockpit::create() {
cockpitId = glGenLists(1);
glNewList(cockpitId, GL_COMPILE);
glDisable(GL_LIGHTING);
glColor3f(0.8, 0.8, 0.7);
glBegin(GL_TRIANGLE_FAN);
glVertex3f(0, -1, 0);
glVertex3f(1, -1, 0);
for (double x = 1.0; x >= -1.05; x -= 0.05) {
glVertex3f(x, 20*cos(x / 10.0) - 20.6, 0);
}
glVertex3f(-1, -1, 0);
glEnd();
glEnable(GL_LIGHTING);
glEndList();
}
inline void Cockpit::draw() {
glMatrixMode(GL_MODELVIEW);
glPushMatrix();
glLoadIdentity();
glMatrixMode(GL_PROJECTION);
glPushMatrix();
glLoadIdentity();
glCallList(cockpitId);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
glPopMatrix();
}
#endif