Thursday, December 5, 2013

Remaking SOUND and THREAD into Classes

This post continues my series about rewriting the code from Game and Graphics Programming for iOS and Android with OpenGL ES 2.0 to better leverage C++ abilities.  In Chapter 9 the author introduces his module which wraps the OpenAL library.  This wrapper is the SOUND module.  Also, the author introduces his module for managing threads, called THREAD.

To get a copy of the code as described in this post pull a copy from GitHub using the tag chapter9.

Most of the changes to SOUND are straight forward.  The original version of the module defines two data types:  SOUND, and SOUNDBUFFER. The original code implements a sort of poor man's polymorphism.  The module does this by providing two different ways to allocate and initialize a SOUNDBUFFER:  the functions SOUNDBUFFER_load(), and SOUNDBUFFER_load_stream().  In my rewrite of the module I mimic this by making a new class called SOUNDBUFFERSTREAM which is subclassed from SOUNDBUFFER.  The old SOUNDBUFFER_load() function becomes the constructor for the SOUNDBUFFER class, and SOUNDBUFFER_load_stream() becomes the constructor for the SOUNDBUFFERSTREAM class.  All of the other methods for the SOUNDBUFFERSTREAM class are inherited directly from the SOUNDBUFFER class.

I did notice that there is a fair amount of duplicate code between the constructors for the SOUNDBUFFER, and SOUNDBUFFERSTREAM classes so I created a new, private SOUNDBUFFER:init() method so that all of the formerly duplicated code is in one place.

The SOUND module leverages the AUDIO module.  The AUDIO module creates a singleton, that is, it creates a class which can only be used to create a single instance of the AUDIO data type.  The AUDIO module shares this trait with the GFX module, which is, in part, why I haven't yet rewritten the GFX module.  It's been suggested to me that to make the code more general purpose that the AUDIO module should be rewritten to support more than one audio device.  For the time being I've left AUDIO alone but have been experimenting with various ways of implementing AUDIO as a singleton class using various C++ techniques.

Regarding the changes needed to make THREAD into a class, there really isn't anything which hasn't been discussed in previous posts.

The next post will cover real C++ polymorphism when I discuss the changes I made to the LAMP data type, and its subclasses in Chapter 10.

Wednesday, December 4, 2013

Remaking NAVIGATION and FONT into Classes


This post continues my series about rewriting the code from Game and Graphics Programming for iOS and Android with OpenGL ES 2.0 to better leverage C++ abilities.  In Chapter 8 the author introduces his module which wraps the Recast and Detour libraries.  This wrapper is the NAVIGATION module.  Also, the author introduces his module for drawing text.  While previous code displayed text on the screen, it did so use Wavefront objects, not by dynamically rendering strings.  Now, with the addition of the FONT module, any string can be rendered at runtime.

To get a copy of the code as described in this post pull a copy from GitHub using the tag chapter8.

Many of the changes to the NAVIGATION module are routine, that is, they are things which have been seen numerous times in previous posts:
  • where appropriate change while loops to for loops, and
  • where appropriate change unsigned char to bool or const bool.
The first change to NAVIGATION which is out of the ordinary is the old function NAVIGATION_set_default_configuration().  The old NAVIGATION_init() function used it to initialize the data member navigationconfiguration which, unsurprisingly, is of data type NAVIGATIONCONFIGURATION.  I changed NAVIGATIONCONFIGURATION into a C++ class, and remade NAVIGATION_set_default_configuration() into its constructor.  The NAVIGATION constructor implicitly calls the NAVIGATIONCONFIGURATION constructor to initialize the NAVIGATION object's navigationconfiguration data member.

While rewriting the old NAVIGATION_build() function into a class method I recognized that the "OBJ *obj" and, "unsigned int mesh_index" parameters were indicating that the actual argument to the NAVIGATION_build() function is an OBJMESH object so the signature for NAVIGATION::build() reflects this actuality.  Inside the NAVIGATION::build() method the code needs to access some of parent's data structures.  I've discussed similar situations in the post where I described rewriting SDK/common/obj.cpp.

On to rewriting the FONT module.

