15 June 2011

How To Create Dynamic Textures with CCRenderTexture

This post is also available in: Chinese (Simplified)

Create textures on the fly (such as these stripes) with CCRenderTexture!

Create textures on the fly (such as these stripes) with CCRenderTexture!

You’re probably familiar with adding premade backgrounds into your games. But what if you could dynamically create backgrounds and modify their colors, gradients, and effects on the fly?

If you’ve seen the game Tiny Wings by Andreas Illiger on the App Store, you’ve seen an example of this in action.

In this tutorial series, you’ll get hands-on experience doing this yourself! You’ll learn:

  • How to create textures on the fly
  • How to create seamless textures with Gimp
  • How to blend shadows and highlights onto textures for realistic effects
  • How to create striped textures
  • How to set up textures to repeat
  • And much more!

This tutorial is based on a great sample project by Sergey Tikhonov that was created as part of an investigation of Tiny Wings on the Cocos2D forums.

Sergey did an excellent job on the demo project, so rather than reinvent the wheel, I’m going to convert his demo code into a tutorial series, along with some extra cool features!

This tutorial assumes you are familiar with Cocos2D. If you are new to either of these, check out some of the other Cocos2D tutorials on this site.

Creating Dynamic Textures with CCRenderTexture

One of the cool things about Tiny Wings is that the textures change every day, as you can see in the below screenshot:

Tiny Wings Dynamic Backgrounds

But how can you create a dynamic texture in Cocos2D? Well, there is a cool class you can use called CCRenderTexture that allows you to draw to a texture, and then re-use that texture in your game.

Using CCRenderTexture is simple – you just take the following 5 steps:

  1. Create new CCRenderTexture. You specify the width and height of the texture you want to create here.
  2. Call CCRenderTexture:begin. This sets up OpenGL so that any further drawing draws into the CCRenderTexture (rather than onto the screen).
  3. Draw into the texture. You can draw by using raw OpenGL commands, or by calling the visit methods of existing Cocos2D objects (which will issue the required OpenGL commands to draw those objects).
  4. Call CCRenderTexture:end. This will render the texture and turn off drawing into the texture.
  5. Create a new Sprite from the texture. You can now create a new sprite from the CCRenderTexture’s sprite.texture property.

Note that you can repeat steps 1-3 to continually add/modify the texture over time. For example this might be handy to implement a drawing app. However for this tutorial, we just need to do the drawing once and then we’re done.

Let’s try out RenderTexture to see how it works, to just create a simple colored texture.

But first you need a new project! So in Xcode, go to File\New\New Project, and the choose iOS\cocos2d\cocos2d_box2d template. Even though this tutorial doesn’t use Box2D, some of the follow-up tutorials will, so we’re picking that now to be set up in advance.

Name the project TinySeal, click Next, choose a folder on your hard drive, and click Create.

Then open up HelloWorldLayer.h and replace it with the following:

#import "cocos2d.h"
 
@interface HelloWorldLayer : CCLayer
{
	CCSprite * _background;
}
 
+(CCScene *) scene;
 
@end

This removes the “Hello, World” Box2D code and adds an instance variable to keep track of the dynamic background we’re about to create:

Next, switch to HelloWorldLayer.m and replace it with the following (just to fully remove the Box2D code and get an empty scene):

#import "HelloWorldLayer.h"
 
@implementation HelloWorldLayer
 
+(CCScene *) scene {
	CCScene *scene = [CCScene node];
	HelloWorldLayer *layer = [HelloWorldLayer node];
	[scene addChild: layer];
	return scene;
}
 
-(id) init {
	if((self=[super init])) {   
	}
	return self;
}
@end

You can compile and run if you’d like at this point, and you should get a blank screen.

Next, add the following new method above the init method:

-(CCSprite *)spriteWithColor:(ccColor4F)bgColor textureSize:(float)textureSize {
 
    // 1: Create new CCRenderTexture
    CCRenderTexture *rt = [CCRenderTexture renderTextureWithWidth:textureSize height:textureSize];
 
    // 2: Call CCRenderTexture:begin
    [rt beginWithClear:bgColor.r g:bgColor.g b:bgColor.b a:bgColor.a];
 
    // 3: Draw into the texture
    // We'll add this later
 
    // 4: Call CCRenderTexture:end
    [rt end];
 
    // 5: Create a new Sprite from the texture
    return [CCSprite spriteWithTexture:rt.sprite.texture];
 
}

As you can see, the five steps to create a dynamic texture are the same as we discussed earlier.

