Recolor sprites on the fly

The algorithm in the article How to Use a Shader to Dynamically Swap a Sprite's Colors is very simple. It is based on a one dimensional lookup table with 256 entries. This allows the algorithm to map only 256 different colors.

In detail, the new colors (the colors used to replace) are stored in a one dimensional texture with 256 entries. When a color is read from the original texture a key is used to find the new color in the one dimensional swap texture. The key which is used is the red color channel of the original color, this means that all different colors in the original text must also have different red color values. This is another restriction.
The original document (How to Use a Shader to Dynamically Swap a Sprite's Colors) says:

Note that this may not work as expected if two or more colors on the sprite texture share the same red value! When using this method, it's important to keep the red values of the colors in the sprite texture different.

Further the algorithm mix the original color and the swap color by the alpha channel of the swap color. That causes that the swap color is drawn if the swap color is completely opaque and the original color is drawn if the swap color is completely transparent, in between will be linearly interpolated.

A GLSL function with this algorithm is very short and looks somehow like this:

uniform sampler2D u_spriteTexture; // sprite texture 
uniform sampler1D u_swapTexture;   // lookup texture with swap colors

vec4 SwapColor( vec2 textureCoord )
{
    vec4 originalColor = texture( u_spriteTexture, textureCoord.st );
    vec4 swapColor     = texture( u_swapTexture, originalColor.r );
    vec3 finalColor    = mix( originalColor.rgb, swapColor.rgb, swapColor.a );
    return vec4( finalColor.rgb, originalColor.a );
}

Suggested Algorithm

Reading the suggested shader from the question, I came up to the following solution. The shader is using an algorithm to convert from RGB to hue, saturation, and value and back. I took this idea and introduced my own thoughts.

Performant conversion functions between RGB and HSV can be found at RGB to HSV/HSL/HCY/HCL in HLSL, which can easily translated from HLSL to GLSL:

RGB to HSV

const float Epsilon = 1e-10;

vec3 RGBtoHCV( in vec3 RGB )
{
   vec4 P = (RGB.g < RGB.b) ? vec4(RGB.bg, -1.0, 2.0/3.0) : vec4(RGB.gb, 0.0, -1.0/3.0);
   vec4 Q = (RGB.r < P.x) ? vec4(P.xyw, RGB.r) : vec4(RGB.r, P.yzx);
   float C = Q.x - min(Q.w, Q.y);
   float H = abs((Q.w - Q.y) / (6.0 * C + Epsilon) + Q.z);
   return vec3(H, C, Q.x);
}

vec3 RGBtoHSV(in vec3 RGB)
{
    vec3 HCV = RGBtoHCV(RGB);
    float S = HCV.y / (HCV.z + Epsilon);
    return vec3(HCV.x, S, HCV.z);
}

HSV to RGB

vec3 HUEtoRGB(in float H)
{
    float R = abs(H * 6.0 - 3.0) - 1.0;
    float G = 2.0 - abs(H * 6.0 - 2.0);
    float B = 2.0 - abs(H * 6.0 - 4.0);
    return clamp( vec3(R,G,B), 0.0, 1.0 );
}

vec3 HSVtoRGB(in vec3 HSV)
{
    vec3 RGB = HUEtoRGB(HSV.x);
    return ((RGB - 1.0) * HSV.y + 1.0) * HSV.z;
}

As in the first algorithm of this answer, again a one dimensional lookup table is of need. But the length of the look up table has not to be exactly 256, it is completely user dependent. The key is not the red channel, it is the hue value which is a clear expression of the color and can easily be calculated as seen in RGBtoHSV and RGBtoHSV. The look-up table however has to, contain a color assignment distributed linearly over the * hue * range from 0 to 1 of the original color.

The algorithm can be defined with the following steps:

  • Convert the original color to the original hue, saturation, and value
  • Use the original hue as key to find the swap color in the look up table
  • Convert the swap color to the swap hue, saturation, and value
  • Convert the hue of the swap color and the original saturation, and value to a new RGB color
  • Mix the original color and the new color by the alpha channel of the swap color

With this algorithm any RGB color can be swapped, by keeping the saturation and value of the original color. See the following short and clear GLSL function:

uniform sampler2D u_spriteTexture; // sprite texture 
uniform sampler1D u_swapTexture;   // lookup texture with swap colors 
                                   // the texture coordinate is the hue of the original color

vec4 SwapColor( vec2 textureCoord )
{
    vec4 originalColor = texture( u_spriteTexture, textureCoord.st );
    vec3 originalHSV   = RGBtoHSV( originalColor.rgb );
    vec4 lookUpColor   = texture( u_swapTexture, originalHSV.x );
    vec3 swapHSV       = RGBtoHSV( lookUpColor.rgb );
    vec3 swapColor     = HSVtoRGB( vec3( swapHSV.x, originalHSV.y, originalHSV.z ) );
    vec3 finalColor    = mix( originalColor.rgb, swapColor.rgb, lookUpColor.a );
    return vec4( finalColor.rgb, originalColor.a );
}


Apply to cocos2d-x v3.15

To apply the shader to cocos2d-x v3.15 I adapted the HelloWorldScene.h and HelloWorldScene.cpp in the project cpp-empty-test of the cocos2d-x v3.15 test projects.
The shader can be applied to any sprite a can swap up to 10 color tints, but this can easily be expanded. Note, the shader does not only change a single color, it searches all colors which are similar to a color, even the colors with a completely different saturation or brightness. Each color is swapped with a color, that has a equal saturation and brightness, but a new base color.
The information which swaps the colors, is stored in an array of vec3. The x component contains the hue of the original color, the y component contains the hue of the swap color, and the z component contains an epsilon value, which defines the color range.

The shader source files should be placed in the "resource/shader" subdirectory of the project directory.

Vertex shader shader/colorswap.vert

attribute vec4 a_position;
attribute vec2 a_texCoord;
attribute vec4 a_color;

varying vec4 cc_FragColor;
varying vec2 cc_FragTexCoord1;

void main()
{
    gl_Position = CC_PMatrix * a_position;
    cc_FragColor = a_color;
    cc_FragTexCoord1 = a_texCoord;
}

Fragment shader shader/colorswap.frag

#ifdef GL_ES
precision mediump float;
#endif

varying vec4 cc_FragColor;
varying vec2 cc_FragTexCoord1;

const float Epsilon = 1e-10;

vec3 RGBtoHCV( in vec3 RGB )
{
   vec4 P = (RGB.g < RGB.b) ? vec4(RGB.bg, -1.0, 2.0/3.0) : vec4(RGB.gb, 0.0, -1.0/3.0);
   vec4 Q = (RGB.r < P.x) ? vec4(P.xyw, RGB.r) : vec4(RGB.r, P.yzx);
   float C = Q.x - min(Q.w, Q.y);
   float H = abs((Q.w - Q.y) / (6.0 * C + Epsilon) + Q.z);
   return vec3(H, C, Q.x);
}

vec3 RGBtoHSV(in vec3 RGB)
{
    vec3 HCV = RGBtoHCV(RGB);
    float S = HCV.y / (HCV.z + Epsilon);
    return vec3(HCV.x, S, HCV.z);
}

vec3 HUEtoRGB(in float H)
{
    float R = abs(H * 6.0 - 3.0) - 1.0;
    float G = 2.0 - abs(H * 6.0 - 2.0);
    float B = 2.0 - abs(H * 6.0 - 4.0);
    return clamp( vec3(R,G,B), 0.0, 1.0 );
}

vec3 HSVtoRGB(in vec3 HSV)
{
    vec3 RGB = HUEtoRGB(HSV.x);
    return ((RGB - 1.0) * HSV.y + 1.0) * HSV.z;
}

#define MAX_SWAP 10
uniform vec3 u_swap[MAX_SWAP];
uniform int  u_noSwap;

void main()
{
    vec4 originalColor = texture2D(CC_Texture0, cc_FragTexCoord1);
    vec3 originalHSV   = RGBtoHSV( originalColor.rgb );
    vec4 swapColor     = vec4( originalColor.rgb, 1.0 );

    for ( int i = 0; i < 10 ; ++ i )
    {
        if ( i >= u_noSwap )
            break;
        if ( abs( originalHSV.x - u_swap[i].x ) < u_swap[i].z )
        {
            swapColor.rgb = HSVtoRGB( vec3( u_swap[i].y, originalHSV.y, originalHSV.z ) );
            break;
        }
    }

    vec3 finalColor    = mix( originalColor.rgb, swapColor.rgb, swapColor.a );
    gl_FragColor       = vec4( finalColor.rgb, originalColor.a );
} 

Header file HelloWorldScene.h:

#ifndef __HELLOWORLD_SCENE_H__
#define __HELLOWORLD_SCENE_H__

#include "cocos2d.h"

#define MAX_COLOR 10

class HelloWorld : public cocos2d::Scene
{
public:
    virtual bool init() override;
    static cocos2d::Scene* scene();
    void menuCloseCallback(Ref* sender);
    CREATE_FUNC(HelloWorld);
    void InitSwapInfo( int i, const cocos2d::Color3B &sourceCol, const cocos2d::Color3B &swapCol, float deviation );
private:
    cocos2d::GLProgram* mProgramExample;
    cocos2d::Vec3 mSource[MAX_COLOR];
    cocos2d::Vec3 mSwap[MAX_COLOR];
    float mDeviation[MAX_COLOR];
    cocos2d::Vec3 mSwapInfo[MAX_COLOR];
};

#endif // __HELLOWORLD_SCENE_H__

Source file HelloWorldScene.cpp:

Note, the C++ function RGBtoHue and the GLSL function RGBtoHue, should implement the exactly same algorithm.
The input to the function SwapInfo are RGB colors encoded to cocos2d::Vec3. If the source channels of the RGB colors are bytes (unsigned char), then this can easily converted to cocos2d::Vec3 by cocos2d::Vec3( R / 255.0f, G / 255.0f, B / 255.0f ).

#include "HelloWorldScene.h"
#include "AppMacros.h"

USING_NS_CC;

float RGBtoHue( const cocos2d::Vec3 &RGB )
{
   const float Epsilon = 1e-10f;
   cocos2d::Vec4 P = (RGB.y < RGB.z) ? 
       cocos2d::Vec4(RGB.y, RGB.z, -1.0f, 2.0f/3.0f) :
       cocos2d::Vec4(RGB.y, RGB.z, 0.0f, -1.0f/3.0f);
   cocos2d::Vec4 Q = (RGB.x < P.x) ? 
       cocos2d::Vec4(P.x, P.y, P.w, RGB.x) :
       cocos2d::Vec4(RGB.x, P.y, P.z, P.x);
   float C = Q.x - (Q.w < Q.y ? Q.w : Q.y);
   float H = fabs((Q.w - Q.y) / (6.0f * C + Epsilon) + Q.z);
   return H;
}

cocos2d::Vec3 SwapInfo( const cocos2d::Vec3 &sourceCol, const cocos2d::Vec3 &swapCol, float epsi )
{
  return cocos2d::Vec3( RGBtoHue( sourceCol ), RGBtoHue( swapCol ), epsi );
}

void  HelloWorld::InitSwapInfo( int i, const cocos2d::Color3B &sourceCol, const cocos2d::Color3B &swapCol, float deviation )
{
    mSource[i]    = cocos2d::Vec3( sourceCol.r/255.0, sourceCol.g/255.0, sourceCol.b/255.0 );
    mSwap[i]      = cocos2d::Vec3( swapCol.r/255.0, swapCol.g/255.0, swapCol.b/255.0 );
    mDeviation[i] = deviation;
    mSwapInfo[i]  = SwapInfo( mSource[i], mSwap[i], mDeviation[i] );
}

Scene* HelloWorld::scene()
{
     return HelloWorld::create();
}

bool HelloWorld::init()
{
    if ( !Scene::init() )  return false;     
    auto visibleSize = Director::getInstance()->getVisibleSize();
    auto origin = Director::getInstance()->getVisibleOrigin();

    auto closeItem = MenuItemImage::create(
                                        "CloseNormal.png",
                                        "CloseSelected.png",
                                        CC_CALLBACK_1(HelloWorld::menuCloseCallback,this));

    closeItem->setPosition(origin + Vec2(visibleSize) - Vec2(closeItem->getContentSize() / 2));

    auto menu = Menu::create(closeItem, nullptr);
    menu->setPosition(Vec2::ZERO);
    this->addChild(menu, 1);

    auto sprite = Sprite::create("HelloWorld.png");
    sprite->setPosition(Vec2(visibleSize / 2) + origin);

    mProgramExample = new GLProgram();
    mProgramExample->initWithFilenames("shader/colorswap.vert", "shader/colorswap.frag");
    mProgramExample->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_POSITION, GLProgram::VERTEX_ATTRIB_POSITION);
    mProgramExample->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_COLOR, GLProgram::VERTEX_ATTRIB_COLOR);
    mProgramExample->bindAttribLocation(GLProgram::ATTRIBUTE_NAME_TEX_COORD, GLProgram::VERTEX_ATTRIB_TEX_COORDS);
    mProgramExample->link();
    mProgramExample->updateUniforms(); 
    mProgramExample->use();

    GLProgramState* state = GLProgramState::getOrCreateWithGLProgram(mProgramExample);
    sprite->setGLProgram(mProgramExample);
    sprite->setGLProgramState(state);

    InitSwapInfo( 0, cocos2d::Color3B( 41, 201, 226 ), cocos2d::Color3B( 255, 0, 0 ),   0.1f );
    InitSwapInfo( 1, cocos2d::Color3B( 249, 6, 6 ),    cocos2d::Color3B( 255, 255, 0 ), 0.1f );
    int noOfColors = 2;
    state->setUniformVec3v("u_swap", noOfColors, mSwapInfo);
    state->setUniformInt("u_noSwap", noOfColors);

    this->addChild(sprite);

    return true;
}

