Texto de cor gradiente

8

O que eu realmente tento alcançar: eu gostaria de desenhar texto com uma cor vertical degradê. Encontrei essa solução, mas ela não se encaixa bem, pois possui um quadrado preto em torno da fonte gradiente no meu caso - não sei como me livrar dela, então comecei uma pergunta simples (a parte irrelevante) a entender melhor a física da mistura e do buffer de quadros no opengl e libgdx

O que eu estava tentando entender, irrelevante para o meu objetivo: tenho uma textura com um quadrado branco, desenho em cima de fundo vermelho. Estou tentando desenhar um quadrado verde em cima do branco, o quadrado verde cobre parcialmente o branco e parcialmente em cima do fundo vermelho (veja a figura abaixo).

Minha intenção é: a área branca que fica atrás do quadrado verde deve ser pintada em verde, mas todo o fundo vermelho não deve ser afetado e permanecer inalterado (vermelho como é).

Como posso fazer isso?

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

Idealmente, o quadrado verde não deve ser transparente, apenas deve bloquear o branco onde oculta a área branca.

A saída que estou recebendo: captura de tela

Atualização : Marque a resposta do @Xoppa como correta, pois resolve minha pergunta original com o seguinte resultado:

insira a descrição da imagem aqui

exenza
fonte
3
Você não pode usar as informações de cores para orientar a mistura dessa maneira. A única diferença nos pixels no buffer estará no canal G e B, que será 1 para o quadrado branco e 0 para o fundo, mas isso não é algo que você possa utilizar com a mistura direta. Uma maneira melhor seria desenhar o rectângulo verde para o estêncil de tampão, e depois processar o quadrado branco.
Bartek Banachewicz
Seria uma boa idéia atualizar seu exemplo / pergunta para que outros possam ver o que você realmente precisa
Luis Fernando Frontanilla
Sim, atualizado, obrigado
exenza
Você pode postar uma nova pergunta com mais detalhes para o seu caso de uso atualizado. (por exemplo: detalhes da fonte, cores gradientes / direção / etc) Como outros disseram, você provavelmente será um sombreador para isso, e eu posso ajudá-lo.
Nicolas
Você está certo @Nicolas, eu deveria ter postado uma nova pergunta com o caso de uso direto
exenza

Respostas:

6

Você poderia realmente usar algum tipo de máscara para misturá-lo usando um quadrado. Para isso, você pode primeiro renderizar o texto no buffer de estêncil usando um sombreador personalizado que descarta fragmentos com um valor alfa abaixo de um determinado limite. Depois disso, você pode renderizar o quadrado usando a função stencil para afetar apenas os fragmentos "tocados" pelo texto. Observe que isso envolve várias chamadas de renderização e, portanto, adiciona complexidade ao seu código de chamada.

No entanto, você diz que realmente deseja renderizar texto usando gradiente. Para isso, você não precisa de uma abordagem tão complexa e pode simplesmente aplicar o gradiente na mesma chamada de renderização.

Quando você desenha texto, na verdade você renderiza muitos pequenos quadrados, para cada caractere no texto, um quadrado. Cada um desses quadrados possui uma região de texturização aplicada que contém o caractere em um plano de fundo transparente. Se você abrir a imagem da fonte (por exemplo, esse é o padrão ), verá a imagem de origem.

Assim como você pode aplicar um gradiente a um quadrado normal, também pode aplicar um gradiente a cada um dos quadrados individuais que compõem o texto. Existem várias maneiras de fazer isso. Qual a melhor opção depende do caso de uso. Por exemplo, se você precisar de um gradiente horizontal ou possuir texto com várias linhas, precisará de algumas etapas adicionais. Como você não especificou isso, vou assumir que você deseja aplicar um gradiente vertical em uma única linha de texto:

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

Aliás, para obter melhores respostas, você deve incluir o problema real que está tentando resolver, em vez de se concentrar no que você acha que é a solução. Consulte também: https://stackoverflow.com/help/asking .

Xoppa
fonte
Obrigado por compartilhar. Poucas perguntas de acompanhamento: 1. primeiro, o loop supõe iterar a chamada "página de textura dos glifos"; Eu acho que seria o número de caracteres na minha string, mas ele realmente leva toda a textura png da fonte (é por isso que você chama o iterador "página"?), Porque para mim esse loop sempre executa apenas um
exenza
2. o segundo loop applyGradient. Suponho que é aí que você define o gradiente (você está certo que eu queria vertical), mas não tenho muita certeza de como isso funciona em relação ao número mágico index +=20(por exemplo, esse valor é específico da fonte ou por que é 20?) e constantes SpriteBatch.CX(são os cantos da textura?). É a coloração dos vértices padrão do OpenGL?
exenza
3. sobre otimização: está certo, se eu mover o material drawGradientpara o construtor de classe interna, para que ele não seja chamado em todas as etapas de renderização (o construtor seria chamado de algum lugar do create()método) e deixo drawGradientapenas cache.draw(batch)e return layoutlinhas?
exenza
1
1) É o número de arquivos PNG que sua fonte usa. Cada arquivo PNG requer uma chamada de desenho separada e os vértices correspondentes. Não depende do número de caracteres no próprio texto. 2) 4 cantos x 5 atributos por canto (x, y, u, v, cor) = 20. Isso é codificado no spritebatch (e é shader). 3) Otimização não é necessária. Pode funcionar se você não fizer mais nada com a fonte, mas nada está impedindo isso. Então, isso é um bug esperando para acontecer. Provavelmente existem maneiras melhores de fazer isso. Mas isso ainda seria uma otimização prematura desnecessária.
Xoppa
4

Você pode falsificar a mistura fazendo algumas contas, eis o que eu vim:

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

E os resultados são: insira a descrição da imagem aqui

E com:

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

insira a descrição da imagem aqui

Luis Fernando Frontanilla
fonte
Muito bom, obrigado Luis pelo feedback. Minha pergunta fornece um exemplo errado do que realmente estou tentando fazer. A área branca pode ter qualquer formato, digamos círculo ou até mais complexo, então, infelizmente, nem sempre é possível ajustar o quadrado
exenza
Quais são os critérios no seu caso de uso real para onde você não deseja que ele desenhe? Você literalmente deseja desenhar apenas onde os pixels já são brancos? Ou onde alguns sprites específicos já foram desenhados? Algo mais?
Tenfour04 18/02
Receio que as únicas maneiras pelas quais sei como fazer o que você quer seja com shaders ou mistura avançada, tente investigar sobre o mascaramento LibGDX, pois nunca usei nenhum deles antes
Luis Fernando Frontanilla
Eu adicionei a atualização à pergunta original, @ Tenfour04
exenza