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

Last Update: 2023.3.1

Blender 2.7

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

Blender 2.7

Last Update: 2023.3.1

3-8. 座標変換を活用する①

3-1節 から 3-5節 では、Blenderが提供するUIのフレームワークを超えて独自のUIを構築する方法について説明しました。 しかし、これらのUIは基本的に2D座標で表現されるため、3Dビューエリアにおいてリージョン座標から3D空間の座標、またはその逆のように座標変換する必要がいずれ出てきます。 このため本節では、Blenderが提供するAPIを使って座標変換を行う方法を説明します。

座標変換について

Blenderが提供する座標変換のAPIの使い方を本節で説明すると書きましたが、3Dプログラミングに馴染みのない方にとって、座標変換の必要性を問われてもよくわからないかもしれません。 筆者が初めて3Dプログラミングに挑戦した時も同じでした。 しかし、3Dゲームや3Dソフト(アドオン含む)を作る場合は、座標変換を理解して使いこなせる必要があります。 例えば、3-1節 のマウスをクリックした位置の面を削除する処理を独自に実装するためには、スクリーン上の座標から [3Dビュー] エリアの3D空間上の座標へ変更する必要があります。 幸いなことに 3-1節 では、スクリーン座標を受け取って [3Dビュー] エリアの3D空間上の位置にある面を選択するAPIがBlenderから提供されているため、座標変換を意識する必要がありませんでした。 しかし仮に、実現したいことに対してBlenderがAPIを提供していない場合は、座標変換する処理が必要になります。 このため本節では、Blenderで使われている座標変換についても説明します。 なお、3Dプログラミングにおける座標変換については、世の中にすでにたくさんの解説記事や書籍があるため詳細な説明はしません。 また、本節を読み進めるためには、行列やベクトル計算の知識が必要となります。

アドオンで座標変換する方法を理解する前に、Blender本体が行っている座標変換を知っておくことは大切ですので、ここで簡単に説明します。 Blenderは、ユーザから与えられた頂点情報(座標や法線)などをもとに3Dポリゴンを画面に表示しますが、このときBlenderの内部では次の座標変換を行っています。

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

ローカル座標やグローバル座標については、Blenderを使っている方であればすでに知っているかもしれません。 ローカル座標は [エディットモード] 時のプロパティパネルの項目 [トランスフォーム] の頂点から [ローカル] を選んだ時の [頂点] に表示されている座標値、グローバル座標は [グローバル] を選んだ時の [頂点] に表示されている座標値です。 ローカル座標に対して、[オブジェクトモード] 時のプロパティパネルの [トランスフォーム] で行った座標変換処理(グローバル座標変換)を適用したものが、グローバル座標になります。

実際にこのことを確認するために、[オブジェクトモード] でオブジェクトに対して座標変換した時の、頂点のグローバル座標の値の変化を見てみましょう。

1 [エディットモード] であることを確認し、[3Dビュー] エリアのプロパティパネルの項目 [トランスフォーム] にある [中点] の座標値を確認します。グローバル座標の座標値を確認するため、[グローバル] ボタンが選択されていることを確認します。
2 [オブジェクトモード] に切り替え、同じく [3Dビュー] エリアのプロパティパネルの項目 [トランスフォーム] にある [位置] の値を、X方向に+1、Y方向に-1します。
3 [エディットモード] に切り替え、[3Dビュー] エリアのプロパティパネルの項目 [トランスフォーム] にある [頂点] 値を確認すると、Xの値が+1、Yの値が-1されていることが確認できます。
3-8節 座標変換 手順3

このように、ローカル座標に対して [オブジェクトモード] で行った座標変換(グローバル座標変換)を適用することで、グローバル座標を得られることがわかります。 グローバル座標からリージョン座標への座標変換についてもBlender内のデータを使って行うことができますが、あまり直感的ではないことと、本節で紹介するAPIがこの変換処理を自動で行うことから、ここでは省略します。 実際のところ、座標変換APIの使い方を理解して本節のサンプルを理解するためには、ローカル座標・グローバル座標・リージョン座標のみを理解できていれば問題ありません。 ただし、座標変換APIが内部で行っている具体的な処理の内容が気になる方のために、3-9節 の一番最後では、自力でローカル座標からリージョン座標へ変換する方法を紹介します。 興味のある方は確認してみてください。

作成するアドオンの仕様

本節では、[3Dビュー] エリア上の3D空間座標からリージョン(2D)座標へ変換する方法を示すため、次のような仕様のアドオンを作成します。

アドオンを作成する

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

import bpy
from bpy.props import BoolProperty
from bpy_extras import view3d_utils
import bgl


