Development Blog

Cocos2D: Additive Coloring & Flash Style Tinting

Posted by:

Cocos2D: Additive Coloring & Flash Style Tinting

Share on TwitterShare on TumblrSubmit to StumbleUponSave on DeliciousDigg ThisSubmit to reddit

We all love Cocos2d. Well, I love Cocos2d and if you’re an iOS developer I’m sure you do too. I mean why not? It’s free and it’s a fantastic 2d game engine. Coinciding this love of Cocos2d comes a love of Flash. And while Flash may be taking a diminishing mobile presence it’s still one of the best platforms to develop a 2d game. I say this from a strictly development perspective, it’s just so easy to prototype and even develop a game in Flash. Mostly because the tools you need are just a couple lines of code away and it has years worth of frameworks and libraries to help get you where you want to go quickly.

Here’s the thing, Flash can color a sprite in ways that vanilla Cocos2d does not. We’ve seen quite a few articles on the web about how to color a CCSprite in order to use one sprite for a variety of purposes. And they have been excellent articles at that, here’s a couple if you want to check them out:

And this works incredibly well… until you need additive coloring. And don’t even think about tinting, that’s a whole new level of crazy.  You might be saying, there’s CCTintTo and CCTintBy actions in Cocos2d, it CAN tint. Well we’ll leave that to semantics, but to me CCTintTo and CCTintBy are not tinting anything. It’s just more of the same “subtractive” style of coloring that we get with Cocos2d. Before we go on, if you don’t know how to color a sprite as illustrated in the two articles above, I recommend you check them out. I’ll go into it with slight detail but it’s going to be important that you understand it because much of the same methods will be employed to get our additive and tinting effects.

Seeing is Believing

Okay so before I explain how we’re going to achieve coloring bliss let’s just take a look at what we’re going to be able to do.


Has this got you excited? No. Okay well let me explain. I took the same graphic created 3 sprites and used three different coloring methods to apply a setColor of pure blue. So in other words I ran [sprite setColor:ccc3(0, 0, 255)] on all three sprites and produced different results.

  • SPRITE – This is our wonderful sprite before we’ve done any coloring.
  • DEFAULT – This is standard cocos2d way of coloring sprites. If you are wondering what really happened here then let me explain. We didn’t add any blue on the contrary we removed all the red and green. By setting red and green to 0 we removed all of their color from the sprite.
  • ADD – This is the first new method of coloring that we are going to discuss. In this situation we did the exact opposite of what Cocos2d does by default. Instead of subtracting red and green, we added blue. Using this we can turn a black sprite any color but a white sprite will always stay white. Also using add we have to set the color to ccc3(0, 0, 0) by default instead of ccc3(255, 255, 255) or every sprite will automatically appear white. It’s quite the reverse of the cocos2d standard
  • TINT – My favorite, that took me a few headaches to figure out. We literally just turned every pixel in that sprite blue. And while, at the moment, this seems almost pointless you are missing the best part. I can turn it blue incrementally instead of completely by adding a new variable to control the amount of tinting.

Here’s some tinting in increments instead of all out. This is tinting to a magenta color ([sprite setColor:ccc3(255, 0, 255)]).  Tint amount is set on a 0-255 scale.

Now, if you are still not excited then this isn’t for you. Come back later when this looks like fun :D

How does this work, exactly?

It’s magic voodoo. Wait, no it’s something worse… OpenGL. I posed a question a few months back in the Cocos2d forums trying to produce the above tinting effect. What I wanted was Flash like tinting. In Flash I’ve always been able to use colorTransform and change incrementally any graphic to a color I wanted. This can be huge for games! I can make a character green if they are poisoned, red if they are burnt, flash if they are selected, the list goes on. But I couldn’t do this with Cocos2D without using at least two sprites. I even thought it was going to take two sprites no matter what I tried, I just wanted an easy solution. In the end the above effects are accomplished with one sprite, which makes me very happy.

So Birkemose on the Cocos2d forum pointed me in the right direction… openGL and glTexEnv. **Head explodes** Okay, okay, maybe not explode. In seriousness though, I spent the next few days breaking code repetitively searching for a solution and trying to understand with what I was dealing. Months later, I’m still not sure I fully understand :D

