Godot: Image::blend_rect might not blend as expected

Created on 6 Aug 2019  路  17Comments  路  Source: godotengine/godot

Godot 3.1.1

tl;dr: Image::blend_rect() may cause transparent regions to darken, contrary to what you would get with a drawing app.

Explanation:

When trying to work on a painting app in which you can paint on transparent images using shaders and a transparent viewport, I got to deal with alpha blending. I tried blending a diagonal white-to-transparent-white gradient with the Godot icon, and quickly stumbled on the following problem:

Expected (Paint.NET):
image

Obtained (Godot):
image

Notice the darkened corner. From a pure math standpoint, it's just the result of averaging colors from the "invisible" pixels of the destination image, ending up darkening the gradient. But it may not be the expected result, at least to me it wasn't.
So I went bulldozer mode and blended myself in shaders, coming up with this:

vec4 blend_alpha(vec4 a, vec4 b) {
    vec4 res;
    res.rgb = b.a * b.rgb + a.a * a.rgb * (1.0 - b.a);
    res.a = b.a + a.a * (1.0 - b.a);
    res.rgb /= res.a; // THIS
    return res;
}

That last division did the trick.

But then, looking at Image::blend_rect, I noticed it does the same thing, except the division. So I tested it, and it has the same issue.

So is this really expected or should it be patched?

bug discussion core rendering

Most helpful comment

I've tryed and seems to work perfectly fine with Color.blend()

All 17 comments

Maybe an additional parameter could determine what to do with alpha.

Image::blend_rect Works for me (opening this image on github will show the transparent areas as black color)
t2t
tt3
tt23

Sorry I should add a reproduction project because it really didn't work as expected for me.

ImageAlphaBlend.zip

Run main.tscn, then see the result in the file explorer: you will notice it will have dark corners, while the expected result doesn't.

pos(62,2) shows # 132e3f A7 on expected.png (tested with aseprite)
pos(62,2) shows # 000101 A7 on icon.png (tested with aseprite)

looks like the expected.png also has an error

Which leads to this solution
https://en.wikipedia.org/wiki/Alpha_compositing#Alpha_blending
https://github.com/godotengine/godot/blob/7e9c1041ac1d30c7620713635a76ba4caf29d673/core/image.cpp#L2207

double out_a = (double)(sc.a + dc.a * (1.0 - sc.a));
dc.r = (double)(sc.a * sc.r + dc.r * dc.a * (1.0 - sc.a))/out_a;
dc.g = (double)(sc.a * sc.g + dc.g * dc.a * (1.0 - sc.a))/out_a;
dc.b = (double)(sc.a * sc.b + dc.b * dc.a * (1.0 - sc.a))/out_a;
dc.a = (double)(sc.a + dc.a * (1.0 - sc.a));

pos(62,2) shows # 132e3f A7 on expected.png (tested with aseprite)
pos(62,2) shows # 000101 A7 on icon.png (tested with aseprite)

I think that difference is meaningless since that pixel is located in a transparent area. Godot saves PNGs without compressing the transparent areas, while my painting software does. It's not related to the issue, which is about the top-left corner.

Are you suggesting we change the blending formula?

Yes it looks like it is not working properly.

I've tried the solution above, testing with pixelorama to see results, and indeed it works better.

Photoshop style blending is a bit different to game style blending, as you need to deal with situations like blending onto a transparent background, whereas usual game blending is designed to be fast. There are also other tradeoffs with speed and accuracy. So if you want to support this you might be best off with a separate routine / code path for the photoshop style blending.

Of course if you wanted to make photoshop like app you would probably need other blending modes too. And consider the colour spaces, doing your blending in linear for instance.

In practice for anything except simple apps I think you'd need to write some custom code in c++ for such an app.

So, this could be addressed adding a 4th param, something like a bool fix_alpha or something, default to false. And if that parameter is ture, use the formula from above... Would that change have a chance to get merged in master?

@azagaya Sounds good to me. Personally, I'd name the parameter accurate_alpha or something like that to imply it results in more "correct" visuals at the cost of performance. The documentation should also mention the difference between non-accurate blending and accurate blending.

@Calinou And should the same be implemented in blend_rect_mask()?

@azagaya Probably, it makes sense to implement it there as well if it can be done easily enough.

@Calinou Also, now that i'm there, if the source pixel is fully transparent, we could avoid blending the colors, as it should result in the destination color anyways... similar to how mask works in blend_rect_mask() we could avoid calculations if sc.a == 0

Myself I would like to advocate replacing the old behaviour completely. My reasoning being:

  • for opaque destination it works the same
  • for transparent destination blending white transparent source repeatedly will produce black image instead of slowly fading to white and who would ever want that? That is borked premultipled alpha behaviour where conversion is applied every time! It does nothing right, imho very clearly bugged.
  • someone said the additional division is cost. Well, the original algorithm already does quite a bit of work, like constructing new objects for every pixel in the image for whatever reason, so that is not a meaningful factor
  • for proper blending mode choice you would probably want to implement multiple blend modes in Color class, which btw has blend() function which properly blends alpha and should probably be used in Image blend functions to cut down unnecessary code

Overall it seems to be a very simple bug and until a proper rich blending library is maybe made someday, replacing the current behaviour with Color.blend() is the most sane and correct solution which adds less lines than it removes.

That makes sense to me. If some core contributor is ok with that, i can replace the PR to do exactly what you say.
I didn't know about the Color.blend() function.

I've tryed and seems to work perfectly fine with Color.blend()

Was this page helpful?
0 / 5 - 0 ratings

Related issues

nunodonato picture nunodonato  路  3Comments

gonzo191 picture gonzo191  路  3Comments

EdwardAngeles picture EdwardAngeles  路  3Comments

n-pigeon picture n-pigeon  路  3Comments

Zylann picture Zylann  路  3Comments