bl_info = {
    "name": "サンプル3-8: オブジェクト移動の軌跡を描くアドオン",
    "author": "Nutti",
    "version": (2, 0),
    "blender": (2, 75, 0),
    "location": "3Dビュー > プロパティパネル > オブジェクトの軌跡表示",
    "description": "選択中のオブジェクトが移動した時の軌跡を描くアドオン",
    "warning": "",
    "support": "TESTING",
    "wiki_url": "",
    "tracker_url": "",
    "category": "3D View"
}


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

    bl_idname = "view3d.draw_object_trajectory"
    bl_label = "オブジェクトの軌跡表示"
    bl_description = "選択中のオブジェクトが移動した時に軌跡を表示します"

    __handle = None           # 描画関数ハンドラ
    __loc_history = []

    def __handle_add(self, context):
        if DrawObjectTrajectory.__handle is None:
            # 描画関数の登録
            space = bpy.types.SpaceView3D
            DrawObjectTrajectory.__handle = space.draw_handler_add(
                DrawObjectTrajectory.__render, (context, ),
                'WINDOW', 'POST_PIXEL'
            )

    def __handle_remove(self, context):
        if DrawObjectTrajectory.__handle is not None:
            # 描画関数の登録を解除
            bpy.types.SpaceView3D.draw_handler_remove(
                DrawObjectTrajectory.__handle, 'WINDOW'
            )
            DrawObjectTrajectory.__handle = None

    @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)

    @staticmethod
    def __render(context):
        # 指定したリージョンとスペースを取得する
        region, space = DrawObjectTrajectory.__get_region_space(
            context, 'VIEW_3D', 'WINDOW', 'VIEW_3D'
        )
        if (region is None) or (space is None):
            return

        # 選択されたオブジェクトを取得
        objs = [o for o in bpy.data.objects if o.select]
        # オブジェクトの位置座標(3D座標)をリージョン座標(2D座標)に変換
        DrawObjectTrajectory.__loc_history.append([
            view3d_utils.location_3d_to_region_2d(
                region,
                space.region_3d,
                o.location
            ) for o in objs
        ])

        # 一定期間後、最も古い位置情報を削除する
        if len(DrawObjectTrajectory.__loc_history) >= 100:
            DrawObjectTrajectory.__loc_history.pop(0)

        # 軌跡を描画
        size = 6.0
        bgl.glEnable(bgl.GL_BLEND)
        # 保存されている過去の位置情報をすべて表示
        for hist in DrawObjectTrajectory.__loc_history:
            # 選択されたすべてのオブジェクトについて表示
            for loc in hist:
                bgl.glBegin(bgl.GL_QUADS)
                bgl.glColor4f(0.2, 0.6, 1.0, 0.7)
                bgl.glVertex2f(loc.x - size / 2.0, loc.y - size / 2.0)
                bgl.glVertex2f(loc.x - size / 2.0, loc.y + size / 2.0)
                bgl.glVertex2f(loc.x + size / 2.0, loc.y + size / 2.0)
                bgl.glVertex2f(loc.x + size / 2.0, loc.y - size / 2.0)
                bgl.glEnd()

    def invoke(self, context, event):
        sc = context.scene
        if context.area.type == 'VIEW_3D':
            # 開始ボタンが押された時の処理
            if sc.dot_running is False:
                sc.dot_running = True
                DrawObjectTrajectory.__loc_history = []
                self.__handle_add(context)
                print("サンプル3-8: オブジェクトの軌跡表示を開始しました。")
            # 終了ボタンが押された時の処理
            else:
                sc.dot_running = False
                self.__handle_remove(context)
                print("サンプル3-8: オブジェクトの軌跡表示を終了しました。")
            # 3Dビューの画面を更新
            if context.area:
                context.area.tag_redraw()
            return {'FINISHED'}
        else:
            return {'CANCELLED'}


