[pyassimp] Large rewrite: more generic, easier to use

Main changes:
- dynamic creation of idiomatic python fields corresponding to ASSIMP ones,
- hidding of pointers,
- use of numpy for transformation and mesh data storage

For instance, to access the list of meshes of a children of the root
node, previously we did:

scene.mRootNode.contents.mChildren[1].contents.mMeshes

Now, it is:
scene.rootnode.children[1].meshes

Arrays are now regular Python list.

Also added a 'post-processing' to access directly to certain objects,
and not through their index. For instance:

Before:

mymesh_id = scene.mRootNode.contents.mChildren[1].contents.mMeshes[2]
mymesh = scene.mMeshes[mymesh_id]

Now:
scene.rootnode.children[1].meshes[2]

Initialization of the Python wrappers is not delayed anymore: everything
is done during the loading (which leads to long start time, but prevent
unexpected slowing at runtime)

This commit also remove several 'ad-hoc' manipulation that should not
be needed anymore.

While here, use Python logging when necessary.
pull/8/merge^2
Séverin Lemaignan 2012-10-15 16:18:44 +02:00
parent bed0e4e83f
commit 7000ea05c5
2 changed files with 368 additions and 276 deletions

View File

@ -1 +1,369 @@
#-*- coding: UTF-8 -*- #-*- coding: UTF-8 -*-
"""
PyAssimp
This is the main-module of PyAssimp.
"""
import sys
if sys.version_info < (2,5):
raise 'pyassimp: need python 2.5 or newer'
import ctypes
import os
import helper
from errors import AssimpError
import structs
import logging; logger = logging.getLogger("pyassimp")
#logging.basicConfig(level=logging.DEBUG)
logging.basicConfig()
assimp_structs_as_tuple = (
structs.Matrix4x4,
structs.Matrix3x3,
structs.Vector2D,
structs.Vector3D,
structs.Color3D,
structs.Color4D,
structs.Quaternion,
structs.Plane,
structs.Texel)
def make_tuple(ai_obj):
return tuple([getattr(ai_obj, e[0]) for e in ai_obj._fields_])
def call_init(obj, caller = None):
# init children
if hasattr(obj, '_init'):
logger.debug("Init of children: ")
obj._init()
logger.debug("Back to " + str(caller))
# pointers
elif hasattr(obj, 'contents'):
if hasattr(obj.contents, '_init'):
logger.debug("Init of children (pointer): ")
obj.contents._init(target = obj)
logger.debug("Back to " + str(caller))
def _init(self, target = None):
"""
Custom initialize() for C structs, adds safely accessable member functionality.
:param target: set the object which receive the added methods. Useful when manipulating
pointers, to skip the intermediate 'contents' deferencing.
"""
if hasattr(self, '_is_init'):
logger.debug("" + str(self.__class__) + " already initialized.")
return self
self._is_init = True
if not target:
target = self
#for m in self.__class__.__dict__.keys():
for m in dir(self):
name = m[1:].lower()
#if 'normals' in name : import pdb;pdb.set_trace()
if m.startswith("_"):
continue
obj = getattr(self, m)
if m.startswith('mNum'):
if 'm' + m[4:] in dir(self):
continue # will be processed later on
else:
setattr(target, name, obj)
# Create tuples
if isinstance(obj, assimp_structs_as_tuple):
setattr(target, name, make_tuple(obj))
logger.debug(str(self) + ": Added tuple " + str(getattr(target, name)) + " as self." + name.lower())
continue
if isinstance(obj, structs.String):
setattr(target, 'name', str(obj.data))
setattr(target.__class__, '__repr__', lambda x: str(x.__class__) + "(" + x.name + ")")
setattr(target.__class__, '__str__', lambda x: x.name)
continue
if m.startswith('m'):
if hasattr(self, 'mNum' + m[1:]):
length = getattr(self, 'mNum' + m[1:])
# -> special case: properties are
# stored as a dict.
if m == 'mProperties':
setattr(target, name, _get_properties(obj, length))
continue
#import pdb;pdb.set_trace()
if not obj: # empty!
setattr(target, name, [])
logger.debug(str(self) + ": " + name + " is an empty list.")
continue
try:
if obj._type_ in assimp_structs_as_tuple:
setattr(target, name, [make_tuple(obj[i]) for i in xrange(length)])
logger.debug(str(self) + ": Added a list data (type "+ str(type(obj)) + ") as self." + name)
else:
setattr(target, name, [obj[i] for i in xrange(length)]) #TODO: maybe not necessary to recreate an array?
logger.debug(str(self) + ": Added list of " + str(obj) + " " + name + " as self." + name + " (type: " + str(type(obj)) + ")")
# initialize array elements
for e in getattr(target, name):
call_init(e, caller = self)
except IndexError:
logger.warning("in " + str(self) +" : mismatch between mNum" + name + " and the actual amount of data in m" + name + ". This may be due to version mismatch between libassimp and pyassimp. Skipping this field.")
except ValueError as e:
logger.warning(str(e))
logger.warning("in " + str(self) +" : table of " + name + " not initialized (NULL pointer). Skipping this field.")
else: # starts with 'm' but not iterable
setattr(target, name, obj)
logger.debug("Added " + name + " as self." + name + " (type: " + str(type(obj)) + ")")
if name not in ["parent"]: # do not re-initialize my parent
call_init(obj, caller = self)
if isinstance(self, structs.Mesh):
_finalize_mesh(self, target)
if isinstance(self, structs.Texture):
_finalize_texture(self, target)
return self
"""
Python magic to add the _init() function to all C struct classes.
"""
for struct in dir(structs):
if not (struct.startswith('_') or struct.startswith('c_') or struct == "Structure" or struct == "POINTER") and not isinstance(getattr(structs, struct),int):
logger.debug("Adding _init to class " + struct)
setattr(getattr(structs, struct), '_init', _init)
class AssimpLib(object):
"""
Assimp-Singleton
"""
load, release, dll = helper.search_library()
#the loader as singleton
_assimp_lib = AssimpLib()
def pythonize_assimp(type, obj, scene):
""" This method modify the Assimp data structures
to make them easier to work with in Python.
Supported operations:
- MESH: replace a list of mesh IDs by reference to these meshes
:param type: the type of modification to operate (cf above)
:param obj: the input object to modify
:param scene: a reference to the whole scene
"""
if type == "MESH":
meshes = []
for i in obj:
meshes.append(scene.meshes[i])
return meshes
def recur_pythonize(node, scene):
""" Recursively call pythonize_assimp on
nodes tree to apply several post-processing to
pythonize the assimp datastructures.
"""
node.meshes = pythonize_assimp("MESH", node.meshes, scene)
for c in node.children:
recur_pythonize(c, scene)
def load(filename, processing=0):
"""
Loads the model with some specific processing parameters.
filename - file to load model from
processing - processing parameters
result Scene-object with model-data
throws AssimpError - could not open file
"""
#read pure data
model = _assimp_lib.load(filename, processing)
if not model:
#Uhhh, something went wrong!
raise AssimpError, ("could not import file: %s" % filename)
#logger.debug("Initializing recursively tuples")
#_init_tuples(model.contents)
logger.debug("Initializing recursively objects")
scene = model.contents._init()
recur_pythonize(scene.rootnode, scene)
return scene
def release(scene):
from ctypes import pointer
_assimp_lib.release(pointer(scene))
def _finalize_texture(tex, target):
setattr(target, "achformathint", tex.achFormatHint)
data = [make_tuple(getattr(tex, pcData)[i]) for i in xrange(tex.mWidth * tex.mHeight)]
setattr(target, "data", data)
def _finalize_mesh(mesh, target):
""" Building of meshes is a bit specific.
We override here the various datasets that can
not be process as regular fields.
For instance, the length of the normals array is
mNumVertices (no mNumNormals is available)
"""
nb_vertices = getattr(mesh, "mNumVertices")
def fill(name):
mAttr = getattr(mesh, name)
if mAttr:
data = [make_tuple(getattr(mesh, name)[i]) for i in xrange(nb_vertices)]
setattr(target, name[1:].lower(), data)
else:
setattr(target, name[1:].lower(), [])
def fillarray(name):
mAttr = getattr(mesh, name)
data = []
for index, mSubAttr in enumerate(mAttr):
if mSubAttr:
data.append([make_tuple(getattr(mesh, name)[index][i]) for i in xrange(nb_vertices)])
setattr(target, name[1:].lower(), data)
fill("mNormals")
fill("mTangents")
fill("mBitangents")
fillarray("mColors")
fillarray("mTextureCoords")
def _get_properties(properties, length):
"""
Convenience Function to get the material properties as a dict
and values in a python format.
"""
result = {}
#read all properties
for p in [properties[i] for i in xrange(length)]:
#the name
p = p.contents
key = str(p.mKey.data)
#the data
from ctypes import POINTER, cast, c_int, c_float, sizeof
if p.mType == 1:
arr = cast(p.mData, POINTER(c_float*(p.mDataLength/sizeof(c_float)) )).contents
value = [x for x in arr]
elif p.mType == 3: #string can't be an array
value = cast(p.mData, POINTER(structs.String)).contents.data
elif p.mType == 4:
arr = cast(p.mData, POINTER(c_int*(p.mDataLength/sizeof(c_int)) )).contents
value = [x for x in arr]
else:
value = p.mData[:p.mDataLength]
result[key] = value
return result
def aiGetMaterialFloatArray(material, key):
AI_SUCCESS = 0
from ctypes import byref, pointer, cast, c_float, POINTER, sizeof, c_uint
out = structs.Color4D()
max = c_uint(sizeof(structs.Color4D))
r=_assimp_lib.dll.aiGetMaterialFloatArray(pointer(material),
key[0],
key[1],
key[2],
byref(out),
byref(max))
if (r != AI_SUCCESS):
raise AssimpError("aiGetMaterialFloatArray failed!")
out._init()
return [out[i] for i in xrange(max.value)]
def aiGetMaterialString(material, key):
AI_SUCCESS = 0
from ctypes import byref, pointer, cast, c_float, POINTER, sizeof, c_uint
out = structs.String()
r=_assimp_lib.dll.aiGetMaterialString(pointer(material),
key[0],
key[1],
key[2],
byref(out))
if (r != AI_SUCCESS):
raise AssimpError("aiGetMaterialString failed!")
return str(out.data)
def decompose_matrix(matrix):
if not isinstance(matrix, structs.Matrix4x4):
raise AssimpError("pyassimp.decompose_matrix failed: Not a Matrix4x4!")
scaling = structs.Vector3D()
rotation = structs.Quaternion()
position = structs.Vector3D()
from ctypes import byref, pointer
_assimp_lib.dll.aiDecomposeMatrix(pointer(matrix), byref(scaling), byref(rotation), byref(position))
return scaling._init(), rotation._init(), position._init()