I fixed a bug in the FONT::load() method (fka FONT_load()).  There was only one return statement and it always returned false.  I added a second return statement which returns true for those situations where the method is successful.

When rewriting the FONT::load() method I noticed that when the sample code calls this method it always passes in this‑>name as the parameter filename.  As a class method FONT::load() already has access to this‑>name without having it passed in as a parameter.  I debated about whether I should remove filename from the parameter list for the method and rewrite the body of the method to use this‑>name explicitly.  Ultimately, I decided against this because, I speculated, there may actually be some useful case where the filename is different than this‑>name.

Changes to the various sample programs to use the "class-ified" versions of NAVIGATION and FONT are straight forward so I'm omitting their description from this post.

In my next post I'm going to discuss rewriting the SOUND, and  THREAD.

Tuesday, December 3, 2013

Optimizing Calculation of the Model-View Matrix

This post continues my series about rewriting the code from Game and Graphics Programming for iOS and Android with OpenGL ES 2.0 to better leverage C++ abilities.  In Chapter 7 the author discusses various means of manipulating the camera.  In reviewing the sample code for this chapter I think that the code can be made more efficient.  Doing so doesn't have any significant impact on the sample code for the book.  However, real games are likely to need every bit of performance they can get, especially running on mobile platforms.

To get a copy of the code as described in this post pull a copy from GitHub using the tag chapter7.

The sample program chapter7‑1 has the follow sequence of code in its templateAppDraw() function:

    GFX_load_identity();
    .
    .
    .
    GFX_translate(eye_location.x,
                eye_location.y,
                eye_location.z);
    GFX_rotate(rotz, 0.0f, 0.0f, 1.0f);
    GFX_rotate(90.0f, 1.0f, 0.0f, 0.0f);
    mat4_invert(GFX_get_modelview_matrix());

As you can see the code uses the routine mat4_invert() to do a matrix inversion of the Model-View matrix.  Matrix inversion is a relatively expensive numerical operation.  In the author's defense the mat4_invert() routine has been hand coded to exploit the particular features of 4D homogeneous space transformation matrices.  Still, if the code could avoid doing any matrix inversions at all the code would be more efficient.

I don't intend to write any fully qualified math proofs but I do need to introduce a few details for readers who aren't familiar with linear algebra, and/or need a refresher before forging ahead.

When you learned to do math using fractions you learned that when you divide by a fraction that it's the same thing as multiplying by that fraction's reciprocal.  When a number is divided by itself the result is 1 (one).  If that number is a fraction dividing it by itself is the same as multiplying the fraction by its own inverse.  If the fraction is equivalent/equal to the number 0 (zero), its reciprocal (aka its multiplicative inverse) isn't a "real" number.

Matrices have a similar concept, i.e., a matrix can have a multiplicative inverse.  One of the conditions for a matrix to have a multiplicative inverse is that the matrix must be square, i.e., it must have the same number of rows as it has columns.  Another condition required for having an inverse is that the matrix must not be 0 (zero) but, for the time being I'm not going to explain what that means in the context of matrix algebra.  If a matrix A has an inverse, which we call A‑1, the product of that matrix and its inverse is the identity matrix.  An identity matrix is like the matrix version of the number 1 (one).  An identity matrix is a square matrix which has 1 (one) along its diagonal, and 0 (zero) everywhere else.  For example, the 4×4 identity matrix is normally written as:



1 0 0 0
0 1 0 0
0 0 1 0
0 0 0 1


If we have a matrix M which is the product of the matrices A, and B, and if M has an inverse, the inverse of M (aka M‑1) can be found by

    M‑1 = (A × B)‑1 = B‑1 × A‑1

Going back to the code above each call to GFX_rotate(), and GFX_translate() executes a matrix multiplication:

    GFX_load_identity(); // Load the matrix version of "1"
    .
    .
    .
    GFX_translate(eye_location.x, // Times Matrix A, i.e.,
                eye_location.y, // 1 * A is A.
                eye_location.z);
    GFX_rotate(rotz, 0.0f, 0.0f, 1.0f); // Times Matrix B
    GFX_rotate(90.0f, 1.0f, 0.0f, 0.0f); // Times Matrix C

