python 模拟在天空中放风筝的示例代码


Posted in Python onApril 21, 2021

1 前言

昨天是农历的三月初三,相传这一天是轩辕黄帝的诞辰日。春秋时期,三月初三的纪念活动还是非常隆重的,至魏晋则演变为达官显贵、文人雅士临水宴饮的节日。兰亭序中提到的"曲水流觞",也许就是这一习俗的写照吧(个人猜想,未经考证)。唐以后,三月初三渐渐湮没于历史的长河中。
于我而言,三月初三却是一个放风筝的日子。每逢这一天,耳边总会响起一首老歌:又是一年三月三,风筝飞满天……上班路上,看道路两侧草长莺飞、杨柳拂面,一时玩心顿起:何不用3D构造一个天上白云飘飘,地上绿草茵茵的虚幻空间,在里面放飞几只风筝自娱自乐呢?
心动不如行动。打开Python的IDLE,经过一番尝试,竟然轻松在一片辽阔的草原上放飞了几只风筝。风筝们迎风飘动,长长的风筝线像悬链一样跟着摆动。拖动鼠标,还可以从不同的角度、距离欣赏,恍若置身于大草原上。

python 模拟在天空中放风筝的示例代码

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

2 原材料

2.1 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完全手册》。

2.2 草原和风筝素材

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

python 模拟在天空中放风筝的示例代码

风筝素材:butterfly.jpg

python 模拟在天空中放风筝的示例代码

风筝素材:eagle.jpg

python 模拟在天空中放风筝的示例代码

风筝素材:fish.jpg

python 模拟在天空中放风筝的示例代码

2.3 打开IDLE,导入模块

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

3 制作工序

3.1 蓝天和草原

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

python 模拟在天空中放风筝的示例代码

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

python 模拟在天空中放风筝的示例代码

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

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

咦?不对啊,为什么我在天空盒外而不是天空盒内呢?

python 模拟在天空中放风筝的示例代码

原来,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()

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

python 模拟在天空中放风筝的示例代码

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

>>> 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)的位置(观察者左前上方)。我们需要根据风筝素材的尺寸,确定风筝在空间中的坐标。

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

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

python 模拟在天空中放风筝的示例代码

3.3 给风筝加上线

风筝线近似于一条悬链线,我们可以用三次曲线模拟。如果放风筝的人在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*dx
        y = v1[1] - m*dy
        z = 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用于设置线宽。如果觉得风筝线不够明显,可以适当增加线宽。

python 模拟在天空中放风筝的示例代码

3.4 让风筝动起来

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

水平方向延弧线摆动,幅度约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°]的范围内左右摆动,悬链线也跟着一起飘动。

python 模拟在天空中放风筝的示例代码

3.5 放飞更多的风筝

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

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

至此,大功告成。

python 模拟在天空中放风筝的示例代码

4 完整源码

# -*- 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 模拟放风筝的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
python实现统计代码行数的方法
May 22 Python
python使用正则表达式匹配字符串开头并打印示例
Jan 11 Python
Python爬虫获取整个站点中的所有外部链接代码示例
Dec 26 Python
使用sklearn进行对数据标准化、归一化以及将数据还原的方法
Jul 11 Python
利用Python对文件夹下图片数据进行批量改名的代码实例
Feb 21 Python
numpy实现神经网络反向传播算法的步骤
Dec 24 Python
python实现加密的方式总结
Jan 19 Python
基于Python把网站域名解析成ip地址
May 25 Python
实现Python3数组旋转的3种算法实例
Sep 16 Python
python基于exchange函数发送邮件过程详解
Nov 06 Python
windows下python 3.9 Numpy scipy和matlabplot的安装教程详解
Nov 28 Python
Django给表单添加honeypot验证增加安全性
May 06 Python
如何使用Python对NetCDF数据做空间相关分析
python实现简单倒计时功能
python Polars库的使用简介
python基础之匿名函数详解
Apr 21 #Python
Python基础之字符串格式化详解
Apr 21 #Python
python 自动刷新网页的两种方法
python实现Thrift服务端的方法
You might like
javascript的内存管理详解
2013/08/07 Javascript
js调试系列 控制台命令行API使用方法
2014/06/18 Javascript
Javascript遍历table中的元素示例代码
2014/07/08 Javascript
兼容主流浏览器的JS复制内容到剪贴板
2014/12/12 Javascript
原生javascript实现Tab选项卡切换功能
2015/01/12 Javascript
jQuery下拉美化搜索表单效果代码分享
2015/08/25 Javascript
关于事件mouseover ,mouseout ,mouseenter,mouseleave的区别
2015/10/12 Javascript
基于AngularJs + Bootstrap + AngularStrap相结合实现省市区联动代码
2016/05/30 Javascript
javascript实现简易计算器
2017/02/01 Javascript
js实现省份下拉菜单效果
2017/02/15 Javascript
jQuery实现简单弹窗遮罩效果
2017/02/27 Javascript
为jquery的ajax请求添加超时timeout时间的操作方法
2018/09/04 jQuery
vue+iview 实现可编辑表格的示例代码
2018/10/31 Javascript
[04:11]DOTA2亚洲邀请赛小组赛第一日 TOP10精彩集锦
2015/01/30 DOTA
Python Sleep休眠函数使用简单实例
2015/02/02 Python
Python中自定义函数的教程
2015/04/27 Python
python处理二进制数据的方法
2015/06/03 Python
解决python使用open打开文件中文乱码的问题
2017/12/29 Python
Python基于递归算法实现的汉诺塔与Fibonacci数列示例
2018/04/18 Python
Python补齐字符串长度的实例
2018/11/15 Python
Python模块、包(Package)概念与用法分析
2019/05/31 Python
Python直接赋值、浅拷贝与深度拷贝实例分析
2019/06/18 Python
使用python模拟命令行终端的示例
2019/08/13 Python
Python获取统计自己的qq群成员信息的方法
2019/11/15 Python
python 已知一个字符,在一个list中找出近似值或相似值实现模糊匹配
2020/02/29 Python
Python使用进程Process模块管理资源
2020/03/05 Python
利用HTML5 Canvas API绘制矩形的超级攻略
2016/03/21 HTML / CSS
HTML最新标准HTML5总结(必看)
2016/06/13 HTML / CSS
法国时尚品牌乐都特瑞士站:La Redoute瑞士
2016/09/05 全球购物
公务员职务工作的自我评价
2013/11/01 职场文书
质检员的岗位职责
2013/11/15 职场文书
《晏子使楚》教学反思
2014/02/08 职场文书
党的群众路线剖析材料
2014/10/09 职场文书
运动会广播稿200字(10篇)
2014/10/12 职场文书
一般纳税人申请报告
2015/05/18 职场文书
利用nginx搭建RTMP视频点播、直播、HLS服务器
2022/05/25 Servers