renpy / renpy

The Ren'Py Visual Novel Engine
http://www.renpy.org/
4.99k stars 702 forks source link

Blend Functions "min" alpha bug #5699

Open raspberry-soft opened 2 months ago

raspberry-soft commented 2 months ago

Using the "min" blend function in Ren'Py 8.2.3 causes the alpha component to be black:

    show eileen happy:
        blend "min"

2024-08-11 05_38_07-Window

The other default functions work as intended.

This also happen if you use it in a layeredimage:

        attribute worried:
            "eyebrows worried" blend "min"

The custom "darken" blend mode has the same problem. Both "darken" and "min" has in common the GL_MIN function so maybe that's related to the problem. I tested with other custom blend modes like "screen" or "lighten" and works fine.

init python:
    from renpy.uguu import GL_FUNC_ADD, GL_ONE, GL_ONE_MINUS_SRC_ALPHA, GL_MIN, GL_ONE_MINUS_DST_COLOR, GL_MAX

    ## (rgb_equation, src_rgb, dst_rgb, alpha_equation, src_alpha, dst_alpha)
    config.gl_blend_func["screen"] = (GL_FUNC_ADD, GL_ONE_MINUS_DST_COLOR, GL_ONE, GL_FUNC_ADD, GL_ONE, GL_ONE)
    config.gl_blend_func["lighten"] = (GL_MAX, GL_ONE, GL_ONE, GL_FUNC_ADD, GL_ONE, GL_ONE_MINUS_SRC_ALPHA)
    config.gl_blend_func["darken"] = (GL_MIN, GL_ONE, GL_ONE, GL_FUNC_ADD, GL_ONE, GL_ONE_MINUS_SRC_ALPHA)

Use case: I wanted to use "darken" in the eyebrow layer of my sprite so it blends better with the hair.

Gouvernathor commented 2 months ago

That's most likely related to the alpha-premultiplication, which turns the color values to 0 when alpha is 0.

renpytom commented 2 months ago

I'm pretty sure it is alpha premultiplication, so I've fixed this in f0a86c2a9 by writing a bit of documentation. It is probably a bad idea to use blend with a displayable that isn't opaque, as it's hard to wrap your head around the result.

Alpha premultiplication is necessary to get things like window scaling right, so it's not going away - it's something we have to live with.

raspberry-soft commented 2 months ago

Is there any solutions or work -around to get these working? For example using the original alpha information to make it transparent again? As I see it, "min" and "darken" works fine except the alpha is set to black.

Transparent displayables blending could be useful when building sprites.

renpytom commented 2 months ago

Yes. What you want to do is to make sure the mask has 100% alpha. That will then cause the colors to bring properly with the underlying image. Pixels that are transparent can be white.

Gouvernathor commented 2 months ago

Maybe if it's just about fixing the pixels that are 100% transparent, a solution could be to apply a transform that adds a #fff or #000 solid underneath (depending on the targeted math result), using some sort of Fixed, that way the whole thing renders opaque and neuters whatever alpha premultiplication. And then the blend applies.

raspberry-soft commented 2 months ago

Maybe if it's just about fixing the pixels that are 100% transparent, a solution could be to apply a transform that adds a #fff or #000 solid underneath (depending on the targeted math result), using some sort of Fixed, that way the whole thing renders opaque and neuters whatever alpha premultiplication. And then the blend applies.

If I get you right, that would be the equivalent of using a white background on the layer. I did that and it resolved the black pixels problem, however, since the semi-transparent colored pixels on the border are now mixed with white the result is a different color and the blend does not look as intended and is not useful.

raspberry-soft commented 2 months ago

Yes. What you want to do is to make sure the mask has 100% alpha. That will then cause the colors to bring properly with the underlying image. Pixels that are transparent can be white.

I tried using AlphaMask in many different ways and couldn't make it work. I guess I don't understand exactly what to do or how to do it.

renpytom commented 2 months ago

Maybe if it's just about fixing the pixels that are 100% transparent, a solution could be to apply a transform that adds a #fff or #000 solid underneath (depending on the targeted math result), using some sort of Fixed, that way the whole thing renders opaque and neuters whatever alpha premultiplication. And then the blend applies.

If I get you right, that would be the equivalent of using a white background on the layer. I did that and it resolved the black pixels problem, however, since the semi-transparent colored pixels on the border are now mixed with white the result is a different color and the blend does not look as intended and is not useful.

Are you using min or multiply here? In both cases, the results should be correct. White is always greater than any other color, so it will always take the other color except when the mask has a different color. Similarly, for multiply. Multiplying with right will just produce the original color.

Gouvernathor commented 2 months ago

For the areas that were fully transparent, yes. But for the semi-transparent parts, the boundaries, mixing with white changes the actual color so the result is not the same.

Maybe the solution then would be a shader that turns only the pixels that are fully transparent, solid white ? and pass that operation before the blend applies somehow ?

raspberry-soft commented 2 months ago

Yes, if you have a white background mixed with semitransparent color the resulting color is different (I'm using "min and "darken" which are very similar). You can reproduce that in photoshop too. In my particular case, using a white background on the eyebrow layer and blending with "darken" will make the borders of the eyebrow to have weird colors and don't look good or "clean" like in photoshop.

I'm not experienced enough to work around this or use shaders. If there's nothing I can do, I will render the eyebrow in photoshop together with the head, although that would not be ideal because it goes against the point of saving resources and bloat with layered images.

My doubt is why pre-multiplication only affects transparents pixels? And why only GL_MIN ("min and "darken")? That's why I thought it was a bug. As a note, while debugging, using GL_FUNC_SUBTRACT instead of GL_MIN gives the same problem. All the other blends work fine.