void HelloWorld::menuCloseCallback(Ref* sender)
{
    Director::getInstance()->end();

#if (CC_TARGET_PLATFORM == CC_PLATFORM_IOS)
    exit(0);
#endif
}


Compare RGB values instead of Hue

A fragment shader which directly compares RGB colors would look like this:

#ifdef GL_ES
precision mediump float;
#endif

varying vec4 cc_FragColor;
varying vec2 cc_FragTexCoord1;

const float Epsilon = 1e-10;

vec3 RGBtoHCV( in vec3 RGB )
{
   vec4 P = (RGB.g < RGB.b) ? vec4(RGB.bg, -1.0, 2.0/3.0) : vec4(RGB.gb, 0.0, -1.0/3.0);
   vec4 Q = (RGB.r < P.x) ? vec4(P.xyw, RGB.r) : vec4(RGB.r, P.yzx);
   float C = Q.x - min(Q.w, Q.y);
   float H = abs((Q.w - Q.y) / (6.0 * C + Epsilon) + Q.z);
   return vec3(H, C, Q.x);
}

vec3 RGBtoHSV(in vec3 RGB)
{
    vec3 HCV = RGBtoHCV(RGB);
    float S = HCV.y / (HCV.z + Epsilon);
    return vec3(HCV.x, S, HCV.z);
}

