Search code examples
opengllibgdxshaderbrushglblendfunc

libgdx - blendFunc for brush line drawing? (additive/non-additive mixture)


I am drawing sprites to the framebuffer at each pixel for every step in a line between two points, but I have an issue with the blending:

enter image description here

I am currently using:

(GL_ONE, GL_ONE_MINUS_SRC_ALPHA);

Then, in my shader I do:

if( color.a == 0 )
  color.rgb = 0;

However, if you look at the red and green star, you can see the "additive blending" that happens where the alpha is between interval of (0,1).


Without additive blending this 3D effect occurs:

enter image description here


Is there a way to have additive blending of like colors, but avoid when colors are not matching?

It should look like this:

enter image description here


Solution

  • You can do this using pre-multiplied alpha.

    First you need to set up your texture properly. Draw it white on black like this: enter image description here

    Then duplicate the texture's brightness into its alpha channel. In GIMP you can do this by right-clicking the layer, choosing Add Layer Mask, and then Grayscale Copy of Layer. It will look kind of like this (as shown in Gimp):

    enter image description here

    Then you can recolor it's RGB channel if you like, although it's more versatile to leave it white and color it in your code.

    In your code, when you draw the shape, use the blend function for pre-multiplied alpha:

    batch.setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA);
    

    And draw it using the default SpriteBatch shader. After you've drawn the shape, switch back to regular alpha blending:

    batch.setBlendFunction(GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
    

    Sample game:

    enter image description here

    public class GameMain extends ApplicationAdapter implements InputProcessor{
        SpriteBatch batch;
    
        Texture star;
        Color color = new Color();
        Viewport viewport;
        int screenWidth, screenHeight;
        FrameBuffer fbo;
        boolean shouldClearFBO = false;
        boolean fboUpdated = false;
        OrthographicCamera fullScreenCamera;
    
        boolean touchDown = false;
        Vector2 touchLocation = new Vector2();
    
        int colorIndex = 0;
        static final int[] COLORS = {0xff0000ff, 0x00ff00ff, 0x0000ffff, 0xffff00ff, 0xff00ffff, 0x00ffffff, 0xffffffff};
    
        static final float SIZE = 150;
    
        @Override
        public void create () {
            batch = new SpriteBatch();
            star = new Texture("star.png");
            viewport = new ExtendViewport(800, 480);
            fullScreenCamera = new OrthographicCamera();
            Gdx.input.setInputProcessor(this);
        }
    
        @Override
        public void resize (int width, int height){
            viewport.update(width, height, true);
            fullScreenCamera.setToOrtho(false, width, height);
            fullScreenCamera.position.set(width/2, height/2, 0);
            fullScreenCamera.update();
            fboUpdated = false;
            screenWidth = width;
            screenHeight = height;
        }
    
        @Override
        public void render () {
            if (!fboUpdated){
                fboUpdated = true;
                if (fbo!=null)
                    fbo.dispose();
                fbo = new FrameBuffer(Pixmap.Format.RGBA8888, screenWidth, screenHeight, false);
                fbo.begin();
                Gdx.gl.glClearColor(0, 0, 0, 1);
                Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
                fbo.end();
            }
    
            color.set(COLORS[colorIndex]);
    
            fbo.begin();
            if (shouldClearFBO){
                shouldClearFBO = false;
                Gdx.gl.glClearColor(0, 0, 0, 1);
                Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
            }
            if (touchDown){
                batch.setProjectionMatrix(viewport.getCamera().combined);
                batch.setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA);
                batch.begin();
                batch.setColor(color);
                batch.draw(star, touchLocation.x - SIZE/2, touchLocation.y - SIZE/2, SIZE, SIZE);
                batch.end();
            }
            fbo.end();
    
            Gdx.gl.glClearColor(0, 0, 0, 1);
            Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
            batch.setColor(Color.WHITE);
            batch.setProjectionMatrix(fullScreenCamera.combined);
            batch.setBlendFunction(GL20.GL_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA);
            batch.begin();
            batch.draw(fbo.getColorBufferTexture(), 0, screenHeight, screenWidth, -screenHeight);
            batch.end();
        }
    
        @Override
        public boolean keyDown(int keycode) {
            switch (keycode){
                case Input.Keys.SPACE:
                    colorIndex = (++colorIndex) % COLORS.length;
                    return true;
                case Input.Keys.X:
                    shouldClearFBO = true;
                    return true;
            }
    
            return false;
        }
    
        @Override
        public boolean keyUp(int keycode) {
            return false;
        }
    
        @Override
        public boolean keyTyped(char character) {
            return false;
        }
    
        @Override
        public boolean touchDown(int screenX, int screenY, int pointer, int button) {
            viewport.unproject(touchLocation.set(screenX, screenY));
            if (pointer==1){
                    colorIndex = (++colorIndex) % COLORS.length;
                    return true;
            }
            if (pointer == 2){
                shouldClearFBO = true;
                return true;
            }
            touchDown = true;
            return true;
        }
    
        @Override
        public boolean touchUp(int screenX, int screenY, int pointer, int button) {
            touchDown = false;
            return true;
        }
    
        @Override
        public boolean touchDragged(int screenX, int screenY, int pointer) {
            viewport.unproject(touchLocation.set(screenX, screenY));
            return true;
        }
    
        @Override
        public boolean mouseMoved(int screenX, int screenY) {
            return false;
        }
    
        @Override
        public boolean scrolled(int amount) {
            return false;
        }
    }