C++ ときどき ごはん、わりとてぃーぶれいく☆

USAGI.NETWORKのなかのひとのブログ。主にC++。

Blender Scripting: head と tail の座標と適当なフィルター条件でボーンの親子関係を自動構築する `connect-bones` の作り方と Add-on 化の方法

こんにちは、Python初心者∧Blender初心者です。フィーリングやマウスぽちぽちよりパキッとコードで処理する方が得意なので、 un-h002; Atroposcluster 対応で私自身に追加実装された Blener Scripting を入門講座を兼ねて整理します💪

Note: 読者の想定=PythonまたはBlenderは初心者Lv1くらいだけど、他に何かしらのプログラミング言語のLv25くらいの使い手の方(但しLv値はよくある1..99を基本想定とした古典的JRPGくらいの感覚値)で、必要な予備知識はAPI Referenceがあれば読みに行ける方。

準備するもの: bone が親子関係なしでたくさん入っていて、一部の bone は head と tail が同じ座標にあって親子関係で connected にできる Blender の作業状態。またはこのメモの学習用に Connected な親子関係のボーンを含んだデータを Blender ですべての armature を選択して親子関係を clear した作業状態。例えば セシル変身アプリ で作った VRM を前回のメモのように Blender へ Import するとそういう状態になるかもしれません。

もくじ

  1. はじまりの Blender Scripting
  2. すべての bone を列挙
  3. 列挙された bone 群をフィルター
  4. head と tail が同じ座標の bone 組の抽出
  5. 2つの bone 組の「選択」
  6. Blender 組み込み機能の Armature: Make Parent を呼ぶ
  7. かんせいの connect-bones Blender Scripting → Add-on 化

1. はじまりの Blender Scripting

  1. Blender上部のタブを Scripting へ切り替え、
  2. New Text して、
  3. 適当な名前を付けて
  4. 動作確認程度のコードを書いて
  5. Armature を Edit Mode で選択して (†後でここもコードにできます✨)
  6. Run Script

f:id:USAGI-WRP:20200412104746p:plain

import bpy # Blender API

print( bpy ) # You can output to STDOUT anything!
print( "Let's start", 123.45, 'Blender Scripting!' ) # You can concatenate a string and any object with a comma.
# In this world, you can use the `#` for a line commenting. `//` is not work.

Note: Blender-2.82@Windows 現在の Scripting のテキストエディターやSTDOUTの出力先の System Console Window は UNICODE 表示にデフォルトでは対応していないので非ASCIIをコードに使わないようにしました。

