2
\$\begingroup\$

I want to blit an image onto the parts of a surface that aren't transparent. I was wondering if there were a quick way to do this in Pygame. I want to be able to add effects to stuff.

I know that I could use set_at and get_at to do it, but that would be too slow.

\$\endgroup\$
  • \$\begingroup\$ I dont know how to do that in pygame, but rendering the surface with a fragment shader that checks pixel alpha is enough to know which are the opaque parts of it. Then, Just use uv coordenates to get the color in the image to be blit. \$\endgroup\$ – dsilva.vinicius Apr 18 '17 at 20:16
  • \$\begingroup\$ Libraries like cairo call that "masking" (zetcode.com/gfx/cairo/clippingmasking), now let's find out how to do that in pygame. I will post an answer if I find a working method. I see you are concerned about performance, then the method has to be efficient. \$\endgroup\$ – Hatoru Hansou Jun 21 '17 at 2:21
1
\$\begingroup\$

Do a pixel by pixel set inside a loop is, as observed in the question, not efficient enough for game development. Masking is the way to go (assuming our libraries of choice implement this in an efficient way).

How to mask in pygame

When calling Surface.blit() you will use the special_flags parameter to control how pixels are combined. In concrete, most situations will require the pygame.BLEND_RGBA_MULT and an extra image asset to use as the mask image, probably all opaque pixels to white and the rest with alpha set to 0. Procedural generation of the mask image is possible but I found more practical to use an asset.

Also I find more efficient to cache a version of the image with the mask already applied.

BLEND_RGBA_MULT will multiply source pixels with destination surface pixels. This can be used to achieve many funny effects, but the most useful is when your mask image contains all white pixels and those you want to discard with alpha set to 0. Alpha 0 in the mask results in alpha 0. And a white pixel in the mask results in the same color than source image.

Demonstration

masking.gif

To run the following demo you will need the following assets:

avatar1.png avatar1.png

avatar2.png avatar2.png

avatar3.png avatar3.png

mask.png mask.png

mask.png image is invisible with this site background color, but if you move the mouse pointer over it you will see where it is. Right click and save.

The assets were quickly made thanks to opengameart.org. Original authors require attribution so:

  1. Credit Noble Master Games as follows (linking is optional): "Avatar graphics created by Noble Master Games" and link to http://www.noblemaster.com
  2. Credit the artist "Liea" as follows (optional): "Avatar graphics designed by Mei-Li Nieuwland" and link to http://liea.deviantart.com

From game: http://www.ageofconquest.com/

And finally, the Python code:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys, pygame
pygame.init()

size = width, height = 640, 480
black = 0, 0, 0

screen = pygame.display.set_mode(size)

# some globals we need
avatar_w = 64
avatar_h = 64

# our game objects are (inneficient) maps with two keys: rect and image
avatar1 = dict()
avatar2 = dict()
avatar3 = dict()

avatar1["rect"] = pygame.Rect(16, 16, avatar_w, avatar_h)
avatar2["rect"] = pygame.Rect(16 + avatar_w + 8, 16, avatar_w, avatar_h)
avatar3["rect"] = pygame.Rect(16 + avatar_w * 2 + 8 + 8, 16, avatar_w, avatar_h)

clickables = (avatar1, avatar2, avatar3)

# resource loading
# crash if the files are not in place (OK for now)
avatar1["image"] = pygame.image.load("avatar1.png")
avatar2["image"] = pygame.image.load("avatar2.png")
avatar3["image"] = pygame.image.load("avatar3.png")
selected_avatar = None

# procedural generation of a circled mask is possible but for now we
# will use an asset
mask = pygame.image.load("mask.png")

masked_avatar = dict()
masked_avatar["image"] = mask
masked_avatar["rect"] = pygame.Rect(size[0] / 2 - avatar_w / 2, 16 + avatar_h * 2, avatar_w, avatar_h) 

renderables = (avatar1, avatar2, avatar3, masked_avatar)

def update_masked_avatar (avatar):
    global masked_avatar
    # TODO: consider clearing then blit to masked_avatar rather than
    # cloning mask each time, but it's not so simple as it sounds, when
    # you blit, you need the alphas from mask to be transferred too.
    # The lazy, I don't want to do research now, approach is to clone.
    surf = mask.copy()
    surf.blit(avatar["image"], (0, 0), None, pygame.BLEND_RGBA_MULT)
    masked_avatar["image"] = surf

# main loop
while 1:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()

    mouse_pos = pygame.mouse.get_pos()

    # process the clickables list
    for obj in clickables:
        if obj["rect"].collidepoint(mouse_pos):
            if selected_avatar != obj:
                selected_avatar = obj
                update_masked_avatar(obj)

    screen.fill(black)

    # process the renderables list
    for obj in renderables:
        screen.blit(obj["image"], obj["rect"])

    pygame.display.flip()
\$\endgroup\$
0
\$\begingroup\$

I don't think there is a single call that can make objects appear on opaque parts of a screen. You would probably have to create a background class and have a function tied to that class that uses the .blit command to only show objects on the opaque parts.

class Background(pygame.sprite.Sprite):
    def __init__(self, image_file, location):
        pygame.sprite.Sprite.__init__(self)  #call Sprite initializer
        self.image = pygame.image.load(image_file)
        self.rect = self.image.get_rect()
        self.rect.left, self.rect.top = location

And inside this class have a function that calls in your objects.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.