How to texturize objects with GLKit

This is the fifth post in my 2D Game Engine Tutorial. This post relies on code created in earlier posts, so if you jumped in on this post I recommend you at least skim the previous posts in the series. You can also grab the tagged code so that you don’t have to start from scratch if you don’t want to.

In this post we’ll learn how to add textures (images) to our shapes. In addition to adding visual flair, this technique allows us to use sprites as game objects.

Iteration 9: Texturizing a rectangle

GLKit uses OpenGL ES 2.0, so the full array of possibilities exists for you if you wish to write your own shader programs. We’re keeping it simple, though, and we’ll use GLKBaseEffect (which mimics OpenGL ES 1.1 functionality) to handle our textures.

Images are always rectangular, so we’ll start with a 1-to-1 mapping of an image onto a rectangle. We’ll use this nice landscape photo I found on Flickr, licensed CC-Attribution by NeilsPhotography.

There are three steps to using textures with GLKit.

  1. Loading the texture.
  2. Configuring the GLKBaseEffect.
  3. Sending texture coordinates to OpenGL.

Before we get started loading the texture, we need to add it to the project. Save the photo linked above to your Desktop as “landscape.jpg” (or another file and name of your choice, naturally). Right click on a group and select “Add Files to ‘ExampleEngine’…”, select the file, check “Copy items into Destination’s group folder”, and make sure your ExampleEngine target is checked; then click “Add.” We now have access to the file from within our application’s main bundle.

Textures are stored with the GLKTextureInfo class, so we’ll add one of those to our shape base class. But we’d like to encapsulate the texture loading code in our class, so we’ll let the client code set the texture as an image.

// EEShape.h
@interface EEShape : NSObject {
  GLKTextureInfo *texture;
  ...
}
-(void)setTextureImage:(UIImage *)image;
...
@end
 
// EEShape.m
-(void)setTextureImage:(UIImage *)image {
  NSError *error;
  texture = [GLKTextureLoader textureWithCGImage:image.CGImage options:nil error:&error];
  if (error) {
    NSLog(@"Error loading texture from image: %@",error);
  }
}

That’s all it takes using GLKTextureLoader. There are some options we can pass (we’ll look at a few in a moment), and we’re not doing any error handling aside from a log statement, but this’ll work for the common case.

Next we have to configure the GLKBaseEffect to use the texture. We’ll only do this if we’ve set the texture, so that we can continue to have shapes without textures as well. Add this to your effect configuration:

// EEShape.m in renderInScene:
if (texture != nil) {
  effect.texture2d0.envMode = GLKTextureEnvModeReplace;
  effect.texture2d0.target = GLKTextureTarget2D;
  effect.texture2d0.name = texture.name;
}

Easy peasy! Although I notice again that we’re creating our effect on every single frame, which is inefficient. Caching the effect in the object is an optimization that a production game engine would want to make in order to give it more time to process game logic in between frames.

Finally, we need to pass in texture coordinates per vertex. This defines where and how the texture is placed on the shape. OpenGL uses a normalized coordinate system for texture coordinates, with an origin in the lower left of an image. And instead of using x and y, since those are already used for position data, the convention is to refer to the coordinates as s and t.

The word “normalized” means that the coordinate system only goes from 0 to 1; the right side of the texture image is at s=1, and the top is at t=1. For a rectangle, mapping the whole image to it is simple: the bottom left corner is (0,0), the bottom right is (1,0), the top right is (1,1), and the top left is (0,1).

In our code, we still need a place to store our texture coordinates per vertex. We’ll take the same approach as we did with per-vertex coloring and position vertex data.

// EEShape.h
@interface EEShape : NSObject {
  NSMutableData *textureCoordinateData;
  ...
}
@property(readonly) GLKVector2 *textureCoordinates;
...
@end
 
 
// EEShape.m
@implementation EEShape
- (GLKVector2 *)textureCoordinates {
  if (textureCoordinateData == nil)
    textureCoordinateData = [NSMutableData dataWithLength:sizeof(GLKVector2)*self.numVertices];
  return [textureCoordinateData mutableBytes];
}
...
@end

And we need to enable the data and send it to OpenGL just like we did with colors and positions as well. We’ll need to add the following set up and tear down code to our rendering code.

// EEShape.m in renderInScene:
...
if (texture != nil) {
  glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
  glVertexAttribPointer(GLKVertexAttribTexCoord0, 2, GL_FLOAT, GL_FALSE, 0, self.textureCoordinates);
}
...
glDrawArrays(GL_TRIANGLE_FAN, 0, self.numVertices);
...
if (texture != nil)
  glDisableVertexAttribArray(GLKVertexAttribTexCoord0);

Now we’re ready to make sure it all works! Create a LandscapeScene, set it up with a rectangle, and configure the texture and texture coordinates like so to make it full screen (don’t worry, I too had to look back into EERectangle.m to remember which vertex index corresponded to which corner).

// LandscapeScene.m
-(id)init {
  self = [super init];
  if (self) {
    rectangle = [[EERectangle alloc] init];
    rectangle.width = 6;
    rectangle.height = 4;
 
    [rectangle setTextureImage:[UIImage imageNamed:@"landscape.jpg"]];
    rectangle.textureCoordinates[0] = GLKVector2Make(1,0);
    rectangle.textureCoordinates[1] = GLKVector2Make(1,1);
    rectangle.textureCoordinates[2] = GLKVector2Make(0,1);
    rectangle.textureCoordinates[3] = GLKVector2Make(0,0);
  }
  return self;
}

