3D世界
让我们现在开始写一个3D的程序,巩固一下这几次学习的东西。因为我们还没有好好深入如何画3D物体,暂时就先用最简单的投影(上次讨论过的第二种)方法来画吧。这个程序画一个空间里的立方体,只不过各个部分并不会随着距离而产生大小上的变化。
您可以看到,很多的小球构成了立方体的各个边,通过按住方向键,可以水平或垂直方向的更改“摄像头”的位置,Q和A键会把摄像头拉近或拉远,而W和S会改变视距,绿色的三角是视距和视角的示意图。fov角大的话,立方体就显得比较短,反之就显得比较长。
代码稍微有点长,下面有解释,静下心来慢慢阅读。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
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)