将精灵从灰度着色为彩色

Posted

技术标签:

【中文标题】将精灵从灰度着色为彩色【英文标题】:Colorize sprites from grayscale to color 【发布时间】:2018-02-08 06:17:18 【问题描述】:

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

我可以创建灰度图像并寻找这样做的可能性:

-> 着色为:

-> 着色为:

【问题讨论】:

【参考方案1】:

要调整灰度精灵,可以通过一个简单的片段着色器来完成,它将纹理的纹理像素的颜色与色调颜色相乘。 这会导致灰度纹理在亮度上改变恒定颜色。 以下所有着色器都考虑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;

创建一个着色器程序,将其添加到精灵并在初始化期间设置制服:

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);

从精灵创建灰度并为灰度着色

如果您首先必须从 RGB 精灵创建灰度,然后要为精灵着色,那么您必须稍微调整片段着色器。

灰度颜色通常用公式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 纹理成员:

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,则算法必须是像这样初始化:

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 完成。

简化插值算法

由于颜色通道被编码为一个字节 (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]);

【讨论】:

以上是关于将精灵从灰度着色为彩色的主要内容,如果未能解决你的问题,请参考以下文章

Unity:在 UI 画布图像的着色器中访问精灵二级纹理

检测一个精灵的颜色是不是不是另一个精灵的颜色

如何在片段着色器中平铺部分纹理

在立方体贴图中跨摄像机渲染点精灵

带有CSS的图像灰度和鼠标悬停时重新着色?

Swift iOS Sprite Kit - 将精灵从一种颜色淡化为另一种颜色[关闭]