m2b.py 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. import sys
  2. import os
  3. import pymel.core as pm
  4. import maya.cmds as cmds
  5. import tempfile
  6. import json
  7. import subprocess
  8. # // SETTINGS
  9. GEO_EXT = '.obj'
  10. APP_EXT = '.blend'
  11. PORT = 6006
  12. EDITOR = '/Applications/blender.app/Contents/MacOS/blender'
  13. # shader-independent way of figuring out texture file from mat
  14. # /// UTILS
  15. def guess_diffuse_file(node):
  16. mat = [pm.ls(pm.listConnections(se), materials=True)[0] for se in node.getShape().outputs(type='shadingEngine')]
  17. textures = [c for c in mat[0].listConnections() if c.type() =='file']
  18. for t in textures:
  19. plugs = t.listConnections(plugs=True)
  20. if any ([p.name() for p in plugs if 'color' in p.name() or 'diff' in p.name()]):
  21. return t.fileTextureName.get()
  22. return 'untextured'
  23. def get_blender_path(root='/Applications'):
  24. bin_location = 'Contents/MacOS/blender'
  25. for directory in os.listdir(root):
  26. full_path = os.path.join(root, directory)
  27. if os.path.isdir(full_path):
  28. if directory.endswith('blender.app'):
  29. return os.path.join(root, directory, bin_location)
  30. else:
  31. for subdir in os.listdir(full_path):
  32. if subdir.endswith('blender.app'):
  33. return os.path.join(full_path, subdir, bin_location)
  34. def this_script():
  35. # does not work - can we get the current script's location?
  36. return os.path.join(sys.argv[0])
  37. def replace_mesh(old, new):
  38. shading_engines = old.getShape().outputs(type='shadingEngine')[0]
  39. old_mesh = old.listRelatives(shapes=True)[0]
  40. old_mesh_name = old_mesh.name()
  41. pm.delete(old_mesh)
  42. print new
  43. new_mesh = new.listRelatives(shapes=True)[0]
  44. pm.rename(new_mesh, old_mesh_name)
  45. pm.parent(new_mesh, old, shape=True, relative=True)
  46. pm.delete(new)
  47. pm.sets(shading_engines, edit=True, forceElement=old)
  48. def load_geofile(filepath):
  49. try:
  50. imported_nodes = pm.importFile(filepath, i=True, returnNewNodes=True)
  51. # print imported_nodes
  52. relevant_nodes = []
  53. trash = []
  54. for node in imported_nodes:
  55. print node, 'TYPE', node.type()
  56. if node.type() == 'mesh' or node.type() == 'transform':
  57. relevant_nodes.append(node)
  58. print '> append', node
  59. else:
  60. trash.append(str(node.name()))
  61. # seems like a bug in pymel, materials et al must be
  62. # explicitly deleted by string name with cmds
  63. for item in trash:
  64. try:
  65. cmds.delete(item)
  66. except ValueError:
  67. pass
  68. for node in relevant_nodes:
  69. if node.getParent() is None:
  70. return node
  71. except RuntimeError:
  72. print "> unreadable:", filepath
  73. return False
  74. def open_port(number):
  75. if not cmds.commandPort(':'+str(number), q=True):
  76. cmds.commandPort('mayaCommand', name=':'+str(number), sourceType='python')
  77. def get_temp_dir():
  78. prefix = 'm2b'
  79. base_tmp = tempfile.gettempdir()
  80. m2b_tmp = os.path.join(base_tmp, prefix)
  81. if not os.path.isdir(m2b_tmp):
  82. os.makedirs(m2b_tmp)
  83. return m2b_tmp
  84. # /// CLASSES
  85. class AtOrigin(object):
  86. def __init__(self, node):
  87. self.node = node
  88. self.matrix = pm.xform(matrix=True, query=True)
  89. def __enter__(self):
  90. print '> discard trs'
  91. mat_neutral = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]
  92. pm.xform(self.node, matrix=mat_neutral)
  93. def __exit__(self, type, value, traceback):
  94. print '> restore trs'
  95. pm.xform(self.node, matrix=self.matrix)
  96. class KeepSelection(object):
  97. def __enter__(self):
  98. print '> store sel'
  99. self.selection = pm.selected()
  100. def __exit__(self, type, value, traceback):
  101. print '> restore sel'
  102. pm.select(self.selection)
  103. class BridgedNode(object):
  104. def __init__(self, uuid):
  105. self.uuid = uuid
  106. self.node = None
  107. self.texture = None
  108. self.geofile = None
  109. self.metadatafile = None
  110. self.matrix = None
  111. self.valid = False
  112. self.blendfile = False
  113. self.updatescript = None
  114. self.ingest()
  115. def is_valid(self):
  116. print '> validating:'
  117. #print [ (key, self.__dict__[key]) for key in self.__dict__.keys()]
  118. if not any([self.__dict__[key] is None for key in self.__dict__.keys()]):
  119. print '> valid'
  120. return True
  121. else:
  122. print '> invalid'
  123. print [self.__dict__[key] for key in self.__dict__.keys()]
  124. def has_history(self):
  125. if os.path.isfile(self.geofile) and os.path.isfile(self.metadatafile) and os.path.isfile(self.blendfile):
  126. return True
  127. def ingest(self):
  128. # get scene object
  129. for node in pm.ls(transforms=True):
  130. if cmds.ls(node.name(), uuid=True)[0] == self.uuid:
  131. self.node = node
  132. self.geofile = os.path.join(get_temp_dir(), self.uuid + GEO_EXT)
  133. self.blendfile = os.path.join(get_temp_dir(), self.uuid + APP_EXT)
  134. self.metadatafile = os.path.join(get_temp_dir(), self.uuid + '.json')
  135. self.matrix = pm.xform(matrix=True, query=True)
  136. self.texture = guess_diffuse_file(self.node)
  137. self.updatescript = this_script()
  138. def dump_metadata(self):
  139. metadata = {'obj': self.geofile,
  140. 'tex': self.texture,
  141. 'blend': self.blendfile,
  142. 'uuid': self.uuid,
  143. 'updatescript': self.updatescript,
  144. 'port': PORT
  145. }
  146. print '> metadata: ', metadata
  147. with open(self.metadatafile, 'w') as f:
  148. json.dump(metadata, f, indent=4)
  149. def dump_geo(self):
  150. print '> dumping mesh'
  151. # save position
  152. with AtOrigin(self.node):
  153. cmds.file(self.geofile,
  154. force=True,
  155. pr=1,
  156. typ="OBJexport",
  157. exportSelected=True,
  158. op="groups=0;materials=0;smoothing=0;normals=1")
  159. def edit_externally(self, path_to_app):
  160. blender_bridge_script = os.path.join(os.path.expanduser('~'), 'Documents', 'lab', 'm2b', 'b2m.py')
  161. blendfile = self.blendfile
  162. cmd = [path_to_app, '--python', blender_bridge_script, blendfile]
  163. print 'CMD', cmd
  164. subprocess.Popen(cmd)
  165. # /// CONTROLS
  166. def init():
  167. print 'init'
  168. selection = pm.selected()
  169. if len(selection) != 1:
  170. print 'Must select one object'
  171. else:
  172. selection = selection[0]
  173. uuid = cmds.ls(selection.name(), uuid=True)[0]
  174. bn = BridgedNode(uuid)
  175. bn.ingest()
  176. if bn.is_valid():
  177. if bn.has_history():
  178. print '> file has history. opening source.'
  179. else:
  180. bn.dump_geo()
  181. bn.dump_metadata()
  182. else:
  183. bn.dump_geo()
  184. bn.dump_metadata()
  185. open_port(6006)
  186. bn.edit_externally(get_blender_path())
  187. def update(uuid):
  188. print '> running on', uuid
  189. bn = BridgedNode(uuid)
  190. bn.ingest()
  191. if bn.is_valid():
  192. with KeepSelection():
  193. print 'GEO', bn.geofile
  194. imported_node = load_geofile(bn.geofile)
  195. replace_mesh(bn.node, imported_node)
  196. else:
  197. print '> UUID invalid.'