Note that instead of calling the plain old CCRenderTexture:begin method, we call a convenience method named beginWithClear:g:b:a: that clears the texture with a particular color before drawing.

We don’t draw anything yet – for now let’s just see what this plain colored texture looks like.

Wrap this up by replacing your init method with the following new version (plus a few new methods):

- (ccColor4F)randomBrightColor {
 
    while (true) {
        float requiredBrightness = 192;
        ccColor4B randomColor = 
            ccc4(arc4random() % 255,
                 arc4random() % 255, 
                 arc4random() % 255, 
                 255);
        if (randomColor.r > requiredBrightness || 
            randomColor.g > requiredBrightness ||
            randomColor.b > requiredBrightness) {
            return ccc4FFromccc4B(randomColor);
        }        
    }
 
}
 
- (void)genBackground {
 
    [_background removeFromParentAndCleanup:YES];
 
    ccColor4F bgColor = [self randomBrightColor];
    _background = [self spriteWithColor:bgColor textureSize:512];
 
    CGSize winSize = [CCDirector sharedDirector].winSize;
    _background.position = ccp(winSize.width/2, winSize.height/2);        
    [self addChild:_background z:-1];
 
}
 
-(id) init {
    if((self=[super init])) {		        
        [self genBackground];
        self.isTouchEnabled = YES;        
    }
    return self;
}
 
- (void)ccTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
 
    [self genBackground];
 
}

The randomBrightColor method is a helper method to create a random color. Note it uses ccc4B (so we can specify the R/G/B/A values in the 0-255 range), and makes sure at least one of them is > 192 so we don’t get dark colors. It then converts it to a ccc4F (which converts the values to the 0-1 range).

Then genBackground method calls the spriteWithColor method we just wrote, and adds it to the center of the screen.

As for init, it calls genBackground and enables touches so that we can re-generate another random background just by tapping the screen.

Compile and run, and every time you run the app or tap the screen it will have a different colored background!

A dynamic background with a random color

Creating Noise For the Texture

As you’ve probably noticed in Tiny Wings, the textures aren’t just flat colors – they’re decorated with a bit of noise to make them look like they have shadows and highlights.

You can always write some code to make dynamic noise, but it’s often easier (and more performant) to just ship your app with some premade noise – and that’s what we’re going to do in this tutorial.

One easy way to make random noise is through a free image editing program called Gimp. The rest of this section will show you how to make this texture for yourself, but if you’re like this guy:

"F that!" guy

Then you can just download the noise image I already made, add that to your project, and skip to the next section :]

If you want to follow along, download Gimp if you haven’t already, and start it up.

After Gimp starts up, go to File\New, and create a new Image of size 512×512. Then go to Filter\Render\Clouds\Solid Noise, tweak the parameters if you want, and click OK. You should then have something that looks like this:

Creating random noise with Gimp

We’re going to use this image to multiply the texture’s colors. So wherever the image is white, the original color will show through, and wherever it’s black, the original color will be darkened.

As it currently stands, there’s too much black in the image for the subtle effect we’re looking for. So to reduce the amount of black, go to Colors\Levels, and drag the leftmost slider in the “Output Levels” section to the right, and you’ll see the image begin to lighten up:

Modifying levels with Gimp

Click OK when you’re done. Then there’s one last step – the noise texture needs to be made seamless, so that if we repeat the texture we apply it to everything lines up OK.

Gimp makes this extremely easy. Just go to Filters\Map\Make Seamless – and you’re done!

Use File\Save As and save your image as Noise.png somewhere on your hard drive. Then find the file in Finder and drag it into your TinySeals project. Verify that “Copy items into destination group’s folder” is selected, and click Finish.

Congrats, now you have a noise texture you can use to make your dynamic textures look more cool and realistic!

And now that you know how to create this basic texture, you can play around with it using Gimp’s filters to get different effects!

Applying Noise to Texture

Now that we have an image with some noise data, let’s apply it to the texture we’re creating with CCRenderTexture.

Inside spriteWithColor:textureSize, add the following code right after the comment for step 3:

CCSprite *noise = [CCSprite spriteWithFile:@"Noise.png"];
[noise setBlendFunc:(ccBlendFunc){GL_DST_COLOR, GL_ZERO}];
noise.position = ccp(textureSize/2, textureSize/2);
[noise visit];

This creates a CCSprite with the noise texture, centers it within the render texture, and calls visit. The visit routine is what executes all of the OpenGL commands required to draw the texture.

