【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エリアのログが消えます。