At the end of this sequence of function calls the Model-View matrix holds the product of A × B × C.  The next step

    mat4_invert(GFX_get_modelview_matrix());

calculates the inverse of this matrix product and stores it back in the Model-View matrix.

[Note:  Matrix multiplication, unlike the multiplication of real numbers, is not, in general, commutative.  The matrix operations must be performed in the order described below in order to get consistently correct results.]

What is the inverse of A × B × C?

    (A × B × C)‑1 = C ‑1 × B‑1 × A‑1

So, if after loading the Model-View matrix with the identity matrix the code multiplied by the inverse of C, then by the inverse of B, and finally by the inverse of A, the Model-View matrix would have the same results of multiplying by A, B, C, and then taking the inverse.

That is, we can change the order so that instead of performing a translation followed by two rotations, we can reverse the order as follows to do two rotations followed by a translation:

    GFX_rotate(-90.0f, 1.0f, 1.0f, 1.0f);// Note that the angle
                                         // of rotation is
                                         // negated.
    GFX_rotate(-rotz, 0.0f, 0.0f, 1.0f); // Likewise, here.
    GFX_translate(-eye_location.x, // And here the
                -eye_location.y, // direction of
                -eye_location.z);      // translation is
                                         // negated.

This has the same effect as the original code but without the cost of the matrix inversion.

[Note:  I'm not going to do a rigorous mathematical proof that negating the rotation angle calculates the matrix inverse for rotation, or that negating the translation amount generates the matrix inverse for translation.  I'm just going to make the assertion and leave it at that.]

Because the new code avoids the call to mat4_inverse() we can now calculate the same Model-View matrix with:
  • 21 fewer floating point multiplies,
  • 8 fewer floating point additions,
  • 1 fewer floating point division, and
  • 1 fewer matrix copy operation.
Again, the sample programs are small enough that it's probably not a big deal but in a real game where you need to make every CPU cycle count it may be the edge you need to get smooth animation.  Remember, these calculations happen every time the game calculates a new frame, perhaps 30 times, or more, per second.

I'm not done optimizing the code.  Going back to the revised code we have:

    GFX_load_identity();
    .
    .
    .
    GFX_rotate(-90.0f, 1.0f, 1.0f, 1.0f);
    GFX_rotate(-rotz, 0.0f, 0.0f, 1.0f);
    GFX_translate(-eye_location.x,
                -eye_location.y,
                -eye_location.z);
    for (auto objmesh=obj-objmesh.begin;
        objmesh!=obj->objmesh.end(); ++objmesh) {
        GFX_push_matrix();
GFX_translate(objmesh->location.x,
                    objmesh->location.y,
                    objmesh->location.z);
.
.
.
GFX_pop_matrix();
    }

Notice that in this version of the code we have consecutive translations.  First the code translates "away" from the eye location, and then translates to the mesh location.  Without any proof I'm going to assert that these two translations can be performed as a single translation by changing the code to:

    GFX_load_identity();
    .
    .
    .
    GFX_rotate(-90.0f, 1.0f, 1.0f, 1.0f);
    GFX_rotate(-rotz, 0.0f, 0.0f, 1.0f);
    for (auto objmesh=obj-objmesh.begin;
        objmesh!=obj->objmesh.end(); ++objmesh) {
GFX_push_matrix();
GFX_translate(objmesh->location.x-eye_location.x,
                    objmesh->location.y-eye_location.y,
                    objmesh->location.z-eye_location.z);
.
.
.
GFX_pop_matrix();
    }

Again, this saves the code a few floating point operations, and every little bit helps.

[Note:  There is a similar optimization which can be made to express the two rotations as a single rotation, but I don't yet have all the tools in place to do the optimization easily.  I promise, I'll get there.]

Let's move on to sample program chapter7-2.  If the code is changed to move the translation operation inside the loop it causes breakage.  See Figure 1.

Figure 1 is a screen capture where the two translations have been performed as a single operation.  Because the sample program chapter7-2 uses a frustum to decide which objects to cull in an effort to optimize drawing performance, moving the first translation past the calculation of the frustum introduces a bug.  The point of telling you this is to help you to avoid errors in your own code.  There are order dependencies lurking around every corner waiting to devour the unwary programmer.  It might be possible to tweak the calculation of the frustum to work around the problem, but I haven't tried to do so.

