シーン内の全てのメッシュオブジェクトの原点を変更する
Pythonを使ってBlenderでできる事をまとめています。解説もあります。
はじめに
レシピ一覧
Pythonの難易度: ★★★★☆
動機
原点、ずれますよね?
Ctrl-Shift-Alt-C押しにくいですよね。
まとめて変更しましょう。
細かい仕様
- オブジェクトの原点(Origin)をGeometryの中心に移動してください。
- 原点の変更はメッシュオブジェクトだけにしてください。
- 全てのオブジェクトを選択しているとは限りません。
- オブジェクトによっては隠れていたり、別レイヤーにあったりするかもしれません。
- 現在の選択状態、アクティブ状態などは変更しないでください。
サンプルコード
import bpy
objs = [obj for obj in bpy.data.objects
if obj.type == "MESH"]
override = bpy.context.copy()
override["selected_editable_objects"] = objs
bpy.ops.object.origin_set(override, type='ORIGIN_GEOMETRY')
上記サンプルコードを、Blender内蔵のテキストエディタに貼り付けた後、Alt-Pを実行してください。
解説
bpy.ops
の使用とcontextのオーバーライド
bpy
はBlenderのpython APIのライブラリーですが、その中のbpy.ops
は「Blenderを編集するための関数」を格納しています。
このbpy.ops
内の関数を使うときに欠かせない考え方が、contextとcontextのoverrideです。
なお、今回説明するcontextのoverrideは公式ドキュメントでも紹介されていますが、ドキュメントが少なく、APIとして未完成な点が多いと感じています。
contextとは
同じくbpy
に含まれる、bpy.context
は現在のcontextを保持しています。
contextは英語で「文脈」を指しますが、blenderにおけるcontext(=文脈)とは一体何でしょうか?
それを確かめるために以下のコードを実行してみましょう。
import bpy
from pprint import pprint # 辞書を綺麗に表示するために使っています
context = bpy.context.copy()
pprint(context)
以下は出力結果の一部です。
{'active_base': bpy.data.scenes['Scene']...ObjectBase,
'active_object': bpy.data.objects['Cube'],
# 中略
'active_object': bpy.data.objects['Cube'],
'active_operator': None,
# 中略
'mode': 'OBJECT',
'object': bpy.data.objects['Cube'],
# 中略
'scene': bpy.data.scenes['Scene'],
'screen': bpy.data.screens['Scripting'],
# 中略
'selectable_objects': [bpy.data.objects['Cube'],
bpy.data.objects['Lamp'],
bpy.data.objects['Camera']],
'selected_bases': [bpy.data.scenes['Scene']...ObjectBase],
# 中略
'selected_objects': [bpy.data.objects['Cube']],
# 中略
'window': bpy.data.scenes['Scene']...Window,
'window_manager': bpy.data.window_managers['WinMan']}
このように、contextの中には「選択されたオブジェクトたち"selected_objects"
」や「3Dビューのモード"mode"
」などの情報が格納されています。色々試して、どういう項目がどういう時に変わるのか試してみて下さい。
bpy.ops
とcontextの関係
bpy.ops
内の関数は全て、このcontextを参照して実行されています。
例えば、Gキーでオブジェクトを移動することは、bpy.ops.transform.translate
を実行することと同じです。
例えば、以下のコードを実行したとします。
bpy.ops.transform.translate(value = (1,1,1))
すると、その時に移動するのは選択されているオブジェクトです。
これはつまり、bpy.ops.transform.translate
がbpy.context
から、選択中のオブジェクトの情報を得ていることを示しています。
選択外のオブジェクトを操作する
選択中のオブジェクト以外を対象にbpy.ops
の関数を実行したい時があります。
例として、bpy.data.objects["Lamp"]
以外の何かを選択している状態で、bpy.data.objects["Lamp"]
を移動させたいと仮定します。
一番わかりやすい手法は以下の通りです。
import bpy
# 実行前の選択を保持
old_selected_objects = [obj for obj in bpy.data.objects if obj.select]
# 選択解除
bpy.ops.object.select_all(action='DESELECT')
# Lampだけ選択
bpy.data.objects["Lamp"].select = True
# 移動
bpy.ops.transform.translate(value = (1,1,1))
# 元に戻す
bpy.data.objects["Lamp"].select = False
for obj in old_selected_objects:
obj.select = True
ここで重要なのは「bpy.ops
の関数などを駆使して、contextを直接、変更している」ことです。
これには2つ問題があります。
1. コードがわかりにくい
様々な手法を駆使するため、それぞれ手法の役割が把握しにくくなります。
2. contextを元に戻せる確証がない
先ほどのコードでは、実はアクティブなオブジェクトが変わっている可能性があります。
このように、一度変えてしまったcontextは簡単には戻せません。
ここで出てくる概念がoverride contextです。
override context
"override context"とは「bpy.contextとは異なるcontextをbpy.ops
に伝えて実行すること」を指します。
bpy.ops
内の全ての関数は実は「第1引数にcontextを受けることが出来る」のです。ここでいうcontextとは、bpy.context.copy()
で取得できるような辞書のことを指します。
以下は、ランプを移動するコードを書き換えたものです。
import bpy
# {Object.name : ObjectBase} の対応を作る
obj2base = {obj_b.object.name : obj_b for obj_b in
bpy.data.scenes[0].object_bases}
# コンテクストをコピー
override = bpy.context.copy()
# コンテキストを書き換える
t_name = "Lamp"
obj = bpy.data.objects[t_name]
obj_b = obj2base[t_name]
# 以下のコメントアウトは一旦無視してください
# override["active_object"] = obj
# override["acitve_base"] = obj_b
# override["selectable_objects"] += [obj]
# override["selected_objects"] = [obj]
# override["selected_editable_objects"] = [obj]
# override["selectable_bases"] += [obj_b]
override["selected_bases"] = [obj_b]
# override["selected_editable_bases"] = [obj_b]
# 実行
bpy.ops.transform.translate(override, value=(1,1,1))
上のコードでは、
- bpy.contextから現在のcontextをディープコピーする
- コピーしたcontext(
override_context
)を編集する - 変更したcontextを
bpy.ops
の関数の第1引数に渡した上で実行する
という流れを踏んだものです。
このoverride_context
はただの辞書ですので、何をどうしようと元のcontextに影響しません。なんなら元のcontext.copy
を使わずに0から作っても、必要なキーが揃っていればcontextとして正常に作動します。
override contextの問題点
ここでクセモノなのが「必要なキー」です。どのcontextを書き換えるべきかは、実はよくわかりませんし、ドキュメントもありません。
たとえばbpy.ops.transform.translate
の場合、selected_bases
ですが、bpy.ops.object.origin_set
ならselected_editable_objects
を書き換える必要があります。別の関数なら別のキーです。Objectへの操作なら大概、コメントアウトされているキーのどれかです。
この問題については、私が使った便利ツールを記事の最後に載せておきますので良ければお使いください。
以下、selected_bases
について説明します。
Object Base
Lamp移動のサンプルでは
override["selected_bases"] = [base]
のように、"selected_bases"
を変更しています。
これは、bpy.types
で定義されているObjectBase
のインスタンス(のリスト)です。
このObjectBase
は本来、blender内部実装深くの住人ですのでその正体に深入りはしません。
次の事項について把握しておけば問題ないでしょう。
-
ObjectBase
はオブジェクトを格納しており、ObjectBase.object
からアクセスできる -
ObjectBase
一覧にアクセスする方法は多分bpy.data.scenes[シーン名/インデックス].object_bases
だけ - そのオブジェクトを格納している
object_base
を書き換えればよく、「object
も書き換えて整合性を取る」というのは要らない -
bpy.context
はcontextと同じだが、ObjectBase
のプロパティーは持たない
もっとObjectBase
について深く知りたいなら、公式ドキュメントにちょろっと書いてありますので参考にしてください。
私にはよく理解できなかったのですが、Objectのインスタンスを格納するもので、作業中にObjectを削除しても処理が重くならないようにしてくれるもの、だと思ってます。
便利ツール
context_overrideを便利に使うContextEditor
というクラスを作りましたので、ぜひお使いください。
使用例
c = ContextEditor()
c["selected_objects"] = []
c["object"] = bpy.data.objects["Cube.001"]
bpy.ops.object.origin_set(c.rc,type='ORIGIN_GEOMETRY')
"""
- The user instantiates the ContextEditor and edit it via dict-like access.
- c["selected_objects"] = [] will clear not only selected_objects but also bases and other related ones.
- c["object"] = Object will also set keys like active_object and selected_editable_bases.
- To pass ContextEditor as an override_context, the user uses the rc prop
"""