Appendix B: Adding a Cube Map and Velocity

Since we already have a nice Apple logo texture applied to our cube faces, lets not replace it with a cube map, but rather add a cube map to give it a groovy “reflective” look. We’ll make it reminiscent of the old shiny plastic G3 PowerMac cases.

First, we need to find some cube maps. A quick web search leads us to www.humus.name, a site with a great collection of high-quality cube map images. We’ll download a few of those and make our application capable of cycling through a small group of cube maps by double-tapping. We’ll also need to reduce the size of the individual cube face images, since they’re a bit large for the iPhone. I reduced the image sizes to 512×512 and created a directory structure in my Xcode project.

 

 

For fun, we’re also going to add velocity to our cube. After we code our changes, flicking the cube will cause it to spin wildly and slowly come to a stop.

Look in the EDCubeDemoViewController.h file and note the following changes.

    GLuint textureName;
    GLuint cubeMapName0, cubeMapName1, cubeMapName2, cubeMapName3, cubeMapName4;

Under the existing textureMap variable, we’ve added variables to hold the OpenGL names for the cube maps we’re going to load. We’ll cycle through these by handling double taps in the view controller.

    BOOL animating;
    NSInteger animationFrameInterval;
    CADisplayLink *displayLink;

    float lastXVelocity, lastYVelocity;    
    float velocityDecay;

    GLuint currentCubeMap;
    GLuint nextCubeMap;
    int changeCubeMap;
    float cubeMapAlpha;

We’ve also added variables for handling the cube velocity and cycling through the cube maps.

@property (readonly, nonatomic, getter=isAnimating) BOOL animating;
@property (nonatomic) NSInteger animationFrameInterval;

- (void)loadTexture:(GLuint *)textureName fromFile:(NSString *)fileName;
- (void)loadCubeMap:(GLuint *)newTextureName fromFiles:(NSArray *)cubeFaces inFolder:(NSString *)folder;
- (void)rotateCubeAroundX:(float)x andY:(float)y;
- (void)drawFrame;
- (void)startAnimation;
- (void)stopAnimation;

Finally, we add the method definition for the new loadCubeMap:fromFiles:inFolder: method.

In the EDCubeDemoViewController.m file, we have several changes to the top of the file.

// Uniform index.
enum {
    UNIFORM_MVP_MATRIX,
    UNIFORM_TEXTURE,
    UNIFORM_CUBE_MAP,
    UNIFORM_CUBE_MAP_ALPHA,
    NUM_UNIFORMS
};
GLint uniforms[NUM_UNIFORMS];

// Attribute index.
enum {
    ATTRIB_VERTEX,
    ATTRIB_TEXTURE_COORD,
    NUM_ATTRIBUTES
};

@interface EDCubeDemoViewController ()
@property (nonatomic, retain) EAGLContext *context;
@property (nonatomic, assign) CADisplayLink *displayLink;
- (BOOL)loadShaders;
- (BOOL)compileShader:(GLuint *)shader type:(GLenum)type file:(NSString *)file;
- (BOOL)linkProgram:(GLuint)prog;
- (BOOL)validateProgram:(GLuint)prog;
@end

@implementation EDCubeDemoViewController

@synthesize animating, context, displayLink;

We’ve added a few new uniform enums for the cube map and cube map alpha values.

In the awakeFromNib method, we’ve added a few things as well.

    animating = FALSE;
    animationFrameInterval = 1;
    self.displayLink = nil;

    lastXVelocity = lastYVelocity = 0;
    velocityDecay = 0.5;

    NSArray *cubeMap = [[NSArray alloc] initWithObjects:@"posx.jpg", @"negx.jpg", @"posy.jpg", @"negy.jpg", @"posz.jpg", @"negz.jpg", nil];
    [self loadCubeMap:&cubeMapName0 fromFiles:cubeMap inFolder:@"CubeMaps/ForbiddenCity"];
    [self loadCubeMap:&cubeMapName1 fromFiles:cubeMap inFolder:@"CubeMaps/Stairs"];
    [self loadCubeMap:&cubeMapName2 fromFiles:cubeMap inFolder:@"CubeMaps/GamlaStan"];
    [self loadCubeMap:&cubeMapName3 fromFiles:cubeMap inFolder:@"CubeMaps/CNTower2"];
    [self loadCubeMap:&cubeMapName4 fromFiles:cubeMap inFolder:@"CubeMaps/Tantolunden6"];
   
    currentCubeMap = cubeMapName0;
    changeCubeMap = 0;
    cubeMapAlpha = 1.0;

