Blender 2.6 の Export plug-in を作る (3) 出力と mathutils

スキニング用のジオメトリ情報を調べます。

def decode_shape2( obj ):
  mesh= obj.data
  for poly in mesh.polygons:
    for id in range( poly.loop_start, poly.loop_start + poly.loop_total ):
      vertex= mesh.vertices[ mesh.loops[ id ].vertex_index ]
      for vgroup in vertex.groups:
        print( vgroup.group, vgroup.weight )

shape( bpy.data.objects['Cube'] )

頂点自体に、影響を受けるジオメトリの番号 (vertex group)と重みのセットが
必要個数含まれています。
vertex group の番号が実際に指しているジオメトリの情報は
Object に入っています。

def decode_shape1( obj ):
  if len(obj.vertex_groups):
    for vgroup in obj.vertex_groups:
      print( vgroup.index, ':', vgroup.name )

Object ローカルに影響を受けるジオメトリセットがあり、
その index 番号 (group 番号) が頂点に格納されています。
ゲームなどリアルタイム描画時の構造と同じなので、非常にわかりやすく
扱いやすい形式になっているようです。

実際に影響を与えるジオメトリは Armature と呼ばれているようです。
Modifier の扱いとなっており Subdivision Surface 等と同じです。
Object から Modifier を列挙できるため、その中から ‘ARMATURE’ を
取り出すことができます。

def decode_shape3( obj ):
  for modifier in obj.modifiers
    if modifier.type == 'ARMATURE':
      armature_object= mod.object

通常の Node と同じだろうと思っていたのですが構造が少々違うようです。
Bone 周りは Blender の機能やデータ構造を知らないといけないので
正直よく理解していません。
なので後回しにします。

●ファイル出力

実際に addon としてデータを出力してみます。

# io_export_test_a6.py

import bpy
from bpy_extras.io_utils import ExportHelper
from bpy.props import BoolProperty
from mathutils import Vector, Matrix, Color

bl_info= {
  "name": "Export flatlib a6",
  "category": "Import-Export",
}

class ExportTestA6( bpy.types.Operator, ExportHelper ):
    bl_label= 'export flatlib a6'
    bl_idname= 'export.flatlib_a6'
    filename_ext= '.a6'

    def execute( self, context ):
        A6Export( self ).export( self.filepath )
        return  {'FINISHED'}

def menu_func(self, context):
    self.layout.operator( ExportTestA6.bl_idname, text="Export flatlib a6 (.a6)" )

def unregister():
    bpy.utils.unregister_module(__name__)
    bpy.types.INFO_MT_file_export.remove(menu_func)

def register():
    bpy.utils.register_module(__name__)
    bpy.types.INFO_MT_file_export.append(menu_func)

exporter 本体を A6Export() として別に作成し呼び出す形に。
a6 は独自フォーマットの名称で CPU とかは無関係です。

class A6Export:
    def __init__( self, options ):
        self.options= options

    def decode_tree( self, tree, obj ):
        tree.append( Shape( obj ) )
        for child in obj.children:
            self.decode_tree( tree, child )

    def decode_node( self ):
        tree= []
        for obj in bpy.data.objects:
            if not obj.parent:
                if obj.type == 'MESH' or obj.type == 'EMPTY' or obj.type == 'ARMATURE':
                    self.decode_tree( tree, obj )
        return  tree

    def decode_shape( self, tree ):
        pass

    def output_geometry( self, fo, tree ):
        fo.write( "T \"Geometry\"\n" )
        for gid,node in enumerate(tree):
            fo.write( "N \"%s\"\n" % node.getName() )
            if node.getParent():
                fo.write( "S \"Parent\" \"%s\"\n" % node.getParent() )
            m= node.getMatrix()
            fo.write( "I \"ID\" %d\n" % gid )
            fo.write( "F \"Matrix\"\n" )
            fo.write( "F %f %f %f %f\n" % (m[0][0],m[1][0],m[2][0],m[3][0]) )
            fo.write( "F %f %f %f %f\n" % (m[0][1],m[1][1],m[2][1],m[3][1]) )
            fo.write( "F %f %f %f %f\n" % (m[0][2],m[1][2],m[2][2],m[3][2]) )
            fo.write( "F %f %f %f %f\n" % (m[0][3],m[1][3],m[2][3],m[3][3]) )

    def export( self, filename ):
        print( self.options )
        tree= self.decode_node()
        self.decode_shape( tree )
        fo= open( filename, 'w' )
        self.output_geometry( fo, tree )
        fo.close()