Okay so here’s the basics… we need to change up the texture  pipeline. If you want the full details of how what we are getting to experiment with works then read this, http://ofps.oreilly.com/titles/9780596804824/chadvanced.html. I do recommend the reading; however, if you just want to get things working just follow me for now.

Let’s get Additive

So if you’ve used Cocos2d before you know that all our OpenGL is performed in the draw call. So we know that’s where we are going to start and, for additive sprite coloring, it’s also where we are going to end.  So if you are following along with me, you have two choices either edit CCSprite directly or subclass it and copy the draw method from CCSprite into the subclass. I named my subclass CCSpriteAdd but you can do whatever you want.

Here’s what the CCSprite draw method looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
-(void) draw
{
 
[super draw];
 
NSAssert(!usesBatchNode_, @"If CCSprite is being rendered by CCSpriteBatchNode, CCSprite#draw SHOULD NOT be called");
 
// Default GL states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Needed states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Unneeded states: -
 
BOOL newBlend = blendFunc_.src != CC_BLEND_SRC || blendFunc_.dst != CC_BLEND_DST;
if( newBlend )
glBlendFunc( blendFunc_.src, blendFunc_.dst );
 
#define kQuadSize sizeof(quad_.bl)
glBindTexture(GL_TEXTURE_2D, [texture_ name]);
 
long offset = (long)&quad_;
 
// vertex
NSInteger diff = offsetof( ccV3F_C4B_T2F, vertices);
glVertexPointer(3, GL_FLOAT, kQuadSize, (void*) (offset + diff) );
 
// color
 
diff = offsetof( ccV3F_C4B_T2F, colors);
glColorPointer(4, GL_UNSIGNED_BYTE, kQuadSize, (void*)(offset + diff));
 
// tex coords
diff = offsetof( ccV3F_C4B_T2F, texCoords);
glTexCoordPointer(2, GL_FLOAT, kQuadSize, (void*)(offset + diff));
 
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
 
if( newBlend )
glBlendFunc(CC_BLEND_SRC, CC_BLEND_DST);
 
#if CC_SPRITE_DEBUG_DRAW == 1
// draw bounding box
CGPoint vertices[4]={
ccp(quad_.tl.vertices.x,quad_.tl.vertices.y),
ccp(quad_.bl.vertices.x,quad_.bl.vertices.y),
ccp(quad_.br.vertices.x,quad_.br.vertices.y),
ccp(quad_.tr.vertices.x,quad_.tr.vertices.y),
};
 
ccDrawPoly(vertices, 4, YES);
#elif CC_SPRITE_DEBUG_DRAW == 2
// draw texture box
CGSize s = self.textureRect.size;
CGPoint offsetPix = self.offsetPositionInPixels;
CGPoint vertices[4] = {
ccp(offsetPix.x,offsetPix.y), ccp(offsetPix.x+s.width,offsetPix.y),
ccp(offsetPix.x+s.width,offsetPix.y+s.height), ccp(offsetPix.x,offsetPix.y+s.height)
};
 
ccDrawPoly(vertices, 4, YES);
#endif // CC_SPRITE_DEBUG_DRAW
 
}
-(void) draw
{

[super draw];

NSAssert(!usesBatchNode_, @"If CCSprite is being rendered by CCSpriteBatchNode, CCSprite#draw SHOULD NOT be called");

// Default GL states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Needed states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Unneeded states: -

BOOL newBlend = blendFunc_.src != CC_BLEND_SRC || blendFunc_.dst != CC_BLEND_DST;
if( newBlend )
glBlendFunc( blendFunc_.src, blendFunc_.dst );

#define kQuadSize sizeof(quad_.bl)
glBindTexture(GL_TEXTURE_2D, [texture_ name]);

long offset = (long)&quad_;

// vertex
NSInteger diff = offsetof( ccV3F_C4B_T2F, vertices);
glVertexPointer(3, GL_FLOAT, kQuadSize, (void*) (offset + diff) );

// color

diff = offsetof( ccV3F_C4B_T2F, colors);
glColorPointer(4, GL_UNSIGNED_BYTE, kQuadSize, (void*)(offset + diff));

// tex coords
diff = offsetof( ccV3F_C4B_T2F, texCoords);
glTexCoordPointer(2, GL_FLOAT, kQuadSize, (void*)(offset + diff));

glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

if( newBlend )
glBlendFunc(CC_BLEND_SRC, CC_BLEND_DST);

#if CC_SPRITE_DEBUG_DRAW == 1
// draw bounding box
CGPoint vertices[4]={
ccp(quad_.tl.vertices.x,quad_.tl.vertices.y),
ccp(quad_.bl.vertices.x,quad_.bl.vertices.y),
ccp(quad_.br.vertices.x,quad_.br.vertices.y),
ccp(quad_.tr.vertices.x,quad_.tr.vertices.y),
};

ccDrawPoly(vertices, 4, YES);
#elif CC_SPRITE_DEBUG_DRAW == 2
// draw texture box
CGSize s = self.textureRect.size;
CGPoint offsetPix = self.offsetPositionInPixels;
CGPoint vertices[4] = {
ccp(offsetPix.x,offsetPix.y), ccp(offsetPix.x+s.width,offsetPix.y),
ccp(offsetPix.x+s.width,offsetPix.y+s.height), ccp(offsetPix.x,offsetPix.y+s.height)
};

ccDrawPoly(vertices, 4, YES);
#endif // CC_SPRITE_DEBUG_DRAW

}

Okay first of all, if you are subclassing CCSprite, remove that [super draw] call. We need to get it out of the way because we are going to write a draw function in our subclass that replaces the one in CCSprite’s draw. If you are editing CCSprite directly then you’ll just make changes to the draw call. (This isn’t about speed or future proofing but about showing how to manipulate textures. If you would like to write a class after this is over that is optimized for these things then feel free to do so.)

Okay find the line that reads:

1
glBindTexture(GL_TEXTURE_2D, [texture_ name]);
glBindTexture(GL_TEXTURE_2D, [texture_ name]);

and add beneath it these lines so that it reads:

1
2
3
4
5
glBindTexture(GL_TEXTURE_2D, [texture_ name]);
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
//the magic
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_ADD);
glBindTexture(GL_TEXTURE_2D, [texture_ name]);
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
//the magic
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_ADD);

That’s it. That’s all the magic.  You may notice an issue, I’m forcing glBlendFunc here. That’s because you are going to need to use this blend function to make it work, there may be other setups that will work as well and may work better but for my case it worked perfectly. You can pull the blendFunc line out and set it on the sprite itself if you want to but this works fine.

Let’s break this down:

  • glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE); Okay first we set the GL_TEXTURE_ENV_MODE to GL_COMBINE so we can combine the rgbs of our texture and color. (GL_ADD produces same results)
  • glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_ADD); Now when it combines RGB it adds our sprite texture and the setColor color together. So if our sprite has a texture with a pixel that has 10 blue in it and our sprites color is set to 240 then our blue when appear at 250. This occurs in all 3 channels. So in other words, every pixel’s color has the color we set our sprite added to it until it reaches 255.

We’re done, except you really should do one more thing. Reset the glTexEnvi states back to default. If you don’t then every sprite rendered afterwards will go through the same process.

So at the very bottom of the draw method add:

1
2
3
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB,      GL_MODULATE);
glBlendFunc( CC_BLEND_SRC, CC_BLEND_DST );
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB,      GL_MODULATE);
glBlendFunc( CC_BLEND_SRC, CC_BLEND_DST );

That’s it. you’re draw function should look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
-(void) draw
 
{
 
NSAssert(!usesBatchNode_, @"If CCSprite is being rendered by CCSpriteBatchNode, CCSprite#draw SHOULD NOT be called");
 
// Default GL states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Needed states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Unneeded states: -
 
BOOL newBlend = NO;
if( blendFunc_.src != CC_BLEND_SRC || blendFunc_.dst != CC_BLEND_DST ) {
newBlend = YES;
glBlendFunc( blendFunc_.src, blendFunc_.dst );
}
 
#define kQuadSize sizeof(quad_.bl)
 
glBindTexture(GL_TEXTURE_2D, [texture_ name]);
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
//the magic
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_ADD);
 
