wxPython和pycairo练习记录15
BitMask
BitMask 有翻译成位掩码,也有翻译成位遮罩的。BitMask 用作碰撞检测的原理是将屏幕划分成二维网格,每个网格用一个 bit 表示,没有碰撞体填0,有碰撞体填1。这样,将屏幕划分为 N行 * N列 的矩形网格,网格宽度和高度都设定为8像素,当主角的像素坐标是 (100, 100) 时,向下取整 100/8 即网格坐标为 (12, 12),只要判断这一格是1还是0,就可以判断它能否碰撞了。
来源:位遮罩(BitMask)碰撞检测技术 https://zhuanlan.zhihu.com/p/425402147 作者:皮皮关
pygame 中每个像素都分别表示一格,需要根据每个像素的值来计算存储0还是1,那像素到底是什么?
什么是像素
像素:是指在由一个数字序列表示的图像中的一个最小单位,称为像素。来源:https://baike.baidu.com/item/%E5%83%8F%E7%B4%A0/95084
这里的像素并非我们平时所说的眼睛看到的点,它更像是屏幕上的一个网格,不同的显示设备可能有不同大小的格子。在 Python 中,我们可以方便地通过一些库读取图像的某个像素值。以 PIL 为例(安装 pip install Pillow):
# -*- coding: utf-8 -*-
from io import BytesIO
import requests
from PIL import Image
resp = requests.get("https://img-home.csdnimg.cn/images/20201124032511.png")
stream = BytesIO(resp.content)
img = Image.open(stream).convert('RGBA')
# img.show()
# print(img.size)
print(img.getpixel((10, 44)))
# (215, 149, 113, 210)
这个例子中,返回的像素值是 RGBA 数值元组。
collide_mask
collide_mask 方法正是通过 BitMask 来检测碰撞的,它会先检查 Sprite 对象有没有 mask 属性,如果没有就根据 surface 创建 mask。有时候需要不同于 surface 形状的 mask,还是有必要区分开的,所以得在原来坦克游戏的 Sprite 类中添加 mask 属性。
先看看 mask 到底是什么,文档中说 Mask 是用于表示二维位掩码的 pygame 对象。pygame.mask.from_surface
是通过判断像素透明度值来填充0或1,默认透明度阈值是127,0为完全透明,255为完全不透明,默认大于127即填充为1,小于等于127填充为0,返回的结果就是0和1组成的一维数组,通过除以行宽得到类似二维数组。
创建 10*10 的透明 surface,在坐标 (0, 0) 画上红色填充的 5*5 矩形,转换为 mask,最终得到 10 * 10 的01数组
>>> import pygame
>>> surface = pygame.Surface((10, 10), pygame.SRCALPHA, 32)
>>> pygame.draw.rect(surface, (255, 0, 0, 255), pygame.Rect(0, 0, 5, 5), 1)
>>> surface.get_at((0, 0))
(255, 0, 0, 255)
>>> surface.get_at((5, 5))
(0, 0, 0, 0)
>>> surface.get_at((4, 4))
(255, 0, 0, 255)
>>> mask = pygame.mask.from_surface(surface)
>>> mask.get_size()
(10, 10)
>>> mask.get_at((0, 0))
1
>>> mask.get_at((5, 5))
0
>>> mask.get_at((4, 4))
1
文档:
https://www.pygame.org/docs/ref/mask.html
源码查看:
https://www.pygame.org/docs/ref/mask.html#pygame.mask.from_surface
from_surface(surface) -> Mask
from_surface(surface, threshold=127) -> Mask
https://www.pygame.org/docs/ref/mask.html#pygame.mask.Mask
Mask(size=(width, height)) -> Mask
Mask(size=(width, height), fill=False) -> Mask
https://github.com/pygame/pygame/blob/main/src_c/mask.c#L826
static PyObject *
mask_from_surface(PyObject *self, PyObject *args, PyObject *kwargs)
int threshold = 127; /* default value */
int use_thresh = 1;
maskobj = CREATE_MASK_OBJ(surf->w, surf->h, 0);
use_thresh = (SDL_GetColorKey(surf, &colorkey) == -1);
if (use_thresh) {
set_from_threshold(surf, maskobj->mask, threshold);
}
else {
set_from_colorkey(surf, maskobj->mask, colorkey);
}
static void
set_from_threshold(SDL_Surface *surf, bitmask_t *bitmask, int threshold)
{
SDL_PixelFormat *format = surf->format;
Uint8 bpp = format->BytesPerPixel;
Uint8 *pixel = NULL;
Uint8 rgba[4];
int x, y;
for (y = 0; y < surf->h; ++y) {
pixel = (Uint8 *)surf->pixels + y * surf->pitch;
for (x = 0; x < surf->w; ++x, pixel += bpp) {
SDL_GetRGBA(get_pixel_color(pixel, bpp), format, rgba, rgba + 1,
rgba + 2, rgba + 3);
if (rgba[3] > threshold) {
bitmask_setbit(bitmask, x, y);
}
}
}
}
https://github.com/pygame/pygame/blob/main/src_c/include/pygame_mask.h#L28
typedef struct {
PyObject_HEAD bitmask_t *mask;
void *bufdata;
} pgMaskObject;
https://github.com/pygame/pygame/blob/main/src_c/include/bitmask.h#L53
#define BITMASK_W unsigned long int
#define BITMASK_W_LEN (sizeof(BITMASK_W) * CHAR_BIT)
#define BITMASK_W_MASK (BITMASK_W_LEN - 1)
#define BITMASK_N(n) ((BITMASK_W)1 << (n))
typedef struct bitmask {
int w, h;
BITMASK_W bits[1];
} bitmask_t;
/* Sets the bit at (x,y) */
static INLINE void
bitmask_setbit(bitmask_t *m, int x, int y)
{
m->bits[x / BITMASK_W_LEN * m->h + y] |= BITMASK_N(x & BITMASK_W_MASK);
}
除了碰撞检测,暂时没有对 Mask 进行其他操作的需求,就不单独创建类了,使用 namedtuple 记录 Mask 的属性。bitmask.c 里转换来转换去,看着有点懵,其实就是通过尺寸和坐标差判断有没有相交,确定相交形成的矩形的左上角顶点分别相对于两个 Mask 矩形左上角作为原点的坐标,遍历相交部分有没有某个点的值同时为1。用 PIL 转换 cairo.ImageSurface-> wx.Bitmap -> wx.Image -> Image
要么提示 format 不对,要么提示 buffer 不够大,最后还是直接用 wx.Image.GetAlpha() 直接获取透明值列表。
https://github.com/pygame/pygame/blob/main/src_py/sprite.py#L1664
https://github.com/pygame/pygame/blob/main/src_c/bitmask.c#L258
@staticmethod
def collide_mask(left, right):
xoffset = right.GetX() - left.GetX()
yoffset = right.GetY() - left.GetY()
leftmask = left.GetMask() or Collision.mask_from_surface(left.GetSurface())
rightmask = right.GetMask() or Collision.mask_from_surface(right.GetSurface())
return Collision.mask_overlap(leftmask, rightmask, xoffset, yoffset)
@staticmethod
def mask_from_surface(surface, threshold=127):
bmp = wx.lib.wxcairo.BitmapFromImageSurface(surface)
img = bmp.ConvertToImage()
alphas = list(img.GetAlpha())
# pilimg = Image.frombuffer("RGBA", (surface.get_width(), surface.get_height()), img.GetDataBuffer())
#
# bits = []
# for y in range(pilimg.height):
# for x in range(pilimg.width):
# alpha = pilimg.getpixel((x, y))[-1]
# bits[pilimg.width * y + x] = (alpha > threshold) and 1 or 0
bits = []
for alpha in alphas:
bits.append((alpha > threshold) and 1 or 0)
return Collision.Mask(surface.get_width(), surface.get_height(), bits)
@staticmethod
def mask_overlap(a, b, xoffset, yoffset):
if ((xoffset >= a.w) or (yoffset >= a.h) or (yoffset <= -b.h) or
(xoffset <= -b.w) or (not a.h) or (not a.w) or (not b.h) or (not b.w)):
return False
if xoffset >= 0:
a_entry_x = abs(xoffset)
b_entry_x = 0
overlap_w = a.w - abs(xoffset)
else:
a_entry_x = 0
b_entry_x = abs(xoffset)
overlap_w = b.w - abs(xoffset)
if yoffset >= 0:
a_entry_y = abs(yoffset)
b_entry_y = 0
overlap_h = a.h - abs(yoffset)
else:
a_entry_y = 0
b_entry_y = abs(yoffset)
overlap_h = b.h - abs(yoffset)
for y in range(overlap_h):
for x in range(overlap_w):
if (a.bits[a.w * (a_entry_y + y) + a_entry_x + x] == 1 and
b.bits[b.w * (b_entry_y + y) + b_entry_x + x] == 1):
return True
单元测试
https://github.com/pygame/pygame/blob/main/test/sprite_test.py#L198
def test_collide_mask__opaque(self):
# create before wx.lib.wxcairo.BitmapFromImageSurface
app = App()
# make some fully opaque sprites that will collide with masks.
for s in [self.s1, self.s2, self.s3]:
surface = s.GetSurface()
ctx = Context(surface)
ctx.set_source_rgba(255, 255, 255, 255)
ctx.paint()
s.SetSurface(surface, surface.get_width(), surface.get_height())
# masks should be autogenerated from image if they don't exist.
self.assertTrue(Collision.collide_mask(self.s1, self.s2))
self.assertFalse(Collision.collide_mask(self.s1, self.s3))
for s in [self.s1, self.s2, self.s3]:
surface = s.GetSurface()
mask = Collision.mask_from_surface(surface)
s.SetMask(mask)
# with set masks.
self.assertTrue(Collision.collide_mask(self.s1, self.s2))
self.assertFalse(Collision.collide_mask(self.s1, self.s3))
def test_collide_mask__transparent(self):
# create before wx.lib.wxcairo.BitmapFromImageSurface
app = App()
# make some sprites that are fully transparent, so they won't collide.
for s in [self.s1, self.s2, self.s3]:
surface = s.GetSurface()
ctx = Context(surface)
ctx.set_source_rgba(255, 255, 255, 0)
ctx.paint()
mask = Collision.mask_from_surface(surface, 255)
s.SetMask(mask)
self.assertFalse(Collision.collide_mask(self.s1, self.s2))
self.assertFalse(Collision.collide_mask(self.s1, self.s3))
还没有实际使用,有些问题可能暂时没发现。