Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

Parallel

Programming with OpenGL Primitives


SP 96: Programming with OpenGL Primitives

Ron runs Data Visualization, a software consulting group specializing in data exploration and visualizing techniques. He is the author of the forthcoming book OpenGL for Windows 95 and Windows NT (Addison-Wesley). He can be contacted at [email protected].


OpenGL is a graphics API that provides portable, hardware-assisted, 3-D rendering. OpenGL includes the basic building blocks for creating objects that are limited only by your imagination (and your programming ability). It's a powerful graphics interface for creating effects such as hidden-surface removal, shading, lighting, texture mapping, and so on. To support as many hardware graphics accelerators as possible, however, it has a limited set of drawing primitives. By limiting you to ten primitives, OpenGL forces you to write your own routines (or use those in the auxiliary library). In this article, I'll examine these primitives and show how you can put them to work in your own programs.

OpenGL Primitives

The ten primitives supported in OpenGL can be divided into three categories: point, lines, and polygons. All other functionality (lighting, texture mapping, and the like) is provided by the OpenGL API. OpenGL contains just a single point primitive, but the primitive gives you a great deal of flexibility and control over how the point is displayed. You can control the size and the antialiasing of the point. The default rendering of a point is simply a pixel, but you can control the size to make points any number of pixels in diameter, even fractional pixels. While you're not really displaying half a pixel, OpenGL will "blur" the point across the pixels (antialiasing the pixel), which gives the point the visual effect of being located fractionally.

For instance, Example 1(a) draws four points in a diamond pattern about the origin. As with all OpenGL commands, you must wrapper the commands between glBegin() and glEnd() statements. The glBegin() statement takes a single argument representing one of the ten enum values for the primitive you're creating; Table 1 and Table 2 list these values. The actual pixels drawn on the screen depend upon a number of factors. Normally, the locations of a point are reduced to a single pixel on the screen. If antialiasing is turned on, you might instead get a group of pixels of varying intensity, with the sum of their location representing a single pixel located across multiple pixel boundaries. If you change the default pixel size with the glPointSize() command, the pixels drawn will correspond to that size. If antialiasing also is turned on, the edges of the point might be slightly "fuzzed," because OpenGL attempts to represent a point that doesn't exactly fall on pixel boundaries. You can query for the maximum and minimum size using the glGetFloatv() function with the GL_POINT_ SIZE_ RANGE argument.

OpenGL Lines

OpenGL provides a bit more latitude in manipulating lines. You not only control line width, but you can specify stipple patterns. Lines are the first primitive we've seen that are really affected by lighting calculations. Unlike the point primitive, the order in which vertexes are specified (for both lines and polygons) is important. When you construct your primitives, make sure that the order in which vertexes are specified is correct. There are three different line primitives that can be created; see Table 1.

To draw the diamond pattern used for the points in Example 1(a), simply change the primitive specified in the glBegin() call, as in Example 1(b). This will draw a parallelogram. Just as with points, the default size is one pixel, but you can control the line size using glLineWidth() and the line smoothness with antialiasing. Again, just as with points, you can get the maximum and minimum line size by using the glGetFloatv() call with the appropriate arguments.

You can also specify stippled (patterned) lines. These patterns are essentially a series of 0s and 1s specified in a 16-bit variable. For every 1 appearing in the pattern, drawing is turned on, and for every 0, drawing is turned off. You can also increase the size of the pattern by specifying a multiplying factor. This allows you to make the patterns appear larger. When the full 16 bits have been used, the pattern is restarted. For example, a pattern of 0xFFFF renders a solid line, while 0xFF00 renders a dashed line with drawn and undrawn parts of equal length.

