1. KTakahiro1729

    Posted

    KTakahiro1729
Changes in title
+#bpy_cookbook シーン内の全てのメッシュオブジェクトの原点を変更する (Python for Blender クックブック)
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,227 @@
+# シーン内の全てのメッシュオブジェクトの原点を変更する
+Pythonを使ってBlenderでできる事をまとめています。解説もあります。
+[はじめに](https://qiita.com/KTakahiro1729/items/5de5f60982af37d23aa0)
+[レシピ一覧](https://qiita.com/KTakahiro1729/items/57f4e1f5495c5e54d431)
+
+Pythonの難易度: ★★★☆☆
+
+### 動機
+原点、ずれますよね?
+<kbd>Ctrl</kbd>-<kbd>Shift</kbd>-<kbd>Alt</kbd>-<kbd>C</kbd>押しにくいですよね。
+まとめて変更しましょう。
+
+### 細かい仕様
+- オブジェクトの原点(Origin)をGeometryの中心に移動してください。
+- 原点の変更はメッシュオブジェクトだけにしてください。
+- 全てのオブジェクトを選択しているとは限りません。
+- オブジェクトによっては隠れていたり、別レイヤーにあったりするかもしれません。
+- 現在の選択状態、アクティブ状態などは変更しないでください。
+
+## サンプルコード
+
+
+```python
+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内蔵のテキストエディタに貼り付けた後、<kbd>Alt</kbd>-<kbd>P</kbd>を実行してください。
+
+## 解説
+### `bpy.ops`の使用とcontextのオーバーライド
+`bpy`はBlenderのpython APIのライブラリーですが、その中の`bpy.ops`は「Blenderを編集するための関数」を格納しています。
+この`bpy.ops`内の関数を使うときに欠かせない考え方が、contextとcontextのoverrideです。
+
+なお、今回説明するcontextのoverrideは[公式ドキュメント](https://docs.blender.org/api/2.79/bpy.ops.html#overriding-context)でも紹介されていますが、ドキュメントが少なく、APIとして未完成な点が多いと感じています。
+
+#### contextとは
+同じく`bpy`に含まれる、`bpy.context`は現在のcontextを保持しています。
+contextは英語で「文脈」を指しますが、blenderにおけるcontext(=文脈)とは一体何でしょうか?
+それを確かめるために以下のコードを実行してみましょう。
+
+```python
+import bpy
+from pprint import pprint # 辞書を綺麗に表示するために使っています
+context = bpy.context.copy()
+pprint(context)
+```
+
+以下は出力結果の一部です。
+
+```python
+{'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を参照して実行されています。
+例えば、<kbd>G</kbd>キーでオブジェクトを移動することは、`bpy.ops.transform.translate`を実行することと同じです。
+例えば、以下のコードを実行したとします。
+
+```python
+bpy.ops.transform.translate(value = (1,1,1))
+```
+
+すると、その時に移動するのは選択されているオブジェクトです。
+これはつまり、`bpy.ops.transform.translate`が`bpy.context`から、選択中のオブジェクトの情報を得ていることを示しています。
+
+#### 選択外のオブジェクトを操作する
+選択中のオブジェクト以外を対象に`bpy.ops`の関数を実行したい時があります。
+例として、`bpy.data.objects["Lamp"]`以外の何かを選択している状態で、`bpy.data.objects["Lamp"]`を移動させたいと仮定します。
+
+一番わかりやすい手法は以下の通りです。
+
+```python
+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`に伝えて実行すること」を指します。
+Autocompleteだとわかりませんが、`bpy.ops`内の全ての関数は実は「第1引数にcontextを受けることが出来る」のです。ここでいうcontextとは、`bpy.context.copy()`で取得できるような辞書のことを指します。
+
+以下は、ランプを移動するコードを書き換えたものです。
+
+```python
+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))
+```
+
+上のコードでは、
+
+1. bpy.contextから現在のcontextをディープコピーする
+2. コピーしたcontext(`override_context`)を編集する
+3. 変更した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移動のサンプルでは
+
+```python
+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`について深く知りたいなら、[公式ドキュメント](https://en.blender.org/index.php/Dev:Source/Architecture/Overview#Data_structure)にちょろっと書いてありますので参考にしてください。
+私にはよく理解できなかったのですが、Objectのインスタンスを格納するもので、作業中にObjectを削除しても処理が
+
+### 便利ツール
+context_overrideを便利に使う[`ContextEditor`というクラス](https://gist.github.com/KTakahiro1729/cd476845b275a01d343318edb0fab5db)を作りましたので、ぜひお使いください。
+
+
+### 使用例
+
+```python
+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
+"""
+```