At the end of the awakeFromNib method, we’re adding few new variables to handle cube velocity. The new loadCubeMap:fromFiles:inFolder: method takes an array of file names for the cube faces, so we’re initializing that array here too.

Since all of the cube faces in all of the cube map folders are named uniformly, we’re using the folder names to differentiate between them. This way, we can use one array to represent the file names, regardless of the actual cube map we’re using, and change the folder names to tell them apart. We’ll look at this in more detail when we inspect the loadCubeMap:fromFiles:inFolder: method below.

The last few variables set up the mechanism to switch between cube maps on a double-tab, complete with a pleasant fade-out, fade-in effect.

    if(lastXVelocity > 0 || lastXVelocity < 0 || lastYVelocity > 0 || lastYVelocity < 0) {
        [self rotateCubeAroundX:lastXVelocity andY:lastYVelocity];
       
        if(lastXVelocity > 0) {
            lastXVelocity -= velocityDecay;
        } else {
            lastXVelocity += velocityDecay;
        }
       
        if(lastYVelocity > 0) {
            lastYVelocity -= velocityDecay;
        } else {
            lastYVelocity += velocityDecay;
        }
    }

    if(changeCubeMap == 1) {
        if(cubeMapAlpha > 0) {
            cubeMapAlpha -= 0.01;
        } else {
            cubeMapAlpha = 0;
            changeCubeMap = 2;
            currentCubeMap = nextCubeMap;
        }
    } else if(changeCubeMap == 2) {
        if(cubeMapAlpha < 1) {
            cubeMapAlpha += 0.01;
        } else {
            cubeMapAlpha = 1.0;
            changeCubeMap = 0;
        }
    }

Near the top of the drawFrame method, we add logic to handle velocity and any changes to the current cube map.

If the user flicks across the screen, the program will read and remember the distance between the last two touches. This distance will be used to guess about how fast the user flicked across the screen. The larger the value, the faster the flick.

As the cube spins, a decay will be applied to the spin value, slowly decrementing the spin value over time, making the cube come to a gradual stop.

Likewise, if the user has double-tapped, the changeCubeMap and cubeMapAlpha variables will slowly fade out the current cube map, switch it, and then fade back in until the alpha value is back to 1.

         // Update uniform value.
        glUniformMatrix4fv(uniforms[UNIFORM_MVP_MATRIX], 1, 0, mvpMatrix);
        glActiveTexture(GL_TEXTURE0);
        glBindTexture(GL_TEXTURE_2D, textureName);
        glUniform1i(uniforms[UNIFORM_TEXTURE], 0);
        glActiveTexture(GL_TEXTURE1);
        glBindTexture(GL_TEXTURE_CUBE_MAP, currentCubeMap);
        glUniform1i(uniforms[UNIFORM_CUBE_MAP], 1);
        glUniform1f(uniforms[UNIFORM_CUBE_MAP_ALPHA], cubeMapAlpha);

The only other change in method is the addition of the two new uniform variables for the cube map and the cube map alpha.

The glActivateTexture() and glBindTexture() calls make sure that the right texture is active and bound to the right texture target before going into the shaders. If you try to use more than one texture bound to the same target in the shader, the shader will fail.

Now let’s take a look at the new loadCubeMap:fromFiles:inFolder: method.