There’s only one tricky bit – it’s this “setBlendFunc” method. What in the heck does that doe?!

Well, the first constant passed in (GL_DST_COLOR) specifies how to multiply the incoming/source color (which is the noise texture), and the second constant passed in (GL_ZERO) specified how to multiply the existing/destination color (which is the colored texture).

So effectively:

  • The existing color is multiplied by GL_ZERO, which means the existing color is cleared out.
  • The noise texture colors are multiplied by GL_DST_COLOR. GL_DST_COLOR means the existing colors, so the noise texture colors are multiplied by the existing color. So the more “white” in the noise, the more of the existing color appears, but the more “black” in the noise the darker the existing color is.
  • The above two colors are added together, and since the first is zero all that really matters in this case is the second result.

By the way, I found (and still sort-of find) these blend constants confusing, but luckily there’s a great online tool that you can use to visualize the effects of these blend constants.

Anyway, that’s all you need! Compile and run your code, and now you’ll see a subtle shadow effect on your texture. Note that it often doesn’t look good on the simulator, you might need to try an actual device.

A dynamic background with noise applied for shadows and lighting

Adding a Gradient to the Texture

To make the texture look even better, let’s add a gradient from top to bottom, where the texture will get darker and darker going down.

We could do this by modifying the noise image with Gimp, but we can also do it in code, which makes things more dynamic and easily modifiable.

The basic idea is we’ll draw a black rectangle on top of the texture, but it will be completely transparent up top, and opaque at the bottom. This will keep the top untouched, but gradually darken the image going down.

To do this, we’ll need to use some OpenGL commands. If you’re new to OpenGL, don’t worry – I’ll show you the code you need and explain how it works at a high level, which will get you through this tutorial. At the end of the tutorial, I’ll give you a reference for more information.

Inside spriteWithColor:textureSize, add the following code right before creating the noise sprite:

glDisable(GL_TEXTURE_2D);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
 
float gradientAlpha = 0.7;    
CGPoint vertices[4];
ccColor4F colors[4];
int nVertices = 0;
 
vertices[nVertices] = CGPointMake(0, 0);
colors[nVertices++] = (ccColor4F){0, 0, 0, 0 };
vertices[nVertices] = CGPointMake(textureSize, 0);
colors[nVertices++] = (ccColor4F){0, 0, 0, 0};
vertices[nVertices] = CGPointMake(0, textureSize);
colors[nVertices++] = (ccColor4F){0, 0, 0, gradientAlpha};
vertices[nVertices] = CGPointMake(textureSize, textureSize);
colors[nVertices++] = (ccColor4F){0, 0, 0, gradientAlpha};
 
glVertexPointer(2, GL_FLOAT, 0, vertices);
glColorPointer(4, GL_FLOAT, 0, colors);
glDrawArrays(GL_TRIANGLE_STRIP, 0, (GLsizei)nVertices);
 
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glEnable(GL_TEXTURE_2D);

The first thing this does is disable the GL_TEXTURE_2D and GL_TEXTURE_COORD_ARRAY OpenGL state, because we’re about to draw just colors – not textures.

One weird thing about drawing textures is that the upper left is 0,0 – rather than the lower left like we’re used to in Cocos2D.

So next it defines the four vertices for the texture in the order
top left, top right, bottom left, bottom right – and the colors at each point.

You might wonder why the vertices were drawn in this order. That is because we’re going to draw two triangles to make up this rectangle:

Drawing texture with an OpenGL triangle strip

We’re going to draw these triangle using GL_TRIANGLE_STRIP, which means the first triangle is the first three vertices in the array, and the rest of the triangles take the previous two vertices and the next vertex.

So the first triangle is V0, V1, V2, and the second triangle is V1, V2, V3.

After defining the array of vertices and colors, we pass them to OpenGL with glVertexPointer (specifying 2 floats per vertex) and glColorPointer (specifying 4 floats per color value), and call glDrawArrays specifying GL_TRIANGLE_STRIP.

At the end it re-enables GL_TEXTURE_COORD_ARRAY and GL_TEXTURE_2D, to get things back to the way they were when they started.

Compile and run your code, and you should see a neat gradient texture!

Dynamic background with noise and gradient

Creating a Texture with Stripes

Before we start writing code to draw stripes, let’s take a minute to think about our strategy.

We’ll start by coloring the texture one color (say blue), and then we’ll draw several stripes diagonally across it (say green), as you can see in the diagram below:

Algorithm for drawing stripes

Notice that since the stripes are diagonal, we’re actually going to have to start the drawing outside of the bounds of the texture, and continue drawing some of the stripes outside of the bounds of the texture.

Also note that to get a nice 45 degree angle, we’ll offset V1 from V0 by the size of the texture (that way both the DX and the DY are textureSize, hence a 45 degree angle).

OK, let’s see the code for this. Add a new method above the init method as follows:

-(CCSprite *)stripedSpriteWithColor1:(ccColor4F)c1 color2:(ccColor4F)c2 textureSize:(float)textureSize  stripes:(int)nStripes {
 
    // 1: Create new CCRenderTexture
    CCRenderTexture *rt = [CCRenderTexture renderTextureWithWidth:textureSize height:textureSize];
 
    // 2: Call CCRenderTexture:begin
    [rt beginWithClear:c1.r g:c1.g b:c1.b a:c1.a];
 
    // 3: Draw into the texture    
 
    // Layer 1: Stripes
    glDisable(GL_TEXTURE_2D);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);
    glDisableClientState(GL_COLOR_ARRAY);
 
    CGPoint vertices[nStripes*6];
    int nVertices = 0;
    float x1 = -textureSize;
    float x2;
    float y1 = textureSize;
    float y2 = 0;
    float dx = textureSize / nStripes * 2;
    float stripeWidth = dx/2;
    for (int i=0; i<nStripes; i++) {
        x2 = x1 + textureSize;
        vertices[nVertices++] = CGPointMake(x1, y1);
        vertices[nVertices++] = CGPointMake(x1+stripeWidth, y1);
        vertices[nVertices++] = CGPointMake(x2, y2);
        vertices[nVertices++] = vertices[nVertices-2];
        vertices[nVertices++] = vertices[nVertices-2];
        vertices[nVertices++] = CGPointMake(x2+stripeWidth, y2);
        x1 += dx;
    }
 
    glColor4f(c2.r, c2.g, c2.b, c2.a);
    glVertexPointer(2, GL_FLOAT, 0, vertices);
    glDrawArrays(GL_TRIANGLES, 0, (GLsizei)nVertices);
 
    glEnableClientState(GL_COLOR_ARRAY);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);
    glEnable(GL_TEXTURE_2D);
 
    // Layer 2: Noise    
    CCSprite *noise = [CCSprite spriteWithFile:@"Noise.png"];
    [noise setBlendFunc:(ccBlendFunc){GL_DST_COLOR, GL_ZERO}];
    noise.position = ccp(textureSize/2, textureSize/2);
    [noise visit];
 
    // 4: Call CCRenderTexture:end
    [rt end];
 
    // 5: Create a new Sprite from the texture
    return [CCSprite spriteWithTexture:rt.sprite.texture];
 
}

Most of this method is review for how to create a CCRenderTexture, however the code to create the stripes layer is new so let’s discuss that.

It first creates an array of vertices – for each stripe, we need 6 vertices – 3 vertices times 2 triangles. We can’t use triangle strip here, because the stripes aren’t adjacent.

The first vertex is at (-textureSize, textureSize) as you can see in the diagram above, and the next vertex is (-textureSize+stripWidth, textureSize). The third is at (0, 0) and the fourth is at (stripeWidth, textureSize). These are the vertices we need for one stripe, and each time we advance by double the length of a stripe and continue until all stripes are done.

Now you can try this out by modifying your genBackground method as follows:

- (void)genBackground {
 
    [_background removeFromParentAndCleanup:YES];
 
    ccColor4F bgColor = [self randomBrightColor];
    ccColor4F color2 = [self randomBrightColor];
    //_background = [self spriteWithColor:bgColor textureSize:512];
    int nStripes = ((arc4random() % 4) + 1) * 2;
    _background = [self stripedSpriteWithColor1:bgColor color2:color2 textureSize:512 stripes:nStripes];
 
    self.scale = 0.5;
 
    CGSize winSize = [CCDirector sharedDirector].winSize;
    _background.position = ccp(winSize.width/2, winSize.height/2);        
    [self addChild:_background];
 
}

This calls the new method, and also sets the scale of the layer to 0.5 to make it easier to see the entire 512×512 texture.

Compile and run, and as you tap you should see randomly generated stripe textures!

Dynamically generated stripe

Repeating Backgrounds

For both the striped background and the gradient background, we want to be able to tile them across an area of space that may be wider than the texture.

A simple way of doing this would be to make multiple CCSprites and chain them together. But that would be crazy, because there’s a simpler way – we can set up the textures so they repeat!

