Archives

December 2012 の記事

スキニング用 weight/group は頂点(MeshVertex)への追加情報でした。
同じように MeshPolygon (Face) にはマテリアル情報が含まれています。

          Table                  Table の IDが含まれる
------------------------------------------------------------
group     Object.vertex_groups   MeshVertex.groups[n].group
material  Mesh.materials         MeshPolygon.material_index

Maya では Object Material と Face Material の両方を参照する必要が
ありますが、Blender は Face Material だけで情報が取れます。

その代わり Blender には Material 無しのデータが存在できるようです。
この場合でも MeshPolygon.material_index には有効な値 (0) が
入ってしまうのでこれだけでは判定できません。

事前に Mesh.materials のデータ数を調べておき、Material が無い場合は
適当なデフォルトデータを設定しておく必要があります。

def decode_material( obj ):
  mesh= obj.data
  if not mesh.materials:
    primt( 'Empty' )
  else:
    for material in mesh.materials:
      print( material.name )
      print( material.ambient )
      print( material.diffuse_color )
      print( material.specular_color )

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

Texture は Material.texture_slots からたどることができます。
画像かつ外部ファイルと限定すると下記のように取れます。

def decode_material( obj ):
  mesh= obj.data
  if not mesh.materials:
    primt( 'Empty' )
  else:
    for material in mesh.materials:
      print( material.name )
      print( material.ambient )
      print( material.diffuse_color )
      print( material.specular_color )
      for slotid, slot in enumerate( material.texture_slots ):
        if slot and material.use_textures[ slotid ] and slot.use:
         if slot.texture.type == 'IMAGE':
            image= slot.texture.image
            if image.source == 'FILE':
              print( 'Texture=', image.filepath )

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

実際の描画時は Material 単位に行うので、
ポリゴンの描画データは Material 単位に分類しておく必要があります。

最終的には Shader によって必要となるデータが変わるので、
事前に Material やデータ構造を調べて Shader の機能を分類し、
必要となる情報を export する流れとなります。


実際に頂点データを出力してみます。
Primitive は material で分割した Mesh です。
この中に頂点と index を格納していきます。
index 化のために辞書を使っているので、出力時は index 順に
並べ替える必要があります。
Primitive.getVertex() で配列を作り直しています。


MeshPolygon は 4角形以上の可能性があるので decode_shape() で
3角形に変換しています。
まだ Fan なのであまり良い変換ではありません。

法線以外の頂点要素や Material はまだ含まれていません。

# io_export_flatlib_test_a6.py

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

bl_info= {
  "name": "Export Test flatlib a6",
  "author": "Hiroyuki Ogasawara",
  "version": (1, 0, 0),
  "blender": (2, 6, 5),
  "location": "File > Export > flatlib (.a6)",
  "category": "Import-Export",
}

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

class Primitive:
    def __init__( self, material_id ):
        self._vertexBuffer= {}
        self._indexBuffer= []
        self._material_id= material_id
    def addVertex( self, vertex ):
        if vertex in self._vertexBuffer:
            return  vertex.getVBID()
        vertex.setVBID( len(self._vertexBuffer) )
        self._vertexBuffer[ vertex ]= vertex
        return  vertex.getVBID()
    def addIndex( self, index ):
        self._indexBuffer.append( index )
    def getVertexBuffer( self ):
        mapbuf= [ None ] * len( self._vertexBuffer )
        for vertex in self._vertexBuffer:
            mapbuf[ vertex.getVBID() ]= vertex
        return  mapbuf
    def getIndexBuffer( self ):
        return  self._indexBuffer



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