vec3 HUEtoRGB(in float H)
{
    float R = abs(H * 6.0 - 3.0) - 1.0;
    float G = 2.0 - abs(H * 6.0 - 2.0);
    float B = 2.0 - abs(H * 6.0 - 4.0);
    return clamp( vec3(R,G,B), 0.0, 1.0 );
}

vec3 HSVtoRGB(in vec3 HSV)
{
    vec3 RGB = HUEtoRGB(HSV.x);
    return ((RGB - 1.0) * HSV.y + 1.0) * HSV.z;
}

#define MAX_SWAP 10
uniform vec3  u_orig[MAX_SWAP];
uniform vec3  u_swap[MAX_SWAP];
uniform float u_deviation[MAX_SWAP];
uniform int   u_noSwap;

void main()
{
    vec4 originalColor = texture2D(CC_Texture0, cc_FragTexCoord1);
    vec3 originalHSV   = RGBtoHSV( originalColor.rgb );
    vec4 swapColor     = vec4( originalColor.rgb, 1.0 );

    for ( int i = 0; i < 10 ; ++ i )
    {
        if ( i >= u_noSwap )
            break;
        if ( all( lessThanEqual( abs(originalColor.rgb - u_orig[i]), vec3(u_deviation[i]) ) ) )
        {
            vec3 swapHSV  = RGBtoHSV( u_swap[i].rgb );
            swapColor.rgb = HSVtoRGB( vec3( swapHSV.x, originalHSV.y, originalHSV.z ) );
            break;
        }
    }

    vec3 finalColor    = mix( originalColor.rgb, swapColor.rgb, swapColor.a );
    gl_FragColor       = vec4( finalColor.rgb, originalColor.a );
}