These examples hit the highlights of how one might optimize some of the transformation stack code.  Similiar changes have been made to the sample programs:

  • chapter7‑3,
  • chapter7‑4,
  • chapter10‑1,
  • chapter10‑2,
  • chapter10‑3,
  • chapter10‑4,
  • chapter10‑5,
  • chapter10‑6,
  • chapter11‑1,
  • chapter11‑2,
  • chapter11‑3,
  • chapter11‑4,
  • chapter12‑1,
  • chapter12‑2,
  • chapter12‑3, and
  • chapter12‑4.
The next post will cover Chapter 8, and discuss rewriting the author's NAVIGATION, and FONT modules.

Making MEMORY a Class

This post continues the discussion of the book Game and Graphics Programming for iOS and Android with OpenGL ES 2.0.  Since Chapters 5 and 6 don't introduce any of the author's modules for writing games I'm going to use this post to

  • discuss cleaning up some of the utility functions,
  • I'm going to finally admit that I really should have made MEMORY a class a long time ago, and
  • I'm going to discuss a variety of other changes I've made to the code as I improve my knowledge of C++.
To get a copy of the code with the changes described in this post pull a copy from GitHub using the tag MEMORY.

I'm tired of making some of these changes piecemeal.  This is especially true with respect to the code passing unsigned char variables when what it's really passing are bool values.  The files SDK/common/utils.cpp and SDK/common/memory.cpp implement a number of such functions.  I realized my reasons for postponing this work weren't sensible, and with Chapters 5 and 6 not introducing any new modules I decided to quit procrastinating.  In the utils.cpp and utils.h files I:
  • changed the third argument to get_file_extension() from unsigned char to const bool,
  • changed the return types of the functions point_in_frustum(), and box_in_frustum() to bool.
  • added a new enumerated type InFrustum,
  • changed the return type of the functions sphere_intersect_frustum(), and box_intersect_frustum() to be the enumerated type InFrustum,
  • replaced various while loops with for loops,
  • added comments to note various portions of the code which aren't well documented so they can be found for later study, and
  • added a comment to point out a future strategy of using the vec4 dot product computation to make the code more readable.
The changes to use bool parameter types and return values necessitated changes in the OBJ, SOUND, and TEXTURE modules.  Any place in these three modules where get_file_extension() is called was changed to pass a bool value by changing the use of 0, or 1 to false, or true, respectively.  But you knew that already from previous posts, right?

The functions point_in_frustum(), box_in_frustum(), sphere_intersect_frustum() and box_intersect_frustum() are never used in any of the other modules, or in the sample programs so there are no changes which need to be propagated as a result of their respective changes.

To see the use of the vector dot product to simplify the code search for the function vec4_dot_vec4() in the file SDK/common/utils.cpp.  The old code used a somewhat complicated method of multiplying the frustum vector with the location vector.  The complication was caused by the frustum vector being of type vec4, and the location vector being of type vec3.  Without getting into the theory of the mathematics of 3D graphics transformations the 4D frustum vector describes a plane in homogeneous space.  The simple way of converting a 3D location (vec3) into its equivalent location in homogeneous space is to add a 1 (one) as the vector's fourth component.  You should be able to quickly prove to yourself that the dot product of the frustum vector and the location vector described in homogeneous space computes the same value as the original code.  Finally/Obviously, the function vec4_dot_vec4() computes the dot product of two 4D vectors (vec4).  If you look through the rest of the file you'll see many places this simplification can be made.  Resist the temptation to manually fix the code.  Later on I'll have more tools in place, and those tools (I hope) will make the task much easier.

Moving on to SDK/common/memory.cpp I was simply going to change the definition of mopen() to use const bool as the type of its second argument.  But, as I was poking around, I changed my mind about leaving the MEMORY data type as a C-type struct, and so I changed MEMORY into a full C++ class.  Most of the changes are unremarkable.  The mopen() function was the basis for the constructor, the mclose() function was the basis for the destructor, and the functions mread(), and minsert() became the class methods read(), and insert(), respectively.