class Shape(Node):
    def __init__( self, obj ):
        self._obj= obj
        self._primitive= {}
    def getMesh( self ):
        return  self._obj.data
    def createPrimitive( self, material_id ):
        self._primitive[material_id]= Primitive( material_id )
    def getPrimitive( self, material_id ):
        return  self._primitive[material_id]
    def getPrimitiveList( self ):
        return  self._primitive.values()
    def isMesh( self ):
        return  True


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

    def decode_tree( self, tree, obj ):
        tree.append( Shape( obj ) if obj.type == 'MESH' else Node( 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_material( self, tree ):
        for node in tree:
            mesh= node.getMesh()
            materiallist= {}
            if not mesh.materials:
                node.createPrimitive( 0 )
            else:
                for poly in mesh.polygons:
                    materiallist[ poly.material_index ]= poly.material_index
                for material_id in materiallist.values():
                    node.createPrimitive( material_id )

    def decode_shape( self, tree ):
        ver_index= [ None, None, None ]
        for node in tree:
            mesh= node.getMesh()
            for poly in mesh.polygons:
                prim= node.getPrimitive( poly.material_index )
                triangle= 0
                for id in range( poly.loop_start, poly.loop_start + poly.loop_total ):
                    vertex= mesh.vertices[ mesh.loops[id].vertex_index ]
                    ver= Vertex( vertex.co )
                    ver.setNormal( vertex.normal if poly.use_smooth else poly.normal )
                    ver_index[triangle]= ver
                    triangle+= 1
                    if triangle >= 3:
                        prim.addIndex( prim.addVertex( ver_index[0] ) )
                        prim.addIndex( prim.addVertex( ver_index[1] ) )
                        prim.addIndex( prim.addVertex( ver_index[2] ) )
                        triangle= 2
                        ver_index[1]= ver_index[2]

    def output_geometry( self, fo, tree ):
        fo.write( "#\nT \"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 output_vertex( self, fo, tree ):
        fo.write( "#\nT \"Vertex\"\n" )
        for nodeid, node in enumerate( tree ):
            for primid, prim in enumerate( node.getPrimitiveList() ):
                fo.write( "N \"_vb%03d_%03d\"\n" % (nodeid, primid) )
                buffer= prim.getVertexBuffer()
                stride= 24
                fo.write( "I \"Size\" %d %d\n" % (len(buffer), stride) )
                fo.write( "M \"v\"\n" )
                for vertex in buffer:
                    fo.write( "F %f %f %f\n" % (vertex.pos.x, vertex.pos.y, vertex.pos.z) )
                    fo.write( "F %f %f %f\n" % (vertex.normal.x, vertex.normal.y, vertex.normal.z) )

    def output_index( self, fo, tree ):
        fo.write( "#\nT \"Index\"\n" )
        for nodeid, node in enumerate( tree ):
            for primid, prim in enumerate( node.getPrimitiveList() ):
                fo.write( "N \"_ib%03d_%03d\"\n" % (nodeid, primid) )
                fo.write( "I \"Size\" %d\n" % len(prim.getIndexBuffer()) )
                fo.write( "I \"i\"\n" )
                indexlist= prim.getIndexBuffer()
                for tid in range( 0, len(indexlist), 3 ):
                    fo.write( "I %d %d %d\n" % (indexlist[tid], indexlist[tid+1], indexlist[tid+2] ) )

    def output_shape( self, fo, tree ):
        fo.write( "#\nT \"Command\"\n" )
        for nodeid, node in enumerate( tree ):
            for primid, prim in enumerate( node.getPrimitiveList() ):
                fo.write( "N \"_pr%03d_%03d\"\n" % (nodeid, primid) )
                fo.write( "S \"Geometry\" \"%s\"\n" % node.getName() )
                fo.write( "S \"Vertex\" \"_vb%03d_%03d\"\n" % (nodeid, primid) )
                fo.write( "S \"Index\" \"_ib%03d_%03d\"\n" % (nodeid, primid) )
                fo.write( "S \"Material\" \"unknown\"\n" )

    def output_material( self, fo, tree ):
        pass

    def export( self, filename ):
        print( self.options )
        tree= self.decode_node()
        meshlist= [ node for node in tree if node.isMesh()]
        self.decode_material( meshlist )
        self.decode_shape( meshlist )
	#
        fo= open( filename, 'w' )
        self.output_material( fo, meshlist )
        self.output_geometry( fo, tree )
        self.output_vertex( fo, meshlist )
        self.output_index( fo, meshlist )
        self.output_shape( fo, meshlist )
        fo.close()


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

    flag_selected= BoolProperty( name= "Selected Objects",
                description= "Export selected objects",
                default= False )

    flag_lefthand= BoolProperty( name= "Left-Hand (DirectX)",
                description= "Lfef-Hand (DirectX)",
                default= True )

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


def menu_func(self, context):
    self.layout.operator( ExportTestA6.bl_idname, text="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)

今頃気が付きましたが 3ds Max のように Z-up なので、出力時に
Y-up に変換しておいた方がよさそうです。


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


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

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)


Blender の API は bpy で始まり、データアクセス手段は
bpy.data 以下に集約されています。

例えばシーン内のオブジェクトは bpy.data.objects で取れます。
下記のように Python Console で確認できます。
(太字がキー入力です)

>>> bpy.data.objects['Cube']
bpy.data.objects['Cube']

>>> bpy.data.objects.keys()
['Camera', 'Cube', 'Lamp']

>>> bpy.data.objects.values()
[bpy.data.objects['Camera'], bpy.data.objects['Cube'], bpy.dta.objects['Lamp']]

↑Python の辞書(連想配列)のように見えますがちょっと違います。

>>> bpy.data.objects
<bpy_collection[3], BlendDataObjects>

>>> bpy.data.objects[0]
bpy.data.objects['Camera']

>>> bpy.data.objects.find('Cube')
1

>>> bpy.data.objects[1:]
[bpy.data.objects['Cube'], bpy.dta.objects['Lamp']]

↑数値 index でもアクセス可能で要素の順番が固定。
find() を使うと index を取ることが可能です。
特に for in では要素が列挙され、配列のような振る舞いをします。

>>> for a in bpy.data.objects: print( a )
<bpy_struct, Object("Camera")>
<bpy_struct, Object("Cube")>
<bpy_struct, Object("Lamp")>

key を持つがシーケンスとして扱える特殊な collection となっています。
python に慣れてないせいもありますが、
辞書のつもりで script を読んでいると少々混乱します。


● Object Type

Object の種類は type プロパティで確認できます。

>>> for a in bpy.data.objects: print( a.type )
CAMERA
MESH
LAMP

bpy.types にデータの種類一覧があります。

Types (bpy.types)

この bpy.types が Maya でいう Node に相当するもののようです。
例えば 'Cube' は bpy.types.Mesh で、独自のインスタンスととデータは
Object.data からアクセスします。

>>> bpy.data.objects['Cube'].data.vertices[1].co
Vector((1.0, -1.0, -1.0))

Object 自体が階層構造を持てるので DagNode を兼ねています。
Maya ではインスタンスである Object とメソッドでである Function が
分離していましたが、Blender はそのようなことがなくシンプルです。

Object の階層は下記のようにたどることができます。
Maya のように Transform Node が別だったりしません。

def tree( obj ):
  print( obj )
  print( obj.matrix_local )
  for child in obj.children:
    tree( child )

def root():
  for obj in bpy.data.objects:
    if not obj.parent:
      tree( obj )

root()

root の object だけ得るには
[obj for obj in bpy.data.objects if not obj.parent]
で出来ます。

bpy.data.objects はすべてのオブジェクトを列挙しますが、
同時に data type ごとの collection も持っているようです。

>>> bpy.data.objects.keys()
['Camera', 'Cube', 'Lamp', 'Sphere', 'Suzanne']

>>> bpy.data.meshes.keys()
['Cube', 'Sphere', 'Suzanne']

bpy.data.meshes に入っているのはデータそのもの、Object.data です。
こちらは階層などの Node 情報を含んでいません。

>>> bpy.data.meshes['Cube'].vertices[1].co
Vector((1.0, -1.0, -1.0))

Object を削除しても Mesh は残っているようです。
この辺の Blender の内部挙動はまだ私の方がきちんと理解しておりません。

matrix_local はドキュメント Object(ID) では float array
と書かれてますが mathutils.Matrix を返すようです。


● Mesh のアクセス方法

Mesh には形状データが入っています。
頂点データ自体は Mesh.vertices ですが、下記ページの解説にあるように
edge, loop, polygon と 3種類のインデックスがあります。

Mesh(ID)

・MeshVertex : 頂点データそのもの
・MeshEdge : 2頂点の index
・MeshLoop : 1頂点 + edge の index
・MeshPolygon : n個の loop の index

mesh= Suzanne
<bpy_collection[500], MeshPolygons>
<bpy_collection[507], MeshVertices>
<bpy_collection[1968], MeshLoops>
<bpy_collection[1005], MeshEdges>
mesh= Cube
<bpy_collection[6], MeshPolygons>
<bpy_collection[8], MeshVertices>
<bpy_collection[24], MeshLoops>
<bpy_collection[12], MeshEdges>
mesh= Sphere
<bpy_collection[512], MeshPolygons>
<bpy_collection[482], MeshVertices>
<bpy_collection[1984], MeshLoops>
<bpy_collection[992], MeshEdges>

Mesh 内の MeshPolygon (Face) の列挙は下記の通り。

def shape( obj ):
  mesh= obj.data
  for poly in mesh.polygons:
    print( poly )

MeshPolygon に含まれる頂点 (Face Vertex) を得るには下記の通り。

# Loop を使った場合
def shape( obj ):
  mesh= obj.data
  for poly in mesh.polygons:
    for id in range( poly.loop_start, poly.loop_start + poly.loop_total ):
      print( mesh.verticies[ mesh.loops[ id ].vertex_index ].co )

MeshPolygon に含まれる MeshLoop をたどって MeshLoop.vertex_index を
参照しています。
なお、Loop を使わなくても MeshPolygon.vertices で直接頂点 index を
取れることがわかりました。

def shape( obj ):
  mesh= obj.data
  for poly in mesh.polygons:
    for ver in poly.vertices:
      print( mesh.vertices[ ver ].co )

ただし、mesh.loops の参照用 index 値 (「Loop を使った場合」の id ) は
uv など他のデータアクセスにも使います。
loop は Face Vertex 単位のデータとなり、Cube の場合 24 個あります。
一見複雑ですが「Loop を使った場合」の方が都合が良いようです。


● 法線

頂点データ MeshVertex に直接頂点法線が格納されています。
頂点座標が共有される場合法線も共有されており、この値は
ハードエッジになりません。

頂点とは別に MeshPolygon に面法線が格納されており、プロパティ
Mesh.use_smooth の値によってこの両者を使い分ける必要があるようです。


● UV / 頂点カラー

Mesh.uv_layers に格納されます。値そのものは MeshUVLoop.uv です。
uv_layers が MeshUVLoopLayer で data に MeshUVLoop が頂点分 (Loop 数分)
格納されます。

>>> len(bpy.data.objects['Cube'].data.uv_layers)
1

>>> len(bpy.data.objects['Cube'].data.uv_layers[0].data)
24

>>> bpy.data.objects['Cube'].data.uv_layers[0].data[0].uv
Vector((0.0), 0.0))

>>> bpy.data.objects['Cube'].data.uv_layers.active.data[0].uv
Vector((0.0), 0.0))

頂点カラーも全く同じで Mesh.vertex_colors に入ります。
今までの情報を取り出すコードがこれです。

def shape( obj ):
  print( 'mesh=', obj.name )
  mesh= obj.data
  uv_array= mesh.uv_layers.active.data if len(mesh.uv_layers) else None
  vcolor_array= mesh.vertex_colors.active.data if len(mesh.vertex_colors) else None
  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 ]
      position= vertex.co
      normal= vertex.normal
      if not poly.use_smooth:
        normal= poly.normal
      print( 'pos=', position )
      print( 'normal=', normal )
      if uv_array:
        uv= uv_array[id].uv
        print( 'uv=', uv )
      if vcolor_array:
        vcolor= vcolor_array[id].color
        print( 'vcolor=', vcolor )


