123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574 |
- """
- PyAssimp
- This is the main-module of PyAssimp.
- """
- import sys
- if sys.version_info < (2,6):
- raise RuntimeError('pyassimp: need python 2.6 or newer')
- # xrange was renamed range in Python 3 and the original range from Python 2 was removed.
- # To keep compatibility with both Python 2 and 3, xrange is set to range for version 3.0 and up.
- if sys.version_info >= (3,0):
- xrange = range
- try:
- import numpy
- except ImportError:
- numpy = None
- import logging
- import ctypes
- from contextlib import contextmanager
- logger = logging.getLogger("pyassimp")
- # attach default null handler to logger so it doesn't complain
- # even if you don't attach another handler to logger
- logger.addHandler(logging.NullHandler())
- from . import structs
- from . import helper
- from . import postprocess
- from .errors import AssimpError
- class AssimpLib(object):
- """
- Assimp-Singleton
- """
- load, load_mem, export, export_blob, release, dll = helper.search_library()
- _assimp_lib = AssimpLib()
- def make_tuple(ai_obj, type = None):
- res = None
- #notes:
- # ai_obj._fields_ = [ ("attr", c_type), ... ]
- # getattr(ai_obj, e[0]).__class__ == float
- if isinstance(ai_obj, structs.Matrix4x4):
- if numpy:
- res = numpy.array([getattr(ai_obj, e[0]) for e in ai_obj._fields_]).reshape((4,4))
- #import pdb;pdb.set_trace()
- else:
- res = [getattr(ai_obj, e[0]) for e in ai_obj._fields_]
- res = [res[i:i+4] for i in xrange(0,16,4)]
- elif isinstance(ai_obj, structs.Matrix3x3):
- if numpy:
- res = numpy.array([getattr(ai_obj, e[0]) for e in ai_obj._fields_]).reshape((3,3))
- else:
- res = [getattr(ai_obj, e[0]) for e in ai_obj._fields_]
- res = [res[i:i+3] for i in xrange(0,9,3)]
- else:
- if numpy:
- res = numpy.array([getattr(ai_obj, e[0]) for e in ai_obj._fields_])
- else:
- res = [getattr(ai_obj, e[0]) for e in ai_obj._fields_]
- return res
- # Returns unicode object for Python 2, and str object for Python 3.
- def _convert_assimp_string(assimp_string):
- if sys.version_info >= (3, 0):
- return str(assimp_string.data, errors='ignore')
- else:
- return unicode(assimp_string.data, errors='ignore')
- # It is faster and more correct to have an init function for each assimp class
- def _init_face(aiFace):
- aiFace.indices = [aiFace.mIndices[i] for i in range(aiFace.mNumIndices)]
- assimp_struct_inits = { structs.Face : _init_face }
- def call_init(obj, caller = None):
- if helper.hasattr_silent(obj,'contents'): #pointer
- _init(obj.contents, obj, caller)
- else:
- _init(obj,parent=caller)
- def _is_init_type(obj):
- if obj and helper.hasattr_silent(obj,'contents'): #pointer
- return _is_init_type(obj[0])
- # null-pointer case that arises when we reach a mesh attribute
- # like mBitangents which use mNumVertices rather than mNumBitangents
- # so it breaks the 'is iterable' check.
- # Basically:
- # FIXME!
- elif not bool(obj):
- return False
- tname = obj.__class__.__name__
- return not (tname[:2] == 'c_' or tname == 'Structure' \
- or tname == 'POINTER') and not isinstance(obj, (int, str, bytes))
- def _init(self, target = None, parent = None):
- """
- Custom initialize() for C structs, adds safely accessible member functionality.
- :param target: set the object which receive the added methods. Useful when manipulating
- pointers, to skip the intermediate 'contents' deferencing.
- """
- if not target:
- target = self
- dirself = dir(self)
- for m in dirself:
- if m.startswith("_"):
- continue
- # We should not be accessing `mPrivate` according to structs.Scene.
- if m == 'mPrivate':
- continue
- if m.startswith('mNum'):
- if 'm' + m[4:] in dirself:
- continue # will be processed later on
- else:
- name = m[1:].lower()
- obj = getattr(self, m)
- setattr(target, name, obj)
- continue
- if m == 'mName':
- target.name = str(_convert_assimp_string(self.mName))
- target.__class__.__repr__ = lambda x: str(x.__class__) + "(" + getattr(x, 'name','') + ")"
- target.__class__.__str__ = lambda x: getattr(x, 'name', '')
- continue
- name = m[1:].lower()
- obj = getattr(self, m)
- # Create tuples
- if isinstance(obj, structs.assimp_structs_as_tuple):
- setattr(target, name, make_tuple(obj))
- logger.debug(str(self) + ": Added array " + str(getattr(target, name)) + " as self." + name.lower())
- continue
- if m.startswith('m') and len(m) > 1 and m[1].upper() == m[1]:
- if name == "parent":
- setattr(target, name, parent)
- logger.debug("Added a parent as self." + name)
- continue
- if helper.hasattr_silent(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
- if not length: # empty!
- setattr(target, name, [])
- logger.debug(str(self) + ": " + name + " is an empty list.")
- continue
- try:
- if obj._type_ in structs.assimp_structs_as_tuple:
- if numpy:
- setattr(target, name, numpy.array([make_tuple(obj[i]) for i in range(length)], dtype=numpy.float32))
- logger.debug(str(self) + ": Added an array of numpy arrays (type "+ str(type(obj)) + ") as self." + name)
- else:
- setattr(target, name, [make_tuple(obj[i]) for i in range(length)])
- logger.debug(str(self) + ": Added a list of lists (type "+ str(type(obj)) + ") as self." + name)
- else:
- setattr(target, name, [obj[i] for i in range(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
- try:
- init = assimp_struct_inits[type(obj[0])]
- except KeyError:
- if _is_init_type(obj[0]):
- for e in getattr(target, name):
- call_init(e, target)
- else:
- for e in getattr(target, name):
- init(e)
- except IndexError:
- logger.error("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. Quitting now.")
- sys.exit(1)
- except ValueError as e:
- logger.error("In " + str(self) + "->" + name + ": " + str(e) + ". Quitting now.")
- if "setting an array element with a sequence" in str(e):
- logger.error("Note that pyassimp does not currently "
- "support meshes with mixed triangles "
- "and quads. Try to load your mesh with"
- " a post-processing to triangulate your"
- " faces.")
- raise e
- else: # starts with 'm' but not iterable
- setattr(target, name, obj)
- logger.debug("Added " + name + " as self." + name + " (type: " + str(type(obj)) + ")")
- if _is_init_type(obj):
- call_init(obj, target)
- if isinstance(self, structs.Mesh):
- _finalize_mesh(self, target)
- if isinstance(self, structs.Texture):
- _finalize_texture(self, target)
- if isinstance(self, structs.Metadata):
- _finalize_metadata(self, target)
- return self
- 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
- - ADDTRANSFORMATION: add a reference to an object's transformation taken from their associated node.
- :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
- if type == "ADDTRANSFORMATION":
- def getnode(node, name):
- if node.name == name: return node
- for child in node.children:
- n = getnode(child, name)
- if n: return n
- node = getnode(scene.rootnode, obj.name)
- if not node:
- raise AssimpError("Object " + str(obj) + " has no associated node!")
- setattr(obj, "transformation", node.transformation)
- 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 mesh in node.meshes:
- mesh.material = scene.materials[mesh.materialindex]
- for cam in scene.cameras:
- pythonize_assimp("ADDTRANSFORMATION", cam, scene)
- for c in node.children:
- recur_pythonize(c, scene)
- def release(scene):
- '''
- Release resources of a loaded scene.
- '''
- _assimp_lib.release(ctypes.pointer(scene))
- @contextmanager
- def load(filename,
- file_type = None,
- processing = postprocess.aiProcess_Triangulate):
- '''
- Load a model into a scene. On failure throws AssimpError.
- Arguments
- ---------
- filename: Either a filename or a file object to load model from.
- If a file object is passed, file_type MUST be specified
- Otherwise Assimp has no idea which importer to use.
- This is named 'filename' so as to not break legacy code.
- processing: assimp postprocessing parameters. Verbose keywords are imported
- from postprocessing, and the parameters can be combined bitwise to
- generate the final processing value. Note that the default value will
- triangulate quad faces. Example of generating other possible values:
- processing = (pyassimp.postprocess.aiProcess_Triangulate |
- pyassimp.postprocess.aiProcess_OptimizeMeshes)
- file_type: string of file extension, such as 'stl'
- Returns
- ---------
- Scene object with model data
- '''
- from ctypes import c_char_p
- if hasattr(filename, 'read'):
- # This is the case where a file object has been passed to load.
- # It is calling the following function:
- # const aiScene* aiImportFileFromMemory(const char* pBuffer,
- # unsigned int pLength,
- # unsigned int pFlags,
- # const char* pHint)
- if file_type is None:
- raise AssimpError('File type must be specified when passing file objects!')
- data = filename.read()
- model = _assimp_lib.load_mem(data,
- len(data),
- processing,
- c_char_p(file_type.encode(sys.getfilesystemencoding())))
- else:
- # a filename string has been passed
- model = _assimp_lib.load(filename.encode(sys.getfilesystemencoding()), processing)
- if not model:
- raise AssimpError('Could not import file!')
- scene = _init(model.contents)
- recur_pythonize(scene.rootnode, scene)
- try:
- yield scene
- finally:
- release(scene)
- def export(scene,
- filename,
- file_type = None,
- processing = postprocess.aiProcess_Triangulate):
- '''
- Export a scene. On failure throws AssimpError.
- Arguments
- ---------
- scene: scene to export.
- filename: Filename that the scene should be exported to.
- file_type: string of file exporter to use. For example "collada".
- processing: assimp postprocessing parameters. Verbose keywords are imported
- from postprocessing, and the parameters can be combined bitwise to
- generate the final processing value. Note that the default value will
- triangulate quad faces. Example of generating other possible values:
- processing = (pyassimp.postprocess.aiProcess_Triangulate |
- pyassimp.postprocess.aiProcess_OptimizeMeshes)
- '''
- exportStatus = _assimp_lib.export(ctypes.pointer(scene), file_type.encode("ascii"), filename.encode(sys.getfilesystemencoding()), processing)
- if exportStatus != 0:
- raise AssimpError('Could not export scene!')
- def export_blob(scene,
- file_type = None,
- processing = postprocess.aiProcess_Triangulate):
- '''
- Export a scene and return a blob in the correct format. On failure throws AssimpError.
- Arguments
- ---------
- scene: scene to export.
- file_type: string of file exporter to use. For example "collada".
- processing: assimp postprocessing parameters. Verbose keywords are imported
- from postprocessing, and the parameters can be combined bitwise to
- generate the final processing value. Note that the default value will
- triangulate quad faces. Example of generating other possible values:
- processing = (pyassimp.postprocess.aiProcess_Triangulate |
- pyassimp.postprocess.aiProcess_OptimizeMeshes)
- Returns
- ---------
- Pointer to structs.ExportDataBlob
- '''
- exportBlobPtr = _assimp_lib.export_blob(ctypes.pointer(scene), file_type.encode("ascii"), processing)
- if exportBlobPtr == 0:
- raise AssimpError('Could not export scene to blob!')
- return exportBlobPtr
- def available_formats():
- """
- Return a list of file format extensions supported to import.
- Returns
- ---------
- A list of upper-case file extensions, e.g. [3DS, OBJ]
- """
- from ctypes import byref
- extension_list = structs.String()
- _assimp_lib.dll.aiGetExtensionList(byref(extension_list))
- return [e[2:].upper() for e in str(extension_list.data, sys.getfilesystemencoding()).split(";")]
- def _finalize_texture(tex, target):
- setattr(target, "achformathint", tex.achFormatHint)
- if numpy:
- data = numpy.array([make_tuple(getattr(tex, "pcData")[i]) for i in range(tex.mWidth * tex.mHeight)])
- else:
- data = [make_tuple(getattr(tex, "pcData")[i]) for i in range(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 numpy:
- if mAttr:
- data = numpy.array([make_tuple(getattr(mesh, name)[i]) for i in range(nb_vertices)], dtype=numpy.float32)
- setattr(target, name[1:].lower(), data)
- else:
- setattr(target, name[1:].lower(), numpy.array([], dtype="float32"))
- else:
- if mAttr:
- data = [make_tuple(getattr(mesh, name)[i]) for i in range(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 range(nb_vertices)])
- if numpy:
- setattr(target, name[1:].lower(), numpy.array(data, dtype=numpy.float32))
- else:
- setattr(target, name[1:].lower(), data)
- fill("mNormals")
- fill("mTangents")
- fill("mBitangents")
- fillarray("mColors")
- fillarray("mTextureCoords")
- # prepare faces
- if numpy:
- faces = numpy.array([f.indices for f in target.faces], dtype=numpy.int32)
- else:
- faces = [f.indices for f in target.faces]
- setattr(target, 'faces', faces)
- def _init_metadata_entry(entry):
- entry.type = entry.mType
- if entry.type == structs.MetadataEntry.AI_BOOL:
- entry.data = ctypes.cast(entry.mData, ctypes.POINTER(ctypes.c_bool)).contents.value
- elif entry.type == structs.MetadataEntry.AI_INT32:
- entry.data = ctypes.cast(entry.mData, ctypes.POINTER(ctypes.c_int32)).contents.value
- elif entry.type == structs.MetadataEntry.AI_UINT64:
- entry.data = ctypes.cast(entry.mData, ctypes.POINTER(ctypes.c_uint64)).contents.value
- elif entry.type == structs.MetadataEntry.AI_FLOAT:
- entry.data = ctypes.cast(entry.mData, ctypes.POINTER(ctypes.c_float)).contents.value
- elif entry.type == structs.MetadataEntry.AI_DOUBLE:
- entry.data = ctypes.cast(entry.mData, ctypes.POINTER(ctypes.c_double)).contents.value
- elif entry.type == structs.MetadataEntry.AI_AISTRING:
- assimp_string = ctypes.cast(entry.mData, ctypes.POINTER(structs.String)).contents
- entry.data = _convert_assimp_string(assimp_string)
- elif entry.type == structs.MetadataEntry.AI_AIVECTOR3D:
- assimp_vector = ctypes.cast(entry.mData, ctypes.POINTER(structs.Vector3D)).contents
- entry.data = make_tuple(assimp_vector)
- return entry
- def _finalize_metadata(metadata, target):
- """ Building the metadata object is a bit specific.
- Firstly, there are two separate arrays: one with metadata keys and one
- with metadata values, and there are no corresponding mNum* attributes,
- so the C arrays are not converted to Python arrays using the generic
- code in the _init function.
- Secondly, a metadata entry value has to be cast according to declared
- metadata entry type.
- """
- length = metadata.mNumProperties
- setattr(target, 'keys', [str(_convert_assimp_string(metadata.mKeys[i])) for i in range(length)])
- setattr(target, 'values', [_init_metadata_entry(metadata.mValues[i]) for i in range(length)])
- class PropertyGetter(dict):
- def __getitem__(self, key):
- semantic = 0
- if isinstance(key, tuple):
- key, semantic = key
- return dict.__getitem__(self, (key, semantic))
- def keys(self):
- for k in dict.keys(self):
- yield k[0]
- def __iter__(self):
- return self.keys()
- def items(self):
- for k, v in dict.items(self):
- yield k[0], v
- 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 range(length)]:
- #the name
- p = p.contents
- key = str(_convert_assimp_string(p.mKey))
- key = (key.split('.')[1], p.mSemantic)
- #the data
- if p.mType == 1:
- arr = ctypes.cast(p.mData,
- ctypes.POINTER(ctypes.c_float * int(p.mDataLength/ctypes.sizeof(ctypes.c_float)))
- ).contents
- value = [x for x in arr]
- elif p.mType == 3: #string can't be an array
- value = _convert_assimp_string(ctypes.cast(p.mData, ctypes.POINTER(structs.MaterialPropertyString)).contents)
- elif p.mType == 4:
- arr = ctypes.cast(p.mData,
- ctypes.POINTER(ctypes.c_int * int(p.mDataLength/ctypes.sizeof(ctypes.c_int)))
- ).contents
- value = [x for x in arr]
- else:
- value = p.mData[:p.mDataLength]
- if len(value) == 1:
- [value] = value
- result[key] = value
- return PropertyGetter(result)
- 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()
- _assimp_lib.dll.aiDecomposeMatrix(ctypes.pointer(matrix),
- ctypes.byref(scaling),
- ctypes.byref(rotation),
- ctypes.byref(position))
- return scaling._init(), rotation._init(), position._init()
-
|