View File

@ -1,276 +0,0 @@
#-*- coding: UTF-8 -*-
"""
PyAssimp
This is the main-module of PyAssimp.
"""
import sys
if sys.version_info < (2,5):
raise 'pyassimp: need python 2.5 or newer'
import structs
import ctypes
import os
import helper
from errors import AssimpError
class aiArray:
"""
A python class to 'safely' access C arrays.
For m<Name> and mNum<Name> assimp class members.
"""
def __init__(self, instance, dataName, sizeName, i=None):
self.instance = instance
self.dataName = dataName
self.sizeName = sizeName
self.i = i
self.count = 0
def _GetSize(self):
return getattr(self.instance, self.sizeName)
def _GetData(self, index):
if self.i != None:
if not bool(getattr(self.instance, self.dataName)[self.i]):
return None
item = getattr(self.instance, self.dataName)[self.i][index]
else:
item = getattr(self.instance, self.dataName)[index]
if hasattr(item, 'contents'):
return item.contents._init()
elif hasattr(item, '_init'):
return item._init()
else:
return item
def next(self):
if self.count >= self._GetSize():
self.count = 0
raise StopIteration
else:
c = self.count
self.count += 1
return self._GetData(c)
def __getitem__(self, index):
if isinstance(index, slice):
indices = index.indices(len(self))
return [self.__getitem__(i) for i in range(*indices)]
if index < 0 or index >= self._GetSize():
raise IndexError("aiArray index out of range")
return self._GetData(index)
def __iter__(self):
return self
def __len__(self):
return int(self._GetSize())
def __str__(self):
return str([x for x in self])
def __repr__(self):
return str([x for x in self])
class aiTuple:
"""
A python class to 'safely' access C structs in a python tuple fashion.
For C structs like vectors, matrices, colors, ...
"""
def __init__(self, instance):
self.instance = instance
self.count = 0
def _GetSize(self):
return len(self.instance._fields_)
def _GetData(self, index):
return getattr(self.instance, self.instance._fields_[index][0])
def next(self):
if self.count >= self._GetSize():
self.count = 0
raise StopIteration
else:
c = self.count
self.count += 1
return self._GetData(c)
def __getitem__(self, index):
if isinstance(index, slice):
indices = index.indices(len(self))
return [self.__getitem__(i) for i in range(*indices)]
if index < 0 or index >= self._GetSize():
raise IndexError("aiTuple index out of range")
return self._GetData(index)
def __iter__(self):
return self
def __len__(self):
return int(self._GetSize())
def __str__(self):
return str([x for x in self])
def __repr__(self):
return str([x for x in self])
def _init(self):
"""
Custom initialize() for C structs, adds safely accessable member functionality.
"""
if hasattr(self, '_is_init'):
return self
self._is_init = True
if str(self.__class__.__name__) == "MaterialProperty":
self.mKey._init()
for m in self.__class__.__dict__.keys():
if m.startswith('mNum'):
name = m.split('mNum')[1]
if 'm'+name in self.__class__.__dict__.keys():
setattr(self.__class__, name.lower(), aiArray(self, 'm'+name , m))
if name.lower() == "vertices":
setattr(self.__class__, "normals", aiArray(self, 'mNormals' , m))
setattr(self.__class__, "tangents", aiArray(self, 'mTangents' , m))
setattr(self.__class__, "bitangets", aiArray(self, 'mBitangents' , m))
setattr(self.__class__, "colors", [aiArray(self, 'mColors' , m, o) for o in xrange(len(self.mColors))])
setattr(self.__class__, "texcoords", [aiArray(self, 'mTextureCoords' , m, o) for o in xrange(len(self.mColors))])
elif m == "x" or m == "a1" or m == "b": # Vector, matrix, quat, color
self._tuple = aiTuple(self)
setattr(self.__class__, '__getitem__', lambda x, y: x._tuple.__getitem__(y))
setattr(self.__class__, '__iter__', lambda x: x._tuple)
setattr(self.__class__, 'next', lambda x: x._tuple.next)
setattr(self.__class__, '__repr__', lambda x: str([c for c in x]))
break
elif m == "data": #String
setattr(self.__class__, '__repr__', lambda x: str(x.data))
setattr(self.__class__, '__str__', lambda x: str(x.data))
break
if hasattr(getattr(self, m), '_init'):
getattr(self, m)._init()
return self
"""
Python magic to add the _init() function to all C struct classes.
"""
for struct in dir(structs):
if not (struct.startswith('_') or struct.startswith('c_') or struct == "Structure" or struct == "POINTER") and not isinstance(getattr(structs, struct),int):
setattr(getattr(structs, struct), '_init', _init)
class AssimpLib(object):
"""
Assimp-Singleton
"""
load, release, dll = helper.search_library()
#the loader as singleton
_assimp_lib = AssimpLib()
def load(filename, processing=0):
"""
Loads the model with some specific processing parameters.
filename - file to load model from
processing - processing parameters
result Scene-object with model-data
throws AssimpError - could not open file
"""
#read pure data
model = _assimp_lib.load(filename, processing)
if not model:
#Uhhh, something went wrong!
raise AssimpError, ("could not import file: %s" % filename)
return model.contents._init()
def release(scene):
from ctypes import pointer
_assimp_lib.release(pointer(scene))
def aiGetMaterialFloatArray(material, key):
AI_SUCCESS = 0
from ctypes import byref, pointer, cast, c_float, POINTER, sizeof, c_uint
out = structs.Color4D()
max = c_uint(sizeof(structs.Color4D))
r=_assimp_lib.dll.aiGetMaterialFloatArray(pointer(material),
key[0],
key[1],
key[2],
byref(out),
byref(max))
if (r != AI_SUCCESS):
raise AssimpError("aiGetMaterialFloatArray failed!")
out._init()
return [out[i] for i in xrange(max.value)]
def aiGetMaterialString(material, key):
AI_SUCCESS = 0
from ctypes import byref, pointer, cast, c_float, POINTER, sizeof, c_uint
out = structs.String()
r=_assimp_lib.dll.aiGetMaterialString(pointer(material),
key[0],
key[1],
key[2],
byref(out))
if (r != AI_SUCCESS):
raise AssimpError("aiGetMaterialString failed!")
return str(out.data)
def GetMaterialProperties(material):
"""
Convenience Function to get the material properties as a dict
and values in a python format.
"""
result = {}
#read all properties
for p in material.properties:
#the name
key = p.mKey.data
#the data
from ctypes import POINTER, cast, c_int, c_float, sizeof
if p.mType == 1:
arr = cast(p.mData, POINTER(c_float*(p.mDataLength/sizeof(c_float)) )).contents
value = [x for x in arr]
elif p.mType == 3: #string can't be an array
value = cast(p.mData, POINTER(structs.String)).contents.data
elif p.mType == 4:
arr = cast(p.mData, POINTER(c_int*(p.mDataLength/sizeof(c_int)) )).contents
value = [x for x in arr]
else:
value = p.mData[:p.mDataLength]
result[key] = value
return result
def aiDecomposeMatrix(matrix):
if not isinstance(matrix, structs.Matrix4x4):
raise AssimpError("aiDecomposeMatrix failed: Not a aiMatrix4x4!")
scaling = structs.Vector3D()
rotation = structs.Quaternion()
position = structs.Vector3D()
from ctypes import byref, pointer
_assimp_lib.dll.aiDecomposeMatrix(pointer(matrix), byref(scaling), byref(rotation), byref(position))
return scaling._init(), rotation._init(), position._init()