print による STDOUT の結果は F3 ->Wm: Toggle System Console` などすると表示されるシステムコンソール窓で確認できます。

<module 'bpy' from 'C:\\Program Files\\Blender Foundation\\Blender 2.82\\2.82\\scripts\\modules\\bpy\\__init__.py'>
Let's start 123.45 Blender Scripting!

こんな表示が Run Script で出れば Blender Scripting の Lv1 はクリアーです。おめでとうございます🎉

Lv1 をクリアーした報酬として BlenderAPI Documentation を進呈します:

今後の Blender Scripting ライフにお役立て下さい💪

2. すべての bone を列挙

bpy (たぶん Blender-PYthon とかそういう命名由来でしょう…) オブジェクトのメンバーを読んだり呼んだりすると、たいていの Blender の管理オブジェクトや組み込み機能の呼び出しを行えます。

例えば:

import bpy
from itertools import chain

armatures = bpy.data.armatures
bones     = list( chain( *[ a.edit_bones for a in armatures ] ) )

for b in bones:
 print( b, ' { head=', b.head, ' tail=', b.tail, '}' )

こう↑して Run Script すると↓

<bpy_struct, Bone("NekoMimiBone_L")>  { head= <Vector (0.0699, 0.2111, 0.0399)>  tail= <Vector (0.0825, 0.2277, 0.0401)> }
<bpy_struct, Bone("NekoMimiBone_L_001")>  { head= <Vector (0.0825, 0.2277, 0.0401)>  tail= <Vector (0.1012, 0.2548, 0.0416)> }
<bpy_struct, Bone("NekoMimiBone_L_002")>  { head= <Vector (0.1012, 0.2548, 0.0416)>  tail= <Vector (0.1210, 0.2814, 0.0430)> }
<bpy_struct, Bone("NekoMimiBone_L_003")>  { head= <Vector (0.1210, 0.2814, 0.0430)>  tail= <Vector (0.1408, 0.3081, 0.0444)> }
<bpy_struct, Bone("Hair")>  { head= <Vector (-0.0000, 0.1213, 0.0039)>  tail= <Vector (-0.0000, 0.0988, 0.0286)> }
<bpy_struct, Bone("Hoho_R")>  { head= <Vector (-0.0817, 0.0463, 0.0867)>  tail= <Vector (-0.1030, 0.0584, 0.1093)> }
<bpy_struct, Bone("Hoho_L")>  { head= <Vector (0.0817, 0.0463, 0.0867)>  tail= <Vector (0.1030, 0.0584, 0.1093)> }
<bpy_struct, Bone("Ago")>  { head= <Vector (-0.0000, 0.0127, 0.0660)>  tail= <Vector (-0.0000, 0.0190, 0.0987)> }
<bpy_struct, Bone("Chest")>  { head= <Vector (-0.0000, -0.1039, 0.0311)>  tail= <Vector (0.0000, 0.0702, -0.0152)> }
<bpy_struct, Bone("Mune_R")>  { head= <Vector (-0.0940, -0.1429, 0.1315)>  tail= <Vector (-0.0521, -0.1654, 0.0071)> }
<bpy_struct, Bone("Mune_R_001")>  { head= <Vector (0.0000, 0.0000, 0.0000)>  tail= <Vector (-0.0000, 0.0333, -0.0000)> }
<bpy_struct, Bone("Mune_L")>  { head= <Vector (0.0940, -0.1429, 0.1315)>  tail= <Vector (0.0521, -0.1654, 0.0071)> }
<bpy_struct, Bone("Mune_L_001")>  { head= <Vector (0.0000, 0.0000, 0.0000)>  tail= <Vector (0.0000, 0.0333, -0.0000)> }
<bpy_struct, Bone("Koshi")>  { head= <Vector (-0.0000, 0.0000, 0.0000)>  tail= <Vector (-0.0000, 0.0100, 0.0000)> }

↑こんな具合の出力を得られます。

例を読んだ時点で既に本項の目的を達成していましましたね!🎉

素晴らしい学習能力を発揮中で知識を吸い込みたい盛りのあなたがLv2をクリアーした記念に:

この↑ Bone 型のリファレンスマニュアルを進呈します。 headtail が何かも書いてあるよ✨

3. 列挙された bone 群をフィルター

ところで、先程のコードに [ ] で囲まれたリストをコードで生成している部分がありました。あれが Python 言語の特徴的な言語機能の1つとして有名な "リスト内包表記" でした✨

リスト内包表記ではリストへ入れたい何かを、

  • 他のリストを for で列挙しつつ
  • if で条件を付けたり、
  • 取り出した結果そのものではなく加工を加えたオブジェクトにして

新たなリストを作成する、そんなような事ができる事を既に知りました。そこで、例えば:

import bpy
from itertools import chain

armatures = bpy.data.armatures
bones     = list( chain( *[ a.edit_bones for a in armatures ] ) )

filtered_bones = [ b for b in bones if b.name.startswith( 'NekoMimiBone' )  ]

for b in filtered_bones:
 print( b, ' { head=', b.head, ' tail=', b.tail, '}' )

こんな↑具合で前項の例に1行 filtered_bones を "NekoMimiBone" で名前が始まる bone だけを抽出して作るように追加して、bones に替えて filtered_bonesprint してみると:

<bpy_struct, Bone("NekoMimiBone")>  { head= <Vector (-0.0000, 0.1213, 0.0039)>  tail= <Vector (-0.0000, 0.2111, 0.0399)> }
<bpy_struct, Bone("NekoMimiBone_R")>  { head= <Vector (-0.0699, 0.2111, 0.0399)>  tail= <Vector (-0.0825, 0.2277, 0.0401)> }
<bpy_struct, Bone("NekoMimiBone_R_001")>  { head= <Vector (-0.0825, 0.2277, 0.0401)>  tail= <Vector (-0.1012, 0.2548, 0.0416)> }
<bpy_struct, Bone("NekoMimiBone_R_002")>  { head= <Vector (-0.1012, 0.2548, 0.0416)>  tail= <Vector (-0.1210, 0.2814, 0.0430)> }
<bpy_struct, Bone("NekoMimiBone_R_003")>  { head= <Vector (-0.1210, 0.2814, 0.0430)>  tail= <Vector (-0.1408, 0.3081, 0.0444)> }
<bpy_struct, Bone("NekoMimiBone_L")>  { head= <Vector (0.0699, 0.2111, 0.0399)>  tail= <Vector (0.0825, 0.2277, 0.0401)> }
<bpy_struct, Bone("NekoMimiBone_L_001")>  { head= <Vector (0.0825, 0.2277, 0.0401)>  tail= <Vector (0.1012, 0.2548, 0.0416)> }
<bpy_struct, Bone("NekoMimiBone_L_002")>  { head= <Vector (0.1012, 0.2548, 0.0416)>  tail= <Vector (0.1210, 0.2814, 0.0430)> }
<bpy_struct, Bone("NekoMimiBone_L_003")>  { head= <Vector (0.1210, 0.2814, 0.0430)>  tail= <Vector (0.1408, 0.3081, 0.0444)> }

おめでとうございます🎉 今回も例を読んだだけで目的を達成してしまいましたね。Lv3のクリアーの記念に:

"Python-3.7" の公式チュートリアルの "§5.1.3. リスト内包表記" を進呈します💪 執筆現在の Python そのものは 3.8 が最新版ですが、 Blender-2.82 に組み込まれた Python は 3.7.4 です。リファレンスを参照する時は自分が扱う言語処理系のバージョンにも注意しましょう。

Note: ちなみに、ぐぐるとリスト内包表記は何なのか程度の"入門情報"はたくさんの野良解説が出てくると思います。Pythonの言語の入門書籍でも紹介されているでしょう。"Must"や"Should"と言うほどお節介ではありませんが、たいていのプログラミング言語の"基礎的な何か"は言語公式の資料を第一に参照すると書き間違いも滅多にありませんし、必要な事は必ず書いてありますから合理的でいいですよ👍

4. head と tail が同じ座標の bone 組の抽出

少し複雑さが増加しそうなので、リスト内包表記やワンライナーを無理に作ろうとせず、そろそろ関数を定義して数年後の自分でも見た瞬間に読めるように書きます:

import bpy
from itertools import chain

armatures = bpy.data.armatures
bones     = list( chain( *[ a.edit_bones for a in armatures ] ) )

filtered_bones = [ b for b in bones if b.name.startswith( 'NekoMimiBone' )  ]

def find_connectable_pair( parent ):
 for candidate in filtered_bones:
  if parent.tail == candidate.head:
   return ( parent, candidate )

connectable_pairs = [ p for p in map( find_connectable_pair, filtered_bones ) if p != None ]

for p in connectable_pairs:
 print( 'Parent:', p[0], ' <==/connectable/==< Child:', p[1] )

map は言語組み込みの元になるリストの要素群を任意のファンクターで射影変換した要素群から作成したリストを得る Python 言語のリスト処理用に組み込まれた関数です。また、 Python では [ ] はリスト、 ( ) はタプルになります。💪

Note: Python では def で関数を定義できます。いままで触れていませんでしたが、 Python の「らしい」言語機能のもう1つの大きな特徴のインデント(indent)/デデント(dedent)によるブロック構造の生成を前項までの最後の forprint の組み合わせでも書いてありました。 defif も同様に : の後の行で indent = { から dedent = } まで同じインデントレベルの連続した複数の行が1つのブロックとして処理されます。また、 Python では関数の return が未定義の場合は None が還ります。

↑これを実行すると↓

Parent: <bpy_struct, Bone("NekoMimiBone_R")>  <==/connectable/==< Child: <bpy_struct, Bone("NekoMimiBone_R_001")>
Parent: <bpy_struct, Bone("NekoMimiBone_R_001")>  <==/connectable/==< Child: <bpy_struct, Bone("NekoMimiBone_R_002")>
Parent: <bpy_struct, Bone("NekoMimiBone_R_002")>  <==/connectable/==< Child: <bpy_struct, Bone("NekoMimiBone_R_003")>
Parent: <bpy_struct, Bone("NekoMimiBone_L")>  <==/connectable/==< Child: <bpy_struct, Bone("NekoMimiBone_L_001")>
Parent: <bpy_struct, Bone("NekoMimiBone_L_001")>  <==/connectable/==< Child: <bpy_struct, Bone("NekoMimiBone_L_002")>
Parent: <bpy_struct, Bone("NekoMimiBone_L_002")>  <==/connectable/==< Child: <bpy_struct, Bone("NekoMimiBone_L_003")>

今回も簡単に目的を達成できました✨ 余力がみなぎっていかもしれないので、こちら↓をLv4のクリアーの記念に進呈します。ご活用下さい:

↑は "Python-3.7" 言語リファレンスの "§2.1.8. インデント" です💪 言語リファレンスはチュートリアルよりもカチッと規格の解説らしい文書なので、じっくり整理しながら読む必要のある部分も少なくありませんが、言語機能の中枢を成す基礎的な部分、例えばインデントとデデントによるコードブロックの生成の正確な情報、空白文字なら何でも良いのか、タブとスペースが混ざっているとどうなるのか、そういった情報について言語規格に基づいた間違いの(たいていは)無い正確な情報を確認できます。言語機能の基礎的な要素について気になる事があれば、はじめに言語リファレンスを参照すると不安の無い確実な情報が(たぶん)取得できて便利です✨

5. 2つの bone 組の「選択」

ここまでは Blender API を通して Blender が管理中のオブジェクトについて read 的なアクセスでした。そろそろ write 的な事をしてみましょう。世界は"副作用"で"進んで"います:

import bpy
from itertools import chain

armatures = bpy.data.armatures
bones     = list( chain( *[ a.edit_bones for a in armatures ] ) )

filtered_bones = [ b for b in bones if b.name.startswith( 'NekoMimiBone' )  ]

def find_connectable_pair( parent ):
 for candidate in filtered_bones:
  if parent.tail == candidate.head:
   return ( parent, candidate )

connectable_pairs = [ p for p in map( find_connectable_pair, filtered_bones ) if p != None ]

for p in connectable_pairs:
 print( 'Parent:', p[0], ' <==/connectable/==< Child:', p[1] )
 p[0].select = True
 p[1].select = True

↑最後の2行が増えただけですが、これまでにない大きな違いがあります。このコードでは最後の2行でスクリプトの外の Blender の世界の値を書き換えています。実行後に Edit Mode で今回操作対象のネコミミのあたりを見ると:

f:id:USAGI-WRP:20200412104948p:plain

ネコミミ部分の Connected な親子関係を見るからに構築できそうなボーン群が選択状態になりましたね!このスクリプトは実行によりスクリプトの外の Blender の世界に副作用を引き起こす事に成功しました。おめでとうございます🎉

ちなみに↑で EditBoneselect プロパティーの仕様を確認できます✨ Blender API には多くの "操作" が bpy.ops にメソッドとして提供されていて、例えば bpy.ops.armature.select_all のような使い所次第では便利な機能もあります。一方で、今回使った EditBoneselect プロパティーのように、直接変数の値を書き換えればよい場合もあります💪

6. Blender 組み込み機能の Armature: Make Parent を呼ぶ

既に bpy.ops.armature.parent_set を呼ぶだけでしょ、と勘付いているかもしれません。その通りです💪 その通りではあるのですが、きちんと API Reference を見てから使わないと使えません。

ついで、そろそろ仕上げも近づいているので、スクリプトの実行前に手作業で Edit Mode へ切り替えておく必要も無いように bpy.ops.object.mode_set も組み込んでしましょう:

import bpy
from itertools import chain

bpy.ops.object.mode_set(mode='EDIT')

armatures = bpy.data.armatures
bones     = list( chain( *[ a.edit_bones for a in armatures ] ) )

filtered_bones = [ b for b in bones if b.name.startswith( 'NekoMimiBone' )  ]

def find_connectable_pair( parent ):
 for candidate in filtered_bones:
  if parent.tail == candidate.head:
   return ( parent, candidate )

connectable_pairs = [ p for p in map( find_connectable_pair, filtered_bones ) if p != None ]

for p in connectable_pairs:
 print( 'connect-bones:', p[0].name, '<-', p[1].name )
 p[1].select = True
 p[0].select = True
 armatures[0].edit_bones.active = p[0]
 bpy.ops.armature.parent_set()
 p[0].select = False
 p[1].select = False

for p in connectable_pairs:
 p[1].select = True
 p[0].select = True

GUIで操作する場合には「先っぽボーン」→「根本ボーン」の順序で「選択」して Armature: Make Parent すれば期待動作するこの機能ですが、 bpy.ops.armature.parent_setスクリプトで使う場合は「選択」だけでなく「アクティブ状態」も意識的に操作を記述する必要があります。GUI操作では最後にぽちっと選択したボーンが「選択状態」かつ「アクティブ状態」になりますが、スクリプトでは「選択」と「アクティブ化」のコードはそれぞれ別に記述する機能として API が提供されています💪

加えて、GUI操作と同様の点として、複数の親子関係の構築をする場合には、先に親子関係を構築したボーンは選択を解除して、それから次の親子関係の選択を行う必要があります。

なお、今回は bpy.ops.object.mode_set(mode='EDIT') も加えたので、例えば Pose ModeRun Script しても自動的に Edit Mode になり、スクリプトが期待動作するようになっています💪

さらに、おまけ機能として親子関係の構築をすべて行った後、もう一度、親子関係のタプルをトラバースして親子関係を構築した(はず)のボーン、つまり変更があったボーン群を「選択」した状態にしています。システムコンソールのログを読まなくてもGUIで視覚的にどこがくっついたのかわかりやすくする工夫です。

f:id:USAGI-WRP:20200412105016p:plain

実行すると "NekoMimiBone" で始まるボーン群について、いまは互いにばらばらで親子関係は無いけれど tail と head が同じ座標で重なっている bone の組をすべて見つけて Connected な親子関係を構築してくれます✨ Connected なので、繋がったボーンの1つを適当に選択して G で動かすなどすると繋がったボーンも影響を受けて回転したり拡縮したり追従するようになっています:

f:id:USAGI-WRP:20200412105547p:plain

7. かんせいの connect-bones Blender Scripting -> Add-on 化✨

↑までで目的を達成するスクリプトを手書きで実行できるようになりました✨ せっかくなので Add-on にしてみます:

チュートリアルを参考にもにょもにょ書いて試して修正して:

### The code for add-on ecosystem ###

bl_info = {
 'name'    : 'connect-bones',
 'blender' : ( 2, 80, 0 ),
 'category': 'Armature',
 'author'  : 'USAGI.NETWORK / Usagi Ito',
 'version' : ( 0, 0, 0 ),
 'support' : 'TESTING',
}

addon_keymaps = []

import bpy
from itertools import chain
from re import compile

class ConnectBones( bpy.types.Operator ):
 '''Connect bones if it find a bone pair that has a same position of a head and a tail'''
 bl_idname  = 'armature.connect_bones'
 bl_label   = 'Connect bones'
 bl_options = { 'REGISTER', 'UNDO' }
 
 name_regex: bpy.props.StringProperty( name = 'name_regex', default = '.*' )
 
 def execute( self, context ):
  return connect_bones( self.name_regex )

def register():
 bpy.utils.register_class( ConnectBones )
 bpy.types.VIEW3D_MT_object.append( menu )
 wm = bpy.context.window_manager
 kc = wm.keyconfigs.addon
 if kc:
  km = wm.keyconfigs.addon.keymaps.new( name = '3D View', space_type = 'VIEW_3D' )
  kmi = km.keymap_items.new( idname = ConnectBones.bl_idname, type = 'C', value = 'PRESS', ctrl = True, shift = True, alt = True )
  kmi.properties.name_regex = '(?!)'
  addon_keymaps.append( ( km, kmi ) )

def unregister():
 bpy.utils.unregister_class( ConnectBones )
 for km, kmi in addon_keymaps:
  km.keymap_items.remove(kmi)

def menu( self, context ):
 self.layout.separator()
 self.layout.operator( ConnectBones.bl_idname )
 addon_keymaps.clear()

if __name__ == '__main__':
 register()

### The main code of the add-on ###

def connect_bones( name_regex ):
 bpy.ops.object.mode_set(mode='EDIT')
 
 armatures = bpy.data.armatures
 bones     = list( chain( *[ a.edit_bones for a in armatures ] ) )
 
 regex = compile( name_regex )
 
 filtered_bones = [ b for b in bones if regex.match( b.name ) ]
 
 def find_connectable_pair( parent ):
  for candidate in filtered_bones:
   if parent.tail == candidate.head:
    return ( parent, candidate )
 
 connectable_pairs = [ p for p in map( find_connectable_pair, filtered_bones ) if p != None ]
 
 for p in connectable_pairs:
  print( 'connect-bones:', p[0].name, '<-', p[1].name )
  p[1].select = True
  p[0].select = True
  armatures[0].edit_bones.active = p[0]
  bpy.ops.armature.parent_set()
  p[0].select = False
  p[1].select = False
 
 for p in connectable_pairs:
  p[0].select = True
  p[1].select = True
 
 return {'FINISHED'}

こんな具合でできました✨

add-on 化にあたり、

  1. add-on として機能を汎用的にするため前項までの試作では startwith でフィルターしていた bone の名前を正規表現でフィルターする方式に変更
  2. CTRL + SHIFT + ALT + C ショートカットキーで正規表現のパターンを指定するダイアログ付きで発動可能
  3. Python Console を使う場合は bpy.ops.armature.connect_bones( name_regex= 'Neko' ) のように発動可能

にしました💪

f:id:USAGI-WRP:20200412110456p:plain

f:id:USAGI-WRP:20200412110450p:plain

この add-on を使うと、名前を正規表現で狙ったボーン群に Connected な親子関係を構築できるので、例えばネコミミだけ、髪の毛だけなどある程度簡単に狙って Connected な親子関係をばらばらのボーンから構築できます。こうして Connected な親子関係を作ると Blender の内臓機能で Connected な親子ボーン群は任意に Armature: Merge Bones でき、そうするとマージ元のボーンそれぞれに割り当てられていた頂点ブレンディングのウェイトを合成して引き継いだボーンと頂点ウェイトの関係が自動的にできるので便利、という add-on です。

f:id:USAGI-WRP:20200412110246p:plain

↑見えるボーンのほとんどが Connected な親子関係にできるデータですが、手で作業するには多すぎて多すぎます…。でも、この add-on があれば↓

f:id:USAGI-WRP:20200412110255p:plain

めでたしめでたし。おしまい💪