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):

Obtained (Godot):

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?
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)



Sorry I should add a reproduction project because it really didn't work as expected for me.
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:
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()
Most helpful comment
I've tryed and seems to work perfectly fine with
Color.blend()