pygame-community / pygame-ce

🐍🎮 pygame - Community Edition is a FOSS Python library for multimedia applications (like games). Built on top of the excellent SDL library.
https://pyga.me
773 stars 120 forks source link

Gaussian-blurred alpha doesn't appear to blit correctly onto another semi-transparent surface #2808

Closed iamjackg closed 1 month ago

iamjackg commented 2 months ago

Environment:

You can get some of this info from the text that pops up in the console when you run a pygame program.

Current behavior:

I feel like there is some fundamental aspect of alpha blending that I'm not understanding here.

I'm doing the following in Pygame to create a blurred drop shadow:

So far, so good. Now I blit this on top of a surface filled with a semi-transparent color, and then I blit that on top of the background. When I run this, however, the alpha of the shadow seems to pull the semi-transparent surface with it and makes the shadow brighter instead of black.

In my real project, I can't blit the two surfaces independently on the background: I need to return a single merged surface.

Screenshots

This is what the "wrong" drop shadow looks like.

image

As soon as I make the semi-transparent surface completely transparent, the issue disappears, and the shadow is dark again.

image

I have tried playing around with all the special_flags to change the blending mode when blitting, but no combination seems to help, or perhaps I just don't understand what I'm supposed to use.

Test code

Here is code to fully reproduce the above:

import pygame as pg

pg.init()
screen = pg.display.set_mode((400, 400))
clock = pg.time.Clock()

black_circle_surface = pg.Surface((100, 100), pg.SRCALPHA)
pg.draw.circle(black_circle_surface, (0, 0, 0), (50, 50), 25)

white_circle_surface = pg.Surface((100, 100), pg.SRCALPHA)
pg.draw.circle(white_circle_surface, (255, 255, 255), (50, 50), 25)

circle_and_shadow_surface = pg.Surface((100, 100), pg.SRCALPHA)
circle_and_shadow_surface.blit(
    pg.transform.gaussian_blur(black_circle_surface, radius=10), (0, 0)
)
circle_and_shadow_surface.blit(white_circle_surface, (0, 0))

semi_transparent_surface = pg.Surface((200, 200), pg.SRCALPHA)
semi_transparent_surface.fill(
    (255, 255, 255, 1)  # change the 1 to a 0 here to make the issue disappear
)
semi_transparent_surface.blit(
    circle_and_shadow_surface,
    (50, 50),
)

running = True
while running:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            running = False

    screen.fill((20, 70, 80))

    screen.blit(semi_transparent_surface, (100, 100))

    pg.display.update()
    clock.tick(60)
MyreMylar commented 2 months ago

Sounds like you need to use premultiplied alpha blending.

On Mon, 15 Apr 2024, 02:26 Jack Gaino, @.***> wrote:

Environment:

You can get some of this info from the text that pops up in the console when you run a pygame program.

  • Operating system (e.g. Windows, Linux(Debian), Linux(Ubuntu), Mac): Linux(Ubuntu)
  • Python version (e.g. 3.11.1, 3.8.5) : 3.10
  • SDL version (e.g. SDL 2.0.12): 2.0.11
  • pygame-ce version (e.g. 2.4.0.dev4, 2.1.3): 2.4.1
  • Relevant hardware (e.g. if reporting a bug about a controller, tell us the brand & name of it):

Current behavior:

I feel like there is some fundamental aspect of alpha blending that I'm not understanding here.

I'm doing the following in Pygame to create a blurred drop shadow:

  • draw a white object
  • draw a black version of it on another surface
  • apply gaussian blur to the black version
  • blit the dark version and then the white version to a new surface

So far, so good. Now I blit this on top of a surface filled with a semi-transparent color, and then I blit that on top of the background. When I run this, however, the alpha of the shadow seems to pull the semi-transparent surface with it and makes the shadow brighter instead of black.

In my real project, I can't blit the two surfaces independently on the background: I need to return a single merged surface.

Screenshots

This is what the "wrong" drop shadow looks like.

image.png (view on web) https://github.com/pygame-community/pygame-ce/assets/19474350/1ba99e65-4387-4c17-a372-556fff6c42f7

As soon as I make the semi-transparent surface completely transparent, the issue disappears, and the shadow is dark again.

image.png (view on web) https://github.com/pygame-community/pygame-ce/assets/19474350/bda34bf7-e8d2-4ecc-9fa1-90cd642934e4

I have tried playing around with all the special_flags to change the blending mode when blitting, but no combination seems to help, or perhaps I just don't understand what I'm supposed to use.

Test code

Here is code to fully reproduce the above:

import pygame as pg

