Build Reliable Export Tools with Blender’s Depsgraph

Build Reliable Export Tools with Blender’s Depsgraph

Pythonスクリプトを作成してBlenderでジオメトリをエクスポートまたは解析するなら、たいてい嘘をついています。obj.dataを取得して頂点をループし、数値を得ても、それはビューポートで見ているものやレンダーが生成するものと一致しません。犯人はほぼいつも同じです。モディファイア、ドライバ、制約が適用される前の元のメッシュデータを読んでいるのです。

しかしBlenderには、シーン内のあらゆるオブジェクトの最終版を生成する仕組みがあります。これは依存関係グラフ(dependency graph)で、depsgraphと呼ばれます。この記事では、それにアクセスする方法、何をすべきか、そしてパイプラインで使える実用的なエクスポートツールを作る方法を、あなたに正確に示します。

その結果、シーン内のすべてのメッシュオブジェクトを反復し、モディファイア適用後のジオメトリを読み取り、頂点数、ポリゴン数、ワールド空間での寸法を含むCSVレポートを書き出す、動作するPythonスクリプトが手に入ります。このレポートは、ポリゴン予算のチェックにそのまま組み込めたり、事前レンダリング検証ステップとして使えたり、アセットが次のチームへ渡る前の引き継ぎ監査(handoff audit)にしたりできます。

Depsgraphとは

Blenderのdepsgraph(依存関係グラフ)は、オブジェクト、モディファイア、制約、ドライバ、シェイプキー、アニメーションなど、シーン内のデータ同士の関係や依存関係を追跡する内部システムです。そして、それらを、各ノードがデータの一部を表し、各エッジが依存関係を表す有向非巡回グラフ(DAG)に整理します。

何かが変わると、たとえばオブジェクトを移動したり値を変更したりした場合、depsgraphは、最小限で再計算が必要なものの集合を判断し、重複作業を避けるために正しい順序で更新します。

depsgraphは、Blenderの評価を正しくします(常に正しい順序で更新されるため、別のオブジェクトに依存する制約は常に最新のトランスフォームを見ます)。同時に効率も高めます(グラフの変更された部分だけを再評価するためです。これは、何百ものオブジェクト、シミュレーション、ドライバを含む複雑なシーンでのパフォーマンスにとって重要です)。

さらに、CoW(Copy-on-Write)といった機能を可能にし、元データを破損させずに、評価済みのデータのコピーをビューポートやレンダーが扱えるようにします。また、グラフの独立したブランチを並列に処理できるマルチスレッド評価も支えています。

Use Cases

アニメーションスタジオのテクニカルディレクターやパイプラインエンジニアにとって、depsgraphはBlenderのPython API経由で直接アクセスでき、強力なスクリプト利用の可能性を開きます。

TDは、手作業でベイクせずに、外部レンダラーやゲームエンジンへ最終メッシュデータをエクスポートするために、完全に評価されたオブジェクトのバージョンを取得できます。

スタジオはまた、評価済みのオブジェクトインスタンスを反復して、シーン内のあらゆるパーティクルインスタンス化されたプロップやジオメトリノードで散布されたアセットを収集し、カスタムエクスポート用パイプラインやアセットレポートツールに活用できます。

depsgraphは、更新タグ(update tags)を監視することでフレーム間の変化を検出できます。これにより、依存関係が実際に変更されたときだけアセットを賢く再エクスポートまたは再処理できるスマートキャッシュシステムに対応します。これは長時間動作するレンダーファームのパイプラインで大幅な時間節約になります。

1. Depsgraphへの参照を取得する

depsgraphは現在のコンテキストから利用できます。Scriptingワークスペース、またはオペレーターの内部では、evaluated_depsgraph_getメソッドで取得します:

import bpy

depsgraph = bpy.context.evaluated_depsgraph_get()

この呼び出しは、現在のフレームにおけるシーンの完全に評価された状態を表すDepsgraphオブジェクトを返します。

スクリプトが評価済みデータを読む前にシーンを変更する場合は、Blenderに再計算を伝える必要があります:

depsgraph.update()

この呼び出しがないと、depsgraphは変更前の状態を反映します。そして、読み取り専用スクリプトなら不要です。

2. 各オブジェクトの評価済みバージョンを取得する

depsgraphがあってもそれだけでは不十分です。そこから、各オブジェクトの評価済みコピーを取得する必要があります。これはevaluated_get()で行います:

obj = bpy.context.active_object
obj_eval = obj.evaluated_get(depsgraph)

evaluated_get()の呼び出しは、そのオブジェクトの一時的な評価済みコピーを返します。これは元のオブジェクトではありません!このコピーに加えた変更は、スクリプトが終了した後は保持されません。あなたはそれを読むだけです。

この評価済みオブジェクトでは、obj_eval.dataがモディファイア適用後のメッシュです。一方、元のオブジェクトではobj.dataはモディファイア適用前のメッシュのままです。同じスクリプト内で両者を混ぜると、エラーメッセージなしで不正確な結果になります。そのため、バグの発見が難しくなります。