Note, the initialization of the uniforms has to be adapt:

int noOfColors = 2;
state->setUniformVec3v("u_orig", noOfColors, mSource);
state->setUniformVec3v("u_swap", noOfColors, mSwap);
state->setUniformFloatv("u_deviation", noOfColors, mDeviation);
state->setUniformInt("u_noSwap", noOfColors);

Extension to the answer

If exactly specified colors should be exchanged, the shader can be much more simplified. For this, the deviations u_deviation have to be restricted (e.g deviation = 0.02;).

#ifdef GL_ES
precision mediump float;
#endif

varying vec4 cc_FragColor;
varying vec2 cc_FragTexCoord1;

#define MAX_SWAP 11
uniform vec3  u_orig[MAX_SWAP];
uniform vec3  u_swap[MAX_SWAP];
uniform float u_deviation[MAX_SWAP];
uniform int   u_noSwap;

void main()
{
    vec4 originalColor = texture2D(CC_Texture0, cc_FragTexCoord1);
    vec4 swapColor     = vec4( originalColor.rgb, 1.0 );

    for ( int i = 0; i < MAX_SWAP ; ++ i )
    {
        vec3  deltaCol = abs( originalColor.rgb - u_orig[i] );
        float hit      = step( deltaCol.x + deltaCol.y + deltaCol.z, u_deviation[i] * 3.0 );
        swapColor.rgb  = mix( swapColor.rgb, u_swap[i].rgb, hit );
    }

    gl_FragColor    = vec4( swapColor.rgb, originalColor.a );
}