pg.init()screen = pg.display.set_mode((400, 400))clock = pg.time.Clock() black_circle_surface = pg.Surface((100, 100), pg.SRCALPHA)pg.draw.circle(black_circle_surface, (0, 0, 0), (50, 50), 25) white_circle_surface = pg.Surface((100, 100), pg.SRCALPHA)pg.draw.circle(white_circle_surface, (255, 255, 255), (50, 50), 25) circle_and_shadow_surface = pg.Surface((100, 100), pg.SRCALPHA)circle_and_shadow_surface.blit( pg.transform.gaussian_blur(black_circle_surface, radius=10), (0, 0) )circle_and_shadow_surface.blit(white_circle_surface, (0, 0)) semi_transparent_surface = pg.Surface((200, 200), pg.SRCALPHA)semi_transparent_surface.fill( (255, 255, 255, 1) # change the 1 to a 0 here to make the issue disappear )semi_transparent_surface.blit( circle_and_shadow_surface, (50, 50), ) running = Truewhile running: for event in pg.event.get(): if event.type == pg.QUIT: running = False

screen.fill((20, 70, 80))

screen.blit(semi_transparent_surface, (100, 100))

pg.display.update()
clock.tick(60)

— Reply to this email directly, view it on GitHub https://github.com/pygame-community/pygame-ce/issues/2808, or unsubscribe https://github.com/notifications/unsubscribe-auth/ADGDGGQUABOIWAXAXXIYOGTY5MUEPAVCNFSM6AAAAABGGNRNT2VHI2DSMVQWIX3LMV43ASLTON2WKOZSGI2DENBZG42TANA . You are receiving this because you are subscribed to this thread.Message ID: @.***>

iamjackg commented 2 months ago

I did try that, but couldn't find the specific magic combination to get it to work right. I got very similar (broken) results. Would you be so kind to adjust the demo code I posted to correctly use premultiplied blending?

MyreMylar commented 2 months ago

I did try that, but couldn't find the specific magic combination to get it to work right. I got very similar (broken) results. Would you be so kind to adjust the demo code I posted to correctly use premultiplied blending?

Sure, I added a bunch of comments to try and help you understand a bit more where I was bothering to pre-multiply or not. In the average application it is safe just to pre-multiply everything and always blit with premultiplication, but as it happens here you have some alpha'd black and solid white pixels which are not affected by an alpha pre-multiplication operation (multiply a 0 colour by anything and it is still 0, multiply 255 by 1 and it is still 255):

Anyway here is the working program:

import pygame
import pygame as pg

pg.init()
screen = pg.display.set_mode((400, 400))
clock = pg.time.Clock()

black_circle_surface = pg.Surface((100, 100), pg.SRCALPHA)
pg.draw.circle(black_circle_surface, (0, 0, 0), (50, 50), 25)
black_circle_surface = pg.transform.gaussian_blur(
    black_circle_surface, radius=10
)  # no need to pre-multiply as colour is zeros will not change whatever we multiply it by

white_circle_surface = pg.Surface((100, 100), pg.SRCALPHA)
white_circle_surface.fill(
    (0, 0, 0, 0)
)  # no need to pre-multiply, alpha is zero and colour is zero
pg.draw.circle(
    white_circle_surface, (255, 255, 255), (50, 50), 25
)  # no need to pre-multiply alpha and colour are the same (either 0 or 255)

circle_and_shadow_surface = pg.Surface((100, 100), pg.SRCALPHA)
circle_and_shadow_surface.fill((0, 0, 0, 0))  # no need to pre-multiply alpha is zero
circle_and_shadow_surface.blit(black_circle_surface, (0, 0))
circle_and_shadow_surface.blit(white_circle_surface, (0, 0))

semi_transparent_surface = pg.Surface((200, 200), pg.SRCALPHA)
semi_transparent_surface.fill(
    (255, 255, 255, 1)  # change the 1 to a 0 here to make the issue disappear
)
semi_transparent_surface = (
    semi_transparent_surface.convert_alpha().premul_alpha()
)  # need to pre-multiply RGB values will all be changed to 1
semi_transparent_surface.blit(
    circle_and_shadow_surface, (50, 50), special_flags=pygame.BLEND_PREMULTIPLIED
)

running = True
while running:
    for event in pg.event.get():
        if event.type == pg.QUIT:
            running = False

    screen.fill((20, 70, 80))

    screen.blit(
        semi_transparent_surface, (100, 100), special_flags=pygame.BLEND_PREMULTIPLIED
    )

    pg.display.update()
    clock.tick(60)

This is the normal behaviour of the standard 'straight' alpha (the pygame default) and the superior 'premultiplied' alpha blending.

The main advantage of 'straight' alpha is that it is easier to understand and easy to dynamically alter the alpha value - otherwise it sucks.

Here is what the premultiplied alpha version looks like if we dial the semi-transparent surface up to 50 alpha:

image

MyreMylar commented 2 months ago

I also wrote a tutorial on premultiplied alpha blending here:

https://pyga.me/docs/tutorials/en/premultiplied-alpha.html

iamjackg commented 2 months ago

Thank you so much: you've been incredibly helpful. I originally asked this on Stack Overflow, and the only comment I got was "this seems to be a bug with pygame-ce -- you should report it there."

If you have a stack overflow account, feel free to copy your answer here and I'll accept it: https://stackoverflow.com/questions/78322770/why-are-my-blurred-drop-shadows-lightening-instead-of-darkening-a-semi-transpare

If you don't, I'd love to copy it there with your permission, giving full credit to you.

MyreMylar commented 2 months ago

Copy away - I don't have a stack overflow account.

On Tue, 16 Apr 2024, 00:44 Jack Gaino, @.***> wrote:

Thank you so much: you've been incredibly helpful. I originally asked this on Stack Overflow, and the only comment I got was "this seems to be a bug with pygame-ce -- you should report it there."

If you have a stack overflow account, feel free to copy your answer here and I'll accept it: https://stackoverflow.com/questions/78322770/why-are-my-blurred-drop-shadows-lightening-instead-of-darkening-a-semi-transpare

If you don't, I'd love to copy it there with your permission and giving full credit to you.

— Reply to this email directly, view it on GitHub https://github.com/pygame-community/pygame-ce/issues/2808#issuecomment-2057992532, or unsubscribe https://github.com/notifications/unsubscribe-auth/ADGDGGT7I266Z5XOASUV7VDY5RQ4FAVCNFSM6AAAAABGGNRNT2VHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDANJXHE4TENJTGI . You are receiving this because you commented.Message ID: @.***>

Starbuck5 commented 2 months ago

@iamjackg could you copy over the answer to stack overflow? I'd like to fully resolve this issue so it can be closed in a good state.

iamjackg commented 2 months ago

Done! Sorry about the delay, got sidetracked by other things.