Chapter 7. Display Lists and Fonts

 

It is a capital mistake to theorize before one has data.

 
 --Sherlock Holmes

Caching OpenGL Commands

In chapter 6 we created a stock background for our default OpenGL view. Although we can easily generate a background for our model, it’s time consuming to go through all those calculations every time we need to rerender the scene. After all, the stock scene doesn’t change from frame to frame. Only the viewpoint or the model will change. If we have to rerender something that doesn’t change, there should be a method of optimizing our program by caching those OpenGL commands. Well, there is, and it’s called display lists, the subject of this chapter.

The limited commands that are acceptable between a glBegin()/glEnd() pair only describe an OpenGL primitive. Thus if you’re going to repeatedly execute the same sequence of OpenGL commands, you can create and store a display list and then have this cached sequence of calls repeated with minimal overhead, since all of the vertices, lighting calculations, textures, and matrix operations stored in that list are calculated only when the list is created, not when it’s replayed. Only the results of the calculations end up being stored in display lists; thus any complicated calculations can usually benefit from being placed in a display list and having them replayed at a different time. This speeds up your program and is called display-list, or deferred or retained mode: The commands are compiled, not executed. In contrast, immediate mode refers to putting OpenGL commands into the graphics pipeline when they are encountered.

Dynamic or Adaption Tessellation

A particularly useful feature of display lists is that they enable you to cache changes in models without having to compute them more than once. For example, suppose that you’re trying to model a flight simulator above a busy airport. You could draw each type of airplane and hangar from a display list to save time, but if the plane or hangar is a kilometer or more away from the current viewpoint, that level of detail in the original model might be too much. Instead it’s a simple matter to generate three or four display lists that gradually simplify a model as the distance from the viewpoint becomes large enough to overwhelm any visible differences. This greatly speeds up the rendering time required for the simulation, especially when there’s more than one model to render.

Dynamic or Adaption Tessellation

Creating a Display List

Creating a display list is very simple. Let’s suppose that we have some code that creates a static model. The important part of the code is within the glBegin()/glEnd() pair. The normal function for the stock scene does something like this:

// start rendering the stock scene
glBegin( GL_POLYGON );
    ..... details don't matter
glEnd();
// finished rendering the stock scene

A display list records the results of a glBegin()/glEnd() sequence and saves the resulting vertices, matrices, materials, lighting calculations, raster images, and textures. Thus when you record a display list, the current state of OpenGL is used to create the display list; when the list ends, the current state is whatever the original state was plus any changes made during the recording of the display list. Replaying the display list uses the current state, not the state that was in effect when the list was recorded. When the replay ends, the current state is the state when the replay started plus any changes to the state that were replayed as part of the display list.

Creating a Display List

This concept can be used to distinct advantage. Remember, when recording and playing back display lists, you have to be aware of the state that was in effect when the list was recorded, the states that are changed by executing the display list, and the state in which you’ll be executing the list. You usually don’t want to save all state information and pop it afterward; after all, we’re doing this to speed up the program! However, you’ll need to understand what the current states are and to push/pop or in some other way reset those states that need to remain unchanged after the display list executes. We’ll go over this in an example in a later section. For now let’s examine how you go about recording a display list.

Recording a Display List

When creating a display list, you use the glNewList() and glEndList() commands as separators. The first function—glNewList()—takes two arguments: The first is a unique integer argument used to identify the display list; the second is a flag indicating whether to simply compile the display list or to compile and immediately execute the display list. The second parameter is useful for the lazy evaluation of display lists. The glEndList() function takes no arguments and simply serves as a delineator.

Creating a display list is pretty simple:

// define a unique number for our display list
#define MYLIST    1
// start recording the display list
::glNewList( MYLIST, GL_COMPILE ); // just record for now
    // start rendering the display list
    ::glBegin( GL_POLYGON );
        ..... details don't matter
    ::glEnd();
    // finished rendering the display list
::glEndList();
// finshed creating the display list

The commands executed between a glNewList() and glEndList() pair use the parameter values in effect when the list was created, not values when the list is replayed. Thus if you generate a display list that’s based on a vector pointer, only the values of the pointer are used, not the pointer itself. In other words, you can’t set up the array of vertices, create the display list using that array, change the vertices in the array, and then execute the display list and expect the new array values to be in effect.

In addition to the restrictions on the commands that can be placed in glBegin() and glEnd() pairs, there are further restrictions on commands that can be used in display lists but not in the glBegin() and glEnd() pairs. These commands aren’t compiled into the display list but are executed immediately, regardless of the display list mode. Most of these commands are listed below.

  • glIsList()

  • glGenLists()

  • glDeleteLists()

  • glFeedbackBuffer()

  • glSelectBuffer()

  • glRenderMode()

  • glReadPixels()

  • glPixelStore()

  • glFlush()

  • glFinish()

  • glIsEnabled()

  • glGet*()

