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.
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.