PythonイベントハンドラーでBlenderを自動化する

PythonイベントハンドラーでBlenderを自動化する
⚙️
Blenderのイベントを使えば、アーティストに追加の手順を課すことなくワークフローを自動化できます。

深夜2時にレンダリングが完了するのに、誰も見ていない。出力は誰かが「移動しなきゃ」と思い出すまで、一時フォルダに置かれたままです。アーティストが間違った名前でファイルを書き出します。納品前に、クライアントへの受け渡し用として正しくない焦点距離のままカメラが残ってしまいます。

これらの問題は積み重なります。幸い、すべてに共通する簡単な解決策があります。BlenderのPython APIでは、アプリケーションを動かすイベントに直接アクセスできます。そうしたイベントをリッスンするコードを書けば、アーティストの関与なしに、それらを自動的に処理できます。この記事の最後までに、自分のパイプラインに合わせて適用できる2つの動作する実例を手に入れられます。


Blenderでイベントをリッスンする3つの方法

BlenderのPython APIでは、イベントに応答するための主な仕組みが3つあります:

  • app.handlers は受動的なリスナーで、Blenderが特定のアクションを実行すると発火します。たとえばレンダが完了したとき、ファイルが読み込まれたとき、フレームが切り替わったときなどです。コードは関数を登録し、該当のタイミングになったらBlenderが呼び出します。アーティスト側で何かをする必要がないため、背景系のパイプライン処理を自動化するのに適したツールであることが多いです。
  • モーダルオペレーターは能動的なリスナーです。特定のウィンドウにおいてBlenderのイベントループを引き継ぎ、アーティストが実時間で行う操作をすべてインターセプトします。マウスクリック、キー入力、カーソル移動など、オペレーターが終了するかキャンセルされるまで継続します。ビューポート内でアーティストが物理的に行っていることに応じて反応するインタラクティブツールを作りたいときに適したツールです。
  • 3つ目の方法として、msgbus では、アクティブオブジェクトやシーン設定のような特定のデータプロパティの変更にサブスクライブできます。便利ですが適用範囲は狭く、この記事では扱いません。

この記事で扱う2つの例は、スタジオの自動化ニーズとして最も一般的なものをカバーしています。1つ目はハンドラーによって、背景タスクをアーティストから完全に取り除きます。もう1つは、遅くて手作業が多いワークフローを、モーダルオペレーターによるワンクリックに置き換えます。


1. レンダー完了時の自動書き出し

利用可能な便利なハンドラーはいくつかあります。たとえば:

  • render_init - レンダージョブが開始したときに発火
  • render_pre - 各フレームがレンダされる直前に発火
  • render_post - 各フレームがレンダされた直後に発火
  • load_pre / load_post - .blend ファイルが読み込まれる前/後に発火
  • save_pre / save_post - .blend ファイルが保存される前/後に発火

Blenderを開き、上部タブバーからScriptingワークスペースに切り替えます。左側にPythonコンソールがあり、右側にテキストエディターが表示されます。コードはテキストエディターに書き、Alt+Pで実行してください。

また、addonを使ってスクリプトを永続化することもできます。

フルのレンダーパイプラインツールを作り込む代わりに、まずは主要なパターンを理解するために小さく始めます。つまり、レンダが完了した瞬間に発火し、タイムスタンプ付きの確認をファイルに書き込む最小限のハンドラーです。より複雑なレンダ後処理ロジックを作り込む前に、ハンドラーが正しく動いていることを検証するための良い出発点になります:

import bpy
from datetime import datetime

@bpy.app.handlers.persistent def on_render_complete(scene, depsgraph): timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") open("test.txt", "w").write(f"Completed: {timestamp}\n")

bpy.app.handlers.render_complete.append(on_render_complete)

@bpy.app.handlers.persistent デコレーターは、ファイルの読み込みをまたいでハンドラーを登録したままにするため、セッション中のシーン変更にも耐えられます。

レンダ完了時に、datetime.now() は終了時刻を取得し、それをコンパクトなタイムスタンプ文字列に整形します。その文字列は固定のパスに直接書き込まれ、レンダのたびにファイルが上書きされます。

最後に、bpy.app.handlers.render_complete.append によって関数を登録することで、レンダが完了したときにBlenderが自動的に呼び出してくれます。

完全なレンダを待たずにテストするには、Render Single Frameを使い、指定したターゲットパスに test.txt が存在し、期待するタイムスタンプが含まれていることを確認してください。

その後、ハンドラーを拡張して出力ファイルのコピー、シーンメタデータの記録、下流ワークフローのトリガーなども行えます。

このパターンは例と常に同じです。関数を定義し、必要に応じて @bpy.app.handlers.persistent でデコレートして、該当するリストに追加します。


2. モーダルオペレーター

app.handlers は、タスクが「アーティストがいまビューポートで行っていること」に応答する必要がある場合には役に立ちません。その代わりにモーダルオペレーターが必要です。

ここでのユースケースは、ワンクリックでカメラのフレーミングを行うことです。アーティストがあるオブジェクトをクリックすると、アクティブカメラがスタジオ標準の構図に合わせて位置と画角を再設定します。手動でのカメラ調整も、焦点距離の推測もありません。そのため、アーティスト間での不一致が起こりません。

