geometry.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443
  1. import os
  2. from .. import constants, logger
  3. from . import base_classes, io, api
  4. FORMAT_VERSION = 3
  5. class Geometry(base_classes.BaseNode):
  6. """Class that wraps a single mesh/geometry node."""
  7. def __init__(self, node, parent=None):
  8. logger.debug("Geometry().__init__(%s)", node)
  9. #@TODO: maybe better to have `three` constants for
  10. # strings that are specific to `three` properties
  11. geo_type = constants.GEOMETRY.title()
  12. if parent.options.get(constants.GEOMETRY_TYPE):
  13. opt_type = parent.options[constants.GEOMETRY_TYPE]
  14. if opt_type == constants.BUFFER_GEOMETRY:
  15. geo_type = constants.BUFFER_GEOMETRY
  16. elif opt_type != constants.GEOMETRY:
  17. logger.error("Unknown geometry type %s", opt_type)
  18. logger.info("Setting %s to '%s'", node, geo_type)
  19. self._defaults[constants.TYPE] = geo_type
  20. base_classes.BaseNode.__init__(self, node,
  21. parent=parent,
  22. type=geo_type)
  23. @property
  24. def animation_filename(self):
  25. """Calculate the file name for the animation file
  26. :return: base name for the file
  27. """
  28. compression = self.options.get(constants.COMPRESSION)
  29. if compression in (None, constants.NONE):
  30. ext = constants.JSON
  31. elif compression == constants.MSGPACK:
  32. ext = constants.PACK
  33. key = ''
  34. for key in (constants.MORPH_TARGETS, constants.ANIMATION):
  35. try:
  36. self[key]
  37. break
  38. except KeyError:
  39. pass
  40. else:
  41. logger.info("%s has no animation data", self.node)
  42. return
  43. return '%s.%s.%s' % (self.node, key, ext)
  44. @property
  45. def face_count(self):
  46. """Parse the bit masks of the `faces` array.
  47. :rtype: int
  48. """
  49. try:
  50. faces = self[constants.FACES]
  51. except KeyError:
  52. logger.debug("No parsed faces found")
  53. return 0
  54. length = len(faces)
  55. offset = 0
  56. bitset = lambda x, y: x & (1 << y)
  57. face_count = 0
  58. masks = (constants.MASK[constants.UVS],
  59. constants.MASK[constants.NORMALS],
  60. constants.MASK[constants.COLORS])
  61. while offset < length:
  62. bit = faces[offset]
  63. offset += 1
  64. face_count += 1
  65. is_quad = bitset(bit, constants.MASK[constants.QUAD])
  66. vector = 4 if is_quad else 3
  67. offset += vector
  68. if bitset(bit, constants.MASK[constants.MATERIALS]):
  69. offset += 1
  70. for mask in masks:
  71. if bitset(bit, mask):
  72. offset += vector
  73. return face_count
  74. @property
  75. def metadata(self):
  76. """Metadata for the current node.
  77. :rtype: dict
  78. """
  79. metadata = {
  80. constants.GENERATOR: constants.THREE,
  81. constants.VERSION: FORMAT_VERSION
  82. }
  83. if self[constants.TYPE] == constants.GEOMETRY.title():
  84. self._geometry_metadata(metadata)
  85. else:
  86. self._buffer_geometry_metadata(metadata)
  87. return metadata
  88. def copy(self, scene=True):
  89. """Copy the geometry definitions to a standard dictionary.
  90. :param scene: toggle for scene formatting
  91. (Default value = True)
  92. :type scene: bool
  93. :rtype: dict
  94. """
  95. logger.debug("Geometry().copy(scene=%s)", scene)
  96. dispatch = {
  97. True: self._scene_format,
  98. False: self._geometry_format
  99. }
  100. data = dispatch[scene]()
  101. try:
  102. data[constants.MATERIALS] = self[constants.MATERIALS].copy()
  103. except KeyError:
  104. logger.debug("No materials to copy")
  105. return data
  106. def copy_textures(self, texture_folder=''):
  107. """Copy the textures to the destination directory."""
  108. logger.debug("Geometry().copy_textures()")
  109. if self.options.get(constants.COPY_TEXTURES):
  110. texture_registration = self.register_textures()
  111. if texture_registration:
  112. logger.info("%s has registered textures", self.node)
  113. dirname = os.path.dirname(self.scene.filepath)
  114. full_path = os.path.join(dirname, texture_folder)
  115. io.copy_registered_textures(
  116. full_path, texture_registration)
  117. def parse(self):
  118. """Parse the current node"""
  119. logger.debug("Geometry().parse()")
  120. if self[constants.TYPE] == constants.GEOMETRY.title():
  121. logger.info("Parsing Geometry format")
  122. self._parse_geometry()
  123. else:
  124. logger.info("Parsing BufferGeometry format")
  125. self._parse_buffer_geometry()
  126. def register_textures(self):
  127. """Obtain a texture registration object.
  128. :rtype: dict
  129. """
  130. logger.debug("Geometry().register_textures()")
  131. return api.mesh.texture_registration(self.node)
  132. def write(self, filepath=None):
  133. """Write the geometry definitions to disk. Uses the
  134. desitnation path of the scene.
  135. :param filepath: optional output file path
  136. (Default value = None)
  137. :type filepath: str
  138. """
  139. logger.debug("Geometry().write(filepath=%s)", filepath)
  140. filepath = filepath or self.scene.filepath
  141. io.dump(filepath, self.copy(scene=False),
  142. options=self.scene.options)
  143. if self.options.get(constants.MAPS):
  144. logger.info("Copying textures for %s", self.node)
  145. self.copy_textures()
  146. def write_animation(self, filepath):
  147. """Write the animation definitions to a separate file
  148. on disk. This helps optimize the geometry file size.
  149. :param filepath: destination path
  150. :type filepath: str
  151. """
  152. logger.debug("Geometry().write_animation(%s)", filepath)
  153. for key in (constants.MORPH_TARGETS, constants.ANIMATION):
  154. try:
  155. data = self[key]
  156. break
  157. except KeyError:
  158. pass
  159. else:
  160. logger.info("%s has no animation data", self.node)
  161. return
  162. filepath = os.path.join(filepath, self.animation_filename)
  163. if filepath:
  164. logger.info("Dumping animation data to %s", filepath)
  165. io.dump(filepath, data, options=self.scene.options)
  166. return filepath
  167. else:
  168. logger.warning("Could not determine a filepath for "\
  169. "animation data. Nothing written to disk.")
  170. def _component_data(self):
  171. """Query the component data only
  172. :rtype: dict
  173. """
  174. logger.debug("Geometry()._component_data()")
  175. if self[constants.TYPE] != constants.GEOMETRY.title():
  176. return self[constants.ATTRIBUTES]
  177. components = [constants.VERTICES, constants.FACES,
  178. constants.UVS, constants.COLORS,
  179. constants.NORMALS, constants.BONES,
  180. constants.SKIN_WEIGHTS,
  181. constants.SKIN_INDICES, constants.NAME,
  182. constants.INFLUENCES_PER_VERTEX]
  183. data = {}
  184. anim_components = [constants.MORPH_TARGETS, constants.ANIMATION]
  185. if self.options.get(constants.EMBED_ANIMATION):
  186. components.extend(anim_components)
  187. else:
  188. for component in anim_components:
  189. try:
  190. self[component]
  191. except KeyError:
  192. pass
  193. else:
  194. data[component] = os.path.basename(
  195. self.animation_filename)
  196. break
  197. else:
  198. logger.info("No animation data found for %s", self.node)
  199. for component in components:
  200. try:
  201. data[component] = self[component]
  202. except KeyError:
  203. logger.debug("Component %s not found", component)
  204. return data
  205. def _geometry_format(self):
  206. """Three.Geometry formatted definitions
  207. :rtype: dict
  208. """
  209. data = self._component_data()
  210. if self[constants.TYPE] != constants.GEOMETRY.title():
  211. data = {constants.ATTRIBUTES: data}
  212. data[constants.METADATA] = {
  213. constants.TYPE: self[constants.TYPE]
  214. }
  215. data[constants.METADATA].update(self.metadata)
  216. return data
  217. def _buffer_geometry_metadata(self, metadata):
  218. """Three.BufferGeometry metadata
  219. :rtype: dict
  220. """
  221. for key, value in self[constants.ATTRIBUTES].items():
  222. size = value[constants.ITEM_SIZE]
  223. array = value[constants.ARRAY]
  224. metadata[key] = len(array)/size
  225. def _geometry_metadata(self, metadata):
  226. """Three.Geometry metadat
  227. :rtype: dict
  228. """
  229. skip = (constants.TYPE, constants.FACES, constants.UUID,
  230. constants.ANIMATION, constants.SKIN_INDICES,
  231. constants.SKIN_WEIGHTS, constants.NAME,
  232. constants.INFLUENCES_PER_VERTEX)
  233. vectors = (constants.VERTICES, constants.NORMALS)
  234. for key in self.keys():
  235. if key in vectors:
  236. try:
  237. metadata[key] = int(len(self[key])/3)
  238. except KeyError:
  239. pass
  240. continue
  241. if key in skip:
  242. continue
  243. metadata[key] = len(self[key])
  244. faces = self.face_count
  245. if faces > 0:
  246. metadata[constants.FACES] = faces
  247. def _scene_format(self):
  248. """Format the output for Scene compatability
  249. :rtype: dict
  250. """
  251. data = {
  252. constants.UUID: self[constants.UUID],
  253. constants.TYPE: self[constants.TYPE]
  254. }
  255. component_data = self._component_data()
  256. if self[constants.TYPE] == constants.GEOMETRY.title():
  257. data[constants.DATA] = component_data
  258. data[constants.DATA].update({
  259. constants.METADATA: self.metadata
  260. })
  261. else:
  262. if self.options.get(constants.EMBED_GEOMETRY, True):
  263. data[constants.DATA] = {
  264. constants.ATTRIBUTES: component_data
  265. }
  266. else:
  267. data[constants.ATTRIBUTES] = component_data
  268. data[constants.METADATA] = self.metadata
  269. data[constants.NAME] = self[constants.NAME]
  270. return data
  271. def _parse_buffer_geometry(self):
  272. """Parse the geometry to Three.BufferGeometry specs"""
  273. self[constants.ATTRIBUTES] = {}
  274. options_vertices = self.options.get(constants.VERTICES)
  275. option_normals = self.options.get(constants.NORMALS)
  276. option_uvs = self.options.get(constants.UVS)
  277. pos_tuple = (constants.POSITION, options_vertices,
  278. api.mesh.buffer_position, 3)
  279. uvs_tuple = (constants.UV, option_uvs,
  280. api.mesh.buffer_uv, 2)
  281. normals_tuple = (constants.NORMAL, option_normals,
  282. api.mesh.buffer_normal, 3)
  283. dispatch = (pos_tuple, uvs_tuple, normals_tuple)
  284. for key, option, func, size in dispatch:
  285. if not option:
  286. continue
  287. array = func(self.node, self.options) or []
  288. if not array:
  289. logger.warning("No array could be made for %s", key)
  290. continue
  291. self[constants.ATTRIBUTES][key] = {
  292. constants.ITEM_SIZE: size,
  293. constants.TYPE: constants.FLOAT_32,
  294. constants.ARRAY: array
  295. }
  296. def _parse_geometry(self):
  297. """Parse the geometry to Three.Geometry specs"""
  298. if self.options.get(constants.VERTICES):
  299. logger.info("Parsing %s", constants.VERTICES)
  300. self[constants.VERTICES] = api.mesh.vertices(
  301. self.node, self.options) or []
  302. if self.options.get(constants.NORMALS):
  303. logger.info("Parsing %s", constants.NORMALS)
  304. self[constants.NORMALS] = api.mesh.normals(
  305. self.node, self.options) or []
  306. if self.options.get(constants.COLORS):
  307. logger.info("Parsing %s", constants.COLORS)
  308. self[constants.COLORS] = api.mesh.vertex_colors(
  309. self.node) or []
  310. if self.options.get(constants.FACE_MATERIALS):
  311. logger.info("Parsing %s", constants.FACE_MATERIALS)
  312. self[constants.MATERIALS] = api.mesh.materials(
  313. self.node, self.options) or []
  314. if self.options.get(constants.UVS):
  315. logger.info("Parsing %s", constants.UVS)
  316. self[constants.UVS] = api.mesh.uvs(
  317. self.node, self.options) or []
  318. if self.options.get(constants.FACES):
  319. logger.info("Parsing %s", constants.FACES)
  320. self[constants.FACES] = api.mesh.faces(
  321. self.node, self.options) or []
  322. no_anim = (None, False, constants.OFF)
  323. if self.options.get(constants.ANIMATION) not in no_anim:
  324. logger.info("Parsing %s", constants.ANIMATION)
  325. self[constants.ANIMATION] = api.mesh.skeletal_animation(
  326. self.node, self.options) or []
  327. #@TODO: considering making bones data implied when
  328. # querying skinning data
  329. bone_map = {}
  330. if self.options.get(constants.BONES):
  331. logger.info("Parsing %s", constants.BONES)
  332. bones, bone_map = api.mesh.bones(self.node, self.options)
  333. self[constants.BONES] = bones
  334. if self.options.get(constants.SKINNING):
  335. logger.info("Parsing %s", constants.SKINNING)
  336. influences = self.options.get(
  337. constants.INFLUENCES_PER_VERTEX, 2)
  338. self[constants.INFLUENCES_PER_VERTEX] = influences
  339. self[constants.SKIN_INDICES] = api.mesh.skin_indices(
  340. self.node, bone_map, influences) or []
  341. self[constants.SKIN_WEIGHTS] = api.mesh.skin_weights(
  342. self.node, bone_map, influences) or []
  343. if self.options.get(constants.MORPH_TARGETS):
  344. logger.info("Parsing %s", constants.MORPH_TARGETS)
  345. self[constants.MORPH_TARGETS] = api.mesh.morph_targets(
  346. self.node, self.options) or []