These are commands that can be stored on the server and not on the client. For example, only the client knows its current state, so any commands that query the current state can’t be placed in a display list, in keeping with OpenGL’s client/server architecture. Remember, even though you might be running Windows95, OpenGL can run on many other platforms. So those functions that query the state, such as glGet*(), or routines that depend on clients’ state, such as glFinish(), can’t be compiled in display lists. You should also note that calling glNewList() while inside another glNewList() generates an error. You can, however, call a display list while creating a display list, as we’ll see later.

Executing a Display List

Once you’ve created a display list, it’s a simple matter to execute it. Executing a display list is done with one of two functions. One is a specialized form that is used for sequences of display lists, a topic we’ll cover in a later section. The other function is the basic method of calling a single display list. The glCallList() command takes a single argument, the unique integer ID that you gave a previously defined display list. If the ID doesn’t correspond to a valid display list, nothing happens. When a display list is executed, it’s as if the display list commands were inserted into the place where you made the glCallList() command, except that any state variables that have changed between the display list creation and execution times are ignored. However, if any state variables are changed from within the display list, they remain changed after the display list completes execution. The OpenGL commands glPushAttrib() and glPopAttrib() were created for such cases. These commands save and restore state variables. If you also want to save and restore the current matrix, you’ll need to use the glPushMatrix() and glPopMatrix() commands.

To call the display list we generated previously, we’d issue the following command:

::glCallList( MYLIST ); // execute my list

The state of OpenGL after a display list is executed depends on what’s in the list. For example, suppose that our list was created as follows:

::glNewList( MYLIST, GL_COMPILE );
    // start rendering the stock scene
    ::glBegin( GL_QUAD );
    ::glVertex3f( 1.0f, 0.0f,-1.0f );
    ::glVertex3f( 1.0f, 0.0f, 1.0f );
    ::glVertex3f(-1.0f, 0.0f, 1.0f );
    ::glVertex3f(-1.0f, 0.0f,-1.0f );
    ::glEnd();
    ::glTranslatef( 1.0f, 0.0f, 1.0f );
::glEndList();

Note that there’s a translation after the vertices have been defined. This causes the vertices following the execution of this display list to be translated. We could use this to draw the same square repeatedly, having it translated each time, and have the squares connected at the corners. In order to get five connected squares, we would use the following commands:

::glCallList( MYLIST );
::glCallList( MYLIST );
::glCallList( MYLIST );
::glCallList( MYLIST );
::glCallList( MYLIST );

This sequence would also leave the Modelview matrix translated five units in the +z and +x directions. You’d have to be careful about what commands you executed before and during display list creation to make sure that these are the ones really needed.

Generating a Unique Display List ID

Some complications can arise in getting a unique display list ID. You can hope that your programs will always be so simple that when someone adding a feature doesn’t accidentally reuse a display list ID. But it’s easy enough to guarantee that all your IDs are unique. Windows provides the function RegisterWindowsMessage() so that you can get a guaranteed unique message ID from the operating system, necessary when processes need to communicate. Display list IDs are unique to each RC that’s running, so if you’re just working on an application that’s going to have a short lifetime and is pretty much self-contained, you’re probably safe in assuming that you don’t have to worry about nonunique IDs. However, if this is for your job, then you’re probably aware that prototypes have a nasty habit of turning into product.

The glIsList() Function

Much as you can generate unique message IDs using the operating system, OpenGL provides two functions to guarantee the uniqueness of display list IDs. The glIsList() function takes an ID and returns GL_TRUE if the ID is already in use. Since there is nothing special about display list IDs, it’s a pretty simple matter to construct a function to generate a unique ID. If we allocate the IDs in order and keep track of the last ID issued, we can easily construct a function to always return a unique ID. Listing 7.1 shows how you might implement such a function. Note two features: The m_LastDisplayListID variable should be considered a member of the COpenGLView class; second, the purpose of the variable is to retain the last ID dispensed. The range of valid IDs doesn’t include 0, so it’s reserved to indicate either that a display list needs to be generated (or else it would have an ID) or that the function failed to return an ID.

Example 7.1. Generating a Unique Display List ID Using glIsList()

GLint COpenGLView::GetNewDisplayListID()
{
    GLint first, last;
    first = m_LastDisplayListID; // in class definition
    last = first+10001;
    ASSERT( last > first );
    while( ++first <= last )
        {
        if ( GL_TRUE == ::glIsList( first ) )
            continue;
        // we found a unique ID, return it
        return (m_LastDisplayListID = first);
        }
    // reserve 0 as failure
    return 0;
}

This function is fine when you need to generate only a few IDs at a time. However, sometimes you need to generate a whole stream of unique, continuous IDs, as described next.

The glGenLists() Function