def tree( obj ):
  if obj.type == 'MESH':
      shape( obj )
  #print( 'matrix=', obj.matrix_local )
  for child in obj.children:
    tree( child )

def root():
  for obj in bpy.data.objects:
    if not obj.parent:
      tree( obj )

root()

それぞれ最初の(active な) UV set と VertexColor しか参照していないので注意してください。
uv_array, vcolor_array の参照に Loop と同じ id を使っていることがわかります。
Matrix 同様、ドキュメントではただの array ですが実際に返す値は
mathutils.Vector, mathutils.Color となっています。

続きます


関連エントリ
Blender 2.6 の Export plug-in を作る (1)


Blender を使った経験が全く無いので練習兼ねてプラグイン作ります。

Blender

Blender 2.65 を install 。
Script は python 3.3 で、plug-in (addon) もこれで記述するようです。

Scripts Development

本体に DirectX .x や fbx など多くの exporter addon が含まれているので、これらのスクリプトが参考になります。

Windows だとこの辺に入っています。
C:\Program Files\Blender Foundation\Blender\2.65\scripts\addons


●Python Console

Blender 上の 3D View など、Window を Python Console に切り替えられます。
直接コマンドを実行して動作確認ができるので便利です。



●スクリプトの置き場所

addon として作成した script は下記の場所に入れておきます。

