Monday, February 20, 2012

The Texturing


In this post I'm changing the default Xcode 4 OpenGL Game project to load a texture file and use it when drawing the two cubes.

I started with a brand new OpenGL Game project.  See my previous post if you don't know how to create one.  If you run the program after creating the project you will see two cubes orbiting about the Y-axis.  The blue one is drawn using the vertex and fragment shaders, and the red one is drawn using Apple's fixed functionality pipeline.  Our goal is to overlay a texture on both cubes.


The first thing we need to do is find / create our texture and load it into our project.  For my texture, I got a screenshot of the Google blogger loading icon and used Mac Preview to crop out the rounded edges and resize it to 64 x 64 (I tried an 84 by 84 texture and it didn't work.  Apparently, in iOS textures are still required to have lengths and widths that are powers of two).  I saved it as bloogle.png on my desktop.

To load the texture into your project just drag the file from wherever it is on your computer into the Supporting Files folder in the Project navigator in Xcode.  Make sure "Copy items into destination group's folder (if needed) is checked, then click the "Finish" button.


Now the file is in the project, but we still need our program to load the image data into memory so OpenGL can use it.  To do this we will create a couple of new functions and a member variable in the view controller .m file.

In the interface section of the view controller .m file, add the following line:

    GLKTextureInfo * _texture;

This object will end up holding all the information OpenGL needs about our texture.

In the function declarations section add the following lines:

- (void)loadTextures;
- (void)logError:(NSError *) error;

This declares our loadTextures function, which we will use to load our texture data into memory, and a logger function that will help use figure out what we are doing wrong.  I added error logging after I googled my own errors and found that a lot of other people were posting on Stack Overflow with inscrutable texture loading failures of their own.  Hopefully this will help you avoid that.

After you add the two function declarations a warning may pop up complaining about "Incomplete implementation."  We are going to address that next by implementing the two functions.

At the end of the view controller .m file, right above the @end statement, add the following function implementation skeletons:

#pragma mark -  Texture Loading

- (void)loadTextures
{
    
}

- (void)logError:(NSError *) error
{
    
}


The pragma mark statement makes it easy to find your functions in the bread crumb navigator at the top of the editor.  Notice how your functions have their own little section in the symbol drop down.


Now add the following statements to the loadTextures function you just created:

    NSError *error = nil;   // stores the error message if we mess up
    NSDictionary *options = [NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES
                                                        forKey:GLKTextureLoaderGenerateMipmaps];
    
    NSString *bundlepath = [[NSBundle mainBundle] pathForResource:@"bloogle" ofType:@"png"];
    
    _texture = [GLKTextureLoader textureWithContentsOfFile:bundlepath options:options error:&error];
    
   [self logError:error];

First we declare a NSError object to hold our error message.  This isn't strictly necessary.  If you want, you can pass nil to the textureWithContentsOfFile function.  However, the error object is about the only way to get a meaningful error message out Xcode, and you can spend a lot of time banging your head against the wall without it.

Next we declare our options dictionary.  This is also not strictly needed for our purposes.  You can pass a nil value in for this as well.  But I wanted to show you how to go about creating an options dictionary in case you didn't know how.  This options dictionary tells the texture loader to automatically create mipmaps for the texture.

Next, we get the path of our image resource.  You can't just use "bloogle.png".  Also note the lack of a period in the parameters we pass to this function.

Finally we generate the texture.  This one function call is equivalent to quite a few OpenGL calls.  It's responsible for loading the image data, passing the image data into OpenGL, getting a texture id back, and setting a number of OpenGL parameters for the texture.  Refer to any good OpenGL reference manual to get an idea of the things you don't have to do here.

After we (attempt to) generate the texture we pass the error to our logging function.  If something went wrong, the logging function will output something meaningful to the console.  To make this happen add the following statements to the logError function:

    if (error) 
    {
        NSString * domain = [error domain];
        NSLog(@"Error loading texture: %@.  Domain: %@", [error localizedDescription],domain);
        NSDictionary * userInfo = [error userInfo];
        if (domain == GLKTextureLoaderErrorDomain)
        {
            if (nil != [userInfo objectForKey:GLKTextureLoaderErrorKey]) 
                NSLog(@"%@", [userInfo objectForKey:GLKTextureLoaderErrorKey]);
            if (nil != [userInfo objectForKey:GLKTextureLoaderGLErrorKey]) 
                NSLog(@"%@", [userInfo objectForKey:GLKTextureLoaderGLErrorKey]);
        }
    }

The outer if statement checks to see if there was an error at all.  If everything went ok, our error object should still be nil. 

If there was an error, the first thing we do get the error domain.  Each part of the the iOS framework has its own error domain, and each domain has a different set of error codes.  To find out what an error code means, you must know what domain the code is coming from.

Next we output the domain and a high level description of the error to the console.  Other than the error number, the description tends to be vague and worthless.

Depending on the error domain, you may be able to pull a more helpful message out of the error object.    The next lines get a userInfo dictionary out of the error object, which may more information.  If the domain is from the GLKTextureLoader itself, we try to pull two additional error messages out of the dictionary and output them to the console.



I'll show you how to induce some of these error messages in a second, but first lets get our program working correctly.  Add the following line of code to the setupGL function, right below the call to loadShaders:

[self loadTextures];


If you run the project now you shouldn't see any error messages in the console, which is good, except it's not testing our error logging function.  To induce an error, change the textureWithContentsOfFile function call so that we are passing in the filename of texture directly instead of using the bundle path:

_texture = [GLKTextureLoader textureWithContentsOfFile:@"bloogle" options:options error:&error];


Now when we run the program we get the following error in our console:

2012-02-21 00:04:22.839 TheTexturing[7689:10103] Error loading texture: The operation couldn’t be completed. (Cocoa error 260.).  Domain: NSCocoaErrorDomain

This is a generic unhelpful message from the NSCocoaErrorDomain.  If you know how to get a useful message out of this error programically let me know.  I ended up hunting down Apple's documentation which told me that error code 260 in the NSCocoaErrorDomain was a "NSFileReadNoSuchFileError" which makes sense since we futzed with the filename to get the error.  Don't forget to change the function call back to the way it was before continuing.

Another way to induce an error is to change the loadTexture call in setupGL so that it is before the setCurrentContext call.  

When you run the program now, you should get the following errors in the console:

2012-02-20 23:56:30.378 TheTexturing[7631:10103] Error loading texture: The operation couldn’t be completed. (GLKTextureLoaderErrorDomain error 17.).  Domain: GLKTextureLoaderErrorDomain
2012-02-20 23:56:30.383 TheTexturing[7631:10103] Invalid EAGL context

The first error is our generic unhelpful error.  But since it's from the GLKTextureLoaderErrorDomain we also get a slightly more helpful message informing us that our EAGL context is invalid.  This is because we called our load texture function before we set the context.  Move the load texture function call back to where it was to make this error go away.  The complete list of error codes for the GLKTextureLoaderErrorDomain can be found in Apple's documentation.

Now that we've loaded the image data into a texture, we need to tell OpenGL how to use that texture.  First, we will apply the texture to the red fixed function cube.

The first thing we need to do is add texture coordinates to the vertex data near the top of our view controller .m file.  Find the declaration of the gCubeVertexData array and change it like so:

GLfloat gCubeVertexData[] = 
{
    // Data layout for each line below is:
    // positionX, positionY, positionZ,     normalX, normalY, normalZ, texCoordS, texCoordT
    0.5f, -0.5f, -0.5f,        1.0f, 0.0f, 0.0f,    0,1,
    0.5f, 0.5f, -0.5f,         1.0f, 0.0f, 0.0f,    1,1,
    0.5f, -0.5f, 0.5f,         1.0f, 0.0f, 0.0f,    0,0,
    0.5f, -0.5f, 0.5f,         1.0f, 0.0f, 0.0f,    0,0,
    0.5f, 0.5f, 0.5f,          1.0f, 0.0f, 0.0f,    1,0,
    0.5f, 0.5f, -0.5f,         1.0f, 0.0f, 0.0f,    1,1,
    
    0.5f, 0.5f, -0.5f,         0.0f, 1.0f, 0.0f,    1,1,
    -0.5f, 0.5f, -0.5f,        0.0f, 1.0f, 0.0f,    0,1,
    0.5f, 0.5f, 0.5f,          0.0f, 1.0f, 0.0f,    1,0,
    0.5f, 0.5f, 0.5f,          0.0f, 1.0f, 0.0f,    1,0,
    -0.5f, 0.5f, -0.5f,        0.0f, 1.0f, 0.0f,    0,1,
    -0.5f, 0.5f, 0.5f,         0.0f, 1.0f, 0.0f,    0,0,
    
    -0.5f, 0.5f, -0.5f,        -1.0f, 0.0f, 0.0f,   1,1,
    -0.5f, -0.5f, -0.5f,       -1.0f, 0.0f, 0.0f,   0,1,
    -0.5f, 0.5f, 0.5f,         -1.0f, 0.0f, 0.0f,   1,0,
    -0.5f, 0.5f, 0.5f,         -1.0f, 0.0f, 0.0f,   1,0,
    -0.5f, -0.5f, -0.5f,       -1.0f, 0.0f, 0.0f,   0,1,
    -0.5f, -0.5f, 0.5f,        -1.0f, 0.0f, 0.0f,   0,0,
    
    -0.5f, -0.5f, -0.5f,       0.0f, -1.0f, 0.0f,   0,1,
    0.5f, -0.5f, -0.5f,        0.0f, -1.0f, 0.0f,   1,1,
    -0.5f, -0.5f, 0.5f,        0.0f, -1.0f, 0.0f,   0,0,
    -0.5f, -0.5f, 0.5f,        0.0f, -1.0f, 0.0f,   0,0,
    0.5f, -0.5f, -0.5f,        0.0f, -1.0f, 0.0f,   1,1,
    0.5f, -0.5f, 0.5f,         0.0f, -1.0f, 0.0f,   1,0,
    
    0.5f, 0.5f, 0.5f,          0.0f, 0.0f, 1.0f,    1,0,
    -0.5f, 0.5f, 0.5f,         0.0f, 0.0f, 1.0f,    0,0,
    0.5f, -0.5f, 0.5f,         0.0f, 0.0f, 1.0f,    1,1,
    0.5f, -0.5f, 0.5f,         0.0f, 0.0f, 1.0f,    1,1,
    -0.5f, 0.5f, 0.5f,         0.0f, 0.0f, 1.0f,    0,0,
    -0.5f, -0.5f, 0.5f,        0.0f, 0.0f, 1.0f,    0,1,
    
    0.5f, -0.5f, -0.5f,        0.0f, 0.0f, -1.0f,   1,1,
    -0.5f, -0.5f, -0.5f,       0.0f, 0.0f, -1.0f,   0,1,
    0.5f, 0.5f, -0.5f,         0.0f, 0.0f, -1.0f,   1,0,
    0.5f, 0.5f, -0.5f,         0.0f, 0.0f, -1.0f,   1,0,
    -0.5f, -0.5f, -0.5f,       0.0f, 0.0f, -1.0f,   0,1,
    -0.5f, 0.5f, -0.5f,        0.0f, 0.0f, -1.0f,   0,0
};

A brief explanation of this array:  Each line describes a single vertex.  The first three numbers are the X,Y,Z position of the vertex.  The second three numbers describe a normal vector that points away from the face of the cube that this vertex is part of.  Normal vectors are used in lighting calculations.  OpenGL uses the dot product of the normal vector and the vector from the light source to the vertex to find the angle between them.  If the angle is very small, the light is shining directly on the geometry and it appears very bright.  As the angle increases, the light gets dimmer.

The two numbers we added to each line are the texture coordinates for the vertex.  These map the image data to the face of the geometry.  For texture coordinates, 0,0 is the upper left corner of the image, and 1,1 is the lower right.  Note that the up-down coordinate gets higher as you go down the texture, which is opposite of the geometry coordinate system.  

Each group of six lines in the array represent one face of the cube.  Each face is made up of two triangles.  Notice that this array only holds information for a single cube.  This data is loaded once, then used twice to draw two cubes on the screen.

If you're adding the texture coordinates one at a time like I did, don't forget to add a comma after the last normal vector.  Also don't forget to remove the size of the array from the square brackets.  It is unnecessary and will break your code if you add elements to the array without updating it.


After changing vertex data array, we need to go back down to the setupGL function and make some changes.  First find the line that sets the light0.diffuseColor and change the 0.4s to 1.0s.

self.effect.light0.diffuseColor = GLKVector4Make(1.0f, 1.0f, 1.0f, 1.0f);

This line is setting the color of the light source for the fixed functionality cube.  The four numbers represent the red, green, blue, and alpha (or opacity) values of the light.  OpenGL tends to specify these values from 0 to 1.  If you work with HTML at all, you may be more familiar with the hexadecimal representation of color, from 00 to FF.  This is changing the light shining on the cube from red to white.  This is so it looks normal when the texture is applied.


Next, add the following lines of code directly under code you just changed:

    self.effect.texture2d0.enabled = GL_TRUE;
    self.effect.texture2d0.envMode = GLKTextureEnvModeModulate;
    self.effect.texture2d0.target = GLKTextureTarget2D;
    self.effect.texture2d0.name = _texture.name;

The first line enables the texture we loaded.

The second line describes how OpenGL will apply the texture.  Modulate mixes the texture color with the light color.  If we had chosen Replace, we would end up with a flat, perfectly bright cube that is not impacted by light at all.

The third line and fourth line sets the active texture to the one we loaded earlier.  I believe (but I could be wrong) that the third and fourth line is roughly the same as calling glBindTexture(GL_TEXTURE_2D, _texture.name).


Now we need to change the pointers to the vertex array that we pass OpenGL.  Change the calls the VertexArrib* functions like so:

    glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 32, BUFFER_OFFSET(0));
    glEnableVertexAttribArray(GLKVertexAttribNormal);
    glVertexAttribPointer(GLKVertexAttribNormal, 3, GL_FLOAT, GL_FALSE, 32, BUFFER_OFFSET(12));
    glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
    glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, 32, BUFFER_OFFSET(24));

