如何使用Pygame围绕中心旋转图像?

51 投票
7 回答
101497 浏览
提问于 2025-04-16 07:05

我一直在尝试用 pygame.transform.rotate() 来围绕图像的中心旋转它,但总是出问题。具体来说,出问题的地方是 rot_image = rot_image.subsurface(rot_rect).copy()。我遇到了一个异常错误:

ValueError: subsurface rectangle outside surface area

下面是用来旋转图像的代码:

def rot_center(image, angle):
    """rotate an image while keeping its center and size"""
    orig_rect = image.get_rect()
    rot_image = pygame.transform.rotate(image, angle)
    rot_rect = orig_rect.copy()
    rot_rect.center = rot_image.get_rect().center
    rot_image = rot_image.subsurface(rot_rect).copy()
    return rot_image

7 个回答

8

这个回答有一些问题:我们需要在函数中获取之前矩形的位置,这样才能把它赋值给新的矩形,比如:

rect = new_image.get_rect(center=rect.center) 

在另一个回答中,位置是通过从原始图像创建一个新的矩形来获得的,但这样的话,它会被放在默认的(0, 0)坐标位置。

下面的例子应该能正确工作。新的矩形需要旧矩形的center位置,所以我们也把它传递给函数。然后旋转图像,调用get_rect来获取一个大小正确的新矩形,并把旧矩形的center属性作为center参数传入。最后,返回旋转后的图像和新的矩形,作为一个元组,并在主循环中解包它。

import pygame as pg


def rotate(image, rect, angle):
    """Rotate the image while keeping its center."""
    # Rotate the original image without modifying it.
    new_image = pg.transform.rotate(image, angle)
    # Get a new rect with the center of the old rect.
    rect = new_image.get_rect(center=rect.center)
    return new_image, rect


def main():
    clock = pg.time.Clock()
    screen = pg.display.set_mode((640, 480))
    gray = pg.Color('gray15')
    blue = pg.Color('dodgerblue2')

    image = pg.Surface((320, 200), pg.SRCALPHA)
    pg.draw.polygon(image, blue, ((0, 0), (320, 100), (0, 200)))
    # Keep a reference to the original to preserve the image quality.
    orig_image = image
    rect = image.get_rect(center=(320, 240))
    angle = 0

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

        angle += 2
        image, rect = rotate(orig_image, rect, angle)

        screen.fill(gray)
        screen.blit(image, rect)
        pg.display.flip()
        clock.tick(30)


if __name__ == '__main__':
    pg.init()
    main()
    pg.quit()

这里还有一个关于旋转pygame精灵的例子。

import pygame as pg


class Entity(pg.sprite.Sprite):

    def __init__(self, pos):
        super().__init__()
        self.image = pg.Surface((122, 70), pg.SRCALPHA)
        pg.draw.polygon(self.image, pg.Color('dodgerblue1'),
                        ((1, 0), (120, 35), (1, 70)))
        # A reference to the original image to preserve the quality.
        self.orig_image = self.image
        self.rect = self.image.get_rect(center=pos)
        self.angle = 0

    def update(self):
        self.angle += 2
        self.rotate()

    def rotate(self):
        """Rotate the image of the sprite around its center."""
        # `rotozoom` usually looks nicer than `rotate`. Pygame's rotation
        # functions return new images and don't modify the originals.
        self.image = pg.transform.rotozoom(self.orig_image, self.angle, 1)
        # Create a new rect with the center of the old rect.
        self.rect = self.image.get_rect(center=self.rect.center)


def main():
    screen = pg.display.set_mode((640, 480))
    clock = pg.time.Clock()
    all_sprites = pg.sprite.Group(Entity((320, 240)))

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

        all_sprites.update()
        screen.fill((30, 30, 30))
        all_sprites.draw(screen)
        pg.display.flip()
        clock.tick(30)


if __name__ == '__main__':
    pg.init()
    main()
    pg.quit()
8

你正在删除rotate创建的矩形。你需要保留这个矩形,因为它在旋转时会改变大小。

