pygame-ce icon indicating copy to clipboard operation
pygame-ce copied to clipboard

Wrong alpha values applied when blitting to an SRCALPHA Surface

Open bigwhoopgames opened this issue 1 year ago • 1 comments
trafficstars

Environment:

Windows 10 pygame-ce 2.4.0 (SDL 2.28.5, Python 3.12.0)

Current behavior:

When blitting a Surface with a global alpha value to an SRCALPHA Surface the alpha value is applied twice.

Expected behavior:

Each pixel of the SRCALPHA surface should maintain the alpha value of the surface blitted to it and not apply the global alpha value a second time. In the below code the two images should be identical.

Test code

import os
import sys
import pygame

pygame.init()

width, height = 128, 64
screen = pygame.display.set_mode((width, height), flags = pygame.SCALED)
pygame.display.set_caption("Test")

clock = pygame.time.Clock()

cobweb = pygame.image.load(os.path.join('assets/sprites/entities', 'cobweb1.png'))
cobweb.convert()
cobweb.set_colorkey('white')
cobweb.set_alpha(224)

test_surface = pygame.Surface(cobweb.get_size(), flags = pygame.SRCALPHA)
test_surface.blit(cobweb, (0, 0))

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

    screen.fill('black')

    screen.blit(cobweb, (0, 0))
    screen.blit(test_surface, (50, 0))

    pygame.display.flip()

    clock.tick(60)

image

bigwhoopgames avatar Feb 04 '24 02:02 bigwhoopgames

I think the main issue you are encountering here is the classic 'straight alpha' composition problem.

For example if I switch to premultiplied blending, the problem goes away:

import os
import sys
import pygame

pygame.init()

width, height = 128, 64
screen = pygame.display.set_mode((width, height), flags=pygame.SCALED)
pygame.display.set_caption("Test")

clock = pygame.time.Clock()

cobweb = pygame.image.load(os.path.join("images", "cobweb.png"))
cobweb.convert()
cobweb.set_colorkey("white")
cobweb.set_alpha(225)
cobweb = cobweb.convert_alpha().premul_alpha()

test_surface = pygame.Surface(cobweb.get_size(), flags=pygame.SRCALPHA)
test_surface.blit(cobweb, (0, 0), special_flags=pygame.BLEND_PREMULTIPLIED)

font = pygame.font.Font(size=16)

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

    screen.fill("black")

    screen.blit(cobweb, (0, 0), special_flags=pygame.BLEND_PREMULTIPLIED)
    screen.blit(test_surface, (50, 0), special_flags=pygame.BLEND_PREMULTIPLIED)

    text = str(screen.get_at(pygame.mouse.get_pos()))

    text_surf = font.render(text, True, pygame.Color("white"))
    screen.blit(text_surf, (0, 48))

    pygame.display.flip()

    clock.tick(60)

It is just a bit of a confusing way of getting there as setting the global alpha switches a colorkey surface to use standard alpha blending rather than the special color key blending.

If this is for a specific workflow rather than a general problem you are having with 'straight alpha' blending, you could just fill the intermediate transparent surface with white in the RGB and the blend will be a lot closer:

import os
import sys
import pygame

pygame.init()

width, height = 128, 64
screen = pygame.display.set_mode((width, height), flags=pygame.SCALED)
pygame.display.set_caption("Test")

clock = pygame.time.Clock()

cobweb = pygame.image.load(os.path.join("images", "cobweb.png"))
cobweb.convert()
cobweb.set_colorkey("white")
cobweb.set_alpha(224)


test_surface = pygame.Surface(cobweb.get_size(), flags=pygame.SRCALPHA)
test_surface.fill((255, 255, 255, 0))
test_surface.blit(cobweb, (0, 0))

font = pygame.font.Font(size=16)

while True:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            pygame.quit()
            sys.exit()

    screen.fill("black")

    screen.blit(cobweb, (0, 0))
    screen.blit(test_surface, (50, 0))

    text = str(screen.get_at(pygame.mouse.get_pos()))

    text_surf = font.render(text, True, pygame.Color("white"))
    screen.blit(text_surf, (0, 48))

    pygame.display.flip()

    clock.tick(60)

You can read more about the composition problems with straight alpha blending and why premultipled is superior over here: What is Premultiplied alpha?

MyreMylar avatar Feb 04 '24 10:02 MyreMylar

I'm going to close this now, because this is just the results of the 'straight alpha' blending formula when blending two surfaces with alpha and there isn't really anything we can do about it.

You are basically getting (255-225) = 30 alpha worth of extra black over what you were intuitively expecting. This (blending onto a 'transparent' surface) is basically the standard case which makes people discover pre-multiplied alpha blending.

MyreMylar avatar May 30 '24 19:05 MyreMylar