Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Gradient color text

What I actually try to achieve: I'd like to draw text with a gradient vertical color. I found this solution, but it doesn't quite fit for me, as it has black square around the gradient font in my case - don't know how to get rid of it, so I started simple (the irrelevant part) question to understand better the physics of blending and frame buffer in opengl and libgdx

What I was trying to understand, irrelevant to my goal: I have a texture with a white square on it, I draw it on top of red background. I am trying to draw a green square on top of the white one, the green square partially covers the white one, and partially on top of the red background (see picture below).

My intention is: the white area, that is behind of the green square should be painted in green color, but all red background should not be affected and stayed unchanged (red as it is).

How can I do this?

package com.mygdx.game;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;

public class Game extends ApplicationAdapter {
    SpriteBatch batch;
    Texture img;
    private int height;
    private int width;
    private ShapeRenderer shapeRenderer;

    @Override
    public void create() {
        batch = new SpriteBatch();
        img = new Texture("white.png");
        width = Gdx.graphics.getWidth();
        height = Gdx.graphics.getHeight();
        shapeRenderer = new ShapeRenderer();
        shapeRenderer.setAutoShapeType(true);
    }

    @Override
    public void render() {
        Gdx.gl.glClearColor(1, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        batch.begin();
        batch.draw(img, width / 7, height / 4);
        batch.end();

        Gdx.gl.glEnable(GL20.GL_BLEND);
        Gdx.gl.glBlendFunc(GL20.GL_ONE, GL20.GL_SRC_COLOR);
        shapeRenderer.begin();
        shapeRenderer.set(ShapeRenderer.ShapeType.Filled);
        shapeRenderer.setColor(Color.GREEN);
        shapeRenderer.rect(width / 2 - 100, height / 4 - 50, 200, 200);
        shapeRenderer.end();
        Gdx.gl.glDisable(GL20.GL_BLEND);
    }

    @Override
    public void dispose() {
        batch.dispose();
        img.dispose();
    }
}

Ideally, the green square should not be transparent anyhow, just should block white where it hides the white area.

The output I'm getting: screenshot

Update: I mark @Xoppa 's answer as correct, as it solves my original question with the following result:

enter image description here

like image 624
exenza Avatar asked Feb 17 '20 10:02

exenza


2 Answers

You could indeed use some kind of mask to blend it using a square. For that you can first render the text to the stencil buffer using a custom shader that discards fragments with an alpha value below a certain threshold. After that you can render the square using the stencil function to only affect the fragments "touched" by the text. Note that this does involve multiple render calls though and therefore adds complexity to your calling code as well.

However, you say that you actually just want to render text using gradient. For that you don't need such complex approach and can simply apply the gradient within the same render call.

When you draw text, you actually render many little squares, for each character in the text one square. Each of this square has a textureregion applied that contains the character on a transparent background. If you open the font image (e.g. this is the default), then you'll see this source image.

Just like you can apply a gradient to a normal square, you can also apply a gradient to each of those individual squares that make up the text. There are multiple ways to do that. Which best suits depends on the use-case. For example if you need a horizontal gradient or have multiline text, then you need some additional steps. Since you didn't specify this, I'm going to assume that you want to apply a vertical gradient on a single line of text:

public class MyGdxGame extends ApplicationAdapter {
    public static class GradientFont extends BitmapFont {
        public static void applyGradient(float[] vertices, int vertexCount, float color1, float color2, float color3, float color4) {
            for (int index = 0; index < vertexCount; index += 20) {
                vertices[index + SpriteBatch.C1] = color1;
                vertices[index + SpriteBatch.C2] = color2;
                vertices[index + SpriteBatch.C3] = color3;
                vertices[index + SpriteBatch.C4] = color4;
            }
        }

        public GlyphLayout drawGradient(Batch batch, CharSequence str, float x, float y, Color topColor, Color bottomColor) {
            BitmapFontCache cache = getCache();
            float tc = topColor.toFloatBits();
            float bc = bottomColor.toFloatBits();
            cache.clear();
            GlyphLayout layout = cache.addText(str, x, y);
            for (int page = 0; page < cache.getFont().getRegions().size; page++) {
                applyGradient(cache.getVertices(page), cache.getVertexCount(page), bc, tc, tc, bc);
            }
            cache.draw(batch);
            return layout;
        }
    }

