Make a new paper airplane .

新型「紙ヒコーキ」を作るアニメです。
Python ソースコードを公開します。(2023.06.19 by Kero, 08.02 revised by Kero)
“blit=True”で描画速度を早める改訂をしました。(2023.08.10 by Kero)
コードを実行すると、画像の視点を変えることができますが、”blit=True”の副作用でおかしな絵が出ます。gifファイルは無事です。”blit=True”を削除するとまともな絵になります。
Matplotlib 3D の陰影処理にはクセがあり、複数の面の場合、同じ方向から描かないと陰影が不自然になります。それを解消するために、悪戦苦闘し、コードがメチャクチャになりました。

plane-1
plane-2
# paperplane-final-230815.py    2023.08.15  revised by Kero
# 最初に長方形出現。
# paperplane-final-230802.py    2023.08.02  revised by Kero
# 完成後の発進を加えました。
# paperplane-final-230619.py    2023.06.19  by Kero
# 新型紙ヒコーキの機体を作ります。
# ジェットエンジンとして左右対称に「渦巻きせんべい」をアニメで作ります。
# 接線の傾きをらせんの回転角とします。
# 各点における接線の傾きは点iから点i-1の傾きとします。
# 陰影を統一するためにコーディングがぐちゃぐちゃになりました。

import sys
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import ArtistAnimation
from mpl_toolkits.mplot3d import Axes3D
pi = np.pi

fig = plt.figure(figsize=(8, 8)) # 描画サイズを設定。
ax = fig.add_subplot(111, projection='3d') # 3Dグラフ表示します。
ax.set_box_aspect((20,12,5)) # 軸のアスペクト比。
# ax.view_init(elev=5, azim=95) # 飛び去る姿を見るように初期視点を設定。
ax.view_init(elev=-20) # 下からエンジンを巻く様子を見る初期視点設定。

ax.set_xlim(-90,90)
ax.set_ylim(0,100)
ax.set_zlim(-10,25)

artists = []

# 空中に四角形を出現させます。
kz = 30 # 大きくする動画のコマ数
rectx = np.array([[-75,-75],[75,75]]) / kz
recty = np.array([[0,100],[0,100]]) / kz
rectz = np.array([[0,0],[0,0]])
for big in range(kz+1):
    rect = [ax.plot_surface(rectx*big, recty*big, rectz, color="gold")]
    artists.append(rect)

# 紙ヒコーキの機体の右半分を作り、対称物(左半分)を作り、くっつけます。
# まず、右半分です。
x1 = np.array([[0, 0],[7.5, 7.5]])
y1 = np.array([[0, 24.2],[0, 27]])
z1 = np.array([[0, 0],[0, 0]])
x2 = np.array([[0, 0],[75, 75]])
y2 = np.array([[24.2, 75],[52, 90]])
z2 = z1
x3 = np.array([[0, 0],[50, 50]])
y3 = np.array([[75, 100],[85, 100]])
z3 = z1

# 右の垂直尾翼部
x4 = np.array([[50, 50],[75, 75]])
y4 = np.array([[85,100],[90, 100]])
z4 = z1

# 立体化します。
body1 = [ax.plot_surface(x1,y1,z1, color="gold")]
body2 = [ax.plot_surface(x2,y2,z2, color="gold")]
body3 = [ax.plot_surface(x3,y3,z3, color="gold")]
body4 = [ax.plot_surface(x4,y4,z4, color="gold")]
bodyR = body1 + body2 + body3

# 機体の左半分を作ります。以下のようにしないと、陰影が逆になります。
x1r = -x1[::-1]
x2r = -x2[::-1]
x3r = -x3[::-1]
x4r = -x4[::-1]
y1r = y1[::-1]
y2r = y2[::-1]
y3r = y3[::-1]
y4r = y4[::-1]

body1r = [ax.plot_surface(x1r,y1r,z1, color="gold")]
body2r = [ax.plot_surface(x2r,y2r,z2, color="gold")]
body3r = [ax.plot_surface(x3r,y3r,z3, color="gold")]
body4r = [ax.plot_surface(x4r,y4r,z4, color="gold")]
bodyL = body1r + body2r + body3r
bodyRL = bodyR + bodyL # 機体の完成。垂直尾翼は動くので後で設定。

# エンジン部分をアニメーションで作ります。
ki = 150 # エンジン部キザミ数
enu = 27 # エンジン部展開図の対称線側長さ
ens = 52 # エンジン部展開図の外側長さ
enw = 67.5 # エンジン部展開図の幅
btw = 7.5 # エンジン間距離/2
biu = 15 # 尾翼内側長さ
bis = 10 # 尾翼外側長さ
biw = 25 # 尾翼幅

# 正面図で考える。
y = []
yr = [] # 左のエンジンを描くため。
yi = np.linspace(enu, ens, ki+1) # エンジン部後端のy座標。
yir = yi[::-1] # 左エンジン関連。
for j in range(ki+1):
    y.append([0.0, yi[j]]) # エンジン部キザミごとの前端後端「y座標のセット」。
    yr.append([yir[j], 0.0]) # 左エンジン関連。
yr = yr[::-1] # 左エンジン関連。


sp = enw / ki # キザミ幅
xi = np.linspace(btw, btw + enw, ki+1) # 各点のx座標の初期値。
zi = [0.0] * (ki+1) # 各点のz座標の初期値。すべてゼロ。

