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线程创建和终止实例代码
Jan 20 Python
numpy的文件存储.npy .npz 文件详解
Jul 09 Python
python 文本单词提取和词频统计的实例
Dec 22 Python
使用python绘制3维正态分布图的方法
Dec 29 Python
在Pycharm中将pyinstaller加入External Tools的方法
Jan 16 Python
pyqt实现.ui文件批量转换为对应.py文件脚本
Jun 19 Python
python把转列表为集合的方法
Jun 28 Python
python列表插入append(), extend(), insert()用法详解
Sep 14 Python
基于Python爬取爱奇艺资源过程解析
Mar 02 Python
keras绘制acc和loss曲线图实例
Jun 15 Python
python-图片流传输的思路及示例(url转换二维码)
Dec 21 Python
numba提升python运行速度的实例方法
Jan 25 Python
如何使用Python对NetCDF数据做空间相关分析
python实现简单倒计时功能
python Polars库的使用简介
python基础之匿名函数详解
Apr 21 #Python
Python基础之字符串格式化详解
Apr 21 #Python
python 自动刷新网页的两种方法
python实现Thrift服务端的方法
You might like
php 表单验证实现代码
2009/03/10 PHP
php数组函数序列之array_slice() - 在数组中根据条件取出一段值,并返回
2011/11/07 PHP
Symfony2获取web目录绝对路径、相对路径、网址的方法
2016/11/14 PHP
yii2实现 "上一篇,下一篇" 功能的代码实例
2017/02/04 PHP
防止xss和sql注入:JS特殊字符过滤正则
2013/04/18 Javascript
alert中断settimeout计时功能
2013/07/26 Javascript
JavaScript对内存分配及管理机制详细解析
2013/11/11 Javascript
JavaScript中的闭包介绍
2015/03/15 Javascript
javascript+HTML5的Canvas实现Lab单车动画效果
2015/08/07 Javascript
jQuery插件Validation快速完成表单验证的方式
2016/07/28 Javascript
Angular4编程之表单响应功能示例
2017/12/13 Javascript
vue实现拖拽的简单案例 不超出可视区域
2019/07/25 Javascript
Python中线程的MQ消息队列实现以及消息队列的优点解析
2016/06/29 Python
Python实现抓取网页生成Excel文件的方法示例
2017/08/05 Python
hmac模块生成加入了密钥的消息摘要详解
2018/01/11 Python
python 移动图片到另外一个文件夹的实例
2019/01/10 Python
Python神奇的内置函数locals的实例讲解
2019/02/22 Python
pytorch方法测试——激活函数(ReLU)详解
2020/01/15 Python
3种适用于Python的疯狂秘密武器及原因解析
2020/04/29 Python
结束运行python的方法
2020/06/16 Python
python将YUV420P文件转PNG图片格式的两种方法
2021/01/22 Python
Python实现微信表情包炸群功能
2021/01/28 Python
Python 爬取淘宝商品信息栏目的实现
2021/02/06 Python
用CSS3的box-reflect设置文字倒影效果的方法讲解
2016/03/07 HTML / CSS
css3实现画半圆弧线的示例代码
2017/11/06 HTML / CSS
钉钉企业内部H5微应用开发详解
2020/05/12 HTML / CSS
Clarks鞋澳大利亚官方网站:Clarks Australia
2019/12/25 全球购物
美国用餐电影院:Alamo Drafthouse Cinema
2020/01/23 全球购物
大学毕业生简单自荐信
2013/11/05 职场文书
《再别康桥》教学反思
2014/02/12 职场文书
致垒球运动员加油稿
2014/02/16 职场文书
优秀班主任事迹材料
2014/12/16 职场文书
2016年教师反腐倡廉心得体会
2016/01/13 职场文书
Ajax实现三级联动效果
2021/10/05 Javascript
Golang连接并操作MySQL
2022/04/14 MySQL
Golang 字符串的常见操作
2022/04/19 Golang