long offset = (long)&quad_;
 
// vertex
NSInteger diff = offsetof( ccV3F_C4B_T2F, vertices);
glVertexPointer(3, GL_FLOAT, kQuadSize, (void*) (offset + diff) );
 
// color
diff = offsetof( ccV3F_C4B_T2F, colors);
glColorPointer(4, GL_UNSIGNED_BYTE, kQuadSize, (void*)(offset + diff));
 
// tex coords
diff = offsetof( ccV3F_C4B_T2F, texCoords);
glTexCoordPointer(2, GL_FLOAT, kQuadSize, (void*)(offset + diff));
 
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
 
if( newBlend )
glBlendFunc(CC_BLEND_SRC, CC_BLEND_DST);
 
#if CC_SPRITE_DEBUG_DRAW
CGSize s = [self contentSize];
CGPoint vertices[4]={
ccp(0,0),ccp(s.width,0),
ccp(s.width,s.height),ccp(0,s.height),
};
 
ccDrawPoly(vertices, 4, YES);
 
#endif // CC_TEXTURENODE_DEBUG_DRAW
 
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB,      GL_MODULATE);
glBlendFunc( CC_BLEND_SRC, CC_BLEND_DST );
 
}
-(void) draw

{

NSAssert(!usesBatchNode_, @"If CCSprite is being rendered by CCSpriteBatchNode, CCSprite#draw SHOULD NOT be called");

// Default GL states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Needed states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Unneeded states: -

BOOL newBlend = NO;
if( blendFunc_.src != CC_BLEND_SRC || blendFunc_.dst != CC_BLEND_DST ) {
newBlend = YES;
glBlendFunc( blendFunc_.src, blendFunc_.dst );
}

#define kQuadSize sizeof(quad_.bl)

glBindTexture(GL_TEXTURE_2D, [texture_ name]);
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
//the magic
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_ADD);

long offset = (long)&quad_;

// vertex
NSInteger diff = offsetof( ccV3F_C4B_T2F, vertices);
glVertexPointer(3, GL_FLOAT, kQuadSize, (void*) (offset + diff) );

// color
diff = offsetof( ccV3F_C4B_T2F, colors);
glColorPointer(4, GL_UNSIGNED_BYTE, kQuadSize, (void*)(offset + diff));

// tex coords
diff = offsetof( ccV3F_C4B_T2F, texCoords);
glTexCoordPointer(2, GL_FLOAT, kQuadSize, (void*)(offset + diff));

glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

if( newBlend )
glBlendFunc(CC_BLEND_SRC, CC_BLEND_DST);

#if CC_SPRITE_DEBUG_DRAW
CGSize s = [self contentSize];
CGPoint vertices[4]={
ccp(0,0),ccp(s.width,0),
ccp(s.width,s.height),ccp(0,s.height),
};

ccDrawPoly(vertices, 4, YES);

#endif // CC_TEXTURENODE_DEBUG_DRAW

glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB,      GL_MODULATE);
glBlendFunc( CC_BLEND_SRC, CC_BLEND_DST );

}

Crazy simple, huh? If you are bored play with some of the different settings for GL_COMBINE_RGB including GL_REPLACE, GL_BLEND, and GL_ADD_SIGNED. I recommend trying out GL_ADD_SIGNED in place of GL_ADD as it can produce some very useful effects as well. GL_ADD_SIGNED can be used to remove and add a color to the rendered sprite.

Cocos2D Sprite Tinting: the Real Way

Okay so I recommend subclassing CCSprite again and name this sprite CCSpriteClr or CCSpriteTint or whatever you prefer. Copy the draw function, remove the [super draw] call and let’s get started adding code. But before we modify draw we are going to need a new variable called colorOpacity. colorOpacity will be the amount of tint we are applying to our sprite. We will also want to have setColorOpacity and colorOpacity methods so we can access it outside the sprite. You can synthesize this but I prefer to write the methods myself incase we need to modify how they function later. So my CCSpriteClr.h reads as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
#import
#import "cocos2d.h"
 