The one new wrinkle now that MEMORY is a full class is that I needed to worry about whether the compiler was going to implicitly create a copy constructor, and an assignment operator behind my back, and then, even worse, use them implicitly, and introduce bugs.  Why would this, potentially, introduce bugs?  Because the MEMORY data type has a pointer to dynamically allocated memory.  If the code makes a copy of a MEMORY object (which is what will happen using a copy constructor, or an assignment operator), the programmer needs to start making decisions about how dynamic memory is handled for this object class. For example:

  • When making a copy of a MEMORY object does the new object point to same dynamically allocated space as the object it was copied from?
  • If two MEMORY objects point to the same dynamically allocated memory space how does the code coordinate the releasing of the dynamic memory space with the destruction of the MEMORY objects?
  • Does the new MEMORY object have its own private copy?
None of the code provided by the author needs to copy a MEMORY object so I solved this problem by declaring my own copy constructor, and assignment operator for the class but I declared them as private.  If any function which is not a method of the MEMORY class, or a friend of the MEMORY class, would cause the compiler to implicitly invoke the copy constructor, or assignment operator the compiler will generate an error.  What about the case that a MEMORY method (or a friend of the MEMORY class) needs to implicitly invoke one of these methods?  Since I've declared the functions in the header file but not provided implementations of the functions the link editor will generate an error message when it is run.  Delaying the error message until the link stage, rather than in the compile stage, is a bit of an inconvenience, but if you want to compile using the C++ 11 standard the compiler can detect the problem in the compile stage.  The comments in the SDK/common/memory.h file discuss all the details.

Before moving on I should add that I probably should have added a private default constructor for the MEMORY class for the same reasons I added private versions of the copy constructor, and the assignment operator.  And I should re-examine the modules which have already been converted to classes, and decide on a case-by-case basis whether they also need to have private default constructors, copy constructors, and/or assignment operators.

It's a fairly simple process to change the various calls to mopen() to use "new MEMORY(…)", and so on.  This change, however, does have a ripple effect.  The functions FONT_load(), MD5_load_mesh(), and MD5_load_action(), and the class methods OBJ::load_mtl(), one of the OBJ constructors, one of the PROGRAM constructors, PROGRAM::load_gfx(), PROGRAM::build(), one of the TEXTURE constructors, and TEXTURE::build() pass an argument to the MEMORY constructor which they, in turn, received as arguments from the code which invoked them.  The corresponding arguments to each of these functions, and/or class methods needed to be changed to be of type const bool.  Likewise, up through the call stack.  Every place where one of these methods was called I changed the code to pass a bool value.  In most cases, this involved replacing the integers 0 (zero), and 1 (one), with false, and true, respectively.  But you've seen me do this several times before; sorry if it's getting tedious.

As always, I keep finding ways to clean my code up.  One of these is that I changed the PROGRAM::init() method to test if the pointer to the name string is NULL.  If it is, the method clears the name field of this to all zeroes.  Otherwise, like before, the method tests to make sure that the name string will fit in the space allotted by the PROGRAM class, and, if possible, copies the string into the name field.

The last clean-up in this release is to change the copy constructors for OBJMESH, OBJTRIANGLELIST, and OBJMATERIAL to use initializer lists.  Again, I should probably go back through the other classes I've implemented to see where initializer lists could be used for the copy constructors.

In my next post I will be talking about the code from Chapter 7 (duh!).  Chapter 7, like Chapters 5 and 6, doesn't introduce any new modules by the author so I will discuss a small optimization which can be made.

Friday, November 22, 2013

Improving Chapter 4 Maintainability

This post takes a break from rewriting various modules from the book Game and Graphics Programming for iOS and Android with OpenGL ES 2.0 to focus on some minor style issues which can impact maintainability.  I'm taking the time to cover these issues here partly because Chapter 4 doesn't introduce any new modules and partly because these particular issues first come up in this chapter.