Try this out for yourself by making the following changes to HelloWorldLayer.m:

// Add to genBackground, right BEFORE the call to addChild
ccTexParams tp = {GL_LINEAR, GL_LINEAR, GL_REPEAT, GL_REPEAT};
[_background.texture setTexParameters:&tp];
 
// Add to bottom of init
[self scheduleUpdate];
 
// Add after init
- (void)update:(ccTime)dt {
 
    float PIXELS_PER_SECOND = 100;
    static float offset = 0;
    offset += PIXELS_PER_SECOND * dt;
 
    CGSize textureSize = _background.textureRect.size;
    [_background setTextureRect:CGRectMake(offset, 0, textureSize.width, textureSize.height)];
 
}

The important part is the texture parameters:

  • GL_LINEAR is a fancy way of saying “when displaying the texture at a smaller or larger scale than the original size, take a weighted average of the nearby pixels.”
  • GL_REPEAT is a fancy way of saying “if you try to index a texture at a coordinate outside the texture bounds, put what would be there if the texture were to continuously tile.”

Also, it schedules an update that updates the visible part of the texture to be continuously moving forward along the x-axis. This allows you to see the texture repeating over time.

Compile and run and now you’ll see a continuously scrolling and repeating texture! You can try this with the background gradient texture and that will work also.

Gratuitous Highlights

If you look at the Tiny Wings implementation of the hill texture, you’ll see it has a slight highlight as top and a gradient along the bottom to make it look nicer. So let’s modify our stripe texture for this.

Add the following right after the call to glDrawArrays in stripedSpriteWithColor1:

// layer 2: gradient
glEnableClientState(GL_COLOR_ARRAY);
 
float gradientAlpha = 0.7;    
ccColor4F colors[4];
nVertices = 0;
 
vertices[nVertices] = CGPointMake(0, 0);
colors[nVertices++] = (ccColor4F){0, 0, 0, 0 };
vertices[nVertices] = CGPointMake(textureSize, 0);
colors[nVertices++] = (ccColor4F){0, 0, 0, 0};
vertices[nVertices] = CGPointMake(0, textureSize);
colors[nVertices++] = (ccColor4F){0, 0, 0, gradientAlpha};
vertices[nVertices] = CGPointMake(textureSize, textureSize);
colors[nVertices++] = (ccColor4F){0, 0, 0, gradientAlpha};
 
glVertexPointer(2, GL_FLOAT, 0, vertices);
glColorPointer(4, GL_FLOAT, 0, colors);
glDrawArrays(GL_TRIANGLE_STRIP, 0, (GLsizei)nVertices);
 
// layer 3: top highlight    
float borderWidth = textureSize/16;
float borderAlpha = 0.3f;
nVertices = 0;
 
vertices[nVertices] = CGPointMake(0, 0);
colors[nVertices++] = (ccColor4F){1, 1, 1, borderAlpha};
vertices[nVertices] = CGPointMake(textureSize, 0);
colors[nVertices++] = (ccColor4F){1, 1, 1, borderAlpha};
 
vertices[nVertices] = CGPointMake(0, borderWidth);
colors[nVertices++] = (ccColor4F){0, 0, 0, 0};
vertices[nVertices] = CGPointMake(textureSize, borderWidth);
colors[nVertices++] = (ccColor4F){0, 0, 0, 0};
 
glVertexPointer(2, GL_FLOAT, 0, vertices);
glColorPointer(4, GL_FLOAT, 0, colors);
glBlendFunc(GL_DST_COLOR, GL_ONE_MINUS_SRC_ALPHA);
glDrawArrays(GL_TRIANGLE_STRIP, 0, (GLsizei)nVertices);

This code should be review by now, but the first part creates a gradient like we did for the gradient background earlier, and the second part adds a highlight to the top part of the stripes to make it look like the sun is shining on it a bit.

Compile and run, and now you should see your stripes looking even better, with a gradient and a highlight!

Finished dynamic background with stripes, highlight, gradient, and shading mask

Where To Go From Here?

Here is the sample project with all of the code we’ve developed in the above tutorial.

If you want, why not try to modify the noise texture to something of your own creation to see what sort of effects you can make? Or tweak this code in other ways to see if you can make it look even better. If you do, please share your results in the forum discussion!

From here, check out the next part of the tutorial series, where you’ll learn how you can use these dynamic textures to make a game like Tiny Wings! :]


iPhoneCategory:

Tags: , , , , ,

I'd love to hear your thoughts!