@interface CCSpriteClr : CCSprite {
 
GLubyte colorOpacity;
 
}
 
-(void)setColorOpacity:(GLubyte)o;
-(GLubyte)colorOpacity;
 
@end
#import
#import "cocos2d.h"

@interface CCSpriteClr : CCSprite {

GLubyte colorOpacity;

}

-(void)setColorOpacity:(GLubyte)o;
-(GLubyte)colorOpacity;

@end

And the added functions to CCSpriteClr.m look like this:

1
2
3
4
5
6
7
8
9
-(GLubyte)colorOpacity
{
return colorOpacity;
}
 
-(void) setColorOpacity:(GLubyte)o
{
colorOpacity = o;
}
-(GLubyte)colorOpacity
{
return colorOpacity;
}

-(void) setColorOpacity:(GLubyte)o
{
colorOpacity = o;
}

Now you can call [sprite setColorOpacity:128]; to tint your sprite about 50%. Let’s jump to the draw method and start setting up our texture pipeline.

Like before find:

1
glBindTexture(GL_TEXTURE_2D, [texture_ name]);
glBindTexture(GL_TEXTURE_2D, [texture_ name]);

and add to it so it reads:

1
2
3
4
5
6
7
8
9
10
11
12
13
glBindTexture(GL_TEXTURE_2D, [texture_ name]);
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
float tinting[4] = { color_.r/255.0f, color_.g/255.0f, color_.b/255.0f, colorOpacity/255.0f };
glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, tinting);
 
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB,      GL_INTERPOLATE);
glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB,         GL_TEXTURE);
glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB,         GL_CONSTANT);
glTexEnvi(GL_TEXTURE_ENV, GL_SRC2_RGB,         GL_CONSTANT);
glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND2_RGB,   GL_ONE_MINUS_SRC_ALPHA);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_SUBTRACT);
glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glBindTexture(GL_TEXTURE_2D, [texture_ name]);
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
float tinting[4] = { color_.r/255.0f, color_.g/255.0f, color_.b/255.0f, colorOpacity/255.0f };
glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, tinting);

glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB,      GL_INTERPOLATE);
glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB,         GL_TEXTURE);
glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB,         GL_CONSTANT);
glTexEnvi(GL_TEXTURE_ENV, GL_SRC2_RGB,         GL_CONSTANT);
glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND2_RGB,   GL_ONE_MINUS_SRC_ALPHA);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_SUBTRACT);
glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

Wow, that’s a lot more code than before and it can be pretty confusing. There’s a lot of math involved and rather than explain it in detail allow me to simply touch on what is going on here.

  • float tinting[4] = { color_.r/255.0f, color_.g/255.0f, color_.b/255.0f, colorOpacity/255.0f }; Here we are taking the color we set with setColor and mixing our colorOpacity in with it.
  • glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, tinting); We are setting the color that we will combine with our texture. The color of course being drawn from the tinting variable we created in the line before.
  • glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB,      GL_INTERPOLATE); This thing (GL_INTERPOLATE) combines three separate sources to create our final product using this math: OutputColor = Arg0 ∗ Arg2 + Arg1 ∗ (1 − Arg2)
  • glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB,         GL_TEXTURE); This is the first source which is our sprite texture.
  • glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB,         GL_CONSTANT); glTexEnvi(GL_TEXTURE_ENV, GL_SRC2_RGB,         GL_CONSTANT); The second and third source are the same (GL_CONSTANT).  GL_CONSTANT refers to our GL_TEXTURE_ENV_COLOR that we set a few lines before.
  • glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND2_RGB,   GL_ONE_MINUS_SRC_ALPHA); We change the way we want  the third source  to mix with the others.
  • glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_SUBTRACT); glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_ONE_MINUS_SRC_ALPHA); Change the way the alpha combines.

If you want to understand the math behind what is going on above then you can get all the information you need here: http://ofps.oreilly.com/titles/9780596804824/chadvanced.html

A much better person than I has explained how each of these different settings work at the above link. Using it you can probably create all kinds of cool effects.  But before we get off track we need to do one more thing, reset all our settings. So at the end of your draw function add this:

1
2
3
4
5
6
7
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE);
glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PREVIOUS);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_MODULATE);
glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA);
 
glBlendFunc( CC_BLEND_SRC, CC_BLEND_DST );
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE);
glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PREVIOUS);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_MODULATE);
glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA);

glBlendFunc( CC_BLEND_SRC, CC_BLEND_DST );

And your full draw function should look something like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
-(void) draw
{
NSAssert(!usesBatchNode_, @"If CCSprite is being rendered by CCSpriteBatchNode, CCSprite#draw SHOULD NOT be called");
 
// Default GL states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Needed states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Unneeded states: -
 
BOOL newBlend = NO;
if( blendFunc_.src != CC_BLEND_SRC || blendFunc_.dst != CC_BLEND_DST ) {
newBlend = YES;
glBlendFunc( blendFunc_.src, blendFunc_.dst );
}
 
#define kQuadSize sizeof(quad_.bl)
 
glBindTexture(GL_TEXTURE_2D, [texture_ name]);
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );
 
float tinting[4] = { color_.r/255.0f, color_.g/255.0f, color_.b/255.0f, colorOpacity/255.0f };
glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, tinting);
 
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_INTERPOLATE);
glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE);
glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_CONSTANT);
glTexEnvi(GL_TEXTURE_ENV, GL_SRC2_RGB, GL_CONSTANT);
glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND2_RGB, GL_ONE_MINUS_SRC_ALPHA);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_SUBTRACT);
glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
 
long offset = (long)&quad_;
 
// vertex
NSInteger diff = offsetof( ccV3F_C4B_T2F, vertices);
glVertexPointer(3, GL_FLOAT, kQuadSize, (void*) (offset + diff) );
 
// color
diff = offsetof( ccV3F_C4B_T2F, colors);
glColorPointer(4, GL_UNSIGNED_BYTE, kQuadSize, (void*)(offset + diff));
 
// tex coords
diff = offsetof( ccV3F_C4B_T2F, texCoords);
glTexCoordPointer(2, GL_FLOAT, kQuadSize, (void*)(offset + diff));
 
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
 
if( newBlend )
glBlendFunc(CC_BLEND_SRC, CC_BLEND_DST);
 
#if CC_SPRITE_DEBUG_DRAW
CGSize s = [self contentSize];
CGPoint vertices[4]={
ccp(0,0),ccp(s.width,0),
ccp(s.width,s.height),ccp(0,s.height),
};
ccDrawPoly(vertices, 4, YES);
#endif // CC_TEXTURENODE_DEBUG_DRAW
 
glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE);
glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PREVIOUS);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_MODULATE);
glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA);
 
glBlendFunc( CC_BLEND_SRC, CC_BLEND_DST );
 
}
-(void) draw
{
NSAssert(!usesBatchNode_, @"If CCSprite is being rendered by CCSpriteBatchNode, CCSprite#draw SHOULD NOT be called");

// Default GL states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Needed states: GL_TEXTURE_2D, GL_VERTEX_ARRAY, GL_COLOR_ARRAY, GL_TEXTURE_COORD_ARRAY
// Unneeded states: -

BOOL newBlend = NO;
if( blendFunc_.src != CC_BLEND_SRC || blendFunc_.dst != CC_BLEND_DST ) {
newBlend = YES;
glBlendFunc( blendFunc_.src, blendFunc_.dst );
}

#define kQuadSize sizeof(quad_.bl)

glBindTexture(GL_TEXTURE_2D, [texture_ name]);
glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );

float tinting[4] = { color_.r/255.0f, color_.g/255.0f, color_.b/255.0f, colorOpacity/255.0f };
glTexEnvfv(GL_TEXTURE_ENV, GL_TEXTURE_ENV_COLOR, tinting);

glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_INTERPOLATE);
glTexEnvi(GL_TEXTURE_ENV, GL_SRC0_RGB, GL_TEXTURE);
glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_CONSTANT);
glTexEnvi(GL_TEXTURE_ENV, GL_SRC2_RGB, GL_CONSTANT);
glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND2_RGB, GL_ONE_MINUS_SRC_ALPHA);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_SUBTRACT);
glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