The glVertexAttribPointer functions are telling OpenGL where to find the position, normal, and texture coordinate information in the array we pass in.

The first parameter is an enum value that we use to refer to different information in our vertex array.  By passing in these values here, we can use them later to refer to when working with OpenGL to refer to the position, normal, or texture coordinate information. The second parameter is how many dimensions to look for.  The third is the type of the array.  The fourth is whether the data is normalized.  A normalized vector has a length of one.  The fifth parameter is the number of bytes to skip when reading the information for each vertex from the array.  You can also think of this as the number of bytes in a single line in the array above ((3 position coordinates + 3 normal coordinates + 2 texture coordinates) * 4 bytes per coordinate).  The last parameter is where in the array to start reading (in bytes).


After making these changes, run the program.  The red cube should now be texturized!  Note that the shading from the light source is still applied.

Even though we've only changed one cube we are well over half way done.  Most of the work to load the texture information into OpenGL applies to both types of rendering.  Now all we have to do is set up the shaders to use the information we have provided.

Let's start by going to the Shader.vsh file and changing it to take advantage of our texture data.

First, add a new attribute declaration below the position and normal attributes:

attribute vec2 texcoord0;

Attributes are values that we pass to the shader from our program on a per vertex basis.  In our case, the attributes are contained in the vertex array that we added texture coordinates to.