モーダルオペレーターは、2つの重要なメソッドを持つクラスです:

  • invoke() はオペレーターを開始し、ウィンドウマネージャーに登録します。
  • modal() は、その後に発生するすべてのイベントを受け取り、どう扱うかを判断します。オペレーターは FINISHED または CANCELLED を返すまでアクティブのままで、イベントを受け取り続けます。
import bpy

class AutoFrameOperator(bpy.types.Operator): bl_idname = "studio.auto_frame" bl_label = "Auto Frame Selected"

def invoke(self, context, event):
    context.window_manager.modal_handler_add(self)
    return {'RUNNING_MODAL'}

def modal(self, context, event):
    if event.type == 'LEFTMOUSE' and event.value == 'PRESS':
        target = context.active_object
        if target:
            self.frame_camera_to(context, target)
        return {'FINISHED'}

    if event.type in {'RIGHTMOUSE', 'ESC'}:
        return {'CANCELLED'}

    return {'RUNNING_MODAL'}

def frame_camera_to(self, context, target):
    camera = context.scene.camera
    if not camera:
        return
    focal_length = 85
    camera.data.lens = focal_length
    
    print(f"Framed camera on: {target.name}")

def register(): bpy.utils.register_class(AutoFrameOperator)

def unregister(): bpy.utils.unregister_class(AutoFrameOperator)

ここでは studio.auto_frame というIDのもとにBlenderが公開する、再利用可能なアクションとして AutoFrameOperator というBlenderオペレーターを定義します。トリガーされると、invoke がそれをモーダルハンドラーとして登録します。つまり、すぐに実行されるのではなく、アクションがアクティブな状態のままユーザー入力を待つようになります。

modal メソッドは、あらゆるインタラクションで実行されるイベントループです。左クリックは、現在アクティブなオブジェクトを取得し、それを frame_camera_to に渡してから終了します。右クリックまたはEscでキャンセルされ、その他の入力ではオペレーターが待機し続けます。

RUNNING_MODAL の戻り値は、オペレーターを生かしてイベントをリッスンし続けるためのものです。あなたが扱う条件に一致しないイベントは、オペレーターをアクティブのままにするために RUNNING_MODAL を返すべきです。代わりに PASS_THROUGH を返すと、オペレーターに渡すだけでなく、Blenderにも通常どおりイベントを処理させます。これは、オペレーター実行中でもアーティストがビューポートを操作できるようにしたい場合に便利です。

frame_camera_to が中核となるロジックです。シーンのアクティブカメラを取得し、その焦点距離を85mmに設定します。ただし、カメラを再配置してターゲットオブジェクトを適切にフレーミングするための実際の計算はこの記事の範囲外なので、ここでは実装していません。

registerunregister は、スクリプトが読み込まれたときにオペレーターを利用可能にし、アンロードされたときにきれいに削除するための、標準的なBlenderアドオンのボイラープレートです。

アドオンとしてスクリプトをインストールした後にオペレーターを呼び出すには、F3で検索メニューを開き、「Auto Frame Selected」と入力します。ショートカットに割り当てるには、register() 関数の中に次のスニペットを追加するだけで済みます:

wm = bpy.context.window_manager
kc = wm.keyconfigs.addon
if kc:
km = kc.keymaps.new(name='3D View', space_type='VIEW_3D')
kmi = km.keymap_items.new("studio.auto_frame", type='F', value='PRESS', ctrl=True)

ショートカットには慎重にネームスペースを付けることが重要です。3Dビューでの Ctrl+F はBlenderにデフォルトの割り当てがありませんが、展開する前にスタジオで既に使われている設定を必ず確認してください。デフォルトのBlenderアクションを、気づかないうちに静かに上書きしてしまうショートカット競合は、原因の特定が難しく、アーティストを混乱させることになります。

もう1つ守るべきルールがあります。modal() メソッドは軽量に保ちましょう。modal() 内で重い計算をすると、イベントが発生するたびに(つまりマウスを動かすたびに)実行されることになります。フレーミングロジックが重い場合は、先ほど示したように別メソッドに切り出し、関連するイベントが発火したときだけ呼び出すようにしてください。たとえば frame_camera_to のようにです。


結論

これで、アーティストのワークフローに手順を追加することなく、現実のスタジオ課題に対応する2つのツールの例を手に入れました。

レンダーハンドラーなら、手作業でミスが起きやすい引き継ぎを、あなたのパイプラインから完全に排除できます。そしてモーダルオペレーターは、アーティストに対して、スタジオ標準へカメラをフレーミングするための一貫したワンクリック手段を提供します。

同じパターンはさらに広げられます。load_post ハンドラーなら、ファイルが開いた瞬間に命名規則を強制できます。depsgraph_update_post ハンドラーなら、シーンの予算を逸脱するオブジェクトを警告できるかもしれません。レンダ完了ハンドラーでは、ウェブフックへHTTPリクエストを飛ばし、ショット完了時にプロダクション用チャンネルへSlack通知を投稿するといったことも可能です。

イベントシステムはすでに用意されています。あとはリッスンを開始するだけです!

📽️
アニメーション制作プロセスについてもっと知りたいなら、Discordコミュニティに参加することを検討してください!ベストプラクティスを共有する1,000人以上の専門家とつながり、たまに対面イベントも企画しています。ぜひ歓迎します! 😊