long offset = (long)&quad_;

// vertex
NSInteger diff = offsetof( ccV3F_C4B_T2F, vertices);
glVertexPointer(3, GL_FLOAT, kQuadSize, (void*) (offset + diff) );

// color
diff = offsetof( ccV3F_C4B_T2F, colors);
glColorPointer(4, GL_UNSIGNED_BYTE, kQuadSize, (void*)(offset + diff));

// tex coords
diff = offsetof( ccV3F_C4B_T2F, texCoords);
glTexCoordPointer(2, GL_FLOAT, kQuadSize, (void*)(offset + diff));

glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

if( newBlend )
glBlendFunc(CC_BLEND_SRC, CC_BLEND_DST);

#if CC_SPRITE_DEBUG_DRAW
CGSize s = [self contentSize];
CGPoint vertices[4]={
ccp(0,0),ccp(s.width,0),
ccp(s.width,s.height),ccp(0,s.height),
};
ccDrawPoly(vertices, 4, YES);
#endif // CC_TEXTURENODE_DEBUG_DRAW

glTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_MODULATE);
glTexEnvi(GL_TEXTURE_ENV, GL_SRC1_RGB, GL_PREVIOUS);
glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_ALPHA, GL_MODULATE);
glTexEnvi(GL_TEXTURE_ENV, GL_OPERAND1_ALPHA, GL_SRC_ALPHA);

glBlendFunc( CC_BLEND_SRC, CC_BLEND_DST );

}

There you go! You should be set to tint and add color to sprites!

Taking Sprite Coloring to a Whole New Level

Okay so first of all, you may want to optimize this a bit. Maybe you want to set it up so that you have one sprite subclass that can jump between multiple modes and do additive and tinting both in the same sprite. Maybe you want to move this up so it works in batch nodes as well (If that’s the case, look at the CCTextureAtlas). Maybe you just want to play around and see what you can create, I recommend this :D I’d love to see a “cookbook” of different ways you can change the pipeline to do a variety of effects. If you create something new then please post it in the comments! Before we go here’s the source code if you want to download it:


10


About the Author:

Jonathan Parsons (Par) is the lead developer & designer for Digitally Bold. Jonathan began working with web and graphic design back when Photoshop 5.5 (not CS 5.5) was the cool kids weapon of choice. Likewise, he was introduced to programming and started game development back on the original TI-83. The flash game Blueberry is a throwback to his first attempts at game development with the TI-83.