評価済みオブジェクトは、正確なトランスフォームデータも提供します。obj_eval.matrix_worldは、ベースのトランスフォーム値だけでなく、制約やドライバも反映しています。

ワールド空間のバウンディングボックスを計算したり、正しい位置を必要とするエクスポータを作ったりする場合は、評価済みオブジェクトからトランスフォームを読み取ってください。

3. メッシュに変換してデータを読む

評価済みオブジェクトは、to_mesh()を通じてメッシュにアクセスできます。これは、反復可能なスタンドアロンのメッシュデータブロックを作成します:

mesh = obj_eval.to_mesh()

これはタダではありません。メモリを確保するため、解放する責任があります。読み取りが終わったら次を呼び出します:

obj_eval.to_mesh_clear()

to_mesh_clear()を呼ばないと、Blenderセッション内でメモリリークが発生します。数百のオブジェクトを含む大規模シーンでは、それがすぐに積み上がります。エラーが起きても確実に後処理が行われるように、必ずtry/finallyブロックでメッシュ読み取りを包んでください:

mesh = obj_eval.to_mesh()
try:
    for poly in mesh.polygons:
        print(poly.area)
finally:
    obj_eval.to_mesh_clear()

デフォルトでは、to_mesh()はパフォーマンスのためにUVマップや頂点カラーレイヤーを削除することがあります。エクスポートスクリプトでそれらが必要なら、追加のパラメータを渡します:

mesh = obj_eval.to_mesh(preserve_all_data_layers=True)

生成されたメッシュオブジェクトにより、mesh.verticesmesh.polygonsmesh.loopsmesh.uv_layersmesh.vertex_colorsへアクセスでき、最終的に評価されたジオメトリを反映できます。

4. 本番用スクリプトを作る

完成した例のスクリプトを見てみましょう。これは、アクティブなシーン内のすべてのメッシュオブジェクトを反復し、評価済みのジオメトリを読み取り、ホームディレクトリにCSVレポートを書き出します:

import bpy
import csv
import os

def get_evaluated_mesh_stats(context, output_path):
    depsgraph = context.evaluated_depsgraph_get()
    rows = []

    for obj in context.scene.objects:
        if obj.type != 'MESH':
            continue

        obj_eval = obj.evaluated_get(depsgraph)
        mesh = obj_eval.to_mesh()

        try:
            vert_count = len(mesh.vertices)
            poly_count = len(mesh.polygons)
            dims = obj_eval.dimensions

            rows.append({
                "name": obj.name,
                "verts": vert_count,
                "polys": poly_count,
                "dim_x": round(dims.x, 4),
                "dim_y": round(dims.y, 4),
                "dim_z": round(dims.z, 4),
            })
        finally:
            obj_eval.to_mesh_clear()

    with open(output_path, "w", newline="") as f:
        writer = csv.DictWriter(
            f,
            fieldnames=["name", "verts", "polys", "dim_x", "dim_y", "dim_z"]
        )
        writer.writeheader()
        writer.writerows(rows)

    print(f"Report written to {output_path}")


get_evaluated_mesh_stats(
    bpy.context,
    os.path.expanduser("~/mesh_report.csv")
)

Scriptingワークスペースで、このスクリプトをテキストエディタに貼り付けて「Run Script」を押すことで実行できます。出力ファイルは~/mesh_report.csvに保存されます。どんなスプレッドシートアプリでも開けます。

各行には、オブジェクト名、モディファイア適用後の頂点数とポリゴン数、そしてX・Y・Zのワールド空間におけるバウンディングボックスの寸法が含まれます。寸法はobj_eval.dimensionsから取得します。これは評価済みオブジェクトを読んでいるため、モディファイアスタック全体を反映します。

自分のパイプラインに合わせるには、コレクションでフィルタしたり、ポリゴン予算の閾値を追加してそれを超えるオブジェクトをフラグしたり、CSV出力をJSONペイロードに置き換えて、プロジェクト管理ツールやアセットデータベースに簡単に取り込めるようにしたりできます。

Conclusion

この例のスクリプトは基本的な出発点です。しかし、評価済みメッシュデータを確実に読み取る方法を理解すれば、同じパターンはエクスポータ、LODバリデータ、コリジョンメッシュジェネレータ、事前レンダリングのジオメトリ監査などにもそのまま適用できます。重要な洞察はそれらすべてのツールに移植できます。つまり、常に評価済みオブジェクトから読み取り、常にto_mesh_clear()で後処理を行い、そして同じ操作で元データと評価済みデータを混ぜないことです。

次の論理的なステップは、bpy.app.handlers.save_preを使ってこれをファイル保存ハンドラとして登録し、アーティストが品質保証のためにシーンを保存するたびにレポートが自動生成されるようにすることです。これにより、ポリゴン予算の違反をレンダリング工程に到達する前に見つけられます。