作者 | 天元浪子

来源 | Python作业辅导员

农历的三月初三,相传这一天是轩辕黄帝的诞辰日。春秋时期,三月初三的纪念活动还是非常隆重的,至魏晋则演变为达官显贵、文人雅士临水宴饮的节日。兰亭序中提到的"曲水流觞",也许就是这一习俗的写照吧(个人猜想,未经考证)。唐以后,三月初三渐渐湮没于历史的长河中。

于我而言,三月初三却是一个放风筝的日子。每逢这一天,耳边总会响起一首老歌:又是一年三月三,风筝飞满天……上班路上,看道路两侧草长莺飞、杨柳拂面,一时玩心顿起:何不用3D构造一个天上白云飘飘,地上绿草茵茵的虚幻空间,在里面放飞几只风筝自娱自乐呢?

心动不如行动。打开Python的IDLE,经过一番尝试,竟然轻松在一片辽阔的草原上放飞了几只风筝。风筝们迎风飘动,长长的风筝线像悬链一样跟着摆动。拖动鼠标,还可以从不同的角度、距离欣赏,恍若置身于大草原上。

如果觉得好玩,就跟我一起到草原放风筝吧。先说好了,你可以搭我的便车,食宿请自理。不多说了,快上车!

原材料

Python环境和模块

一台安装了Python环境的电脑,Python环境需要安装以下模块。

  • numpy

  • scipy

  • pillow

  • wxgl

如果没有上述模块,请参考下面的命令安装。我刚刚升级了wxgl模块(从0.6.3升级到0.6.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完全手册》。

草原和风筝素材

请下载下面的草原和风筝素材,保存到项目路径下的res文件夹中。如果使用其他图片,请保持草原图片的宽高比为4:3,风筝素材需要带透明通道的png格式。

草原素材:sky.jpg

风筝素材:butterfly.jpg

风筝素材:eagle.jpg

风筝素材:fish.jpg

打开IDLE,导入模块

>>> import numpy as np
>>> from PIL import Image
>>> import wxgl.wxplot as plt # 交互式3D绘图库
>>> from scipy.spatial.transform import Rotation # 空间旋转计算

制作工序

蓝天和草原

用3D绘制天空,最常用的方法是天空顶和天空盒。不过,这两个方法都有局限性,效果只能说差强人意。我们这里用的是天空盒。所谓天空盒,顾名思义,就是从一张图片上裁切出六个矩形,拼成一个六面体,观察者站在六面体内,就有了“天苍苍野茫茫”的赶脚。

下图是从上图裁切出的上下前后左右六个面。

了解了天空盒的原理,实现起来就简单多了。先来裁切上下前后左右六个面。

>>> 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]范围内。

>>> 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函数绘制天空盒了。

>>> 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个元素分别表示视景体的左、右、上、下面,以及前后面距离观察者的距离。

>>> 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()

天空盒最终的效果如下图所示。尝试拖动鼠标、滑动滚轮,你会发现天空盒的缺陷。不过,这不会影响我们放飞风筝。

为了方便后续操作,我们将绘制天空盒的代码封装成一个函数。

>>> 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)
>>>

第一只风筝

现在观察者位于(0.8,0,0)的位置,假定风筝中心位于v1点(-0.5,-0.3,0.2)的位置(观察者左前上方)。我们需要根据风筝素材的尺寸,确定风筝在空间中的坐标。

>>> 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()

至此,终于在草原上放飞了第一只风筝。

给风筝加上线

风筝线近似于一条悬链线,我们可以用三次曲线模拟。如果放风筝的人在v0点,风筝中心位于v1点,风筝线就可以用k个点来描述。先来定义一个根据v0点和v1点计算风筝线的函数。

>>> 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*dxy = v1[1] - m*dyz = np.linspace(v1[2], v0[2], k)return x, y, z
>>>

重复一遍绘制天空盒和风筝的代码,稍加修改,即可加上风筝线。

>>> 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用于设置线宽。如果觉得风筝线不够明显,可以适当增加线宽。

让风筝动起来

想象一下风筝在天空中的飘动姿态,其运动轨迹有两个特点:

  • 水平方向延弧线摆动,幅度约30°左右

  • 摆动到左侧则左侧稍低,摆动到右侧则右侧稍低

据此,不难模拟出风筝的摆动轨迹,计算出运动轨迹线上每一处风筝的坐标,同时计算出对应的风筝悬链线。启动一个定时器,顺序显示轨迹线上每一处风筝及其悬链线,形成动画。

WxGL的plt.surface()函数和plt.plot()函数,支持通过参数slide=True将对应的模型放入一个动画序列,执行plt.show()的时候,会自动播放这个模型序列,时间间隔由plt.figure()函数的interval参数决定,默认值100毫秒。如果多个模型需要同时显示,只需要用name参数为多个模型指定相同的名字即可。

好,我们来定义一个绘制飘动风筝的函数。

>>> 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)
>>>

调用一下试试看。

>>> 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°]的范围内左右摆动,悬链线也跟着一起飘动。

放飞更多的风筝

现在,我们有三张风筝的图片,把它们都放飞到天空盒中吧。至于风筝的位置、放飞者的位置,你可以根据自己的想象,随意定义。

>>> 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()

至此,大功告成。

完整源码

# -*- 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*dxy = v1[1] - m*dyz = np.linspace(v1[2], v0[2], k)return x, y, zdef draw_kite(fn, v0, v1, dh=0.03, ex=(-20,20), fs=160):"""绘制风筝fn      - 风筝图片文件名(png格式,带透明通道)dh      - 风筝后仰高度,默认0.02ex      - 风筝左右摆动的角度范围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()