Next, add a new varying variable declaration below the colorVarying declaration:

varying mediump vec2 texcoord_varying;

These varying values will be interpolated and passed to the fragment shader.

Next, change the diffuseColor vector so that it is white instead of blue.  This is changing the blue light to white for exactly the same reason we changed the red light to white earlier.

vec4 diffuseColor = vec4(1.0, 1.0, 1.0, 1.0);

Finally, add a line that assigns the texcoord value the vertex shader receives from the program  to the texcood_varying value that it passes to the fragment shader.

texcoord_varying = texcoord0;

That's all we have to do to the vertex shader.  It should now look like the screen shot:


Now lets go to the Shader.fsh file and modify the fragment shader.  Even though it's doing the actual work of specifying each pixel's color, it is much smaller.

First, add a declaration for a varying variable to match the one in the vertex shader:

varying mediump vec2 texcoord_varying;

Next, add a uniform sampler declaration.  This is the actual image data for our texture:

uniform sampler2D texture;

Finally, change the gl_FragColor assignment in the main function like so:

gl_FragColor = texture2D(texture, texcoord_varying) * colorVarying;

The texture2D function uses the interpolated texture coordinate to pull a color from a image data.  We then multiply it by the colorVarying attribute which contains the lighting value as calculated in the vertex shader.  If the light is shining directly on the face, then this has the effect of multiplying the texture color by one, which does not change it at all.  If the light is shining on the face at an angle, this multiplies the texture color by some number between one and zero, making it darker.  This is why each face of the cube gets darker as it rotates away from the light source.