- (void)loadCubeMap:(GLuint *)newTextureName fromFiles:(NSArray *)cubeFaces inFolder:(NSString *)folder {
    glGenTextures(1, newTextureName);
   
    glBindTexture(GL_TEXTURE_CUBE_MAP, *newTextureName);
   
    for(int i = 0; i < [cubeFaces count]; i++) {
        // Load image from file and get reference
        NSBundle *myBundle = [NSBundle bundleForClass:[self class]];
        NSString *myFilePath = [myBundle pathForResource:(NSString *)[cubeFaces objectAtIndex:i] ofType:nil inDirectory:folder];
       
        UIImage *image = [[UIImage alloc] initWithContentsOfFile:myFilePath];
        CGImageRef imageRef = [image CGImage];
       
        if(imageRef) {
            // get width and height
            size_t imageWidth = CGImageGetWidth(imageRef);
            size_t imageHeight = CGImageGetHeight(imageRef);
           
            GLubyte *imageData = (GLubyte *)malloc(imageWidth * imageHeight * 4);
            memset(imageData, 0, (imageWidth * imageHeight * 4));
           
            CGContextRef imageContextRef = CGBitmapContextCreate(imageData, imageWidth, imageHeight, 8, imageWidth * 4, CGImageGetColorSpace(imageRef), kCGImageAlphaPremultipliedLast);
           
            // Make CG system interpret OpenGL style texture coordinates properly by inverting Y axis
            CGContextTranslateCTM(imageContextRef, 0, imageHeight);
            CGContextScaleCTM(imageContextRef, 1.0, -1.0);
           
            CGContextDrawImage(imageContextRef, CGRectMake(0.0, 0.0, (CGFloat)imageWidth, (CGFloat)imageHeight), imageRef);
           
            CGContextRelease(imageContextRef);
           
            GLenum cubeFaceEnum = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i;
           
            glTexImage2D(cubeFaceEnum, 0, GL_RGBA, imageWidth, imageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
           
            free(imageData);
        }
    }
   
    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glEnable(GL_BLEND);
   
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_REPEAT, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
   
    glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
   
    glEnable(GL_TEXTURE_CUBE_MAP);
}

Cube maps work a lot like textures in that you generate a new texture name from OpenGL and bind it to a texture target, in this case GL_TEXTURE_CUBE_MAP. The difference is that we’re going to load six individual images for this one texture name, one for each side of the cube.

Another important difference with cube maps is that we’re not going to specify any texture coordinates. OpenGL is going to project the cube map onto the cube faces based on the orientation of each face.

    glGenTextures(1, newTextureName);
   
    glBindTexture(GL_TEXTURE_CUBE_MAP, *newTextureName);

The first few lines are pretty standard, we ask OpenGL for a single new texture name, then bind that name to a texture target of GL_TEXTURE_CUBE_MAP.

    for(int i = 0; i < [cubeFaces count]; i++) {
        // Load image from file and get reference
        NSBundle *myBundle = [NSBundle bundleForClass:[self class]];
        NSString *myFilePath = [myBundle pathForResource:(NSString *)[cubeFaces objectAtIndex:i] ofType:nil inDirectory:folder];
       
        UIImage *image = [[UIImage alloc] initWithContentsOfFile:myFilePath];
        CGImageRef imageRef = [image CGImage];

This logic is very similar to the old loadTexture:fromFile: method, except now we’re going to be reading in and loading image data for six individual cube faces. We passed in an array of file names and a folder name in which to find them. This loop will go through the files in the array and load and process each one individually.

Important note: when you add stuff to your Xcode projects that you’ll want to organize into folders, be sure to select the “Create folder references for any added folders” option when adding the files. If you use the “Create groups for any added folders”, Xcode will view that structure as representative, not physical. Even if you create folders in your project and move files around, Xcode will not find files in those directories.

 

 

A quick way to see if the groups are representative or physical is to look at the folder colors in the Xcode IDE.

 

 

The blue folder represent physical directories that contain the displayed files, while the yellow folder represents a virtual grouping. The files in the yellow folder named “Supporting Files” aren’t actually in a directory of that name, but the files in the blue folder named “CubeMaps” are.

Once we’ve found the file we’re looking for, we load in the image data and get a CGImageRef object.

        if(imageRef) {
            // get width and height
            size_t imageWidth = CGImageGetWidth(imageRef);
            size_t imageHeight = CGImageGetHeight(imageRef);
           
            GLubyte *imageData = (GLubyte *)malloc(imageWidth * imageHeight * 4);
            memset(imageData, 0, (imageWidth * imageHeight * 4));
           
            CGContextRef imageContextRef = CGBitmapContextCreate(imageData, imageWidth, imageHeight, 8, imageWidth * 4, CGImageGetColorSpace(imageRef), kCGImageAlphaPremultipliedLast);
           
            // Make CG system interpret OpenGL style texture coordinates properly by inverting Y axis
            CGContextTranslateCTM(imageContextRef, 0, imageHeight);
            CGContextScaleCTM(imageContextRef, 1.0, -1.0);
           
            CGContextDrawImage(imageContextRef, CGRectMake(0.0, 0.0, (CGFloat)imageWidth, (CGFloat)imageHeight), imageRef);
           
            CGContextRelease(imageContextRef);
           
            GLenum cubeFaceEnum = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i;
           
            glTexImage2D(cubeFaceEnum, 0, GL_RGBA, imageWidth, imageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);
           
            free(imageData);
        }
    }

Once we have a valid reference to a CGImageRef object, the code is nearly identical to the existing loadTexture:fromFile: method, we’re just doing it once per cube face. There is one bit of code that requires more explanation, however.

            GLenum cubeFaceEnum = GL_TEXTURE_CUBE_MAP_POSITIVE_X + i;
           
            glTexImage2D(cubeFaceEnum, 0, GL_RGBA, imageWidth, imageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, imageData);

When you use a GL_TEXTURE_CUBE_MAP target, OpenGL expects you to give it information about each side. We do that with the glTexImage2D() function, specifying image information for each of the six sides. Since the target parameter is a GLenum type, we can ‘cheat’ and increment through those as we loop through the NSArray entries.

The six targets that we need to supply information for are GL_TEXTURE_CUBE_MAP_POSITIVE_X, GL_TEXTURE_CUBEMAP_NEGATIVE_X, GL_TEXTURE_CUBEMAP_POSITIVE_Y, GL_TEXTURE_CUBEMAP_NEGATIVE_Y, GL_TEXTURE_CUBEMAP_POSITIVE_Z, and GL_TEXTURE_CUBEMAP_NEGATIVE_Z.

If these assignments weren’t in a loop, the code would look something like this.

glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_X, 0, GL_RGBA, imageWidth, imageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, image0);
glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_X, 0, GL_RGBA, imageWidth, imageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, image1);
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Y, 0, GL_RGBA, imageWidth, imageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, image2);
glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Y, 0, GL_RGBA, imageWidth, imageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, image3);
glTexImage2D(GL_TEXTURE_CUBE_MAP_POSITIVE_Z, 0, GL_RGBA, imageWidth, imageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, image4);
glTexImage2D(GL_TEXTURE_CUBE_MAP_NEGATIVE_Z, 0, GL_RGBA, imageWidth, imageHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, image5);

