はじめてのBlenderアドオン開発

Last Update: 2023.3.1

Blender 2.7

はじめてのBlenderアドオン開発

Blender 2.7

Last Update: 2023.3.1

3-9. 座標変換を活用する②

3-8節では、3D空間の座標からリージョン座標への座標変換を、bpy_extraモジュールのサブモジュールview3d_utilsを使って行う方法を説明しました。 本節では、その逆変換であるリージョン座標から3D空間の座標へ、座標変換する方法を説明します。 また、本節の最後では、bpy_extraモジュールを使わずに3D空間の座標からリージョン座標へ座標変換できることを示します。

作成するアドオンの仕様

本節では、リージョン座標から [3Dビュー] エリア上の3D空間座標へ変換できることを示すため、次のような仕様のアドオンを作成します。 なお、本節のサンプルを理解することで、[3Dビュー] エリア上のオブジェクトと、直線との交差判定方法についても理解することができます。

アドオンを作成する

1-5節 を参考にして以下のソースコードを入力し、ファイル名 sample_3_9.py として保存してください。

import bpy
from bpy.props import BoolProperty
from bpy_extras import view3d_utils
from mathutils import Vector


bl_info = {
    "name": "サンプル3-9: マウスカーソルが重なったオブジェクトを選択するアドオン",
    "author": "Nutti",
    "version": (2, 0),
    "blender": (2, 75, 0),
    "location": "3Dビュー > プロパティパネル > マウスオーバでオブジェクト選択",
    "description": "マウスカーソルが重なったオブジェクトを選択状態に、重なっていないオブジェクトを非選択状態にするアドオン",
    "warning": "",
    "support": "TESTING",
    "wiki_url": "",
    "tracker_url": "",
    "category": "3D View"
}


# オブジェクト名を表示
class SelectObjectOnMouseover(bpy.types.Operator):

    bl_idname = "view3d.select_object_on_mouseover"
    bl_label = "マウスオーバでオブジェクト選択"
    bl_description = "マウスカーソルが重なったオブジェクトを選択状態に、重なっていないオブジェクトを非選択状態にします"

    def __init__(self):
        self.__intersected_objs = []      # マウスカーソルの位置に向けて発したレイと交差するオブジェクト一覧

    @staticmethod
    def __get_region_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)

    def modal(self, context, event):
        sc = context.scene

        if context.mode == 'OBJECT':
            # マウスカーソルのリージョン座標を取得
            mv = Vector((event.mouse_region_x, event.mouse_region_y))
            # 3Dビューエリアのウィンドウリージョンと、スペースを取得する
            region, space = SelectObjectOnMouseover.__get_region_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
            # レイの終点(線分の長さは2000とした)
            end = ray_orig + ray_dir * 2000
            # カメラやライトなど、メッシュ型ではないオブジェクトは除く
            objs = [o for o in bpy.data.objects if o.type == 'MESH']
            self.__intersected_objs = []
            for o in objs:
                try:
                    # レイとオブジェクトの交差判定
                    # 交差判定はオブジェクトのローカル座標で行われるため、
                    # レイの始点と終点をローカル座標に変換する
                    mwi = o.matrix_world.inverted()
                    result = o.ray_cast(mwi * start, mwi * end)
                    # オブジェクトとレイが交差した場合は交差した面のインデックス、
                    # 交差しない場合は-1が返ってくる
                    if result[2] != -1:
                        self.__intersected_objs.append(o)
                # メッシュタイプのオブジェクトが作られているが、
                # ray_cast対象の面が存在しない場合
                except RuntimeError:
                    print(
                        """サンプル3-9: オブジェクト生成タイミングの問題により、
                        例外エラー「レイキャスト可能なデータなし」が発生"""
                    )

        # レイと交差したオブジェクトを選択
        for o in bpy.data.objects:
            o.select = True if o in self.__intersected_objs else False

        # 3Dビューの画面を更新
        if context.area:
            context.area.tag_redraw()

        # 作業時間計測を停止
        if sc.soom_running is False:
            return {'FINISHED'}

        return {'PASS_THROUGH'}

    def invoke(self, context, event):
        sc = context.scene
        if context.area.type == 'VIEW_3D':
            # 開始ボタンが押された時の処理
            if sc.soom_running is False:
                sc.soom_running = True
                context.window_manager.modal_handler_add(self)
                print("サンプル3-9: オブジェクト名の表示を開始しました。")
                return {'RUNNING_MODAL'}
            # 終了ボタンが押された時の処理
            else:
                sc.soom_running = False
                print("サンプル3-9: オブジェクト名の表示を終了しました。")
                return {'FINISHED'}
        else:
            return {'CANCELLED'}


