There's more than one way to wrap an interface. Templates make a particularly thin and useful wrapper.
When faced with a complex API such as OpenGL, and armed with a powerful language such as C++, it is tempting to simplify and build some kind of layer around the API to make it easier to use. In my previous article [1], I discussed encapsulation as one approach. In that article, I wrapped the details of making OpenGL work with Windows into a reusable window class (an MFC CWnd-derived class, to be precise). I did not attempt to wrap OpenGL itself, however, for several reasons. First, OpenGL is a large and standard language, and I did not want to force users to learn yet another interface. Second, OpenGL is very well-documented, and I could not compete with that amount of good documentation. Third, OpenGL was built to be fast. In 3-D graphics, speed is always a concern, so I felt adding an extra layer would be inappropriate. But there's more than one way to use C++ to simplify an API. In this article, I discuss two simple template classes that help while working with OpenGL. This article proves how high-octane C++ features like templates can be of considerable help in using a C API.
Vectors and Points
You can't go far in OpenGL without dealing with vectors and points; you must use them throughout your code. Vectors are used mainly to define surface normals, while points are used mainly to define polygon vertices. Vectors and points are fairly simple concepts, but to complicate matters, in 3-D languages you must use homogeneous coordinates.
The homogeneous coordinate system defines an extra coordinate with respect to the familiar Cartesian coordinate system. When homogeneous coordinates are used in 3-D space, an extra coordinate w is added to each triplet of Cartesian coordinates x, y, and z. When w is not zero, the point indicated by homogeneous coordinates (x, y, z, w) is the same as the point indicated by normal Cartesian coordinates (x/w, y/w, z/w), which is the same as the point indicated by homogeneous coordinates (x/w, y/w, z/w, 1). Each point can be represented by different homogeneous coordinate quadruplets, as long as they are multiples of each other. When w is zero (and at least one of x, y, or z is nonzero), the homogeneous coordinates indicate a point at infinity along the line that joins the origin (0, 0, 0) to Cartesian point (x, y, z).
The main advantage of using homogeneous coordinates is that translation transformations (as well as scaling and rotation transformations) can be treated as vector multiplications. This makes it possible to combine many transformations easily. All transformations and projections can be combined into a single operation, which is carried out by multiplying a 4x4 matrix to the homogeneous coordinates of each point (vertex).
If you are unfamiliar with homogeneous coordinates, I suggest you read Chapter 5 ("Geometrical Transformations") of Computer Graphics [2]. Appendix G of the OpenGL Programming Guide [3] also describes them briefly.
For the purposes of this article, a vector is just a directed line segment that joins two points in 3-D space. It has a length (magnitude) and a direction, but it does not have a position. You can define a vector with three orthogonal components: x, y, and z.
Points and vectors have some useful relationships. A point minus a point yields a vector. A point plus a vector yields a point. Vectors can also be added, subtracted, or multiplied. (Multiplication can be defined as yielding either a cross or dot product.)
Creating point and vector classes enables you to build C++ operators that implement the relationships between points and vectors. But you cannot use simple C++ classes, or you would have to build one for each type of parameter that the OpenGL functions take. OpenGL lets you use many types; in fact, most OpenGL functions come in many forms. For instance, the glVertex function, which lets you set the vertex of a polygon, comes in 24 forms (see [4]), among which are:
glVertex3f, glVertex4d, glVertex3dv, glVertex4fvThe 3 and 4 indicate the number of parameters (3 for Cartesian coordinates, 4 for homogeneous coordinates). f indicates type GLfloat. d indicates GLdouble. v indicates that the parameters lie in a vector (here, a C-style array). This is why I use template classes.
In standard OpenGL, you identify vectors and points with either separate arguments for each coordinate:
glVertex3f(1.0F, 2.0F, 3.0F);or with C arrays of numbers:
GLfloat V[3]= { 1.0F, 2.0F, 3.0F }; glVertex3fv(V);Naturally, C++ can give you vector and point classes that are semantically much closer to the vector and point concepts. These classes also encapsulate the necessary data and provide useful operations you'll often need when dealing with these concepts.
A Point Template Class
Figure 1 shows the GLpoint4 template class, which is fairly straightforward. This class has four numeric data members, x, y, z, and w. (Yes, I know they are public; I think they should be in this case, as this is an example of those "the implementation is the interface" cases.) The Normalize member function normalizes the homogeneous coordinates by dividing each coordinate by w.
The only member I feel requires explanation is:
operator const Numeric * () const { return &x; }This operator allows you to use the OpenGL API with GLpoint4 classes directly, without adding many new functions that take GLpoint4 as a parameter. Consider the following example:
GLpoint4<GLfloat> P(1.0F,2.0F,3.0F); glVertex3fv(P);glVertex3fv expects an array of three GLfloat values, and the above conversion operator provides such an array, returning the address of the x-coordinate of the point. Since the y, z, and w coordinates follow in the class declaration, the memory layout should be like the one of a C array of Numeric (here float) objects. To ensure this, I added a STATIC_ASSERT macro to the conversion operator described above (and I added a similar one in class GLvector3). I implemented STATIC_ASSERT using Andrei Alexandrescu's static_checker template [6]. If, due to some unfortunate trick of your platform, the memory layout of the contiguous class members x, y, z, and w is not the same as the layout of array members of the same type, the STATIC_ASSERT will produce a compile-time error. If it happens, you might want to fiddle with your compiler's packing strategy by using #pragmas or compiler switches.
Figure 1 shows the definition and use of STATIC_ASSERT. Note that the expression within STATIC_ASSERT is evaluated at compile time, not run time, so I leave STATIC_ASSERT defined also in release builds (this is particularly important since the compiler's packing directives are often different in release builds). Also note that although the w data member follows z, and is initialized to the value 1.0, it is not used in the function glVertex3fv.
The 4 in the GLpoint4 class name indicates that this class contains four data members, and that the conversion operator above returns an array of four numbers. Many OpenGL functions take arrays of three numbers to indicate points (Cartesian coordinates, not homogeneous). In this case, you can still use GLpoint4 as long as you know that the fourth data member, w, is 1.0. w will be 1.0 unless you've forced it to something different. On the other hand, you might want to define a similar class, GLpoint3, to represent points with just three normal Cartesian coordinates. For a discussion of this option, see the "Customizations and Extensions" section near the end of this article.
A Vector Template Class
Figure 2 shows the GLvector3 template class. Like GLpoint4, it is also fairly straightforward. It has three data members, x, y, and z, which represent the three components of a 3-D vector. The Normalize member function turns the vector into a unit vector (a vector that points in the same direction but whose length equals one), and GetLength returns the length of the vector. Like the point class, the vector class defines a function to provide direct access to its data members:
operator const Numeric * () const { return &x; }This function returns the address of the x coordinate, which is followed in the class declaration by y and z, thus resembling an array of three variables in memory. It lets you write code like this:
GLvector3<GLfloat> V(1.0F, 2.0F, 3.0F); glNormal3fv(V);which should be almost as efficient as using straight C arrays, since the operator above is inline.
Other members worth mentioning are the constructor that takes an array of numbers:
GLvector3(const Numeric * v) :x(v[0]), y(v[1]), z(v[2]){}and the corresponding assignment operator:
GLvector3 & operator=(const Numeric * v) { x=v[0]; y=v[1]; z=v[2]; return *this; }which provide a conversion from C arrays to GLvector3 objects, giving symmetry to this class's interaction with C arrays. (GLpoint4 also has a similar constructor and assignment operator.) Note that the copy constructor and copy operator are missing from my class declarations, as the ones generated by the compiler work correctly and are much faster.
Operator Functions
As you can see, these classes are very simple. They encapsulate vectors and points in a way that integrates very well with the existing OpenGL API, and they should be fairly efficient as well. But where the classes really shine is in a few simple operator (and some non-operator) functions. These functions are shown in Figure 3.
These functions implement the following useful relationships:
- A vector plus a vector is a resultant vector.
- A vector minus a vector is a vector.
- A point plus a vector is a point.
- A vector multiplied by a vector is the cross product vector.
- The dot product between two vectors is a number.
- A point minus a point is a vector.
These operators can make your life much easier in many situations, especially when defining normals.
The following is a code snippet to draw a triangle with its normal, which is necessary if you want to use lighting effects.
GLpoint4<GLfloat> P1(1.0f, 2.0f, 3.0f); GLpoint4<GLfloat> P2(2.0f, 3.0f, 1.0f); GLpoint4<GLfloat> P3(3.0f, 1.0f, 2.0f); glBegin(GL_TRIANGLES); // Note following line glNormal3fv((P2-P1)*(P3-P1)); glVertex3fv(P1); glVertex3fv(P2); glVertex3fv(P3); glEnd();The line with the call to glNormal3fv is the most interesting one. The normal of a triangle can be obtained by taking the cross product between any two vectors parallel to its sides. (P2-P1) is one of these vectors, and (P3-P1) is another. This code first uses the operator that defines subtraction between two points. Then the two resulting vectors are multiplied, giving the cross product, which is another vector. This multiplication shows operator* (between vectors) at work. Then the conversion operator returns the address of the array containing the vector coordinates, which represent the normal. I find the above code both readable and concise, though I would not bet my life on its being the fastest possible solution.
Note that this example assumes GL_NORMALIZE is enabled. When this mode is enabled in OpenGL, vectors specified as normals are automatically converted to unit vectors. It is also important to know that the operator- for two points will throw a runtime_error exception if either of the two points is at infinity (i.e., if w == 0).
Using GLpoint4 and GLvector3
Using these classes is easy; all you have to do is #include "OGLUtil.h" in your source files. There is no implementation file. The OpenGL headers will be included automatically, and if you use my COpenGLCtrl and COpenGLCtrls classes [1] they will include the necessary libraries and take care of all the Windows-specific details of OpenGL, as well. Although I use Windows and MFC, the GLpoint4 and GLvector3 classes should work on other platforms, perhaps with minor modifications.
CUJ's ftp site includes the source code for the sample application SAMPLE3D from my previous article [1], modified to use these classes (see p. 3 for downloading instructions). Figure 4 illustrates the function that draws the shaded pyramid from that sample application, and Figure 5 shows the result.
Caveats
Like most software, these classes are not perfect. First of all, they use a type conversion operator and a single argument constructor in order to allow conversions to and from C arrays. These types of conversions are dangerous (see [5]), but in this case I could not resist their appeal. After all, my classes are just another way to look at C arrays; that's basically the point of using them. If you can think of situations in which they mess things up, let me know. You might also dislike my use of public variables and my trick when returning the vector containing them. In this case, on CUJ's ftp site you will find a version of the classes that you will probably prefer: it has no public variables, no automatic conversion operators, and no trick when returning the vector (so no need for STATIC_ASSERT). I find it less convenient to use, though, and I prefer the version described in this article.
Another thing to watch out for is that OpenGL functions don't statically check the types of these classes. There is no reason you couldn't pass a GLvector3<GLfloat> object to glVertex4fv, which should really take a GLpoint4<GLfloat>. Nor will OpenGL check the array size; that size is included in the class name to remind you, but you could write code like this:
GLfloat A[2]= { 1.0F, 2.0F }; GLvector3 V(A); // Danger! GlNormal3fv(V);The GLvector3 constructor will try to access three elements in an array that contains only two. But you are in no worse position with these classes than you are without them, as these problems are inherent in OpenGL's C interface. To solve these problems, would require encapsulating OpenGL, but that is another story.
Customizations and Extensions
There are various ways to customize these classes for your own use. If you don't use glEnable(GL_NORMALIZE) in your OpenGL code, you might want to define the operator* between two vectors to return the normalized cross product instead of the cross product. I doubt this would be the most efficient and neatest solution, but it's an option. Otherwise, you can use the function NormalizedCrossProduct (defined in Figure 3) when necessary, like this:
glNormal3fv(NormalizedCrossProduct(P2-P1,P3-P2));This is not as concise, but is just as readable.
To optimize the speed of these classes, you might try to use memcpy and memset instead of copying and assigning the individual data members in many member functions. But you'd have to profile your code to make sure that the speed you gain (if any) is worth the readability you loose.
You can use similar classes to represent other concepts in OpenGL land color, for instance. Colors are also arrays of numbers, either RGB triplets (red, green, and blue factors) or RGBA quadruplets (red, green, blue, and alpha factors). I don't know what operators are meaningful with colors (can you add and subtract colors?), but I am sure such a class would be a very convenient place to put many functions that work on colors. The usual conversion operators will provide the resulting color factors to the good old OpenGL API functions.
Class GLpoint4 might also be useful when rendering surfaces hardly a rare situation with OpenGL. In these cases, you will usually need to store the coordinates of many points. You can use the STL container classes (or any other containers you prefer) to hold instances of class GLpoint4. These containers, in turn, hold the actual coordinates, and the previously described operators will help when passing these points (or vectors derived from them) to OpenGL functions.
As previously mentioned, some OpenGL functions take arrays of three elements, but the GLpoint4 class provides four. It is possible to create a GLpoint3 class, but you will have to redefine many operators to work with both GLpoint3 and GLpoint4, and perhaps with combinations of the two. Another alternative is to derive GLpoint4 from GLpoint3. However, to make this work nicely you'd have to introduce virtual functions, and this might not be the place where you want to pay for the overhead. Besides, it is incorrect to say that a homogeneous point "is a" normal Cartesian coordinate point (in fact, the opposite is true). Points at infinity cannot be represented by normal Cartesian coordinates, so therefore inheritance might not be the best solution. I didn't think it was worth the trouble to define a GLpoint3 class, so I did not do it for my own uses. I just use GLpoint4.
Conclusion
The presented classes are very simple, fairly efficient, and solve just one simple problem, but in my opinion really show the beauty of C++. I use them mainly when storing surfaces in containers and when defining surface normals. You can, of course, use them in many more places. You can also extend them with other operators/functions you often use on points and vectors, and you can create new classes that integrate in the same way with OpenGL (or other APIs, for that matter).
Acknowledgments
My sincere thanks to Andrei Alexandrescu for his stimulating and honest criticism, his sharp and insightful advice, and his kind willingness to share ideas, some of which I used in this article.
References
[1] Giovanni Bavestrelli. "An OpenGL Wrapper for Win32," C/C++ Users Journal, December 1998.
[2] van Dam Foley, et al. Computer Graphics, 2nd Ed. (Addison-Wesley, 1990).
[3] Neider, Davis, and Woo. OpenGL Programming Guide (Addison-Wesley, 1993).
[4] OpenGL Architecture Review Board. OpenGL Reference Manual (Addison-Wesley, 1992).
[5] Scott Meyers. More Effective C++ (Addison-Wesley 1996), Item 5.
[6] Andrei Alexandrescu. "Adapting Automation Arrays to the Standard Vector Interface," C/C++ Users Journal, April 1999, page 26.
Giovanni Bavestrelli lives in Milan and is a software engineer for Techint S.p.A., Castellanza, Italy. He has a degree in Electronic Engineering from the Politecnico di Milano, and writes automation software for Pomini Roll Grinding machines. He has been working in C++ under Windows since 1992, specializing in the development of reusable object-oriented libraries. He can be reached at giovanni.bavestrelli@pomini.it.