Point sprite alpha blending issue (Android / OpenGL ES 2.0)

1.4k views Asked by At

I've recently started looking into OpenGL ES for Android and am working on a drawing app. I've implemented some basics such as point sprites, path smoothing and FBO for double buffering. At the moment I am playing around with the glBlendFunc, more specifically when I put two textures close to each other with the same color/alpha values, the alpha gets added so it appears darker at the intersection of the sprites. This is a problem because the stroke opacity is not preserved if a lot of points are close together, as the color tends to more opaque rather than staying with the same opacity. Is there a way to make the textures have the same color on the intersection, i.e. have the same alpha value for the intersecting pixels, but keep the alpha values for the rest of the pixels?

Here's how I've done the relevant parts of the app:

  • for drawing the list of point sprites I use blending like this:

    GLES20.glEnable(GLES20.GL_BLEND);
    GLES20.glBlendFunc(GLES20.GL_ONE, GLES20.GL_ONE_MINUS_SRC_ALPHA);
    
  • the app uses an FBO with a texture, where it renders each brush stroke first and then this texture is rendered to the main screen. The blending func there is:

    GLES20.glEnable(GLES20.GL_BLEND);
    GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
    
  • OpenGL ES 2.0 doesn't support alpha masking;

  • there is no DEPTH_TEST function used anywhere in the app;
  • the textures for the point sprites are PNGs with transparent backgrounds;
  • the app supports texture masking which means one texture is used for the shape and one texture is used for the content;
  • my fragment shader looks like this:

    precision mediump float;
    
    uniform sampler2D uShapeTexture;
    uniform sampler2D uFillTexture;
    uniform float vFillScale;
    
    varying vec4 vColor;
    varying float vShapeRotation;
    varying float vFillRotation;
    varying vec4 vFillPosition;
    
    vec2 calculateRotation(float rotationValue) {
        float mid = 0.5;
        return vec2(cos(rotationValue) * (gl_PointCoord.x - mid) + sin(rotationValue) * (gl_PointCoord.y - mid) + mid,
                cos(rotationValue) * (gl_PointCoord.y - mid) - sin(rotationValue) * (gl_PointCoord.x - mid) + mid);
    }
    
    void main() {
        // Calculations.
        vec2 rotatedShape = calculateRotation(vShapeRotation);
        vec2 rotatedFill = calculateRotation(vFillRotation);
        vec2 scaleVector = vec2(vFillScale, vFillScale);
        vec2 positionVector = vec2(vFillPosition[0], vFillPosition[1]);
    
        // Obtain colors.
        vec4 colorShape = texture2D(uShapeTexture, rotatedShape);
        vec4 colorFill = texture2D(uFillTexture, (rotatedFill * scaleVector) + positionVector);
    
        gl_FragColor = colorShape * colorFill * vColor;
    }
    
  • my vertex shader is this:

    attribute vec4 aPosition;
    attribute vec4 aColor;
    attribute vec4 aJitter;
    attribute float aShapeRotation;
    attribute float aFillRotation;
    attribute vec4 aFillPosition;
    attribute float aPointSize;
    
    varying vec4 vColor;
    varying float vShapeRotation;
    varying float vFillRotation;
    varying vec4 vFillPosition;
    
    uniform mat4 uMVPMatrix;
    
    void main() {
        // Sey position and size.
        gl_Position = uMVPMatrix * (aPosition + aJitter);
        gl_PointSize = aPointSize;
    
        // Pass values to fragment shader.
        vColor = aColor;
        vShapeRotation = aShapeRotation;
        vFillRotation = aFillRotation;
        vFillPosition = aFillPosition;
    }
    

I've tried playing around with the glBlendFunc parameters but I can't find the right combination to draw what I want. I've attached some images showing what I would like to achieve and what I have at the moment. Any suggestions?

enter image description here

The Solution

Finally managed to get this working properly with a few lines thanks to @ Rabbid76. First of all I had to configure my depth test function before I draw to the FBO:

GLES20.glEnable(GLES20.GL_DEPTH_TEST);
GLES20.glDepthFunc(GLES20.GL_LESS);

// Drawing code for FBO.

GLES20.glDisable(GLES20.GL_DEPTH_TEST);

Then in my fragment shader I had to make sure that any pixels with alpha < 1 in the mask are discarded like this:

...
vec4 colorMask = texture2D(uMaskTexture, gl_PointCoord);
if (colorMask.a < 1.0)
    discard;
else
    gl_FragColor = calculatedColor;

And the result is (flickering is due to Android emulator and gif capture tool):

enter image description here

2

There are 2 answers

4
Rabbid76 On BEST ANSWER

If you set the glBlendFunc with the functions (GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) and you use glBlendEquation with the equation GL_FUNC_ADD then the destination color is calculated as follows:

C_dest = C_src * A_src + C_dest * (1-A_src)

If you blend for example C_dest = 1 with C_src = 0.5 and A_src = 0.5 then:

C_dest = 0.75 = 1 * 0.5 + 0.5 * 0.5

If you repeat blending the same color C_src = 0.5 and A_src = 0.5 then the destination color becomes darker:

C_dest = 0.625 = 0.75 * 0.5 + 0.5 * 0.5

Since the new target color is always a function of the original target color and the source color, the color can not remain equel when blending 2 times, because the target color has already changed after the 1st time blending (except GL_ZERO).

You have to avoid that any fragment is blended twice. If all fragments are drawn to the same depth (2D) then you can use the depth test for this:

glEnable( GL_DEPTH_TEST );
glDepthFunc( GL_LESS );

// do the drawing with the color

glDisable( GL_DEPTH_TEST );

Or the stencil test can be used. For example, the stencil test can be set to pass only when the stencil buffer is equal to 0. Every time a fragment is to be written the stencil buffer is incremented:

glClear( GL_STENCIL_BUFFER_BIT );
glEnable( GL_STENCIL_TEST );
glStencilOp( GL_KEEP, GL_KEEP, GL_INCR );
glStencilFunc( GL_EQUAL, 0, 255 );

// do the drawing with the color

glDisable( GL_STENCIL_TEST );

Extension to the answer

Note that you can discard fragments which should not be drawn. If the fragment in your sprite texture has an alpha channel of 0 you should discard it.

Note, if you discard a fragment neither the color nor the depth and stencil buffer will be written.

Fragment shaders also have access to the discard command. When executed, this command causes the fragment's output values to be discarded. Thus, the fragment does not proceed on to the next pipeline stages, and any fragment shader outputs are lost.

Fragment shader

if ( color.a < 1.0/255.0 )
    discard;
0
solidpixel On

It's not possible to do this using the fixed-function blending in OpenGL ES 2.0, because what you want isn't actually alpha blending. What you want is a logical operation (e.g. max(src, dst)) which is rather different to how OpenGL ES blending works.

If you want to do path / stroke / fill rendering with pixel-exact edges you might get somewhere with using stencil masks and stencil tests, but you can't do transparency in this case - just boolean operators.