4

I'm masking a surface in pygame as suggested by nkorth in response to the question is there any way to only blit or update in a mask, updating the mask and masked surfaces each frame. And while it is possible to achieve this using his method, the frame rate drops immediately, and that's with just one instance of this surface being rendered on the screen at any one time.

Here is the image I want to mask, and here is how it should look like on the application. These other circles are not important, they are created sequentially and are drawn on top of one another. I tried compressing the image to 100kb (more than 10 times it's original size), but that only helped a little bit. It still runs poorly.

Is this even possible on pygame? If so, then how?

Here's the respective class for this:

class NoiseCircle(Circle):

    def __init__(self, center=SCREEN_CENTER, radius=5):
        # init code

    ...

    def draw(self, surf):
        self.masked.blit(self.noise_image, (0, 0))
        self.mask.fill((0, 0, 0, 0))
        pygame.draw.circle(self.mask, (255, 255, 255), self.center, self.radius)
        self.masked.blit(self.mask, (0, 0), None, pygame.BLEND_RGBA_MULT)
        pygame.draw.circle(self.masked, (0, 0, 0), self.center, self.radius, 1)
        surf.blit(self.masked, (0, 0))

The main loop just passes the display surface to draw() and that takes care of the object rendering.

Community
  • 1
  • 1
bzrr
  • 1,490
  • 3
  • 20
  • 39
  • Could you show the code you're using? Otherwise it's hard to say where your code can be improved. – sloth Feb 24 '15 at 13:53
  • Is `self.center` or `self.radius` supposed to be changed? If not, you don't have to blit to/draw on `self.masked` every time you `draw` is called. Just do it once and use the resulting surface for blitting on `surf`. – sloth Feb 24 '15 at 16:00
  • Yeah, the circles expand and can change position/center. – bzrr Feb 24 '15 at 16:06
  • Do you use `self.masked` and `self.mask` elsewhere in your code? Is `self.masked` the same size as `self.noise_image`? Why big are they? – sloth Feb 24 '15 at 16:13
  • **1)** `self.masked` and `self.mask` are attributes of the NoiseCircle object, so they're used in each instance. Other than that, no. **2)** The surfaces are big because there is a possibility that the circle will expand until it reaches the display size without any other circle being drawn on top of it. I could increase the size of the surfaces as needed, but I figured that would take more processing time and decided to have it at its maximum size (the display size) since that works for all cases. – bzrr Feb 24 '15 at 16:26
  • I also see that there's no need for each instance of the class to hold a copy of this ``self.noise_image`` since its static and doesn't change, but as I said previously, there wasn't much improvement in the frame rate even when I compressed the "noise" image file to a tenth of its original size, so I don't think that's a problem. – bzrr Feb 24 '15 at 16:34

1 Answers1

0

It is hard to tell where is the bottleneck in you program. Probably it is draw.circle. Try with my code, it implements my own circle drawing algorithm (note, it is not precise circle). Tested with pygame 1.9.2 on Windows, quite slow computer - Atom 1.6 GHz, and it gives me around 40 milliseconds (see DT in code). Then try your implementation and see if it is faster or slower. It would be interesting to compare. BTW, what about using colorkey?

def circle(r, x0, y0, dest, value):
    x1 = int(r / math.sqrt(2))
    h = []
    x = x1
    while(1):
        x += 1
        if x == r:
            h.append(ch/3)
            break
        ch = int(math.sqrt(r**2-x**2))
        h.append(ch)
    p = 0
    dest[x0-x1:x0+x1+1, y0-x1:y0+x1+1] = value  
    while p < len(h):
        dest[x0+x1+p+1, y0-h[p]:y0+h[p]+1] = value
        dest[x0-x1-p-1, y0-h[p]:y0+h[p]+1] = value
        dest[x0-h[p]:x0+h[p]+1, y0-x1-p-1] = value
        dest[x0-h[p]:x0+h[p]+1, y0+x1+p+1] = value
        p += 1

def put_alpha(Dest, Src):                       # write alpha values
    ref = pygame.surfarray.pixels_alpha (Dest)
    numpy.copyto(ref, Src) 
    del ref


Layer = pygame.Surface ((w,h),flags = pygame.SRCALPHA)
Layer.fill(0xff4B432E)
Mask = numpy.zeros((w,h), dtype = numpy.uint8)
Mask[:] = 255
cur_time = pygame.time.get_ticks()
old_time = cur_time

circle(125, 400, 300, Mask, 125)
circle(75, 400, 300, Mask, 255)
put_alpha(Layer, Mask)

cur_time = pygame.time.get_ticks()
DT = cur_time - old_time
print DT

Update: First, the custom circle function acts ca 5 times slower than pygame's, so it is not what can slow things down. I've sumed up some performance results with various variants. First, make sure you use 24 bit surfaces everywhere, unless you want full 256 grades of transparency, this alone can give better results. If you run following example the output must give "24 32 24". Those are bit depths of corresponding surfaces. This would be optimal for this particular case.

pygame.init()
w = 800
h = 600
DISP = pygame.display.set_mode((w, h), 0, 24)
clock = pygame.time.Clock( ) 

Noise = pygame.Surface ((w,h))
Noise.fill((110,0,10))  
color24bit = (169, 163, 144)    # r g b
color = (169, 163, 144, 255)    # r g b a
color_a = (169, 163, 144, 112)  # r g b a
Layer = pygame.Surface ((w,h), flags = pygame.SRCALPHA)
Layer.fill(color)       # ( 169, 163, 144, 255)

Layer_colorkey = pygame.Surface ((w,h))
Layer_colorkey.fill(color24bit)
color_key = (50, 50, 50)
Layer_colorkey.set_colorkey(color_key)

print Noise.get_bitsize(), Layer.get_bitsize(), Layer_colorkey.get_bitsize()

Mask = numpy.zeros((w,h), dtype = numpy.uint8)
Mask[:] = 255

t=0
cur_time = pygame.time.get_ticks()
old_time = cur_time

# 1. blit with per-pixel alpha and custom Mask array
while t < 0:
    circle(296, 400, 300, Mask, 112)
    circle(15, 400, 300, Mask, 255)
    put_alpha(Layer, Mask)
    Noise.blit(Layer,(0,0))
    DISP.blit(Noise,(0,0))
    t += 1

# 2. blit with per-pixel alpha and draw functions
while t < 0:
    pygame.draw.circle(Layer, color_a, (400,300), 296, 0)
    pygame.draw.circle(Layer, color, (400,300), 15, 0)
    Noise.blit(Layer,(0,0))
    DISP.blit(Noise,(0,0))
    t += 1

# 3. blit with colorkey 
while t < 1:
    pygame.draw.circle(Layer_colorkey, color_key, (400,300), 296, 0)
    pygame.draw.circle(Layer_colorkey, color, (400,300), 15, 0)
    Noise.blit(Layer_colorkey,(0,0))
    DISP.blit(Noise,(0,0))
    t += 1

cur_time = pygame.time.get_ticks()
DT = cur_time - old_time
print "time:", DT

Conclusion:
1. Full alpha with applied custom Mask array: slowest, but you have full control of the mask form and can make cool effects with transparency.
2. Full alpha + just use built-in draw functions, just define color with needed alpha value. A litle bit faster (ITPC), but no direct control over color values.
3. Normal 24 bit surface with colorkey. 2x faster than above, but no custom transparency.

Mikhail V
  • 1,416
  • 1
  • 14
  • 23
  • It is quite easy - say `Layer` is your top layer, where you want a transparent ring (as i have understood from your screenshots). Now I create a numpy array `Mask`, this must be of type `uint8` and same size as `Layer`, now set the values inside `Mask` as you wish (draw 2 filled circles in this case) - 255 is no transparency, 0 is full transparency, 125 in my example would be half-transparent. Then just copy whole Mask array to the Alpha channel of the Layer, see `put_alpha` function. – Mikhail V Feb 25 '15 at 14:13
  • I meant how your circle algorithm works. Is it similar to Midpoint? – bzrr Feb 25 '15 at 17:24
  • Midpoint is for drawing continous circle, not filled circle, thus it is a bit other concept initially. Here I just start from diagonal point (at 45 degrees) and increment x, then calculate y through circle equation and it will be the heith of the column to fill. I store the list of heigths in `h`. The rest is obvios - fill the pixels in range [-h,h] for each quarter. After all lines are filled it leaves a square inside the circle which I fill then with one command. I don't have an idea how effective is it, must be good enough thou. – Mikhail V Feb 25 '15 at 20:33