This function takes one argument, the range, and returns an ID such that the next range IDs are continuous. (In other words, if ID n is returned, the last valid ID would be n + range1). The display list IDs that are generated are empty display lists. They are marked as reserved but are empty. If you fail to assign a display list to any of these IDs, they are wasted. If the display lists can’t be generated for some reason, 0 is returned and no lists are generated.

Deleting and Reusing Display List IDs

Occasionally you might want to release or free up some display list IDs. The glDeleteLists() function deletes a contiguous group of display lists. The two parameters the function requires are the ID of the first list to delete and the range of lists to delete. After the function returns, the display lists n though n + range1 are now available. All storage locations allocated to the specified display lists are freed, and the names are available for reuse. Any IDs within the range that do not have an associated display list are ignored. If the range is 0, nothing happens.

If you want to immediately reuse one or more display list IDs, you can simply reuse the ID(s) immediately by using the glNewList() function with the IDs to be reused. When a display list ID is reused, it is replaced only when glEndList() is called.

Sequential Display Lists

The command for creating display list IDs is glGenLists(). The primary purpose of this command is convenience, as verifying a series of potential IDs for availability is tedious at best, so OpenGL has provided a mechanism for reserving a sequence of unique display list IDs. This is where the range parameter for the glGenLists() comes in.

This feature is particularly handy for rendering objects that exist (or are created) in some order, such as 3D character sets. An example in the OpenGL Programming Guide uses this technique to generate stroked fonts. Each character is assigned to a display list ID that corresponds to its ASCII value. For example, the letter A is ASCII value 65, so display list ID 65 is assigned the strokes for generating the character A. These steps are repeated for the rest of the letters needed, and then the display lists are executed by using the glCallLists() function. Unlike the glCallList() function seen earlier, glCallLists() function takes three arguments. The last argument is a void pointer to a vector that contains the order of the display list IDs you want executed. The middle parameter tells glCallLists() the size (in bytes) of each element of the vector, using one of a series of enum values provided by OpenGL.

What use is this for generating more than one set of sequential display lists? Well, there is one more OpenGL command that is provided to make sequential display lists easier to use (but not easier to create!). The glListBase() command takes an integer value that is used as an offset when calling subsequent glCallLists() commands. Note that it has no effect on either glCallList() (singular) or glNewList(). Thus when you use glGenLists(), the returned value is used as the base value when you make calls to glCallLists(). However, you have to manually add this base value when you define a display list.

Generating a Display List for a Character Set

For example, let’s generate a list for the twenty-six uppercase characters in the ASCII character set. First, we’d reserve twenty-six consecutive display list IDs for the characters and reserve the returned value, as follows:

GLuint base = ::glGenLists(26);

We have no control over the returned value; we know only that there are now twenty-six reserved IDs, from base to base+25. A convenient way of calling display lists for characters is to use the value of the character. Thus we could allocate some extra space at the beginning of the reserved display list IDs to account for the initial value of our first vector index (in this case it’s 65, or ‘A’). Or we could subtract the initial value from the generated base value, which is preferred if you’re going to be generating lots of lists. The other alternative makes it simpler if you don’t have that many. An added complication is that glGenLists() returns an unsigned int, and thus you’d have to be careful with the calculations to avoid wrapping the values around. For example, using the base value generated earlier and letting RenderLetter be some function that takes an index into an array of rendering commands for each letter, we have:

for ( GLint i = 0 ; i < 25 ; i++ )
    {
    ::glNewList( base+i, GL_COMPILE );
    RenderLetter(i);
    ::glEndList();
    }

Then, to have the text rendered, you’d create a sequence of display list indices that spelled out what you wanted. This is made easier because we’ve associated the value of each ASCII letter with the corresponding display list. Thus to render a text string, the code might look like this:

char *text = "THIS IS SOME OPENGL TEXT";
::glCallLists( strlen(text), GL_BYTE, text );

Note that a GL_BYTE is the same size as an ASCII char, but if you are using Unicode, you’ll have to make the appropriate changes.

Finally, you may wonder just what the RenderLetter() routine does. It does whatever it has to—it’s entirely up to you. Since serial display lists were made to accommodate a sequence of display lists, not just character rendering, you are entirely responsible for the definition of each display list. For character rendering you probably want to render each letter in the xy or xz plane, making them either flat or extruded, and probably performing a translation after you’re done rendering, placing the updated origin just after the end of the character that you’ve just rendered. This allows sequential display lists to be strung together and to have the rendered characters positioned one after the other. This is the end of OpenGL’s support of character lists. However, wgl provides some extra support for Windows character sets, and we’ll explore these features later in this chapter.

Sharing Display Lists between RCs and Threads