This would supply OpenGL with all of the image information for each face.

    glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    glEnable(GL_BLEND);
   
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_REPEAT, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
   
    glGenerateMipmap(GL_TEXTURE_CUBE_MAP);
   
    glEnable(GL_TEXTURE_CUBE_MAP);

The last bit of code after the loop simply configures and enables blending, and then configures and enables the GL_TEXTURE_CUBE_MAP texture target.

The glGenerateMipmap() call will create multiple versions of the graphics at progressively smaller sizes (each half of the previous) for performance. Instead of having to scale one image up and down all of the time, OpenGL can pick from a set of pre-scaled images that most closely matched the desired resolution.

Finally, we add a touchesEnded:withEvent method to handle the velocity and cube map changing functionality.

- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
    NSSet *allTouches = [event allTouches];
   
    if([[touches anyObject] tapCount] > 1) {
        // User has multi-tapped
        changeCubeMap = 1;
       
        if(currentCubeMap == cubeMapName0) {
            nextCubeMap = cubeMapName1;
        } else if(currentCubeMap == cubeMapName1) {
            nextCubeMap = cubeMapName2;
        } else if(currentCubeMap == cubeMapName2) {
            nextCubeMap = cubeMapName3;
        } else if(currentCubeMap == cubeMapName3) {
            nextCubeMap = cubeMapName4;
        } else if(currentCubeMap == cubeMapName4) {
            nextCubeMap = cubeMapName0;
        }
    } else if([allTouches count] == 1) {
        CGPoint currentTouchPosition = [[touches anyObject] locationInView:self.view];
       
        float xMovement = lastTouchPosition.x - currentTouchPosition.x;
        float yMovement = lastTouchPosition.y - currentTouchPosition.y;
       
        lastXVelocity = yMovement;
        lastYVelocity = xMovement;
    }
}