Now we are done modifying the fragment shader.  It should look like the screen shot below:


Now that we've modified the shaders to take advantage of the texture data, we need to connect the new variables we declared to their counterparts in our view controller.  Start by adding the following line to the enum at the very top of the view controller .m file:

UNIFORM_TEXTURE_SAMPLER,

Be sure to add this above the NUM_UNIFORMS entry, so that it is still incremented correctly.  This enum represents the set of identifiers that we use to refer to the variables in the shaders.

Next go to the loadShaders function and add the following call to glBindAttributeLocation below the two that are already there.

glBindAttribLocation(_program, GLKVertexAttribTexCoord0, "texcoord0");

As the function name implies, this associates the texcoord0 variable in the vertex shader with the data referred to by the GLKVertexAttribTexCoord0 constant.  Whenever we need to refer to the shader variable we can use this constant.  


If you look carefully you'll notice that the BindAttribute function call we added does not match the two that are already there.  The other two function calls use members of an enum that is declared at the top of the file.  You may be wondering why we didn't add a member to this enum and pass it to the glBindAttributeLocation function.  It took me a while to realize it, but that enum is actually completely pointless, misleading, and a trap for the unwary programmer.  

OpenGL works by assigning identifiers to resources.  When you give OpenGL a vertex array, you also give it an identifier to use to refer to that array.  In the glEnableVertexAttribArray and glVertexAttribPointer calls in the setupGL function, we pass in GLKVertexAttribPosition, GLKVertexAttribNormal, and GLKVertexAttribTexCoord0 constants to refer to the array's position, normal, and texture coordinate information.  Now it just so happens that GLKVertexAttribPosition and GLKVertexAttribNormal are equal to 0 and 1 respectively.  So when the anonymous author of OpenGL Game project later uses ATTRIB_VERTEX and ATTRIB_NORMAL, which are also equal to 0 and 1, to bind these resources to the shader attributes, everything works out ok.  But this is just a happy coincidence due to the similar ordering of these values within their respective enum declarations.  Adding an ATTRIB_TEXCOORD member to our enum would give it a value of 2 (assuming you follow standard practice and insert it above the NUM_ATTRIBUTES member.)  Unfortunately the GLKVertexAttribtexCoord0 constant we used to refer to the texture coordinate data earlier has a value of 3.  So if we follow the example given us in the OpenGL Game project we would end up with broken code.