When you create an OpenGL rendering context, it has its own display list space. Sometimes you don’t want to go through the process of re-creating display lists, particularly if they are common to most of your OpenGL programs. The OpenGL function wglShareLists() was designed just for this purpose; it enables a rendering context to share the display list space of another RC. Any number of rendering contexts can share a single display list space. All the IDs and definitions of display lists in a shared display list space are shared. Once a rendering context shares a display list space, the rendering context always uses the shared space until the rendering context is deleted. When the last rendering context of a shared space is deleted, the shared space is deleted.

You can share display lists with rendering contexts only within the same process. However, not all rendering contexts in a process can share display lists. Rendering contexts can share display lists only if they use the same implementation of OpenGL functions. All client rendering contexts of a given pixel format can always share display lists.

There are two instances when you might want to share display list space. The first is when your programs share a common OpenGL DLL, one that creates shared models. In this case you’d want the savings of reusing these display lists rather than re-creating them for each instance. The other instance is when you’re performing multithreaded OpenGL rendering. Remember that a thread can have only one current rendering context at a time. A process can have multiple rendering contexts by means of multithreading. An application can perform multithreaded drawing by making different RCs current to different threads and supplying each thread with its own RC and DC. If you want to share display lists, you should note that it’s available only with the updated OpenGL version 1.0 that came with Windows NT 3.51 or Windows 95.

Sharing Display Lists between RCs and Threads

Hierarchical Display Lists

When you call glCallList() inside the definition of a new display list, you are creating nested, or hierarchical, display lists. Nested display lists are useful when the object you’re compiling into a display list is itself made up of repeated parts. When glNewList() encounters a call to glCallList(), the command that is inserted is a call to that display list, not the commands represented by that display list. For example, consider the following code:

// create three display lists
::glNewList( LIST_ONE, GL_COMPILE );
    ... some code
::glEndList();

::glNewList( LIST_TWO, GL_COMPILE );
    ... some code
::glEndList();

::glNewList( LIST_THREE, GL_COMPILE );
    ... some code
::glEndList();

This simply creates three unique display lists. To generate a hierarchical display list composed of these three lists, we’d write the following:

// create a hierarchical display list
::glNewList( NESTED_LIST, GL_COMPILE );
    ::glCallList( LIST_ONE );
    ::glCallList( LIST_TWO );
    ::glCallList( LIST_THREE );
::glEndList();

The following line will execute the nested display list and thus call the three display lists nested inside:

::glCallList( NESTED_LIST );

That’s all pretty straightforward. But say that you now wanted to modify the LIST_TWO sequence, retain the other two, and execute this modified sequence. This is the advantage of hierarchical display lists; you can modify the commands associated with a display list ID and have the new commands executed in other display lists that use this ID.

// modify the second list
::glNewList( LIST_TWO, GL_COMPILE );
    ... some code that's different
::glEndList();

// now execute the modified hierarchical list
::glCallList( NESTED_LIST );

You can see the power associated with this feature. If you’re writing an application that uses many static objects in nonstatic ways, such as an architectural program that creates buildings out of standard parts, the ability to cache objects and to replay them whenever you need, plus the ability to nest them, provides the basis for a very powerful modeling feature.

Using Display Lists in the COpenGLView Class

Now let’s incorporate display lists into the COpenGLView class. A natural place to use them is the RenderStockScene() function. After all, we had only one stock scene, and users of the COpenGLView class would probably like to define some stock scenes for their own use. So let’s add another stock scene.

We’ll use the surface made up of triangles arranged about the origin that we created in chapter 6 and will call this routine StockSceneTriangles(), shown in Listing 7.2. All it does is render the triangles about the current origin in the xz plane.

Example 7.2. A Triangled Surface on the XZ Plane

