3D世界
让我们现在开始写一个3D的程序,巩固一下这几次学习的东西。因为我们还没有好好深入如何画3D物体,暂时就先用最简单的投影(上次讨论过的第二种)方法来画吧。这个程序画一个空间里的立方体,只不过各个部分并不会随着距离而产生大小上的变化。
您可以看到,很多的小球构成了立方体的各个边,通过按住方向键,可以水平或垂直方向的更改“摄像头”的位置,Q和A键会把摄像头拉近或拉远,而W和S会改变视距,绿色的三角是视距和视角的示意图。fov角大的话,立方体就显得比较短,反之就显得比较长。
代码稍微有点长,下面有解释,静下心来慢慢阅读。
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 calculate_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)
default_font = pygame.font.get_default_font()
font = pygame.font.SysFont(default_font, 24)
ball = pygame.image.load("ball.png").convert_alpha()
# 3D points
points = []
fov = 90. # Field of view
viewing_distance = calculate_viewing_distance(radians(fov), SCREEN_SIZE[0])
# 边沿的一系列点
for x in xrange(0, CUBE_SIZE+1, 20):
edge_x = x == 0 or x == CUBE_SIZE
for y in xrange(0, CUBE_SIZE+1, 20):
edge_y = y == 0 or y == CUBE_SIZE
for z in xrange(0, CUBE_SIZE+1, 20):
edge_z = z == 0 or z == CUBE_SIZE
if sum((edge_x, edge_y, edge_z)) >= 2:
point_x = float(x) - CUBE_SIZE/2
point_y = float(y) - CUBE_SIZE/2
point_z = float(z) - CUBE_SIZE/2
points.append(Vector3(point_x, point_y, point_z))
# 以z序存储,类似于css中的z-index
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.)
camera_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_passed_seconds = time_passed / 1000.
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
if pressed_keys[K_w]:
fov = min(179., fov+1.)
w = SCREEN_SIZE[0]
viewing_distance = calculate_viewing_distance(radians(fov), w)
elif pressed_keys[K_s]:
fov = max(1., fov-1.)
w = SCREEN_SIZE[0]
viewing_distance = calculate_viewing_distance(radians(fov), w)
camera_position += direction * camera_speed * time_passed_seconds
# 绘制点
for point in points:
x, y, z = point - camera_position
if z > 0:
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 = font.render("camera = "+str(camera_position), True, white)
screen.blit(cam_text, (5, 5))
fov_text = font.render("field of view = %i"%int(fov), True, white)
screen.blit(fov_text, (5, 35))
txt = "viewing distance = %.3f"%viewing_distance
d_text = font.render(txt, True, white)
screen.blit(d_text, (5, 65))
pygame.display.update()
if __name__ == "__main__":
run()
上面的例子使用Vector3来管理向量数据,点的存储是按照z坐标来的,这样在blit的时候,离摄像机近的后画,就可以覆盖远的,否则看起来就太奇怪了……
在主循环的代码中,会根据按键摄像头会更改位置——当然这是用户的感觉,实际上代码做的是更改了点的位置。而3D的移动和2D是非常像的,只不过多了一个z来判断覆盖远近(也就是绘制顺序),一样的基于时间移动,一样的向量运算,只是由Vector2变为了Vector3。
然后代码需要绘制全部的点。首先,点的位置需要根据camera_position变量校正,如果结果的z比0还大,说明点在摄像头之前,需要画的,否则就是不需要画。y需要校准一下方向,最后把x、y定位在中间(小球还是有一点点尺寸的)。
留下的代码是给出信息的,都是我们学习过的东西了。
如果想好好学习,把立方体换成其他的图像吧!改一下更能加深印象。
3D第一部分总结
3D是迄今为止游戏发展中最大的里程碑(下一个会是什么呢?虚拟体验?),我们这几次学习的,是3D的基础,你可以看到,仅有2D绘图经验也能很方便的过渡过来。仅仅是Vector2→Vector3,担任3D向量还是有一些特有的操作的,需要慢慢学习,但是基本的思想不会变。
但是,请继续思考~ 难道所有的3D游戏就是一系列的3D坐标再手动转换为2D画上去就好了?很可惜或者说很幸运不是的,我们有3D引擎来做这些事情,对Pygame来说,原生的3D处理时不存在的,那么如何真正绘制3D画面?有一个非常广泛的选择——OpenGL,不了解的请自行Wiki,不过OpenGL并不是Pygame的一部分,而且3D实际做下去实在很繁杂,这个系列就暂时不深入讲解了。
尽管有3D引擎帮我们做投影等事情,我们这几次学的东西绝对不是白费,对基础的一定了解可以更好的写入3D游戏,对画面的掌握也会更好。如果有需要,这个系列的主线完成后,会根据大家的要求追加讲解OpenGL的内容。
接下来开始讲解Pygame中的声音,基本游戏制作的全部知识我们都快学习完了:)
>> 用Python和Pygame写游戏-从入门到精通(20)
可以给力讲讲openGL啊,想问下opengl和dx那个好入门些啊 楼主比较认真啊 精神值得赞扬
有时间拉一个合适的3D引擎进来写写吧
这一次忘了ball.png了,看来我得自己去找一个^_^
想了很久没想通viewing_distance是怎么回事……感觉跟camera的z值不是一回事吗?好像钻牛角尖里了
觉得这样理解比较容易, viewing_distance 是之前的函数 perspective_project 中出现的
def perspective_project(vector3, d):
x, y, z = vector3
return (x * d/z, –y * d/z)
它用来把三维物体投影到一个平面上的,而 camera_z 是距离这个平面(物体)的距离。 在这个例子中,增大 fov 相当于跟投影平面距离变近,调整 camera_z也会也有类似的感觉。
@wing:个人认为viewing_distance跟camera_z不是一回事。viewing_distance是眼睛到屏幕的距离,而camera_z是眼睛到离眼睛最近的小球的距离(这个距离只是z方向的距离不是用勾股定理算的直线距离)。可以把屏幕想象成一块透明玻璃,你在透过这块玻璃看前方的一个方块。
确实是3D的基础课,对以后学3D引擎大有帮助的。那FOV不仅有左右,还有上下的,是个圆锥。楼主肯定知道,没有讲罢了。
Pingback: 用Python和Pygame写游戏-从入门到精通(19)-演道网
作者能否提供ball.png图片?
python3下运行总有问题…
….line 92, in _get_z
return self._v[2]
TypeError: ‘map’ object is not subscriptable
libpng warning: iCCP: known incorrect sRGB profile
请问解决了吗?我也遇到这个问题了
吧vector3.py中
self._v = map(float, args[:3])改为
self._v = list(map(float, args[:3]))
self._v = map(float, args[0][:3])改为
self._v = list(map(float, args[0][:3]))就可以了
今天看到我以前发的评论啊,光阴似箭啊,我对这句不太理解: x, y, z = point – camera_position,为什么要减去摄像机的坐标呢,摄像机就相当于眼睛吧。
个人理解
x, y, z = point – camera_position
得到的坐标是以camera为原点(0,0,0),立方体棱上某个点相对原点的位置
程序开始未移动时 camera的位置是(0,0,-700),如立方体顶点A(-150,-150,-150),转换后点A相对原点的位置变为(-150,-150,550)