Discussion

  1. center  January 14, 2012

    Thanks for the posts!
    Does it work in a batch node? Let’s say in my game I will have a batch node for all of my sprites. And I just need several sprites to have additive coloring.

    (reply)
    • Par  January 15, 2012

      It will work in a batch node but rendering is done differently with batch nodes than sprites. You will need to either modify the draw call of the CCSpriteBatchNode or the (I think) drawQuads of the CCTextureAtlas. I prefer working with CCTextureAtlas because it’s closer to the draw. For me to make it work, I subclassed CCSpriteBatchNode and CCTextureAtlas. In CCSpriteBatchNode all I did was make sure with it initialized it’s CCTextureAtlas it initialized my subclass instead. Then in the CCTextureAtlas subclass I wrote my code for additive coloring and/or tinting.

      (reply)
      • center  January 17, 2012

        Thanks for your help!

        Could you give me some more clues on what I should override in the drawQuads method? I have tried to study and understand the code and methods in CCTextureAtlas but I don’t really know how to modify it so that I can do both additive tinting for some sprites and normal tinting for the other sprites in one single batchnode.

        (reply)
        • Par  January 17, 2012

          That’s the issue with a Cocos2d CCSpriteBatchNode. It’s made to render multiple sprites together. If you want one to be rendered differently than the other you are going to have to rewrite how the CCSpriteBatchNode works or use different multiple batch nodes.

          Depending on what you are doing, I think you will find that using multiple batch nodes will take a very minimal hit on performance. And I do mean very minimal.

          (reply)
  2. Firstmage  February 2, 2012

    Thanks to sharing.

    I tried your code(add) on cocos2d 2.0beta2. but it doesn’t work…

    if you have any idea of this, plz share it.

    this is cocos2d 2.0 beta2 – CCSprite – draw method.

    {
    CC_PROFILER_START_CATEGORY(kCCProfilerCategorySprite, @”CCSprite – draw”);

    NSAssert(!batchNode_, @”If CCSprite is being rendered by CCSpriteBatchNode, CCSprite#draw SHOULD NOT be called”);

    CC_NODE_DRAW_SETUP();

    ccGLBlendFunc( blendFunc_.src, blendFunc_.dst );

    ccGLBindTexture2D( [texture_ name] );

    //
    // Attributes
    //

    ccGLEnableVertexAttribs( kCCVertexAttribFlag_PosColorTex );

    #define kQuadSize sizeof(quad_.bl)
    long offset = (long)&quad_;

    // vertex
    NSInteger diff = offsetof( ccV3F_C4B_T2F, vertices);
    glVertexAttribPointer(kCCVertexAttrib_Position, 3, GL_FLOAT, GL_FALSE, kQuadSize, (void*) (offset + diff));

    // texCoods
    diff = offsetof( ccV3F_C4B_T2F, texCoords);
    glVertexAttribPointer(kCCVertexAttrib_TexCoords, 2, GL_FLOAT, GL_FALSE, kQuadSize, (void*)(offset + diff));

    // color
    diff = offsetof( ccV3F_C4B_T2F, colors);
    glVertexAttribPointer(kCCVertexAttrib_Color, 4, GL_UNSIGNED_BYTE, GL_TRUE, kQuadSize, (void*)(offset + diff));

    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);

    CHECK_GL_ERROR_DEBUG();

    #if CC_SPRITE_DEBUG_DRAW == 1
    // draw bounding box
    CGPoint vertices[4]={
    ccp(quad_.tl.vertices.x,quad_.tl.vertices.y),
    ccp(quad_.bl.vertices.x,quad_.bl.vertices.y),
    ccp(quad_.br.vertices.x,quad_.br.vertices.y),
    ccp(quad_.tr.vertices.x,quad_.tr.vertices.y),
    };
    ccDrawPoly(vertices, 4, YES);
    #elif CC_SPRITE_DEBUG_DRAW == 2
    // draw texture box
    CGSize s = self.textureRect.size;
    CGPoint offsetPix = self.offsetPosition;
    CGPoint vertices[4] = {
    ccp(offsetPix.x,offsetPix.y), ccp(offsetPix.x+s.width,offsetPix.y),
    ccp(offsetPix.x+s.width,offsetPix.y+s.height), ccp(offsetPix.x,offsetPix.y+s.height)
    };
    ccDrawPoly(vertices, 4, YES);
    #endif // CC_SPRITE_DEBUG_DRAW

    CC_PROFILER_STOP_CATEGORY(kCCProfilerCategorySprite, @”CCSprite – draw”);
    }

    have a good day.

    (reply)
    • Par  February 2, 2012

      It will not work with 2.0.

      2.0 uses OpenGL ES 2.0 which uses shaders. If you are using 2.0 there are much easier and better ways to manipulate how textures are rendered. That being said 2.0 is not supported by older devices.

      Quoted from the cocos2d website about the 2.0 vs 1.0 line:

      A: It depends on your requirements, and varies from case to case.
      A: If you need to support older devices (1st & 2nd generation iPhones) you should use cocos2d v1.x
      A: If you need to GL shaders, you should use cocos2d v2.x
      A: v2.0 is about supporting new technologies (blocks, OpenGL ES 2.0, etc) and improving the current API, while cocos2d v1.x is about backward compatibility

      If you want to use 2.0 then you should look into shaders.

      (reply)
      • Firstmage  February 2, 2012

        ah……… got it.

        I’ll let you know When I solve this problem.

        Thanks.

        (reply)
  3. JamesGrote  February 3, 2012

    Fantastic article! Thanks for sharing. I’ve been buried in subtractive color shading for my apps, and this post is inspiring me to try adding colors.

    (reply)
  4. Taylor Garvin  September 26, 2012

    Open GL, huh? Sounds like witchcraft to me!

    (reply)

Add a Comment