    SpriteBatch batch;
    GradientFont font;
    float topColor;
    float bottomColor;

    @Override
    public void create () {
        batch = new SpriteBatch();
        font = new GradientFont();
    }

    @Override
    public void render () {
        Gdx.gl.glClearColor(1, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        batch.begin();
        font.drawGradient(batch, "Hello world", 0, 100, Color.GREEN, Color.BLUE);
        batch.end();
    }

    @Override
    public void dispose () {
        batch.dispose();
        font.dispose();
    }
}

Btw, to get better answers you should include the actual problem you are trying to solve, instead of focusing on what you think is the solution. See also: https://stackoverflow.com/help/asking.

like image 174
Xoppa Avatar answered Oct 30 '22 19:10

Xoppa


You can fake blending by doing some math here's what I came up with:

import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;

public class CalculatedMask extends Game {

    private SpriteBatch batch;          // The SpriteBatch to draw the white image
    private ShapeRenderer renderer;     // The ShapeRenderer to draw the green rectangle
    private Texture img;                // The texture of the image
    private Rectangle imgBounds;        // The bounds of the image
    private Rectangle squareBounds;     // The bounds of the square
    private float width;                // The width of the screen
    private float height;               // The height of the screen
    private float squareX;              // The x position of the green square
    private float squareY;              // The y position of the green square
    private float squareWidth;          // The width of the green square
    private float squareHeight;         // The height of the green square

    @Override
    public void create() {
        width = Gdx.graphics.getWidth();
        height = Gdx.graphics.getHeight();
        batch = new SpriteBatch();
        renderer = new ShapeRenderer();
        renderer.setAutoShapeType(true);

        img = new Texture("pixel.png");                 // A 1x1 white pixel png
        imgBounds = new Rectangle();                    // The white image bounds
        imgBounds.setPosition(width / 7f, height / 4f); // Position the white image bounds
        imgBounds.setSize(400f, 300f);                  // Scale the white image bounds
        calculateRectangle();
    }

    private void calculateRectangle() {
        // Here we define the green rectangle's original position and size
        squareBounds = new Rectangle();
        squareX = width / 2f - 300f;
        squareY = height / 4f - 50f;
        squareWidth = 200f;
        squareHeight = 200f;
        // Adjust green square x position
        squareBounds.x = MathUtils.clamp(squareX, imgBounds.x, imgBounds.x + imgBounds.width);
        // Adjust green square y position
        squareBounds.y = MathUtils.clamp(squareY, imgBounds.y, imgBounds.y + imgBounds.height);
        // Adjust green square width
        if (squareX < imgBounds.x) {
            squareBounds.width = Math.max(squareWidth + squareX - imgBounds.x, 0f);
        } else if (squareX + squareWidth > imgBounds.x + imgBounds.width) {
            squareBounds.width = Math.max(imgBounds.width - squareX + imgBounds.x, 0f);
        } else {
            squareBounds.width = squareWidth;
        }
        // Adjust green square height
        if (squareY < imgBounds.y) {
            squareBounds.height = Math.max(squareHeight + squareY - imgBounds.y, 0f);
        } else if (squareY + squareHeight > imgBounds.y + imgBounds.height) {
            squareBounds.height = Math.max(imgBounds.height - squareY + imgBounds.y, 0f);
        } else {
            squareBounds.height = squareHeight;
        }
    }

    @Override
    public void render() {
        // Clear previous frame
        Gdx.gl.glClearColor(1, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        // Draw the white image
        batch.begin();
        batch.draw(img, imgBounds.x, imgBounds.y, imgBounds.width, imgBounds.height);
        batch.end();
        // Draw the green rectangle without affecting background
        renderer.begin();
        renderer.setColor(Color.GREEN);

        // Debug so we can see the real green rectangle
        renderer.set(ShapeRenderer.ShapeType.Line);
        renderer.rect(squareX, squareY, squareWidth, squareHeight);
        // Draw the modified green rectangle
        renderer.set(ShapeRenderer.ShapeType.Filled);
        renderer.rect(squareBounds.x, squareBounds.y, squareBounds.width, squareBounds.height);

        renderer.end();
    }
}

And the results are: enter image description here

And with:

squareX = width / 2f + 100f;
squareY = height / 4f + 150f;

enter image description here

like image 34
Luis Fernando Frontanilla Avatar answered Oct 30 '22 20:10

Luis Fernando Frontanilla