c++ - 从灰色到彩色对 Sprite 进行着色

标签 c++ glsl shader cocos2d-x

我有很多相同的图形,但颜色不同。我想通过从灰度图像着色来优化它。此外,我想在游戏中为实时 Sprite 对象动态更改其颜色。也逐渐将颜色值从一种颜色类型更改为另一种颜色类型。

Don't know it it useful - Image-Transformation-Grayscale-to-Color.

最佳答案

要色调灰度 Sprite ,可以通过一个简单的片段着色器完成,该着色器将纹理的纹理像素的颜色与色调颜色相乘。
这导致灰度纹理使亮度呈现恒定的颜色。
以下所有着色器均考虑Premultiplied Alpha

顶点着色器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;
}

片段着色器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 );
}

为着色器程序对象添加一个类成员:
cocos2d::GLProgram* mProgram;

创建一个着色器程序,将其添加到 Sprite 中,并在初始化期间设置制服:
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);

从 Sprite 创建灰度并为灰度着色

如果首先必须从RGB Sprite 创建灰度,然后又想着色该 Sprite ,则必须稍微调整片段着色器。

通常使用gray = 0.2126 * red + 0.7152 * green + 0.0722 * blue公式创建灰度颜色(在网络上,有不同的亮度公式和说明:Luma (video)Seven grayscale conversion algorithms。)
根据距离,您可以在原始颜色和黑白之间进行插值。
#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 );
}

渐变纹理贴图

要进行从灰度到颜色的映射,也可以使用渐变纹理。请参见以下片段着色器:
#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 );
}

要使用此着色器,必须添加2D纹理Mebmer:
cocos2d::Texture2D* mGradinetTexture;

纹理和制服必须像这样设置:
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);

进一步的改进将是自动调整颜色的渐变
#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 );
}

如果纹理的不透明部分和纹理的透明部分之间应该存在硬过渡,则设置片段颜色的着色器部分必须像这样进行调整:
float alpha  = step( 0.5, texColor.a ) * lookUpCol.a;
gl_FragColor = vec4( lookUpCol.rgb * alpha, alpha );

生成渐变纹理

要通过一组颜色创建渐变纹理,建议使用Newton polynomial。以下算法处理任意数量的颜色,这些颜色必须分布在渐变上。
每种颜色都必须映射为灰度值,并且灰度值必须按升序设置。该算法必须设置至少2种颜色。

这意味着,例如,如果存在颜色c0c1c2,它们对应于灰度值g0g1g2,则必须像这样初始化算法:

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++代码:
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 );

请注意,在这种情况下,当然必须使用没有自动调整的着色器,因为调整会线性化非线性渐变。
这是从灰度颜色到RGB颜色的简单映射。映射表的左侧(灰度值)是恒定的,而表的右侧(RGB值)必须调整为纹理,必须从灰度纹理重新创建。优点是可以生成所有灰度值,因为会生成渐变映射纹理。
映射表的颜色与源纹理完全匹配时,将插值介于两者之间的颜色。

注意,对于渐变纹理,必须将纹理过滤器参数设置为GL_NEAREST,以获得准确的结果。在cocos2d-x中,可以通过Texture2D::setAliasTexParameters完成。

简化插值算法

由于将颜色 channel 编码为一个字节(unsigned byte),因此可以简化插值算法,而不会明显降低质量,尤其是当某些颜色的颜色不止3种时。
以下算法对基点之间的颜色进行线性插值。从起点到第一个点,从RGB颜色(0,0,0)到第一个颜色存在线性插值。最后,保留最后一个RGB颜色(超出最后一个基点),以避免出现明亮的白色毛刺。
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]);
}

关于c++ - 从灰色到彩色对 Sprite 进行着色,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/45962006/

相关文章:

c++ - 如何将文本文件中的数据读入结构数组

c++ - 在函数内部使用 "new"

opengl - Nvidia 显卡上的阴影痤疮

opengl - GLSL:整数指数的 pow 与乘法

c++ - 循环 bool 值

c++ - new[] 是否连续分配内存?

c++ - GLSL 计算着色器不适用于大输入

opengl - 我需要在片段着色器中进行输出 Gamma 校正吗?

java - GLSL奇怪的编译错误

DirectX 11 顶点和像素着色器如何工作