If each color in the source texture has an individual color channel (this means the color value is only use for this special color, e.g. red color channel), then the shader code can be further simplified, because only the one channel has to be compared:

void main()
{
    vec4 originalColor = texture2D(CC_Texture0, cc_FragTexCoord1);
    vec4 swapColor     = vec4( originalColor.rgb, 1.0 );

    for ( int i = 0; i < MAX_SWAP ; ++ i )
    {
        float hit      = step( abs( originalColor.r - u_orig[i].r ), u_deviation[i] );
        swapColor.rgb  = mix( swapColor.rgb, u_swap[i].rgb, hit );
    }

    gl_FragColor    = vec4( swapColor.rgb, originalColor.a );
}


A further optimization would bring us back to the first algorithm, which was described in this answer. The big advantage of this algorithm would be, that each color is swapped (except the alpha channel of the swap texture is 0), but no expensive searching in the look up table has to be done in the shader.
Each color will be swapped by the corresponding color according to its red color channel. As mentioned, if a color should not be swapped, the alpha channel of the swap texture has to be set to 0.

A new member mSwapTexture has to be add to the class:

cocos2d::Texture2D* mSwapTexture;

The texture can be easily created, and the uniform texture sampler can be set like this:

#include <array>

.....

std::array< unsigned char, 256 * 4 > swapPlane{ 0 };
for ( int c = 0; c < noOfColors; ++ c )
{
    size_t i = (size_t)( mSource[c].x * 255.0 ) * 4;
    swapPlane[i+0] = (unsigned char)(mSwap[c].x*255.0);
    swapPlane[i+1] = (unsigned char)(mSwap[c].y*255.0);
    swapPlane[i+2] = (unsigned char)(mSwap[c].z*255.0);
    swapPlane[i+3] = 255;
}
mSwapTexture = new Texture2D();
mSwapTexture->setAliasTexParameters();
cocos2d::Size contentSize;
mSwapTexture->initWithData( swapPlane.data(), swapPlane.size(), Texture2D::PixelFormat::RGBA8888, 256, 1, contentSize );
state->setUniformTexture( "u_swapTexture", mSwapTexture );

The fragment shader would look like this:

#ifdef GL_ES
precision mediump float;
#endif

varying vec4 cc_FragColor;
varying vec2 cc_FragTexCoord1;

uniform sampler2D u_swapTexture;   // lookup texture with 256 swap colors

void main()
{
    vec4 originalColor = texture2D(CC_Texture0, cc_FragTexCoord1);
    vec4 swapColor     = texture2D(u_swapTexture, vec2(originalColor.r, 0.0));
    vec3 finalColor    = mix(originalColor.rgb, swapColor.rgb, swapColor.a); 
    gl_FragColor       = vec4(finalColor.rgb, originalColor.a);
}