In touchesEnded:withEvent:, we’re going to check for a double-tap and set the next cube map to transition to. The changeCubeMap variable will trigger the transition code in the drawFrame method.

If we received a single tap, that means that the current touch has ended. In the case of a flick, the finger may have moved off of the screen. Either way, we look at the last touch we got and subtract it from the touch before that. We use that value as our cube velocity. The cube will continue to spin with this velocity until the decay variable decrements it to zero.

The last couple of changes that we need to make will be in the shaders. Since cube maps work by projecting the images onto the faces, we’ll need our vertex position passed through to the fragment shader, which will calculate the viewing angle based on the reflection of the cube map as an environment.

attribute vec4 position;
attribute vec4 texture_coord;

varying vec2 texCoordVarying;
varying vec4 vertexData;

uniform mat4 mvp_matrix;

void main()
{
    gl_Position = mvp_matrix * position;
   
    vertexData = gl_Position;
   
    texCoordVarying = texture_coord.st;
}

In the vertex shader, we’re adding a vertexData varying to pass through to the fragment shader. The fragment shader will use this data to calculate a reflection on our cube.

varying highp vec2 texCoordVarying;
varying highp vec4 vertexData;

uniform sampler2D texture;
uniform samplerCube cube_map;
uniform highp float cube_map_alpha;

void main()
{
    highp vec4 eye_direction = vec4(0, 0, 30, 0);
    highp vec3 reflection_direction = reflect(eye_direction, normalize(vertexData)).xyz;
    highp vec4 texture_color = texture2D(texture, texCoordVarying);
    highp vec4 cube_color = textureCube(cube_map, reflection_direction);

    cube_color = vec4(cube_color.r * cube_map_alpha, cube_color.g * cube_map_alpha, cube_color.b * cube_map_alpha, cube_color.a);

    gl_FragColor = mix(cube_color, texture_color, 0.50);
}

The fragment shader creates a viewing position of (0, 0, 20), right in the center of OpenGL space and 20 units out. This should make our reflection a nice size on the faces of the cube, since the further back you stand from a reflection, the more of it you can see.

The shader function reflect() takes an incident vector and a surface normal vector, which we get from using the normalize() function on the vertex data that we passed through. This reflection direction value is used by the textureCube() function below, which needs the reflection direction vector to calculate which part of the cube map it will need to render at the current texel.

The texture2D() function works as it did before, taking the 2D texture and texture coordinates.

Once we have the two texel colors, one from the Apple logo texture and one from the current cube map, we can combine them to create that groovy plastic reflection look on the cube surface. Before we use the mix() function to do that, however, we apply the cube_map_alpha color to the cube map texel color to make sure it will fade out and in when it’s supposed to.

Building and running the program now will give us a cube that can be “flicked” to spin wildly, and cube mapped, reflected environments. Double tap to cycle through the cube maps.

 

 

One caveat – my reflection shader code is a hack, at best. I’m still researching and learning how to code reflections properly. What I’ve done here is passable, but the reflections on this cube are not even close to emulating real life. It’s a cool effect, but it’s not a true reflection. If you have experience coding reflections in shaders and can offer any tips or tricks, I’d greatly appreciate it.

Appendix A | Index

2 Comments to “Appendix B: Adding a Cube Map and Velocity”

  1. By JoshM, May 9, 2011 @ 8:38 am

    It has to be said. These are the best tutorials on OpenGLES2.0 on the internet hands down! I’m sick of not saying anything…you are too amazing and helpful! I hope people are noticing this amazingness. :) Thanks, for essentially teaching me OpenGL!

  2. By Joe, May 9, 2011 @ 8:44 am

    Thank you for the kind words, JoshM. I’m glad that you’ve found the information to be helpful!