// Draw a square surface of red and blue triangles
// all touching the origin.
void COpenGLView::StockSceneTriangles( )
{
    // define all vertices   X     Y     Z
    GLfloat surface0[3] = { 0.0f, 0.0f, 0.0f };
    GLfloat surface1[3] = {+5.0f, 0.0f, 0.0f };
    GLfloat surface2[3] = {+5.0f, 0.0f,-5.0f };
    GLfloat surface3[3] = { 0.0f, 0.0f,-5.0f };
    GLfloat surface4[3] = {-5.0f, 0.0f,-5.0f };
    GLfloat surface5[3] = {-5.0f, 0.0f, 0.0f };
    GLfloat surface6[3] = {-5.0f, 0.0f,+5.0f };
    GLfloat surface7[3] = { 0.0f, 0.0f,+5.0f };
    GLfloat surface8[3] = {+5.0f, 0.0f,+5.0f };
    GLfloat surface9[3] = {+5.0f, 0.0f, 0.0f };

    // define the two colors
    GLfloat color1[3] = { 0.5f, 0.0f, 0.0f };
    GLfloat color2[3] = { 0.0f, 0.0f, 0.5f };

    ::glBegin( GL_TRIANGLES );
        ::glColor3fv( color1 );
        ::glVertex3fv( surface0 );
        ::glVertex3fv( surface1 );
        ::glVertex3fv( surface2 );
        ::glColor3fv( color2 );
        ::glVertex3fv( surface0 );
        ::glVertex3fv( surface2 );
        ::glVertex3fv( surface3 );
        ::glColor3fv( color1 );
        ::glVertex3fv( surface0 );
        ::glVertex3fv( surface3 );
        ::glVertex3fv( surface4 );
        ::glColor3fv( color2 );
        ::glVertex3fv( surface0 );
        ::glVertex3fv( surface4 );
        ::glVertex3fv( surface5 );
        ::glColor3fv( color1 );
        ::glVertex3fv( surface0 );
        ::glVertex3fv( surface5 );
        ::glVertex3fv( surface6 );
        ::glColor3fv( color2 );
        ::glVertex3fv( surface0 );
        ::glVertex3fv( surface6 );
        ::glVertex3fv( surface7 );
        ::glColor3fv( color1 );
        ::glVerte3fv( surface0 );
        ::glVertex3fv( surface7 );
        ::glVertex3fv( surface8 );
        ::glColor3fv( color2 );
        ::glVertex3fv( surface0 );
        ::glVertex3fv( surface8 );
        ::glVertex3fv( surface9 );
    ::glEnd();
}

We’ll also rename our original stock scene function to StockSceneCheckerboard(), and we’ll add another, empty routine, called StockSceneUser(), which is available for derived classes to override. To generate the stock scenes, we’ll add a function that uses a user-set member variable to determine which stock scene to render. Listing 7.3 shows how this function looks.

Example 7.3. Selecting a Stock Scene to Render

// Call the routines to render the stock scene
BOOL COpenGLView::GenerateStockScene( eStockSceneID id )
{
    // if the display list for the stock
    // scene hasn't been generated, then do so
    // we'll reuse this ID for all stock scenes
    if ( 0 == m_StockSceneListIndex )
        {
        // get a unique id
        m_StockSceneListIndex = GetNewDisplayListID( );

        if ( 0 == m_StockSceneListIndex ) // failed
            {
            return FALSE;
            }
        }

    // we have an ID, so set (or reset) it
    // and create the new stock scene

    ::glNewList( m_StockSceneListIndex, GL_COMPILE );
    GenerateThisStockScene( id );
    ::glEndList();

    return TRUE;
}

// This routine generates the stock scenes. The functions
// called are simply expected to render a scene
void COpenGLView::GenerateThisStockScene( eStockSceneID id )
{
    switch( id )
        {
        case eStockSceneSet:
        case eStockSceneNone:
            ; // an empty list
            break;

        case eStockSceneUserDefined:
            StockSceneUserDefined();
            break;

        case eStockSceneTriangles:
            StockSceneTriangles();
            break;

        case eStockSceneCheckerboard:
            StockSceneCheckerboard();
            break;

        default:
            break;
        }
}

Finally, we can rewrite the RenderStockScene() function to work on a member variable that can be set, m_SelectedStockScene. This member is selected directly or by calling the function SelectStockScene(). Since you typically want to set up all the parameters of a view when it’s created, you’d normally do this in the constructor. However, it’s impossible to generate an OpenGL display list without first initializing an OpenGL RC. We get around this problem by letting the stock scene display list be generated just before the display list is used. If m_SelectedStockScene is set, a new display list is to be generated. After the display list is generated and the display list ID for the stock scene is saved, m_SelectedStockScene is cleared. Listing 7.4 shows this updated function. Note that the stock scene can also be dynamically changed and that this will cause a new display list to be generated, but the original stock scene display list ID is reused.

Example 7.4. RenderStockScene() Using a Display List

void COpenGLView::RenderStockScene()
{
    // if the display list for the stock
    // scene hasn't been generated, then do so
    // the selected variable is used only to change
    // the stock scene
    if ( m_SelectedStockScene )
        {
        GenerateStockScene( m_SelectedStockScene );
        m_SelectedStockScene = eStockSceneSet; // clear it
        }
    if ( 0 != m_StockSceneListIndex )
        {
        ::glCallList( m_StockSceneListIndex );
        }
}

Rendering Windows Fonts in OpenGL

Probably the most glaring deficiency in OpenGL’s implementation is its lack of direct support of text objects. However, the reasoning is simple. Since text depends on there being a font in which to display the characters, there needs to be a description of these glyphs somewhere. OpenGL is a system-independent API, and there is really no reason to carry around the baggage of some font-rendering code when the host machine’s operating system no doubt has its own font-rendering software. Indeed this is the approach found on all OpenGL implementations, Windows being no exception.

In fact, if you’ve used GDI fonts before, you’re already familiar with the setup for using them in OpenGL. There are just a few twists to watch out for. If you’re using a double-buffered window, you can’t use GDI calls in your OpenGL window. You have to generate the glyphs for each character and then save them, which is where display lists come in handy. In fact, you’ll see that most of the display list commands we have examined are designed to support text rendering.

