Skip to content

Latest commit

 

History

History
427 lines (251 loc) · 32.7 KB

09_Use_Coordinate_Transformation_2.md

File metadata and controls

427 lines (251 loc) · 32.7 KB

座標変換を活用する②

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

作成するアドオンの仕様

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

  • マウスカーソルの位置に向けて放った直線(レイ)と交差するオブジェクト名を選択状態にし、交差していないオブジェクトは非選択状態にする

アドオンを作成する

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

import

アドオンを使用する

アドオンを有効化する

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

サンプル3-9: アドオン「サンプル3-9」が有効化されました。
3Dビュー エリアのプロパティパネルを表示し、項目 マウスオーバでオブジェクト選択 が追加されていることを確認します。 3-9節 アドオン有効化

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

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

Work
1
3Dビュー エリアのプロパティパネルに追加された項目 マウスオーバでオブジェクト選択 に配置されている 開始 ボタンをクリックします。 3-9節 アドオンの使用 手順1

2
オブジェクトモード の状態でマウスカーソルをオブジェクトに重ねると、マウスカーソルが重なったオブジェクトが選択状態になります。マウスカーソルがオブジェクトから離れると選択状態が解除されます。 3-9節 アドオンの使用 手順2

3
項目 マウスオーバでオブジェクト選択終了 ボタンをクリックすると、マウスカーソルがオブジェクトに重なっても自動的に選択されないようになります。 3-9節 アドオンの使用 手順3

アドオンを無効化する

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

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

ソースコードの解説

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

  • リージョン座標から 3Dビュー エリアの3D空間座標への座標変換方法
  • レイとオブジェクトの交差判定

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

複数のクラス間で共有するプロパティ一覧を次に示します。本節のサンプルでは、共有するプロパティが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. マウスカーソルのリージョン座標を取得する

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

import:"get_mouse_region_coord", unindent:"true"

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

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

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

import:"calc_ray_dir_and_orig", unindent:"true"

レイの発生源は、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で取得したレイの向きと発生源の座標からレイの始点と終点を求めます。

import:"calc_ray_start_end", unindent:"true"

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

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

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

import:"check_intersection", unindent:"true"

レイとオブジェクトの交差は、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 に保存した、レイと交差したオブジェクトを選択します。

import:"select_object", unindent:"true"

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

自力で座標変換を行う

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

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

import

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 モジュールを使って取得します。

import:"get_local_coord", unindent:"true"

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

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

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

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

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

import:"transform_local_to_global", unindent:"true"

グローバル座標変換行列は 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 を提供しているため、これを利用することにします。透視投影変換行列を用いた、グローバル座標から射影座標への座標変換をコードにすると、次のようになります。

import:"transform_global_to_pers", unindent:"true"

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() 関数で、その処理を次に示します。

import:"viewport_transform", unindent:"true"

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

import:"transform_pers_to_region", unindent:"true"

view3d_utilsを使った場合との比較

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

import

次に、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を利用するほうがよいでしょう。

ポイント

  • ray_cast() 関数を使用することで、レイとオブジェクトの交差判定を行うことができ、交差位置や交差した面を取得することができる
  • view3d_utils サブモジュールを使わずとも、ローカル座標からリージョン座標へ座標変換することが可能である