# UI
class OBJECT_PT_SOOM(bpy.types.Panel):

    bl_label = "マウスオーバでオブジェクト選択"
    bl_space_type = "VIEW_3D"
    bl_region_type = "UI"

    def draw(self, context):
        sc = context.scene
        layout = self.layout
        # 開始/停止ボタンを追加
        if sc.soom_running is False:
            layout.operator(
                SelectObjectOnMouseover.bl_idname, text="開始", icon="PLAY"
            )
        else:
            layout.operator(
                SelectObjectOnMouseover.bl_idname, text="終了", icon="PAUSE"
            )


# プロパティの作成
def init_props():
    sc = bpy.types.Scene
    sc.soom_running = BoolProperty(
        name="動作中",
        description="マウスオーバでオブジェクト選択機能が動作中か?",
        default=False
    )


# プロパティの削除
def clear_props():
    sc = bpy.types.Scene
    del sc.soom_running


def register():
    bpy.utils.register_module(__name__)
    init_props()
    print("サンプル3-9: アドオン「サンプル3-9」が有効化されました。")


def unregister():
    clear_props()
    bpy.utils.unregister_module(__name__)
    print("サンプル3-9: アドオン「サンプル3-9」が無効化されました。")


if __name__ == "__main__":
    register()

アドオンを使用する

アドオンを有効化する

1-5節 を参考にして作成したアドオンを有効化すると、コンソールウィンドウに文字列が出力されます。

サンプル3-9: アドオン「サンプル3-9」が有効化されました。

[3Dビュー] エリアのプロパティパネルを表示し、項目 [マウスオーバでオブジェクト選択] が追加されていることを確認します。

アドオンの機能を使用する

有効化したアドオンの機能を使い、動作を確認します。

1 [3Dビュー] エリアのプロパティパネルに追加された項目 [マウスオーバでオブジェクト選択] に配置されている [開始] ボタンをクリックします。
2 [オブジェクトモード] の状態でマウスカーソルをオブジェクトに重ねると、マウスカーソルが重なったオブジェクトが選択状態になります。マウスカーソルがオブジェクトから離れると選択状態が解除されます。
3 項目 [マウスオーバでオブジェクト選択] の [終了] ボタンをクリックすると、マウスカーソルがオブジェクトに重なっても自動的に選択されないようになります。

アドオンを無効化する

1-5節 を参考にして有効化したアドオンを無効化すると、コンソールウィンドウに文字列が出力されます。

サンプル3-9: アドオン「サンプル3-9」が無効化されました。

ソースコードの解説

3-8節 と同様、本節でも座標変換に関する部分に絞って説明します。 このため、サンプルでは invoke メソッドや modal メソッドを使っていますが、本節では説明を省略します。 なお、これらの話題については、3-1節 で説明しています。 本節のサンプルでポイントとなるのは、次の通りです。

アドオン内で利用するプロパティを定義する

複数のクラス間で共有するプロパティ一覧を次に示します。 本節のサンプルでは、共有するプロパティが1つであるため、bpy.types.PropertyGroup によるプロパティのグループ化を行っていません。

変数 意味
soom_running マウスオーバしたオブジェクトを選択する状態である場合は True