◎ Windows 7/8

 C:\Users\<ユーザー名>\AppData\Roaming\Blender Foundation\Blender\2.65\scripts\addons

◎ MacOS X

 /Users/<ユーザー名>/Library/Application Support/Blender/2.65/scripts/addons

Add-Ons
Configuration & Data Paths

上記場所に入れたスクリプトでは最低限 bl_info の定義が必要です。
User Preferences の Addons を開くときに、フォルダ内の python ファイルから bl_info を検索して定義内容を取り込んでいるようです。
bl_info がなかったり定義にミスがあるとエラーになります。

Script meta informations

# test.py (Preference に列挙されるための最小限)

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

print( "on load message" );


●スクリプトの読み込み&実行

Blender のメニューから addon を有効にします。

   File → User Preferences ... → Addons

上記の Addon スクリプト置き場に入っていて、かつ bl_info が定義されていればここに列挙されます。
左サイドの Categories で絞り込むと見つけやすくなります。
上の test.py なら "Import-Export" に入ります。

test.py である「Import-Export: Test」のチェックボックスを On にすると script が読み込まれて走ります。
ただし上の test.py は class を定義しておらず、内容が不完全なのでエラーになります。


●エラーコンソール

addon としてロードした script の print 出力 (stdout) は、
Blender 内の Python Console には表示されません。
エラーなどのログを見るにはコンソールを開きます。

Windows の場合、メニューの "Window" から

   Window → Toggle System Console

Maya でも別ウィンドウになっているあれ (Output Window) と同じです。

MacOS X/Linux の場合上記のメニューがないのでコマンドラインから起動します。
Finder からではなく、ターミナルを開いて blender コマンドを直接実行します。

$ /Applications/Blender/blender.app/Contents/MacOS/blender

The Console Window

これで上記 test.py を読み込もうとすると "on load message" が表示されたあと
エラーが出ていることを確認できます。


●コマンドの追加と実行

addon はコマンドの class を定義してシステムに登録する必要があります。

Preferences Addons のチェックボックスの on/off で script 内の
register()/unregister() が呼び出されています。
このタイミングで class を登録を行います。

# test.py (2)
import bpy

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

class ExportTest( bpy.types.Operator ):
    bl_label= 'export test'
    bl_idname= 'export.test_a6'

    def execute( self, context ):
        print( "exec-command" )
        return {'FINISHED'}

def unregister():
    bpy.utils.register_module(__name__)

def register():
    bpy.utils.unregister_module(__name__)

定義クラスは bl_label, bl_idname が必須で、bl_idname が識別子になっています。
(2) の test.py は Preferences Addos で正しくロードできます。

Preferences Addos の "Import-Export: Test" のチェックを入れると
register() が呼び出されて bl_idname で指定したコマンド名が有効になります。

Python Console を開いて export.test_a6() が実行できることを確認できます。
(↓太字は出力結果)

>>> bpy.ops.export.test_a6()
exec-command
{'NIFISHED'}


●Export Menu に追加する

付属の scripts/addons/io_scene_fbx を参考にしました。
io_export_direct_x.py の方は ExportHelper を使わずに実装されているようです。

# io_export_test_a6.py
import bpy
from bpy_extras.io_utils import ExportHelper

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

class ExportTestA6( bpy.types.Operator, ExportHelper ):
    bl_label= 'export test a6'
    bl_idname= 'export.test_a6'
    filename_ext= '.txt' # ←必須

    def execute( self, context ):
        print( 'file=' + self.filepath )
        return  {'FINISHED'}


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

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)

register()/unregister() の中で class だけでなく menu_func も登録しています。
この menu_func() はメニューの

   File → Export

のサブメニューを開くタイミングで呼ばれるようです。
self.layout.operator() の最初の引数が実行する class id、text が Menu のラベルです。
例えば menu_func() を

def menu_func(self, context):
    self.layout.operator( "export_scene.fbx", text="Export Test a6?" )

と書き換えると File → Export →「Export Test a6?」の選択で fbx の
export を呼び出せます。


●Export 時のオプション

Export 実行時に左側のサイドバーでオプション選択ができます。
例えば選択されたオブジェクトだけ Export したり、Axis を変更したりなど。
このようなプロパティは Export class に追加しておきます。

~

class ExportTestA6( bpy.types.Operator, ExportHelper ):
    bl_label= 'export test a6'
    bl_idname= 'export.test_a6'
    filename_ext= '.txt'

    # ↓追加
    flag_selected= BoolProperty( name= "Selected Objects",
                description= "Export selected objects",
                default= False )

    def execute( self, context ):
        print( 'file=' + self.filepath )
        print( 'flag=', self.flag_selected )
        return  {'FINISHED'}

~

これで File → Export →「Export Test a6」を選択すると、
Export ファイル選択画面となり、左サイドバーから「Selected Objects」の
チェックが可能となります。

右上の「export test a6」ボタンを押すと、コンソール出力で
入力したファイル名と "Selected Objects" のチェック状態が読み取れている事がわかります。


まだ必要最小限ですが addon 登録周りの挙動がわかったので、
次は内部データのアクセスを行います。


Qualcomm の CPU Krait の浮動小数点演算の速度を調べてみました。

                 (1)     (2)     (3)      (4)    (5)
                 iPad3  Touch5   EVO 3D  iPad4 Butterfly
                 A5X     A5      8660     A6X    8064
                 C-A9    C-A9   Scorpion Swift   Krait
--------------------------------------------------------
a:mat44 neon_AQ  4.784   5.979   2.879   1.204   1.919
b:mat44 neon_BQ  2.408   3.008   1.146   1.266   1.273
c:mat44 neon_AD  4.781   5.974   3.079   1.554   2.453
d:mat44 neon_BD  2.406   3.007   1.440   1.344   2.041
e:fadds      A   4.010   5.013   3.460   2.882   3.791
f:fmuls      A   4.010   5.012   4.361   2.953   3.671
g:fmacs      A   4.012   5.011   4.034   5.763   7.334
h:vfma.f32   A   -----   -----   -----   5.765   3.725
i:vadd.f32 D A   4.111   5.136   3.493   2.877   3.706
j:vmul.f32 D A   4.110   5.136   3.502   2.950   3.667
k:vmla.f32 D A   4.512   5.650   3.638   2.951   7.557
l:vadd.f32 Q A   8.023  10.036   3.408   2.878   3.677
m:vmul.f32 Q A   8.022  10.028   3.427   2.952   3.647
n:vmla.f32 Q A   8.025  10.028   3.400   2.955   7.362
o:vfma.f32 D A   -----   -----   -----   2.494   3.676
p:fadds      B   4.014   5.013   5.972   5.757   4.664
q:fmuls      B   5.013   6.265   5.960   5.760   4.583
r:fmacs      B   8.023  10.024   8.573  11.521   8.266
s:vfma.f32   B   -----   -----   -----  11.519   4.611
t:vadd.f32 D B   4.113   5.137   5.945   2.881   4.746
u:vmul.f32 D B   4.118   5.145   5.098   2.951   4.680
v:vmla.f32 D B   9.027  11.278   8.498   5.757   8.361
w:vadd.f32 Q B   8.021  10.023   5.950   2.879   4.702
x:vmul.f32 Q B   8.029  10.023   5.095   2.951   4.595
y:vmla.f32 Q B   9.026  11.277   8.497   5.762   8.464
z:vfma.f32 D B   -----   -----   -----   5.759   4.660
--------------------------------------------------------
↑数値は実行時間(秒) 数値が小さい方が速い


(1)=Apple iPad 3           A5X      ARM Cortex-A9 x2  1.0GHz
(2)=Apple iPod touch 5     A5       ARM Cortex-A9 x2  0.8GHz
(3)=HTC EVO 3D ISW12HT     MSM8660  Scorpion      x2  1.2GHz
(4)=Apple iPad 4           A6X      A6 (swift)    x2    ?GHz
(5)=HTC J butterfly HTL21  APQ8064  Krait         x4  1.5GHz

(1)~(4) は過去の計測値です。
SDK や OS などの条件が完全に同一ではないのと、計測方法にも
いろいろミスや勘違いが含まれてる可能性があります。
予めご了承ください。

またアプリケーションの実速度ではなく CPU 単体の能力やパイプラインを
調べるのが目的なので、オンキャッシュ前提で意味のない演算を走らせています。


● a:~d: NEON を使った Matrix 4x4 同士の乗算

A はパイプライン最適化なし、B はインターリーブしたものです。
Q=128bit(float x4), D=64bit(float x2) 命令です。
64bit の方が実行命令数が倍になります。
具体的な違いは下記の通り。

; AQ
    vldmia	%0, {d0-d7}
    vldmia	%1, {d8-d15}
    vmul.f32	q8,q0,d8[0]
    vmla.f32	q8,q1,d8[1]
    vmla.f32	q8,q2,d9[0]
    vmla.f32	q8,q3,d9[1]
    vstmia	%2!, {d16,d17}
    ~

; BQ
    vldmia	%0, {d0-d7}
    vldmia	%1, {d8-d15}
    vmul.f32	q8,q0,d8[0]
    vmul.f32	q9,q0,d10[0]
    vmul.f32	q10,q0,d12[0]
    vmul.f32	q11,q0,d14[0]
    ~

; BD
    vldmia	%0, {d0-d7}
    vldmia	%1, {d8-d15}
    vmul.f32	d16,d0,d8[0]
    vmul.f32	d17,d1,d8[0]
    vmla.f32	d16,d2,d8[1]
    vmla.f32	d17,d3,d8[1]
    vmla.f32	d16,d4,d9[0]
    vmla.f32	d17,d5,d9[0]
    vmla.f32	d16,d6,d9[1]
    vmla.f32	d17,d7,d9[1]
    vstmia	%2!, {d16,d17}
    ~


● e:~z: VFP/NEON の命令単体の実行

vfma は VFPv4 の新しい命令なので、CPU A6(Swift), Krait のみ
実行できます。(Cortex-A9/Scorpion は VFPv3)

無印 = VFP スカラー (float x1)
D    = NEON d レジスタ 64bit (float x2)
Q    = NEON q レジスタ 128bit (float x4)

A は in/out が完全に別レジスタです。
B は 4 命令毎のインターリブで、レイテンシが大きい場合は
A よりもストールする可能性が高くなります。

