Blender 上で Python を使ってあれこれ情報を覗いたり操作する
概要
-
趣旨
「どのオブジェクトにアクセスしたら、目的とするblender上のデータが得られるのか」の調べ方で多分に躓いた、
過去の自分宛てメモの 一節 的な書き散らし -
環境
Blender 4.1.1 -
留意
既存のモデルや.fbx
のボーン位置・名前などを一括変更したり、
オブジェクトの構成を変えたい…といった、
いわゆる 自動変換系マクロ 目的の偏った知識が多めかも
実行環境
メニュー最上部スクリプト作成 タブからPython実行環境に触れられる。
左側のコンソール画面 >>>
行から対話可能
例 : bpy.app.version
と実行すれば、Blenderのバージョン値が返ってくる
bpy について
Blender と Python をつなぐライブラリ…というかモジュールというかAPIというか。
ほぼすべての情報はbpy.
を辿ってアクセス・操作するといっても過言ではない
-
公式ドキュメント
Blender Python API Documentation -
紐づいたオブジェクトを覗く・調べる
対話コンソール上でオートコンプリートが使えるので、
bpy.
まで打ち込んで Tab キーを押せば、諸々の配下が候補に挙がる
.py スクリプトを書いて実行
「スクリプト作成」タブ → テキストエディター欄から、 .py
を Blender に読ませて実行可。
- 適当な場所に用意した任意
.py
を読み込む
「テキスト → 開く」 or 右上のフォルダアイコンボタンから開けます 適当に書く
スクリプトをそのまま上書き保存したければAlt+S
( Cltr + S ではないので注意 )
※ 自環境では日本語入力が出来なかったので、後述の「外部エディタ → 都度読み込み」
…のほうが自分の性に合ってました- 都度右上ボタンから実行 or
Alt+P
- × ボタン (リンク切断) が ファイルを閉じる系 相当っぽい
-
外部エディタを使いたい場合
外部エディタで更新保存されたことを検知して再読み込みする機能がある。
VSCode で都度書きながら、Blender 側エディタで再読み込み → 実行 てのもアリ。 -
注 : print 出力はコンソールウィンドウで確認する事
対話機能と違いprint()
の出力先が変わるため、
上部メニュー「ウィンドウ → システムコンソール切り替え」で別ウィンドウを開いてモニタする。
開発検証には必須だと考えてます
プロジェクト内オブジェクト等へアクセス
スクリプト作成タブの アウトライナー > Blender ファイル ( "現在のファイル" 表記 ) 領域が直感的に感じる。
操作対象の場所を探したいなら、この構成を見て辿る・掘り下げると良い
( 画像にはないけど、アニメーション/カメラ/ライト etcオブジェクトもある )
bpy.data.armatures
bpy.data.window_managers
bpy.data.objects
bpy.data.cameras
bpy.data.collections
bpy.data.scenes
bpy.data.screens
bpy.data.palettes
bpy.data.brushes
bpy.data.materials
bpy.data.meshes
bpy.data.linestyles
bpy.data.workspaces
bpy.data.worlds
bpy.data.lights
bpy.data.images
...等
bpy.context と bpy.data の違い?
体感と推察でしかない、と前置きして、
bpy.context
系は、今選んでいる、今触っている、今表示している… といったものだけで
bpy.data
系は、プロジェクト全体の… という性質を持つ気がする。
なので、選択中の○○だけ操作したい なら bpy.context
(等)がアプローチ視野に入り、
一括で、○○の中にある××を操作したい なら、 bpy.data
が向いている印象
型を調べよ、おのずと道は開かれる
print(type(some_object))
print(some_object)
兎に角これで型などを吐き出させて、ググって公式ドキュメント等とにらめっこ。
下記画像例として 「bpy.data.armatures
の子要素群が bpy.types.Armature
ってやつぽいので、ググって調べるか」
…みたいな延々掘り下げプレイ。
Python ツールチップはONにするべき
Python 使うなら、少なく書も最初のうちは絶対にONにしておきたい機能
Blender の設定ウィンドウ(プリファレンス)から、「インターフェース」を選択。
「表示 > ツールチップ > Python ツールチップスを表示」と辿ってON
以降、いろいろな場所でマウスオーバーを行うと、bpy
を経由した情報へのアクセス手段/ヒントが可視化される
↓ いろんな場所でヒントが出てくるようになる
以下、スクリプトサンプル集
全コレクションの中身を覗き、属するオブジェクト名を表示
Blender Python API - Collection(ID)
import bpy
def main_process():
print(f"コレクション数 {len(bpy.data.collections)}")
for clc in bpy.data.collections:
print(f"コレクション : {clc.name}")
print(f"コレクションの子オブジェクト数 : {len(clc.objects)}")
for child in clc.objects:
print(f"子アイテム名 : {child.name}")
main_process()
(アーマチュア配下の)全ボーン名を変更
Blender Python API - Armature(ID)
※ 削除や移動などの変更は、本処理の延長線上で実現出来ないので注意
import bpy
def main_process():
print(f"アーマチュア数 {len(bpy.data.armatures)}")
for arm in bpy.data.armatures:
print(f"名前 : {arm.name}")
print(f"ボーン数 : {len(arm.bones)}")
for bone in arm.bones:
# "ボーン名" が "ボーン名.hoge" になる
new_bone_name = ( bone.name + ".hoge")
print(f"{bone.name} → {new_bone_name}")
bone.name = new_bone_name
main_process()
アーマチュアへのアクセス方法2種
<class 'bpy.types.Armature>
にアクセスしたい場合、
bpy.data.armatures
経由だと子アイテムとしてそのまま触れるが、
bpy.data.objects
経由だと、子オブジェクトの .data
を経由して得る
import bpy
def main_process():
print(f"=== bpy.data.armatures からアーマチュアを覗く")
for arm in bpy.data.armatures:
print(f"{arm.name} / {type(arm)}")
print(f"=== bpy.data.objects からアーマチュアを覗く")
for obj in bpy.data.objects:
if obj.type.upper() == "ARMATURE":
arm = obj.data
print(f"{arm.name} / {type(arm)}")
main_process()
全アーマチュア配下のボーンを削除(+注意メモ書き)
「A をするために B が必要で、Bを使うためには C を HogeHoge モードにしなきゃいけなくて…」
「しかも C を HogeHoge にしたら、今度は D を操作するために FugaFuga モードにしなきゃダメで…」
みたいなあまりにも面倒くさいお膳立ての必要がありました。
- 仕様上、以下の条件を満たさないとエラーになる模様
オブジェクトを選択状態にするには、Object.select_set(True)
を使うのだそう。
なので Object
を触れる bpy.data.objects
からのアプローチとしました。
import bpy
def main_process():
# 全アーマチュア配下のボーンを削除
for obj in bpy.data.objects:
if obj.type.upper() == "ARMATURE":
arm = obj.data
# 1. オブジェクト操作モードに切替
bpy.ops.object.mode_set(mode='OBJECT')
# 2. 対象オブジェクトをアクティブにする
bpy.context.view_layer.objects.active = obj
# 3. オブジェクト(便宜的にはアーマチュア)を選択状態にしておく
obj.select_set(True)
print(f"{arm.name}")
# 4. オブジェクトをアクティブかつ選択状態にしたので、ボーン削除のために編集モードへ移行
bpy.ops.object.mode_set(mode='EDIT')
print(f"{arm.name} 配下の全ボーンを削除")
print(f"検出ボーン数 : {len(arm.bones)}\n検出エディットボーン数 : {len(arm.edit_bones)}")
# 5. bone(s) ではなく edit_bone(s) が走査(あるいは操作)対象となる ( 洒落じゃないよ )
# ================================
for edit_bone in arm.edit_bones:
print(f"\r{edit_bone.name} を削除します")
arm.edit_bones.remove(edit_bone)
# このタイミングでもう edit_bone はなくなっているので、以下のようにアクセスできない
# print(f"\r{edit_bone.name} を削除しました") ← これはエラーとなる
main_process()
1 ~ 5 のどれか一つでも前提が崩れていると、以下のようなエラーにあたったり、
bones
の中身は検出されるのに edit_bones
が検出されなかったりとつまずいちゃうかも
typeError: Converting py args to operator properties: enum "EDIT" not found in ('OBJECT')
TypeError: ArmatureEditBones.remove(): error with argument 1, "bone" - Function.bone expected a EditBone type, not Bone
俺得プロジェクトリセット テンプレ
「一旦全部消すか」となった時、
アウトライナーのビューレイヤーアイテムを全削除(階層を削除)でそれっぽくなるものの、
オブジェクトが多数プロジェクト内に残ったまま…といった状況に苛まれました。
( 自環境だとアーマチュアが残ったりしてて、それに気づかずインポートしたモデルのボーン名が勝手に変わったりしてた )
# 新規プロジェクトを作成
bpy.ops.wm.read_homefile()
↑ このワンライナーを使えば完全初期化できるのですが、
読み込んだ .py
スクリプトまで解除されたり、表示中のアドオン等も勝手にしまわれてやや不便。
なので、もう少しマイルドな俺得リセット処理を目指して書いてみました。
プロジェクト内オブジェクト( "現在のファイル" 表記) の任意カテゴリを全削除する、というもので、
clear_categories
に、コレクション定義を突っ込めば、その中身全部消すよ みたいなやつです。
ほか消したいファイルのカテゴリがあれば、都度増やして、どうぞ。
import bpy
def main_process():
clear_categories = [
bpy.data.objects ,
bpy.data.armatures ,
bpy.data.materials ,
bpy.data.meshes ,
bpy.data.images ,
]
for target in clear_categories:
if hasattr(target, '__iter__') == False:
print(f"{target} is not iterable")
continue
print(f"{len(target)}件のデータが含まれています")
for data in target:
print(f"{data.name} を削除します")
target.remove(data)
main_process()
メッシュの位置を移動変更
全メッシュの z 位置を 0.5 増やし、角度を z に 90度増やす
角度はそのままの数値(90とか180とか)で入力できないようで、
math.radians()
を使います。
import bpy
# math のインポート忘れずに
import math
def main_process():
for obj in bpy.data.objects:
if obj.type.upper() == "MESH":
print(f"{obj.name} の位置変更")
obj.location.z += 0.5
obj.rotation_euler.z += math.radians(90)
main_process()
マテリアルの任意情報書き換えサンプル
マテリアルに割り当てられているメタリック情報の値を 0 に。
さらに、マテリアル内で使われるノードに プリンシプル BSDF のものを見つけたら、
それが持っているメタリック情報も 0 にする
import bpy
def main_process(_value):
# 全マテリアルのメタリック情報を 0 に書き換える
for material in bpy.data.materials:
#print(f"{material.name} をチェック...")
material.metallic = _value
if material.use_nodes == False:
continue
nodes = material.node_tree.nodes
for node in nodes:
# プリンシプル BSDF ならメタリック解除
if node.type == "BSDF_PRINCIPLED":
for n_input in node.inputs:
if n_input.name == "Metallic":
n_input.default_value = _value
# 引数付きで実行
main_process( 0 )
- 他参考メモ
bpy.data.materials['マテリアル名'].node_tree.get_output_node("ALL")
bpy.data.materials["マテリアル名"].node_tree.nodes["プリンシプルBSDF"].inputs[1].default_value
対象アーマチュア内の全ボーン位置を変更
個人的にちょい汎用性のある処理です。
座標などは Vector
で作成して送っています
import bpy
import mathutils
import math
# 対象アーマチュアオブジェクト配下の全ボーンを、指定座標分移動させる (=head,tailの座標変更)
def move_all_bone_positions( armature_obj , location=None ):
if isinstance( armature_obj.data , bpy.types.Armature ) == False:
raise TypeError("bpy.types.Armature ではありません")
bpy.ops.object.mode_set(mode='OBJECT')
bpy.context.view_layer.objects.active = armature_obj
armature_obj.select_set(True)
bpy.ops.object.mode_set(mode='EDIT')
edit_count = 0
for bone in armature_obj.data.edit_bones:
#print(f"{bone} , {type(bone)}")
print(f"{bone.head} , {type(bone.head)}")
print(f"{location} , {type(location)}")
print(f"A=====================")
bone.head.x += location[0]
bone.head.y += location[1]
bone.head.z += location[2]
bone.tail.x += location[0]
bone.tail.y += location[1]
bone.tail.z += location[2]
edit_count+=1
# <bpy_struct, PoseBone("xxxxxx") at 0x0000029409073008> , <class 'bpy_types.PoseBone'>
#print(f"name : {bone.name} , head-info : {bone.head} , tail-info : {bone.tail}")
print(f"{edit_count} 件のBone位置を変更しました")
for obj in bpy.data.objects:
if obj.type.upper() == "ARMATURE":
arm_obj = obj
move_all_bone_positions( arm_obj , location = mathutils.Vector((0.0 , 0.0 , 0.2)))
本来は bone.head = (bone.head @ location)
で Vector 同士の合算をさせたかったのですが、
自分の知識が甘いのかなんなのか、うまくいかず以下のようなエラーが出てしまい…
ValueError: bpy_struct: item.attr = val: sequence expected at dimension 1, not 'float'
結局、イケてない計算方法をとりました orz
もっと知識が付いたら、任意角度を変える引数とかも設けてみたいですね
( ベクトルとかいう知識が要るんだとか…難しそう )
表示中のスクリーンエリア情報を覗く
Blender Python API - Area
bpy.types.Area
import bpy
def test():
print(f"{bpy.context.window.screen.areas}")
for area in bpy.context.window.screen.areas:
print(f"{area}")
print(f"{area.type}")
test()
ボーンの長さを求める
Vecter
って3次元同士の計算できるんだ…
しかも .length
でそのまま長さを計算してくれる Σ(´ ` )
import bpy
import math
def test():
_some_bone = None
# 適当にヒットしたボーンを1本だけ漁る
for _obj in bpy.data.objects:
if isinstance( _obj.data , bpy.types.Armature ):
for _bone in _obj.data.bones:
_some_bone = _bone
break
_diff_vector = ( _some_bone.head - _some_bone.tail)
print( _diff_vector ) # <Vector (0.0000, 0.0722, 0.0000)>
print( _some_bone.name , _diff_vector.length )
test()
任意ベクトル延長線上の距離+Nの座標先を導く
座標A , 座標B があり、AからBへ2点を繋ぐように直線を引き、さらにその直線上で距離を +N した C の座標を得る
クソAA図 #伝われ
[A]
\
\
[B]
\
\ ← 距離 N
\
[C] ← ココの座標を知りたい
末端ボーンが持つ B.tail の向き(座標)を「親ボーン A.head ↔ 自身の B.head を繋ぐベクトル(向き)」に自動で合わせる
…みたいなことをしたかった #伝われ
import bpy
import numpy as np
# ココに適当なボーンデータ2つ用意して実行してください
a_bone_ = None
b_bone = None
_vec = ( b_bone.head - a_bone_.head )
_vec_norm = np.linalg.norm( _vec )
_vec_normalized = _vec / _vec_norm
_distance = ( b_bone.head - b_bone.tail ).length
#print( "ボーン長さ(距離)" , _distance )
_target_pos = b_bone.head + _vec_normalized * _distance
# print("結果座標" , _target_pos )
b_bone.tail = _target_pos
メッシュ内未使用の頂点グループを削除
import bpy
def delete_empty_vertex_groups():
# <class 'bpy_types.Object'>
# ここへ対象のメッシュオブジェクトを代入
_mesh_obj = None
remove_list = []
# 頂点グループを回す
for v_grp in _mesh_obj.vertex_groups :
grp_index = v_grp.index
# 全頂点を走査して、本グループindexが1件でも頂点データ内に含まれていれば真とする
result_mv = next(filter(lambda vt: [ True for vge in vt.groups if vge.group == grp_index ] , _mesh_obj.data.vertices), None)
if isinstance( result_mv , bpy.types.MeshVertex ) == False :
remove_list.append( v_grp )
else :
print( f"頂点グループ {grp_index}/{v_grp.name} に属する頂点データは最低1件アリ" )
print("グループ削除開始")
for _vs_group in remove_list:
print( _vs_group.name , "を削除" )
_mesh_obj.vertex_groups.remove(_vs_group)
delete_empty_vertex_groups()
VertexGroupElement からグループIndex (int)回収するの .group
なんですけど、名前がやや直感的ではない気がする。
.index
とか .group_index
とかに何でしなかったんやろか…
あと欲を言えば、VertexGroupElement
に属する頂点グループを引っぱり出せる機構が欲しかったなァと。
特定3Dビューポート内のボーン表示トグル
一部環境下(アドオンとか)であれば、以下のようなテイでいけたりします…が、
bpy.context.space_data.show_bones = none bpy.context.space_data.show_bones
実のところ bpy.context.
自体が、現在の/今表示されている といったあやふやな状態なので、
スクリプトのテキストエディタや、対話コンソール上ではうまく触れません。
色々試した結果、動作確認に於いては bpy.data.workspaces
から掘り下げるのが確実ぽいと今のところ考えています
( bpy.data.screens["Layout"].areas
アプローチからでも安定しない節があった )
import bpy
def test():
for workspace in bpy.data.workspaces:
for screen in workspace.screens:
# <class 'bpy.types.Screen'>
if screen.name.upper() != "LAYOUT":
continue
for area in screen.areas:
# <class 'bpy.types.Area'>
if area.type != "VIEW_3D":
continue
for space in area.spaces:
# <class 'bpy.types.SpaceView3D'>
space.overlay.show_bones = not space.overlay.show_bones
return
test()
bpy.types.View3DOverlay
までたどり着ければOK
ビューポートシェーディングの照明切り替え
import bpy
def test():
for workspace in bpy.data.workspaces:
for screen in workspace.screens:
if screen.name.upper() != "LAYOUT":
continue
for area in screen.areas:
if area.type != "VIEW_3D":
continue
for space in area.spaces:
if hasattr( space , "shading"):
space.shading.light = "FLAT" # "STUDIO" , "MATCAP" , "FLAT"
return
test()
Blender Python API - View3DShading
この辺のやつ色々弄れるハズ
指定プロパティ内タブをピンポイントで自動アクティブ化
「レイアウト」Screen 内 →「プロパティ」Area 内 → 「データ」Space をアクティブ化
import bpy
def main_process():
# <class 'bpy.types.Screen'>
for screen in bpy.data.screens:
if screen.name.upper() != "LAYOUT":
continue
# <class 'bpy.types.Area'>
for area in screen.areas:
if area.type.upper() != "PROPERTIES":
continue
# <class 'bpy.types.SpaceProperties'>
for space in area.spaces:
space.context = "DATA"
return
main_process()
bpy.types.Screen
bpy.types.Area
bpy.types.SpaceProperties
対象メッシュ内の選択中頂点データを得る
bmesh
を使ってます
import bpy
import bmesh
def get_active_mesh_selected_vertices():
# とりあえずアクティブオブジェクトとした
mesh_obj = bpy.context.active_object.data
if isinstance( mesh_obj , bpy.types.Mesh ) == False:
print(f"メッシュ未選択(というか非アクティブ)かも")
return
if 0 => mesh_obj.total_vert_sel:
print(f"1つも選択状態になってないぜ!")
return
# 先にEDITにしないと弾かれる
bpy.ops.object.mode_set(mode='EDIT')
bm = bmesh.from_edit_mesh(mesh_obj)
selected_verts = []
for vert in bm.verts:
if vert.select:
selected_verts.append( vert )
▼ 簡略版
import bpy
import bmesh
def get_active_mesh_selected_vertices():
mesh_obj = bpy.context.active_object.data
if isinstance( mesh_obj , bpy.types.Mesh ) == False:
return
selected_verts = [v for v in mesh_obj.vertices if v.select]
get_active_mesh_selected_vertices()
任意メッシュオブジェクト内の頂点グループHogeのみ選択状態にする
import bpy
def select_vertex_groups():
mesh_obj = None
vg_index = -1
for obj in bpy.data.objects:
if type(obj.data) == bpy.types.Mesh and obj.name == "TargetMeshName...":
vg_index = obj.vertex_groups.find("Hoge")
if vg_index > -1:
mesh_obj = obj
break
if vg_index != -1 or mesh_obj is None:
return
# メッシュオブジェクトの頂点を走査し、頂点グループ名Hogeに属している物だけ抽出
target_vertices = [v for v in mesh_obj.data.vertices if vg_index in [ vg.group for vg in v.groups ] ]
print( len(target_vertices ) , "件の選択予定頂点データを確保しました")
# Operators 機能などを使って、オブジェクト内の指定頂点グループを選択状態にする
mesh_obj.vertex_groups.active_index = vg_index
bpy.ops.object.mode_set(mode='EDIT')
bpy.ops.mesh.select_all(action='DESELECT')
bpy.ops.object.vertex_group_select()
# 以下の update 処理を挟まないと、後述の選択状態確認処理が正常に通らなかったので注意
bpy.context.edit_object.update_from_editmode()
# メッシュの頂点を走査し、選択状態あるものを抽出して確認してみる
selected_vertices = [v for v in mesh_obj.data.vertices if v.select ]
print( len(selected_vertices) , "件の選択中頂点データを取得確認しました" )
return
select_vertex_groups()