Anywho, after you add the glBindAttribLocation call, go down a little further in the loadShaders function and add an additional uniform assignment:

uniforms[UNIFORM_TEXTURE_SAMPLER] = glGetUniformLocation(_program, "texture");

This follows the typical OpenGL pattern of assigning an unsigned integer identifier to a OpenGL resource.


Finally go to the drawInRect function and add the code to bind the texture sampler with the image data:

glUniform1i(uniforms[UNIFORM_TEXTURE_SAMPLER],0);

While I was modifying the drawInRect function, I took a moment to change the ClearColor from gray to white:

glClearColor(1.0f, 1.0f, 1.0f, 1.0f);

This is by no means necessary, I just think the gray background looks really ugly.


Running the program now gives you two cubes, both textured and lit:


As I have said before, I am chronicling my blind, fumbling efforts with iOS programming in the hopes that other people may avoid my mistakes (at least the ones I'm aware of.)  Use this code with extreme caution.  I feel confident that it still contains errors.  If you happen to notice one, please let me know.

3 comments:

  1. Great article !
    I'm on video texturing on this cubes and I'm trapped on GLKTextureLoaderErrorDomain error 8.) Perhaps you have an idea ... ;)
    Keep going

    ReplyDelete
    Replies
    1. Hey, neat! A comment! As for the dreaded GLKTextureLoaderErrorDomain 8, I have not fallen victim to it yet. I did notice a few questions on StackOverflow about this error. One guy solved it by moving his glEnable(GL_TEXTURE) call after his texture loading code, but I wasn't able to repo it using the self.effect.texture0.* calls. If I run across the error and figure out how to solve it I will definitely post it here.

      Delete