Running it, we see…

An upside down landscape! In this case it has to do with the fact that GLKit tries to match UIKit’s convention of having the origin in the top left instead of the bottom left.

Honestly, no matter how many times I use textures and try to be careful about it, I hit the upside down problem all the time. It’s eminently solvable, though, and quick to catch in most cases, so I wouldn’t worry about trying to memorize everything—just flip stuff when you catch it.

Remember the options parameter when we loaded our texture? Investigating the documentation I see that we could have passed a GLKTextureLoaderOriginBottomLeft boolean—if YES it flips our data for us. So now we have two options: set the option, or flip our texture coordinates.

If we want to think of the texture mapping as having the origin at the top left instead of the bottom left, we don’t have to do anything except change our texture coordinates for our shapes.

rectangle.textureCoordinates[0] = GLKVector2Make(1,1);
rectangle.textureCoordinates[1] = GLKVector2Make(1,0);
rectangle.textureCoordinates[2] = GLKVector2Make(0,0);
rectangle.textureCoordinates[3] = GLKVector2Make(0,1);

Or, if we’d like to think of the texture mapping as having the origin at the bottom left, we could refrain from flipping it on texture load.

[GLKTextureLoader textureWithCGImage:image.CGImage 
                             options:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] 
                                                                 forKey:GLKTextureLoaderOriginBottomLeft] 
                               error:&error];

It’s up to you which solution you want to pursue—I’m going to stick with GLKTextureLoader solution and keep my s,t coordinate system origin in the bottom left.

The next “problem” with our texture is that it doesn’t actually quite fit the aspect ratio of our shape. Our shape matches our screen with a 3:2 ratio, but our original texture is 4:3. It’s not very visible in this example, but the mountains appear a little squished. You don’t have to fix this if you don’t want to—you might even like the effect in some cases. But we’ll fix it here to be proper and since it’s easy. To fix it, we’ll just trim off some of the image by using our texture coordinates.

rectangle.textureCoordinates[0] = GLKVector2Make(1,0);
rectangle.textureCoordinates[1] = GLKVector2Make(1,0.88);
rectangle.textureCoordinates[2] = GLKVector2Make(0,0.88);
rectangle.textureCoordinates[3] = GLKVector2Make(0,0);

The number 0.88 was obtained by doing a little algebra to see what fraction would create the proper 3:2 aspect ratio (0.88*768/1024=2/3). And presto, we’ve cut off the top of the sky!

iteration-9

Update: Iteration 9′s tagged code was built with iOS 5 beta 5; in beta 6 the API for textures changed slightly. You changes necessary are applied in 04d7980.

Iteration 10: Texturizing other shapes

Other shapes are no different to texturize, but you may need to be careful when you specify your texture coordinates.

Here we’ll texture a circle with this sandy tennis (?) ball picture by marc falardeau on Flickr. First we’ll trim the ball image down to a square shape.

This makes our math easier. Because the texture coordinate system is normalized from 0 to 1 on each axis, we need to find the texture coordinates that correspond to a circle centered at (0.5,0.5) with a radius of 0.5 units. We’ve already calculated the points on a circle centered at the origin with any given radius, so this is the same thing with each point shifted over and up 0.5 units each.

// BeachBallScene.m
-(id)init {
  self = [super init];
  if (self) {
    ellipse = [[EEEllipse alloc] init];
    ellipse.radiusX = 1;
    ellipse.radiusY = 1;
 
    [ellipse setTextureImage:[UIImage imageNamed:@"ball.jpg"]];
    float textureBallRadius = 0.5;
    float textureCenterOffset = 0.5;
    for (int i = 0; i < ellipse.numVertices; i++){
      float theta = ((float)i) / ellipse.numVertices * M_TAU;
      ellipse.textureCoordinates[i] = GLKVector2Make(textureCenterOffset+cos(theta)*textureBallRadius, 
                                                     textureCenterOffset+sin(theta)*textureBallRadius);
    }
  }
  return self;
}

iteration-10a

Other polygons are just the same! Consider a triangle with this Sierpinsky fractal by SphinxTheGeek on it. There’s a little trigonometry below to make sure we create an equilateral triangle (don’t be afraid of math!).

// SierpinskyTriangleScene.m
-(id)init {
  self = [super init];
  if (self) {
    triangle = [[EETriangle alloc] init];
 
    triangle.vertices[0] = GLKVector2Make(-1, -1);
    triangle.vertices[1] = GLKVector2Make( 1, -1);
    triangle.vertices[2] = GLKVector2Make( 0,  -1+2*sin(M_TAU/6));
 
    [triangle setTextureImage:[UIImage imageNamed:@"sierpinksy.jpg"]];
    triangle.textureCoordinates[0] = GLKVector2Make(  0,0);
    triangle.textureCoordinates[1] = GLKVector2Make(  1,0);
    triangle.textureCoordinates[2] = GLKVector2Make(0.5,1);
  }
  return self;
}

iteration-10b

Continue on to the next article in the tutorial to learn how to create sprites and add transparent textures.

This entry was posted in 2D Game Engine Tutorial, GLKit, Lessons, OpenGL. Bookmark the permalink.