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.
This is what the "wrong" drop shadow looks like.
As soon as I make the semi-transparent surface completely transparent, the issue disappears, and the shadow is dark again.
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.
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)
How can I pre-mix the two surfaces together into one while preserving the black shadow?
NOTE: https://github.com/MyreMylar answered this for me on https://github.com/pygame-community/pygame-ce/issues/2808, I'm copying a slightly edited version of their response here with their permission.
You need to use premultiplied alpha blending.
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):
[...]
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: