【Blender 2.8 アドオン開発】003 Blender 内のデータにアクセスしよう(Context と データ構造 と レイアウト)
前回、Blender 上で API を調べる方法を説明しました。
ですが、前回使用した API は Blender に操作を指示する命令 (bpy.ops
) だけでした。
Blender はモデリングだけをみても 3D Object、Camera、Light、などがあり、さらにそれぞれの 3D 座標上の位置など様々なデータを保持しています。
そして、3D View がオブジェクトモードなのかエディットモードなのかなど、アプリケーションとしての情報もあります。
アドオンを開発するにあたり、これらの情報やデータにアクセスし、判別したり追加したり変更したりすることが必須になってきます。
今回は、API を通じてBlender が保持する情報やデータへのアクセスの仕方。
そして、その情報やデータがどういう構造で格納されているのかを、説明できればとおもいます。
それではレイアウトを「Scripting」にして新規テキストを作成し、コンソールウィンドウを出してください。
やり方はこちらを見てください。
目標
Blender 内のデータへアクセスしてみよう
データ構造
Blender ファイルごとにデータベースを持っており、データはデータベースへに格納されています。
その中でデータ同士が関連するデータを参照している構造になっており、データベースへアクセスする API はbpy.data
以下に公開されています。
例えば、データベース内にあるオブジェクトへアクセスしたい場合
import bpy for object in bpy.data.objects: print(object.name) # Camera # Cube # Light
とすれば、データベース内に保持されているオブジェクト名を列挙できます。
bpy.data.objects
はデータベース内のオブジェクトが格納されているコレクションです。
name
で名前を取得していますが他にもtype
とすればそのオブジェクトの種類 (MESH、CAMERA、LIGHT など)を取得できます。
注意しなければならないのは、この objects コレクションに格納されているデータはシーンなどの区分なく『全て』のオブジェクトが格納されているということです。
試しにシーンを追加して、追加したシーンへキューブを追加した状態で再度スクリプトを実行してみてください。
シーンの追加はウィンドウ右上から行えます。
元からあったシーンにあるオブジェクトと新しく追加したシーンにあるオブジェクト全てが列挙されたとおもいます。
では、シーンごとのオブジェクトへのアクセスはどうするかというと、
import bpy for scene in bpy.data.scenes: print(scene.name) for object in scene.objects: print(" " + object.name) # Scene # Cube # Light # Camera # Scene.001 # Cube.001
bpy.data.scenes
というシーンデータが格納されているコレクションへアクセスし、それぞれのシーンが参照しているオブジェクトへアクセスするという書き方になります。
一見するとシーンの中にオブジェクトがあると感じますが、Blender のデータ構造でいえばシーンからオブジェクトを参照 (リンク) していて、シーンとオブジェクトは独立したものという構造になっています。
ですので、Cube.001
をCube
と名前を変えると、Scene が参照している Cube の名前が Cube.001 へ変わります。
これは objects コレクション内で名前のバッティングが起きるためです。
これをふまえて次は Context の説明です。
Context
コンテキストへアクセスする API はbpy.context
以下に公開されています。
Context は現在の状態を保持していると説明されることが多いです。
もちろん間違いではないのですが、最初のころはいまいちピンとこなかったのが正直なところです。
ですが、各データがそれぞれのコレクションに格納されており、それぞれを必要に応じ参照しているという、データ構造を考えればこの Context の「現在の状態を保持している」というのは非常に強力なものであることがわかります。
例えば、現在表示しているシーン (current scene) の名前を取得したい場合、bpy.data
から取得しようと思えば
print(bpy.data.window_managers[0].windows[0].scene.name) # Scene or Scene.001
となります。
Blender はマルチウィンドウが可能なので、現在表示しているシーンはウィンドウごとに違う場合があります。ですので表示しているシーンを参照しているのはwindow
です。
そして、window
はwindow_manager
に管理されていて、window_manager
は複数個もてるのでコレクションで管理されています。
今回は1ウィンドウなので windows
のインデックスはベタ打ちしていてこのスクリプトで取得できますが、マルチウィンドウでそれぞれのウィンドウに表示しているシーンが違う場合、やったことはないですが、カーソル位置からウィンドウを判定して・・・と、もっとややこしくなります。
それが、bpy.context
から取得する場合は
print(bpy.context.scene.name) # Scene or Scene.001
で、すみます。
Context は Blender が自動で設定していて、変更不可です。
そのため、bpy.context
直下は読み取り専用 (Read Only) として公開されています。
例えば、現在のシーンを別のシーンに切り替えたい場合、
import bpy # Scene が表示されているとして、Scene.001 へ変更 bpy.context.scene = bpy.data.scenes['Scene.001']
とした場合、Context の scene は読み取り専用だよとアトリビュートエラーがでます。
この場合は、
import bpy # Scene が表示されているとして、Scene.001 へ変更 print(bpy.context.scene.name) bpy.context.window.scene = bpy.data.scenes['Scene.001'] print(bpy.context.scene.name) # Scene # Scene.001
これでシーンが変更されます。
bpy.context
直下のアトリビュート、この場合はbpy.context.window
は読み取り専用として公開されていますが、そのアトリビュートが参照しているデータはそうではないためです。
- 現在 (
bpy.context
) のシーンを変更 -> Error - 現在 (
bpy.context
) のウィンドウが参照しているシーンを変更 -> O.K.
そして、bpy.context.window.scene
を変更すると Context が更新されてbpy.context.scene
が変更されます。
レイアウト構造
bpy.ops.info.report_delete()
を思い出してください。結果は context が違うよとエラーが出たと思います。
もう少し具体的にいうと、context のアトリビュートが不正だよ。というエラーです。
context が現在の状態(データ)を保持しているというのは前述の通りですが、context はウィンドウの現在のレイアウト構造も保持しています。
ここでいう、レイアウト構造を保持しているというのは「スクリプトがどこで実行されたか」ということです。
試しに Python Console で、
print(bpy.context.area.type) #CONSOLE
を実行してみてください。結果は「CONSOLE」となっているとおもいます。
次に、Text Editor で、
import bpy print(bpy.context.area.type) #TEXT_EDITOR
を実行してください。コンソールウィンドウへ「TEXT_EDITOR」と出力されたかと思います。
bpy.ops.info.report_delete()
という API は内部でこのbpy.context.area
を見ていて、INFOエリアで実行されていない場合、エラーとなる仕様になっています。
では、bpy.ops.info.report_delete()
は INFOエリア以外から実行出来ないのか?というともちろんそんなことはありません。
context が違うなら context を変えてやればいいのです。
それが Context のオーバーライドです。
が、その前に Blender 内のレイアウト構造について少し説明しておきます。
window を最上位に置いた場合、その中に workspace, その中に screen その中に area その中に region 最後に ui という構造になっています。
workspace | レイアウト構成のプリセット |
screen | 実際のレイアウト構造を持つ、現在の状態とデフォルト構造など複数ある |
area | 各編集エリア、ユーザー操作により複数、レイアウト上にある。 |
region | エリア内のレイアウト、ヘッダーとか |
レイアウト構造を列挙するスクリプト
import bpy window = bpy.context.window workspace = window.workspace print(workspace.name + " : Workspace") for screen in workspace.screens: print(" " + screen.name + " : Screen") for area in screen.areas: print(" " + area.type + " : Area") for region in area.regions: print(" " + region.type + " : Region")
どうでしょうか?レイアウト構造がなんとなく見えましたでしょうか?
とはいえ、「現在の」レイアウト構造を知りたいのであれば context からアクセスできるので「このエリアは今開いてるかな?」とか、「スクリーンに何個エリアがあるかな?」とか参照目的以外でスクリプトからレイアウト構造をゴリゴリいじるというのはあまり考えにくいので親子関係だけ覚えておけばひとまず大丈夫かとおもいます。
#「現在の」スクリーンの名前を取得 print("Current Screen : " + bpy.context.screen.name)
Contextオーバーライド
まずはスクリプトを見てください
import bpy override = bpy.context.copy() for area in bpy.context.screen.areas: if area.type == 'INFO': override['area'] = area bpy.ops.info.select_all(override) bpy.ops.info.report_delete(override)
まず、大前提として、前述したとおり context は Blender によって自動的に設定されるので変更などは出来ません。出来てもしてはいけません。
ですので、コピーをします。それがbpy.context.copy()
で、戻り値は辞書型のコレクションです。
そして、現在のスクリーン内に Infoエリアがあるか判定をして、コピーした context(override) のエリアの項目を上書きします。
最後に API を呼ぶのですが、これが大きな罠です。
Blender API の命令群は引数に Context を指定できます。指定が無ければ Blender が自動設定した context が渡ります。
罠というのも、Pythonコンソールで調べたり、リファレンス内のbpy.ops.info
ページを見ても context が渡せるなんて書いていません。
このことを説明しているのは、bpy.ops
のページです。
それは、API オペレーターの共通項だからだと思うのですが、これって目次に書いてある様なものだと思うんですよね。
もちろん、ドキュメントやリファレンスは隅々まで読むに越したことはないんですが・・・罠です。
とはいえ、このスクリプトを実行すれば晴れて Infoエリアのログが消えます。