575 lines
21 KiB
Python
575 lines
21 KiB
Python
"""
|
|
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()
|
|
|