如果你想保持这个对象的位置,可以这样做:

def rot_center(image, angle):
    """rotate a Surface, maintaining position."""
    
    loc = image.get_rect().center  #rot_image is not defined 
    rot_sprite = pygame.transform.rotate(image, angle)
    rot_sprite.get_rect().center = loc
    return rot_sprite
    
    # or return tuple: (Surface, Rect)
    # return rot_sprite, rot_sprite.get_rect()
144

简短回答:

当你使用 pygame.transform.rotate 来旋转图像时,旋转后的图像大小会比原始图像大。你需要确保旋转后的图像中心仍然在未旋转图像的中心位置。为此,先获取原始图像的矩形区域,然后设置位置。接着获取旋转图像的矩形区域,并通过原始矩形的中心来设置位置。
这个函数 red_center 会返回一个元组,包含旋转后的图像和旋转图像的边界矩形:

def rot_center(image, angle, x, y):
    
    rotated_image = pygame.transform.rotate(image, angle)
    new_rect = rotated_image.get_rect(center = image.get_rect(center = (x, y)).center)

    return rotated_image, new_rect

或者你可以写一个函数来旋转并 .blit 图像:

def blitRotateCenter(surf, image, topleft, angle):

    rotated_image = pygame.transform.rotate(image, angle)
    new_rect = rotated_image.get_rect(center = image.get_rect(topleft = topleft).center)

    surf.blit(rotated_image, new_rect)

详细回答:

图像(pygame.Surface)可以通过 pygame.transform.rotate 来旋转。

如果在一个循环中逐步旋转图像,图像会变形并迅速增大:

while not done:

    # [...]

    image = pygame.transform.rotate(image, 1)
    screen.blit(image, pos)
    pygame.display.flip()

这是因为旋转图像的边界矩形总是比原始图像的边界矩形大(除了每90度的旋转)。
图像变形是因为多次复制。每次旋转都会产生一个小误差,误差的总和在不断增加,导致图像质量下降。

可以通过保留原始图像,并只用一次旋转操作生成的图像来解决这个问题。

angle = 0
while not done:

    # [...]

    rotated_image = pygame.transform.rotate(image, angle)
    angle += 1

    screen.blit(rotated_image, pos)
    pygame.display.flip()

现在图像似乎在随意改变位置,因为图像的大小因旋转而变化,而原点始终是图像边界矩形的左上角。

可以通过比较旋转前后的 轴对齐边界框 来进行补偿。
在接下来的计算中使用 pygame.math.Vector2。注意,在屏幕坐标中,y轴向下,而数学中的y轴是从下到上的。这导致在计算时需要“翻转”y轴。

设置一个包含边界框四个角点的列表:

w, h = image.get_size()
box = [pygame.math.Vector2(p) for p in [(0, 0), (w, 0), (w, -h), (0, -h)]]

通过 pygame.math.Vector2.rotate 旋转这些角点的向量:

box_rotate = [p.rotate(angle) for p in box]

获取旋转点的最小值和最大值:

min_box = (min(box_rotate, key=lambda p: p[0])[0], min(box_rotate, key=lambda p: p[1])[1])
max_box = (max(box_rotate, key=lambda p: p[0])[0], max(box_rotate, key=lambda p: p[1])[1])

通过将旋转框的最小值加到位置上,计算图像左上角的“补偿”原点。对于y坐标,max_box[1] 是最小值,因为y轴是“翻转”的:

origin = (pos[0] + min_box[0], pos[1] - max_box[1])

rotated_image = pygame.transform.rotate(image, angle)
screen.blit(rotated_image, origin)

甚至可以在原始图像上定义一个支点。计算从图像中心到支点的偏移向量并旋转这个向量。一个向量可以用 pygame.math.Vector2 表示,并可以用 pygame.math.Vector2.rotate 进行旋转。注意,pygame.math.Vector2.rotate 的旋转方向与 pygame.transform.rotate 是相反的,因此角度需要反转:

