core.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. #-*- coding: UTF-8 -*-
  2. """
  3. PyAssimp
  4. This is the main-module of PyAssimp.
  5. """
  6. import sys
  7. if sys.version_info < (2,6):
  8. raise 'pyassimp: need python 2.6 or newer'
  9. import ctypes
  10. import os
  11. import numpy
  12. import logging; logger = logging.getLogger("pyassimp")
  13. # Attach a default, null handler, to the logger.
  14. # applications can easily get log messages from pyassimp
  15. # by calling for instance
  16. # >>> logging.basicConfig(level=logging.DEBUG)
  17. # before importing pyassimp
  18. class NullHandler(logging.Handler):
  19. def emit(self, record):
  20. pass
  21. h = NullHandler()
  22. logger.addHandler(h)
  23. from . import structs
  24. from .errors import AssimpError
  25. from . import helper
  26. assimp_structs_as_tuple = (
  27. structs.Matrix4x4,
  28. structs.Matrix3x3,
  29. structs.Vector2D,
  30. structs.Vector3D,
  31. structs.Color3D,
  32. structs.Color4D,
  33. structs.Quaternion,
  34. structs.Plane,
  35. structs.Texel)
  36. def make_tuple(ai_obj, type = None):
  37. res = None
  38. if isinstance(ai_obj, structs.Matrix4x4):
  39. res = numpy.array([getattr(ai_obj, e[0]) for e in ai_obj._fields_]).reshape((4,4))
  40. #import pdb;pdb.set_trace()
  41. elif isinstance(ai_obj, structs.Matrix3x3):
  42. res = numpy.array([getattr(ai_obj, e[0]) for e in ai_obj._fields_]).reshape((3,3))
  43. else:
  44. res = numpy.array([getattr(ai_obj, e[0]) for e in ai_obj._fields_])
  45. return res
  46. def call_init(obj, caller = None):
  47. # init children
  48. if helper.hasattr_silent(obj, '_init'):
  49. obj._init(parent = caller)
  50. # pointers
  51. elif helper.hasattr_silent(obj, 'contents'):
  52. if helper.hasattr_silent(obj.contents, '_init'):
  53. obj.contents._init(target = obj, parent = caller)
  54. def _init(self, target = None, parent = None):
  55. """
  56. Custom initialize() for C structs, adds safely accessable member functionality.
  57. :param target: set the object which receive the added methods. Useful when manipulating
  58. pointers, to skip the intermediate 'contents' deferencing.
  59. """
  60. if helper.hasattr_silent(self, '_is_init'):
  61. return self
  62. self._is_init = True
  63. if not target:
  64. target = self
  65. for m in dir(self):
  66. name = m[1:].lower()
  67. if m.startswith("_"):
  68. continue
  69. obj = getattr(self, m)
  70. if m.startswith('mNum'):
  71. if 'm' + m[4:] in dir(self):
  72. continue # will be processed later on
  73. else:
  74. setattr(target, name, obj)
  75. # Create tuples
  76. if isinstance(obj, assimp_structs_as_tuple):
  77. setattr(target, name, make_tuple(obj))
  78. logger.debug(str(self) + ": Added array " + str(getattr(target, name)) + " as self." + name.lower())
  79. continue
  80. if isinstance(obj, structs.String):
  81. setattr(target, 'name', obj.data.decode("utf-8"))
  82. setattr(target.__class__, '__repr__', lambda x: str(x.__class__) + "(" + x.name + ")")
  83. setattr(target.__class__, '__str__', lambda x: x.name)
  84. continue
  85. if m.startswith('m'):
  86. if name == "parent":
  87. setattr(target, name, parent)
  88. logger.debug("Added a parent as self." + name)
  89. continue
  90. if helper.hasattr_silent(self, 'mNum' + m[1:]):
  91. length = getattr(self, 'mNum' + m[1:])
  92. # -> special case: properties are
  93. # stored as a dict.
  94. if m == 'mProperties':
  95. setattr(target, name, _get_properties(obj, length))
  96. continue
  97. if not length: # empty!
  98. setattr(target, name, [])
  99. logger.debug(str(self) + ": " + name + " is an empty list.")
  100. continue
  101. try:
  102. if obj._type_ in assimp_structs_as_tuple:
  103. setattr(target, name, numpy.array([make_tuple(obj[i]) for i in range(length)], dtype=numpy.float32))
  104. logger.debug(str(self) + ": Added an array of numpy arrays (type "+ str(type(obj)) + ") as self." + name)
  105. else:
  106. setattr(target, name, [obj[i] for i in range(length)]) #TODO: maybe not necessary to recreate an array?
  107. logger.debug(str(self) + ": Added list of " + str(obj) + " " + name + " as self." + name + " (type: " + str(type(obj)) + ")")
  108. # initialize array elements
  109. for e in getattr(target, name):
  110. call_init(e, caller = target)
  111. except IndexError:
  112. 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.")
  113. sys.exit(1)
  114. except ValueError as e:
  115. logger.error("In " + str(self) + "->" + name + ": " + str(e) + ". Quitting now.")
  116. if "setting an array element with a sequence" in str(e):
  117. logger.error("Note that pyassimp does not currently "
  118. "support meshes with mixed triangles "
  119. "and quads. Try to load your mesh with"
  120. " a post-processing to triangulate your"
  121. " faces.")
  122. sys.exit(1)
  123. else: # starts with 'm' but not iterable
  124. setattr(target, name, obj)
  125. logger.debug("Added " + name + " as self." + name + " (type: " + str(type(obj)) + ")")
  126. call_init(obj, caller = target)
  127. if isinstance(self, structs.Mesh):
  128. _finalize_mesh(self, target)
  129. if isinstance(self, structs.Texture):
  130. _finalize_texture(self, target)
  131. return self
  132. """
  133. Python magic to add the _init() function to all C struct classes.
  134. """
  135. for struct in dir(structs):
  136. if not (struct.startswith('_') or struct.startswith('c_') or struct == "Structure" or struct == "POINTER") and not isinstance(getattr(structs, struct),int):
  137. setattr(getattr(structs, struct), '_init', _init)
  138. class AssimpLib(object):
  139. """
  140. Assimp-Singleton
  141. """
  142. load, release, dll = helper.search_library()
  143. #the loader as singleton
  144. _assimp_lib = AssimpLib()
  145. def pythonize_assimp(type, obj, scene):
  146. """ This method modify the Assimp data structures
  147. to make them easier to work with in Python.
  148. Supported operations:
  149. - MESH: replace a list of mesh IDs by reference to these meshes
  150. - ADDTRANSFORMATION: add a reference to an object's transformation taken from their associated node.
  151. :param type: the type of modification to operate (cf above)
  152. :param obj: the input object to modify
  153. :param scene: a reference to the whole scene
  154. """
  155. if type == "MESH":
  156. meshes = []
  157. for i in obj:
  158. meshes.append(scene.meshes[i])
  159. return meshes
  160. if type == "ADDTRANSFORMATION":
  161. def getnode(node, name):
  162. if node.name == name: return node
  163. for child in node.children:
  164. n = getnode(child, name)
  165. if n: return n
  166. node = getnode(scene.rootnode, obj.name)
  167. if not node:
  168. raise AssimpError("Object " + str(obj) + " has no associated node!")
  169. setattr(obj, "transformation", node.transformation)
  170. def recur_pythonize(node, scene):
  171. """ Recursively call pythonize_assimp on
  172. nodes tree to apply several post-processing to
  173. pythonize the assimp datastructures.
  174. """
  175. node.meshes = pythonize_assimp("MESH", node.meshes, scene)
  176. for mesh in node.meshes:
  177. mesh.material = scene.materials[mesh.materialindex]
  178. for cam in scene.cameras:
  179. pythonize_assimp("ADDTRANSFORMATION", cam, scene)
  180. #for light in scene.lights:
  181. # pythonize_assimp("ADDTRANSFORMATION", light, scene)
  182. for c in node.children:
  183. recur_pythonize(c, scene)
  184. def load(filename, processing=0):
  185. """
  186. Loads the model with some specific processing parameters.
  187. filename - file to load model from
  188. processing - processing parameters
  189. result Scene-object with model-data
  190. throws AssimpError - could not open file
  191. """
  192. #read pure data
  193. #from ctypes import c_char_p, c_uint
  194. #model = _assimp_lib.load(c_char_p(filename), c_uint(processing))
  195. model = _assimp_lib.load(filename.encode("ascii"), processing)
  196. if not model:
  197. #Uhhh, something went wrong!
  198. raise AssimpError("could not import file: %s" % filename)
  199. scene = model.contents._init()
  200. recur_pythonize(scene.rootnode, scene)
  201. return scene
  202. def release(scene):
  203. from ctypes import pointer
  204. _assimp_lib.release(pointer(scene))
  205. def _finalize_texture(tex, target):
  206. setattr(target, "achformathint", tex.achFormatHint)
  207. data = numpy.array([make_tuple(getattr(tex, "pcData")[i]) for i in range(tex.mWidth * tex.mHeight)])
  208. setattr(target, "data", data)
  209. def _finalize_mesh(mesh, target):
  210. """ Building of meshes is a bit specific.
  211. We override here the various datasets that can
  212. not be process as regular fields.
  213. For instance, the length of the normals array is
  214. mNumVertices (no mNumNormals is available)
  215. """
  216. nb_vertices = getattr(mesh, "mNumVertices")
  217. def fill(name):
  218. mAttr = getattr(mesh, name)
  219. if mAttr:
  220. data = numpy.array([make_tuple(getattr(mesh, name)[i]) for i in range(nb_vertices)], dtype=numpy.float32)
  221. setattr(target, name[1:].lower(), data)
  222. else:
  223. setattr(target, name[1:].lower(), numpy.array([], dtype="float32"))
  224. def fillarray(name):
  225. mAttr = getattr(mesh, name)
  226. data = []
  227. for index, mSubAttr in enumerate(mAttr):
  228. if mSubAttr:
  229. data.append([make_tuple(getattr(mesh, name)[index][i]) for i in range(nb_vertices)])
  230. setattr(target, name[1:].lower(), numpy.array(data, dtype=numpy.float32))
  231. fill("mNormals")
  232. fill("mTangents")
  233. fill("mBitangents")
  234. fillarray("mColors")
  235. fillarray("mTextureCoords")
  236. # prepare faces
  237. faces = numpy.array([f.indices for f in target.faces], dtype=numpy.int32)
  238. setattr(target, 'faces', faces)
  239. class PropertyGetter(dict):
  240. def __getitem__(self, key):
  241. semantic = 0
  242. if isinstance(key, tuple):
  243. key, semantic = key
  244. return dict.__getitem__(self, (key, semantic))
  245. def keys(self):
  246. for k in dict.keys(self):
  247. yield k[0]
  248. def __iter__(self):
  249. return self.keys()
  250. def items(self):
  251. for k, v in dict.items(self):
  252. yield k[0], v
  253. def _get_properties(properties, length):
  254. """
  255. Convenience Function to get the material properties as a dict
  256. and values in a python format.
  257. """
  258. result = {}
  259. #read all properties
  260. for p in [properties[i] for i in range(length)]:
  261. #the name
  262. p = p.contents
  263. key = (str(p.mKey.data.decode("utf-8")).split('.')[1], p.mSemantic)
  264. #the data
  265. from ctypes import POINTER, cast, c_int, c_float, sizeof
  266. if p.mType == 1:
  267. arr = cast(p.mData, POINTER(c_float * int(p.mDataLength/sizeof(c_float)) )).contents
  268. value = [x for x in arr]
  269. elif p.mType == 3: #string can't be an array
  270. value = cast(p.mData, POINTER(structs.MaterialPropertyString)).contents.data.decode("utf-8")
  271. elif p.mType == 4:
  272. arr = cast(p.mData, POINTER(c_int * int(p.mDataLength/sizeof(c_int)) )).contents
  273. value = [x for x in arr]
  274. else:
  275. value = p.mData[:p.mDataLength]
  276. if len(value) == 1:
  277. [value] = value
  278. result[key] = value
  279. return PropertyGetter(result)
  280. def decompose_matrix(matrix):
  281. if not isinstance(matrix, structs.Matrix4x4):
  282. raise AssimpError("pyassimp.decompose_matrix failed: Not a Matrix4x4!")
  283. scaling = structs.Vector3D()
  284. rotation = structs.Quaternion()
  285. position = structs.Vector3D()
  286. from ctypes import byref, pointer
  287. _assimp_lib.dll.aiDecomposeMatrix(pointer(matrix), byref(scaling), byref(rotation), byref(position))
  288. return scaling._init(), rotation._init(), position._init()