Of course, the lookup key has not always to be the red channel, any other channel is also possible.
Even a combination of 2 color channels would be possible by using a increased two dimensional lookup texture. See the following example which demonstrates the use of look up texture with 1024 entries. The look up table uses the full red channel (256 indices) in the X dimension and the green channel divided by 64 (4 indices) in the Y dimension.

Create a two dimensional look up table:

std::array< unsigned char, 1024 * 4 > swapPlane{ 0 };
for ( int c = 0; c < noOfColors; ++ c )
{
    size_t ix = (size_t)( mSource[c].x * 255.0 );
    size_t iy = (size_t)( mSource[c].y * 255.0 / 64.0 );
    size_t i = ( iy * 256 + ix ) * 4;
    swapPlane[i+0] = (unsigned char)(mSwap[c].x*255.0);
    swapPlane[i+1] = (unsigned char)(mSwap[c].y*255.0);
    swapPlane[i+2] = (unsigned char)(mSwap[c].z*255.0);
    swapPlane[i+3] = 255;
}
mSwapTexture = new Texture2D();
mSwapTexture->setAliasTexParameters();
cocos2d::Size contentSize;
mSwapTexture->initWithData( swapPlane.data(), swapPlane.size(), Texture2D::PixelFormat::RGBA8888, 256, 4, contentSize ); 

And adapt the fragment shader:

void main()
{
    vec4 originalColor = texture2D(CC_Texture0, cc_FragTexCoord1);
    vec4 swapColor     = texture2D(u_swapTexture, originalColor.rg);
    vec3 finalColor    = mix(originalColor.rgb, swapColor.rgb, swapColor.a); 
    gl_FragColor       = vec4(finalColor.rgb, originalColor.a);
}


Interpolate the texture

Since it is not possible to use GL_LINEAR with the above approach, this has to be emulated, if it would be of need:

#ifdef GL_ES
precision mediump float;
#endif

varying vec4 cc_FragColor;
varying vec2 cc_FragTexCoord1;

uniform sampler2D u_swapTexture;   // lookup texture with 256 swap colors
uniform vec2 u_spriteSize;

void main()
{
    vec2 texS = 1.0 / u_spriteSize;
    vec2 texF = fract( cc_FragTexCoord1 * u_spriteSize + 0.5 );
    vec2 texC = (cc_FragTexCoord1 * u_spriteSize + 0.5 - texF) / u_spriteSize; 

    vec4 originalColor = texture2D(CC_Texture0, texC);
    vec4 swapColor     = texture2D(u_swapTexture, originalColor.rg);
    vec3 finalColor00  = mix(originalColor.rgb, swapColor.rgb, swapColor.a);

    originalColor     = texture2D(CC_Texture0, texC+vec2(texS.x, 0.0));
    swapColor         = texture2D(u_swapTexture, originalColor.rg);
    vec3 finalColor10 = mix(originalColor.rgb, swapColor.rgb, swapColor.a);

    originalColor     = texture2D(CC_Texture0, texC+vec2(0.0,texS.y));
    swapColor         = texture2D(u_swapTexture, originalColor.rg);
    vec3 finalColor01 = mix(originalColor.rgb, swapColor.rgb, swapColor.a);

    originalColor     = texture2D(CC_Texture0, texC+texS.xy);
    swapColor         = texture2D(u_swapTexture, originalColor.rg);
    vec3 finalColor11 = mix(originalColor.rgb, swapColor.rgb, swapColor.a);

    vec3 finalColor0 = mix( finalColor00, finalColor10, texF.x );
    vec3 finalColor1 = mix( finalColor01, finalColor11, texF.x );
    vec3 finalColor  = mix( finalColor0, finalColor1, texF.y );

    gl_FragColor = vec4(finalColor.rgb, originalColor.a);
}

The new uniform variable u_spriteSize has to be set like this:

auto size = sprite->getTexture()->getContentSizeInPixels();
state->setUniformVec2( "u_spriteSize", Vec2( (float)size.width, (float)size.height ) );


Modify the texture on the CPU

Of course the texture can also be modified on the CPU, but then for each set of swap colors a separated texture has to be generated. the advantage would be that no more shader is of need.
The following code swaps the colors when the texture is loaded. The shader has to be skipped completely.

Sprite * sprite = nullptr;