do = [0.0] * (ki+1) # 回転角度を予め計算しておく。最初はゼロ。
ri = sp / 2 # 点x[ki]とらせん中心(仮想)との距離。
for i in range(ki-1, -1, -1):
    do[i] = np.pi/2 - np.arctan2(ri, sp) # x[i]を中心としてx[i+1]がdo[i]だけ回転。
    ri = np.sqrt(np.square(ri) + np.square(sp)) # 点x[i-1]とらせん中心との距離。


for eachx in range(ki+1, 0, -1): # らせん部分を右端から左に増やしていく。
    x = [] # 最初は空っぽ。
    xr = []
    z = [] # 同上。
    for mag in range(ki,eachx-1, -1): # すでにある程度巻いていると考える。
        # らせん上の点(xi[mag],zi[mag])をxi[eachx-1]を中心にdo[eachx-1]回転させる。
        xsa = xi[mag] - xi[eachx-1] # x座標の差。
        zsa = zi[mag] # z座標の差。
        the = np.arctan2(zsa, xsa) # 回転前のらせん上の点とx軸との角度。
        kyo = np.sqrt(np.square(xsa) + np.square(zsa)) # らせん上の点とxi[eachx-1]との距離。 
        kai = the + do[eachx -1] # 回転後のらせん上の点とx軸との角度。
        xi[mag] = xi[eachx-1] + kyo * np.cos(kai) # 回転後のx座標値。
        zi[mag] = zi[eachx-1] + kyo * np.sin(kai) # 回転後のz座標値。
    for j in range(0, ki+1):
        x.append([xi[j],xi[j]]) # 線上の点からplot_surface用の「x座標のセット」を作る。
        z.append([zi[j],zi[j]]) # 線上の点からplot_surface用の「z座標のセット」を作る。
        xr.append([-xi[j],-xi[j]])
    x = np.array(x) # xはリストなので、数値配列に変換。
    z = -np.array(z) # zはリストなので、数値配列に変換し、負の値にする。
    xr = np.array(xr) # 左エンジン関連。
    
    right = [ax.plot_surface(x, y, z, color='gold')] # 各「座標のセット」が揃ったので、描画。
        # 描画物は生のオブジェクトなので、[ ]でくくって「リスト」にする。
    # 左エンジンは、xrとyrを用意しないと陰影が逆になる。
    left =  [ax.plot_surface(xr, yr, z, color='gold')]
    artists.append(right + left + bodyRL + body4 + body4r) # それらをartistsに追加する。
    if eachx == ki+1: # 最初、長方形の紙をじっくり見てもらう。
        for re in range(20):
            artists.append(right + left + bodyRL + body4 + body4r)
    if eachx == 1: # エンジンを形成した後、垂直尾翼を立てる。
        for kz in np.linspace(0, pi/2, 31):
            bx = 25 * np.cos(kz) + 50
            bz = 25 * np.sin(kz)
            x4 = np.array([[50, 50],[bx, bx]])
            y4 = np.array([[85,100],[90, 100]])
            z4 = np.array([[0, 0],[bz, bz]])
            x4r = np.array([[-50, -50],[-bx, -bx]]) # 左尾翼関連。
            y4r = np.array([[100, 85],[100, 90]]) # 左尾翼関連。
            body4 = [ax.plot_surface(x4,y4,z4, color="gold")]
            body4r = [ax.plot_surface(x4r,y4r,z4, color="gold")] # 左尾翼関連。
            artists.append(right + left + bodyRL + body4 + body4r)

# 垂直尾翼が立ったら、前進(y座標を負の方向に)させる。
            if kz == pi/2:
                y = np.array(y)
                yr = np.array(yr)
                for td in range(61):
                    yd = 1.0 * td**2
                    y  = y  - yd
                    yr = yr - yd
                    y1 = y1 - yd
                    y2 = y2 - yd
                    y3 = y3 - yd
                    y4 = y4 - yd
                    y1r = y1r - yd
                    y2r = y2r - yd
                    y3r = y3r - yd
                    y4r = y4r - yd
                    
                    body1 = [ax.plot_surface(x1,y1,z1, color="gold")]
                    body2 = [ax.plot_surface(x2,y2,z2, color="gold")]
                    body3 = [ax.plot_surface(x3,y3,z3, color="gold")]
                    body4 = [ax.plot_surface(x4,y4,z4, color="gold")]
                    bodyR = body1 + body2 + body3 + body4

                    body1r = [ax.plot_surface(x1r,y1r,z1, color="gold")]
                    body2r = [ax.plot_surface(x2r,y2r,z2, color="gold")]
                    body3r = [ax.plot_surface(x3r,y3r,z3, color="gold")]
                    body4r = [ax.plot_surface(x4r,y4r,z4, color="gold")]
                    bodyL = body1r + body2r + body3r+ body4r
                    bodyRL = bodyR + bodyL

                    right = [ax.plot_surface(x, y, z, color='gold')]
                    left =  [ax.plot_surface(xr, yr, z, color='gold')]

                    artists.append(right + left + bodyRL)
#                    if td == 60:
#                        for tome in range(2):
#                            artists.append(right + left + bodyRL)
                    
anim = ArtistAnimation(fig, artists, interval=40, blit=True) # アニメ描画。
anim.save('paperplane-final-230815-1.gif', writer='pillow', dpi=200) # gifファイルとして保存。

plt.show() # 表出。