While some parts of the code can be moved more central other parts need to be further duplicated and in doing so the need to fix the same issues in multiple places make things harder and harder to maintain.
For my goals however I don't need the preprocessor to do much so we can keep everything very simple and we'll limit the functionality to the following:
- support for a #include to insert the text from a file into our shader
- supplying a number of "defines" which we can trigger logic
- very basic #ifdef, #ifndef and #else logic that use these defines to include or exclude parts of the shader code
Changes to our system library
I was thinking about putting most of this code in our system.h file but decided against that for now. I may yet change this in the future. For now one support function has been added here:
// gets the portion of the line up to the specified delimiter(s) // return NULL on failure or if there is no text // returns string on success, calling function is responsible for freeing the text char * delimitText(const char *pText, const char *pDelimiters) { int len = 0; char * result = NULL; bool found = false; int delimiterCount; delimiterCount = strlen(pDelimiters) + 1; // always include our trailing 0 as a delimiter ;) while (!found) { int pos = 0; while ((!found) && (pos < delimiterCount)) { if (pText[len] == pDelimiters[pos]) { found = true; }; pos++; }; if (!found) { len++; }; }; if (len != 0) { result = malloc(len + 1); if (result != NULL) { memcpy(result, pText, len); result[len] = 0; }; }; return result; };This function splits off the first part of the text pointed to by pText from the start until it detects one of the delimiters or the end of the string.
This is fairly similar to the code we wrote before to read out material and object files line by line but without using our varchar implementation.
Changes to our varchar library
We are going to use varchar.h but in combination with a linked list to store our defines in. For this I've added 3 new functions:
// list container for varchars // llist * strings = newVarcharList() llist * newVarcharList() { llist * varcharList = newLlist((dataRetainFunc) varcharRetain, (dataFreeFunc) varcharRelease); return varcharList; };This first function simply returns a linked list setup to accept varchar objects.
// list container for varchars created by processing a string // empty strings will not be added but duplicate strings will be // llist * strings = newVarcharList() llist * newVCListFromString(const char * pText, const char * pDelimiters) { llist * varcharList = newVarcharList(); if (varcharList != NULL) { int pos = 0; while (pText[pos] != 0) { // find our next line char * line = delimitText(pText + pos, pDelimiters); if (line != NULL) { int len = strlen(line); varchar * addChar = newVarchar(); if (addChar != NULL) { varcharAppend(addChar, line, len); llistAddTo(varcharList, addChar); }; if (pText[pos + len] != 0) { // skip our newline character pos += len + 1; } else { // we found our ending pos += len; }; free(line); } else { // skip any empty line... pos++; }; }; }; return varcharList; };This method uses our new delimitText function to pull a given string appart and add each word in the string as an entry into a new linked list.
// check if our list contains a string bool vclistContains(llist * pVCList, const char * pText) { if ((pVCList != NULL) && (pText != NULL)) { llistNode * node = pVCList->first; while (node != NULL) { varchar * text = (varchar *) node->data; if (varcharCmp(text, pText) == 0) { return true; }; node = node->next; }; }; // not found return false; };And finally a function that checks if a given word is present in our linked list.
Changes to our shader library
The real implementation can be found in our shader library. We've added a new parameter to our newShader function so we can pass it the defines we want to use for that shader:
shaderInfo * newShader(const char *pName, const char * pVertexShader, const char * pTessControlShader, const char * pTessEvalShader, const char * pGeoShader, const char * pFragmentShader, const char *pDefines) { shaderInfo * newshader = (shaderInfo *)malloc(sizeof(shaderInfo)); if (newshader != NULL) { llist * defines; ... // convert our defines defines = newVCListFromString(pDefines, " \r\n"); // attempt to load our shader by name if (pVertexShader != NULL) { shaders[count] = shaderLoad(GL_VERTEX_SHADER, pVertexShader, defines); if (shaders[count] != NO_SHADER) count++; }; ... // no longer need our defines if (defines != NULL) { llistFree(defines); }; ... return newshader; };We first convert our new parameter pDefines into a linked list of varchars by calling our new newVCListFromString function.
We then pass our new linked list to each shaderLoad call so it can be used by our preprocessor.
Finally we deallocate our linked list and all the varchars held within.
The only change in shaderLoad is that it no longer called loadFile directly but instead calls shaderLoadAndPreprocess:
varchar * shaderLoadAndPreprocess(const char *pName, llist * pDefines) { varchar * shaderText = NULL; // create a new varchar object for our shader text shaderText = newVarchar(); if (shaderText != NULL) { // load the contents of our file char * fileText = loadFile(shaderPath, pName); if (fileText != NULL) { // now loop through our text line by line (we do this with a copy of our pointer) int pos = 0; bool addLines = true; int ifMode = 0; // 0 is not in if, 1 = true condition not found, 2 = true condition found while (fileText[pos] != 0) { // find our next line char * line = delimitText(fileText + pos, "\n\r"); // found a non-empty line? if (line != NULL) { int len = strlen(line); // check for any of our preprocessor checks if (memcmp(line, "#include \"", 10) == 0) { if (addLines) { // include this file char * includeName = delimitText(line + 10, "\""); if (includeName != NULL) { varchar * includeText = shaderLoadAndPreprocess(includeName, pDefines); if (includeText != NULL) { // and append it.... varcharAppend(shaderText, includeText->text, includeText->len); varcharRelease(includeText); }; free(includeName); }; }; } else if (memcmp(line, "#ifdef ", 7) == 0) { if (ifMode == 0) { char * ifdefined; ifMode = 1; // assume not defined.... ifdefined = delimitText(line + 7, " "); if (ifdefined != NULL) { // check if our define is in our list of defines if (vclistContains(pDefines, ifdefined)) { ifMode = 2; }; free(ifdefined); }; addLines = (ifMode == 2); } else { errorlog(SHADER_ERR_NESTED, "Can't nest defines in shaders"); }; } else if (memcmp(line, "#ifndef ", 8) == 0) { if (ifMode == 0) { char * ifnotdefined; ifMode = 1; // assume not defined.... ifnotdefined = delimitText(line + 7, " "); if (ifnotdefined != NULL) { // check if our define is not in our list of defines if (vclistContains(pDefines, ifnotdefined) == false) { ifMode = 2; }; free(ifnotdefined); }; addLines = (ifMode == 2); } else { errorlog(SHADER_ERR_NESTED, "Can't nest defines in shaders"); }; } else if (memcmp(line, "#else", 5) == 0) { if (ifMode == 1) { ifMode = 2; addLines = true; } else { addLines = false; }; } else if (memcmp(line, "#endif", 6) == 0) { addLines = true; ifMode = 0; } else if (addLines) { // add our line varcharAppend(shaderText, line, len); // add our line delimiter varcharAppend(shaderText, "\r\n", 1); }; if (fileText[pos + len] != 0) { // skip our newline character pos += len + 1; } else { // we found our ending pos += len; }; // don't forget to free our line!!! free (line); } else { // skip empty lines... pos++; }; }; // free the text we've loaded, what we need has now been copied into shaderText free(fileText); }; if (shaderText->text == NULL) { varcharRelease(shaderText); shaderText = NULL; }; }; return shaderText; };I'm not going to detail each and every section, I hope the comments do a good enough job for that. In a nutshell however, we start by creating a new varchar variable called shaderText which is what we'll end up returning. This means that our shaderLoad function also has a small change to work with a varchar instead of a char pointer as a result.
After this we load the contents of our shader file into a variable called fileText but instead of using this directly we use delimitText to loop through our shader text one line at a time.
For each line we check if it starts with one of our preprocessor commands and if so handle the special logic associated with it. If not we simply add our line to our shaderText variable.
#include is the first preprocessor command we handle, it simply checks the filename presented and attempts to load that file by calling shaderLoadAndPreprocess recursively.
This is followed by the code that interprets our #ifdef, #ifndef, #else and #endif preprocessor commands. These basically check if the given define is present in our linked list. They toggle the values of ifMode and addLines that control whether we ignore text in our shader file or add the lines to our shaderText.
Changes to our shaders
I've made two changes to our shaders, the first is that I've created a new shader files called "shadowmap.fs" that contains our samplePCF, shadow and shadowTest functions and we use #include in the various fragment shaders where we need these functions.
The second change is that I've combined our flatshader.fs, textured.fs and reflect.fs fragment shaders into a single standard.fs file that looks as follows:
#version 330 // info about our light uniform vec3 lightPos; // position of our light after view matrix was applied uniform float ambient = 0.3; // ambient factor uniform vec3 lightcol = vec3(1.0, 1.0, 1.0); // color of the light of our sun // info about our material uniform float alpha = 1.0; // alpha for our material #ifdef textured uniform sampler2D textureMap; // our texture map #else uniform vec3 matColor = vec3(0.8, 0.8, 0.8); // color of our material #endif uniform vec3 matSpecColor = vec3(1.0, 1.0, 1.0); // specular color of our material uniform float shininess = 100.0; // shininess #ifdef reflect uniform sampler2D reflectMap; // our reflection map #endif // these are in world coordinates in vec3 E; // normalized vector pointing from eye to V in vec3 N; // normal vector for our fragment // these in view in vec4 V; // position of fragment after modelView matrix was applied in vec3 Nv; // normal vector for our fragment (inc view matrix) in vec2 T; // coordinates for this fragment within our texture map in vec4 Vs[3]; // our shadow map coordinates out vec4 fragcolor; // our output color #include "shadowmap.fs" void main() { #ifdef textured // start by getting our color from our texture fragcolor = texture(textureMap, T); fragcolor.a = fragcolor.a * alpha; if (fragcolor.a < 0.2) { discard; }; #else // Just set our color fragcolor = vec4(matColor, alpha); #endif // Get the normalized directional vector between our surface position and our light position vec3 L = normalize(lightPos - V.xyz); // We calculate our ambient color vec3 ambientColor = fragcolor.rgb * lightcol * ambient; // Check our shadow map float shadowFactor = shadow(Vs[0], Vs[1], Vs[2]); // We calculate our diffuse color, we calculate our dot product between our normal and light // direction, note that both were adjusted by our view matrix so they should nicely line up float NdotL = max(0.0, dot(Nv, L)); // and calculate our color after lighting is applied vec3 diffuseColor = fragcolor.rgb * lightcol * (1.0 - ambient) * NdotL * shadowFactor; // now for our specular lighting vec3 specColor = vec3(0.0); if ((NdotL != 0.0) && (shininess != 0.0)) { // slightly different way to calculate our specular highlight vec3 halfVector = normalize(L - normalize(V.xyz)); float nxHalf = max(0.0, dot(Nv, halfVector)); float specPower = pow(nxHalf, shininess); specColor = lightcol * matSpecColor * specPower * shadowFactor; }; #ifdef reflect // add in our reflection, this is one of the few places where world coordinates are paramount. vec3 r = reflect(E, N); vec2 rc = vec2((r.x + 1.0) / 4.0, (r.y + 1.0) / 2.0); if (r.z < 0.0) { r.x = 1.0 - r.x; }; vec3 reflColor = texture(reflectMap, rc).rgb; // and add them all together fragcolor = vec4(clamp(ambientColor+diffuseColor+specColor+reflColor, 0.0, 1.0), fragcolor.a); #else // and add them all together fragcolor = vec4(clamp(ambientColor+diffuseColor+specColor, 0.0, 1.0), fragcolor.a); #endif }Note the inclusion of our #ifdef blocks to change between our various bits of logic while reusing code that is the same in all three shaders.
We can now change our shader loading code in engine.h to the following:
colorShader = newShader("flatcolor", "standard.vs", NULL, NULL, NULL, "standard.fs", ""); texturedShader = newShader("textured", "standard.vs", NULL, NULL, NULL, "standard.fs", "textured"); reflectShader = newShader("reflect", "standard.vs", NULL, NULL, NULL, "standard.fs", "reflect");If we need it we could very quickly add a fourth shader that combines texture mapping and reflection mapping by simply passing "textured reflect" as our defines.
In the same way I've combined our shadow shaders into a single shader file.
Obviously there is a lot of room for improvement here, but it is a start and enough to keep us going for a little bit longer.
Download the source here
What's next?
Now we're ready to start working on our deferred shader.