# UI
class OBJECT_PT_DOT(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.dot_running is False:
            layout.operator(
                DrawObjectTrajectory.bl_idname, text="開始", icon="PLAY"
            )
        else:
            layout.operator(
                DrawObjectTrajectory.bl_idname, text="終了", icon="PAUSE"
            )


# プロパティの作成
def init_props():
    sc = bpy.types.Scene
    sc.dot_running = BoolProperty(
        name="動作中",
        description="オブジェクトの移動軌跡表示機能が動作中か?",
        default=False
    )


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


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


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


if __name__ == "__main__":
    register()

アドオンを使用する

アドオンを有効化する

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

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

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

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

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

1 [3Dビュー] エリアのプロパティパネルに追加された項目 [オブジェクトの軌跡表示] に配置されている、[開始] ボタンをクリックします。
2 [オブジェクトモード] で選択中のオブジェクトを移動すると、移動前のオブジェクトの中心座標の位置に四角形が表示されます。(一定量の軌跡を表示すると、古いものから順に消えていきます。)
3 [3Dビュー] エリアのプロパティパネルの項目 [オブジェクトの軌跡表示] に配置されている [終了] ボタンをクリックすると、軌跡が表示されなくなります。

アドオンを無効化する

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

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

ソースコードの解説

本節のサンプルでは、座標変換の処理以外に bgl モジュールを利用した図形描画処理を行っています。 bgl モジュールを利用した図形描画処理については 3-4節 を参照してください。 本節では、座標変換処理に関係する処理についてのみ説明します。 変数名やクラス名が異なることを除き、3-4節 と似たような実装を意識してサンプルを作成しましたので、図形描画処理のソースコードに関しては困ることはないと思います。 本節のサンプルのソースコードに関して、ポイントとなる点を次に示します。

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

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

変数 意味
dot_running オブジェクトの軌跡表示中の場合は True

bpy_extraモジュール

Blenderが提供するAPIの大半は bpy モジュールに含まれるため、ほとんどのアドオン開発者はこのモジュールを使ってアドオンを開発することになります。 しかし bpy モジュールは、比較的基本的な機能しか提供しないため、少々使いづらいと感じるかもしれません。 このためBlenderは、開発者が比較的よく使うことを想定した便利なAPI群を、bpy_extra モジュールとして提供しています。 bpy_extra モジュールを利用することで、面倒な処理を1から独自に実装することなく実現できるため、実現したい処理を実装するのが面倒だと感じたら、bpy_extra モジュールで当該処理を実現できるAPIが提供されていないかを確認しましょう。

次に示すように、bpy_extra モジュールは複数のサブモジュールから構成されています。 アドオン開発者は、これらのサブモジュールの中から、利用したいAPIを含むサブモジュールをインポートします。

サブモジュール名 概要
anim_utils アニメーション関連の便利API群
object_utils オブジェクト操作関連の便利API群
io_utils インポータ/エクスポータ向けの便利API群やファイルパスに関する便利API群
image_utils 画像ファイルの読込みに関する便利API群
keyconfig_utils キーコンフィグ関連の便利API群
mesh_utils メッシュ型オブジェクトに関する便利API群
view3d_utils 3Dビューエリア上で座標変換を容易に行うためのAPI群

サンプルでは、座標変換を行うために bpy_extra モジュールを利用します。 座標変換を行うためのAPIは、bpy_extra モジュールのサブモジュール view3d_utils に含まれているため、サブモジュール view3d_utils をインポートします。

from bpy_extras import view3d_utils

オブジェクトの軌跡を表示する

本節のサンプルでは、選択中のオブジェクトを移動した時に、オブジェクトの中心座標の軌跡を表示します。 選択中のオブジェクトの軌跡を表示するのは、DrawObjectTrajectory.__render スタティックメソッドです。DrawObjectTrajectory.__render スタティックメソッドでは次の手順に従って、オブジェクトの軌跡を表示します。

  1. オブジェクトの位置座標を取得する
  2. オブジェクトの位置座標をリージョン座標に変換する
  3. 古い位置情報を削除する
  4. 変換したリージョン座標に四角形を描画する

1. オブジェクトの位置座標を取得する

オブジェクトの位置座標は、bpy.data.objects の各要素から取得できるオブジェクトデータの location インスタンス変数から取得することができます。

2. オブジェクトの位置座標をリージョン座標に変換する

1で取得したオブジェクトの位置座標をリージョン座標に変換します。 この座標変換を自力で実装するとなると、カメラのビュー行列などを用いて行列計算を行う必要があり、3Dプログラミングに慣れていない方にとって難関な処理となります。 しかし、幸いなことに bpy_extra モジュールの view3d_utils サブモジュールが提供する view3d_utils.location_3d_to_region_2d 関数を利用することで、この処理を比較的簡単な方法で実現することができます。

view3d_utils.location_3d_to_region_2d 関数は、引数に指定した3D空間の座標をリージョン座標に変換することのできる関数です。 view3d_utils.location_3d_to_region_2d 関数に指定する引数を次に示します。 2つのリージョン情報を引数に渡す必要がありますが、第1引数で指定するリージョン情報はこれまで説明してきたリージョン情報のことを指します。 一方、第2引数で指定する3Dリージョンは、スペース情報に含まれる3Dリージョン情報であることに注意が必要です。 3Dリージョン情報には、ビュー変換行列や射影変換行列などの情報が含まれています。 3Dリージョン情報は、スペース情報のインスタンス変数 region_3d のことを指します。

引数 意味
第1引数 座標変換対象のリージョン情報
第2引数 座標変換対象の3Dリージョン情報
第3引数 3D空間の座標

この view3d_utils.location_3d_to_region_2d 関数を使って、オブジェクトの位置座標をリージョン座標に変換する処理を次に示します。

# 指定したリージョンとスペースを取得する
region, space = DrawObjectTrajectory.__get_region_space(
    context, 'VIEW_3D', 'WINDOW', 'VIEW_3D'
)
if (region is None) or (space is None):
    return

# 選択されたオブジェクトを取得
objs = [o for o in bpy.data.objects if o.select]
# オブジェクトの位置座標(3D座標)をリージョン座標(2D座標)に変換
DrawObjectTrajectory.__loc_history.append([
    view3d_utils.location_3d_to_region_2d(
        region,
        space.region_3d,
        o.location
    ) for o in objs
])

view3d_utils.location_3d_to_region_2d 関数を呼び出すために必要となるリージョン情報とスペース情報は、DrawObjectTrajectory.__get_region_space スタティックメソッドで取得します。 DrawObjectTrajectory.__get_region_space スタティックメソッドは、3-5節 で説明した RenderText.__get_region スタティックメソッドを改良したものです。

@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)

