Blender Advent Calendar 2018 の23日目の記事です。
Blender 2.8のBeta版が2018/11/29にリリースされたことにより、Blender 2.8におけるPython APIがほぼFixしました。
Blender 2.8は既存のPython APIを大きく変えるリリースであるとアナウンスされ、アドオン開発者はPython APIがFixした後にアドオンのソースコードを修正する必要があると言われています。
実際にBlender 2.8のBeta版が公開された後、多くのBlenderのアドオン開発者が、過去に作成したアドオンをBlender 2.8へ移植するための作業を開始しています。
私がメンテナンスしているアドオン Magic UV についても、Blender 2.8における大きなPython APIの変更の影響を受けていて、本記事を書きながらBlender 2.8の移植を行っている状況です。
本記事では、私がMagic UVをBlender 2.8に移植するために必要であった修正の中で、一般的な修正点を説明します。
なお、Blender 2.8におけるPython APIの変更点は非常に多く、本記事で示した修正を適用したとしても、Blender 2.7xで動作していたアドオンの完全動作は保証できません。
実際にBlender 2.7xのアドオンをBlender 2.8に移植するためには、本記事で行った修正に加えて必要に応じて修正が必要になる可能性があります。
アドオン共通
ここでは、Blender 2.7xでアドオンをBlender 2.8へ移植するために、Python APIの利用状況に関わらず、ほぼすべてのアドオンが適用しなければならない変更点を示します。
bl_infoのバージョン
アドオンの情報を示す変数である bl_info
の version
を (2, 80, 0)
に変更する必要があります。
bl_info
の version
は後方互換ですが、Blender 2.8に限っては bl_info
の version
が (2, 80, 0)
以上に設定されたアドオンしかBlenderで認識しません。
bl_info = {
"version": (2, 79, 0)
}
bl_info = {
"version": (2, 80, 0)
}
Blender関連のクラスの命名規則
bpy.types.Operator
を基底クラスとして作成したオペレータクラスなど、Blender関連のクラスの命名規則は存在していましたが、必ずしもこの命名規則に従う必要はありませんでした。
しかし、命名規則に従わないアドオンが多く存在したことで、Blender 2.7xではアドオン間でクラス名の衝突問題が頻繁に起こっていました。
このため、Blender 2.8では命名規則に従うことが義務付けられ、命名規則に従わないアドオンはコンソールの標準出力にエラーメッセージが出力されるようになります。
具体的な命名規則は、以下の通りです。
なお、XXX
は英大文字から始まる英字/数字から構成される文字列、YYY
は英字/数字/アンダースコア(_)から構成される文字列です。
基底クラス | 派生クラスの命名規則 |
---|---|
bpy.types.Header |
XXX_HT_YYY |
bpy.types.Menu |
XXX_MT_YYY |
bpy.types.Operator |
XXX_OT_YYY |
bpy.types.Panel |
XXX_PT_YYY |
bpy.types.UIList |
XXX_UL_YYY |
Blender関連のクラスの登録
Blender 2.7xでは、bpy.utils.register_module
を利用することにより、一括してBlender関連のクラスを登録することができました。
しかしBlender 2.8では、bpy.utils.register_module
の処理コストが大きいことから廃止されます。
従ってBlender 2.8では、bpy.utils.register_class
を利用してクラスを登録する必要があります。
# クラスの登録
bpy.utils.register_module(__name__)
# クラスの登録解除
bpy.utils.unregister_module(__name__)
classes = [
HOGE_MT_PiyoMenu,
HOGE_OT_FugaOperator,
HOGE_PT_FooPanel,
]
# クラスの登録
for cls in classes:
bpy.utils.register_class(cls)
# クラスの登録解除
for cls in classes:
bpy.utils.unregister_class(cls)
プロパティの宣言
Blender 2.7xにおいて、プロパティ(bpy.props.IntProperty
など)をBlender関連のクラスに含める場合は、単純な代入文で問題ありませんでした。
しかしBlender 2.8では、Python 3.5から導入された Type Hints機能 のための型アノテーション記法を推奨しています。
従来の単純な代入文でもBlender 2.8では動作しますが、コンソールに警告メッセージが出力されます。
class FugaOperator(bpy.types.Operator):
property = IntProperty()
class HOGE_OT_FugaOperator(bpy.types.Operator):
property: IntProperty()
必要に応じて修正が必要
ここでは、Python APIの利用状況によって修正が必要になることがある修正内容を示します。
一部のAPIでキーワード引数が必須化
Blender 2.8では、一部のAPIでキーワード引数が必須になります。
キーワード引数が必須化された全てのPython APIについてここで紹介することはできないため、Magic UVをBlender 2.8へ移植する時に必要となった修正の一部を紹介します。
UILayout.label
Blender 2.7xでは、UILayout.label
の第1引数にラベルで表示する文字列を指定することができますが、Blender 2.8では明示的にキーワード text
を指定して表示する文字列を指定する必要があります。
class FooPanel(bpy.types.Panel):
def draw(self, context):
layout = self.layout
layout.label("bar")
class HOGE_PT_FooPanel(bpy.types.Panel):
def draw(self, context):
layout = self.layout
# textはキーワード引数として指定する必要がある
layout.label(text="bar")
context.window_manager.event_timer_add
Blender 2.8で提供されるタイマを登録するためのAPI context.window_manager.event_timer_add
についても、キーワード引数が必須になっています。
def invoke(self, context, _):
timer = context.window_manager.event_timer_add(0.1, context.window)
def invoke(self, context, _):
# windowはキーワード引数として指定する必要がある
timer = context.window_manager.event_timer_add(0.1, window=context.window)
オブジェクト関連のAPI
オブジェクトの選択
Blender 2.8では、オブジェクトの選択状態を取得/設定するAPIが変更されます。
obj = bpy.data.objects[0]
if obj.select == False:
obj.select = True
obj = bpy.data.objects[0]
if obj.select_get() == False:
obj.select_set(True)
アクティブオブジェクトの設定
アクティブなオブジェクトを設定する方法についても、Blender 2.8で変更されます。
bpy.context.scene.objects.active = bpy.data.objects[0]
bpy.context.view_layer.objects.active = bpy.data.objects[0]
メッシュ関連のAPI
BMeshのTextureレイヤが消えている
Blender 2.7xでは、メッシュに割り当てられているTextureレイヤを、BMeshから取得することができました。
しかしBlender 2.8では、このAPIが削除されています。
import bmesh
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
# Textureレイヤを取得
tex_layer = bm.faces.layers.tex.verify()
# 面に割り当てられたTextureを取得
img = bm.faces[0][tex_layer].image
import bmesh
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
# ★エラー:bm.faces.layersに、texは存在しない
tex_layer = bm.faces.layers.tex.verify()
# ★エラー:bm.faces[0]に、キーtex_layerは存在しない
img = bm.faces[0][tex_layer].image
UI関連のAPI
Tool-Shelfへのパネル配置は不可
Blender 2.7xにおけるTool-Shelfは、Blender 2.8ではToolbarに変更されます。
ToolbarのUIをアドオンから変更することは現状許されていないため、Blender 2.7xでTool-Shelfを利用していたアドオンは修正が必要になります。
アドオンのUI自体を変えてしまうのも1つの手ですが、Blender 2.7xのTool-Shelfとできるだけ近づけたい場合は、タブ機能が存在するSidebarを利用するのがよいでしょう。
Sidebarは、Blender 2.7xのProperty-Shelfから変更されたものになります。
ここでは、Blender 2.7xのTool-Shelfに配置されていたパネルを、Blender 2.8のSidebarに配置し直すために必要な修正方法を示します。
Tool-Shelfにパネルを配置する場合、Blender 2.7xでは bpy.types.Panel
を継承したクラスにおいて、配置先のリージョンを示すプロパティ bl_region_type
に、TOOLS
を指定する必要がありました。
class FooPanel(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
bl_label = "Foo"
bl_category = "Hoge"
bl_context = 'mesh_edit'
Blender 2.8では、'TOOLS'
はToolbar向けに唯一設定可能なものとされ、新たなパネルを配置できません。
実際、bl_region_type
に TOOLS
を指定すると、エラーが発生してしまいます。
このため対応案の1つとして、Sidebarにパネルを配置する方法がありますが、Sidebarにパネルを配置するためには、bl_region_type
に UI
(Blender 2.7xでは、bl_region_type
に UI
を指定するとProperty-Shelfへの配置となる)を指定します。
class HOGE_PT_FooPanel(bpy.types.Panel):
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_label = "Foo"
bl_category = "Hoge"
bl_context = 'mesh_edit'
アイコンの追加と削除
Blender 2.8でUIが大きく変更されたことにより、アドオンから利用可能なアイコンも大きく変更されています。
Blender 2.8で新しくアイコンが追加されているだけでなく、Blender 2.7xで利用可能であったアイコンが削除されている場合もあります。
ここでは、Blender 2.7xとBlender 2.8における利用可能なアイコンを比較します。
各アイコンの隣には、アイコンを表示するためにAPIの引数 icon
に指定する文字列も表示しました。
Blender 2.8についてはアイコンが表示されていないものがありますが、表示されていないアイコンは利用できないアイコンです。
もしこれらのアイコンを表示させようとすると、エラーメッセージが出力されるます。
なお、アイコンを一覧表示するために、本記事の サンプル を使用しました。
Blender 2.7xで利用可能なアイコン
Blender 2.80で利用可能なアイコン
数学関連のAPI
Blender 2.7xでは、行列積を M1 * M2
のように書くことができましたが、Blender 2.8では M1 @ M2
のように書く必要があります。
これは、PEP465 で推奨されている記法に従ったものであり、BlenderのPython APIでもこれに従った形になります。
なお従来の記法 M1 * M2
は、Element-wiseな積(アダマール積)として提供される予定です。
from mathutils import Matrix, Vector
mat = Matrix([[2.4, 1.3], [1.5, 1.0]])
vec = Vector([0.8, 3.5])
# 行列積
# 結果:
# Vector (6.4700, 4.7000)
ret = mat * vec
# アダマール積
# 結果:
# Matrix 2x2 (4.8000, 2.6000)
# (3.0000, 2.0000)
ret = mat * 2
from mathutils import Matrix, Vector
mat = Matrix([[2.4, 1.3], [1.5, 1.0]])
vec = Vector([0.8, 3.5])
# 行列積
# 結果:
# Vector (6.4700, 4.7000)
ret = mat @ vec
# アダマール積
# 結果:
# Matrix 2x2 (4.8000, 2.6000)
# (3.0000, 2.0000)
ret = mat * 2
# ★エラー:*を行列積に使用できない
ret = mat * vec
描画関連のAPI
Blender 2.7xにおいて独自のUIを構築するときは、OpenGLのAPIをPythonから利用可能な bgl
モジュールを利用して描画していました。
Blender 2.8では、バックエンドで使用するOpenGLのバージョンが変わったことにより、bgl
モジュールで提供されるAPIが大きく変わりました。
また、Blender 2.8では bgl
モジュールを利用することが非推奨とされ、代わりに gpu
モジュールを利用することが推奨されています。
gpu
モジュールは、bgl
モジュールと使い方が大きく異なるため、bgl
モジュールを利用するアドオンは gpu
モジュールを使うように修正しなければなりません。
ここでは簡単な例として、四角形を描画するプログラムを比較してみます。
import bpy
import bgl
def draw():
verts = [
[0.0, 0.0],
[0.0, 200.0],
[200.0, 200.0],
[200.0, 0.0],
]
bgl.glEnable(bgl.GL_BLEND)
bgl.glBegin(bgl.GL_QUADS)
bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
for (x, y) in verts:
bgl.glVertex2f(x, y)
bgl.glEnd()
handle = bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_PIXEL')
import bpy
import gpu
from gpu_extras.batch import batch_for_shader
verts = [
[0.0, 0.0],
[0.0, 200.0],
[200.0, 200.0],
[200.0, 0.0],
]
indices = [
[0, 1, 2, 3]
]
shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
batch = batch_for_shader(shader, 'TRIS', {"pos": verts},
indices=indices)
def draw():
shader.bind()
shader.uniform_float("color", (1.0, 1.0, 1.0, 1.0))
batch.draw()
handle = bpy.types.SpaceView3D.draw_handler_add(draw, (), 'WINDOW', 'POST_PIXEL')
gpu
モジュールで提供されるPython APIは、上記で示したAPI以外にも多く存在するため、ここではこれ以上説明しません。
詳細は、公式のドキュメント を確認してください。
※ gpu
モジュールをバックエンドとしてBlender 2.7xの bgl
モジュールのように扱えるライブラリ bglx を開発中です。開発途中で機能の追加はこれからですが、もしよければご利用ください。
タイマ関連のAPI
※ タイマ関連のAPIは、ドキュメント に記載されているのみで、現時点では利用できないようです。このため、本記事ではドキュメントを要約したものについてのみ示します。APIが利用できるようになりましたら、本記事も更新したいと思います。
Blender 2.7xでは、context.window_manager.event_timer_add
と modal
メソッドを利用してタイマを登録し、定期的に処理を実行することができましたが、オペレータクラスの作成やイベントの扱いを考えると、簡単に使えるものではありませんでした。
Blender 2.8では、定期的な処理を実行する仕組みをより簡単に実現できるように、新たなAPI bpy.app.timers
が利用できます。
以下では、登録から5秒後に1秒ごとにカウント値を出力し、カウント数が10になった時にタイマの登録を解除するサンプルを示します。
import bpy
counter = 0
def print_message():
global counter
counter = counter + 1
print("Count: {}".format(counter))
if counter == 10:
print("Finished")
return None # counterが10になった時に、定期的な実行を終了する(タイマを登録解除する)
return 1.0 # 1秒ごとに実行する
# 登録後5秒後に、1秒ごとにprint_messageを実行する(タイマを登録する)
bpy.app.timers.register(print_message, first_interval=5.0)
その他気が付いた変更点
ここでは、Magic UVをBlender 2.8へ移植する時に気がついた、細かいPython APIの仕様変更点をまとめます。
領域1x1のWINDOWリージョン
Blender 2.8では、横幅1pixel、縦幅1pixelの 'WINDOW'
リージョンが、各エリアに追加されています。
このため、'VIEW_3D'
エリアの 'WINDOW'
リージョンを取得する場合は、Blender 2.8で追加された1x1のリージョンを間違って取得しないように修正しなければなりません。
area = None
region = None
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
break
if area:
for region in area.regions:
if region.type == 'WINDOW':
break
area = None
region = None
for area in bpy.context.screen.areas:
if area.type == 'VIEW_3D':
break
if area:
for region in area.regions:
if region.type == 'WINDOW':
# Blender 2.8では、1x1の'WINDOW'リージョンが存在する
if region.width <= 1 or region.height <= 1:
continue
break
UV/Image Editorの2D座標値
UV/Image Editorのカーソルの位置を取得するためには、'IMAGE_EDITOR'
スペースの cursor_location
を参照しますが、Blender 2.7xではテクスチャのサイズの分だけスケールダウンしないと、UV座標と一致しませんでした。
つまり、テクスチャの左下にカーソルがあるときは cursor_location
は (0.0, 0.0)
を返し、右上にカーソルがあるときはテクスチャサイズ (テクスチャの横幅, テクスチャの縦幅)
を返していました。
しかしBlender 2.8では、テクスチャサイズでスケールダウンしなくても、テクスチャの左下にカーソルがあるときは cursor_location
は (0.0, 0.0)
を返し、右上にカーソルがあるときは、(1.0, 1.0)
を返すようになります。
tex_size = area.spaces.active.image.size
loc = space.cursor_location
# テクスチャ座標からUV座標へ変換
cx = loc[0] / tex_size[0]
cy = loc[1] / tex_size[1]
# すでにUV座標に設定されているため、テクスチャサイズを用いてUV座標へ変換する必要がない
loc = space.cursor_location
サンプル
Blender 2.7xとBlender 2.8のアドオンの実装方法の違いを確認するため、Blender 2.7xとBlender 2.8それぞれで利用可能なアイコンを全て表示するサンプルを紹介します。
アドオンの作り方の詳細については [Blender] Blenderプラグインの作り方 を参照していただき、アドオンの作り方については省略します。
なお、Blender 2.7xからBlender 2.8に移植するために変更する必要がある箇所をコメントで示しました。
Blender 2.7x
import bpy
from bpy.props import IntProperty
bl_info = {
"name": "Sample Add-on in Blender 2.7x",
"author": "Nutti",
"version": (1, 0),
"blender": (2, 79, 0),
"location": "View3D > Sidebar",
"description": "Show all icons available in Blender 2.7x",
"warning": "",
"support": "TESTING",
"wiki_url": "",
"tracker_url": "",
"category": "User Interface"
}
def get_num_column(self):
return self.get('num_column', ShowIconPanel.num_column)
def set_num_column(self, value):
self['num_column'] = value
ShowIconPanel.num_column = value
class ShowIcons(bpy.types.Operator):
bl_idname = "object.show_all_icons"
bl_label = "Show All Icons"
bl_description = "Show all icons available in Blender 2.7x"
bl_options = {'REGISTER', 'UNDO'}
num_column = IntProperty(
name="Number of Icons",
description="Number of Icons in one column",
default=2,
min=1,
max=5,
get=get_num_column,
set=set_num_column,
)
def draw(self, context):
layout = self.layout
layout.prop(self, "num_column")
def execute(self, context):
ShowIconPanel.show = True
return {'FINISHED'}
class HideIcons(bpy.types.Operator):
bl_idname = "object.hide_icons"
bl_label = "Hide Icons"
bl_description = "Hide icons"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
ShowIconPanel.show = False
return {'FINISHED'}
class ShowIconPanel(bpy.types.Panel):
bl_label = "Sample Add-on in Blender 2.7x"
bl_space_type = 'VIEW_3D'
bl_region_type = 'TOOLS'
bl_category = "Show Icon"
bl_context = "objectmode"
num_column = 3
show = False
def draw_header(self, context):
layout = self.layout
layout.label(text="", icon='PLUGIN')
def draw(self, context):
layout = self.layout
if ShowIconPanel.show:
layout.operator(HideIcons.bl_idname, text="Hide")
layout.separator()
layout.label(text="Availabe Icons in Blender 2.7x:")
icon = bpy.types.UILayout.bl_rna.functions['prop'].parameters['icon']
for i, key in enumerate(icon.enum_items.keys()):
if i % ShowIconPanel.num_column == 0:
row = layout.row()
row.label(text=key, icon=key)
else:
layout.operator(ShowIcons.bl_idname, text="Show")
def register():
bpy.utils.register_module(__name__)
def unregister():
bpy.utils.unregister_module(__name__)
if __name__ == "__main__":
register()
Blender 2.80
import bpy
from bpy.props import IntProperty
bl_info = {
"name": "Sample Add-on in Blender 2.8",
"author": "Nutti",
"version": (1, 0),
"blender": (2, 80, 0), # ★(2, 80, 0)に変更
"location": "View3D > Sidebar",
"description": "Show all icons available in Blender 2.8",
"warning": "",
"support": "TESTING",
"wiki_url": "",
"tracker_url": "",
"category": "User Interface"
}
def get_num_column(self):
return self.get('num_column', SAMPLE_PT_ShowIcon.num_column)
def set_num_column(self, value):
self['num_column'] = value
SAMPLE_PT_ShowIcon.num_column = value
# ★クラスの命名規則「XXX_OT_YYY」に従うように変更
class SAMPLE_OT_ShowIcons(bpy.types.Operator):
bl_idname = "object.show_all_icons"
bl_label = "Show All Icons"
bl_description = "Show all icons available in Blender 2.8"
bl_options = {'REGISTER', 'UNDO'}
# ★型アノテーションによる記法に変更
num_column: IntProperty(
name="Number of Icons",
description="Number of Icons in one column",
default=2,
min=1,
max=5,
get=get_num_column,
set=set_num_column,
)
def draw(self, context):
layout = self.layout
layout.prop(self, "num_column")
def execute(self, context):
SAMPLE_PT_ShowIcon.show = True
return {'FINISHED'}
# ★クラスの命名規則「XXX_OT_YYY」に従うように変更
class SAMPLE_OT_HideIcons(bpy.types.Operator):
bl_idname = "object.hide_icons"
bl_label = "Hide Icons"
bl_description = "Hide icons"
bl_options = {'REGISTER', 'UNDO'}
def execute(self, context):
SAMPLE_PT_ShowIcon.show = False
return {'FINISHED'}
# ★クラスの命名規則「XXX_PT_YYY」に従うように変更
class SAMPLE_PT_ShowIcon(bpy.types.Panel):
bl_label = "Sample Add-on in Blender 2.8"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI' # ★Sidebarに配置
bl_category = "Show Icon"
bl_context = "objectmode"
num_column = 3
show = False
def draw_header(self, context):
layout = self.layout
layout.label(text="", icon='PLUGIN')
def draw(self, context):
layout = self.layout
if SAMPLE_PT_ShowIcon.show:
layout.operator(SAMPLE_OT_HideIcons.bl_idname, text="Hide")
layout.separator()
layout.label(text="Availabe Icons in Blender 2.8:")
icon = bpy.types.UILayout.bl_rna.functions['prop'].parameters['icon']
for i, key in enumerate(icon.enum_items.keys()):
if i % SAMPLE_PT_ShowIcon.num_column == 0:
row = layout.row()
row.label(text=key, icon=key)
else:
layout.operator(SAMPLE_OT_ShowIcons.bl_idname, text="Show")
classes = [
SAMPLE_OT_ShowIcons,
SAMPLE_OT_HideIcons,
SAMPLE_PT_ShowIcon,
]
def register():
# ★bpy.utils.register_classを利用
for c in classes:
bpy.utils.register_class(c)
def unregister():
# ★bpy.utils.unregister_classを利用
for c in classes:
bpy.utils.unregister_class(c)
if __name__ == "__main__":
register()
おわりに
Blender 2.8で提供されるPython APIについて、確認できた範囲で変更点をまとめてみました。
Blender 2.8では大幅にPython APIが変更されるとアナウンスされていましたが、bgl
モジュールが利用できなくなったことを除けば、Blender 2.7xのアドオンを移植すること自体は難しくはないと思います。
しかし、Blender 2.8に移植したアドオンはBlender 2.7xと互換ではなくなるため、Blender 2.8とBlender 2.7xの両方で動作するようにアドオンを作成するのは、少し難しいかもしれません。
なおMagic UVでは、Blenderのバージョンによって読み込むモジュールを制御することでこの問題に対処しているため、必要でしたらソースコードを確認してみてください。
明日24日目は、drmdrさん です。