// A
#define NEON_OPSETv1_8D(op)	\
	op " d0, d1, d1 \n"	\
	op " d2, d1, d1 \n"	\
	op " d3, d1, d1 \n"	\
	op " d4, d1, d1 \n"	\
	op " d5, d1, d1 \n"	\
	op " d6, d1, d1 \n"	\
	op " d7, d1, d1 \n"	\
	op " d8, d1, d1 \n"

// B
#define NEON_OPSETv2_8D(op)	\
	op " d0, d1, d5 \n"	\
	op " d2, d1, d6 \n"	\
	op " d3, d1, d7 \n"	\
	op " d4, d1, d8 \n"	\
	op " d5, d1, d0 \n"	\
	op " d6, d1, d2 \n"	\
	op " d7, d1, d3 \n"	\
	op " d8, d1, d4 \n"


● ALU

(1)/(2) Cortex-A9 の NEON は 64bit (float x2) なので
j: と m: のように D , Q で倍近い差が生じています。

(3) Qualcomm は Scorpion でも NEON が 128bit なので、
j: と m: の差がなく Q では Cortex-A9 の倍の速度が出ています。

(4)/(5) Scorpion 同様 D,Q の差がほとんど無く、NEON が 128bit
だと考えられます。


● Pipeline

(3) の Scorpion は 128bit NEON かつクロックが高いこともあり、
(1)/(2) の Cortex-A9 と比べると全体的に fp 性能が高くなっています。

ですが e:~o: の A に比べて p:~z: の落ち込みが起きており、
低クロックの Cortex-A9 より遅くなっているケースがあります。
Cortex-A9 の場合一部の命令以外は A と B の差が生じていません。

Scorpion は pipeline のステージが深いこと、Cortex-A9 も積和の
fmacs/vmla ではレイテンシが大きいことがわかります。


(4) の A6(Swift) は NEON の場合 A/B の差がほとんど無く、
全体的にスループットが高くなっています。
NEON も out of order で実行していると考えられます。

ただし VFP のスカラーの方が B で遅くなっているため、
レジスタリネーミングは 64bit 単位ではないかと思います。
特にスカラーの積和命令が極端に苦手です。

おそらく fmacs/vmla/vfma の sレジスタアクセスは d 全体を
置き換えるベクタ命令に置き換えた方が高速化できると思われます。


(5) の Krait は Scorpion と比べて明らかにパイプラインの実行効率が
高くなっています。
A/B の差は小さくなっており A6(swift) 同様 out-of-order か latency が
少ないと考えられます。特に a:~d: で差が顕著です。
それでも A6 には敵いません。

特徴的なのは vmla が遅いことで、VFP v4 の vfma と大きな開きがあります。
HW Native なのは vfma の方で、vmla は演算結果の互換性のために mul,add
2命令に展開している可能性があります。
よって、Krait ではできる限り vfma を使った方が良いことになります。


● Krait

Scorpion は浮動小数点演算能力が非常に高いのですが、
ピーク性能を引き出すのはあまり容易ではありませんでした。
その点 Krait は改良されているようです。

ただし整数演算やアプリケーション全体の実行速度ではまだ
大きな差を検出できていません。
Android NDK r8c/r8d では clang (LLVM) が追加されているので、
コンパイラを変えるとまた違った結果になる可能性があります。

また Scorpion にはなかった Quad core なので、システム全体の
速度では最も演算能力が高いことは間違いありません。
同じ Quad core でも Tegra3 などの Cortex-A9 とは世代が異なっており
特に浮動小数点演算能力は 2倍になります。

前回書いたように APQ8064 の Adreno 320 GPU の方もかなり速いのですが、
ベンチマークに使用しているアプリが安定して動作せず計測できていません。
まだ安定しない原因がわかっておりません。


関連エントリ
Adreno 320 Snapdragon S4 Pro APQ8064
iPhone 5 / A6 の CPU 速度 その 3
iPhone 5 / A6 の浮動小数点演算
Snapdragon の本当の浮動小数点演算能力
ARM Cortex-A8 の NEON と浮動小数演算最適化


HTC J butterfly HTL21 (AQP8064) を手に入れたので、
今更ながらやっと Krait と Adreno 320 を試せるようになりました。

Krait は Qualcomm が開発した ARMv7A CPU の CPU core で
Scorpion の後継にあたります。
ARM でいえば Cortex-A15 の世代に相当し、Apple A6/A6X の CPU core も同様に
この世代に属します。

これら ARM, Qualcomm, Apple 3つの CPU は同じ ARMv7-A の命令
アーキテクチャでアプリケーションに互換性がありますが、
Intel と AMD のように設計や性能は異なります。

ARM            Qualcomm    Apple
---------------------------------------------------------
Cortex-A8/A9   Scorpion                    vfpv3   ~2012
Cortex-A15     Krait       Apple A6        vfpv4   2012~