クラス変数とインスタンス変数

3-8節 のサンプルでは、位置情報をクラス変数 DrawObjectTrajectory.__loc_history に保存していました。 本節のサンプルでは、レイと交差したオブジェクト一覧をインスタンス変数 __intersected_objs に保存しています。 このようにクラス変数とインスタンス変数を使い分けていることについて、疑問を持つかもしれません。

結論から言うと、クラス変数とインスタンス変数を使い分けたのは、3-8節 と本節のサンプルでモーダルモードに入るか否かの違いがあるからです。

3-8節 では、invoke メソッドの処理を終えたあとに、モーダルモードへ移行することなく invoke メソッドが終了します。 モーダルモードに移行しないため、invoke メソッド終了と同時に、定義されたオペレータクラスのインスタンス変数は削除されてしまいます。 そして削除されたインスタンス変数にアクセスしようとすると、そのような変数はないとエラーが発生してしまいます。 このため、3-8節 では、invoke メソッドが終了したあとも実行し続ける描画関数内でもアクセスできる、クラス変数を位置情報の保存先としています。

一方、invoke メソッドが {'RUNNING_MODAL'} を返してモーダルモードに移行した場合、インスタンス変数はモーダルモードが終了するまで破棄されません。 このため本節のサンプルでは、モーダルモード中にアクセスするための変数として、インスタンス変数を利用しています。 なお、3-8節 のサンプルでも、modal メソッドを定義すればインスタンス変数を使うことができますが、処理が複雑化します。

マウスカーソルの位置に向けて発したレイと交差するオブジェクトを選択する

マウスカーソルの位置に向けて発した、レイと交差するオブジェクトを選択するための手順を次に示します。

  1. マウスカーソルのリージョン座標を取得する
  2. リージョン座標から、レイの向きとレイの発生源の座標を求める
  3. レイの始点と終点の座標を求める
  4. レイと [3Dビュー] エリアに配置されているオブジェクトとの交差判定を行う
  5. レイと交差したオブジェクトを選択する

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

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

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

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

3-1節 で説明したように、マウスカーソルのリージョン座標は、mouse_region_x (X座標)と mouse_region_y (Y座標)で取得することができます。 取得したリージョン座標は mathutils モジュールの Vector クラスとして変数 mv に保存します。

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

1で取得したマウスカーソルのリージョン座標から、レイの向きとレイの発生源の座標を求めます。 3-8節 において、[3Dビュー] エリアの3D空間の座標からリージョン座標へ座標変換する場合と同じく、この座標変換を自力で実装するのは少し面倒です。 そこで本節のサンプルでも、bpy_extra モジュールの view3d_utils サブモジュールを利用します。 マウスのリージョン座標から、レイの向きと発生源の座標を求めるためのコードを以下に示します。

# 3Dビューエリアのウィンドウリージョンと、スペースを取得する
region, space = SelectObjectOnMouseover.__get_region_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引数 座標変換対象のリージョン
第2引数 座標変換対象の3Dリージョンデータ
第3引数 リージョン座標

第1引数と第2引数は、3-8節 で説明した view3d_utils.location_3d_to_region_2d 関数と同じものを指定しますが、第3引数にはリージョン座標を指定することに注意してください。

view3d_utils.location_3d_to_region_2d を呼び出すために必要となるリージョン情報とスペース情報は、SelectObjectOnMouseover.__get_region_space スタティックメソッドで取得します。 SelectObjectOnMouseover.__get_region_space スタティックメソッドで行っている処理について知りたい方は、3-5節3-8節 を参照してください。

3. レイの始点と終点の座標を求める

手順4にて、レイと [3Dビュー] エリアに配置されているオブジェクトとの交差判定を行うために使用する ray_cast 関数は、引数にレイの始点と終点を指定する必要があります。 このため次のコードにより、手順2で取得したレイの向きと発生源の座標からレイの始点と終点を求めます。

# レイの始点
start = ray_orig
# レイの終点(線分の長さは2000とした)
end = ray_orig + ray_dir * 2000

レイの始点はレイの発生源と同じですが、レイの終点は発生源からレイの方向に伸ばした線上に設定します。 本節では、発生源から距離が2000だけ離れたところにレイの終点を設定します。 このため、レイの発生源から2000以上距離が離れたオブジェクトは交差判定の対象外となることに注意が必要です。

4. レイと3Dビューエリアに配置されているオブジェクトとの交差判定を行う

レイと [3Dビュー] エリアに配置されているオブジェクトとの交差判定を行うための処理を次に示します。

# カメラやライトなど、メッシュ型ではないオブジェクトは除く
objs = [o for o in bpy.data.objects if o.type == 'MESH']
self.__intersected_objs = []
for o in objs:
    try:
        # レイとオブジェクトの交差判定
        # 交差判定はオブジェクトのローカル座標で行われるため、
        # レイの始点と終点をローカル座標に変換する
        mwi = o.matrix_world.inverted()
        result = o.ray_cast(mwi * start, mwi * end)
        # オブジェクトとレイが交差した場合は交差した面のインデックス、
        # 交差しない場合は-1が返ってくる
        if result[2] != -1:
            self.__intersected_objs.append(o)
    # メッシュタイプのオブジェクトが作られているが、
    # ray_cast対象の面が存在しない場合
    except RuntimeError:
        print(
            """サンプル3-9: オブジェクト生成タイミングの問題により、
            例外エラー「レイキャスト可能なデータなし」が発生"""
        )

レイとオブジェクトの交差は、ray_cast 関数を呼び出すことで判定できます。 しかし ray_cast 関数には、[オブジェクトモード] 以外で実行できないという制限があります。 本節のサンプルでは、[オブジェクトモード] 時のみオブジェクトを選択する仕様にしているため、この制限が問題になることはありませんが、ray_cast 関数を使う場合は、このような制限があることを意識しておきましょう。 また、ray_cast 関数はその関数の仕様から、メッシュ型のオブジェクトを対象とします。 このため、o.type == 'MESH' であるオブジェクトのみ交差判定を行います。

また、ray_cast 関数によるレイとオブジェクトの交差判定は、オブジェクトのローカル座標で行います。 このため、ray_cast 関数に指定するレイの始点と終点は、オブジェクトのローカル座標に座標変換する必要があります。 本節のサンプルでは、o.matrix_world.inverted 関数を使ってレイの始点と終点の座標をローカル座標に座標変換(座標変換の結果を変数 result に保存)し、ray_cast 関数の引数に指定しました。

レイとオブジェクトが交差したか否かは、ray_cast 関数の戻り値で判断できます。 ray_cast 関数は、次に示すタプル型の値を返します。 ray_cast 関数の戻り値の第3要素が -1 以外の場合は、レイがオブジェクトのいずれかの面と交差したと判定できるため、本節のサンプルではこのことを利用してレイとオブジェクトとの交差判定を行います。 レイと交差したオブジェクトは、インスタンス変数 __intersected_objs に保存します。

戻り値 意味
第1要素 レイが交差した座標(ローカル座標)
第2要素 レイが交差した面の法線
第3要素 レイが交差した面のインデックス(交差した面が存在しない場合は-1)

本節のサンプルでは ray_cast の処理を try ブロックで囲み、例外処理を行っています。 これは、メッシュ型のオブジェクトを作成したときに、作成タイミングの問題で ray_cast の処理を実行できずに例外が発生してしまう場合があるからです。 このため、ray_cast の処理を try ブロックで囲んで、処理が中断してしまうことを回避しています。 なお、この問題はタイミングによる問題であるため、常に発生するものではありませんが、安全面を重視してこのような例外処理を追加しています。

ここで紹介した、ray_cast 関数以外の他のAPIでも同じことですが、ray_cast 関数はBlenderのバージョン間で外部仕様が大きく変わっているようです。 本書が対象とするバージョン2.75では、ray_cast 関数の戻り値はレイが交差した座標・面の法線・面のインデックスの3個でした。 一方、バージョン2.77では、レイとオブジェクトとの交差結果(交差した場合はTrue)に加えて交差した座標・面の法線・面のインデックスなど6個の要素から構成されるタプルが、ray_cast 関数の戻り値になります。 また、ray_cast関数の引数についても、バージョン2.75ではレイの始点と終点の2個を指定するのに対し、2.77ではレイの原点と方向および長さの3個の引数を指定します。
このように、BlenderのバージョンによってAPIの外部仕様が変わることはよくあることで、アドオンのバグ報告の大半がBlender本体のバージョンに関係したものになっています。 4-1節 を参考にして、アドオンの開発を行なっているバージョンのAPIの仕様を確認し、2-1節 で説明したサポート対象のBlenderのバージョンを正しく設定しましょう。
なお、バージョン2.77で正しく動作するコードの一部を次に示します。
# マウスカーソルの位置に向けて発したレイの方向を求める
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
# レイの終点(線分の長さは2000とした)
end = ray_orig + ray_dir * 2000
# カメラやライトなど、メッシュ型ではないオブジェクトは除く
objs = [o for o in bpy.data.objects if o.type == 'MESH']
self.__intersected_objs = []
for o in objs:
    try:
        # レイとオブジェクトの交差判定
        # 交差判定はオブジェクトのローカル座標で行われるため、
        # レイの始点と終点をローカル座標に変換する
        mwi = o.matrix_world.inverted()
        mwi_start = mwi * start
        mwi_end = mwi * end
        dir_ = mwi_end - mwi_start
        dir_.normalize()
        result = o.ray_cast(mwi * start, dir_, 2000)
        # オブジェクトとレイが交差した場合は交差した面のインデックス、
        # 交差しない場合は-1が返ってくる
        if result[0]:
            self.__intersected_objs.append(o)
    # メッシュタイプのオブジェクトが作られているが、ray_cast対象の面が存在しない場合
    except RuntimeError as e:
        print(
            """サンプル3-9: オブジェクト生成タイミングの問題により、
            例外エラー「レイキャスト可能なデータなし」が発生"""
        )

5. レイと交差したオブジェクトを選択する

最後に、インスタンス変数 __intersected_objs に保存した、レイと交差したオブジェクトを選択します。

# レイと交差したオブジェクトを選択
for o in bpy.data.objects:
    o.select = True if o in self.__intersected_objs else False

オブジェクトの選択は、bpy.data.objects の各要素の select メンバ変数に True を設定することで実現できます。 一方、オブジェクトの選択を解除する場合は False を設定します

自力で座標変換を行う

3-8節 の冒頭で、APIを使わずとも自力で座標変換できると書きました。 自力で座標変換できることを理解してもらうため、ここでは bpy_extras モジュールのサブモジュール view3d_utils を利用せずに、ローカル座標からリージョン座標へ座標変換する方法を説明します。

文章だけの説明ではわかりづらいと思いますので、選択中の頂点のローカル座標をリージョン座標へ変換するPythonスクリプト transform_wo_view3d_utils.py を用いて説明します。 実際に本スクリプトの動作確認を行う場合は、スクリプトの内容を記載したあとに [テキストエディタ] エリアのメニューから、[テキスト] > [スクリプト実行] を実行します。

import bpy
import bmesh
from mathutils import Vector


# 射影座標からリージョン座標へ変換する関数
def viewport_transform(region, v):
    wh = region.width / 2.0
    hh = region.height / 2.0
    return Vector((wh + wh * v.x / v.w, hh + hh * v.y / v.w))