In previous posts I've ranted about the use of "magic numbers", i.e., numeric constants which don't state explicitly for the reader what their meanings are.  Such as using zero, and one for boolean values, or using integer constants as array indices without explaining to the reader what a particular array element represents.  In Tables 4-1, and 4-2 the author introduces his conventions for assigning various texture map types to OpenGL texture units, and how various vertex attributes are assigned locations.  I like that the code uses a consistent standard throughout the examples starting in Chapter 4.  What I don't like is that if someone were to pick up these modules and reuse them in a work environment that uses a different standard for assigning texture maps to texture units, and/or assignment of vertex attributes to locations that the process of converting the code to use a new set of assignments could become error prone.

In an earlier post I mentioned that I added three new enumeration types to the SDK/common/obj.h file.  I talked about the AttributeOffset enumeration type but deferred discussion of the TextureMap, and VertexAttribute types until now.  The material_draw_callback() function (using my changes to the PROGRAM data type) would look something like

    void material_draw_callback(void *ptr)
    {
        OBJMATERIAL *objmaterial = (OBJMATERIAL *)ptr;
        PROGRAM *program = objmaterial->program;

        for (auto it=program->uniform_map.begin();
             it!=program->uniform_map.end(); ++it) {
            auto    &name = it->first;
            auto    &uniform = it->second;
            if (name == "DIFFUSE") {
                glUniform1(uniform.location, 1);
            } else if (name == "MODELVIEWPROJECTIONMATRIX") {
                .
                .
                .
            }
        }
    }

Looking at Table 4-1 from the book you'll see that it specifies that the diffuse texture map gets assigned to texture unit 1, and that, indeed, when the code above is processing the "DIFFUSE" uniform variable for the shader program the code specifies that the code will use texture unit 1 for the diffuse texture map.  Here is where I think using enumerated types can make the code more easily maintainable.  The type TextureMap is declared as

    enum TextureMap {
        TM_map_Ka       = 0,
        TM_map_Kd       = 1,
        TM_map_Ks       = 2,
        TM_map_Disp     = 3,
        TM_map_Bump     = 4,
        TM_map_Tr       = 5,
        TM_Unused       = 7,

        TM_Ambient      = TM_map_Ka,
        TM_Diffuse      = TM_map_Kd,
        TM_Specular     = TM_map_Ks,
        TM_Displacement = TM_map_Disp,
        TM_Bump         = TM_map_Bump,
        TM_Transparency = TM_map_Tr,
        };

Note that I've declared two different names for the values zero through five.  I don't intend that a program, should use both names for a particular value.  Instead, I define both names because different people will prefer different conventions.  I personally prefer the second set of names for the values zero through five because they're not tied to a particular file type (Wavefront), and so I think they're more generic/descriptive than the names which come out of the Wavefront files.  Also, note that I defined the second set of names for values zero through five using the first set of definitions.  This way changing the first set of definitions automatically changes the corresponding name in the second set of definitions.  I expect that, in practice, an individual programmer, or a development team would pick one set of definitions, and delete the others, if either (i.e., the programmer or the team) were to adopt my changes to the code.

Using these values the code

    glUniform1(uniform.location, 1);

becomes

    glUniform1(uniform.location, TM_Diffuse);

Now, if someone comes along and decides that since most, or all, of their models will have diffuse texture maps, but none of their models will have ambient texture maps, it's easy to redefine TM_map_Kd/TM_Diffuse to use texture unit 0.  Of course, the corresponding change needs to be made in the methods for the OBJ class, or whatever other class might be used to load the graphics data.

While I'm changing this routine there is a second problem.  The code searches for the diffuse texture unit location by comparing against the string literal "DIFFUSE".  The compiler can only detect whether the code provides a syntactically valid string literal; it can't do any checking that the string is valid in the context of the program.  So, the compiler would be just as happy to accept the string literals "DIFFUES", and/or "DEFUSE".  To leverage the compiler to help find typos I've also defined a set of strings with symbolic names which parallel the values declared in the TextureMap enumeration type.  Using the symbolic names changes the test to:

    if (name == TM_Diffuse_String) {
        .
        .
        .
    }

