1 前言
昨天是农历的三月初三,相传这一天是轩辕黄帝的诞辰日。春秋时期,三月初三的纪念活动还是非常隆重的,至魏晋则演变为达官显贵、文人雅士临水宴饮的节日。兰亭序中提到的"曲水流觞",也许就是这一习俗的写照吧(个人猜想,未经考证)。唐以后,三月初三渐渐湮没于历史的长河中。
于我而言,三月初三却是一个放风筝的日子。每逢这一天,耳边总会响起一首老歌:又是一年三月三,风筝飞满天……上班路上,看道路两侧草长莺飞、杨柳拂面,一时玩心顿起:何不用3d构造一个天上白云飘飘,地上绿草茵茵的虚幻空间,在里面放飞几只风筝自娱自乐呢?
心动不如行动。打开python的idle,经过一番尝试,竟然轻松在一片辽阔的草原上放飞了几只风筝。风筝们迎风飘动,长长的风筝线像悬链一样跟着摆动。拖动鼠标,还可以从不同的角度、距离欣赏,恍若置身于大草原上。
如果觉得好玩,就跟我一起到草原放风筝吧。先说好了,你可以搭我的便车,食宿请自理。不多说了,快上车!
2 原材料
2.1 python环境和模块
一台安装了python环境的电脑,python环境需要安装以下模块。
- numpy
- scipy
- pillow
- wxgl
如果没有上述模块,请参考下面的命令安装。我刚刚升级了wxgl模块(从0.6.3升级到0.6.4),如果此前有安装,请删除后再次安装.
1
2
3
4
|
pip install numpy
pip install scipy
pip install pillow
pip install wxgl
|
numpy和pillow是python旗下最常用的科学计算库和图像处理库,属于常用模块。wxgl是一个基于pyopengl的三维数据可视化库,以wx为显示后端,提供matplotlib风格的交互式应用模式,同时,也可以和wxpython无缝结合,在wx的窗体上绘制三维模型。关于wxgl的更多信息,请参阅我的另一篇博客《十分钟玩转3d绘图:wxgl完全手册》。
2.2 草原和风筝素材
请下载下面的草原和风筝素材,保存到项目路径下的res文件夹中。如果使用其他图片,请保持草原图片的宽高比为4:3,风筝素材需要带透明通道的png格式。
草原素材:sky.jpg
风筝素材:butterfly.jpg
风筝素材:eagle.jpg
风筝素材:fish.jpg
2.3 打开idle,导入模块
1
2
3
4
|
>>> import numpy as np
>>> from pil import image
>>> import wxgl.wxplot as plt # 交互式3d绘图库
>>> from scipy.spatial.transform import rotation # 空间旋转计算
|
3 制作工序
3.1 蓝天和草原
用3d绘制天空,最常用的方法是天空顶和天空盒。不过,这两个方法都有局限性,效果只能说差强人意。我们这里用的是天空盒。所谓天空盒,顾名思义,就是从一张图片上裁切出六个矩形,拼成一个六面体,观察者站在六面体内,就有了“天苍苍野茫茫”的赶脚。
下图是从上图裁切出的上下前后左右六个面。
了解了天空盒的原理,实现起来就简单多了。先来裁切上下前后左右六个面。
1
2
3
4
5
6
7
8
|
>>> im = np.array(image. open (r 'd:\temp\kite\res\sky.jpg' )) # 打开蓝天草原的图片
>>> u = im.shape[ 0 ] / / 3 # 天空盒(正六面体的棱长)
>>> im_top = im[:u, u: 2 * u, :]
>>> im_left = im[u: 2 * u, :u, :]
>>> im_front = im[u: 2 * u, u: 2 * u, :]
>>> im_right = im[u: 2 * u, 2 * u: 3 * u, :]
>>> im_back = im[u: 2 * u, 3 * u:, :]
>>> im_bottom = im[ 2 * u:, u: 2 * u, :]
|
再生成立方体的六个面在三维空间中的坐标,其中每个面用四个顶点表示,顶点按逆时针方向排列。立方体的棱长为2,也就是xyzz坐标都在[-1,1]范围内。
1
2
3
4
5
6
|
>>> vs_front = np.array([[ - 1 , - 1 , 1 ], [ - 1 , - 1 , - 1 ], [ - 1 , 1 , - 1 ], [ - 1 , 1 , 1 ]])
>>> vs_left = np.array([[ 1 , - 1 , 1 ], [ 1 , - 1 , - 1 ], [ - 1 , - 1 , - 1 ], [ - 1 , - 1 , 1 ]])
>>> vs_right = np.array([[ - 1 , 1 , 1 ], [ - 1 , 1 , - 1 ], [ 1 , 1 , - 1 ], [ 1 , 1 , 1 ]])
>>> vs_top = np.array([[ 1 , - 1 , 1 ], [ - 1 , - 1 , 1 ], [ - 1 , 1 , 1 ], [ 1 , 1 , 1 ]])
>>> vs_bottom = np.array([[ - 1 , - 1 , - 1 ], [ 1 , - 1 , - 1 ], [ 1 , 1 , - 1 ], [ - 1 , 1 , - 1 ]])
>>> vs_back = np.array([[ 1 , - 1 , 1 ], [ 1 , - 1 , - 1 ], [ 1 , 1 , - 1 ], [ 1 , 1 , 1 ]])
|
有了六个面的材质和顶点,就可以使用surface函数绘制天空盒了。
1
2
3
4
5
6
7
|
>>> plt.surface(vs_front, texture = im_front, alpha = false)
>>> plt.surface(vs_left, texture = im_left, alpha = false)
>>> plt.surface(vs_right, texture = im_right, alpha = false)
>>> plt.surface(vs_top, texture = im_top, alpha = false)
>>> plt.surface(vs_bottom, texture = im_bottom, alpha = false)
>>> plt.surface(vs_back, texture = im_back, alpha = false)
>>> plt.show()
|
咦?不对啊,为什么我在天空盒外而不是天空盒内呢?
原来,wxgl默认观察者距离坐标原点5个单位的距离,而天空盒在[-1,1]范围内,自然就处于天空盒外了。莫着急,只要设置一下画布函数plt.figure()的参数,就ok了。参数dist用于设置观察者距离观察目标的距离,配合方位角参数azimuth和仰角参数elevation,可以确定观察者位置;参数view用于设置视景体,view数组的6个元素分别表示视景体的左、右、上、下面,以及前后面距离观察者的距离。
1
2
3
4
5
6
7
8
|
>>> plt.figure(dist = 0.8 , view = [ - 1 , 1 , - 1 , 1 , 0.8 , 7 ], elevation = 0 , azimuth = 0 )
>>> plt.surface(vs_front, texture = im_front, alpha = false)
>>> plt.surface(vs_left, texture = im_left, alpha = false)
>>> plt.surface(vs_right, texture = im_right, alpha = false)
>>> plt.surface(vs_top, texture = im_top, alpha = false)
>>> plt.surface(vs_bottom, texture = im_bottom, alpha = false)
>>> plt.surface(vs_back, texture = im_back, alpha = false)
>>> plt.show()
|
天空盒最终的效果如下图所示。尝试拖动鼠标、滑动滚轮,你会发现天空盒的缺陷。不过,这不会影响我们放飞风筝。
为了方便后续操作,我们将绘制天空盒的代码封装成一个函数。
1
2
3
4
5
6
7
8
|
>>> def draw_sky_box():
plt.surface(vs_front, texture = im_front, alpha = false)
plt.surface(vs_left, texture = im_left, alpha = false)
plt.surface(vs_right, texture = im_right, alpha = false)
plt.surface(vs_top, texture = im_top, alpha = false)
plt.surface(vs_bottom, texture = im_bottom, alpha = false)
plt.surface(vs_back, texture = im_back, alpha = false)
>>>
|
3.2 第一只风筝
现在观察者位于(0.8,0,0)的位置,假定风筝中心位于v1点(-0.5,-0.3,0.2)的位置(观察者左前上方)。我们需要根据风筝素材的尺寸,确定风筝在空间中的坐标。
1
2
3
4
5
6
7
8
9
10
11
12
|
>>> im_kite = np.array(image. open (r 'd:\temp\kite\res\butterfly.png' )) # 打开风筝图片
>>> max_s = max (im_kite.shape) # 风筝的最长边
>>> dx, dy = 0.1 * im_kite.shape[ 0 ] / max_s, 0.1 * im_kite.shape[ 1 ] / max_s # 计算风筝在空间中的实际尺寸
>>> v1 = ( - 0.5 , - 0.3 , 0.2 ) # 风筝中心位置
>>> vs_kite = np.array([[dx, - dy, 0.03 ], [ - dx, - dy, 0 ], [ - dx,dy, 0 ], [dx,dy, 0.03 ]]) # 风筝四角的坐标,前端略高(后仰0.03)
>>> vs_kite[:, 0 ] + = v1[ 0 ] # 从原点移到v1点
>>> vs_kite[:, 1 ] + = v1[ 1 ] # 从原点移到v1点
>>> vs_kite[:, 2 ] + = v1[ 2 ] # 从原点移到v1点
>>> plt.figure(dist = 0.8 , view = [ - 1 , 1 , - 1 , 1 , 0.8 , 7 ], elevation = 0 , azimuth = 0 ) # 设置画布
>>> draw_sky_box() # 绘制天空盒
>>> plt.surface(vs_kite, texture = im_kite, alpha = true) # 绘制风筝(png格式需要使用透明通道)
>>> plt.show()
|
至此,终于在草原上放飞了第一只风筝。
3.3 给风筝加上线
风筝线近似于一条悬链线,我们可以用三次曲线模拟。如果放风筝的人在v0点,风筝中心位于v1点,风筝线就可以用k个点来描述。先来定义一个根据v0点和v1点计算风筝线的函数。
1
2
3
4
5
6
7
8
|
>>> def get_line(v0, v1, k = 300 ):
m = np.power(np.linspace( 0 ,k,k), 3 ) / (k * k * k)
dx, dy = v1[ 0 ] - v0[ 0 ], v1[ 1 ] - v0[ 1 ]
x = v1[ 0 ] - m * dx
y = v1[ 1 ] - m * dy
z = np.linspace(v1[ 2 ], v0[ 2 ], k)
return x, y, z
>>>
|
重复一遍绘制天空盒和风筝的代码,稍加修改,即可加上风筝线。
1
2
3
4
5
6
7
8
|
>>> v0 = ( 0.5 , 0.2 , - 1 ) # 放风筝的人在v0点
>>> v1 = ( - 0.5 , - 0.3 , 0.2 ) # 风筝中心位于v1点
>>> xs, ys, zs = get_line(v0, v1) # 计算风筝悬链线
>>> plt.figure(dist = 0.8 , view = [ - 1 , 1 , - 1 , 1 , 0.8 , 7 ], elevation = 0 , azimuth = 0 ) # 设置画布
>>> draw_sky_box() # 绘制天空盒
>>> plt.surface(vs_kite, texture = im_kite, alpha = true) # 绘制风筝
>>> plt.plot(xs, ys, zs, color = '#c0c0c0' , width = 0.3 ) # 绘制风筝悬链线
>>> plt.show()
|
plt.plot()函数用于绘制点或线,参数width用于设置线宽。如果觉得风筝线不够明显,可以适当增加线宽。
3.4 让风筝动起来
想象一下风筝在天空中的飘动姿态,其运动轨迹有两个特点:
水平方向延弧线摆动,幅度约30°左右
摆动到左侧则左侧稍低,摆动到右侧则右侧稍低
据此,不难模拟出风筝的摆动轨迹,计算出运动轨迹线上每一处风筝的坐标,同时计算出对应的风筝悬链线。启动一个定时器,顺序显示轨迹线上每一处风筝及其悬链线,形成动画。
wxgl的plt.surface()函数和plt.plot()函数,支持通过参数slide=true将对应的模型放入一个动画序列,执行plt.show()的时候,会自动播放这个模型序列,时间间隔由plt.figure()函数的interval参数决定,默认值100毫秒。如果多个模型需要同时显示,只需要用name参数为多个模型指定相同的名字即可。
好,我们来定义一个绘制飘动风筝的函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
>>> def draw_kite(fn, v0, v1, dh = 0.03 , ex = ( - 20 , 20 ), fs = 160 ):
im_kite = np.array(image. open (fn)) # 打开风筝图片
max_s = max (im_kite.shape) # 风筝的最长边
dx, dy = 0.1 * im_kite.shape[ 0 ] / max_s, 0.1 * im_kite.shape[ 1 ] / max_s # 计算风筝在空间中的实际尺寸
delta = np.hstack((np.linspace( - 0.03 , 0.03 , fs), np.linspace( 0.03 , - 0.03 , fs))) # 风筝左右摆动过程中的高度波动
theta = np.hstack((np.linspace(ex[ 0 ], ex[ 1 ], fs), np.linspace(ex[ 1 ], ex[ 0 ], fs))) # 风筝左右摆动的角度
vs_kite = np.array([[dx, - dy,dh], [ - dx, - dy, 0 ], [ - dx,dy, 0 ], [dx,dy,dh]]) # 风筝四角的坐标,前端略高(后仰)
vs_kite[:, 0 ] + = v1[ 0 ]
vs_kite[:, 1 ] + = v1[ 1 ]
vs_kite[:, 2 ] + = v1[ 2 ]
offset = np.random.randint( 0 , 2 * fs)
for i in range ( 2 * fs):
k = (i + offset) % ( 2 * fs)
rotator = rotation.from_euler( 'xyz' , [ 0 , 0 , theta[k]], degrees = true)
vs = rotator. apply (vs_kite)
vs[: 2 , 2 ] - = delta[k]
vs[ 2 :, 2 ] + = delta[k]
plt.surface(vs, texture = im_kite, alpha = true, slide = true, name = 'id_%d' % i)
xs, ys, zs = get_line(v0, ((vs[ 0 ][ 0 ] + vs[ 2 ][ 0 ]) / 2 ,(vs[ 0 ][ 1 ] + vs[ 2 ][ 1 ]) / 2 ,(vs[ 0 ][ 2 ] + vs[ 2 ][ 2 ]) / 2 ))
plt.plot(xs, ys, zs, color = '#c0c0c0' , width = 0.3 , slide = true, name = 'id_%d' % i)
>>>
|
调用一下试试看。
1
2
3
4
|
>>> plt.figure(dist = 0.8 , view = [ - 1 , 1 , - 1 , 1 , 0.8 , 7 ], elevation = 0 , azimuth = 0 , interval = 50 ) # 设置画布,动画间隔50毫秒
>>> draw_sky_box() # 绘制天空盒
>>> draw_kite(r 'd:\temp\kite\res\butterfly.png' , ( 0.5 , 0.2 , - 1 ), ( - 0.5 , - 0.3 , 0.2 )) # 绘制风筝
>>> plt.show()
|
和我们设想的一样,风筝在[-20°,20°]的范围内左右摆动,悬链线也跟着一起飘动。
3.5 放飞更多的风筝
现在,我们有三张风筝的图片,把它们都放飞到天空盒中吧。至于风筝的位置、放飞者的位置,你可以根据自己的想象,随意定义。
1
2
3
4
5
6
7
8
9
10
|
>>> plt.figure(dist = 0.8 , view = [ - 1 , 1 , - 1 , 1 , 0.8 , 7 ], elevation = 0 , azimuth = 0 , interval = 50 )
>>> draw_sky_box()
>>> draw_kite(r 'd:\temp\kite\res\butterfly.png' , ( 0.5 , 0.2 , - 1 ), ( - 0.5 , - 0.3 , 0.2 ))
>>> plt.show()
>>> plt.figure(dist = 0.8 , view = [ - 1 , 1 , - 1 , 1 , 0.8 , 7 ], elevation = 0 , azimuth = 0 , interval = 50 )
>>> draw_sky_box()
>>> draw_kite(r 'd:\temp\kite\res\butterfly.png' , ( 0.5 , 0.2 , - 1 ), ( - 0.5 , - 0.3 , 0.2 ))
>>> draw_kite(r 'd:\temp\kite\res\fish.png' , ( 0.3 , 0 , - 1 ), ( - 0.2 , - 0.1 , 0.05 ), ex = ( - 40 , 40 ))
>>> draw_kite(r 'd:\temp\kite\res\eagle.png' , ( 0.2 , 0.05 , - 1 ), ( - 0.6 , 0.5 , 0.35 ))
>>> plt.show()
|
至此,大功告成。
4 完整源码
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
|
# -*- coding: utf-8 -*-
import numpy as np
from pil import image
import wxgl.wxplot as plt # 交互式3d绘图库
from scipy.spatial.transform import rotation # 空间旋转计算
def draw_sky_box(fn):
"""绘制天空盒
fn - 图片文件名(宽高比4:3)
"""
im = np.array(image. open (fn)) # 打开资源图片
u = im.shape[ 0 ] / / 3 # 天空盒(正六面体的棱长)
# 裁切出天空盒6个面:上下前后左右
im_top = im[:u, u: 2 * u, :]
im_left = im[u: 2 * u, :u, :]
im_front = im[u: 2 * u, u: 2 * u, :]
im_right = im[u: 2 * u, 2 * u: 3 * u, :]
im_back = im[u: 2 * u, 3 * u:, :]
im_bottom = im[ 2 * u:, u: 2 * u, :]
# 定义天空盒六个面的顶点坐标,4个顶点按逆时针方向排列
vs_front = np.array([[ - 1 , - 1 , 1 ], [ - 1 , - 1 , - 1 ], [ - 1 , 1 , - 1 ], [ - 1 , 1 , 1 ]])
vs_left = np.array([[ 1 , - 1 , 1 ], [ 1 , - 1 , - 1 ], [ - 1 , - 1 , - 1 ], [ - 1 , - 1 , 1 ]])
vs_right = np.array([[ - 1 , 1 , 1 ], [ - 1 , 1 , - 1 ], [ 1 , 1 , - 1 ], [ 1 , 1 , 1 ]])
vs_top = np.array([[ 1 , - 1 , 1 ], [ - 1 , - 1 , 1 ], [ - 1 , 1 , 1 ], [ 1 , 1 , 1 ]])
vs_bottom = np.array([[ - 1 , - 1 , - 1 ], [ 1 , - 1 , - 1 ], [ 1 , 1 , - 1 ], [ - 1 , 1 , - 1 ]])
vs_back = np.array([[ 1 , - 1 , 1 ], [ 1 , - 1 , - 1 ], [ 1 , 1 , - 1 ], [ 1 , 1 , 1 ]])
# 绘制天空盒的六个面
plt.surface(vs_front, texture = im_front, alpha = false)
plt.surface(vs_left, texture = im_left, alpha = false)
plt.surface(vs_right, texture = im_right, alpha = false)
plt.surface(vs_top, texture = im_top, alpha = false)
plt.surface(vs_bottom, texture = im_bottom, alpha = false)
plt.surface(vs_back, texture = im_back, alpha = false)
def get_line(v0, v1, k = 300 ):
"""风筝线:从风筝底部到放飞者,近似悬链线
v0 - 放飞者坐标
v1 - 风筝底部系线处坐标
k - 描绘风筝线的点的数量,默认300点
"""
m = np.power(np.linspace( 0 ,k,k), 3 ) / (k * k * k)
dx, dy = v1[ 0 ] - v0[ 0 ], v1[ 1 ] - v0[ 1 ]
x = v1[ 0 ] - m * dx
y = v1[ 1 ] - m * dy
z = np.linspace(v1[ 2 ], v0[ 2 ], k)
return x, y, z
def draw_kite(fn, v0, v1, dh = 0.03 , ex = ( - 20 , 20 ), fs = 160 ):
"""绘制风筝
fn - 风筝图片文件名(png格式,带透明通道)
dh - 风筝后仰高度,默认0.02
ex - 风筝左右摆动的角度范围
fs - 风筝随风摆动的帧数
"""
im_kite = np.array(image. open (fn)) # 打开风筝图片
max_s = max (im_kite.shape) # 风筝的最长边
dx, dy = 0.1 * im_kite.shape[ 0 ] / max_s, 0.1 * im_kite.shape[ 1 ] / max_s # 计算风筝在空间中的实际尺寸
delta = np.hstack((np.linspace( - 0.03 , 0.03 , fs), np.linspace( 0.03 , - 0.03 , fs))) # 风筝左右摆动过程中的高度波动
theta = np.hstack((np.linspace(ex[ 0 ], ex[ 1 ], fs), np.linspace(ex[ 1 ], ex[ 0 ], fs))) # 风筝左右摆动的角度
vs_kite = np.array([[dx, - dy,dh], [ - dx, - dy, 0 ], [ - dx,dy, 0 ], [dx,dy,dh]]) # 风筝四角的坐标,前端略高(后仰)
vs_kite[:, 0 ] + = v1[ 0 ]
vs_kite[:, 1 ] + = v1[ 1 ]
vs_kite[:, 2 ] + = v1[ 2 ]
offset = np.random.randint( 0 , 2 * fs)
for i in range ( 2 * fs):
k = (i + offset) % ( 2 * fs)
rotator = rotation.from_euler( 'xyz' , [ 0 , 0 , theta[k]], degrees = true)
vs = rotator. apply (vs_kite)
vs[: 2 , 2 ] - = delta[k]
vs[ 2 :, 2 ] + = delta[k]
plt.surface(vs, texture = im_kite, alpha = true, slide = true, name = 'id_%d' % i)
xs, ys, zs = get_line(v0, ((vs[ 0 ][ 0 ] + vs[ 2 ][ 0 ]) / 2 ,(vs[ 0 ][ 1 ] + vs[ 2 ][ 1 ]) / 2 ,(vs[ 0 ][ 2 ] + vs[ 2 ][ 2 ]) / 2 ))
plt.plot(xs, ys, zs, color = '#c0c0c0' , width = 0.3 , slide = true, name = 'id_%d' % i)
if __name__ = = '__main__' :
plt.figure(dist = 0.8 , view = [ - 1 , 1 , - 1 , 1 , 0.8 , 7 ], elevation = 0 , azimuth = 0 , interval = 50 )
draw_sky_box( 'res/sky.jpg' )
draw_kite( 'res/butterfly.png' , ( 0.5 , 0.2 , - 1 ), ( - 0.5 , - 0.3 , 0.2 ))
draw_kite( 'res/fish.png' , ( 0.3 , 0 , - 1 ), ( - 0.2 , - 0.1 , 0.05 ), ex = ( - 40 , 40 ))
draw_kite( 'res/eagle.png' , ( 0.2 , 0.05 , - 1 ), ( - 0.6 , 0.5 , 0.35 ))
plt.show()
|
以上就是python 模拟在天空中放风筝的示例代码的详细内容,更多关于python 模拟放风筝的资料请关注服务器之家其它相关文章!
原文链接:https://mp.weixin.qq.com/s/DPmUMXfaFX-dvwAq9xF3Qw