Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Image::blend_rect might not blend as expected #31124

Closed
Zylann opened this issue Aug 5, 2019 · 17 comments · Fixed by #39200
Closed

Image::blend_rect might not blend as expected #31124

Zylann opened this issue Aug 5, 2019 · 17 comments · Fixed by #39200

Comments

@Zylann
Copy link
Contributor

Zylann commented Aug 5, 2019

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?

@KoBeWi
Copy link
Member

KoBeWi commented Aug 5, 2019

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

@AlexHolly
Copy link
Contributor

AlexHolly commented Oct 2, 2019

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

@Zylann
Copy link
Contributor Author

Zylann commented Oct 2, 2019

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

@Zylann
Copy link
Contributor Author

Zylann commented Oct 2, 2019

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.

@AlexHolly
Copy link
Contributor

AlexHolly commented Oct 4, 2019

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

dc.r = (double)(sc.a * sc.r + dc.a * (1.0 - sc.a) * dc.r);

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

@Zylann
Copy link
Contributor Author

Zylann commented Oct 4, 2019

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?

@AlexHolly
Copy link
Contributor

Yes it looks like it is not working properly.

@azagaya
Copy link
Contributor

azagaya commented Nov 30, 2019

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

@lawnjelly
Copy link
Member

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.

@azagaya
Copy link
Contributor

azagaya commented Apr 1, 2020

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?

@Calinou
Copy link
Member

Calinou commented Apr 1, 2020

@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.

@azagaya
Copy link
Contributor

azagaya commented Apr 1, 2020

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

@Calinou
Copy link
Member

Calinou commented Apr 1, 2020

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

@azagaya
Copy link
Contributor

azagaya commented Apr 1, 2020

@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

@Kobrar
Copy link

Kobrar commented Apr 14, 2020

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.

@azagaya
Copy link
Contributor

azagaya commented Apr 14, 2020

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.

@azagaya
Copy link
Contributor

azagaya commented Apr 14, 2020

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

@akien-mga akien-mga added this to the 4.0 milestone Jun 2, 2020
@akien-mga akien-mga added the bug label Jun 2, 2020
OverloadedOrama added a commit to Orama-Interactive/Pixelorama that referenced this issue Jun 13, 2020
godotengine/godot#31124 has now been fixed in Godot 3.2.2-rc1, so we can use Image.blend_rect() instead of a custom method. This makes exporting large images and drawing with large brush sizes a lot faster.

Once Godot 3.2.2 stable is released, the custom blend_rect method will be completely removed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
9 participants