プロフィール

髭山髭人(ひげひと)

自分の書いた記事が、一人でも誰かの役に立てば...
活動信条の一つとして「貴方のメモは、誰かのヒント」というのがあります。

このサイトについて

本家HP packetroom.net から切り離した いわゆる技術メモ用のブログで、無料レンタルサーバーにて運用しています。広告表示はその義務なのでご容赦。
XREA さんには長年お世話になっています

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 に読ませて実行可。

  1. 適当な場所に用意した任意 .py を読み込む
    「テキスト → 開く」 or 右上のフォルダアイコンボタンから開けます
  2. 適当に書く
    スクリプトをそのまま上書き保存したければ Alt+S ( Cltr + S ではないので注意 )
    ※ 自環境では日本語入力が出来なかったので、後述の「外部エディタ → 都度読み込み」
    …のほうが自分の性に合ってました
  3. 都度右上ボタンから実行 or Alt+P
  4. × ボタン (リンク切断) が ファイルを閉じる系 相当っぽい
  • 外部エディタを使いたい場合
    外部エディタで更新保存されたことを検知して再読み込みする機能がある。
    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 モードにしなきゃダメで…」
みたいなあまりにも面倒くさいお膳立ての必要がありました。

  • 仕様上、以下の条件を満たさないとエラーになる模様
    1. オブジェクトモード にして、対象オブジェクトをアクティブ かつ 選択状態にしておく
    2. ボーン削除の為、編集モード に切り替える
    3. 操作・検索対象を Bone ではなく EditBone にする事

オブジェクトを選択状態にするには、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()