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