はじめてのBlenderアドオン開発
Last Update: 2023.3.1
Blender 2.8~3.0
はじめてのBlenderアドオン開発
Blender 2.8~3.0
Last Update: 2023.3.1
3-8. 座標変換を活用する
3-1節 から 3-5節 では、Blenderが提供するUIのフレームワークを超えて独自のUIを構築する方法について説明しました。 しかしこれらのUIは、基本的に2D座標で表現されるため、[3Dビューポート] スペースにおいてリージョン座標から3D空間の座標、またはその逆のように座標変換する必要が出てきます。 このため本節では、Blenderが提供するAPIを使って座標変換する方法を説明します。
本節では、リージョン座標から [3Dビューポート] スペース上の3D空間の座標へ変換できることを示すため、次のような機能を備えるサンプルアドオンを作成します。 なお、本節のサンプルアドオンを理解することで、[3Dビューポート] スペース上のオブジェクトと直線との交差判定方法についても、理解できます。
1-5節 を参考にして次に示すソースコードを入力し、ファイル名 sample_3-8.py
として保存してください。
import bpy
import mathutils
import bmesh
from bpy_extras import view3d_utils
bl_info = {
"name": "サンプル 3-8: メッシュの面を選択するアドオン",
"author": "ぬっち(Nutti)",
"version": (3, 0),
"blender": (2, 80, 0),
"location": "3Dビューポート > Sidebar > サンプル 3-8",
"description": "マウスカーソルの位置にあるメッシュの面を選択するサンプルアドオン",
"warning": "",
"support": "TESTING",
"doc_url": "",
"tracker_url": "",
"category": "Mesh"
}
def get_region_and_space(context, area_type, region_type, space_type):
region = None
area = None
space = None
# 指定されたエリアの情報を取得する
for a in context.screen.areas:
if a.type == area_type:
area = a
break
else:
return (None, None)
# 指定されたリージョンの情報を取得する
for r in area.regions:
if r.type == region_type:
region = r
break
# 指定されたスペースの情報を取得する
for s in area.spaces:
if s.type == space_type:
space = s
break
return (region, space)
# マウスカーソルの位置にあるメッシュの面を選択するオペレータ
class SAMPLE38_OT_SelectMouseOveredMesh(bpy.types.Operator):
bl_idname = "object.sample38_selelct_mouseovered_face"
bl_label = "メッシュの面選択"
bl_description = "マウスカーソルの位置にあるメッシュの面を選択します"
# Trueの場合は、マウスカーソルの位置にあるメッシュの面を選択する
# (Trueの場合は、モーダルモード中である)
__running = False
# モーダルモード中はTrueを返す
@classmethod
def is_running(cls):
return cls.__running
def modal(self, context, event):
op_cls = SAMPLE38_OT_SelectMouseOveredMesh
active_obj = context.active_object
# エリアを再描画
if context.area:
context.area.tag_redraw()
# パネル [マウスドラッグでオブジェクトを回転] のボタン [終了] を
# 押したときに、モーダルモードを終了
if not self.is_running():
return {'FINISHED'}
# マウスドラッグ中は、マウスカーソルの位置にあるメッシュの面を選択
if event.type == 'MOUSEMOVE':
# マウスカーソルのリージョン座標を取得
mv = mathutils.Vector((event.mouse_region_x, event.mouse_region_y))
# [3Dビューポート] スペースを表示するエリアの [Window] リージョンの
# 情報と、[3Dビューポート] スペースのスペース情報を取得する
region, space = get_region_and_space(
context, 'VIEW_3D', 'WINDOW', 'VIEW_3D'
)
# マウスカーソルの位置に向けて発したレイの方向を求める
ray_dir = view3d_utils.region_2d_to_vector_3d(
region,
space.region_3d,
mv
)
# マウスカーソルの位置に向けて発したレイの発生源を求める
ray_orig = view3d_utils.region_2d_to_origin_3d(
region,
space.region_3d,
mv
)
# レイの始点
start = ray_orig
# レイの終点
end = ray_orig + ray_dir
# レイとオブジェクトの交差判定
# 交差判定はオブジェクトのローカル座標で行われるため、
# レイの始点と終点をローカル座標に変換する
mwi = active_obj.matrix_world.inverted()
# レイの始点
mwi_start = mwi @ start
# レイの終点
mwi_end = mwi @ end
# レイの向き
mwi_dir = mwi_end - mwi_start
# オブジェクトの面選択解除
bpy.ops.mesh.select_all(action='DESELECT')
# bmeshオブジェクトの構築
bm = bmesh.from_edit_mesh(active_obj.data)
# BVHツリーの構築
tree = mathutils.bvhtree.BVHTree.FromBMesh(bm)
# オブジェクトとレイの交差判定を行う
_, _, fidx, _ = tree.ray_cast(mwi_start, mwi_dir, 2000.0)
# メッシュとレイが衝突した場合
if fidx is not None:
bm.faces[fidx].select = True
return {'PASS_THROUGH'}
def invoke(self, context, event):
op_cls = SAMPLE38_OT_SelectMouseOveredMesh
if context.area.type == 'VIEW_3D':
# [開始] ボタンが押された時の処理
if not self.is_running():
# モーダルモードを開始
context.window_manager.modal_handler_add(self)
op_cls.__running = True
print("サンプル 3-8: メッシュの面選択処理を開始しました。")
return {'RUNNING_MODAL'}
# [終了] ボタンが押された時の処理
else:
op_cls.__running = False
print("サンプル 3-8: メッシュの面選択処理を終了しました。")
return {'FINISHED'}
else:
return {'CANCELLED'}
# UI
class SAMPLE38_PT_SelectMouseOveredMesh(bpy.types.Panel):
bl_label = "メッシュの面選択"
bl_space_type = 'VIEW_3D'
bl_region_type = 'UI'
bl_category = "サンプル 3-8"
bl_context = "mesh_edit"
def draw(self, context):
op_cls = SAMPLE38_OT_SelectMouseOveredMesh
layout = self.layout
# [開始] / [終了] ボタンを追加
if not op_cls.is_running():
layout.operator(op_cls.bl_idname,text="開始", icon='PLAY')
else:
layout.operator(op_cls.bl_idname,text="終了", icon='PAUSE')
classes = [
SAMPLE38_OT_SelectMouseOveredMesh,
SAMPLE38_PT_SelectMouseOveredMesh,
]
def register():
for c in classes:
bpy.utils.register_class(c)
print("サンプル 3-8: アドオン『サンプル 3-8』が有効化されました。")
def unregister():
for c in classes:
bpy.utils.unregister_class(c)
print("サンプル 3-8: アドオン『サンプル 3-8』が無効化されました。")
if __name__ == "__main__":
register()
1-5節 を参考にして作成したアドオンを有効化すると、コンソールウィンドウに次に示す文字列が出力されます。
サンプル 3-8: アドオン『サンプル 3-8』が有効化されました。
[3Dビューポート] スペースのSidebarを表示し、メッシュ型オブジェクトを選択した状態で [編集モード] にすると、パネル [サンプル 3-8] > [メッシュの面選択] が追加されます。
有効化したアドオンの機能を使い、動作を確認します。
1 | メッシュ型オブジェクトを選択した状態で、[編集モード] に変更します。 |
2 | [3Dビューポート] スペースのSidebarにおいて、パネル [サンプル 3-8] > [メッシュの面選択] に配置されている [開始] ボタンをクリックします。 |
3 | マウスカーソルが重なったメッシュの面が、選択状態になります。マウスカーソルが面から離れると、選択状態が解除されます。 |
4 | パネル [サンプル 3-8] > [メッシュの面選択] に配置されている [終了] ボタンをクリックすると、マウスカーソルがメッシュの面に重なっても、自動的に選択されないようになります。 |
1-5節 を参考にして有効化したアドオンを無効化すると、コンソールウィンドウに文字列が出力されます。
サンプル 3-8: アドオン『サンプル 3-8』が無効化されました。
本節では、座標変換に関する部分についてのみ説明します。 サンプルアドオンでは invoke
メソッドや modal
メソッドを使っていますが、本節では説明を省略します。 なお、これらのメソッドについては、3-1節 で説明しています。
マウスカーソルの位置に向けて発した、レイと交差するメッシュの面を選択するための手順を次に示します。
これらの処理は全て、SAMPLE38_OT_SelectMouseOveredMesh
クラスの modal
メソッドで行います。
最初に、マウスカーソルのリージョン座標を取得します。 マウスカーソルのリージョン座標を取得するためのコードを次に示します。
# マウスカーソルのリージョン座標を取得
mv = mathutils.Vector((event.mouse_region_x, event.mouse_region_y))
3-1節 で説明したように、マウスカーソルのリージョン座標は、mouse_region_x
(X座標)と mouse_region_y
(Y座標)で取得できます。
1で取得したマウスカーソルのリージョン座標から、レイの向きとレイの始点の座標を、[3Dビューポート] スペースの3D座標で求めます。 リージョン座標からこれらの座標への変換は、bpy_extraモジュールのview3d_utilsサブモジュールを利用すると簡単に実現できます。 事前に、bpy_extraモジュールをインポートしておきましょう。
from bpy_extras import view3d_utils
マウスのリージョン座標から、レイの向きと始点の座標を求めるためのコードを次に示します。
# [3Dビューポート] スペースを表示するエリアの [Window] リージョンの
# 情報と、[3Dビューポート] スペースのスペース情報を取得する
region, space = get_region_and_space(
context, 'VIEW_3D', 'WINDOW', 'VIEW_3D'
)
# マウスカーソルの位置に向けて発したレイの方向を求める
ray_dir = view3d_utils.region_2d_to_vector_3d(
region,
space.region_3d,
mv
)
# マウスカーソルの位置に向けて発したレイの発生源を求める
ray_orig = view3d_utils.region_2d_to_origin_3d(
region,
space.region_3d,
mv
)
レイの始点の座標は、[3Dビューポート] スペースの3D空間を映し出しているカメラの座標(視点)と同じです。 これは、view3d_utils.region_2d_to_origin_3d
関数を使って取得できます。 一方でレイの向きは、マウスカーソルのリージョン座標を、[3Dビューポート] スペースの3D空間の座標に座標変換することで求めることができます。 この座標変換は、view3d_utils.region_2d_to_vector_3d
関数を使って実現できます。 view3d_utils.region_2d_to_vector_3d
関数と view3d_utils.region_2d_to_origin_3d
関数の引数は、次に示すようにどちらも同じ引数を受け取ります。
引数 | 型 | 意味 |
---|---|---|
第1引数 | bpy.types.Region |
座標変換に使用するリージョン |
第2引数 | bpy.types.RegionView3D |
座標変換に使用する3Dリージョンデータ |
第3引数 | mathutils.Vector |
座標変換対象のリージョン座標の値 |
それぞれの関数に渡す第1引数と第2引数は、get_region_space
関数で取得します。
def get_region_and_space(context, area_type, region_type, space_type):
region = None
area = None
space = None
# 指定されたエリアの情報を取得する
for a in context.screen.areas:
if a.type == area_type:
area = a
break
else:
return (None, None)
# 指定されたリージョンの情報を取得する
for r in area.regions:
if r.type == region_type:
region = r
break
# 指定されたスペースの情報を取得する
for s in area.spaces:
if s.type == space_type:
space = s
break
return (region, space)
get_region_space
関数は、3-5節 で紹介した get_region
関数の改良版です。 get_region_space
関数は、引数 area_type
で指定されたエリア上において、region_type
に指定されたリージョン情報を返すことに加え、引数 space_type
に指定されたスペース情報も返します。
エリアで表示しているスペースの情報は、area
のメンバ変数 spaces
に保存されています。 スペース情報の type
と、引数 space_type
を確認し、一致したものが取得対象となるスペース情報です。 サンプルアドオンでは、[3Dビューポート] スペースの情報と [ウィンドウ] リージョンの情報を取得するため、region_type
が 'WINDOW'
であるリージョン情報と、space_type
が 'VIEW_3D'
であるスペース情報を取得します。
get_region_space
関数を呼び出して取得した情報を用いて、view3d_utils.region_2d_to_vector_3d
関数と view3d_utils.region_2d_to_origin_3d
関数を呼び出します。 第1引数にはリージョン情報を、第2引数にはスペース情報のメンバ変数 region_3d
を、第3引数にはマウスカーソルのリージョン座標を指定して呼び出すことで、レイの向きと始点の座標を [3Dビューポート] スペースの3D座標として求めることができます。
6において、レイと [3Dビューポート] スペースに配置されている、メッシュ型オブジェクトの面と交差判定を行うために使用する ray_cast
メソッドは、レイの始点座標とレイの向きを、ローカル座標で引数に指定する必要があります。 しかし、ray_cast
メソッドによるレイとオブジェクトの交差判定は、オブジェクトのローカル座標で行う必要があります。 このため、2で求めたレイの始点座標と向きを、オブジェクトのローカル座標に座標変換する必要があります。 次のコードは、2で取得したレイの始点の座標とレイの向きから、オブジェクトのローカル座標でのレイの始点座標とレイの向きを求める処理です。
# レイの始点
start = ray_orig
# レイの終点
end = ray_orig + ray_dir
# レイとオブジェクトの交差判定
# 交差判定はオブジェクトのローカル座標で行われるため、
# レイの始点と終点をローカル座標に変換する
mwi = active_obj.matrix_world.inverted()
# レイの始点
mwi_start = mwi @ start
# レイの終点
mwi_end = mwi @ end
# レイの向き
mwi_dir = mwi_end - mwi_start
2で取得したレイの始点の座標とレイの向きから、レイの終点の座標を求めたあと、レイの始点と終点の座標をそれぞれローカル座標に変換している点に注意が必要です。 グローバル座標からローカル座標へは、オブジェクトのグローバル座標変換行列の逆行列である matrix_world.inverted
をグローバル座標にかけることで変換できます。 最後に、ローカル座標に変換されたレイの始点と終点の座標を使って、ローカル座標でのレイの向きを求めています。
オブジェクトのすべての面の選択を解除するためには、bpy.ops.mesh.select_all
関数の引数 action
に 'DESELECT'
を渡します。
# オブジェクトの面選択解除
bpy.ops.mesh.select_all(action='DESELECT')
メッシュデータにアクセスするためには、bmesh用のメッシュデータを構築する必要があります。 bmesh用のメッシュデータを構築するためには、オブジェクトのデータ activate_obj.data
を bmesh.from_edit_mesh
関数に渡す必要があります。
# bmeshオブジェクトの構築
bm = bmesh.from_edit_mesh(active_obj.data)
bmesh.from_edit_mesh
関数は、bmeshモジュールに定義されているため、あらかじめbmeshモジュールをインポートしておく必要があります。
import bmesh
続いて、bmeshオブジェクトからBVHツリーを構築します。 bmeshオブジェクトからBVHツリーを構築するためには、mathutilsモジュールに定義されている mathutils.bvhtree.BVHTree.FromBMesh
関数を呼び出す必要があります。
# BVHツリーの構築
tree = mathutils.bvhtree.BVHTree.FromBMesh(bm)
構築したBVHツリーの ray_cast
メソッドを使って、オブジェクトとレイの交差判定を行います。
# オブジェクトとレイの交差判定を行う
_, _, fidx, _ = tree.ray_cast(mwi_start, mwi_dir, 2000.0)
ray_cast
メソッドの引数には、ローカル座標でのレイの始点と向きに加えて、始点からの距離を渡します。 本節では、始点から距離が2000だけ離れたところにレイの終点を設定するため、第3引数に 2000.0
を渡しています。 このため、レイの始点から2000以上距離が離れたオブジェクトの面は、交差判定の対象外となることに注意が必要です。
交差判定結果は、ray_cast
メソッドの戻り値に保存されています。 ray_cast
メソッドの戻り値の第3要素に、レイと交差した面のインデックスが保存されています。
6の結果を使って、レイと交差したメッシュの面を選択します。 bm.faces[fidx]
で、レイと交差したメッシュの面を取得できるため、そのメンバ変数である select
に True
を設定することで、該当する面を選択状態に変更できます。
# メッシュとレイが衝突した場合
if fidx is not None:
bm.faces[fidx].select = True
本節では、bpy_extrasモジュールのview3d_utilsサブモジュールを使って、リージョン座標から [3Dビューポート] スペース上の3D空間の座標へ、座標変換する方法を説明しました。 ここで、view3d_utilsサブモジュールが提供する、座標変換のAPIの一覧を紹介します。
API | 概要 |
---|---|
view3d_utils.region_2d_to_origin_3d |
リージョンを映すカメラの位置(3D空間の座標)を取得する |
view3d_utils.region_2d_to_vector_3d |
リージョンを映すカメラの位置から、指定されたリージョン座標へ発するレイの方向を3Dベクトルで取得する |
view3d_utils.region_2d_to_location_3d |
指定されたリージョン座標を、3D空間の座標へ変換する |
view3d_utils.location_3d_to_region_2d |
指定した3D空間の座標を、リージョン座標へ変換する |
さらに本節のサンプルアドオンでは、ray_cast
メソッドを使ったレイとオブジェクトの交差判定も行いました。 ray_cast
メソッドは非常に便利な関数で、交差した面に加えて交差した位置なども取得できます。 ray_cast
メソッドを使うことで、例えばマウスでクリックしたときにマウスカーソルの位置に穴を開けたり、マウスカーソルが重なっている面を強調表示といった処理を実装できます。
長くなりましたが、サンプルアドオンを使ってBlenderのAPIを紹介するのは、本節で最後になります。 これまで様々なサンプルアドオンを紹介してきましたが、ここまでで紹介したBlenderのAPIを組み合わせることで、いろいろなことが実現できると思います。 APIを組み合わせて使うことで、よりおもしろく、そして便利な機能を提供できます。 このことを理解してもらえるように、5章 では、これまで紹介してきたAPIを組み合わせて作ったサンプルアドオンをいくつか紹介しますので、アドオンを開発するときの参考にしてみてください。
実質最後の章にあたる 4章 では、アドオン開発時や公開時に参考になる情報を紹介します。 ぜひこちらも読んでみてください。
ray_cast
メソッドを使用することで、レイとオブジェクトの交差判定を行うことができる