はじめての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節 で説明しています。

マウスカーソルの位置に向けて発したレイと交差するメッシュの面を選択する

マウスカーソルの位置に向けて発した、レイと交差するメッシュの面を選択するための手順を次に示します。

  1. マウスカーソルのリージョン座標を取得する
  2. リージョン座標から、レイの向きとレイの始点の座標([3Dビューポート] スペースの3D空間座標)を求める
  3. オブジェクトのローカル座標における、レイの始点の座標とレイの向きを求める
  4. オブジェクトの面選択状態を解除する
  5. bmeshオブジェクトとBVHツリーを構築する
  6. オブジェクトとレイの交差判定を行う
  7. レイと交差したメッシュの面を選択する

これらの処理は全て、SAMPLE38_OT_SelectMouseOveredMesh クラスの modal メソッドで行います。

1. マウスカーソルのリージョン座標を取得する

最初に、マウスカーソルのリージョン座標を取得します。 マウスカーソルのリージョン座標を取得するためのコードを次に示します。

# マウスカーソルのリージョン座標を取得
mv = mathutils.Vector((event.mouse_region_x, event.mouse_region_y))

3-1節 で説明したように、マウスカーソルのリージョン座標は、mouse_region_x (X座標)と mouse_region_y (Y座標)で取得できます。

2. リージョン座標から、レイの向きとレイの始点の座標を求める

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座標として求めることができます。

3. オブジェクトのローカル座標における、レイの始点の座標とレイの向きを求める

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 をグローバル座標にかけることで変換できます。 最後に、ローカル座標に変換されたレイの始点と終点の座標を使って、ローカル座標でのレイの向きを求めています。

4. オブジェクトの面選択状態を解除する

オブジェクトのすべての面の選択を解除するためには、bpy.ops.mesh.select_all 関数の引数 action'DESELECT' を渡します。

# オブジェクトの面選択解除
bpy.ops.mesh.select_all(action='DESELECT')

5. bmeshオブジェクトとBVHツリーを構築する

メッシュデータにアクセスするためには、bmesh用のメッシュデータを構築する必要があります。 bmesh用のメッシュデータを構築するためには、オブジェクトのデータ activate_obj.databmesh.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)

6. オブジェクトとレイの交差判定を行う

構築した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要素に、レイと交差した面のインデックスが保存されています。

7. レイと交差したメッシュの面を選択する

6の結果を使って、レイと交差したメッシュの面を選択します。 bm.faces[fidx] で、レイと交差したメッシュの面を取得できるため、そのメンバ変数である selectTrue を設定することで、該当する面を選択状態に変更できます。

# メッシュとレイが衝突した場合
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章 では、アドオン開発時や公開時に参考になる情報を紹介します。 ぜひこちらも読んでみてください。

ポイント