OneLoneCoder/olcPixelGameEngine

Proposal additional pixel mode that preserves alpha after blending

Joseph21-6147 opened this issue · 3 comments

If you Draw a sprite that contains transparency onto a layer, the transparency is used in the blending, but gets lost afterwards. The effect is that the layer itself becomes opaque where that sprite was drawn.

I propose to add an additional pixel mode (on top of NORMAL, MASK, ALPHA and CUSTOM), that preserves the alpha component on the original sprite. In my testcode I called it APROP (for Alpha PROPagation), and added it to the declaration of enum Pixel::Mode:

//	enum Mode { NORMAL, MASK, ALPHA, CUSTOM };        // Altered Joseph21
	enum Mode { NORMAL, MASK, ALPHA, APROP, CUSTOM };

The only other alteration needed is an additional branche in the Draw() method. The code is already there in the ALPHA branche of that method, but the fourth parameter of the call to SetPixel() is commented out there. My proposal comprises an additional branche for the APROP pixel mode, where this fourth parameter is not commented out:

	if (nPixelMode == Pixel::APROP)
	{
		Pixel d = pDrawTarget->GetPixel(x, y);
		float a = (float)(p.a / 255.0f) * fBlendFactor;
		float c = 1.0f - a;
		float r = a * (float)p.r + c * (float)d.r;
		float g = a * (float)p.g + c * (float)d.g;
		float b = a * (float)p.b + c * (float)d.b;
		return pDrawTarget->SetPixel(x, y, Pixel((uint8_t)r, (uint8_t)g, (uint8_t)b, (uint8_t)(p.a * fBlendFactor)));
	}

[ NOTE - not sure if this is an issue or a pull request. Don't have much experience with github... ]

Quarg commented

This is the second issue here I've stumbled onto where there's a feature that I want that you've got the snippet / change I need for.

I think a pull request would indeed be the correct thing for this really.

Quarg commented

After some testing in my own project, I've found that the blending here is incorrect. The p.a * fBlendFactor notably so, as it makes drawing a transparent pixel over a solid pixel result in a transparent pixel.

It appears that this is in fact not the only bug with alpha blending; and this is my code after a bit of looking at the wikipedia article on alpha blending; and it seems to work well from my testing.

if (nPixelMode == Pixel::APROP)
{
    Pixel d = pDrawTarget->GetPixel(x, y);
    // alpha of painted pixel, and amount to blend in for RGB accounting for blend factor.
    float a = (float)(p.a / 255.0f) * fBlendFactor;
    // alpha of existing pixel.
    float a2 = (float)(d.a / 255.0f);
    // alpha of blended pixel.
    float a3 = a + a2 * (1.0f - a);
    // amount of existing pixel to blend in for RGB channels.
    float c = (1.0f - a) * a2;

    float r = a * (float)p.r + c * (float)d.r;
    float g = a * (float)p.g + c * (float)d.g;
    float b = a * (float)p.b + c * (float)d.b;

    return pDrawTarget->SetPixel(x, y, Pixel((uint8_t)r, (uint8_t)g, (uint8_t)b, (uint8_t)(a3 * 255)));
}

EDIT: Actually, this is still missing a term. The r,g,b values above should be divided by a3 so that painting completely transparent pixels over a semi-transparent pixel doesn't wash out the existing RGB values. Of course, with protection to prevent divide by zero.

I adapted the code for my game engine after the above comment (of feb. 12), and I think we are looking at the same wikipedia page:

case flc::Pixel::APROP: {
        // blend the source and the destination value according to alpha blending calculations
        // see: https://en.wikipedia.org/wiki/Alpha_compositing
        flc::Pixel srcPix = Pixel( encodedCol );
        flc::Pixel dstPix = Pixel( pixelPtr[ y * nDTwidth + x ] );
        // lerp new alpha value from src and dst alphas
        float fAlpha_src = float( srcPix.getA()) / 255.0f * m_BlendFactor;
        float fAlpha_dst = float( dstPix.getA()) / 255.0f;
        float fAlpha_new = fAlpha_src + fAlpha_dst * ( 1.0f - fAlpha_src );
        // lerp new rgb values using src and dst alpha, and divide by new alpha value
        int nR_new = int(( float( srcPix.getR() ) * fAlpha_src + float( dstPix.getR() ) * fAlpha_dst * (1.0f - fAlpha_src) ) / fAlpha_new);
        int nG_new = int(( float( srcPix.getG() ) * fAlpha_src + float( dstPix.getG() ) * fAlpha_dst * (1.0f - fAlpha_src) ) / fAlpha_new);
        int nB_new = int(( float( srcPix.getB() ) * fAlpha_src + float( dstPix.getB() ) * fAlpha_dst * (1.0f - fAlpha_src) ) / fAlpha_new);
        int nA_new = int( fAlpha_new * 255 );
        flc::Pixel newPixel = Pixel( (uint8_t)nR_new, (uint8_t)nG_new, (uint8_t)nB_new, (uint8_t)nA_new );
        // write the calculated pixel value to the draw target
        pixelPtr[ y * nDTwidth + x ] = newPixel.Encode();
    }
    break;
``` Afaik these are the same adaptations that you propose.