まずは階層構造の解析と matrix の出力のみ。
読みだしたあとの Node を格納するコンテナが Shape です。

class Shape:
    def __init__( self, obj ):
        self._obj= obj
    def getMatrix( self ):
        return  self._obj.matrix_local
    def getName( self ):
        return  self._obj.name
    def getParent( self ):
        return  self._obj.parent.name if self._obj.parent else None

●コマンドとして呼び出す

addon として上記 io_export_test_a6.py を読み込むと
File → Export のメニューに組み込まれますが、これをコマンドとして
呼び出すこともできます。

>>> bpy.ops.export.flatlib_a6( filepath="output.a6" )

Python Console で実行でき、またダイアログを経由しないので便利です。

● mathutils

頂点情報や Matrix には mathutils が使われています。
今まで出てきたのは Matrix, Vector, Color の 3種類です。

Math Types & Utilities (mathutils)

Vector は shader のような swizzle が可能で、上のページを見ると
xxxx ~ wwww まですべての組み合わせが定義されていることがわかります。
例えば dest.xyz= src.zyx などができるようです。

Color や Matrix には swizzle がないので color.rg のような記述はできません。
それぞれ配列のようにシーケンス型として扱うこともできます。

hlsl 等の shader の場合 swizzle も代入も同一です。

float3  dest, src;

dest.xyz= src.xyz;   // -- (1)
dest= src;           // -- (2)

dest.x= src.x;       // |
dest.y= src.y;       // |-- (3)
dest.z= src.z;       // |

上の (1), (2), (3) は全く同じもので値の代入です。
特に Shader Assembler では swizzle と mask の違いを明確にするため
あえて (1) の表記を用いることがあります。

Vector は python のオブジェクトなので、代入は参照であり
swizzle は copy になります。

from mathutils import Vector

src= Vector((1,2,3))
dest= Vector((4,5,6))

dest= src            // -- (4)  (dest is src) == True
dest.xyz= src.xyz    // -- (5)  (dest is src) == False

dest.x= src.x        // |
dest.y= src.y        // |-- (6) (dest is src) == False
dest.z= src.z        // |

(4) は dest に src の参照が入ります。
(5) は新 object を生成して copy します。
src.xyz の代わりに src.copy() や src[:] と記述した場合も同様です。
(6) は値代入なので dest と src は別物です。新たな object が作られません。

場合によっては (6) が欲しいのですが immutable なら (4) で十分で
むしろ効率が良くなります。

Vector 同士の比較には注意が必要です。

Vector((1,2,3)) >= Vector((3,2,1))  # = True    -- (7)
Vector((1,2,3)) >  Vector((3,2,1))  # = False   -- (8)
Vector((1,2,3)) == Vector((3,2,1))  # = False   -- (9)

(7),(8) の結果を見ると (9) が成り立つはずですが False になります。
一致判定は Vector 個々の値を比較しますが、
大小判定には Vecotr の長さが用いられているためです。

つまり (7),(8) は下記の (10),(11) と同じです。

Vector((1,2,3)).length >= Vector((3,2,1)).length # = True   -- (10)
Vector((1,2,3)).length >  Vector((3,2,1)).length # = False  -- (11)

(9) も .length をつければ True になります。

Vector((1,2,3)).length == Vector((3,2,1)).length # = True  -- (12)

頂点の検索アルゴリズムを作る場合このあたり注意が必要です。

●頂点のコンテナ

頂点をマージして index を作るためにコンテナを作ります。
検索は極めて重要です。
数百万頂点など大きなモデルデータを export した場合に終わらなくなります。

class Vertex:
    def __init__( self, pos, normal ):
        self.pos= Vector( pos )
        self.normal= Vector( normal )
    def __hash__( self ):
        return  int( frexp( self.pos.dot( self.normal ) )[0] * 0x7fffffff )
    def __eq__( self, ver ):
        return  self.pos == ver.pos and self.normal == ver.normal

hash はもっと改良できると思いますがとりあえず key として使えました。
比較に関しても最終的にはもっと考慮が必要で、例えば clamp 処理が入ります。

>>> a={ Vertex(Vector((1,2,3)),Vector((0,1,1))): 123, Vertex(Vector((3,4,5)),Vector((1,0,1))): 456 }

>>> a[ Vertex(Vector((1,2,3)),Vector((0,1,1))) ]
123

続きます

関連エントリ
Blender 2.6 の Export plug-in を作る (2) データアクセス
Blender 2.6 の Export plug-in を作る (1)