Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WebGL banding on smooth alpha gradients #3316

Closed
WillCalderwood opened this issue Jul 29, 2015 · 11 comments
Closed

WebGL banding on smooth alpha gradients #3316

WillCalderwood opened this issue Jul 29, 2015 · 11 comments

Comments

@WillCalderwood
Copy link
Contributor

I've got a LibGDX game with cartoon clouds with a smooth gradient. There are other examples of gradients in the game that have a similar issue, but the clouds are the most obvious example. They look fine in Android, on iOS and on the Desktop version of the game, but on the WebGL version the gradients are not drawn as smooth. It only appears to be alpha gradients that have the problem. Other gradients look ok.

I've used a custom shader to pin down the cause of the banding, and the banding occurs in the colour channels, not in the alpha, but only occurs on textures with an alpha gradient. I'm wondering if it's some kind of precision issue with alpha premultiplication, as the alpha channel itself isn't effected. But if that's the case I don't understand why it's only occurring in WebGL and why the brightness cycles from light to dark across the bands rather than just being a flat colour in the bands. If it was a precision issue within the texture in VRAM then that also wouldn't explain why the problem disappears when using GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA as the blend function when still using the premultiplied png.

I've tried on 3 different devices in Chrome and IE, and all 3 produce the same results. You can find a test of the HTML5 version here.

https://wordbuzzhtml5.appspot.com/canvas/

I've added an example IntelliJ project on github here

https://github.com/WillCalderwood/CloudTest

The critical code is render() here

The bottom image here is the desktop version, the top is the WebGL version, both running on the same hardware.

enter image description here

There's nothing clever going on with the drawing. It's just a call to

    spriteBatch.draw(texture, getLeft(), getBottom(), getWidth(), getHeight());

I'm using the default shader, textures packed with premultiplied alpha with the blend function set as

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

This is the actual image, although alpha not premultiplied as that's done by my packer.

enter image description here

Does anyone know a possible reason for this and how I might resolve it?

This only appears to happen when using the blending mode GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA

I've tried changing the whole game to use non-premultiplied alpha textures. I use Texture Packer which can help fix the halo issues that often occur with non-premultiplied alpha. All this works fine in the Android and Desktop version. In the WebGL version, while I get smooth gradients, I get still get a small halo effect, so I can't use this as a solution either.

Here's an image. Desktop version on the top, web version on the bottom. Blending mode GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA on the left and GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA on the right

enter image description here

Here's a zoomed version of the bottom left image above with increased contrast to show the issue.

enter image description here

@Darkyenus
Copy link
Contributor

The question on SO has already been answered, does that not solve your issue? And if not, please report the issue in libgdx properly. From what I can see, this is the WebGL behavior and we most likely can't change that easily.

@WillCalderwood
Copy link
Contributor Author

@Darkyenus No, that doesn't solve the problem. The answer suggests it something to do with alpha blending behind the canvas, but if you look at the test program mentioned above I'm forcing alpha to 1 both before and after drawing and the problem still occurs. I also created a shader that always sets the alpha to 1, and the problem still occurs.

I too suspect this is a WebGL problem that you can't do anything about, but I posted it here as I can't for the life of me see what's causing it or why it's happening as I was hoping someone else had an idea.

The banding occurs in the colour channels, not in the alpha, but only occurs on textures with an alpha gradient. This is why I'm wondering if it's some kind of precision issue with alpha premultiplication, as the alpha channel itself isn't effected. But if that's the case I don't understand why it's only occurring in WebGL and why the brightness cycles from light to dark across the bands rather than just being a flat colour in the bands.

I'll update the first post to provide more detail.

@xoppa
Copy link
Member

xoppa commented Jul 30, 2015

Can you try if you can reproduce it using ShapeRenderer, so without textures? Or, if that doesn't work, by rendering using ShapeRenderer to a FBO and then use that texture.

@WillCalderwood
Copy link
Contributor Author

@xoppa What's your thinking here? What am I looking to achieve by doing this with a ShapeRenderer?

@xoppa
Copy link
Member

xoppa commented Jul 30, 2015

Well, you are reporting an issue with libgdx, so I guess you want to achieve that the issue gets fixed. To do that we need to figure out which part of the libgdx code is causing the issue. Judging from your description and sscce it might be either caused by the texture (loading the texture, sampling the texture, filtering/mipmaps, etc.) or rendering (blending, shader precision, vertex attributes, etc.). It looks like you're leaning towards thinking it's a rendering issue, but you say also that it only happens with a texture (with alpha channel). Using ShapeRenderer you can verify whether it actually only happens with textures and if so, using FBO you can easily verify whether the texture setup (e.g. bits per channel) matters. Either way, it will result in an even better sscce, because it doesn't rely on any assets anymore. So it will be easier for someone trying to fix it to copy-paste-run.