60+专家,13个技术领域,CSDN 《IT 人才成长路线图》重磅来袭!
直接扫码或微信搜索「CSDN」公众号,后台回复关键词「路线图」,即可获取完整路线图!更多精彩推荐
☞270亿参数的“中文版GPT-3”来了!阿里达摩院发布超大规模语言模型PLUG☞微软每年豪砸安全研发 10 亿美元,聊聊背后的技术密码☞3D 建模费时费力,Python 让照片秒变模型点分享点收藏点点赞点在看

足不出户也能放风筝?OpenGL 一招搞定!相关推荐

  1. 十招搞定SQL2K安全

    十招搞定SQL2K安全 本文详述提高SQL Server 2K安装的安全性实施的十个注意事项:  1.安装最新的服务包 为了提高服务器安全性,最有效的一个方法就是升级到 SQL Server 2000 ...

  2. 一招搞定高等数学! | 今日最佳

    世界只有3.14 % 的人关注了 青少年数学之旅 @瓜皮儿十三妹 @没品图 一张图让你们看看鲨鱼的 血液循环系统到底有多复杂 @普外科曾医生 小猪佩奇其实是"巨猪佩奇" 小猪佩奇真 ...

  3. 简单一招搞定公司牛人 转自 潘文富

    简单一招搞定公司牛人 潘文富 所谓公司牛人,就是在老板之下,众员工之上的人物.公司牛人,有的是凭借资历,有的是身居高位的职业经理人,有的是凭借自己在某方面的专业水平和经验,总之,有牛的资本. 这些牛人 ...

  4. 企业成本费用空缺如何解决?享受核定政策一招搞定!

    本文作者:财税小喇叭 成本费用是一个公司账务板块中非常重要的部分之一,成本费用空缺通常会出现以下两种税负问题: 1·缺少普通的成本费用发票会导致企业利润虚高:所得税税负压力较大: 2·专票进项空缺的情 ...

  5. 怎么复制网页上不能复制的文字(付费文档免费复制),一招搞定

    好多小伙伴上网查资料的时候,想要复制网页内容,但是提示付费复制或者不允许复制,遇到这种情况怎么办呢?下面就是小编分享的一招搞定无法复制网页内容文字的方法. 怎么复制网页上不能复制的文字 借助360安全 ...

  6. 怎么样把计算机桌面的图标改小,怎样将电脑桌面图标变小_三招搞定桌面图标太小问题-系统城...

    电脑安装win10系统后发现桌面的图标太大,想要把这些图标变小,这要怎么操作?由于对操作界面都不熟悉,所以不懂怎么设置,别担忧,小编今天就来分享一种将电脑桌面图标变小的方法,感兴趣的快来试试. 具体方 ...

  7. 免费GIF动图制作,简简单单一招搞定

    免费的GIF动图制作,教你一招搞定,下面就给大家介绍一款好用的gif制作工具,在线一键制作gif动图. 我们在网络聊天中,表情包已经是不可分割的一部分,也是沟通的种的一个桥梁.详细我们每个人的手机里都 ...

  8. android手机做路由器,怎么让手机变成wifi路由器?一招搞定!

    原标题:怎么让手机变成wifi路由器?一招搞定! 当我们的在某些地方的时候需要上网,可电脑没有可用,或是家里的台式电脑没有无线网卡,这样才能让台式通过无线上网呢!等等问题,今天我们就来说说如何把智能手 ...

  9. 几招搞定如何发送招聘兼职通知面试短信

    许多的公司企业需要招聘兼职人员,但是确不太会写通知面试短信.而且招聘兼职通知也看准了群发短信成本低廉,覆盖面广,实时送达.直接命中目标客户等优势,纷纷开通短信平台.蝶信互联短信平台专注短信应用行业6年 ...

最新文章

  1. PostgreSQL — 安装
  2. [笔记]ndarray切片(python)
  3. 【流量】一觉醒来发现CSDN博客访问量增加十倍!原来是这个原因
  4. python 分析两组数据的差异_R语言limma包差异基因分析(两组或两组以上)
  5. fpga经典设计100例_“100例”—优秀产品设计曲线细节美图
  6. Android Animation实现元素在屏幕上按照指定轨迹运动,以及出现NullPointerException的解决方案
  7. 群同态基本定理证明_群论(7): 群代数, 群表示基础
  8. php 正则匹配静态资源,Struts2 配置静态资源文件不经过Strut处理(正则匹配)
  9. 企业实战05:Oracle数据库_操作表中数据
  10. GPT(Improving Language Understandingby Generative Pre-Training) 论文笔记
  11. while循环(包含死循环、while嵌套)
  12. linux命令最终篇
  13. matlab虚拟现实之工具介绍(修改)
  14. react实现div隐藏_在React中显示或隐藏元素
  15. QTTabBar+Office Tab+Quicker 助力高效使用Windows办公
  16. 铺瓷砖问题的C++实现
  17. php得到当前时间戳,php获取当前时间戳的方法
  18. [车]上海外地“苏牌”竟要万元
  19. C++著名库的比较和学习经验
  20. serviceBattery mac换电池 mac怎么换电池mac拆机

热门文章

  1. Inversion of Java Interview - 计算机网络篇
  2. SQL查询语句分步详解——多字段分组查询
  3. Sentinel-Redis高可用方案(二):主从切换
  4. DJ12-2 8086 系列指令系统(第三节课)
  5. 死磕 java同步系列之redis分布式锁进化史
  6. Java基础语法-注释的写法
  7. JS内置对象和数组方法
  8. swaks伪造钓鱼邮件
  9. sed删除代码空行和删去行尾空白
  10. 二本计算机考研简单吗,普通二本考研很难吗 哪些大学不收二本考研