Blender pythonで座標変換の練習。


ちょっと前から少しずつ書いていた座標変換練習です。
今のところ、すごく単純な変換しかできませんが。。。(笑


現在の機能としては、

・選択されているオブジェクトがメッシュオブジェクトだった場合には、
 そのオブジェクトの頂点情報をファイルに出力する。

・開始と終了時刻を指定して、その時刻の間の頂点の状態を出力する。
 (オブジェクトがIpoを持っていても、そうでなくても)

・頂点情報はワールド座標で出力する。


ローカル→ワールド座標への変換は、「こ、これでいいのかな。。」っていう感じですが、
変換された位置と、Blender上で確認したメッシュの頂点位置が一致しているので、
たぶん間違ってはいないと思います。
(無駄な計算をしている可能性はありますけどね)


出力例(時刻 1.0〜11.0まで10.0間隔で出力)

========================================
name: Cube
========================================
<<Mesh Vert>>
    time: 1.0
        <<Center>>
            LOC   (X: 0.071756,	Y: 0.000000,	Z: 3.372549)
            ROT   (X: 0.000000,	Y: 0.000000,	Z: 0.000000)
            SCALE (X: 1.000000,	Y: 1.000000,	Z: 1.000000)
        <<Vert>>
            (X: 1.000000,	Y: 1.000000,	Z: -1.000000)
            (X: 1.000000,	Y: -1.000000,	Z: -1.000000)
            (X: -1.000000,	Y: -1.000000,	Z: -1.000000)
            (X: -1.000000,	Y: 1.000000,	Z: -1.000000)
            (X: 1.000000,	Y: 0.999999,	Z: 1.000000)
            (X: 0.999999,	Y: -1.000001,	Z: 1.000000)
            (X: -1.000000,	Y: -1.000000,	Z: 1.000000)
            (X: -1.000000,	Y: 1.000000,	Z: 1.000000)
    time: 11.0
        <<Center>>
            LOC   (X: 0.260066,	Y: 0.390590,	Z: 3.941661)
            ROT   (X: 0.000000,	Y: 0.000000,	Z: 0.000000)
            SCALE (X: 1.000000,	Y: 1.000000,	Z: 1.000000)
        <<Vert>>
            (X: 1.188309,	Y: 1.390590,	Z: -0.430888)
            (X: 1.188309,	Y: -0.609410,	Z: -0.430888)
            (X: -0.811691,	Y: -0.609410,	Z: -0.430888)
            (X: -0.811690,	Y: 1.390590,	Z: -0.430888)
            (X: 1.188310,	Y: 1.390589,	Z: 1.569112)
            (X: 1.188308,	Y: -0.609411,	Z: 1.569112)
            (X: -0.811691,	Y: -0.609410,	Z: 1.569112)
            (X: -0.811691,	Y: 1.390590,	Z: 1.569112)


少し長いですが、スクリプトを載せておきます。
(内緒ですが、たぶんGUIの部分でメモリリークしてます。。えへへ。。今度こっそり直しときます)

# !BPY
# -*- coding: utf-8 -*-
'''
Name:
Blender:248
Group: "Mesh"
Tooltip: "Export the mesh object information."
'''

__author__ = 'Jkani4'
__url__ = ''
__version__ = '0.1'
__bpydoc__ = """
This script exports the geometry information of the mesh objects selected.
"""
import os
from Blender import Window, Draw, Object, Mesh, Ipo
from Blender.BGL import *
from Blender.Mathutils import RotationMatrix, ScaleMatrix, TranslationMatrix, Vector


class GeoInfo(object):
    '''
    ジオメトリ情報を保持させるためのオブジェクト。
    時刻ごとに様々な情報を保持する。
    '''
    def __init__(self, name):
        '''
        初期化。

        name: 情報名(メッシュオブジェクト名など)
        '''
        self.name = name
        self.vertDict = {}

    def set_verts(self, time, centerLoc, centerRot, centerScale, verts):
        '''
        頂点情報を設定する。
        time:
        centerLoc:
            タプル。
            オブジェクト中心の座標(x, y, z)
        centerRot:
            タプル。
            オブジェクト中心の回転(x, y, z)
        centerScale:
            タプル。
            オブジェクト中心のスケール(x, y, z)
        verts:
            オブジェクト頂点座標(x, y, z)のリストまたはタプル。
        '''
        self.vertDict[time] = (centerLoc, centerRot, centerScale, verts)

    def get_text(self):
        '''
        保持しているすべての情報をテキストに変換して返す

        ARGS: なし。
        RETURN:テキスト
        '''
        text = '========================================\n'
        text += 'name: %s\n' % self.name
        text += '========================================\n'

        text += '<<Mesh Vert>>\n'        
        for time, (cl, cr, cs, vertList) in sorted(self.vertDict.items()):
            text += '    time: %s\n' % time

            text += '        <<Center>>\n'
            text += '            LOC   (X: %f,\tY: %f,\tZ: %f)\n' % cl
            text += '            ROT   (X: %f,\tY: %f,\tZ: %f)\n' % cr
            text += '            SCALE (X: %f,\tY: %f,\tZ: %f)\n' % cs
            text += '        <<Vert>>\n'
            for x, y, z in vertList:
                text += '            (X: %f,\tY: %f,\tZ: %f)\n' % (x, y, z)
        return text


def get_ipo_value(ipo, ipoTypes, time):
    '''
    Ipoオブジェクトから任意の時刻における情報を抽出する。

    ipo:
        情報をとりだす対象のIpoオブジェクト。
    ipoTypes:
        取り出したい情報のタイプ。(タプルまたはリストで指定)
        指定可能なタイプはBlenderリファレンスIpoオブジェクトを参照のこと。
    time:
        時刻(少数値)

    RETURN:
        値のタプル。
        ipiTypesで指定した値が同じ順序で格納されている。

    (使用例)
        Ipoオブジェクトから5フレーム時のX軸、Y軸の位置情報を取り出したい場合、

        result = get_ipo_value(ipo, (Ipo.OB_LOCX, Ipo.OB_LOCY), 5.0)
        
        resultは(X軸の座標値, Y軸の座標値)のタプルとなる。
    '''
    if not ipo:
        return None

    result = []
    for tp in ipoTypes:
        ipoCurve = ipo[tp]
        if not ipoCurve:
            result.append(None)
        else:
            result.append(ipoCurve[time])

    return tuple(result)

def create_world_co_matrix(locX, locY, locZ, rotX, rotY, rotZ, scaleX, scaleY, scaleZ):
    '''
    local -> world座標変換マトリクスを生成。
    '''
    # Scale
    scaleMatrix = ScaleMatrix(scaleX, 4)
    scaleMatrix *= ScaleMatrix(scaleY, 4)
    scaleMatrix *= ScaleMatrix(scaleZ, 4)
    # Rot
    rotMatrix = RotationMatrix(rotX, 4, 'x')
    rotMatrix *= RotationMatrix(rotY, 4, 'y')
    rotMatrix *= RotationMatrix(rotZ, 4, 'z')
    # Loc
    transMatrix = TranslationMatrix(Vector(locX, locY, locZ))
    
    return scaleMatrix * rotMatrix * transMatrix

def mult_matrix_to_verts(verts, matrix):
    '''
    頂点座標にマトリクスを掛け合わせる。

    verts:
        変換対象の座標(x, y, z)のリストまたはタプル
    matrix:
        座標変換マトリクス

    RETURN:
        変換戯れた座標(x, y, z)のリスト
    '''
    result = []
    for v in verts:
        x, y, z = v.co * matrix
        result.append((x, y, z))
    return result

def get_obj_vert_position(obj, time):
    '''
    ある時刻におけるオブジェクトの位置情報を取得する。

    obj:
        計測対象のオブジェクト。
    time:
        フレーム(少数値)
        
    RETURN:
        タプル。(center, vert-positions)
          center: 入れ子のタプル。
                  ((loc.x, loc.y, loc.z),
                   (rot.x, rot.y, rot.z),
                   (scale.x, scale.y, scale.z))
          vert-positions: 入れ子のタプル
                  ((x, y, z), (x, y, z), ....)
    '''
    oMesh = Mesh.Get(obj.getName())
    oMatrix = obj.getMatrix()
    ipo = obj.getIpo()

    # Ipoを持っているオブジェクトの場合にはIpoカーブの情報をもとに
    # 指定された時刻範囲の頂点位置をワールド座標に変換する。
    #
    # Ipoを持っていないオブジェクトの場合にはオブジェクトの保持している
    # マトリクスを使用して頂点をワールド座標に変換する。
    if ipo:
        loc = get_ipo_value(ipo, (Ipo.OB_LOCX, Ipo.OB_LOCY, Ipo.OB_LOCZ), time)
        rot = get_ipo_value(ipo, (Ipo.OB_ROTX, Ipo.OB_ROTY, Ipo.OB_ROTZ), time)
        scale = get_ipo_value(ipo, (Ipo.OB_SCALEX, Ipo.OB_SCALEY, Ipo.OB_SCALEZ), time)

        center = (loc, rot, scale)

        # Ipoの値(object centerのスケール、回転、移動)をもとに
        # 頂点座標変換(local -> world)マトリクスを生成
        matrix = create_world_co_matrix(loc[0], loc[1], loc[2],
                                        rot[0], rot[1], rot[2],
                                        scale[0], scale[1], scale[2])
        vertTup = mult_matrix_to_verts(oMesh.verts, matrix)
    else:
        e = obj.getEuler('worldspace')
        center = (obj.getLocation('worldspace'),
                  (e.x, e.y, e.z),
                  obj.getSize('worldspace'))

        vertTup = mult_matrix_to_verts(oMesh.verts, oMatrix)

    return center, vertTup

def get_mesh_geo_info(start, end, step):
    '''    
    シーン上で現在洗濯中のオブジェクトのうち、
    メッシュオブジェクトに関して座標情報を取得する。
    この関数で取得される座標情報はワールド座標となっているため、
    必要に応じて座標変換すること。

    start:
        計測開始フレーム番号。(小数値)
    end:
        計測終了フレーム番号。(小数値)
    step:
        開始〜終了区間の計測間隔(小数値)

    RETURN:
        GeoInfoオブジェクトのリスト。
        
    (使用例)
        1〜10フレームまでの区間を1フレーム刻みで情報を
        取り出したい場合には、以下のように記述する。
        
        result = get_mesh_geo_info(1.0, 10.0, 1.0)
    '''
    result = []

    for obj in Object.GetSelected():
        if obj.getType() != 'Mesh':
            continue

        geoInfo = GeoInfo(obj.getName())

        time = start
        while time <= end:
            center, vertPosList = get_obj_vert_position(obj, time)
            geoInfo.set_verts(time, center[0], center[1], center[2], vertPosList)

            time += step
            result.append(geoInfo)

    return result


# ==========================================
# User Interface
# ==========================================
EVENT_NUMBER_START = 100
EVENT_NUMBER_END = 101
EVENT_NUMBER_STEP = 102
EVENT_BUTTON_EXPORT = 103
EVENT_BUTTON_FILE_SEL = 104
EVENT_BUTTON_FILE_NAME = 105

g_startNum = Draw.Create(1.0)
g_endNum = Draw.Create(250.0)
g_stepNum = Draw.Create(1.0)
g_exportBt = None
g_fileBt = None
g_filenameBt = Draw.Create('~/')

def cb_draw():
    '''
    描画が発生した際に呼び出されるコールバック。
    '''

    # 開始フレーム番号入力用
    global g_startNum
    g_startNum = Draw.Number('start:', EVENT_NUMBER_START,
                             10, 80, 90, 20, g_startNum.val, 1.0, 250.0,
                             'start frame number')
    # 終了フレーム番号入力用
    global g_endNum
    g_endNum = Draw.Number('end:', EVENT_NUMBER_END,
                           105, 80, 90, 20, g_endNum.val, 0.0, 250.0,
                           'start frame number')
    # フレームのステップ数入力用
    global g_stepNum
    g_stepNum = Draw.Number('step:', EVENT_NUMBER_STEP,
                            200, 80, 90, 20, g_stepNum.val, 1.0, 100.0,
                            'frame steps')
    # ファイル選択用
    global g_fileBt
    g_fileBt = Draw.Button('SEL', EVENT_BUTTON_FILE_SEL,
                           260, 40, 30, 20, 'select file to export' )
    # ファイル名入力用
    global g_filenameBt
    g_filenameBt = Draw.String('', EVENT_BUTTON_FILE_NAME,
                               10, 40, 250, 20, g_filenameBt.val, 256,
                               'file name to export')
    # 処理開始ボタン
    global g_exportBt
    g_exportBt = Draw.Button('export', EVENT_BUTTON_EXPORT,
                             10, 10, 280, 20,
                             'export infomation to file' )

def cb_event(event, val):
    '''
    マウス操作、キーボード操作が行われた際に呼び出されるコールバック。

    event:
        イベント番号(数値定数)
        BlenderのDrawクラスのリファレンスを参照。
    val:
        ボタン/キーの状態。
        0: ボタンまたはキーがはなされた。
        1: ボタンまたはキーが押された。
    '''
    if not val:
        if event in [Draw.QKEY, Draw.ESCKEY]:
            Draw.Exit()

def cb_button_event(event):
    '''
    ボタン操作が行われた場合に呼び出されるコールバック。

    event:
        イベント番号(数値定数)
        BlenderのDrawクラスのリファレンスを参照。
    '''
    if event == EVENT_BUTTON_EXPORT:
        ### 処理開始
        export_info_to_file(g_filenameBt.val)

    elif event == EVENT_BUTTON_FILE_SEL:
        ### ファイル選択
        default = os.path.normpath(os.path.expanduser(g_filenameBt.val))
        Window.FileSelector(cb_file_choose, 'Choose export file', default)

def cb_file_choose(filename):
    '''
    ファイル選択ダイアログから呼び出されるコールバック。

    filename:
        選択されたファイル名(パス文字列)
    '''
    g_filenameBt.val = filename
    Draw.Redraw()

def export_info_to_file(path):
    '''
    情報をファイルへ出力。

    path:
        出力ファイルのパス。
    '''
    expName = os.path.normpath(os.path.expanduser(path))
    if not expName:
        print '[ERR]: Output file is not selected.'
        return

    if os.path.isdir(expName):
        print '[ERR]: Output file is not selected.'
        return

    infoList = get_mesh_geo_info(g_startNum.val, g_endNum.val, g_stepNum.val)

    try:
        f = open(expName, 'w')
        f.write('\n'.join([info.get_text() for info in infoList]))
        f.close()
    except IOError, err:
        print '[ERR]:', err
        return

    print '######## EXPORTED #######'


#
# Register callback
Draw.Register(cb_draw, cb_event, cb_button_event)