@WillCalderwood
Copy link
Contributor Author

@xoppa I'll have a play with the ShapeRenderer and see what I can come up with. I might not get a chance to do this until Monday now.

@WillCalderwood
Copy link
Contributor Author

@xoppa Managed to test the ShapeRenderer with an early start this morning. Interestingly, with the code below, I don't get the banding issue. I'm not sure what this points to with regards to where the problem lies. It doesn't appear to be in the texture, as the premultiplied texture has a smooth gradient when drawn with GL20.GL_SRC_ALPHA, GL20.GL_ONE_MINUS_SRC_ALPHA. It also doesn't appear to be a blending issue as it looks ok with the ShapeRenderer code. I'm not sure where else to look.

@Override
public void create() {
    batch = new SpriteBatch();
    shapeRenderer = new ShapeRenderer();
    fb = new FrameBuffer(Pixmap.Format.RGBA8888,
            Gdx.graphics.getWidth(),
            Gdx.graphics.getHeight(),
            false);

    fb.begin();
    drawGradient();
    fb.end();
}

@Override
public void render() {
    drawGradient();

    batch.begin();
    batch.setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA);
    batch.draw(fb.getColorBufferTexture(),
            Gdx.graphics.getWidth() * 0.5f + 10,
            Gdx.graphics.getHeight(),
            Gdx.graphics.getWidth(),
            -Gdx.graphics.getHeight());
    batch.end();
}

@xoppa
Copy link
Member

xoppa commented Aug 6, 2015

What is the difference between the case where it works correctly and the case where it doesn't? E.g. the pixel format (you're using RGBA8888, is that the same as your image?), texture filters, mipmaps, (pot) size, etc. Can you eliminate these differences and verify which of these changes fixes the issue you're reporting?

@WillCalderwood
Copy link
Contributor Author

There are no differences in format, size or mipmaps.

I've tried saving a pixmap of the FrameBuffer, loading that and rendering that alongside the FrameBuffer itself. That produces this result. The saved and loaded FrameBuffer is on the left, drawing straight from the FrameBuffer is on the right. As you can see, there's a big difference in the smoothness of the gradient.

image

The code used to produce this is below. Uncomment the line to save the FrameBuffer and run in Desktop mode to create the test.png file. I have both desktop and HTML canvas size set to 400x400. I've also updated the CloudTest project

public class MyGdxGame extends ApplicationAdapter {
    SpriteBatch batch;
    Texture testTexture;
    private ShapeRenderer shapeRenderer;
    private FrameBuffer fb;

    @Override
    public void create() {
        batch = new SpriteBatch();
        shapeRenderer = new ShapeRenderer();
        fb = new FrameBuffer(Pixmap.Format.RGBA8888,
                Gdx.graphics.getWidth(),
                Gdx.graphics.getHeight(),
                false);

        fb.begin();
        drawGradient();
//        PixmapIO.writePNG(Gdx.files.local("test.png"),
//                ScreenUtils.getFrameBufferPixmap(0, 0, fb.getWidth(), fb.getHeight()));
        fb.end();

        testTexture = new Texture(Gdx.files.internal("test.png"));
    }

    private void drawGradient() {
        shapeRenderer.begin(ShapeRenderer.ShapeType.Line);

        for (int i = 0; i < Gdx.graphics.getHeight(); i++) {
            float alpha = (float) i / Gdx.graphics.getHeight();
            shapeRenderer.setColor(1f * alpha, 1f * alpha, 1f * alpha, alpha);
            shapeRenderer.line(0, i, Gdx.graphics.getWidth(), i);
        }
        shapeRenderer.end();
    }

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

        batch.begin();
        batch.setBlendFunction(GL20.GL_ONE, GL20.GL_ONE_MINUS_SRC_ALPHA);
        batch.draw(testTexture,
                0,
                Gdx.graphics.getHeight(),
                Gdx.graphics.getWidth() * 0.475f,
                -Gdx.graphics.getHeight());

        batch.draw(fb.getColorBufferTexture(),
                Gdx.graphics.getWidth() * 0.525f,
                Gdx.graphics.getHeight(),
                Gdx.graphics.getWidth() * 0.475f,
                -Gdx.graphics.getHeight());

        batch.end();
    }
}

@WillCalderwood
Copy link
Contributor Author

I've given up on trying to resolve this, I'm unable to work out the cause. I eventually resorted to not using premultiplied alpha for any texture with an alpha gradient.

@xoppa
Copy link
Member

xoppa commented Oct 3, 2015

Going to close this as it looks like a very specific use-case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants