めもてう

忘れっぽいTIPs、HowToのメモ帳です。

【Blender 2.8 アドオン開発】003 Blender 内のデータにアクセスしよう(Context と データ構造 と レイアウト)

前回Blender 上で API を調べる方法を説明しました。
ですが、前回使用した APIBlender に操作を指示する命令 (bpy.ops) だけでした。

Blenderモデリングだけをみても 3D Object、Camera、Light、などがあり、さらにそれぞれの 3D 座標上の位置など様々なデータを保持しています。
そして、3D View がオブジェクトモードなのかエディットモードなのかなど、アプリケーションとしての情報もあります。
アドオンを開発するにあたり、これらの情報やデータにアクセスし、判別したり追加したり変更したりすることが必須になってきます。

今回は、API を通じてBlender が保持する情報やデータへのアクセスの仕方。
そして、その情報やデータがどういう構造で格納されているのかを、説明できればとおもいます。

それではレイアウトを「Scripting」にして新規テキストを作成し、コンソールウィンドウを出してください。
やり方はこちらを見てください。

目標

Blender 内のデータへアクセスしてみよう

データ構造

Blender ファイルごとにデータベースを持っており、データはデータベースへに格納されています。
その中でデータ同士が関連するデータを参照している構造になっており、データベースへアクセスする APIbpy.data以下に公開されています。

例えば、データベース内にあるオブジェクトへアクセスしたい場合

import bpy

for object in bpy.data.objects:
  print(object.name)

# Camera
# Cube
# Light

とすれば、データベース内に保持されているオブジェクト名を列挙できます。
bpy.data.objectsはデータベース内のオブジェクトが格納されているコレクションです。
nameで名前を取得していますが他にもtypeとすればそのオブジェクトの種類 (MESH、CAMERA、LIGHT など)を取得できます。

注意しなければならないのは、この objects コレクションに格納されているデータはシーンなどの区分なく『全て』のオブジェクトが格納されているということです。

試しにシーンを追加して、追加したシーンへキューブを追加した状態で再度スクリプトを実行してみてください。
シーンの追加はウィンドウ右上から行えます。

f:id:Hobbyist:20190210161207p:plain

元からあったシーンにあるオブジェクトと新しく追加したシーンにあるオブジェクト全てが列挙されたとおもいます。
では、シーンごとのオブジェクトへのアクセスはどうするかというと、

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.001Cubeと名前を変えると、Scene が参照している Cube の名前が Cube.001 へ変わります。
これは objects コレクション内で名前のバッティングが起きるためです。

これをふまえて次は Context の説明です。

Context

コンテキストへアクセスする APIbpy.context以下に公開されています。
Context は現在の状態を保持していると説明されることが多いです。
もちろん間違いではないのですが、最初のころはいまいちピンとこなかったのが正直なところです。

ですが、各データがそれぞれのコレクションに格納されており、それぞれを必要に応じ参照しているという、データ構造を考えればこの Context の「現在の状態を保持している」というのは非常に強力なものであることがわかります。

例えば、現在表示しているシーン (current scene) の名前を取得したい場合、bpy.dataから取得しようと思えば

print(bpy.data.window_managers[0].windows[0].scene.name)

# Scene or Scene.001

となります。

Blender はマルチウィンドウが可能なので、現在表示しているシーンはウィンドウごとに違う場合があります。ですので表示しているシーンを参照しているのはwindowです。
そして、windowwindow_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が変更されます。

レイアウト構造

前回の最後に実行したAPI

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.info リファレンス

このことを説明しているのは、bpy.opsのページです。

bpy.ops リファレンス

それは、API オペレーターの共通項だからだと思うのですが、これって目次に書いてある様なものだと思うんですよね。
もちろん、ドキュメントやリファレンスは隅々まで読むに越したことはないんですが・・・罠です。

とはいえ、このスクリプトを実行すれば晴れて Infoエリアのログが消えます。

まとめ

だいぶ駆け足になってしまいましたが、以上でデータアクセス、レイアウト、Context の説明は終わりです。

大分ギュッとした説明になってしまいました。
ケーススタディーで説明した方が良かったのでは?と己の構成力の無さ、説明力の無さに泣きながら書きました。

ですが、ここまで説明したことは僕がアドオンを作る際に頭を悩ませたことですので誰かの悩みの一助になればと思います。

次回から、いよいよアドオン作成に取り掛かります。

最後になりますが、ここで記載されているソースコードを使用する場合は自己責任でお願いします。

プライバシーポリシー (Privacy policy)