c++colorsglslshadercocos2d-x

Colorize sprites from grayscale to color


I have a lot of same graphics but different colors. I want to optimize it by colorizing from grayscale image. Also, I would like to change it color on a fly, during gameplay for a live sprite object. Also gradually change color values from one color type to another.

I can create a grayscale image and looking for possibility to do this:

input -> colorized to: output

input -> colorized to: enter image description here


Solution

  • 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:

    enter image description here

    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[0].r / 255.0f, gradBase[0].g / 255.0f, gradBase[0].b / 255.0f, gradBase[0].a / 255.0f };
            col = { col0[0]*x/x_val[0] , col0[1]*x/x_val[0], col0[2]*x/x_val[0], col0[3]*x/x_val[0] };
        }
        else if ( x_i == x_val.size() )
        {
            col = { gradBase.back().r / 255.0f, gradBase.back().g / 255.0f, gradBase.back().b / 255.0f, gradBase.back().a / 255.0f };             
        }
        else
        {
            std::array< float, 4 > col0{ gradBase[x_i-1].r / 255.0f, gradBase[x_i-1].g / 255.0f, gradBase[x_i-1].b / 255.0f, gradBase[x_i-1].a / 255.0f };
            std::array< float, 4 > col1{ gradBase[x_i].r / 255.0f, gradBase[x_i].g / 255.0f, gradBase[x_i].b / 255.0f, gradBase[x_i].a / 255.0f };
            float a = (x - x_val[x_i-1]) / (x_val[x_i] - x_val[x_i-1]);
            col = { col0[0] + (col1[0]-col0[0])*a, col0[1] + (col1[1]-col0[1])*a, col0[2] + (col1[2]-col0[2])*a, col0[3] + (col1[3]-col0[3])*a };
        }
    
        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] = ClampColor(col[3]);
    }