In Xcode the programmer might not even need to type out the full symbolic name; the editor will often, helpfully, suggest symbols which match the first few letters of the name, and the programmer can pick from one of the suggestions.  Even if the editor doesn't suggest various symbolic names, and the programmer misspells the name, it's likely that the misspelling won't be a valid symbol and the compiler will detect, and report the error.

In the case that the programmer simply uses the wrong string symbol the code might end up looking something like:

    if (name == TM_Ambient_String) {
        glUniform1(uniform.location, TM_Diffuse);
    } else if (name == "MODELVIEWPROJECTIONMATRIX") {
        .
        .
        .
    }

and when the code doesn't work as expected I think it's easier for the programmer to see that TM_Ambient_String and TM_Diffuse probably don't belong together, and, hopefully, this makes debugging the issue easier.

The enumeration type VertexAttribute serves a similar purpose as the TextureMap enumeration type, but instead of mapping the location of shader uniform variables the VertexAttribute enumeration type is used to map vertex attributes (duh!).  The old version of the function program_bind_attrib_location() looked something like:

    void program_bind_attrib_location(void *ptr) {
        PROGRAM *program = (PROGRAM *)ptr;
        glBindAttribLocation(program->pid, 0, "POSITION");
        glBindAttribLocation(program->pid, 2, "TEXCOORD0");
    }

Using the symbolic names the code now looks like

    void program_bind_attrib_location(void *ptr) {
        PROGRAM *program = (PROGRAM *)ptr;
        glBindAttribLocation(program->pid, VA_Position,
                             VA_Position_String);
        glBindAttribLocation(program->pid, VA_TexCoord0,
                             VA_TexCoord0_String);
    }

Also, have a look at SDK/common/obj.cpp and you'll see that I've used the VertexAttribute enumeration type values to, IMHO, make the OBJMESH::set_attributes() method less cryptic.

If you look at sample program chapter4-5 you'll see that I have also added a set of symbolic names for strings related to the material properties which all have the prefix "MP_"

I've changed the code to use these symbolic names for all of the sample code starting in Chapter 4 through the end of the book.  You can pull a copy of this version of the code from GitHub using the tag chapter4.

Notice that all of the string literals I've been replacing refer to items in the modules provided by the author, and also to variable names which are used in the shader programs.  You might have noticed that I didn't replace the literal string "MODELVIEWPROJECTIONMATRIX" even though, it too, is a uniform variable in the shader programs.  Perhaps I should have but remember that one of the reasons for moving away from string literals and magic numbers is to leverage the compiler to find errors in the code.  The literal string "MODELVIEWPROJECTIONMATRIX" in the sample programs doesn't refer to anything in any of the author's modules; it only refers to a uniform variable in the shader programs.  The compiler doesn't (and can't) ensure consistency between the C++ code and the GLSL (aka OpenGL Shader Language) code so there is less benefit gained from replacing this literal string with a symbolic name.  If you think that this change is beneficial to you, please feel free to make the change in your copy of the code; I said there is less benefit, not that there is no benefit.  A programmer could implement a strategy to iterate over all the symbolic names for strings and try to look up the strings' addresses in the shader program.  If the corresponding variable is missing from the program the code could signal an error.  The programmer could go so far as to ensure that the variables are also of the correct type, either uniform, or attribute.  This wouldn't tell the programmer that there is a problem at compile time, only at run time, but it does still provide some level of protection against programming errors.  If I were going to implement this strategy I would probably bracket the code so that it is only included when the code is compiled in debug mode using something like:

    #ifdef  DEBUG
        // Loop over strings to see if they have a match in
        // the shader program
        .
        .
        .
    #endif  /* DEBUG */

One last thing to consider before wrapping up this post.  Both the VertexAttribute and AttributeOffset enumeration types define symbols representing the same things:  the five vertex attribute values position, normal, fnormal, texture coordinate 0, and tangent 0.  Since the code now uses symbolic names there isn't really any reason to keep both; I could easily remove (and have removed) the AttributeOffset enumeration type.  That the two  enumeration types number the attributes in a different order is of no consequence.  It might be if, instead of deleting the definition of AttributeOffset, I deleted the definition of VertexAttribute.

Next there are no new classes to be created for Chapters 5 and 6 so next time I'll talk about cleaning up some of the utility routines.