Radial gradient - procedural texture for cocos2d-x v3.15+

743 views Asked by At

In cocos2d-x there is already a LayerRadialGradient. Works well, but I've other shaders running on the my game scene, so performance in a problem now. It's using a shader.

I want to replace it with just a Sprite with generated radial gradient texture.

Here is example of LayerRadialGradient drawing result:

LayerRadialGradient::create( 
  Color4B(179, 232, 184, 89),
  Color4B(0, 90, 128, 0),
  620,
  center,
  0.0f);

enter image description here

I've some code suggestions like:

static Color4B Lerp(const Color4B& value1, const Color4B& value2, float amount)
{
    amount = clampf(amount, 0.0f, 1.0f);
    return Color4B(
        (int)MathUtil::lerp(value1.r, value2.r, amount),
        (int)MathUtil::lerp(value1.g, value2.g, amount),
        (int)MathUtil::lerp(value1.b, value2.b, amount),
        (int)MathUtil::lerp(value1.a, value2.a, amount)
    );
}

static float Falloff(float distance, float maxDistance, float scalingFactor)
{
    if (distance <= maxDistance / 3)
    {
        return scalingFactor * (1 - 3 * distance * distance / (maxDistance * maxDistance));
    }
    else if (distance <= maxDistance)
    {
        float x = 1 - distance / maxDistance;
        return (3.f / 2.f) * scalingFactor * x * x;
    }
    else
        return 0;
}

static float Falloff(float distance, float maxDistance)
{
    return Falloff(distance, maxDistance, 1.f);
}

static Texture2D* generateRadialGradientTexture(int size) {
    auto radius = size / 2;
    auto colors = new (std::nothrow) GLubyte[size * size * 4];

    Color4B centerColor(Color4B(179, 232, 184, 89));
    Color4B borderColor(Color4B(0, 90, 128, 0));        

    for (int y = 0; y < size; y++)
    {
        for (int x = 0; x < size; x++)
        {
            float distance = Vec2::ONE.distance(Vec2(x, y) / radius);
            float alpha = Falloff(distance, 1, 1);

            int idx = (y * size + x) * 4;
            float innerGradient = Falloff(distance, 0.6f, 0.8f);
            auto color = Lerp(borderColor, centerColor, innerGradient);

            colors[idx + 0] = color.r;
            colors[idx + 1] = color.g;
            colors[idx + 2] = color.b;

            //alpha
            colors[idx + 3] = (GLbyte)clampf(alpha * 256.f + 0.5f, 0.f, 255.f);
        }
    }

    auto txt = new Texture2D();
    txt->initWithData(colors, size * size * 4, Texture2D::PixelFormat::RGBA8888, size, size, Size(size, size));

    delete[] colors;
    return txt;
}

And use just:

Sprite::createWithTexture(generateRadialGradientTexture(radius));

But result is really different:

enter image description here

1

There are 1 answers

4
Rabbid76 On BEST ANSWER

I investigated the shader code of the questions and developed a similar algorithm in C++. The algorithm linearly interpolates between two colors, starting at a center point, with a inner color, radially outwards to a outer color.
The C++ algorithm generates a texture with a size, specified by input parameters:

#include <vector>  // std::vector
#include <math.h>  // sqrt

Texture2D * TextureRadialGradientCreate(
    int            widht,
    int            height,
    const Color4B &startColor,
    const Color4B &endColor,
    float          radius,
    const Vec2    &center,
    float          expand )
{
    Vec4 sCol( startColor.r / 255.0, startColor.g / 255.0, startColor.b / 255.0, startColor.a / 255.0 );
    Vec4 eCol( endColor.r / 255.0, endColor.g / 255.0, endColor.b / 255.0, endColor.a / 255.0 );

    std::vector<unsigned char> plane( widht * height * 4, 0 );
    for ( int y = 0; y < height; ++ y )
    {
        for ( int x = 0; x < widht; ++ x )
        {
             float dx = x - center.x;
             float dy = y - center.y;
             float d = sqrt( dx*dx + dy*dy ) / radius;

             Vec4 mixCol( 0.0f, 0.0f, 0.0f, 0.0f );
             if ( expand < 1.0f && d < 1.0f )
             {
                 float a = ( d - expand ) / ( 1.0 - expand );
                 mixCol = (d <= expand) ? sCol : ( 1.0 - a ) * sCol + a*eCol;
             }

             size_t i = ( y * widht + x ) * 4;
             plane[i+0] = (unsigned char)(mixCol.x * 255.0f);
             plane[i+1] = (unsigned char)(mixCol.y * 255.0f);
             plane[i+2] = (unsigned char)(mixCol.z * 255.0f);
             plane[i+3] = (unsigned char)(mixCol.w * 255.0f);
        }
    }

    Texture2D *texture = new Texture2D();
    if ( texture != nullptr )
        texture->initWithData( plane.data(), plane.size() / 4, Texture2D::PixelFormat::RGBA8888, widht, height, cocos2d::Size(widht, height) );
    return texture;
}

e.g. a quadratic texture wiht a size of 600*600 and a gradient highlight in the center of the texture:

int w = 600;
int h = 600;
float rad = ( w < h ? w : h ) / 2.0f;
Vec2 cpt( w / 2.0f, h / 2.0f );
texture = TextureRadialGradientCreate( w, h, layer_startColor, layer_endColor, rad, cpt, layer_expand );


Extension to the answer

I don't know, maybe radial shader can be optimized?

The conditional branches can be avoided, by using the GLSL clamp function, by limiting the interpolation parameter of the mix function to the range (0, 1).
I suggest to optimize the fragment shader like this:

#ifdef GL_ES
varying lowp vec4 v_position;
#else
varying vec4 v_position; 
#endif

uniform vec4 u_startColor;
uniform vec4 u_endColor;
uniform vec2 u_center;
uniform float u_radius;
uniform float u_expand;

void main()
{
    float d = distance(v_position.xy, u_center) / u_radius;
    float a = (d - u_expand) / (1.0 - u_expand);
    gl_FragColor = mix( u_startColor, u_endColor, clamp(0.0, 1.0, a) ); 
}