pygame是是上世纪的产品,虽然不适合最3D游戏,但我可以使用pygame来绘制简单的3D图形,就像在白纸上画立体图形一样。
主要内容: 视觉上的远近、3D空间、绘制一个空间图形
一、视觉上的远近
人的视觉总是遵循一个原则:近大远小。想象一下,如果有一天躺在草地上什么都不做,仰望星空,天空突然飞过流星雨,离我们近的流星看起来就很狭长而明亮,离我们远的则呈一个暗点状,我们完全可以用pygame模拟出来:
step1:定义Star类
要描述一颗流星,需要有坐标和速度。在定义一个列表来存储点集:
class Star(object):
def __init__(self, x, y, speed):
self.x = x
self.y = y
self.speed = speed
stars = []
step2:生成随机点
我们需要定时的生成流星
x = float(randint(0, 639))
y = float(randint(0, 479))
speed = float(randint(10, 300))
stars.append(Star(x, y, speed))
while True:
y = float(randint(0, 479))
speed = float(randint(10, 300))
stars.append(Star(640., y, speed))
step3:绘制短线
因为近的流星彗尾看起来更狭长,所以我们可以根据速度来绘制一条短线来表示流星。可以使用pygaame的time库函数Clock测量时间,根据时间和速度在不同位置绘制不同长度的短线:
while True:
for star in stars:
new_x = star.x - star.speed*time_passed_seconds
pygame.draw.aaline(screen, white, (new_x, star.y), (star.x+1, star.y))
star.x = new_x
step4:查看效果
import pygame
from pygame.locals import *
from random import randint
class Star(object):
def __init__(self, x, y, speed):
self.x = x
self.y = y
self.speed = speed
def run():
pygame.init()
screen = pygame.display.set_mode((640, 480), 0, 32)
stars = []
for k in range(200):
x = float(randint(0, 639))
y = float(randint(0, 479))
speed = float(randint(10, 300))
stars.append(Star(x, y, speed))
clock = pygame.time.Clock()
white = (255, 255, 255)
while True:
for event in pygame.event.get():
if event.type == QUIT:
pygame.quit()
return
elif event.type == KEYDOWN:
return
y = float(randint(0, 479))
speed = float(randint(10, 300))
stars.append(Star(640., y, speed))
time_passed = clock.tick(60)
time_passed_seconds = time_passed / 1000
screen.fill((0, 0, 0))
for star in stars:
new_x = star.x - star.speed*time_passed_seconds
pygame.draw.aaline(screen, white, (new_x, star.y), (star.x+1, star.y))
star.x = new_x
def on_screen(star):
return star.x > 0
stars = list(filter(on_screen, stars))
pygame.display.update()
if __name__ == "__main__":
run()
这里我们需要注意一个细节,我们定义了一个on_screen函数,若一个点在屏幕上则返回值为True,使用filter函数将不在屏幕的函数过滤掉,而保留屏幕上的点(不在屏幕上的点会消耗资源)。
def on_screen(star):
return star.x > 0
stars = list(filter(on_screen, stars))
我们不断生成长短不一运动的短线,看起来就像流星一样✨
二、3D空间
首先,我们定义我们的坐标系,z粥朝向我们,x轴朝右,就和pygame窗口的x轴一样,y轴朝上,和pygame窗口的y轴方向相反。
投影:平行投影
投影最简单的办法——丢弃z坐标
def parallel_project(vector3):
return(vector3.x, vector3.y)
这种方法简单,但得到的效果很不理想,基本上看不出透视效果。
投影:立体投影
这种投影的效果更接近真实,在它利用透视法把远处的物体缩小了。游戏中广泛应用。
def perspective_project(vector3, d):
x, y, z = vector3
return (x*d/z, -y*d/z)
由于近大远小,所以我们的结果除以一个z,同时,这里考虑了视距d(观察者到屏幕的距离),如下图。
类比:可以想象通过一个孔观察事物,当眼睛远离孔(视距增大)时,视野(fov)减小,看到的内容自然就变少了,当眼睛靠近孔时,视距减小,视野增大,看到的内容增多了。
类比:可以把z看作观察者到被观测物体之间的观测距离,可以看成照相机到物体的距离,当距离z增大时,看到的物体变小,符合透视原理。
视距:计算视距
from math import tan
def calculate_viewing_distance(fov, screen_width):
d = (screen_width/2.0) / tan(fov/2.0)
return d
三、绘制一个空间图形
我们可以根据上面所学的,结合数学基础,可以绘制一个3D图形,关键代码如下:
# 绘制点
for point in points:
x, y, z = point - camera_position
x = x * viewing_distance / z
y = -y * viewing_distance / z
x += center_x
y += center_y
screen.blit(ball, (x-ball_center_x, y-ball_center_y))
在这里,我们先定义了一个相机的位置camera_position,它代表了观测者的位置,初始值为(0, 0, -700),因为要除以距离z,所以我们先将z设的比较大(因为如果z很小,如接近0,则(x, y)就接近(∞, ∞)了),之后我们将(x, y)加上center_y, center_y,目的是吧图形绘制在屏幕中央,最后根据ball的尺寸将球绘制在对于的位置。
完整代码如下:
import pygame
from pygame.locals import *
from gameobjects.vector3 import Vector3
from math import *
from random import randint
SCREEN_SIZE = (640, 480)
CUBE_SIZE = 300
def caculate_viewing_distance(fov, screen_width):
d = (screen_width/2.0) / tan(fov/2.0)
return d
def run():
pygame.init()
screen = pygame.display.set_mode(SCREEN_SIZE, 0, 32)
my_font = pygame.font.SysFont("arial", 23)
ball = pygame.image.load("ball.png")
# 3D points
points = []
fov = 90.0 # Field of view
viewing_distance = caculate_viewing_distance(radians(fov), SCREEN_SIZE[0])
# 正方体边上的点,每条边16个点
for x in range(0, CUBE_SIZE+1, 20):
edge_x = (x == 0 or x == CUBE_SIZE)
for y in range(0, CUBE_SIZE+1, 20):
edge_y = (y ==0 or y == CUBE_SIZE)
for z in range(0, CUBE_SIZE+1, 20):
edge_z = (z == 0 or z == CUBE_SIZE)
# 以(0, 0, 0)为顶点的正方体边的坐标性质
if sum((edge_x, edge_y, edge_z)) >= 2:
# 移动到以(0, 0, 0)为中心
point_x = float(x) - CUBE_SIZE/2
point_y = float(y) - CUBE_SIZE/2
point_z = float(z) - CUBE_SIZE/2
point = Vector3(point_x, point_y, point_z)
points.append(point)
# 以point_z从大到小排序
def point_z(point):
return point.z
points.sort(key=point_z, reverse=True)
center_x, center_y = SCREEN_SIZE
center_x /= 2
center_y /= 2
ball_w, ball_h = ball.get_size()
ball_center_x = ball_w / 2
ball_center_y = ball_h / 2
camera_position = Vector3(0.0, 0.0, -700.0)
camara_speed = Vector3(300.0, 300.0, 300.0)
clock = pygame.time.Clock()
while True:
for event in pygame.event.get():
if event.type == QUIT:
return
screen.fill((0, 0, 0))
pressed_keys = pygame.key.get_pressed()
time_passed = clock.tick()
time_pass_seconds = time_passed / 1000.0
direction = Vector3()
if pressed_keys[K_LEFT]:
direction.x = -1.0
elif pressed_keys[K_RIGHT]:
direction.x = +1.0
if pressed_keys[K_UP]:
direction.y = +1.0
elif pressed_keys[K_DOWN]:
direction.y = -1.0
if pressed_keys[K_q]:
direction.z = +1.0
elif pressed_keys[K_a]:
direction.z = -1.0
# fov大小有范围 0-179
if pressed_keys[K_w]:
fov = min(179.0, fov+0.2)
w = SCREEN_SIZE[0]
viewing_distance = caculate_viewing_distance(radians(fov), w)
elif pressed_keys[K_s]:
fov = max(1.0, fov-0.2)
w = SCREEN_SIZE[0]
viewing_distance = caculate_viewing_distance(radians(fov), w)
camera_position += direction * camara_speed * time_pass_seconds
# 绘制点
for point in points:
x, y, z = point - camera_position
x = x * viewing_distance / z
y = -y * viewing_distance / z
x += center_x
y += center_y
screen.blit(ball, (x-ball_center_x, y-ball_center_y))
# 绘制表
diagram_width = SCREEN_SIZE[0] / 4
col = (50, 255, 50)
diagram_points = []
diagram_points.append((diagram_width/2, 100+viewing_distance/4))
diagram_points.append( (0, 100) )
diagram_points.append( (diagram_width, 100) )
diagram_points.append( (diagram_width/2, 100+viewing_distance/4) )
diagram_points.append( (diagram_width/2, 100) )
pygame.draw.lines(screen, col, False, diagram_points, 2)
# 绘制文字
white = (255, 255, 255)
cam_text = my_font.render("camera = "+str(camera_position), True, white)
screen.blit(cam_text, (5, 5))
fov_text = my_font.render("field of view = %i"%int(fov), True, white)
screen.blit(fov_text, (5, 35))
txt = "viewing distance = %.3f"%viewing_distance
d_text = my_font.render(txt, True, white)
screen.blit(d_text, (5, 65))
pygame.display.update()
if __name__ == "__main__":
run()
下面就是我们最终的效果啦:
第 6 篇pygame学习笔记完结 cheers! ?
参考博客:用Python和Pygame写游戏-从入门到精通
图标引用:Iconfont