-Blender Python で剛体をいっぱい作成-

2. フレーム毎に処理

ここは、Python でスクリプトを組んで、オブジェクトの配置や増減を制御したいというときの、スクリプトの組み方のメモです。
ここでは、スクリプトによってオブジェクトをどんどん作り出すことをしてみます。

■ハンドラーの登録

ハンドラ(ハンドラー)という用語を調べてみると「何らかの処理要求が発生した時に起動されるプログラムのこと」とあります。
ハンドラを登録しておくと、時間が変化したときなどにそのハンドラが呼び出されるので、一定時間間隔でオブジェクトを生成することができるようになります。

handler という関数(引数はscene)を設定(def)して、フレームが変化するごとに呼び出されるハンドラー(frame_change_pre)として登録をしました。

フレームの変更があった際に、そのフレームの処理を行う前に呼び出されます。
このスクリプトでは、print文を使ってフレーム数を出力しています。

注)printの出力は(メイン画面ではなく)コンソールに表示されます。


ところで、少しずつ機能を変えながら何度もスクリプトを実行していくと、登録されたハンドラーがどんどん溜ってゆき、似たような処理を何度も行うようになってしまいます。
しかも、このhandlerという名前の関数は、たとえ関数の中身を変更して(新しい関数として)登録されたのちも、古い関数はずっと残って実行され続けます。

import bpy

def handler(scene):
    frame = scene.frame_current
    print(frame)

bpy.app.handlers.frame_change_pre.clear()
bpy.app.handlers.frame_change_pre.append(handler)
登録前に、clear()を使って登録リストを空にしておきます。

おそらく、これで古い関数も誰からも参照されなくなるので、自動で削除(ガベージコレクション)されるものと信じています。

■オブジェクトのクリアと配置

import fnmatch
  
def ClearObjects():
    scene = bpy.context.scene
    data = bpy.data
    IcoA = [objA for objA in scene.objects if fnmatch.fnmatchcase(objA.name, "Ico*")]
    IcoB = [objB for objB in data.objects if fnmatch.fnmatchcase(objB.name, "Ico*")]
    IcoC = [objC for objC in data.meshes if fnmatch.fnmatchcase(objC.name, "Ico*")]
    IcoD = [objD for objD in data.materials if fnmatch.fnmatchcase(objD.name, "Ico*")]
    
    for item in IcoA:
        if item.type == 'MESH':
            bpy.context.scene.objects.unlink(item)
    for item in IcoB:
        if item.type == 'MESH':
            bpy.data.objects.remove(item)
    for item in IcoC:
        bpy.data.meshes.remove(item)
    for item in IcoD:
        bpy.data.materials.remove(item)
毎回オブジェクトを生成してゆくので、シーンの最初では一度いらないオブジェクトをクリアする必要があります。
クリア用の関数を作成しました。

使い捨てのオブジェクトかどうかは、今回名前で管理するようにしました。

前セクションで作った、オブジェクトを全てクリアするスクリプトを変更して、Ico*という名前のオブジェクトなどを削除する関数を定義してみます。

fnmatchという拡張機能を使うと、ワイルドカードなどが使えて便利ということです。
def handler(scene):
    frame = scene.frame_current

    if frame == 2:
        ClearObjects()
    if frame % 8 == 3:
        CreateObjects(frame)
ClearObjects() は frame 2 で呼ぶようにしました。
本当は1にするべきですが、デバグ中などにシーンの先頭にいくとものが消えてしまうと、少々不便なためです。