Rendering Windows Fonts in OpenGL

The steps for generating a display list of a particular font under Windows are as follows:

  1. Select the desired font characteristics.

  2. Select and then generate that font, using the current DC.

  3. Call one of the two wgl functions to automatically generate the glyphs and assign them to display list IDs sequentially.

  4. Deselect that font from the DC. (We have the display lists we need.)

This is surprisingly easy, because two wgl functions have been added that take care of generating the display lists for the font glyphs for us. In fact, with the proper wrappering, it can be easier to use fonts under OpenGL than in GDI! Let’s look at the two functions that save us a lot of work.

The wglUseFontBitmaps() Function

The wglUseFontBitmaps() function creates a set of bitmap display lists based on the glyphs in the currently selected font in the current DC for use in the current OpenGL RC. These bitmaps can then be used to draw characters in an OpenGL image. The wglUseFontBitmaps() function takes a DC argument and then values that indicate the starting character, the number of characters to create, and the value to use as the base offset for the generated display lists.

The documentation on the wglUseFontBitmaps() function tells us that each of the display lists consists of a single call to glBitmap(). Thus each display list consists of a rendering of a bitmap on the screen. In other words, the bitmap is rendered directly on the frame buffer. This indicates that wglUseFontBitmaps() is useful when your text doesn’t need to change size and you need only a flat representation of text.

Also you’ll have to specify where the bitmap is drawn in terms of a raster position, or window coordinates. Therefore these display lists are useful only when you want text that doesn’t rotate or scale with the rest of your model. For example, if you want to label part of your model and need to make sure that the text is always readable no matter what the user’s selected viewpoint or orientation, you’d use the function wglUseFontBitmaps(). A nice feature is that the current raster position is updated after each bitmap, which places the current raster position just after the last bitmap displayed. In this manner we can simply provide a starting position and then a string of bitmaps, and each bitmap will be correctly positioned so that the following bitmap is positioned correctly after it.

But how are you going to get your text labels to follow around the objects they are supposed to label when the model is spinning around? Never fear! The glRasterPos*() command is used for positioning bitmaps. This command introduces the concept of a current position. In this case it’s the starting position for raster operations and thus is used to position the current raster position for such commands as glBitmap(), glDrawPixels(), and glCopyPixels().

In the case we’re interested in, you use glRasterPos*() just as you would a glVertex*() command; you set the coordinates (in 3D space) and expect the coordinates to get transformed to raster coordinates. The only difference is that the position is being transformed, not the object associated with the position. In this case we’re simply transforming the starting location to render our display lists of bitmaps. Thus when you use wglUseFontBitmaps() to generate display lists and then call them, the resulting text is displayed, starting at the current raster position, and the bitmaps are copied to the raster buffer, giving the effect of always having the text positioned in the xy plane of the screen.

Thus you’d use wglUseFontBitmaps() when you need to make sure that the text is always visible to the user and that the size of the text relative to its distance from the viewpoint doesn’t matter. The transformations are done only on the current raster position, not on the bitmap. When the current raster position is transformed to screen coordinates, then the bitmap is copied to the raster buffer. The bitmap will always be the same size, no matter how far away from the viewpoint it is positioned. It may not always be visible, however, since the raster position is checked for clipping before the bitmaps are displayed. Therefore if the initial raster position is outside the clipping area, no text will be displayed, even if that text flows into the viewing volume.

The wglUseFontBitmaps() Function

Finally, the utility library provides two commands that make it relatively easy to get from screen coordinates to object coordinates and back. These commands are gluProject() and gluUnProject(). You use these commands to position text so that it is always in the same location in the viewport window.

Adding Bitmapped Text to COpenGLView

We can use the wglUseFontBitmaps() function to create a set of bitmap display lists from a specified font name. In order to use this function, you must first select the desired font into the current DC. Since we have the current DC saved away, it’s easy to simply pass in the font name, select that font as the current one, use wglUseFontBitmaps() to generate the display lists, and then deselect the font to return the DC to its previous state.

We’ll create such a function in the COpenGLView class so that we can easily display flat text in our OpenGL window. Since we can’t be sure of what fonts are available on the current machine, let’s build in a default that selects the SYSTEM_FONT if a null name is passed in. This way we can always fall back to a font we know exists. We’ll also make use of the GetNewDisplayListIDs() function to generate our display list IDs. Listing 7.5 shows the initial implementation. The return value is the display list ID that identifies the font bitmaps. This value is used as the base value before using the font.

Example 7.5. Generating a Bitmapped Font