この中で Krait が最も先に登場しており、日本でも 5月の HTC J 以降
Snapdragon S4 MSM8960/8260A/8660A 搭載機種が増えています。
むしろ LTE では Krait でないスマートフォンの方が少ないかもしれません。


Snapdragon S4 Pro APQ8064 のもう一つ新しい点は、GPU も世代交代していることです。
GPU Adreno 320 はハードウエア的には OpenGL ES 3.0 に対応しており、
こちらも他社に先駆けて手に入る状態となりました。

実際に調べてみると、OpenGL ES 3.0 の新しい圧縮テクスチャ形式である
ETC2 / EAC に対応していることがわかります。

TextureFormat 26
tc[00]=87f9  GL_3DC_X_AMD
tc[01]=87fa  GL_3DC_XY_AMD
tc[02]=8c92  GL_ATC_RGB_AMD
tc[03]=8c93  GL_ATC_RGBA_EXPLICIT_ALPHA_AMD
tc[04]=87ee  GL_ATC_RGBA_INTERPOLATED_ALPHA_AMD
tc[05]=8d64  GL_ETC1_RGB8_OES
tc[06]=8b90  GL_PALETTE4_RGB8_OES
tc[07]=8b91  GL_PALETTE4_RGBA8_OES
tc[08]=8b92  GL_PALETTE4_R5_G6_B5_OES
tc[09]=8b93  GL_PALETTE4_RGBA4_OES
tc[10]=8b94  GL_PALETTE4_RGB5_A1_OES
tc[11]=8b95  GL_PALETTE8_RGB8_OES
tc[12]=8b96  GL_PALETTE8_RGBA8_OES
tc[13]=8b97  GL_PALETTE8_R5_G6_B5_OES
tc[14]=8b98  GL_PALETTE8_RGBA4_OES
tc[15]=8b99  GL_PALETTE8_RGB5_A1_OES
tc[16]=9270  GL_COMPRESSED_R11_EAC
tc[17]=9271  GL_COMPRESSED_SIGNED_R11_EAC
tc[18]=9272  GL_COMPRESSED_RG11_EAC
tc[19]=9273  GL_COMPRESSED_SIGNED_RG11_EAC
tc[20]=9274  GL_COMPRESSED_RGB8_ETC2
tc[21]=9275  GL_COMPRESSED_SRGB8_ETC2
tc[22]=9276  GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2
tc[23]=9277  GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2
tc[24]=9278  GL_COMPRESSED_RGBA8_ETC2_EAC
tc[25]=9279  GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC

OpenGL ES 2.0 のままでも列挙されるので、すでに利用できる状態となっています。
ETC1 も同列に列挙されるのは OpenGL ES 2.0 API だからだと思われます。

また GPU Extension に GL_EXT_sRGB が増えています。
sRGB 対応もやはり OpenGL ES 3.0 の新機能です。

Extension の詳細は下記のページに追加しました。

OpenGL ES Extension (Mobile GPU)

GPU 比較はこちら

Mobile GPU の比較


いつものベンチマークはデータをとっている最中です。
使った感じは GPU が非常に速いです。
一番重いケースでも 30fps を余裕で超えており、とても Full HD (1920x1080)
でレンダリングしているとは思えません。

傾向としては Adreno 220 と同じでシェーダーの演算負荷に強く、同じ
Unified Shader ながら PowerVR のように面積が増えても極端に処理落ちしません。
演算精度によるペナルティが無いため、プログラマにとっては非常に扱いやすく
かつ演算精度が高いのでレンダリング結果も綺麗です。

ピクセルシェーダーの命令が少なく演算負荷が軽いケースでは PowerVR が
飛び抜けていますが、ピクセル(フラグメント)の演算量が多い場合はおそらく
A5X の PowerVR SGX543MP4 より速く、A6X の SGX554MP4 に近い数値が出るようです。
もちろん条件によるので万能ではありません。

CPU のデータも計測中ですが CPU core 単体では A6/A6X よりも遅いようです。
やはり A6/A6X は速いです。
その代わり core 数が A6/A6X の 2倍 (Quad core) なので、
総合的な能力では十分だと思います。


関連エントリ
iPad 4 Apple A6X GPU PowerVR SGX554 MP4 の速度
iPhone 5 / A6 の CPU 速度 その 3
iPhone 5 / A6 の CPU 速度 その 2
OpenGL / OpenGL ES ETC1 の互換性と KTX の落とし穴
OpenGL 4.3/ES 3.0 ETC2 Texture 圧縮の仕組み (PVRTC2,ASTC)
OpenGL 4.3 と GL_ARB_ES3_compatibility