# 指定したエリア、リージョン、スペースを取得する関数
def get_region_and_space(area_type, region_type, space_type):
    region = None
    area = None
    space = None

    # 指定されたエリアを取得する
    for a in bpy.context.screen.areas:
        if a.type == area_type:
            area = a
            break
    else:
        return (None, 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 (area, region, space)


def main():
    # 3Dビューエリアのウィンドウリージョンのリージョンとスペースを取得
    (_, region, space) = get_region_and_space('VIEW_3D', 'WINDOW', 'VIEW_3D')
    if space is not None:
        # 選択中の頂点のローカル座標を取得する
        obj = bpy.context.active_object
        bm = bmesh.from_edit_mesh(obj.data)
        vert_local = [
            Vector((v.co[0], v.co[1], v.co[2], 1.0))
            for v in bm.verts if v.select
        ]
        # ローカル座標からグローバル座標への変換
        vert_global = [obj.matrix_world * v for v in vert_local]
        # グローバル座標から射影座標への変換
        vert_perspective = [
            space.region_3d.perspective_matrix * v for v in vert_global
        ]
        # 射影座標からリージョン座標への変換
        vert_region = [viewport_transform(region, v) for v in vert_perspective]
        # 座標を出力
        for l, g, p, r in zip(vert_local, vert_global,
                              vert_perspective, vert_region):
            print("==========")
            print("local: " + repr(l))
            print("global: " + repr(g))
            print("perspective: " + repr(p))
            print("region: " + repr(r))


if __name__ == "__main__":
    main()

3-8節 の冒頭でも書きましたが、ローカル座標からリージョン座標へ座標変換するためには、以下の計算を行う必要があります。

リージョン座標 = ビューポート変換行列 × 射影変換行列 × ビュー変換行列 × グローバル座標変換行列 × ローカル座標

座標変換を行う前に、座標変換に必要となるリージョン情報やスペース情報を取得する必要があります。 リージョン情報やスペース情報の取得は get_region_and_space 関数で行います。 get_region_and_space 関数の処理の詳細については、本節のサンプル sample_3_9.pySelectObjectOnMouseover.__get_region_space スタティックメソッドの説明を参照してください。 第1引数の context を使ってエリア情報を取得するか、bpy.context を使ってエリア情報を取得するかの違いしかありません。 ここで仮に、get_region_and_space 関数の戻り値の第3引数(スペース情報)が None を返した時(指定したスペース情報が存在しなかった場合)は座標変換することができないため、スクリプトの実行を終了します。

リージョン情報とスペース情報を取得をしたあとは、次に示す順番で座標変換を行ないます。

  1. 選択中の頂点のローカル座標を取得する
  2. ローカル座標からグローバル座標へ、座標変換する
  3. グローバル座標から射影座標へ、座標変換する
  4. 射影座標からリージョン座標へ、座標変換する

1. 選択中の頂点のローカル座標を取得する

選択中の頂点のローカル座標は、3-1節 で説明した bmesh モジュールを使って取得します。

# 選択中の頂点のローカル座標を取得する
obj = bpy.context.active_object
bm = bmesh.from_edit_mesh(obj.data)
vert_local = [
    Vector((v.co[0], v.co[1], v.co[2], 1.0))
    for v in bm.verts if v.select
]

メッシュの頂点情報は、リストとして bm.verts に保存されています。 リストの各要素のインスタンス変数 selectTrue の時に頂点が選択されていることから、頂点が選択されているか判断することができます。 頂点のローカル座標は、頂点リストの要素のインスタンス変数 co に保存されています。 座標変換関連のWeb記事や書籍に、より詳しい解説がされているのでここでは詳しくは書きませんが、座標変換する際は3次元に1次元を追加した4次元ベクトル (x, y, z, w) を採用します。 このため、(x, y, z)co メンバ変数から取得した値を使い、残りの座標 ww=1 とします。

2. ローカル座標からグローバル座標へ、座標変換する

手順1で取得した選択中の頂点について、ローカル座標からグローバル座標へ座標変換するための計算式を次に示します。

グローバル座標 = グローバル座標変換行列 × ローカル座標

上記の計算をコードにすると、次のようになります。

# ローカル座標からグローバル座標への変換
vert_global = [obj.matrix_world * v for v in vert_local]

グローバル座標変換行列は obj.matrix_world で取得することができます。 グローバル座標変換行列に、手順1で取得したローカル座標を掛けることで、ローカル座標からグローバル座標へ座標変換できます。 この時、変換行列の掛け算の順番を間違えないように注意してください。 Blenderにおいて変換行列をベクトルに掛ける場合、変換を適用する順番が右から左になるように変換行列を掛けていきます。 例えば、ベクトルに対して変換行列1による変換を行ったあと変換行列2による変換を行いたい場合は次のようになります。

変換後のベクトル = 変換行列2 × 変換行列1 × 変換前のベクトル

3. グローバル座標から射影座標へ、座標変換する

手順2で求めたグローバル座標から射影座標へ、次の計算で座標変換します。

射影座標 = 射影変換行列 × ビュー変換行列 × グローバル座標

ビュー変換行列と射影変換行列は、それぞれスペース情報の3Dリージョン情報のメンバ変数 space.region_3d.view_matrixspace.region_3d.window_matrix で取得することができます。 これらの行列を使って座標変換しても良いのですが、Blenderでは射影変換行列とビュー変換行列を掛けた透視投影変換行列 space.region_3d.perspective_matrix を提供しているため、これを利用することにします。 透視投影変換行列を用いた、グローバル座標から射影座標への座標変換をコードにすると、次のようになります。

# グローバル座標から射影座標への変換
vert_perspective = [
    space.region_3d.perspective_matrix * v for v in vert_global
]
space.region_3d.perspective_matrixは、space.region_3d.window_matrix * space.region_3d.view_matrixで求めることができます。

4. 射影座標からリージョン座標へ、座標変換する

最後に、手順3で求めた射影座標からリージョン座標へ、座標変換します。 座標変換は次の計算で行います。

リージョン座標 = ビューポート変換行列 × 射影座標

Blenderは、ビューポート変換行列を参照するためのAPIを提供していません。 このため、ビューポート変換を自力で行う必要があります。 ビューポート変換を行うために必要な情報は、リージョンの幅と高さの2つで、get_region_and_space 関数で取得したリージョン情報 region から取得することができます。 これらの情報を用いて次の計算を行うことで、ビューポート変換できます。

リージョン座標[X座標] = (リージョンの幅)×(1 + 射影座標[X座標] / 射影座標[W座標])
リージョン座標[Y座標] = (リージョンの高さ)×(1 + 射影座標[Y座標] / 射影座標[W座標])

この計算を行っているのが viewport_transform 関数で、その処理を次に示します。

# 射影座標からリージョン座標へ変換する関数
def viewport_transform(region, v):
    wh = region.width / 2.0
    hh = region.height / 2.0
    return Vector((wh + wh * v.x / v.w, hh + hh * v.y / v.w))

上記の viewport_transform 関数を用いて、射影座標からリージョン座標へ変換するためのコードは次のようになります。

# 射影座標からリージョン座標への変換
vert_region = [viewport_transform(region, v) for v in vert_perspective]

view3d_utilsを使った場合との比較

最後に、自力で座標変換を行った場合と view3d_utils サブモジュールを使って座標変換した場合とで結果が一致することを確認します。 ここでは、view3d_utils サブモジュールを利用して座標変換する場合のスクリプトを transform_w_view3d_utils.py として作成しました。 transform_w_view3d_utils.py が行っている処理については説明しませんので、スクリプトの具体的な処理を理解したい方はソースコードのコメントを参照してください。 スクリプトの内容はこれまでに説明してきた内容だけで作成し、特に新しいことは行っていません。

import bpy
import bmesh
from bpy_extras import view3d_utils


# 指定したエリア、リージョン、スペースを取得する関数
def get_region_and_space(area_type, region_type, space_type):
    region = None
    area = None
    space = None

    # 指定されたエリアを取得する
    for a in bpy.context.screen.areas:
        if a.type == area_type:
            area = a
            break
    else:
        return (None, 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 (area, region, space)


def main():
    # 3Dビューエリアのウィンドウリージョンのリージョンとスペースを取得
    (_, region, space) = get_region_and_space('VIEW_3D', 'WINDOW', 'VIEW_3D')
    if space is not None:
        # 選択中の頂点のローカル座標を取得する
        obj = bpy.context.active_object
        bm = bmesh.from_edit_mesh(obj.data)
        vert_local = [v.co for v in bm.verts if v.select]
        # ローカル座標からリージョン座標への変換
        vert_region = [
            view3d_utils.location_3d_to_region_2d(
                region,
                space.region_3d,
                obj.matrix_world * v
            ) for v in vert_local
        ]
        # 座標を出力
        for l, r in zip(vert_local, vert_region):
            print("==========")
            print("local: " + repr(l))
            print("region: " + repr(r))


if __name__ == "__main__":
    main()

次に、transform_wo_view3d_utils.pytransform_w_view3d_utils.py の2つのスクリプトを [テキストエディタ] エリアにそれぞれ入力し、メニューから [テキスト] > [スクリプト実行] を実行してコンソールウィンドウの出力結果を見てみましょう。

最初に、2つの頂点を選択した状態での transform_wo_view3d_utils.py の実行結果を次に示します。

==========
local: Vector((1.0, 0.9999999403953552, -1.0, 1.0))
global: Vector((5.755486965179443, -2.95807147026062, -1.0, 1.0))
perspective: Vector((-0.35493502020835876, -4.155908107757568, 9.453906059265137, 9.651995658874512))
region: Vector((466.6833801269531, 274.4628601074219))
==========
local: Vector((0.9999993443489075, -1.0000005960464478, 1.0, 1.0))
global: Vector((5.755486488342285, -4.958072185516357, 1.0, 1.0))
perspective: Vector((-2.3789007663726807, -2.613635301589966, 7.838695526123047, 8.037108421325684))
region: Vector((341.093017578125, 325.25555419921875))

選択した2つの頂点の座標について、========== 区切りで各頂点の座標が出力されます。 出力される情報は次の通りです。

情報 意味
local ローカル座標
global グローバル座標
perspective 射影座標
region リージョン座標

同様の条件で、transform_w_view3d_utils.py を実行した時の結果を次に示します。 transform_wo_view3d_utils.py と異なり、local (ローカル座標)と region (リージョン座標)のみ表示します。 リージョン座標を見ると、両者の実行結果が一致していることが確認できると思います。

==========
local: Vector((1.0, 0.9999999403953552, -1.0))
region: Vector((466.6833801269531, 274.4628601074219))
==========
local: Vector((0.9999993443489075, -1.0000005960464478, 1.0))
region: Vector((341.093017578125, 325.25555419921875))

まとめ

本節では、view3d_utils サブモジュールを使って、リージョン座標から [3Dビュー] 上の3D空間の座標へ、座標変換する方法を説明しました。 3-8節 とあわせて、2節にわたって view3d_utils サブモジュールを使った座標変換の方法を説明しましたので、ここで 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 関数を使うことで、例えばマウスでクリックしたときにマウスカーソルの位置に穴を開けたり、マウスカーソルが重なっている面を強調表示といった処理を実装することができます。

本節の最後では、view3d_utils サブモジュールが内部で行っている座標変換について理解したい読者のために、自力でローカル座標からリージョン座標へ座標変換する方法を説明しました。 アドオンを作る上で必ずしも理解する必要がない処理ですが、Blenderがどのように座標変換を行なっているかを理解することは、APIを深く知るきっかけとなります。 また、解説にあたり、自力で座標変換を行うスクリプトを紹介しましたが、細かい最適化やエラー処理は省いています。 座標変換さえ行えれば十分という方は、テストが十分に行われている view3d_utils サブモジュールのAPIを利用するほうがよいでしょう。

ポイント