GLuint COpenGLView::GenerateBitmapListForFont(
        char* fontname )
{
    GLuint id;

    if ( 0 == m_pDC ||
        (GLuint)0 == (id = GetNewDisplayListIDs(256)) )
        {
        return 0;
        }

    CFont newfont;
    CFont* oldfont;
    BOOL success;

    if ( NULL != fontname )
        {
        LOGFONT logfont;
        // select 12 point font for now
        logfont.lfHeight         = -12;
        logfont.lfWidth          = 0;
        logfont.lfEscapement     = 0;
        logfont.lfOrientation    = logfont.lfEscapement;
        logfont.lfWeight         = FW_NORMAL;
        logfont.lfItalic         = FALSE;
        logfont.lfUnderline      = FALSE;
        logfont.lfStrikeOut      = FALSE;
        logfont.lfCharSet        = ANSI_CHARSET;
        logfont.lfOutPrecision   = OUT_DEFAULT_PRECIS;
        logfont.lfClipPrecision  = CLIP_DEFAULT_PRECIS;
        logfont.lfQuality        = DEFAULT_QUALITY;
        logfont.lfPitchAndFamily =
                FF_DONTCARE|DEFAULT_PITCH;

        lstrcpy ( logfont.lfFaceName, fontname );

        // CreateFontIndirect returns 0 if it fails
        success = newfont.CreateFontIndirect( &logfont );
        oldfont = m_pDC->SelectObject( &newfont );
        ASSERT( 0 != oldfont );
        }
    else
        {
        // make the system font the device context's
        // selected font
        oldfont = (CFont*)m_pDC->SelectStockObject(
                SYSTEM_FONT );

        ASSERT( 0 != oldfont );
        }
    // Create a set of display lists based on the glyphs
    // of the font. Notice that we really waste the first
    // 32 spaces....
    // if there's a problem delete the display lists
    if ( 0 == success ||
        FALSE == ::wglUseFontBitmaps( m_pDC->m_hDC,
                0, 256, id ) )
        {
        ::glDeleteLists( id, 256 );
        id = 0;
        }
    else
        {
        // replace old font
        m_pDC->SelectObject( oldfont );
        }
    return id;
}

Thus to generate a bitmapped font and then use it, you’d simply supply the font name (or null, if you wanted the system font) and then use the returned ID as the base value for the font display list. A simple routine to take the font ID and a text string is shown in Listing 7.6.

Example 7.6. The Display List GLTextOut Function

void COpenGLView::GLTextOut( GLuint id,
        const char * const textstring )
{
    if ( 0 == id || 0 == textstring )
        {
        return;
        }

    GLsizei size = strlen( textstring );

    ::glListBase( id );
    ::glCallLists( size, GL_UNSIGNED_BYTE,
        (const GLvoid*)textstring );
}

Windows 95 and Windows NT have some differences. Particularly when selecting a font using the Windows LOGFONT structure, there are differences in the behavior if you use some of the flags to attempt to customize the generated font. Also, some combinations give different behavior between Windows 95 and Windows NT, so unless you write operating system–aware code, you should avoid changing the LOGFONT structure unless you absolutely have to. The routines presented here will work on both operating systems.

The Display List GLTextOut Function
The Display List GLTextOut Function

The routines shown here are ANSI routines. If you must write for an international market and use Unicode, the text-processing routines presented here must be modified to use the macros provided in the compiler-supplied header files. These are simple wrappers that simplify writing code that must be compiled for both ANSI and Unicode.

The wglUseFontOutlines() Function

The wglUseFontOutlines() function creates a set of 3D polygon- or line-based primitive display lists, based on the glyphs in the currently selected TrueType font in the current DC for use in the current OpenGL RC. Stroke and raster fonts are not supported. These objects can then be used to draw 3D characters in an OpenGL image. The em-square size of the font is mapped to 1.0 in the x and y directions, and the characters are positioned in the xy plane in the display lists.

The wglUseFontOutlines() function takes a DC argument and then values that indicate the starting character, the number of characters to create, and the value to use as the base offset for the generated display lists. So far this looks like the wglUseFontBitmaps() function. However, additional arguments control the extrusion of the 3D characters in the +z direction (the depth of the generated characters), the deviation of the generated primitive vertices from the design outline of the font (how blocky you want the resulting characters to be), whether to generate filled polygons or a wire-frame primitive, and finally an array of structures to hold the metrics of each of the generated characters. This last function gives you tighter control over the positioning of the font by letting you do it yourself, if you want.

The wglUseFontOutlines() function approximates glyph outlines by subdividing the quadratic B-spline curves found in the TrueType outline into line segments until the distance between the outline and the interpolated midpoint is within the value specified by the deviation parameter you’ve supplied. This is the final format used when generated format is specified as line segments. When you specify polygons as the format, the outlines are further tessellated into separate triangles, triangle fans, triangle strips, or quadrilateral strips to create the surface of each glyph.