If you are rendering a series of connected lines (that is, they are all in the same glBegin()-glEnd() sequence), the pattern continues across the connecting vertices. This is useful if you're plotting data on a graph and want the pattern to continue along the entire length of the line, or if you're plotting a curved shape and want the pattern to flow along the curve. You can also control the width of stippled lines just as you would solid lines. The width is independent of the pattern, so you have complete control over the line. If you need a line that contains a pattern in two or more colors, then you can create this effect by creating patterns that only draw in the appropriate locations, then using these patterns to create multiple, overlapping lines. As long as you use the exact same vertices you should get the effect you want with only the occasional overlapping pixel being drawn in both colors (use opaque colors and you'll never notice).

OpenGL Polygons

OpenGL polygons are constrained in certain ways from the more-comprehensive mathematical definition of a polygon. A polygon that OpenGL can render correctly is a simple, convex polygon of three or more unique points that all lie on the same plane. A polygon is simple if its edges don't intersect; that is, two edges can't cross without forming a vertex. A polygon is convex if it's never dimpled inwards--that is, for any two vertices of the polygon, a line drawn between them remains on the interior of the polygon. Finally, all of the points must lie on the same plane. (By "plane," I mean any arbitrary plane, not just the three planes formed by intersections of the x, y, and z axes.)

As examples of simple and nonsimple polygons, consider a square of cloth lying on a table with all four corners pinned to the table. This meets our definition of a simple polygon with four vertices. Now, if you take one corner and pull it up, you have a curved surface and a nonsimple polygon. Be aware that OpenGL is perfectly willing to render any nonsimple polygons, and it's up to you to ensure OpenGL receives accurate information. There'll be no warning if you enter a nonsimple polygon--but what happens when you try to render it is undefined. Frequently, OpenGL will do a creditable job of rendering polygons that are only slightly out of true. However, at certain angles the polygon will look inaccurate, and it can be agonizing to figure out what's wrong. This is why triangles are so popular. With only three points, they must lie on a plane. That's why so many routines in OpenGL degenerate objects into groups of triangles, since triangles meet all the requirements of being simple, convex polygons.

Example 1(c) shows how to construct a filled parallelogram on the x-y plane. The order of the vertices is important, since the order tells OpenGL which side of the polygon is the "front." This affects objects that are completely enclosed, or those visible from just one side. By judiciously constructing objects so that you only need to render the front faces and not the back, you can significantly increase performance. The face of a polygon that's rendered to the screen and that has a counterclockwise vertex order around the perimeter is (by default) considered the front face of the polygon, while a clockwise order denotes the back face.

Front Faces, Back Faces, and Rendering Modes

By default, both faces of a polygon are rendered. However, you can select which faces get drawn, as well as how front faces are differentiated from back faces. This is another of those subtle points that can bite you if you aren't paying attention. If you mix the order in which you draw your polygons, rendering becomes problematic, since you frequently don't want the back-facing polygons to be displayed. For example, if you're constructing an astounding 3-D texture-mapped game in tribute to Star Trek, you probably don't want the interior of the Romulan ships drawn. In fact, you probably don't want OpenGL to bother with any of the faces of the objects that aren't visible, so you specify that the back faces can be ignored in order to boost performance. So, how do you tell if the polygon will be clockwise or counterclockwise in screen coordinates? This can be very complicated. In practical terms, just make sure that the object has an "outside" or a "front" and that this is the side with a counterclockwise vertex order with respect to the screen.

You can control rendering using the glCullFace() command with a value indicating front or back faces. This allows you to select which faces get culled. Toggle culling using glEnable() with the appropriate arguments. And you can swap the clockwise or counterclockwise designation of a front face with the glFrontFace() command. Note that you can easily control the culling for an object by prefacing its modeling commands with the appropriate commands. Don't forget to restore the attributes that you'll need later.

It's also possible to individually control how the front face and back face are rendered. The glPolygonMode() command takes two parameters. The first parameter selects a polygon. The second parameter is the rendering mode, which can indicate that the polygon should be rendered as only vertex points, lines connecting the vertexes (as in a wireframe), or a filled polygon. This is useful when the user is inside a model, and you want to give them a hint that they are inside. If the mode were filled polygons, they'd just see a screen full of color. If it were points (or back-face culling enabled) they would probably see nothing recognizable. If the wireframe mode was on, then they'd probably quickly get the idea that they were inside the model. You can control which edges in a polygon are rendered and thus eliminate lines in the wireframe. Finally, OpenGL can construct six different types of polygon primitives, each optimized to assist in the construction of a particular type of surface. Table 2 describes the polygon types you can construct.

Patterned Polygons

Just as you could stipple an OpenGL line, you can also stipple a polygon. Instead of the default filled polygon style, specify a 32-bit-by-32-bit pattern that will be used to turn rendering off and on. The pattern is window aligned so that touching polygons will appear to continue the pattern. This also means that if polygons move and the viewpoint remains the same, the pattern will appear to be moving over the polygon!

The pattern can be placed in any consecutive 1024 bits, with a 4x32 GLubyte array being a convenient format. The actual storage format of the bytes can be controlled so that you can share bitmaps between machines that have different Endian storage, but by default, the storage format is that of the particular machine your program is running on. The first byte is used to control drawing in the lower-left corner, with the bottom line being drawn first. Thus, the last byte is used for the upper-right corner.

Rendering Primitives

Now that you've been exposed to the basic primitives, let's see how you can use some of them in your own code. In addition to specifying the type of primitive and the vertices that make up the primitive, you'll need to specify colors for the vertices. If you're interested in lighting for your model, you'll also have to specify additional information with each vertex.

OpenGL is a state machine, and it is never more obvious than when setting the color of a vertex. Being a state machine means that, unless something explicitly changes a state, it remains in that state. Once a color is selected, all rendering will be done in that color. The glColor*() function is used to set the current rendering color. This family of functions takes three or four arguments: three RGB values and an (optional) alpha value. The glColor3f() function takes three floating-point values for the red, blue, and green color. A value of 0.0 means no intensity, while a value of 1.0 is full intensity, and any intermediate value is partial intensity. Note that if you're using a 256-color driver, you'll probably get a dithered color.

Color is selected on a per-vertex basis, but it's also affected by the current shading model selected. If flat shading is currently selected, only one vertex is used to define the shaded color for the entire polygon. (The vertex defining the shaded color depends upon the primitive type.) If, however, you have smooth shading enabled, each vertex can have a unique shaded color (which depends upon the vertex's unshaded color, the color you assigned that particular vertex, and the current light falling on the vertex). Between vertices of different shaded colors, the intermediate pixel's colors are interpolated between the shaded colors of the vertices. Thus, OpenGL will smoothly blend from the shaded color at one vertex to the shaded color of other vertices, all with no intervention from you.

Calculating Normal Vectors

A normal vector is a vector that is perpendicular to a line or a polygon. Normal vectors are used to calculate the amount of light hitting the surface of the polygon. If the normal vector is pointing directly at the light source, the full value of the light is hitting the surface. As the angle between the light source and the normal vector increases, the amount of light striking the surface decreases. Normals are usually defined along with the vertices of a model.

You must define a normal for each vertex of each polygon that you want to show the effects of incident lighting. Assuming lighting is disabled, the rendered color of each vertex is the color specified for that vertex. If lighting is enabled, the rendered color of each vertex is computed from the specified color and the effects of lighting upon that color. If you use the smooth shading model, the colors across the surface of the polygon are interpolated from each of the vertices of the polygon. If flat shading is selected, then only one normal from a specific vertex is used. If you only use flat shading, your rendering time will be significantly faster, not only because flat shading is a faster shading model, but because you only have to calculate one normal for each polygon. For a single polygon, the first vertex is used to specify the color. For all the other primitives, it's the last vertex in each polygon or line segment.

Calculating normals is relatively easy, especially if you're restricted to simple polygons as you are when using OpenGL primitives. Technically, you can have a normal for either side of a polygon. By convention, however, normals specify the front face. Since you need at least three unique points to specify a planar surface, you can use three vertices of a simple polygon to calculate a normal. You take the three points and generate two vectors sharing a common origin. You then calculate the vector or cross product of these two vectors. The last step is to normalize the vector, which simply means ensuring the vector is a unit vector. OpenGL will normalize all normal vectors for you, if you tell it to. But you can use this routine to automatically provide only unit normals.

Listing One takes three vertices specified in counterclockwise order and calculates a unit normal vector based upon these points. If you use this approach, you'll get reasonably good results, with lighting effects that look good. However, you'll get an artifact called "faceting"--the shading on adjacent polygons will be discontinuous, in some cases clearly showing the individual polygons that make up the surface. If this is unacceptable, then you can switch to smooth shading. However, smooth shading, while interpolating between the vertices of a polygon, does nothing to make sure the interpolygon shading is smooth. You'll need to use a larger number of polygons to define your surface, or modify the normals to simulate a smooth surface.

Analytic Surfaces

An analytic surface is a surface defined by one or more equations. The easiest way of getting surface normals for such surfaces is to take the derivative of the equation(s). If the surface is being generated from a sampling function (a function that provides interpolated values taken from a database), you'll have to estimate the curvature of the surface and get the normal from this estimate. There is an appendix in the OpenGL Programming Guide (Addison-Wesley, 1993) that gives an overview of this approach. An alternative is to take the current information and interpolate your own surface normals. To reduce faceting of all the touching vertices, you'll need to force these vertices to have the same normal. The simplest method is to average all of the normals of each of the vertices and use this averaged value. For n polygons that share a common point, there are n vertices, and n individual normals. You need to sum all the normal values, n0 + n1 + n2 + ... + nn, then normalize this vector and replace the original normal values with this value.

Summary

Creating primitives is at the heart of three-dimensional programming in OpenGL. It doesn't matter if it's a car widget or a velociraptor, the really interesting things usually consist of one complicated thing made up of less complicated parts, made up of still simpler parts. Once you get a feel for creating primitives, you're on your way to creating your own toolbox of primitives that you can reuse in a number of different ways.

Example 1: OpenGL statements to draw (a) four points in a diamond shape around the origin; (b) a parallelogram; (c) a simple polygon.

(a)     glBegin( GL_POINTS )
         glVertex2f( 0.0f, 2.0f ); // note 2D form
         glVertex2f( 1.0f, 0.0f );
         glVertex2f( 0.0f,-2.0f );
         glVertex2f(-1.0f, 0.0f );
     glEnd();
     
