Zeniel Danaku
Zeniel Danaku

Reputation: 92

Blitting a Surface onto another without combining alpha

In a pygame project I'm working on, sprites of characters and objects cast a shadow onto the terrain. Both the shadow and the terrain are normal pygame surfaces so, to show them, the shadow is blitted onto the terrain. When there's no other shadow (only one shadow and the terrain) everything works fine, but when the character walks into the area of a shadow, while casting its own shadow, both shadows combine their alpha values, obscuring the terrain even more. What I want is to avoid this behaviour, keeping the alpha value stable. Is there any way to do it?

EDIT: This is an image, that I made in Photoshop, to show the issue enter image description here

EDIT2: @sloth's answer is ok, but I neglected to comment that my project is more complicated than that. The shadows are not whole squares, but more akin to “stencils”. Like real shadows, they are silhouettes of the objects they are cast from, and therefore they need per pixel alphas which are not compatible with colorkey and whole alpha values.

Here is a YouTube video that shows the issue a bit more clearly.

Upvotes: 2

Views: 708

Answers (3)

skrx
skrx

Reputation: 20438

You can combine per-pixel alpha shadows by blitting them onto a helper surface and then fill this surface with a transparent white and pass the pygame.BLEND_RGBA_MIN flag as the special_flags argument. The alpha value of the fill color should be equal or lower than the alphas of the shadows. Passing the pygame.BLEND_RGBA_MIN flag means that for each pixel the lower value of each color channel will be taken, so it will reduce the increased alpha of the overlapping shadows to the fill color alpha.

import pygame as pg


pg.init()
screen = pg.display.set_mode((800, 600))
clock = pg.time.Clock()
shadow = pg.image.load('shadow.png').convert_alpha()
# Shadows will be blitted onto this surface.
shadow_surf = pg.Surface((800, 600), pg.SRCALPHA)

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

    screen.fill((130, 130, 130))
    screen.blit(shadow, (100, 100))
    screen.blit(shadow, (154, 154))

    shadow_surf.fill((0, 0, 0, 0))  # Clear the shadow_surf each frame.
    shadow_surf.blit(shadow, (100, 100))
    shadow_surf.blit(shadow, (154, 154))
    # Now adjust the alpha values of each pixel by filling the `shadow_surf` with a
    # transparent white and passing the pygame.BLEND_RGBA_MIN flag. This will take
    # the lower value of each channel, therefore the alpha should be lower than
    # the shadow alphas.
    shadow_surf.fill((255, 255, 255, 120), special_flags=pg.BLEND_RGBA_MIN)
    # Finally, blit the shadow_surf onto the screen.
    screen.blit(shadow_surf, (300, 0))

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

enter image description here

Here's the shadow.png.

enter image description here

Upvotes: 0

sloth
sloth

Reputation: 101042

An easy way to solve this is to blit your shadows on another Surface first which has an alpha value, but no per pixel alpha. Then blit that Surface to your screen instead.

Here's a simple example showing the result:

from pygame import *
import pygame

pygame.init()
screen = pygame.display.set_mode((800, 600))

# we create two "shadow" surfaces, a.k.a. black with alpha channel set to something
# we use these to illustrate the problem
shadow = pygame.Surface((128, 128), pygame.SRCALPHA)
shadow.fill((0, 0, 0, 100))
shadow2 = shadow.copy()

# a helper surface we use later for the fixed shadows
shadow_surf = pygame.Surface((800, 600))
# we set a colorkey to easily make this surface transparent
colorkey_color = (2,3,4)
shadow_surf.set_colorkey(colorkey_color)
# the alpha value of our shadow
shadow_surf.set_alpha(100)

# just something to see the shadow effect
test_surface = pygame.Surface((800, 100))
test_surface.fill(pygame.Color('cyan'))

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

    screen.fill(pygame.Color('white'))

    screen.blit(test_surface, (0, 150))

    # first we blit the alpha channel shadows directly to the screen 
    screen.blit(shadow, (100, 100))
    screen.blit(shadow2, (164, 164))

    # here we draw the shadows to the helper surface first
    # since the helper surface has no per-pixel alpha, the shadows
    # will be fully black, but the alpha value for the full Surface image
    # is set to 100, so we still have transparent shadows
    shadow_surf.fill(colorkey_color)
    shadow_surf.blit(shadow, (100, 100))
    shadow_surf.blit(shadow2, (164, 164))

    screen.blit(shadow_surf, (400, 0))

    pygame.display.update()

enter image description here

Upvotes: 2

lakam99
lakam99

Reputation: 605

You could create a function that tests for shadow collision and adjust the blend values of the shadows accordingly.

Upvotes: 0

Related Questions