About cocos2d color blend: (only for opengl es 1.1)

来源:互联网 发布:软件运营维护方案 编辑:程序博客网 时间:2024/05/21 08:03
Ref: http://digitallybold.com/314/cocos2d-additive-sprite-color-tinting

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:
  • Cheetah Lurk’s: Coloring Sprites with Cocos2d-iPhone
  • Brandon Trebs’: Cocos2D Tutorial – Dynamically Coloring Sprites
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(00255)] 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(2550255)]).  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:
Click to toggle codeblock
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061
-(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_; // vertexNSInteger 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 coordsdiff = 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 boxCGPoint 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 boxCGSize 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:
Click to toggle codeblock
1
glBindTexture(GL_TEXTURE_2D, [texture_ name]);
and add beneath it these lines so that it reads:
Click to toggle codeblock
12345
glBindTexture(GL_TEXTURE_2D, [texture_ name]);glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA );//the magicglTexEnvi(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:
Click to toggle codeblock
123
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:
Click to toggle codeblock
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859
-(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 magicglTexEnvi(GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_COMBINE);glTexEnvi(GL_TEXTURE_ENV, GL_COMBINE_RGB, GL_ADD); long offset = (long)&quad_; // vertexNSInteger diff = offsetof( ccV3F_C4B_T2F, vertices);glVertexPointer(3, GL_FLOAT, kQuadSize, (void*) (offset + diff) ); // colordiff = offsetof( ccV3F_C4B_T2F, colors);glColorPointer(4, GL_UNSIGNED_BYTE, kQuadSize, (void*)(offset + diff)); // tex coordsdiff = 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_DRAWCGSize 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:
Click to toggle codeblock
12345678910111213
#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:
Click to toggle codeblock
123456789
-(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:
Click to toggle codeblock
1
glBindTexture(GL_TEXTURE_2D, [texture_ name]);
and add to it so it reads:
Click to toggle codeblock
12345678910111213
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:
Click to toggle codeblock
1234567
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:
Click to toggle codeblock
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
-(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_; // vertexNSInteger diff = offsetof( ccV3F_C4B_T2F, vertices);glVertexPointer(3, GL_FLOAT, kQuadSize, (void*) (offset + diff) ); // colordiff = offsetof( ccV3F_C4B_T2F, colors);glColorPointer(4, GL_UNSIGNED_BYTE, kQuadSize, (void*)(offset + diff)); // tex coordsdiff = 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_DRAWCGSize 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:
原创粉丝点击