(b)     glBegin( GL_LINE_LOOP )// make it a connected line segment
         glVertex2f( 0.0f, 2.0f ); // note 2D form
         glVertex2f( 1.0f, 0.0f );
         glVertex2f( 0.0f,-2.0f );
         glVertex2f(-1.0f, 0.0f );
     glEnd();
     
(c)     glBegin( GL_POLYGON )
         glVertex2f( 0.0f, 0.0f ); // note 2D form
         glVertex2f( 1.0f, 1.0f );
         glVertex2f( 0.0f, 1.0f );
         glVertex2f( 2.0f, 0.0f );
     glEnd();

Table 1: The point and line primitive types that you can specify in the glBegin() statement.

Type           Description

GL_POINTS      Draws a point at each vertex, for as many vertices

                as are specified.

GL_LINES       Draws a line segment for each pair of vertices.

                Vertices v0 and v1 define the first line; v2 and

                v3, the next, and so on. If an odd number of

                vertices is given, the last one is ignored.

GL_LINE_STRIP  Draws a connected group of line segments from

                vertex v0 to vn, connecting a

                line between each vertex and the next in the

                order given.

GL_LINE_LOOP   Draws a connected group of line segments from

                vertex v0 to vn, connecting each

                vertex to the next with a line, in the order

                given, then closing the line by drawing a

                connecting line from vn to v0,

                defining a loop.

Table 2: Polygon primitive types.

Type               Description

GL_POLYGON         Draws a polygon from vertex v0 to

                    vn-1. n

                    must be at least 3, and the vertices must

                    specify a simple, convex polygon. These

                    restrictions are not enforced by OpenGL.

                    In other words, if you don't specify the

                    polygon according to the rules for the

                    primitive, the results are undetermined.

                    Unfortunately, OpenGL will be happy to

                    attempt to render an ill-defined polygon

                    without notifying you, so construct all

                    polygon primitives carefully.

GL_QUADS           Draws a series of separate four-sided

                    polygons. The first quad is drawn using

                    vertices v0, v1, v2, and v3. The next is

                    drawn using v4, v5, v6, v7, and each

                    following quad, using the next four vertices

                    specified. If n isn't a

                    multiple of four, then the extra vertices

                    are ignored.

GL_TRIANGLES       Draws a series of separate three-sided

                    polygons. The first triangle is drawn using

                    vertices v0, v1, and v2. Each set of three

                    vertices is used to draw a triangle.

GL_QUAD_STRIP      Draws a strip of connected quadrilaterals.

                    The first quad is drawn using vertices v0,

                    v1, v3, and v2. The next quad reuses the

                    last two vertices and adds the next two,

                    v2, v3, v4, and v5. Each of the following

                    quads uses the last two vertices from the

                    previous quad. n must be

                    greater than 4 and a multiple of 2.

GL_TRIANGLE_STRIP  Draws a series of connected triangles. The

                    first triangle is drawn using vertices v0,

                    v1, and v2, the next uses v2, v1, and v3,

                    the next v2, v3, and v4. Note that the order

                    ensures that they all are oriented alike.

GL_TRIANGLE_FAN    Draws a series of triangles connected about

                    a common origin, vertex v0. The first

                    triangle is drawn using vertices v0, v1,

                    and v2, the next uses v0, v2, and v3, the

                    next v0, v3, and v4.

Listing One

// Pass in three points, and a vector to be filled
void  NormalVector(GLdouble p1[3],GLdouble p2[3],GLdouble p3[3],GLdouble n[3])
{
    GLdouble v1[3], v2[3], d;
    // calculate two vectors, using the middle point as the common origin    
    v1[0] = p2[0] - p1[0];
    v1[1] = p2[1] - p1[1];
    v1[2] = p2[2] - p1[2];
    v2[0] = p2[0] - p0[0];
    v2[1] = p2[1] - p0[1];
    v2[2] = p2[2] - p0[2];
   
    // calculate the crossproduct of the two vectors
    n[0] = v1[1]*v2[2] - v2[1]*v1[2];
    n[1] = v1[2]*v2[0] - v2[2]*v1[0];
    n[2] = v1[0]*v2[1] - v2[0]*v1[1];
    
   // normalize the vector
    d = ( n[0]*n[0] + n[1]*n[1] + n[2]*n[2] );
   // try to catch very small vectors
    if (  d  <  (Gldouble)0.00000001)
        {
        // error, near zero length vector
        // do our best to recover
        d = (GLdouble)100000000.0;
        }
    else // take the square root
        {
        // multiplication is faster than division
        // so use reciprocal of the length
        d = (GLdouble)1.0/ sqrt( d );
        }
    n[0] *= d;
    n[1] *= d;
    n[2] *= d;
}
End Listing>>
<<


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.