std::string     imageFile = ....;
std::string     fullpath  = FileUtils::getInstance()->fullPathForFilename(imageFile);
cocos2d::Image *img       = !fullpath.empty() ? new Image() : nullptr;
if (img != nullptr && img->initWithImageFile(fullpath))
{
    if ( img->getRenderFormat() == Texture2D::PixelFormat::RGBA8888 )
    {
        unsigned char *plane = img->getData();
        for ( int y = 0; y < img->getHeight(); ++ y )
        {
            for ( int x = 0; x < img->getWidth(); ++ x )
            { 
                size_t i = ( y * img->getWidth() + x ) * 4;
                unsigned char t = plane[i];
                for ( int c = 0; c < noOfColors; ++ c )
                {
                    if ( fabs(mSource[c].x - plane[i+0]/255.0f) < mDeviation[c] &&
                         fabs(mSource[c].y - plane[i+1]/255.0f) < mDeviation[c] &&
                         fabs(mSource[c].z - plane[i+2]/255.0f) < mDeviation[c] )
                    {
                        plane[i+0] = (unsigned char)(mSwap[c].x*255.0);
                        plane[i+1] = (unsigned char)(mSwap[c].y*255.0);
                        plane[i+2] = (unsigned char)(mSwap[c].z*255.0);
                    }
                }
            }
        }
    }

    std::string key = "my_swap_" + imageFile;
    if ( Texture2D *texture = _director->getTextureCache()->addImage( img, key ) )
        sprite = Sprite::createWithTexture( texture );
}


Combined approach on the CPU and GPU

This approach can be used if always the same regions (colors) of the texture are swapped. The advantage of this approach is, that the original texture is modified only once, but every application of the texture can hold its own swap table.
For this approach the alpha channel is used to hold the index of the swap color. In the example code below, the value range from 1 to including 11 is used to store the indices of the swap color. 0 is reserved for absolute transparency.

Sprite * sprite = nullptr;

std::string     imageFile = ....;
std::string     key       = "my_swap_" + imageFile;
Texture2D      *texture   = _director->getTextureCache()->getTextureForKey( key );
if (texture == nullptr)
{
    std::string     fullpath  = FileUtils::getInstance()->fullPathForFilename(imageFile);
    cocos2d::Image *img       = !fullpath.empty() ? new Image() : nullptr;
    if ( img->initWithImageFile(fullpath) &&
         img->getRenderFormat() == Texture2D::PixelFormat::RGBA8888 )
    {
        unsigned char *plane = img->getData();
        for ( int y = 0; y < img->getHeight(); ++ y )
        {
            for ( int x = 0; x < img->getWidth(); ++ x )
            { 
                size_t i = ( y * img->getWidth() + x ) * 4;
                unsigned char t = plane[i];
                for ( int c = 0; c < noOfColors; ++ c )
                {
                    if ( fabs(mSource[c].x - plane[i+0]/255.0f) < mDeviation[c] &&
                         fabs(mSource[c].y - plane[i+1]/255.0f) < mDeviation[c] &&
                         fabs(mSource[c].z - plane[i+2]/255.0f) < mDeviation[c] )
                    {
                        plane[i+3] = (unsigned char)(c+1);
                    }
                }
            }
        }
        texture = _director->getTextureCache()->addImage( img, key );
    }
}
if ( texture != nullptr )
    sprite = Sprite::createWithTexture( texture );

The fragment shader needs only the uniforms u_swap and u_noSwap and does not have to do an expensive searching.

#ifdef GL_ES
precision mediump float;
#endif

varying vec4 cc_FragColor;
varying vec2 cc_FragTexCoord1;

#define MAX_SWAP 11
uniform vec3  u_swap[MAX_SWAP];
uniform int   u_noSwap;

void main()
{
    vec4  originalColor = texture2D(CC_Texture0, cc_FragTexCoord1);
    float fIndex        = originalColor.a * 255.0 - 0.5;
    float maxIndex      = float(u_noSwap) + 0.5; 
    int   iIndex        = int( clamp( fIndex, 0.0, maxIndex ) );
    float isSwap        = step( 0.0, fIndex ) * step( fIndex, maxIndex );
    vec3  swapColor     = mix( originalColor.rgb, u_swap[iIndex], isSwap );
    gl_FragColor        = vec4( swapColor.rgb, max(originalColor.a, isSwap) );
}