geometry.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. """
  2. Module for creating Three.js geometry JSON nodes.
  3. """
  4. import os
  5. from .. import constants, logger
  6. from . import base_classes, io, api
  7. FORMAT_VERSION = 3
  8. class Geometry(base_classes.BaseNode):
  9. """Class that wraps a single mesh/geometry node."""
  10. def __init__(self, node, parent=None):
  11. logger.debug("Geometry().__init__(%s)", node)
  12. # @TODO: maybe better to have `three` constants for
  13. # strings that are specific to `three` properties
  14. geo_type = constants.GEOMETRY.title()
  15. if parent.options.get(constants.GEOMETRY_TYPE):
  16. opt_type = parent.options[constants.GEOMETRY_TYPE]
  17. if opt_type == constants.BUFFER_GEOMETRY:
  18. geo_type = constants.BUFFER_GEOMETRY
  19. elif opt_type != constants.GEOMETRY:
  20. logger.error("Unknown geometry type %s", opt_type)
  21. logger.info("Setting %s to '%s'", node, geo_type)
  22. self._defaults[constants.TYPE] = geo_type
  23. base_classes.BaseNode.__init__(self, node,
  24. parent=parent,
  25. type=geo_type)
  26. @property
  27. def animation_filename(self):
  28. """Calculate the file name for the animation file
  29. :return: base name for the file
  30. """
  31. compression = self.options.get(constants.COMPRESSION)
  32. if compression in (None, constants.NONE):
  33. ext = constants.JSON
  34. elif compression == constants.MSGPACK:
  35. ext = constants.PACK
  36. key = ''
  37. for key in (constants.MORPH_TARGETS, constants.ANIMATION, constants.CLIPS):
  38. if key in self.keys():
  39. break
  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. def bitset(bit, mask):
  57. """
  58. :type bit: int
  59. :type mask: int
  60. """
  61. return bit & (1 << mask)
  62. face_count = 0
  63. masks = (constants.MASK[constants.UVS],
  64. constants.MASK[constants.NORMALS],
  65. constants.MASK[constants.COLORS])
  66. while offset < length:
  67. bit = faces[offset]
  68. offset += 1
  69. face_count += 1
  70. is_quad = bitset(bit, constants.MASK[constants.QUAD])
  71. vector = 4 if is_quad else 3
  72. offset += vector
  73. if bitset(bit, constants.MASK[constants.MATERIALS]):
  74. offset += 1
  75. for mask in masks:
  76. if bitset(bit, mask):
  77. offset += vector
  78. return face_count
  79. @property
  80. def metadata(self):
  81. """Metadata for the current node.
  82. :rtype: dict
  83. """
  84. metadata = {
  85. constants.GENERATOR: constants.THREE,
  86. constants.VERSION: FORMAT_VERSION
  87. }
  88. if self[constants.TYPE] == constants.GEOMETRY.title():
  89. self._geometry_metadata(metadata)
  90. else:
  91. self._buffer_geometry_metadata(metadata)
  92. return metadata
  93. def copy(self, scene=True):
  94. """Copy the geometry definitions to a standard dictionary.
  95. :param scene: toggle for scene formatting
  96. (Default value = True)
  97. :type scene: bool
  98. :rtype: dict
  99. """
  100. logger.debug("Geometry().copy(scene=%s)", scene)
  101. dispatch = {
  102. True: self._scene_format,
  103. False: self._geometry_format
  104. }
  105. data = dispatch[scene]()
  106. try:
  107. data[constants.MATERIALS] = self[constants.MATERIALS].copy()
  108. except KeyError:
  109. logger.debug("No materials to copy")
  110. return data
  111. def copy_textures(self, texture_folder=''):
  112. """Copy the textures to the destination directory."""
  113. logger.debug("Geometry().copy_textures()")
  114. if self.options.get(constants.EXPORT_TEXTURES) and not self.options.get(constants.EMBED_TEXTURES):
  115. texture_registration = self.register_textures()
  116. if texture_registration:
  117. logger.info("%s has registered textures", self.node)
  118. dirname = os.path.dirname(os.path.abspath(self.scene.filepath))
  119. full_path = os.path.join(dirname, texture_folder)
  120. io.copy_registered_textures(
  121. full_path, texture_registration)
  122. def parse(self):
  123. """Parse the current node"""
  124. logger.debug("Geometry().parse()")
  125. if self[constants.TYPE] == constants.GEOMETRY.title():
  126. logger.info("Parsing Geometry format")
  127. self._parse_geometry()
  128. else:
  129. logger.info("Parsing BufferGeometry format")
  130. self._parse_buffer_geometry()
  131. def register_textures(self):
  132. """Obtain a texture registration object.
  133. :rtype: dict
  134. """
  135. logger.debug("Geometry().register_textures()")
  136. return api.mesh.texture_registration(self.node)
  137. def write(self, filepath=None):
  138. """Write the geometry definitions to disk. Uses the
  139. destination path of the scene.
  140. :param filepath: optional output file path
  141. (Default value = None)
  142. :type filepath: str
  143. """
  144. logger.debug("Geometry().write(filepath=%s)", filepath)
  145. filepath = filepath or self.scene.filepath
  146. io.dump(filepath, self.copy(scene=False),
  147. options=self.scene.options)
  148. if self.options.get(constants.MAPS):
  149. logger.info("Copying textures for %s", self.node)
  150. self.copy_textures()
  151. def write_animation(self, filepath):
  152. """Write the animation definitions to a separate file
  153. on disk. This helps optimize the geometry file size.
  154. :param filepath: destination path
  155. :type filepath: str
  156. """
  157. logger.debug("Geometry().write_animation(%s)", filepath)
  158. for key in (constants.MORPH_TARGETS, constants.ANIMATION, constants.CLIPS):
  159. try:
  160. data = self[key]
  161. break
  162. except KeyError:
  163. pass
  164. else:
  165. logger.info("%s has no animation data", self.node)
  166. return
  167. filepath = os.path.join(filepath, self.animation_filename)
  168. if filepath:
  169. logger.info("Dumping animation data to %s", filepath)
  170. io.dump(filepath, data, options=self.scene.options)
  171. return filepath
  172. else:
  173. logger.warning("Could not determine a filepath for "
  174. "animation data. Nothing written to disk.")
  175. def _component_data(self):
  176. """Query the component data only
  177. :rtype: dict
  178. """
  179. logger.debug("Geometry()._component_data()")
  180. if self[constants.TYPE] != constants.GEOMETRY.title():
  181. data = {}
  182. index = self.get(constants.INDEX)
  183. if index is not None:
  184. data[constants.INDEX] = index
  185. data[constants.ATTRIBUTES] = self.get(constants.ATTRIBUTES)
  186. data[constants.GROUPS] = self.get(constants.GROUPS)
  187. return {constants.DATA: data}
  188. components = [constants.VERTICES, constants.FACES,
  189. constants.UVS, constants.COLORS,
  190. constants.NORMALS, constants.BONES,
  191. constants.SKIN_WEIGHTS,
  192. constants.SKIN_INDICES,
  193. constants.INFLUENCES_PER_VERTEX,
  194. constants.INDEX]
  195. data = {}
  196. anim_components = [constants.MORPH_TARGETS, constants.ANIMATION, constants.MORPH_TARGETS_ANIM, constants.CLIPS]
  197. if self.options.get(constants.EMBED_ANIMATION):
  198. components.extend(anim_components)
  199. else:
  200. for component in anim_components:
  201. try:
  202. self[component]
  203. except KeyError:
  204. pass
  205. else:
  206. data[component] = os.path.basename(
  207. self.animation_filename)
  208. break
  209. else:
  210. logger.info("No animation data found for %s", self.node)
  211. option_extra_vgroups = self.options.get(constants.EXTRA_VGROUPS)
  212. for name, index in api.mesh.extra_vertex_groups(self.node,
  213. option_extra_vgroups):
  214. components.append(name)
  215. for component in components:
  216. try:
  217. data[component] = self[component]
  218. except KeyError:
  219. logger.debug("Component %s not found", component)
  220. return data
  221. def _geometry_format(self):
  222. """Three.Geometry formatted definitions
  223. :rtype: dict
  224. """
  225. data = {
  226. constants.METADATA: {
  227. constants.TYPE: self[constants.TYPE]
  228. }
  229. }
  230. data[constants.METADATA].update(self.metadata)
  231. data.update(self._component_data())
  232. draw_calls = self.get(constants.DRAW_CALLS)
  233. if draw_calls is not None:
  234. data[constants.DRAW_CALLS] = draw_calls
  235. return data
  236. def _buffer_geometry_metadata(self, metadata):
  237. """Three.BufferGeometry metadata
  238. :rtype: dict
  239. """
  240. for key, value in self[constants.ATTRIBUTES].items():
  241. size = value[constants.ITEM_SIZE]
  242. array = value[constants.ARRAY]
  243. metadata[key] = len(array)/size
  244. def _geometry_metadata(self, metadata):
  245. """Three.Geometry metadata
  246. :rtype: dict
  247. """
  248. skip = (constants.TYPE, constants.FACES, constants.UUID,
  249. constants.ANIMATION, constants.SKIN_INDICES,
  250. constants.SKIN_WEIGHTS, constants.NAME,
  251. constants.INFLUENCES_PER_VERTEX)
  252. vectors = (constants.VERTICES, constants.NORMALS)
  253. for key in self.keys():
  254. if key in vectors:
  255. try:
  256. metadata[key] = int(len(self[key])/3)
  257. except KeyError:
  258. pass
  259. continue
  260. if key in skip:
  261. continue
  262. metadata[key] = len(self[key])
  263. faces = self.face_count
  264. if faces > 0:
  265. metadata[constants.FACES] = faces
  266. def _scene_format(self):
  267. """Format the output for Scene compatibility
  268. :rtype: dict
  269. """
  270. data = {
  271. constants.NAME: self[constants.NAME],
  272. constants.UUID: self[constants.UUID],
  273. constants.TYPE: self[constants.TYPE]
  274. }
  275. if self[constants.TYPE] == constants.GEOMETRY.title():
  276. data[constants.DATA] = self._component_data()
  277. else:
  278. data.update(self._component_data())
  279. draw_calls = self.get(constants.DRAW_CALLS)
  280. if draw_calls is not None:
  281. data[constants.DRAW_CALLS] = draw_calls
  282. return data
  283. def _parse_buffer_geometry(self):
  284. """Parse the geometry to Three.BufferGeometry specs"""
  285. self[constants.ATTRIBUTES] = {}
  286. options_vertices = self.options.get(constants.VERTICES)
  287. option_normals = self.options.get(constants.NORMALS)
  288. option_uvs = self.options.get(constants.UVS)
  289. option_colors = self.options.get(constants.COLORS)
  290. option_extra_vgroups = self.options.get(constants.EXTRA_VGROUPS)
  291. option_index_type = self.options.get(constants.INDEX_TYPE)
  292. pos_tuple = (constants.POSITION, options_vertices,
  293. lambda m: api.mesh.buffer_position(m, self.options), 3)
  294. uvs_tuple = (constants.UV, option_uvs,
  295. api.mesh.buffer_uv, 2)
  296. uvs2_tuple = (constants.UV2, option_uvs,
  297. lambda m: api.mesh.buffer_uv(m, layer=1), 2)
  298. normals_tuple = (constants.NORMAL, option_normals,
  299. lambda m: api.mesh.buffer_normal(m, self.options), 3)
  300. colors_tuple = (constants.COLOR, option_colors,
  301. lambda m: api.mesh.buffer_color(m, self.options), 3)
  302. dispatch = (pos_tuple, uvs_tuple, uvs2_tuple, normals_tuple, colors_tuple)
  303. for key, option, func, size in dispatch:
  304. if not option:
  305. continue
  306. array = func(self.node) or []
  307. if not array:
  308. logger.warning("No array could be made for %s", key)
  309. continue
  310. self[constants.ATTRIBUTES][key] = {
  311. constants.ITEM_SIZE: size,
  312. constants.TYPE: constants.FLOAT_32,
  313. constants.ARRAY: array
  314. }
  315. for name, index in api.mesh.extra_vertex_groups(self.node,
  316. option_extra_vgroups):
  317. logger.info("Exporting extra vertex group %s", name)
  318. array = api.mesh.buffer_vertex_group_data(self.node, index)
  319. if not array:
  320. logger.warning("No array could be made for %s", name)
  321. continue
  322. self[constants.ATTRIBUTES][name] = {
  323. constants.ITEM_SIZE: 1,
  324. constants.TYPE: constants.FLOAT_32,
  325. constants.ARRAY: array
  326. }
  327. if option_index_type != constants.NONE:
  328. assert(not (self.get(constants.INDEX) or
  329. self.get(constants.DRAW_CALLS)))
  330. indices_per_face = 3
  331. index_threshold = 0xffff - indices_per_face
  332. if option_index_type == constants.UINT_32:
  333. index_threshold = 0x7fffffff - indices_per_face
  334. attrib_data_in, attrib_data_out, attrib_keys = [], [], []
  335. i = 0
  336. for key, entry in self[constants.ATTRIBUTES].items():
  337. item_size = entry[constants.ITEM_SIZE]
  338. attrib_keys.append(key)
  339. attrib_data_in.append((entry[constants.ARRAY], item_size))
  340. attrib_data_out.append(([], i, i + item_size))
  341. i += item_size
  342. index_data, draw_calls = [], []
  343. indexed, flush_req, base_vertex = {}, False, 0
  344. assert(len(attrib_data_in) > 0)
  345. array, item_size = attrib_data_in[0]
  346. i, n = 0, len(array) / item_size
  347. while i < n:
  348. vertex_data = ()
  349. for array, item_size in attrib_data_in:
  350. vertex_data += tuple(
  351. array[i * item_size:(i + 1) * item_size])
  352. vertex_index = indexed.get(vertex_data)
  353. if vertex_index is None:
  354. vertex_index = len(indexed)
  355. flush_req = vertex_index >= index_threshold
  356. indexed[vertex_data] = vertex_index
  357. for array, i_from, i_to in attrib_data_out:
  358. array.extend(vertex_data[i_from:i_to])
  359. index_data.append(vertex_index)
  360. i += 1
  361. if i == n:
  362. flush_req = len(draw_calls) > 0
  363. assert(i % indices_per_face == 0)
  364. if flush_req and i % indices_per_face == 0:
  365. start, count = 0, len(index_data)
  366. if draw_calls:
  367. prev = draw_calls[-1]
  368. start = (prev[constants.DC_START] +
  369. prev[constants.DC_COUNT])
  370. count -= start
  371. draw_calls.append({
  372. constants.DC_START: start,
  373. constants.DC_COUNT: count,
  374. constants.DC_INDEX: base_vertex
  375. })
  376. base_vertex += len(indexed)
  377. indexed.clear()
  378. flush_req = False
  379. #manthrax: Adding group support for multiple materials
  380. #index_threshold = indices_per_face*100
  381. face_materials = api.mesh.buffer_face_material(self.node,self.options)
  382. logger.info("Face material list length:%d",len(face_materials))
  383. logger.info("Drawcall parameters count:%s item_size=%s",n,item_size)
  384. assert((len(face_materials)*3)==n)
  385. #Re-index the index buffer by material
  386. used_material_indexes = {}
  387. #Get lists of faces indices per material
  388. for idx, mat_index in enumerate(face_materials):
  389. if used_material_indexes.get(mat_index) is None:
  390. used_material_indexes[mat_index] = [idx]
  391. else:
  392. used_material_indexes[mat_index].append(idx)
  393. logger.info("# Faces by material:%s",str(used_material_indexes))
  394. #manthrax: build new index list from lists of faces by material, and build the draw groups at the same time...
  395. groups = []
  396. new_index = []
  397. print("Mat index:",str(used_material_indexes))
  398. for mat_index in used_material_indexes:
  399. face_array=used_material_indexes[mat_index]
  400. print("Mat index:",str(mat_index),str(face_array))
  401. print( dir(self.node) )
  402. group = {
  403. 'start': len(new_index),
  404. 'count': len(face_array)*3,
  405. 'materialIndex': mat_index
  406. }
  407. groups.append(group)
  408. for fi in range(len(face_array)):
  409. prim_index = face_array[fi]
  410. prim_index = prim_index * 3
  411. new_index.extend([index_data[prim_index],index_data[prim_index+1],index_data[prim_index+2]])
  412. if len(groups) > 0:
  413. index_data = new_index
  414. self[constants.GROUPS]=groups
  415. #else:
  416. # self[constants.GROUPS]=[{
  417. # 'start':0,
  418. # 'count':n,
  419. # 'materialIndex':0
  420. #}]
  421. #manthrax: End group support
  422. for i, key in enumerate(attrib_keys):
  423. array = attrib_data_out[i][0]
  424. self[constants.ATTRIBUTES][key][constants.ARRAY] = array
  425. self[constants.INDEX] = {
  426. constants.ITEM_SIZE: 1,
  427. constants.TYPE: option_index_type,
  428. constants.ARRAY: index_data
  429. }
  430. if (draw_calls):
  431. logger.info("draw_calls = %s", repr(draw_calls))
  432. self[constants.DRAW_CALLS] = draw_calls
  433. def _parse_geometry(self):
  434. """Parse the geometry to Three.Geometry specs"""
  435. if self.options.get(constants.VERTICES):
  436. logger.info("Parsing %s", constants.VERTICES)
  437. self[constants.VERTICES] = api.mesh.vertices(self.node, self.options) or []
  438. if self.options.get(constants.NORMALS):
  439. logger.info("Parsing %s", constants.NORMALS)
  440. self[constants.NORMALS] = api.mesh.normals(self.node, self.options) or []
  441. if self.options.get(constants.COLORS):
  442. logger.info("Parsing %s", constants.COLORS)
  443. self[constants.COLORS] = api.mesh.vertex_colors(
  444. self.node) or []
  445. if self.options.get(constants.FACE_MATERIALS):
  446. logger.info("Parsing %s", constants.FACE_MATERIALS)
  447. self[constants.MATERIALS] = api.mesh.materials(
  448. self.node, self.options) or []
  449. if self.options.get(constants.UVS):
  450. logger.info("Parsing %s", constants.UVS)
  451. self[constants.UVS] = api.mesh.uvs(self.node) or []
  452. if self.options.get(constants.FACES):
  453. logger.info("Parsing %s", constants.FACES)
  454. material_list = self.get(constants.MATERIALS)
  455. self[constants.FACES] = api.mesh.faces(
  456. self.node, self.options, material_list=material_list) or []
  457. no_anim = (None, False, constants.OFF)
  458. if self.options.get(constants.ANIMATION) not in no_anim:
  459. logger.info("Parsing %s", constants.ANIMATION)
  460. self[constants.ANIMATION] = api.mesh.skeletal_animation(
  461. self.node, self.options) or []
  462. # @TODO: considering making bones data implied when
  463. # querying skinning data
  464. bone_map = {}
  465. if self.options.get(constants.BONES):
  466. logger.info("Parsing %s", constants.BONES)
  467. bones, bone_map = api.mesh.bones(self.node, self.options)
  468. self[constants.BONES] = bones
  469. if self.options.get(constants.SKINNING):
  470. logger.info("Parsing %s", constants.SKINNING)
  471. influences = self.options.get(
  472. constants.INFLUENCES_PER_VERTEX, 2)
  473. anim_type = self.options.get(constants.ANIMATION)
  474. self[constants.INFLUENCES_PER_VERTEX] = influences
  475. self[constants.SKIN_INDICES] = api.mesh.skin_indices(
  476. self.node, bone_map, influences, anim_type) or []
  477. self[constants.SKIN_WEIGHTS] = api.mesh.skin_weights(
  478. self.node, bone_map, influences, anim_type) or []
  479. if self.options.get(constants.BLEND_SHAPES):
  480. logger.info("Parsing %s", constants.BLEND_SHAPES)
  481. mt = api.mesh.blend_shapes(self.node, self.options) or []
  482. self[constants.MORPH_TARGETS] = mt
  483. if len(mt) > 0 and self._scene: # there's blend shapes, let check for animation
  484. tracks = api.mesh.animated_blend_shapes(self.node, self[constants.NAME], self.options) or []
  485. merge = self._scene[constants.ANIMATION][0][constants.KEYFRAMES]
  486. for track in tracks:
  487. merge.append(track)
  488. elif self.options.get(constants.MORPH_TARGETS):
  489. logger.info("Parsing %s", constants.MORPH_TARGETS)
  490. self[constants.MORPH_TARGETS] = api.mesh.morph_targets(
  491. self.node, self.options) or []
  492. # In the moment there is no way to add extra data to a Geomtry in
  493. # Three.js. In case there is some day, here is the code:
  494. #
  495. # option_extra_vgroups = self.options.get(constants.EXTRA_VGROUPS)
  496. #
  497. # for name, index in api.mesh.extra_vertex_groups(self.node,
  498. # option_extra_vgroups):
  499. #
  500. # logger.info("Exporting extra vertex group %s", name)
  501. # self[name] = api.mesh.vertex_group_data(self.node, index)