こんな人にオススメ
某有名カードゲームアニメのCM入り・終わりのような、逆さのピラミッドが回転するようなアニメーションってplotly
で作成できる?
ということで、今回は以下の記事で作成したplotly
の3Dピラミッドをアニイメーションを使って回転、某有名カードゲームアニメのCM入り・終わりのようなグラフを作成する。自己満。
-
-
【python&3Dピラミッド】plotlyで3次元ピラミッドを作成
続きを見る
今回のこのピラミッドシリーズは完全に思いつきで、お手洗いで用を足している最中にそういえばと思っただけ。まあ3Dグラフの練習にはなるとポジティブに捉える。
python環境は以下。
- Python 3.9.7
- numpy 1.21.3
- plotly 5.3.1
- plotly-orca 3.4.2
作成したコード全文
下準備
import numpy as np import plotly.graph_objects as go import plotly.io as pio
まずは下準備としてのimport
関連。plotly
にはpx
というライブラリもあるんだけど、こちらだとSurfaceプロットがサポートされていないのでgo
を使用。
-
-
【plotly&3D】px.scatter_3dとpx.line_3Dで3Dグラフを作成
続きを見る
-
-
【plotly&3D】goで3Dグラフを作成
続きを見る
px
の方がサクッとグラフができるけど、go
の方がカスタムに富んでいる。
グラフ保存用の関数
# グラフ保存用の関数 def save(fig, config, save_name): pio.orca.config.executable = '/Applications/orca.app/Contents/MacOS/orca' pio.write_html(fig, f"{save_name}.html", config=config,) pio.write_image(fig, f"{save_name}.png")
まずはグラフ保存用の関数。保存形式はhtmlとpng。config
はグラフの右上に存在するツールバーのようなもの。
config
を設定しておくとこのツールバーの内容を追加したり削除したりできる。htmlで使用することで、保存後のhtmlファイルでもツールバーの設定が引き継がれる。
-
-
【plotly&config】グラフのツールバーを編集する
続きを見る
今回は3Dグラフでconfig
の追加が反映されないのでconfig
はNone
でしのぐ。このsave関数を使用してグラフの表示と保存を一括で行う関数も定義しておく。
予め定義しておくことで全グラフを保存するときや保存しない時など一括管理が楽になる。個別管理の時はグラフの関数呼び出しの時にその行だけコメントアウトしたらいい。
-
-
【plotly&工夫】楽にグラフを描くためのplotlyの関数化
続きを見る
# グラフ保存用の関数 def show_save(fig, config, save_name): fig.show() save(fig=fig, config=config, save_name=save_name)
ピラミッド用の配列作成用の関数
def make_arr(length): # ピラミッドの値の種類(長さが偶数の時は辺+1したピラミッドを作成) nums = length // 2 + 1 rows = [] # ベースとなる配列の各行の値を入れる2次元配列 for row in range(nums): # 1行ごとに見る n = 0 # 各行の各列の値 cols = [] # row行目の値を入れる for col in range(nums): # row行目の各列 cols.append(n) # 一番左は0 if row: # 0行目の時はここはFalse、それ以外の行目ならTrue n += 1 # 各列の値は1ずつ増えていく row -= 1 # あと何回、列の値が変わるかに相当 rows.append(cols) # row行目の配列を格納 arr = np.array(rows) # 2次元配列の変換 bottom = arr[::-1][1:] # ベース下用の配列 arr2 = np.vstack([arr, bottom]) # ベース下に配列を結合 left = arr2[:, ::-1][:, 1:] # ベース+下の左用の配列 arr3 = np.hstack([arr2, left]) # ベース+下の左に配列を結合 return arr3
3次元のグラフを作成するわけだが、plotly
の場合はピラミッドの高さ方向に値を振り分けるだけでいいので、2次元配列を作成するだけ。詳しい作成の内容は以下の記事参照。
-
-
【python&3Dピラミッド】plotlyで3次元ピラミッドを作成
続きを見る
例えば1辺の長さ5のピラミッド(length=5
)の場合は以下の配列となる。もちろん高さは好みで好きにいじってもいい。
print(make_arr(length=5)) # [[0 0 0 0 0] # [0 1 1 1 0] # [0 1 2 1 0] # [0 1 1 1 0] # [0 0 0 0 0]]
グラフ作成用の関数たち
ここではグラフを作成するための各構成要素を関数化している。関数化することでメインの処理がスッキリするし、何か問題があったときに処理の切り分けがしやすい。
surfaceプロット用の関数
# surfaceプロット def surface(x, y, z, opacity=0.9): contours = dict( show=True, usecolormap=True, highlightcolor='lime', # ピラミッドの中のホバーの線色 ) d = go.Surface( x=x, y=y, z=z, opacity=opacity, # 透明度 showscale=False, # スケールはオフ colorscale='YlOrBR', # っぽい色 contours=dict(x=contours, y=contours, z=contours), ) return d
今回はsurfaceプロットなのでそれ用の関数を作成。contours
でピラミッド本体とxy面などの平面にピラミッドの投影を行う。
といっても今回は背景色を無くしたり軸表示を消したりするので平面へのプロットは適用されない。ただ、ピラミッド本体への投影はされるので、某パズルのような風合いは出せる。
また、透明度を引数で設定できるようにしたりカラーバーはいらないのでオフにしたりしている。
回転角度設定
# 回転角度設定 def set_thetas(deg=0.2): thetas = np.arange(0, 2 * np.pi + deg, deg * np.pi) print(thetas) # deg=0.2の時 # [0. 0.62831853 1.25663706 1.88495559 2.51327412 3.14159265 # 3.76991118 4.39822972 5.02654825 5.65486678 6.28318531] return thetas
ピラミッドを回転させるので何度回転させるのかを指定しないといけない。ということで回転角度用の関数を定義。
本当は細かく回転させたかったけど、記事に載せる際に激重になってしまったので、本記事のグリグリグラフはざっくりした回転角度にしている。
グラフ内テキスト用関数
# グラフ内テキスト def set_annotation(): annotations = [dict( showarrow=False, x=-0, y=-0, z=5, text='<b>遊☆戯☆O<br>デュエルOンスターズ</b>', font=dict(color='#571475', size=15, family='Times New Roman') )] return annotations
グラフに加えて、タイトルロゴっぽいのもつけたかったのでそのテキスト内容を関数化。一応、タイトルとかは伏せ字にしている。これでなんのアニメかはわからないだろう。
<b>
タグで太文字にしたり、カラーはいい感じのものにして、フォントサイズもそれなりに、フォントもかっこいいのにしておいた。
レイアウト設定用の関数
# レイアウトの雛形 def set_scene(xval, yval, zval, annotations, **kwargs): ans = dict( xaxis=dict(range=(-xval, xval)), yaxis=dict(range=(-yval, yval)), zaxis=dict(range=(-zval, 5)), # アスペクト比は手設定 aspectmode='manual', aspectratio=dict(x=2, y=2, z=1), annotations=annotations, # グラフ内にテキストを追加 xaxis_visible=False, yaxis_visible=False, zaxis_visible=False, **kwargs, ) return ans
レイアウト設定用の関数を定義。アニメーションの場合は各軸の表示範囲を設定してあげないと自動で表示範囲が変更されてフレームごとにグラフが揺れることになる。
また、アスペクト比を自動にしているとグラフの比率がいい感じにならなかったので手動で設定している。また、ここで先程のタイトルロゴを入れている。
あとはピラミッドだけ表示したいので各軸の表示は非表示にして見えなくしている。これでピラミッドが際立つ。
初期プロット用の関数
# 初期表示用のプロットとレイアウト def initial_pyramid(x, y, z, xval, yval, zval, annotations): z0 = np.zeros((len(z), len(z))) plot = [ surface(x=x, y=y, z=z0, opacity=1), # 底面用 surface(x=x, y=y, z=z), # ピラミッド本体 ] # Playボタン args = dict( frame=dict(duration=200, ), # フレーム間の動きを早める fromcurrent=True, transition=dict(duration=10, easing='linear'), ) buttons = dict( label='Play', method='animate', args=[None, args], ) # レイアウトの設定 layout = go.Layout( scene=set_scene( xval=xval, yval=yval, zval=zval, annotations=annotations, camera=dict(eye=dict(x=2, y=2, z=0.6)), # カメラ視点指定 ), updatemenus=[dict( type='buttons', x=0.5, y=0.8, xanchor='center', yanchor='bottom', buttons=[buttons], )], paper_bgcolor='rgba(0,0,0,0)', # 背景は透明 margin=dict(t=0, b=0, r=0, l=0), # グラフの余白はなし height=1000, # グラフを高く ) return plot, layout
アニメーションの場合、初期プロットとアニメーションの各フレームでそれぞれプロットしないといけない。ということで、ここは切り分けて初期表示専用の関数を定義。
プロット自体は以下の作業が必要。
- ピラミッドの底面(ひっくり返すから実質天井)のプロット
- ピラミッド自体のプロット
- レイアウトの調節
初期表示ではこれらに加えて以下の情報を追加した。
- アニメーション再生ボタンを追加
- 背景色を無色透明に
- 余白は上下左右0
- グラフの高さを1000に
初期表示で上の内容を設定しておくことでアニメーション中でもこれらの設定を引き継ぐことができる。
初期表示のプロット作成の流れは以下。
- 底面用の配列
z0
を定義 - 初期表示用のプロット
plot
を作成 - 再生ボタン
button
を作成 - レイアウト
layout
の作成 plot
とlayout
を返り値にする
plotly
のボタンについては以下参照。plotly
では簡単にボタンを追加して動きをつけたりプロットの切り替えなどが可能。便利。
-
-
【plotly&ボタン】plotlyのupdatemenusにbuttonsを追加
続きを見る
-
-
【plotly&ボタン】グラフに複数種のボタンを追加
続きを見る
回転中のプロット用関数
# 回転中のピラミッド def frame_pyramid(thetas, x, y, z, scene): frames = [] z0 = np.zeros((len(z), len(z))) for theta in thetas: xr = x * np.cos(theta) - y * np.sin(theta) # 一定角回転後のx yr = x * np.sin(theta) + y * np.cos(theta) # 一定角回転後のy # 各フレーム frame = go.Frame( data=[ surface(x=xr, y=yr, z=z0, opacity=1), # 底面用 surface(x=xr, y=yr, z=z), # ピラミッド本体 ], ) frames.append(frame) return frames
今度は打って変わって回転中のプロットを関数化。今回の回転はz
軸を軸として回転させるので以下の式で対応可能。
$x'\ =\ x\cos(\theta)\ -\ y\sin(\theta)\\y'\ =\ x\sin(\theta)\ +\ y\cos(\theta)$
これを各フレームで作成する。また、回転中は底面も回転するので各フレームで底面のプロットも必要になる。
レイアウトは初期設定で設定したので、フレーム中にタイトルを変更したいとかがなければ設定する必要はない。設定する場合はgo.Frame
のdata
の後にlayout=
として書けばいい。
グラフ化する
ということで実際にグラフ化する。グラフ化はシンプルで、これまでに定義した内容をツラツラと並べたらいい。流れは以下。
- ピラミッドの辺の長さから
x
,y
の1次元配列を計算 x
,y
を回転させるためにmeshgrid
で2次元配列に変換z
としてmake_arr
で2次元配列を作成(逆さにするので負符号をつける)- 表示範囲とグラフ内テキストを設定
- 初期表示を設定
- 回転中のプロットを設定
- グラフ作成と表示と保存
引数としてピラミッドの辺の長さと回転角度を設定できるようにした。辺の長さが大きくなるとより滑らかに、回転角度を少なくすると回転が滑らかになる。
それに伴ってデータ数が多くなりファイルが重くなる。今回は記事に載せられたギリギリのサイズlength=29
、deg=0.2
で設定。ちょっとギザギザで回転もガクガクだけど仕方ない。
def make_pyramid3d(length, deg=0.2): # 横軸・縦軸の1次元配列をの作成 base = np.array(range(length)) # ベースとなる配列 x = base - length // 2 # 配列の真ん中を0にする y = base - length // 2 # yも同様 # x, y, zのデータ x, y = np.meshgrid(x, y) # meshgridで指定してもいい pyramid = -make_arr(length=length) # 負の数にして上下逆転 # 表示範囲の設定 xyval = length - 10 # x, yの表示範囲 zval = length // 2 # zの表示範囲 # グラフ内テキスト annotations = set_annotation() # レイアウトの表示範囲やテキストの追加 scene = set_scene( xval=xyval, yval=xyval, zval=zval, annotations=annotations, ) # 初期表示 plot, layout = initial_pyramid( x=x, y=y, z=pyramid, xval=xyval, yval=xyval, zval=zval, annotations=annotations, ) # 回転中のピラミッド thetas = set_thetas(deg=deg) frames = frame_pyramid(thetas=thetas, x=x, y=y, z=pyramid, scene=scene) # グラフ作成と表示と保存 fig = go.Figure(data=plot, layout=layout, frames=frames) save_name = f"pyramid3d_{length}_{str(deg).replace('.', 'p')}" show_save(fig=fig, config=None, save_name=save_name) make_pyramid3d(length=29)
なお、ファイル保存名はlength
とdeg
で自動で名称変更されるようにしたけど、deg
の小数点がネックなるので小数点: Decimal Pointの「p
」で置き換えている。
データ数を増やしてみる
ということで、最後はデータ数と角度刻みを増やした版を作成。上のピラミッドは初めのピラミッドの45倍くらいの容量になった。記事に載せたら容量オーバー。
# 辺の長さを101、角度刻みを0.05 radに make_pyramid3d(length=101, deg=0.05)
回転させるだけなら結構簡単
今回は自己満のピラミッドを回転させて某アニメをパロってみた。本当はロゴとか背景とかも作成した方がリアリティはあるだろうけど、シンプルに作成するだけならこれでいい。<