8コマごとにオブジェクトを生成するようにします。
def CreateObjects(frame):       
    num = 12
    r = 4
    obj = []
    for i in range(num):
        t = 2.0 * math.pi * i / num
        x = 0.8 * r * math.cos(t)
        y = 0.8 * r * math.sin(t)
        
        bpy.ops.mesh.primitive_ico_sphere_add(subdivisions = 3, size = 0.5)
        obj.append(bpy.context.scene.objects.active)
        
        bpy.ops.rigidbody.object_add()
        obj[i].rigid_body.kinematic = True
        obj[i].rigid_body.restitution = 0.6
        
        obj[i].location = (x, y, 0.1)           
        obj[i].keyframe_insert( data_path='location', frame=frame )
        obj[i].rigid_body.keyframe_insert( data_path='kinematic', frame=frame )
        
        obj[i].location = (x*1.06, y*1.06, 0.1)
        obj[i].keyframe_insert( data_path='location', frame=frame+1 )
        
        obj[i].rigid_body.kinematic = False
        obj[i].rigid_body.keyframe_insert( data_path='kinematic', frame=frame+2 )
オブジェクトを生成するスクリプトは、前セクションのものを少し変更したものです。

幾つかキーフレームを配置することで、初期速度を設定しています。
適当な背景のオブジェクトを配置してアニメーションを再生(Alt+A)すると、球が次々に出現してこぼれ落ちてゆくシーンが設定できました。
def handler(scene):
    frame = scene.frame_current

    if frame == 2:
        ClearObjects()
    if frame % 8 == 3:
        CreateObjects(frame)
    if frame == scene.frame_end:
        bpy.ops.screen.animation_cancel(restore_frame=False)
デフォルト状態ではシーンの最後に達すると、先頭に戻ってオブジェクトがすべてクリアされてしまうので、最後に再生を停止するようにスクリプトを追加しました。

■レンダリング時のハングアップ回避

さて、この状態でレンダリングを開始しても、アニメーション再生と同じようにうまくいきそうな気がするのですが、レンダリングはうまくされないどころか、結構な確率でblenderが落ちてしまうようです(2.74時点)

本当は、フレームが変化した時点で最初にスクリプトが処理されて、それに従ってレンダリング…して欲しいのですが、レンダリングの最中にオブジェクトが消えたりあらわれたりすると、何かが破綻をしてしまうようです。

そこで、レンダリングをする時にはハンドラーの処理を行わずに、アニメーション再生(Alt-A)した時に作られたシーンをそのままレンダリングするようにします。


def handler_render_pre(scene):
    bpy.app.handlers.frame_change_pre.clear()
    
bpy.app.handlers.render_pre.clear() 
bpy.app.handlers.render_pre.append(handler_render_pre)

frame_change_pre をクリアするハンドラを作成して render_pre に登録しました。
これで、一度レンダリングを実行すると、フレームごとのハンドラ処理は行わなくなるので、
オブジェクトのクリアや生成は起こらなくなります。

(再度有効にするには、再度スクリプトの実行が必要になります)

■出現前のオブジェクトを無効状態にする

さて、再生時には再生の途中で出現したオブジェクトたちですが、出現させた時点で、シーン内では最初から存在していることになっています。
そのため、もう一度レンダリングなどでシーンを再生しようとすると、出現前のオブジェクトたちとも相互作用が起きてしまいます

これを防ぐためには、出現前にはオブジェクトは無効状態であり、出現と同時に有効になる、というようにシーンが組みあがっている必要があります。

obj[i].hide_render = True
obj[i].hide_select = True
obj[i].keyframe_insert( data_path='hide_render', frame=frame-1 )
obj[i].keyframe_insert( data_path='hide_select', frame=frame-1 )
obj[i].keyframe_insert( data_path='location', frame=frame-1 )
        
obj[i].hide_render = False
obj[i].hide_select = False
obj[i].keyframe_insert( data_path='hide_render', frame=frame )
obj[i].keyframe_insert( data_path='hide_select', frame=frame )
出現時にキーフレームを配置するまでは、表示や相互作用を無効にして、出現した時に初めて有効になるように、出現時の処理にいくつか追加をしました。
アニメーション再生(Alt-A)を一度実行するとシーンが構築されます。 その状態でレンダリングを行うことで、シーンをもとにレンダリングを実行することができるようになりました。

スクリプトダウンロード

inserted by FC2 system