计算从图像中心到支点的向量:

image_rect = image.get_rect(topleft = (pos[0] - originPos[0], pos[1]-originPos[1]))
offset_center_to_pivot = pygame.math.Vector2(pos) - image_rect.center

旋转这个向量

rotated_offset = offset_center_to_pivot.rotate(-angle)

计算旋转后图像的中心:

rotated_image_center = (pos[0] - rotated_offset.x, pos[1] - rotated_offset.y)

旋转并将图像绘制到表面:

rotated_image = pygame.transform.rotate(image, angle)
rotated_image_rect = rotated_image.get_rect(center = rotated_image_center)

screen.blit(rotated_image, rotated_image_rect)

在下面的示例程序中,函数 blitRotate(surf, image, pos, originPos, angle) 完成了上述所有步骤,并将旋转后的图像绘制到一个表面上。

  • surf 是目标表面

  • image 是需要旋转和绘制的表面

  • pos 是目标表面 surf 上支点的位置(相对于 surf 的左上角)

  • originPos 是支点在 image 表面上的位置(相对于 image 的左上角)

  • angle 是旋转的角度(以度为单位)

这意味着,blitRotate 的第二个参数(pos)是窗口中支点的位置,而第三个参数(originPos)是旋转 表面 上支点的位置:


最小示例: repl.it/@Rabbid76/PyGame-RotateAroundPivot

import pygame

pygame.init()
screen = pygame.display.set_mode((300, 300))
clock = pygame.time.Clock()

def blitRotate(surf, image, pos, originPos, angle):

    # offset from pivot to center
    image_rect = image.get_rect(topleft = (pos[0] - originPos[0], pos[1]-originPos[1]))
    offset_center_to_pivot = pygame.math.Vector2(pos) - image_rect.center
    
    # roatated offset from pivot to center
    rotated_offset = offset_center_to_pivot.rotate(-angle)

    # roatetd image center
    rotated_image_center = (pos[0] - rotated_offset.x, pos[1] - rotated_offset.y)

    # get a rotated image
    rotated_image = pygame.transform.rotate(image, angle)
    rotated_image_rect = rotated_image.get_rect(center = rotated_image_center)

    # rotate and blit the image
    surf.blit(rotated_image, rotated_image_rect)
  
    # draw rectangle around the image
    pygame.draw.rect(surf, (255, 0, 0), (*rotated_image_rect.topleft, *rotated_image.get_size()),2)

def blitRotate2(surf, image, topleft, angle):

    rotated_image = pygame.transform.rotate(image, angle)
    new_rect = rotated_image.get_rect(center = image.get_rect(topleft = topleft).center)

    surf.blit(rotated_image, new_rect.topleft)
    pygame.draw.rect(surf, (255, 0, 0), new_rect, 2)

try:
    image = pygame.image.load('AirPlaneFront.png')
except:
    text = pygame.font.SysFont('Times New Roman', 50).render('image', False, (255, 255, 0))
    image = pygame.Surface((text.get_width()+1, text.get_height()+1))
    pygame.draw.rect(image, (0, 0, 255), (1, 1, *text.get_size()))
    image.blit(text, (1, 1))
w, h = image.get_size()

angle = 0
done = False
while not done:
    clock.tick(60)
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            done = True

    pos = (screen.get_width()/2, screen.get_height()/2)
    
    screen.fill(0)
    blitRotate(screen, image, pos, (w/2, h/2), angle)
    #blitRotate2(screen, image, pos, angle)
    angle += 1
    
    pygame.draw.line(screen, (0, 255, 0), (pos[0]-20, pos[1]), (pos[0]+20, pos[1]), 3)
    pygame.draw.line(screen, (0, 255, 0), (pos[0], pos[1]-20), (pos[0], pos[1]+20), 3)
    pygame.draw.circle(screen, (0, 255, 0), pos, 7, 0)

    pygame.display.flip()
    
pygame.quit()
exit()

另请参见 旋转表面 以及问题的答案:

撰写回答