Each display list ends with a translation specified with the gmfCellIncX and gmfCellIncY fields of the corresponding GLYPHMETRICSFLOAT structure. This translation enables the drawing of successive characters in their natural direction with a single call to glCallLists(). A routine to generate true 3D characters is shown in Listing 7.7; we provide a routine that takes the name of a TrueType font and generates the display lists for each character. The return value is the ID for use as the base offset. These display lists are then used with the GLTextOut() routine discussed in Listing 7.6.

Example 7.7. Generating a 3D Display List Font

GLuint COpenGLView::GenerateDisplayListForFont(
        char* fontname, double xt )
{
    GLuint id;

    if ( 0 == m_pDC ||
        (GLuint)0 == (id = GetNewDisplayListIDs(256)) )
        {
        return 0;
        }
    LOGFONT logfont;
    GLYPHMETRICSFLOAT gmf[256];

    // lfHeight can't be used to change the font size
    logfont.lfHeight         = -12;
    logfont.lfWidth          = 0;
    logfont.lfEscapement     = 0;
    logfont.lfOrientation    = logfont.lfEscapement;
    logfont.lfWeight         = FW_NORMAL;
    logfont.lfItalic         = FALSE;
    logfont.lfUnderline      = FALSE;
    logfont.lfStrikeOut      = FALSE;
    logfont.lfCharSet        = ANSI_CHARSET;
    logfont.lfOutPrecision   = OUT_DEFAULT_PRECIS;
    logfont.lfClipPrecision  = CLIP_DEFAULT_PRECIS;
    logfont.lfQuality        = DEFAULT_QUALITY;
    logfont.lfPitchAndFamily =
            FF_DONTCARE|DEFAULT_PITCH;

    lstrcpy ( logfont.lfFaceName, fontname );
    CFont newfont;
    // CreateFontIndirect returns 0 if it fails
    BOOL success = newfont.CreateFontIndirect( &logfont );
    CFont* oldfont = m_pDC->SelectObject( &newfont );
    ASSERT( 0 != oldfont );

    // Create a set of display lists based on the glyphs
    // of the TrueType font
    // notice that we really waste the first 32 spaces....
    // if there's a problem delete the display lists
    if ( 0 == success ||
        FALSE == ::wglUseFontOutlines( m_pDC->m_hDC,
                0,   256,
                id, 0.0f,
                (float)xt, WGL_FONT_POLYGONS, gmf) )
        {
        ::glDeleteLists( id, 256 );
        id = 0;
        }
    else
        {
        m_pDC->SelectObject( oldfont );
        }
    return id;
}

Both the flat font and the 3D font routines are included as part of the COpenGLView class. A member function called GenerateDefaultFonts() provides a default flat font and a default 3D font as part of COpenGLView initialization. For an example of what these fonts provide, take a look at the following code when it is placed inside a RenderScene() member function. You’ll get bitmap text that is always legible and outline text that’ll rotate with the view.

::glColor3f( 0.0f, 0.0f, 1.0f );
::glRasterPos2f( 0.5f, 0.2f );
GLTextOut( m_DefaultFlatTextID, "OpenGL Bitmap Text" );

::glColor3f( 1.0f, 0.0f, 0.0f );
::glTranslatef( -2.5, 0.0, 0.0 );
::glScalef( .8f, .6f, 0.6f );
GLTextOut( m_DefaultTextID, "OpenGL 3D Text" );

As you can see, using the functionality provided by the COpenGLView class, you can easily add text to your OpenGL scene.

Summary

Using display lists enables you to speed up the rendering process by precalculating models or parts of models. The wrappered display list manipulation routines found in the COpenGLView class make it very easy to generate your own display lists and to later replay them. The added benefit is that you can also use these routines to assist in generating flat and 3D fonts that you can use in your model, with less effort than is normally required to select a GDI font. Since these fonts are stored in display lists, it’s easy to be able to mix calls to different fonts, with nothing more than merely changing the display list ID that you want to render with.

If you find yourself writing modeling code that resembles something you’ve written before, you should examine the code you’re writing to see whether you can replace all similar code with a call to a display list. If only minor parts are different, consider writing a hierarchical display list that encapsulates the differences. It’s easy to make a top-level display list call sublists and to dynamically modify those sublists merely by reassociating their IDs with different display lists. Even if you use a routine only once each time a scene is rendered, you still might get a big boost to your rendering speed by creating a display list, simply because of the way display lists are precomputed. With the COpenGLView class it’s easy to experiment and see what works out best.

Summary

Try This

  • Rewrite the GetNewDisplayListID() function described in this chapter to take an argument for the number of sequential display list IDs to generate. Make this new function use glGenLists(). Compare your code to the GetNewDisplayListIDs() function in the shared implementation of COpenGLView.

  • Write your own stock scene and use it.

  • Now make your stock scene a display list and use it.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset