wxPython和pycairo练习记录15

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 作者:皮皮关​

Bitmask

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 * 1001数组
>>> 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
Mask

源码查看:

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))

还没有实际使用,有些问题可能暂时没发现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值