Blender Python APIで複数オブジェクトを用いたアニメーション作成

# はじめに
Blenderで作成したテトラポッドのオブジェクトに対して、Blender Python APIを利用して、複製や回転、平行移動を行って簡単なアニメーションを作成しました。
下のgif動画が作成したアニメーションになります。

Blender Python APIを使用してアニメーション作成した日本語記事が少なかったので、軽いTipsになればいいなという思いと、Blender Python APIの活用法を示せればいいなという思いで記事を作成しました。

![tetrapods_animation_compressed.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/896745/57e4302f-149e-80b2-eb21-4b732fc459ac.gif)

# 環境
- Blender 3.6.4
- Python 3.10.12
- Numpy 1.23.5

PythonやNumpyはもともとBlenderにインストール済みのものを使用しました。

# 目次
1. [テトラポットの作成](#1-テトラポットの作成)
2. [アニメーション作成の基本&オブジェクトの移動](#2-アニメーション作成の基本オブジェクトの移動)
3. [オブジェクトの回転](#3-オブジェクトの回転)
4. [現状より正確なアニメーションにするための工夫](#4-現状より正確なアニメーションにするための工夫)
5. [全コード](#5-全コード)
6. [完成したアニメーション](#6-完成したアニメーション)

# 1. テトラポットの作成
[「江のモデ」その３１　消波ブロック](https://ameblo.jp/promised-kiminosorae/entry-12616624489.html)という記事に従って作成しました。

イメージとしては円柱を回転させたものを4つ結合させてつくる感じです。

円柱の上部面を小さくして、さらにその面を押し出して最上部面を小さくすることでz軸方向にテトラポッドの1本目の腕を作成します。
その腕を複製し、x軸を回転軸として120°回転させることで2本目の腕を作成します。
さらに2本目の腕を2つ複製し、z軸を回転軸としてそれぞれ120°、240°回転させることで3本目と4本目の腕を作成します。

# 2. アニメーション作成の基本&オブジェクトの移動

オブジェクトを取得した後、

1. プロパティ(位置や回転角など)の設定
2. 指定したフレーム位置へキーフレームの挿入

を繰り返すことで、指定したフレーム間で連続的にプロパティが変化します。

例えば、下のコードでは、テトラポッドのオブジェクトを取得した後、オブジェクトの位置(location)を(0,0,0)に指定しており、それをフレーム0としてキーフレームを挿入しています。その後、位置を(5,5,5)に変更した後、フレーム20としてキーフレームを挿入しています。data_pathにlocationを指定することで、locationプロパティを変更することを指示しています。

このコードを実行することで、フレーム0からフレーム20にかけてテトラポッドのオブジェクトが(0,0,0)から(5,5,5)まで連続的に移動するアニメーションが描画されます。

```python
import bpy

# オブジェクト名の指定
tetrapod_name = "tetrapod"
# オブジェクトの取得
obj = bpy.data.objects[tetrapod_name]

# フレーム0でのテトラポッドの位置を指定
current_frame = 0
obj.location = (0, 0, 0)
obj.keyframe_insert(data_path="location", frame=current_frame)
# フレーム20でのテトラポッドの位置を指定
current_frame += 20
obj.location = (5, 5, 5)
obj.keyframe_insert(data_path="location", frame=current_frame)


```

![tetrapods_animation_qiita_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/896745/5f8cce38-6c4d-a1f7-5cb6-58d4938721e0.gif)



# 3. オブジェクトの回転

オブジェクトの回転を行うには、まず回転の指定方法を決める必要があります。

回転方法は主に、オイラー角、軸-角度表現、クォータニオンの3つがあります。
軸-角度表現とクォータニオンは実質同じで、回転軸と回転角を指定して回転させます。
オイラー角については、どの軸から回転させるかによって、ZYXやXYZなどのパターンがいくつかあります。

## 3.1 軸-角度表現での回転
rotaion_modeプロパティを"AXIS_ANGLE"に設定して、rotation_axis_angleプロパティに回転軸方向と回転角を代入し、キーフレームのdata_pathをrotation_axis_angleにすれば回転できます。

```python
obj.rotation_mode = 'AXIS_ANGLE'
obj.keyframe_insert(data_path="rotation_axis_angle", frame=current_frame)
# z軸を回転軸として360°回転
obj.rotation_axis_angle = (2*math.pi, 0, 0, 1)

current_frame += 20
obj.keyframe_insert(data_path="rotation_axis_angle", frame=current_frame)
```

![tetrapods_animation_qiita_3_1.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/896745/10529bff-4a19-41fb-ca6e-8565941f5610.gif)



## 3.2 オイラー角での回転
rotaion_modeプロパティを"ZYX"(回転させる軸の順番)に設定して、rotation_eulerプロパティにmathutilsモジュール(Blender Python APIのモジュールの1つ)内のEulerクラスのインスタンスを代入し、キーフレームのdata_pathをrotation_eulerにすれば回転できます。

```python
from mathutils import Euler
import numpy as np

obj.rotation_mode = 'ZYX'
obj.keyframe_insert(data_path="rotation_euler", frame=current_frame)
# z軸周りに60°回転→y軸周りに-45°回転→x軸周りに30°回転
obj.rotation_euler = Euler(np.radians([30, -45, 60]), 'ZYX')

current_frame += 20
obj.keyframe_insert(data_path="rotation_euler", frame=current_frame)
```


![tetrapods_animation_qiita_3_2.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/896745/07b620a3-a529-a1ee-3846-51ba50dc0721.gif)




## 3.3 アニメーション途中で回転方法を切り替える方法
アニメーションの途中で回転方法を切り替えるには、特定のフレームでオブジェクトのrotation_modeを変更させる関数を記述し、ハンドラに追加する必要があります。
ただし、アニメーション途中で回転方法を切り替えると、最初の回転方法で行われていた回転処理は、切り替えたタイミングで解除されます。

```python
# テトラポットの回転方法をフレーム番号によって変更するような関数(後にハンドラ追加するための関数)
def change_rotation_mode(scene):
    # オブジェクトを取得
    obj = scene.objects.get("tetrapod")
    
    # フレーム番号に応じて回転方法を変更
    if scene.frame_current == 0:
        obj.rotation_mode = 'AXIS_ANGLE'
    elif scene.frame_current == 40:
        obj.rotation_mode = 'ZYX'

current_frame = 0
obj.keyframe_insert(data_path="rotation_axis_angle", frame=current_frame)
# z軸を回転軸として360°回転
obj.rotation_axis_angle = (2*math.pi, 0, 0, 1)

current_frame += 20
obj.keyframe_insert(data_path="rotation_axis_angle", frame=current_frame)
# (1,1,1)を回転軸として45°回転
obj.rotation_axis_angle = (math.pi/4, 1, 1, 1)

current_frame += 20
obj.keyframe_insert(data_path="rotation_axis_angle", frame=current_frame)
obj.keyframe_insert(data_path="rotation_euler", frame=current_frame)
# z軸周りに60°回転→y軸周りに-45°回転→x軸周りに30°回転
obj.rotation_euler = Euler(np.radians([30, -45, 60]), 'ZYX')

current_frame += 20
obj.keyframe_insert(data_path="rotation_euler", frame=current_frame)

# テトラポットの回転方法をフレーム番号によって変更するようにハンドラに追加
bpy.app.handlers.frame_change_post.append(change_rotation_mode)
```

![tetrapods_animation_qiita_3_3.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/896745/8d3f43e2-32b5-2c52-1be7-1b991bbdd408.gif)





## 3.4 注意点
すべての回転はグローバル座標を基準にして計算されます。
そのため、例えば軸-角度表現でオブジェクトをz軸方向を回転軸として360°回転させた後、さらに同様の回転を行うには、回転角を720°にする必要があります。
オイラー角についても同様で、一度回転させたオブジェクトに再度回転させる場合、そのオイラー角は一度回転されたローカル座標からでなく、グローバル座標から計算されます。

# 4. 現状より正確なアニメーションにするための工夫
現在のアニメーションでは、複製された4つのテトラポッドが中心のテトラポットと結合しているように見えますが、正確には接しておらず、目視でおよそ接しているように調整してあるだけです。

これを正確に接するようにするには、次のようなコードにする工夫が考えられます。
なお、これを行うにはBlenderでテトラポッドの腕の長さの寸法を正確に測定する必要があります。

テトラポッドの各腕の方向は、クォータニオンを用いて(0,0,1),(0,-√3/2,-1/2),(3/4,√3/4,-1/2),(-3/4,√3/4,-1/2)と求められます。
今回の例では、(0,0,1)方向の腕を、原点にあるテトラポッドの(0,-√3/2,-1/2)方向に向けるようにしています。
なお、今回はテトラポッドの腕の長さは正確に測っていないので、回転だけが正確になっています。
```python
import bpy
import numpy as np

# テトラポッドのオブジェクトを選択するためのオブジェクト名を指定します。
tetrapod_name = "tetrapod"

# オブジェクトを選択し、アクティブにします。
bpy.data.objects[tetrapod_name].select_set(True)
bpy.context.view_layer.objects.active = bpy.data.objects[tetrapod_name]

# テトラポッドの腕の長さ(正確ではない)
R = 3.25
# 結合元のテトラポッドの腕の方向(原点にあるテトラポッドからみて新しくテトラポッドを付ける方向)
arm_origin = np.asarray([0, -np.sqrt(3)/2, -1/2])
# 新しいテトラポッドの4本腕の内、結合に使う腕の方向
arm_new = np.asarray([0, 0, 1])

# フレーム間隔を指定
frames_between_action = 20

# カメラやテトラポッドの初期位置を設定
current_frame = 0
obj = bpy.data.objects[tetrapod_name]
obj.location = (0, 0, 0)
obj.keyframe_insert(data_path="location", frame=current_frame)
camera = bpy.data.objects["Camera"]
camera.location = (26.5, -42.2, 8.3)
camera.keyframe_insert(data_path="location", frame=current_frame)

# テトラポッドを複製して、平行移動させる
bpy.ops.object.duplicate()
obj_copy = bpy.data.objects[tetrapod_name+".001"]
obj_copy.location = np.asarray(obj.location) + 2*R*arm_origin
current_frame += frames_between_action
obj_copy.keyframe_insert(data_path="location", frame=current_frame)

# 複製したテトラポッドを正確に回転させて、腕同士が結合しているように見せる
# 軸-角度表現で回転させる(クォータニオンを用いてもよい)
obj_copy.rotation_mode = 'AXIS_ANGLE'
obj_copy.keyframe_insert(data_path="rotation_axis_angle", frame=current_frame)
# arm_newのベクトルを-arm_originの方向に向けたい
# 外積を用いて回転軸を求める
rotation_axis = np.cross(arm_new, -arm_origin)
# 内積とarccosを用いて回転角度を求める
rotation_angle = np.arccos(np.dot(arm_new, -arm_origin)/(np.linalg.norm(arm_new)*np.linalg.norm(-arm_origin)))
# 軸-角度表現で回転させる(クォータニオンを用いてもよい)
obj_copy.rotation_axis_angle = (rotation_angle, rotation_axis[0], rotation_axis[1], rotation_axis[2])

current_frame += frames_between_action
obj_copy.keyframe_insert(data_path="rotation_axis_angle", frame=current_frame)

```


![tetrapods_animation_qiita_4.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/896745/da892c18-ec6f-ac79-ad60-18fe151f25be.gif)




# 5. 全コード

```python
import bpy
import math
import mathutils
import numpy as np
from mathutils import Euler

# テトラポットの回転方法をフレーム番号によって変更するような関数(後にハンドラ追加するための関数)
def change_rotation_mode(scene):
    # オブジェクトを取得
    obj = scene.objects.get("tetrapod")
    obj_1 = scene.objects.get("tetrapod.001")
    obj_2 = scene.objects.get("tetrapod.002")
    obj_3 = scene.objects.get("tetrapod.003")
    obj_4 = scene.objects.get("tetrapod.004")
    
    # フレーム番号に応じて回転方法を変更
    if scene.frame_current == 0:
        obj.rotation_mode = 'AXIS_ANGLE'
        obj_1.rotation_mode = 'AXIS_ANGLE'
        obj_2.rotation_mode = 'AXIS_ANGLE'
        obj_3.rotation_mode = 'AXIS_ANGLE'
        obj_4.rotation_mode = 'AXIS_ANGLE'
    elif scene.frame_current == 60:
        obj.rotation_mode = 'ZYX'
        obj_1.rotation_mode = 'ZYX'
        obj_2.rotation_mode = 'ZYX'
        obj_3.rotation_mode = 'ZYX'
        obj_4.rotation_mode = 'ZYX'

# オブジェクト名の指定
tetrapod_name = "tetrapod"

# オブジェクトを選択する(移動や回転、複製するのに必要)
bpy.data.objects[tetrapod_name].select_set(True)
# オブジェクトをアクティブにする(より詳細な編集ができるようになるが、今回は不要)
bpy.context.view_layer.objects.active = bpy.data.objects[tetrapod_name]

# 複製を行う回数及びフレーム間隔を指定
num_duplications = 4
frames_between_action = 20

# step 0
# テトラポッドの位置とカメラの位置を指定
current_frame = 0
obj = bpy.data.objects[tetrapod_name]
obj.location = (0, 0, 0)
obj.keyframe_insert(data_path="location", frame=current_frame)
camera = bpy.data.objects["Camera"]
camera.location = (26.5, -42.2, 8.3)
camera.keyframe_insert(data_path="location", frame=current_frame)

# step 1
# テトラポッドをz軸を回転軸として360°回転(1周回転)
current_frame += frames_between_action
obj.keyframe_insert(data_path="rotation_axis_angle", frame=current_frame)
obj.rotation_axis_angle = (2*math.pi, 0, 0, 1)

# step 2
# テトラポッドを複製し、回転しながらそれぞれ別の位置に移動
current_frame += frames_between_action
obj.keyframe_insert(data_path="rotation_axis_angle", frame=current_frame)

for i in range(num_duplications):
    bpy.ops.object.duplicate()

obj.rotation_axis_angle = (4*math.pi, 0, 0, 1)
for i in range(num_duplications):
    obj_copy = bpy.data.objects[tetrapod_name+".{:0>3}".format(i+1)]
    obj_copy.keyframe_insert(data_path="location", frame=current_frame)
    obj_copy.keyframe_insert(data_path="rotation_axis_angle", frame=current_frame)
    
    if i == 0:
        obj_copy.location = (6, 6, 0)
    elif i == 1:
        obj_copy.location = (0, -6, 0)
    elif i == 2:
        obj_copy.location = (-6, 6, 0)
    elif i == 3:
        obj_copy.location = (0, 0, 6)
    
    obj_copy.rotation_axis_angle = (4*math.pi, 0, 0, 1)

# step 3
# 移動したテトラポッドの1本の腕が原点を向くように回転し、テトラポッドの腕同士が結合するように移動
# 正確な回転や移動ではない
# 正確に回転するには、腕を向けたい方向ベクトルと現在の腕の方向ベクトルとの外積から回転軸方向ベクトルを算出し、内積から回転角を算出するのが早い
# 正確な移動には、Blenderの機能を用いてテトラポットの腕の長さを正確に測定する必要がある
# 原点にあるテトラポットの各腕の方向は(0,0,1),(0,-√3/2,-1/2),(3/4,√3/4,-1/2),(-3/4,√3/4,-1/2)
current_frame += frames_between_action
obj.keyframe_insert(data_path="rotation_axis_angle", frame=current_frame)
for i in range(num_duplications):
    obj_copy = bpy.data.objects[tetrapod_name+".{:0>3}".format(i+1)]
    obj_copy.keyframe_insert(data_path="location", frame=current_frame)
    obj_copy.keyframe_insert(data_path="rotation_axis_angle", frame=current_frame)
    obj_copy.keyframe_insert(data_path="rotation_euler", frame=current_frame)
    if i == 0:
        obj_copy.location = (4.5, 2.5, -3.5)
        obj_copy.rotation_euler = Euler(np.radians([30, -45, 60]), 'ZYX')
    elif i == 1:
        obj_copy.location = (0, -5.5, -3.5)
        obj_copy.rotation_euler = Euler(np.radians([-55, 0, 60]), 'ZYX')
    elif i == 2:
        obj_copy.location = (-4.5, 2.5, -3.5)
        obj_copy.rotation_euler = Euler(np.radians([30, 45, 60]), 'ZYX')
    elif i == 3:
        obj_copy.location = (0, 0, 6.5)
        obj_copy.rotation_euler = Euler(np.radians([180, 0, 60]), 'ZYX')


# step 4
# 原点のテトラポットに結合しているテトラポットを、結合している腕を回転軸として360°回転させる
current_frame += frames_between_action
for i in range(num_duplications):
    obj_copy = bpy.data.objects[tetrapod_name+".{:0>3}".format(i+1)]
    obj_copy.keyframe_insert(data_path="location", frame=current_frame)
    obj_copy.keyframe_insert(data_path="rotation_euler", frame=current_frame)
    if i == 0:
        obj_copy.rotation_euler = Euler(np.radians([30, -45, 420]), 'ZYX')
    elif i == 1:
        obj_copy.rotation_euler = Euler(np.radians([-55, 0, 420]), 'ZYX')
    elif i == 2:
        obj_copy.rotation_euler = Euler(np.radians([30, 45, 420]), 'ZYX')
    elif i == 3:
        obj_copy.rotation_euler = Euler(np.radians([180, 0, 420]), 'ZYX')

# step 4
# 原点のテトラポットに結合しているテトラポットを、結合している腕を回転軸として-360°回転させる
current_frame += frames_between_action
for i in range(num_duplications):
    obj_copy = bpy.data.objects[tetrapod_name+".{:0>3}".format(i+1)]
    obj_copy.keyframe_insert(data_path="rotation_euler", frame=current_frame)
    if i == 0:
        obj_copy.rotation_euler = Euler(np.radians([30, -45, 60]), 'ZYX')
    elif i == 1:
        obj_copy.rotation_euler = Euler(np.radians([-55, 0, 60]), 'ZYX')
    elif i == 2:
        obj_copy.rotation_euler = Euler(np.radians([30, 45, 60]), 'ZYX')
    elif i == 3:
        obj_copy.rotation_euler = Euler(np.radians([180, 0, 60]), 'ZYX')

# step 5
# テトラポットの回転の終了
current_frame += frames_between_action
for i in range(num_duplications):
    obj_copy = bpy.data.objects[tetrapod_name+".{:0>3}".format(i+1)]
    obj_copy.keyframe_insert(data_path="rotation_euler", frame=current_frame)    

# カメラ(視点)をz軸を回転軸として90°ずつ移動
camera.keyframe_insert(data_path="location", frame=current_frame)
for i in range(4):
    radian = np.radians(90)
    new_x = np.cos(radian) * camera.location.x - np.sin(radian) * camera.location.y
    new_y = np.sin(radian) * camera.location.x + np.cos(radian) * camera.location.y
    camera.location = (new_x, new_y, camera.location.z)
    current_frame += frames_between_action
    camera.keyframe_insert(data_path="location", frame=current_frame)

# 斜め下からオブジェクトを見るようにカメラ(視点)を移動(zが負の方向に移動)
camera.location = (camera.location.x, camera.location.y, -camera.location.z)
current_frame += frames_between_action
camera.keyframe_insert(data_path="location", frame=current_frame)

# カメラ(視点)をz軸を回転軸として90°ずつ移動
for i in range(4):
    radian = np.radians(90)
    new_x = np.cos(radian) * camera.location.x - np.sin(radian) * camera.location.y
    new_y = np.sin(radian) * camera.location.x + np.cos(radian) * camera.location.y
    camera.location = (new_x, new_y, camera.location.z)
    current_frame += frames_between_action
    camera.keyframe_insert(data_path="location", frame=current_frame)

camera.location = (camera.location.x, camera.location.y, -camera.location.z)
current_frame += frames_between_action
camera.keyframe_insert(data_path="location", frame=current_frame)

# すべてのオブジェクトの選択を解除します。
bpy.ops.object.select_all(action='DESELECT')

# テトラポットの回転方法をフレーム番号によって変更するようにハンドラに追加
bpy.app.handlers.frame_change_post.append(change_rotation_mode)
```

# 6. 完成したアニメーション
[5. 全コード](#5-全コード)を実行すると、最初に載せた下のアニメーションが生成されます。

![tetrapods_animation_compressed.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/896745/57e4302f-149e-80b2-eb21-4b732fc459ac.gif)

# おわりに
BlenderとBlender Python APIを利用して、複数のテトラポッドのオブジェクトを用いたアニメーションを作成しました。

Blenderには3Dモデリングのための機能が充実しているので、凝ったモデリングをすればもっと精緻なアニメーションを作成できると思います。

ネット上に3Dモデリングの記事は多かったですが、Python APIでアニメーション作成した日本語記事が少なく思えたので、この記事が少しでも役立てば幸いです。