To tone a grayscale sprite, can be done by a simple fragment shader, which multiplies the color of the texel of the texture, with a tint color.
This causes that a constant color is varayed in the brightness by the grayscale texture.
All the following shaders consider Premultiplied Alpha.
Vertex shader shader/tone.vert
attribute vec4 a_position;
attribute vec2 a_texCoord;
varying vec2 cc_FragTexCoord1;
void main()
{
gl_Position = CC_PMatrix * a_position;
cc_FragTexCoord1 = a_texCoord;
}
Fragment shader shader/tone.frag
#ifdef GL_ES
precision mediump float;
#endif
varying vec2 cc_FragTexCoord1;
uniform vec3 u_tintColor;
void main()
{
float normTint = 0.30 * u_tintColor.r + 0.59 * u_tintColor.g + 0.11 * u_tintColor.b;
vec4 texColor = texture2D( CC_Texture0, cc_FragTexCoord1 );
vec3 mixColor = u_tintColor * texColor / normTint;
gl_FragColor = vec4( mixColor.rgb, texColor.a );
}
Add a class member for the shader program object:
cocos2d::GLProgram* mProgram;
Create a shader program, add it to the sprite and set up the uniforms during initialization:
auto sprite = cocos2d::Sprite::create( ..... );
sprite->setPosition( ..... );
mProgram = new cocos2d::GLProgram();
mProgram->initWithFilenames("shader/tone.vert", "shader/tone.frag");
mProgram->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_POSITION, GLProgram::VERTEX_ATTRIB_POSITION);
mProgram->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_TEX_COORD, GLProgram::VERTEX_ATTRIB_TEX_COORDS);
mProgram->link();
mProgram->updateUniforms();
mProgram->use();
GLProgramState* state = GLProgramState::getOrCreateWithGLProgram(mProgram);
sprite->setGLProgram(mProgram);
sprite->setGLProgramState(state);
cocos2d::Color3B tintColor( 255, 255, 0 ); // e.g yellow
cocos2d::Vec3 tintVal( tintColor.r/255.0f, tintColor.g/255.0f, tintColor.b/255.0f );
state->setUniformVec3("u_tintColor", tintVal);
Create grayscale from sprite and tint the grayscale
If you first have to create a grayscale from an RGB sprite and second you want to tint the sprite, then you have to adapt the fragment shader slightly.
A grayscale color is usually created with the formula gray = 0.2126 * red + 0.7152 * green + 0.0722 * blue
(On the web there are different luminance formulas and explanations: Luma (video), Seven grayscale conversion algorithms.)
Depending on the distance, you interpolate between the original color and the black and white color.
#ifdef GL_ES
precision mediump float;
#endif
varying vec2 cc_FragTexCoord1;
uniform vec3 u_tintColor;
void main()
{
float normTint = 0.30 * u_tintColor.r + 0.59 * u_tintColor.g + 0.11 * u_tintColor.b;
vec4 texColor = texture2D( CC_Texture0, cc_FragTexCoord1 );
float gray = 0.30 * texColor.r + 0.59 * texColor.g + 0.11 * texColor.b;
vec3 mixColor = u_tintColor * gray / normTint;
gl_FragColor = vec4( mixColor.rgb, texColor.a );
}
Gradient texture mapping
To do the mapping from the grayscale to the color, a gradient texture can be used too. See the following fragment shader:
#ifdef GL_ES
precision mediump float;
#endif
varying vec2 cc_FragTexCoord1;
uniform sampler2D u_texGrad;
void main()
{
vec4 texColor = texture2D( CC_Texture0, cc_FragTexCoord1 );
vec4 lookUpCol = texture2D( u_texGrad, vec2( texColor.r / max(texColor.a, 0.01), 0.0 ) );
float alpha = texColor.a * lookUpCol.a;
gl_FragColor = vec4( lookUpCol.rgb * alpha, alpha );
}
To use this shader, a 2D texture mebmer has to be add:
cocos2d::Texture2D* mGradinetTexture;
The texture and the uniform has to be set up like this:
std::string gradPath = FileUtils::getInstance()->fullPathForFilename("grad.png");
cocos2d::Image *gradImg = new Image();
gradImg->initWithImageFile( gradPath );
mGradinetTexture = new Texture2D();
mGradinetTexture->setAliasTexParameters();
mGradinetTexture->initWithImage( gradImg );
state->setUniformTexture("u_texGrad", mGradinetTexture);
A further improvement would be to automatically adjust the gradient of the color
#ifdef GL_ES
precision mediump float;
#endif
varying vec2 cc_FragTexCoord1;
uniform sampler2D u_texGrad;
void main()
{
vec4 texColor = texture2D( CC_Texture0, cc_FragTexCoord1 );
vec4 lookUpCol = texture2D( u_texGrad, vec2( texColor.r / max(texColor.a, 0.01), 0.5 ) );
float lookUpGray = 0.30 * lookUpCol.r + 0.59 * lookUpCol.g + 0.11 * lookUpCol.b;
lookUpCol *= texColor.r / lookUpGray;
float alpha = texColor.a * lookUpCol.a;
gl_FragColor = vec4( lookUpCol.rgb * alpha, alpha );
}
If there should be a hard transition between the opaque part of the texture and the transparent part of the texture, then the part of the shaders, which sets the fragment color has to be adapted like this:
float alpha = step( 0.5, texColor.a ) * lookUpCol.a;
gl_FragColor = vec4( lookUpCol.rgb * alpha, alpha );
Generating a gradient texture
To create a gradient texture by a set of colors, I suggest Newton polynomial. The following algorithm deals with any number of colors, which have to be distributed over the gradient.
Each color has to be mapped to an gray value, and the gray values have to be setup in ascending order. The algorithm has to be setup with at least 2 colors.
This means for example, if there are the colors c0
, c1
and c2
, which corresponds to the gray scale values g0
, g1
and g2
, the the algorithm has to be initialized like this:
g0 = 131
g1 = 176
g2 = 244
std::vector< cocos2d::Color3B > gradBase{ cg0, cg1, cg2 };
std::vector< float > x_val{ 131 / 255.0f, 176 / 255.0f, 244 / 255.0f };
std::vector< cocos2d::Color3B > gradBase{ cr0, cr1, cr2 };
std::vector< float > x_val{ 131 / 255.0f, 176 / 255.0f, 244 / 255.0f };
C++ code:
unsigned char ClampColor( float colF )
{
int c = (int)(colF * 255.0f + 0.5f);
return (unsigned char)(c < 0 ? 0 : ( c > 255 ? 255 : c ));
}
std::vector< cocos2d::Color3B > gradBase{ c0, c1, ..., cN };
std::vector< float > x_val{ g0, g1, ..., gn };
for ( int g = 0; g < x_val.size(); ++ g ) {
x_val[g] = x_val[g] / 255.0f;
}
x_val.push_back( 1.0f );
gradBase.push_back( Color3B( 255, 255, 255 ) );
std::vector< std::array< float, 3 > > alpha;
for ( int c = 0; c < (int)gradBase.size(); ++c )
{
std::array< float, 3 >alphaN{ gradBase[c].r / 255.0f, gradBase[c].g / 255.0f, gradBase[c].b / 255.0f };
for ( int i = 0; i < c; ++ i )
{
alphaN[0] = ( alphaN[0] - alpha[i][0] ) / (x_val[c]-x_val[i]);
alphaN[1] = ( alphaN[1] - alpha[i][1] ) / (x_val[c]-x_val[i]);
alphaN[2] = ( alphaN[2] - alpha[i][2] ) / (x_val[c]-x_val[i]);
}
alpha.push_back( alphaN );
}
std::array< unsigned char, 256 * 4 > gradPlane;
for ( int g = 0; g < 256; ++ g )
{
float x = g / 255.0;
std::array< float, 3 >col = alpha[0];
if ( x < x_val[0] )
{
col = { col[0]*x/x_val[0] , col[1]*x/x_val[0], col[2]*x/x_val[0] };
}
else
{
for ( int c = 1; c < (int)gradBase.size(); ++c )
{
float w = 1.0f;
for ( int i = 0; i < c; ++ i )
w *= x - x_val[i];
col = { col[0] + alpha[c][0] * w, col[1] + alpha[c][1] * w, col[2] + alpha[c][2] * w };
}
}
size_t i = g * 4;
gradPlane[i+0] = ClampColor(col[0]);
gradPlane[i+1] = ClampColor(col[1]);
gradPlane[i+2] = ClampColor(col[2]);
gradPlane[i+3] = 255;
}
mGradinetTexture = new Texture2D();
cocos2d::Size contentSize;
mGradinetTexture->setAliasTexParameters();
mGradinetTexture->initWithData( gradPlane.data(), gradPlane.size() / 4, Texture2D::PixelFormat::RGBA8888, 256, 1, contentSize );
Note, in this case of course the shader without the automatically adjustment has to be used, because the adjustment would linearize the nonlinear gradient.
This is a simple mapping from a grayscale color to a RGB color. The left side of the mapping table (the gray scale values) is constant, while the right side of the table (the RGB values) have to be adjusted to the texture, which has to be recreate from the grayscale texture. The advantage is that all grayscale values can be mapped, because a gradient mapping texture is generated.
While the colors of the mapping table exactly match to the source texture, the colors in between are interpolated.
Note, that the texture filter parameters have to be set to GL_NEAREST
, for the gradient texture, to get a accurate result. In cocos2d-x
this can be done by Texture2D::setAliasTexParameters
.
Simplified interpolation algorithm
Since a color channel is encoded into one byte (unsigned byte
) the interpolation algorithm can be simplified, without a noticeable loss of quality, especially if there are some colors more than only 3.
The following algorithm does a linear interpolation of the colors between the base points. From the beginning to the first point there is a linear interpolation from the RGB color (0, 0, 0) to the first color. At the end the (beyond the last base point) the last RGB color is kept, to avoid bright white glitches.
unsigned char ClampColor( float colF )
{
int c = (int)(colF * 255.0f + 0.5f);
return (unsigned char)(c < 0 ? 0 : ( c > 255 ? 255 : c ));
}
std::vector< cocos2d::Color4B >gradBase {
Color4B( 129, 67, 73, 255 ),
Color4B( 144, 82, 84, 255 ),
Color4B( 161, 97, 95, 255 ),
Color4B( 178, 112, 105, 255 ),
Color4B( 195, 126, 116, 255 ),
Color4B( 211, 139, 127, 255 ),
Color4B( 219, 162, 133, 255 ),
Color4B( 228, 185, 141, 255 ),
Color4B( 235, 207, 149, 255 ),
Color4B( 245, 230, 158, 255 ),
Color4B( 251, 255, 166, 255 )
};
std::vector< float > x_val { 86, 101, 116, 131, 146, 159, 176, 193, 209, 227, 244 };
for ( int g = 0; g < x_val.size(); ++ g ) {
x_val[g] = x_val[g] / 255.0f;
}
std::array< unsigned char, 256 * 4 > gradPlane;
size_t x_i = 0;
for ( int g = 0; g < 256; ++ g )
{
float x = g / 255.0;
if ( x_i < x_val.size()-1 && x >= x_val[x_i] )
++ x_i;
std::array< float, 4 > col;
if ( x_i == 0 )
{
std::array< float, 4 > col0{ gradBase