DrawObjectTrajectory.__get_region_space スタティックメソッドは、引数 area_type で指定されたエリア上の、region_type に指定されたリージョン情報を返すことに加え、引数 space_type に指定されたスペース情報も返します。 エリア情報 area に関するスペース情報は area.spaces に保存されています。 s.type と引数 space_type を確認し、一致したものが必要とするスペース情報です。 サンプルでは、[3Dビュー] エリアの [ウィンドウ] リージョンを座標変換対象とするため、region_typeWINDOW であるリージョン情報と、space_typeVIEW_3D であるスペース情報を取得します。

DrawObjectTrajectory.__get_region_space スタティックメソッドを呼び出して取得した情報を用いて、view3d_utils.location_3d_to_region_2d 関数を呼び出します。 第1引数には取得したリージョン情報を、第2引数にはスペース情報の region_3d インスタンス変数を、第3引数にオブジェクトデータの location インスタンス変数を指定して呼び出すことで、第3引数に指定したオブジェクトの座標をリージョン座標に変換することができます。 変換後のリージョン座標は view3d_utils.location_3d_to_region_2d 関数の戻り値として取得できるため、これをクラス変数 DrawObjectTrajectory.__loc_history に保存します。 なお、クラス変数 DrawObjectTrajectory.__loc_history はリストであり、この末尾に座標変換後の座標値を追加することで、リストの先頭が最も古い位置情報、末尾が最新の位置情報になります。

3. 古い位置情報を削除する

位置情報の履歴は、一定量を超えた時に削除しないと、画面が四角形で埋め尽くされてしまいます。 このため、位置情報の履歴数が100以上になった時に最も古い位置情報(クラス変数 DrawObjectTrajectory.__loc_history の先頭の要素)を削除します。

# 一定期間後、最も古い位置情報を削除する
if len(DrawObjectTrajectory.__loc_history) >= 100:
    DrawObjectTrajectory.__loc_history.pop(0)

4. 変換したリージョン座標に四角形を描画する

最後に、変換したリージョン座標に四角形を描画します。 四角形の描画は bgl モジュールを使い、クラス変数 DrawObjectTrajectory.__loc_history に保存されたすべての要素について 3-4節 で説明した方法で描画します。 描画処理については、特に新しいことは行っていないため、具体的な処理の説明は割愛します。

# 軌跡を描画
size = 6.0
bgl.glEnable(bgl.GL_BLEND)
# 保存されている過去の位置情報をすべて表示
for hist in DrawObjectTrajectory.__loc_history:
    # 選択されたすべてのオブジェクトについて表示
    for loc in hist:
        bgl.glBegin(bgl.GL_QUADS)
        bgl.glColor4f(0.2, 0.6, 1.0, 0.7)
        bgl.glVertex2f(loc.x - size / 2.0, loc.y - size / 2.0)
        bgl.glVertex2f(loc.x - size / 2.0, loc.y + size / 2.0)
        bgl.glVertex2f(loc.x + size / 2.0, loc.y + size / 2.0)
        bgl.glVertex2f(loc.x + size / 2.0, loc.y - size / 2.0)
        bgl.glEnd()

まとめ

本節では、[3Dビュー] エリア上の3D空間座標からリージョン座標へ変換するアドオンのサンプルを紹介しました。 bpy_extra モジュールの view3d_utils サブモジュールを使用することにより、独自に座標変換を実装する必要がなくなりました。 このように bpy_extra モジュールには、独自で実装すると面倒な処理を、簡単な手順で実現することができるAPIが提供されています。 bpy_extra モジュールが提供するAPIの数は多くはありませんが、座標変換に限らず便利なAPIを提供しているため、一度目を通しておくとよいと思います。

本節では、[3Dビュー] エリア上の3D空間座標からリージョン座標へ変換する方法を紹介しましたが、3-9節 ではその逆の座標変換である、リージョン座標から [3Dビュー] エリア上の3D空間座標へ座標変換する方法を説明します。

ポイント